Compare commits

...

81 Commits
v11 ... v12

Author SHA1 Message Date
Kevin Jahns
03685ac84a Release 12.3.3 2017-08-04 20:36:43 +02:00
Kevin Jahns
6fa79320a5 disable preferUntransformed 2017-08-04 20:36:07 +02:00
Kevin Jahns
4e3a297efc created doc for the auth method 2017-07-19 19:17:36 +02:00
Kevin Jahns
ef2cbf210b Release 12.3.2 2017-07-19 18:50:53 +02:00
Kevin Jahns
3a1c7a71d4 Provide sender information in checkAuth. Related to #79 2017-07-19 18:50:18 +02:00
Kevin Jahns
dd6c196135 link to the IPFS connector 2017-07-13 19:51:29 +02:00
Kevin Jahns
a5506a5ded Release 12.3.1 2017-06-17 14:32:28 +02:00
Kevin Jahns
361d4a48e1 implement getPath for any type. types now initialize _parent. fix some cases where observeDeep is not fired 2017-06-17 14:31:00 +02:00
Kevin Jahns
edf47d3491 add disconnect / reconnect return values 2017-05-12 04:19:14 +02:00
Kevin Jahns
14ee42cad5 Release 12.3.0 2017-05-08 12:40:31 +02:00
Kevin Jahns
f990927d3e implemented Y.utils.bubbleEvent 2017-05-08 12:01:51 +02:00
Kevin Jahns
a1cef4662f Release 12.2.1 2017-05-03 18:00:59 +02:00
Kevin Jahns
2c343970c4 fixed sync issue with "preferUntransformed" 2017-05-03 18:00:20 +02:00
Kevin Jahns
74b41e03e3 Release 12.2.0 2017-05-03 16:17:13 +02:00
Kevin Jahns
b242aab955 implemented "preferUntransformed" 2017-05-03 16:13:52 +02:00
Kevin Jahns
8e4efd9bba fixed "waiting for auth" issue in the test suite 2017-04-28 11:21:26 +02:00
Kevin Jahns
47d5899058 fixed test suite. It stopped working because of previous adoptions on the gc behavior. I failed to notice it.. 2017-04-27 14:58:08 +02:00
Kevin Jahns
a126a29876 Added Monaco editor example 2017-04-18 18:24:03 +02:00
Kevin Jahns
4aa720116f add serviceworker example 2017-04-11 16:21:00 +02:00
Kevin Jahns
e29162c3fc Release 12.1.7 2017-04-10 11:22:34 +02:00
Kevin Jahns
aa40855953 Add debug package as dependency. Fixes #66 2017-04-10 10:30:28 +02:00
Kevin Jahns
b6545d62fc update CodeMirror example 2017-03-29 13:43:26 +02:00
Kevin Jahns
3425d95507 update CodeMirror example 2017-03-29 13:23:28 +02:00
Kevin Jahns
53682c17fb added CodeMirror example 2017-03-29 13:21:30 +02:00
Kevin Jahns
a492a83f0c Release 12.1.6 2017-03-20 19:16:06 +01:00
Kevin Jahns
d340e557c1 removed the Changelog from README.md. Instead use the github releases to describe the changes 2017-03-20 19:13:08 +01:00
Kevin Jahns
d5cd9d94d5 implemented logging with the debug logging utility (read the updated docs) 2017-03-20 19:11:59 +01:00
Kevin Jahns
e1a160b894 Release 12.1.5 2017-03-06 13:27:32 +01:00
Kevin Jahns
f996ac83d2 added option *generateUserId* in abstract connector 2017-03-06 13:27:04 +01:00
Kevin Jahns
922637930f Release 12.1.4 2016-12-19 10:42:05 +01:00
Kevin Jahns
ff7e9cdef2 var/let declaration - fixes #61 2016-12-19 10:41:15 +01:00
Kevin Jahns
f02641deb7 Added Yjs Polymer element to module section 2016-12-01 11:15:39 +01:00
Kevin Jahns
f97144356c Release 12.1.3 2016-11-22 13:12:36 +01:00
Kevin Jahns
a9fdd5df66 updated webworker example 2016-11-21 16:29:03 +01:00
Kevin Jahns
e90f241ae0 implemented resetAuth 2016-11-21 01:22:15 +01:00
Kevin Jahns
102bef4f92 added sw example 2016-11-14 16:29:58 +01:00
Kevin Jahns
96e9c3c166 enable empty share property 2016-11-14 16:27:08 +01:00
Kevin Jahns
1080f83990 implemented moduleName, made window statements failsafe 2016-11-14 13:01:41 +01:00
Kevin Jahns
66b6b2a568 added banner to distribution files. Fixes #56 2016-11-14 10:30:10 +01:00
Kevin Jahns
7415f27fbc Release 12.1.2 2016-11-10 17:01:58 +01:00
Kevin Jahns
c9d1f34864 reconnect only when disconnected (and reverse) 2016-11-10 12:54:01 +01:00
Kevin Jahns
34997f940b Release 12.1.1 2016-11-09 14:26:37 +01:00
Kevin Jahns
4e9e21e75e lint 2016-11-09 14:19:13 +01:00
Kevin Jahns
6c375a37c8 update dist dir 2016-11-09 14:18:06 +01:00
Kevin Jahns
cd0cddaf35 fixed several sync issues 2016-11-04 11:42:50 +01:00
Kevin Jahns
93c23ddc09 fixed some gc issues (unfinished) 2016-11-03 16:28:48 +01:00
Kevin Jahns
480dfdfb77 dont modify gc buffer if gcTimeout > 0 or disconnected 2016-11-02 20:55:24 +01:00
Kevin Jahns
dda2a1ef82 another take on sourcedir: null specifies not to load anything automatically 2016-10-31 02:58:21 +01:00
Kevin Jahns
f32ff1b613 fixed several consistency issues with y-indexeddb. Implemented support for .close() - a soft replacement for .destroy() 2016-10-31 01:17:24 +01:00
Kevin Jahns
8ab16f4ada Release 12.1.0 2016-10-29 21:45:38 +02:00
Kevin Jahns
3fdcf82bcc fixed error description for observers that throw errors 2016-10-29 16:55:16 +02:00
Kevin Jahns
6dd33f4f90 copy db, connector, and type properties before handing them over to the connector, etc. This prevents some weird errors using y-elements 2016-10-24 23:10:44 +02:00
Kevin Jahns
0521fac8d8 implemented auth utilities for yjs 2016-10-24 11:57:59 +02:00
Kevin Jahns
666ab8285c update examples 2016-10-13 17:29:24 +02:00
Kevin Jahns
675c7f6638 rephrase intro 2016-10-13 15:53:41 +02:00
Kevin Jahns
463608cb5c Release 12.0.4 2016-10-12 15:52:04 +02:00
Kevin Jahns
d1059b5d04 local sourceDir causes troubles if modules are required by a yjs instance 2016-10-12 15:51:42 +02:00
Kevin Jahns
8b24284e25 Release 12.0.3 2016-10-07 21:00:46 +02:00
Kevin Jahns
08bcdfb008 implemented es6 import utility function (Y.extend(module) === module(Y)) 2016-10-07 21:00:17 +02:00
Kevin Jahns
f93d7b1e70 Release 12.0.2 2016-10-05 01:00:00 +02:00
Kevin Jahns
4d024883bc Fixed "[0] of undefined" server issue 2016-10-04 16:12:24 +02:00
Kevin Jahns
ecd412c6f6 sourcedir is no longer a global variable 2016-10-04 14:54:52 +02:00
Kevin Jahns
b939cdd086 Release 12.0.1 2016-09-27 16:17:33 +02:00
Kevin Jahns
17803266d4 repairChecker: Yjs is now able to detect incorrect states that happen when messages get lost. When Yjs is in an incorrect state it repairs itself and syncs again 2016-09-27 16:12:35 +02:00
Kevin Jahns
f0e88d192c Release 12.0.0 2016-09-20 19:19:18 +02:00
Kevin Jahns
e66c0f8a4e update doc 2016-09-20 19:13:09 +02:00
Kevin Jahns
eba3d590cc update doc 2016-09-20 19:12:41 +02:00
Kevin Jahns
0b31e63b82 update dist 2016-09-20 18:30:52 +02:00
Kevin Jahns
d22fbca6cc ready to publish (lint) 2016-09-01 02:40:35 +02:00
Kevin Jahns
330434ee24 update npm example 2016-08-31 02:58:28 +02:00
Kevin Jahns
2f0216bf89 update help 2016-08-30 21:03:08 +02:00
Kevin Jahns
f9d0625bd2 simplify example 2016-08-30 20:58:17 +02:00
Kevin Jahns
7a9d60770a simplify documentation 2016-08-30 20:43:05 +02:00
Kevin Jahns
059f72ffe1 fix init problems with v12, update examples to be synchronous 2016-08-30 18:10:19 +02:00
Kevin Jahns
d2d74a64ab update dist & added note to changelog 2016-08-29 17:56:39 +02:00
Kevin Jahns
a1f0140069 updated documentation (changelog for v12, added leveldb, consistency) 2016-08-29 17:21:30 +02:00
Kevin Jahns
7bd8e81342 fix test verifier 2016-08-28 22:13:07 +02:00
Kevin Jahns
34f365cd8f implemented support for synchronous type creation 2016-08-26 13:53:31 +02:00
Kevin Jahns
b3ba8e7546 Merge branch 'master' of github.com:y-js/yjs into v12 2016-08-25 04:43:43 +02:00
Kevin Jahns
e1e94bcf5d made createType synchronous 2016-08-25 04:42:23 +02:00
Kevin Jahns
4a83ff8514 updated quill example 2016-08-23 11:13:09 +02:00
13 changed files with 826 additions and 406 deletions

362
README.md
View File

@@ -1,133 +1,235 @@
# ![Yjs](http://y-js.org/images/yjs.png)
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data.
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data
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/).
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/).
You can create you own shared types easily.
Therefore, you can design the structure of your custom type,
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data).
We already provide abstract data types for
### 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 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), [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/)|
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.
##### Other
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.
| 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 with [npm](https://www.npmjs.org/package/yjs).
Install Yjs, and its modules with [bower](http://bower.io/), or
[npm](https://www.npmjs.org/package/yjs).
### Bower
```sh
bower install --save yjs y-array % add all y-* modules you want to use
```
bower install yjs --save
```
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.
```html
<script src="./bower_components/yjs/y.js"></script>
```
### Npm
```
npm install yjs --save
```sh
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)
```js
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
```
Y = require("yjs");
### ES6 Syntax
```js
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
```
Y({
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'))
}
Install dependencies
```sh
bower i yjs y-memory y-webrtc y-array y-text
```
# 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.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.
* 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.
* 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.
* We provide basic authentification for all connectors. The value of
`options.connector.auth` (this can be a passphase) is sent to all connected
Yjs instances. `options.connector.checkAuth` may grant read or write access
depending on the `auth` information.
Example: A client specifies `options.connector.auth = 'superSecretPassword`.
A server specifies
```js
options.connector.checkAuth = function (auth, yjsInstance, sender) {
return new Promise(function (resolve, reject){
if (auth === 'superSecretPassword') {
resolve('write') // grant read-write access
} else if (auth === 'different password') {
resolve('read') // grant read-only access
} else {
reject('wrong password!') // reject connection
}
})
}
```
* 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.
* options.sourceDir
* Path where all y-* modules are stored.
* *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 browserify you can specify all used modules like this:
```
* When using nodejs you need to manually extend Yjs:
```js
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-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'll only share data if they specified the same type with the same property name
* options.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.*`
* 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)
@@ -137,7 +239,8 @@ require('y-map')(Y)
* 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)
* 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`.
@@ -155,70 +258,57 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
* y.connector.disconnect()
* Force to disconnect this instance from the other instances
* y.connector.reconnect()
* Try to reconnect to the other instances (needs to be supported by the connector)
* Not supported by y-xmpp
* y.destroy()
* 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
* 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)
* 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**
## 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.
### 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.
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).
##### 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
```
## Changelog
### 11.0.0
* **All types now return a single event instead of list of events**
* Insert events contain a list of values
* Improved performance for large insertions & deletions
* Several bugfixes (offline editing related)
* Native support for node 4 (see #49)
### 10.0.0
* Support for more complex types (a type can be a composition of several types)
* Fixes several memory leaks
### 9.0.0
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.
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))
### 0.6.0
This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 0.6.0 it is possible to work asynchronously on a persistent database, which enables offline support.
* Switched to semver versioning
* Requires a promise implementation in environment (es6 promises suffice, included in all the major browsers). Otherwise you have to include a polyfill
* Y.Object has been renamed to Y.Map
* Y.Map exchanges `.val(name [, value])` in favor of `.set(name, value)` and `.get(name)`
* Y.Map `.get(name)` returns a promise, if the value is a custom type
* The Connector definition slightly changed (I'll update the wiki)
* The Type definitions completely changed, so you have to rewrite them (I'll rewrite the article in the wiki)
* Support for several packaging systems
* Flowtype
##### Enable logging in the browser
```js
localStorage.debug = 'y*'
```
## Contribution
I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
I created this framework during my bachelor thesis at the chair of computer
science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since
December 2014 I'm working on Yjs as a part of my student worker job at the i5.
## License
Yjs is licensed under the [MIT License](./LICENSE.txt).
Yjs is licensed under the [MIT License](./LICENSE).
<yjs@dbis.rwth-aachen.de>
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki

2
dist

Submodule dist updated: 81c8504462...8739fd3a9c

View File

@@ -10,7 +10,7 @@ module.exports = function (gulp, helperOptions) {
var options = minimist(process.argv.slice(2), {
string: ['modulename', 'export', 'name', 'port', 'testfiles', 'es6'],
default: {
modulename: helperOptions.moduleName,
moduleName: helperOptions.moduleName,
targetName: helperOptions.targetName,
export: 'ignore',
port: '8888',
@@ -36,13 +36,23 @@ module.exports = function (gulp, helperOptions) {
files.distEs5 = [files.dist]
}
var header = require('gulp-header')
var banner = ['/**',
' * <%= pkg.name %> - <%= pkg.description %>',
' * @version v<%= pkg.version %>',
' * @link <%= pkg.homepage %>',
' * @license <%= pkg.license %>',
' */',
''].join('\n')
gulp.task('dist:es5', function () {
var babelOptions = {
presets: ['es2015']
}
return (browserify({
entries: files.distEs5,
debug: true
debug: true,
standalone: options.moduleName
}).transform('babelify', babelOptions)
.bundle()
.pipe(source(options.targetName))
@@ -51,6 +61,7 @@ module.exports = function (gulp, helperOptions) {
.pipe($.if(!options.debug, $.uglify().on('error', function (e) {
console.log('\x07', e.message, JSON.stringify(e)); return this.end()
})))
.pipe(header(banner, { pkg: require('./package.json') }))
.pipe($.sourcemaps.write('.'))
.pipe(gulp.dest('./dist/')))
})
@@ -58,7 +69,8 @@ module.exports = function (gulp, helperOptions) {
gulp.task('dist:es6', function () {
return (browserify({
entries: files.dist,
debug: true
debug: true,
standalone: options.moduleName
}).bundle()
.pipe(source(options.targetName))
.pipe(buffer())
@@ -67,8 +79,8 @@ module.exports = function (gulp, helperOptions) {
.pipe($.rename({
extname: '.es6'
}))
.pipe(header(banner, { pkg: require('./package.json') }))
.pipe($.sourcemaps.write('.'))
.pipe(gulp.dest('./dist/')))
})

View File

@@ -51,7 +51,7 @@ require('./gulpfile.helper.js')(gulp, {
polyfills: [],
entry: './src/y.js',
targetName: 'y.js',
moduleName: 'yjs',
moduleName: 'Y',
includeRuntime: true,
specs: [
'./src/Database.spec.js',

View File

@@ -1,7 +1,7 @@
{
"name": "yjs",
"version": "11.2.5",
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
"version": "12.3.3",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./src/y.js",
"scripts": {
"test": "node --harmony ./node_modules/.bin/gulp test",
@@ -53,6 +53,7 @@
"gulp-concat": "^2.6.0",
"gulp-filter": "^3.0.1",
"gulp-git": "^1.6.0",
"gulp-header": "^1.8.8",
"gulp-if": "^2.0.0",
"gulp-jasmine": "^2.0.1",
"gulp-jasmine-browser": "^0.2.3",
@@ -63,7 +64,7 @@
"gulp-shell": "^0.5.1",
"gulp-sourcemaps": "^1.5.2",
"gulp-tag-version": "^1.3.0",
"gulp-uglify": "1.4.*",
"gulp-uglify": "^2.0.0",
"gulp-util": "^3.0.6",
"gulp-watch": "^4.3.5",
"minimist": "^1.2.0",
@@ -75,5 +76,7 @@
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
"dependencies": {}
"dependencies": {
"debug": "^2.6.3"
}
}

View File

@@ -1,5 +1,5 @@
/* @flow */
'use strict'
function canRead (auth) { return auth === 'read' || auth === 'write' }
function canWrite (auth) { return auth === 'write' }
module.exports = function (Y/* :any */) {
class AbstractConnector {
@@ -14,7 +14,6 @@ module.exports = function (Y/* :any */) {
syncingClients: Array<UserId>;
forwardToSyncingClients: boolean;
debug: boolean;
broadcastedHB: boolean;
syncStep2: Promise;
userId: UserId;
send: Function;
@@ -33,6 +32,10 @@ module.exports = function (Y/* :any */) {
if (opts == null) {
opts = {}
}
// Prefer to receive untransformed operations. This does only work if
// this client receives operations from only one other client.
// In particular, this does not work with y-webrtc.
// It will work with y-websockets-client
if (opts.role == null || opts.role === 'master') {
this.role = 'master'
} else if (opts.role === 'slave') {
@@ -40,6 +43,8 @@ module.exports = function (Y/* :any */) {
} else {
throw new Error("Role must be either 'master' or 'slave'!")
}
this.log = Y.debug('y:connector')
this.logMessage = Y.debug('y:connector-message')
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
this.role = opts.role
this.connections = {}
@@ -50,24 +55,50 @@ module.exports = function (Y/* :any */) {
this.syncingClients = []
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
this.debug = opts.debug === true
this.broadcastedHB = false
this.syncStep2 = Promise.resolve()
this.broadcastOpBuffer = []
this.protocolVersion = 11
this.authInfo = opts.auth || null
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
if (opts.generateUserId === true) {
this.setUserId(Y.utils.generateGuid())
}
}
resetAuth (auth) {
if (this.authInfo !== auth) {
this.authInfo = auth
this.broadcast({
type: 'auth',
auth: this.authInfo
})
}
}
reconnect () {
this.log('reconnecting..')
return this.y.db.startGarbageCollector()
}
disconnect () {
this.log('discronnecting..')
this.connections = {}
this.isSynced = false
this.currentSyncTarget = null
this.broadcastedHB = false
this.syncingClients = []
this.whenSyncedListeners = []
return this.y.db.stopGarbageCollector()
this.y.db.stopGarbageCollector()
return this.y.db.whenTransactionsFinished()
}
repair () {
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
for (var name in this.connections) {
this.connections[name].isSynced = false
}
this.isSynced = false
this.currentSyncTarget = null
this.findNextSyncTarget()
}
setUserId (userId) {
if (this.userId == null) {
this.log('Set userId to "%s"', userId)
this.userId = userId
return this.y.db.setUserId(userId)
} else {
@@ -77,8 +108,12 @@ module.exports = function (Y/* :any */) {
onUserEvent (f) {
this.userEventListeners.push(f)
}
removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => { f !== g })
}
userLeft (user) {
if (this.connections[user] != null) {
this.log('User left: %s', user)
delete this.connections[user]
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
@@ -102,6 +137,7 @@ module.exports = function (Y/* :any */) {
if (this.connections[user] != null) {
throw new Error('This user already joined!')
}
this.log('User joined: %s', user)
this.connections[user] = {
isSynced: false,
role: role
@@ -126,13 +162,8 @@ module.exports = function (Y/* :any */) {
this.whenSyncedListeners.push(f)
}
}
/*
returns false, if there is no sync target
true otherwise
*/
findNextSyncTarget () {
if (this.currentSyncTarget != null || this.isSynced) {
if (this.currentSyncTarget != null) {
return // "The current sync has not finished!"
}
@@ -149,30 +180,39 @@ module.exports = function (Y/* :any */) {
this.y.db.requestTransaction(function *() {
var stateSet = yield* this.getStateSet()
var deleteSet = yield* this.getDeleteSet()
conn.send(syncUser, {
var answer = {
type: 'sync step 1',
stateSet: stateSet,
deleteSet: deleteSet,
protocolVersion: conn.protocolVersion
})
protocolVersion: conn.protocolVersion,
auth: conn.authInfo
}
conn.send(syncUser, answer)
})
} else {
this.y.db.requestTransaction(function *() {
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
conn.isSynced = true
yield* this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of conn.whenSyncedListeners) {
f()
}
conn.whenSyncedListeners = []
})
if (!conn.isSynced) {
this.y.db.requestTransaction(function *() {
if (!conn.isSynced) {
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
conn.isSynced = true
yield* this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of conn.whenSyncedListeners) {
f()
}
conn.whenSyncedListeners = []
}
})
}
}
}
send (uid, message) {
if (this.debug) {
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, message) // eslint-disable-line
}
this.log('Send \'%s\' to %s', message.type, uid)
this.logMessage('Message: %j', message)
}
broadcast (message) {
this.log('Broadcast \'%s\'', message.type)
this.logMessage('Message: %j', message)
}
/*
Buffer operations, and broadcast them when ready.
@@ -207,13 +247,12 @@ module.exports = function (Y/* :any */) {
*/
receiveMessage (sender/* :UserId */, message/* :Message */) {
if (sender === this.userId) {
return
}
if (this.debug) {
console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line
return Promise.resolve()
}
this.log('Receive \'%s\' from %s', message.type, sender)
this.logMessage('Message: %j', message)
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
console.error(
this.log(
`You tried to sync with a yjs instance that has a different protocol version
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
@@ -222,91 +261,123 @@ module.exports = function (Y/* :any */) {
type: 'sync stop',
protocolVersion: this.protocolVersion
})
return
return Promise.reject('Incompatible protocol version')
}
if (message.type === 'sync step 1') {
let conn = this
let m = message
this.y.db.requestTransaction(function *() {
var currentStateSet = yield* this.getStateSet()
yield* this.applyDeleteSet(m.deleteSet)
var ds = yield* this.getDeleteSet()
var ops = yield* this.getOperations(m.stateSet)
conn.send(sender, {
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'
if (message.auth != null && this.connections[sender] != null) {
// authenticate using auth in message
var auth = this.checkAuth(message.auth, this.y, sender)
this.connections[sender].auth = auth
auth.then(auth => {
for (var f of this.userEventListeners) {
f({
action: 'userAuthenticated',
user: sender,
auth: auth
})
}
conn._setSyncedWith(sender)
})
} else if (message.type === 'sync step 2') {
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 if (this.connections[sender] != null && this.connections[sender].auth == null) {
// authenticate without otherwise
this.connections[sender].auth = this.checkAuth(null, this.y, sender)
}
if (this.connections[sender] != null && this.connections[sender].auth != null) {
return this.connections[sender].auth.then((auth) => {
if (message.type === 'sync step 1' && canRead(auth)) {
let conn = this
let m = message
this.y.db.requestTransaction(function *() {
var currentStateSet = yield* this.getStateSet()
if (canWrite(auth)) {
yield* this.applyDeleteSet(m.deleteSet)
}
var ds = yield* this.getDeleteSet()
var answer = {
type: 'sync step 2',
stateSet: currentStateSet,
deleteSet: ds,
protocolVersion: this.protocolVersion,
auth: this.authInfo
}
answer.os = yield* this.getOperations(m.stateSet)
conn.send(sender, answer)
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 {
// broadcast only once!
conn.broadcastOps(ops)
conn.send(sender, {
type: 'sync done'
})
}
})
} else if (message.type === 'sync step 2' && canWrite(auth)) {
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)
if (m.osUntransformed != null) {
yield* this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
} else {
this.store.apply(m.os)
}
/*
* This just sends the complete hb after some time
* Mostly for debugging..
*
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()
})
})
} else if (message.type === 'sync done') {
var self = this
this.syncStep2.then(function () {
self._setSyncedWith(sender)
})
} else if (message.type === 'update') {
if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) {
this.send(client, message)
if (this.y.db.forwardAppliedOperations) {
var delops = message.ops.filter(function (o) {
return o.struct === 'Delete'
})
if (delops.length > 0) {
this.broadcastOps(delops)
}
}
this.y.db.apply(message.ops)
}
}
if (this.y.db.forwardAppliedOperations) {
var delops = message.ops.filter(function (o) {
return o.struct === 'Delete'
})
if (delops.length > 0) {
this.broadcastOps(delops)
}
}
this.y.db.apply(message.ops)
})
} else {
return Promise.reject('Unable to deliver message')
}
}
_setSyncedWith (user) {

View File

@@ -24,11 +24,21 @@ module.exports = function (Y) {
}
},
whenTransactionsFinished: function () {
var ps = []
for (var name in this.users) {
ps.push(this.users[name].y.db.whenTransactionsFinished())
}
return Promise.all(ps)
var self = this
return new Promise(function (resolve, reject) {
// The connector first has to send the messages to the db.
// Wait for the checkAuth-function to resolve
// 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)
}, 10)
})
},
flushOne: function flushOne () {
var bufs = []
@@ -54,8 +64,9 @@ module.exports = function (Y) {
delete buff[sender]
}
var user = globalRoom.users[userId]
user.receiveMessage(m[0], m[1])
return user.y.db.whenTransactionsFinished()
return user.receiveMessage(m[0], m[1]).then(function () {
return user.y.db.whenTransactionsFinished()
}, function () {})
} else {
return false
}
@@ -72,16 +83,14 @@ module.exports = function (Y) {
}
globalRoom.whenTransactionsFinished().then(nextFlush)
} else {
setTimeout(function () {
var c = globalRoom.flushOne()
if (c) {
c.then(function () {
globalRoom.whenTransactionsFinished().then(nextFlush)
})
} else {
resolve()
}
}, 0)
c = globalRoom.flushOne()
if (c) {
c.then(function () {
globalRoom.whenTransactionsFinished().then(nextFlush)
})
} else {
resolve()
}
}
}
globalRoom.whenTransactionsFinished().then(nextFlush)
@@ -107,7 +116,7 @@ module.exports = function (Y) {
this.syncingClientDuration = 0
}
receiveMessage (sender, m) {
super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
}
send (userId, message) {
var buffer = globalRoom.buffers[userId]
@@ -138,11 +147,15 @@ module.exports = function (Y) {
return Y.utils.globalRoom.flushAll()
}
disconnect () {
var waitForMe = Promise.resolve()
if (!this.isDisconnected()) {
globalRoom.removeUser(this.userId)
super.disconnect()
waitForMe = super.disconnect()
}
return this.y.db.whenTransactionsFinished()
var self = this
return waitForMe.then(function () {
return self.y.db.whenTransactionsFinished()
})
}
flush () {
var self = this
@@ -154,7 +167,7 @@ module.exports = function (Y) {
if (buff[sender].length === 0) {
delete buff[sender]
}
this.receiveMessage(m[0], m[1])
yield this.receiveMessage(m[0], m[1])
}
yield self.whenTransactionsFinished()
})

View File

@@ -39,6 +39,7 @@ module.exports = function (Y /* :any */) {
*/
constructor (y, opts) {
this.y = y
this.dbOpts = opts
var os = this
this.userId = null
var resolve
@@ -75,12 +76,12 @@ module.exports = function (Y /* :any */) {
}
this.gc1 = [] // first stage
this.gc2 = [] // second stage -> after that, remove the op
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeouts
function garbageCollect () {
return os.whenTransactionsFinished().then(function () {
if (os.gc1.length > 0 || os.gc2.length > 0) {
if (!os.y.isConnected()) {
console.warn('gc should be empty when disconnected!')
if (!os.y.connector.isSynced) {
console.warn('gc should be empty when not synced!')
}
return new Promise((resolve) => {
os.requestTransaction(function * () {
@@ -109,12 +110,57 @@ module.exports = function (Y /* :any */) {
})
}
this.garbageCollect = garbageCollect
this.startGarbageCollector()
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
this.opsReceivedTimestamp = new Date()
this.startRepairCheck()
}
startGarbageCollector () {
this.gc = this.dbOpts.gc == null || this.dbOpts.gc
if (this.gc) {
this.gcTimeout = !this.dbOpts.gcTimeout ? 50000 : this.dbOpts.gcTimeout
} else {
this.gcTimeout = -1
}
if (this.gcTimeout > 0) {
garbageCollect()
this.garbageCollect()
}
}
startRepairCheck () {
var os = this
if (this.repairCheckInterval > 0) {
this.repairCheckIntervalHandler = setInterval(function repairOnMissingOperations () {
/*
Case 1. No ops have been received in a while (new Date() - os.opsReceivedTimestamp > os.repairCheckInterval)
- 1.1 os.listenersById is empty. Then the state was correct the whole time. -> Nothing to do (nor to update)
- 1.2 os.listenersById is not empty.
* Then the state was incorrect for at least {os.repairCheckInterval} seconds.
* -> Remove everything in os.listenersById and sync again (connector.repair())
Case 2. An op has been received in the last {os.repairCheckInterval } seconds.
It is not yet necessary to check for faulty behavior. Everything can still resolve itself. Wait for more messages.
If nothing was received for a while and os.listenersById is still not emty, we are in case 1.2
-> Do nothing
Baseline here is: we really only have to catch case 1.2..
*/
if (
new Date() - os.opsReceivedTimestamp > os.repairCheckInterval &&
Object.keys(os.listenersById).length > 0 // os.listenersById is not empty
) {
// haven't received operations for over {os.repairCheckInterval} seconds, resend state vector
os.listenersById = {}
os.opsReceivedTimestamp = new Date() // update so you don't send repair several times in a row
os.y.connector.repair()
}
}, this.repairCheckInterval)
}
}
stopRepairCheck () {
clearInterval(this.repairCheckIntervalHandler)
}
queueGarbageCollector (id) {
if (this.y.isConnected()) {
if (this.y.connector.isSynced && this.gc) {
this.gc1.push(id)
}
}
@@ -147,6 +193,8 @@ module.exports = function (Y /* :any */) {
}
stopGarbageCollector () {
var self = this
this.gc = false
this.gcTimeout = -1
return new Promise(function (resolve) {
self.requestTransaction(function * () {
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
@@ -169,7 +217,7 @@ module.exports = function (Y /* :any */) {
TODO: rename this function
Rulez:
* Only gc if this user is online
* Only gc if this user is online & gc turned on
* The most left element in a list must not be gc'd.
=> There is at least one element in the list
@@ -178,7 +226,9 @@ module.exports = function (Y /* :any */) {
* addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true
op.deleted === true &&
this.store.gc &&
this.store.y.connector.isSynced
) {
var gc = false
if (left != null && left.deleted === true) {
@@ -204,9 +254,7 @@ module.exports = function (Y /* :any */) {
this.gc2 = this.gc2.filter(filter)
delete op.gc
}
* destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
destroyTypes () {
for (var key in this.initializedTypes) {
var type = this.initializedTypes[key]
if (type._destroy != null) {
@@ -216,6 +264,11 @@ module.exports = function (Y /* :any */) {
}
}
}
* destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
this.stopRepairCheck()
}
setUserId (userId) {
if (!this.userIdPromise.inProgress) {
this.userIdPromise.inProgress = true
@@ -246,12 +299,14 @@ module.exports = function (Y /* :any */) {
/*
Apply a list of operations.
* we save a timestamp, because we received new operations that could resolve ops in this.listenersById (see this.startRepairCheck)
* get a transaction
* check whether all Struct.*.requiredOps are in the OS
* check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied
*/
apply (ops) {
this.opsReceivedTimestamp = new Date()
for (var i = 0; i < ops.length; i++) {
var o = ops[i]
if (o.id == null || o.id[0] !== this.y.connector.userId) {
@@ -396,8 +451,7 @@ module.exports = function (Y /* :any */) {
*/
* operationAdded (transaction, op) {
if (op.struct === 'Delete') {
var target = yield* transaction.getInsertion(op.target)
var type = this.initializedTypes[JSON.stringify(target.parent)]
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
if (type != null) {
yield* type._changed(transaction, op)
}
@@ -465,10 +519,8 @@ module.exports = function (Y /* :any */) {
resolve: resolve,
promise: promise
}
return promise
} else {
return this.transactionsFinished.promise
}
return this.transactionsFinished.promise
} else {
return Promise.resolve()
}
@@ -505,6 +557,48 @@ module.exports = function (Y /* :any */) {
}, 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
}

View File

@@ -11,9 +11,7 @@ require('../../y-memory/src/Memory.js')(Y)
require('../../y-array/src/Array.js')(Y)
require('../../y-map/src/Map.js')(Y)
require('../../y-indexeddb/src/IndexedDB.js')(Y)
if (typeof window === 'undefined') {
require('../../y-leveldb/src/LevelDB.js')(Y)
}
module.exports = Y
var g
@@ -121,6 +119,10 @@ function * applyTransactions (relAmount, numberOfTransactions, objects, users, t
// There will be an artificial delay until ops can be executed by the type,
// therefore, operations of the database will be (pre)transformed until user operations arrive
yield (function simulateConcurrentUserInteractions (type) {
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
type = type.y
}
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
type.eventHandler.awaiting = 1
type.eventHandler._debuggingAwaiting = true
@@ -156,6 +158,10 @@ function * applyTransactions (relAmount, numberOfTransactions, objects, users, t
}
function fixAwaitingInType (type) {
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
type = type.y
}
return new Promise(function (resolve) {
type.os.whenTransactionsFinished().then(function () {
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
@@ -220,6 +226,20 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
var allDels1, allDels2 // all deletions
var db1 = [] // operation store of user1
yield Y.utils.globalRoom.flushAll()
yield g.garbageCollectAllUsers(users)
yield Y.utils.globalRoom.flushAll()
// disconnect, then reconnect all users
// We do this to make sure that the gc is updated by everyone
for (var i = 0; i < users.length; i++) {
yield users[i].disconnect()
yield wait()
yield users[i].reconnect()
}
yield wait()
yield Y.utils.globalRoom.flushAll()
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
function * t1 () {
s1 = yield* this.getStateSet()
@@ -237,9 +257,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
allDels2.push(d)
})
}
yield Y.utils.globalRoom.flushAll()
yield g.garbageCollectAllUsers(users)
yield Y.utils.globalRoom.flushAll()
var buffer = Y.utils.globalRoom.buffers
for (var name in buffer) {
if (buffer[name].length > 0) {
@@ -329,7 +347,9 @@ g.createUsers = async(function * createUsers (self, numberOfUsers, database, ini
name: database,
namespace: 'User ' + i,
cleanStart: true,
gcTimeout: -1
gcTimeout: -1,
gc: true,
repairCheckInterval: -1
},
connector: {
name: 'Test',

View File

@@ -30,7 +30,11 @@ module.exports = function (Y/* :any */) {
*/
Delete: {
encode: function (op) {
return op
return {
target: op.target,
length: op.length || 0,
struct: 'Delete'
}
},
requiredOps: function (op) {
return [] // [op.target]
@@ -265,7 +269,7 @@ module.exports = function (Y/* :any */) {
}
// try to merge original op.left and op.origin
for (let i = 0; i < tryToRemergeLater.length; i++) {
for (i = 0; i < tryToRemergeLater.length; i++) {
var m = yield* this.getOperation(tryToRemergeLater[i])
yield* this.tryCombineWithLeft(m)
}

View File

@@ -82,57 +82,6 @@ module.exports = function (Y/* :any */) {
os: 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!)
* does not check for Struct.*.requiredOps()
@@ -147,7 +96,7 @@ module.exports = function (Y/* :any */) {
send.push(Y.Struct[op.struct].encode(op))
}
}
if (!this.store.y.connector.isDisconnected() && send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
// is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps(send)
}
@@ -257,7 +206,8 @@ module.exports = function (Y/* :any */) {
yield* this.store.operationAdded(this, {
struct: 'Delete',
target: target.id,
length: targetLength
length: targetLength,
targetParent: target.parent
})
}
// need to gc in the end!
@@ -419,6 +369,9 @@ module.exports = function (Y/* :any */) {
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('gc should be empty after sync')
}
if (!this.store.gc) {
return
}
yield* this.os.iterate(this, null, null, function * (op) {
if (op.gc) {
delete op.gc
@@ -726,7 +679,7 @@ module.exports = function (Y/* :any */) {
}
if (this.store.forwardAppliedOperations) {
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)
}
}
@@ -764,7 +717,7 @@ module.exports = function (Y/* :any */) {
}
* addOperation (op) {
yield* this.os.put(op)
if (!this.store.y.connector.isDisconnected() && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
if (this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
// is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps([op])
}
@@ -1060,6 +1013,56 @@ module.exports = function (Y/* :any */) {
}
return send.reverse()
}
/*
* Get the plain untransformed operations from the database.
* You can apply these operations using .applyOperationsUntransformed(ops)
*
*/
* getOperationsUntransformed () {
var ops = []
yield* this.os.iterate(this, null, null, function * (op) {
if (op.id[0] !== '_') {
ops.push(op)
}
})
return {
untransformed: ops
}
}
* applyOperationsUntransformed (m, stateSet) {
var ops = m.untransformed
for (var i = 0; i < ops.length; i++) {
var op = ops[i]
// create, and modify parent, if it is created implicitly
if (op.parent != null && op.parent[0] === '_') {
if (op.struct === 'Insert') {
// update parents .map/start/end properties
if (op.parentSub != null && op.left == null) {
// op is child of Map
let parent = yield* this.getOperation(op.parent)
parent.map[op.parentSub] = op.id
yield* this.setOperation(parent)
} else if (op.right == null || op.left == null) {
let parent = yield* this.getOperation(op.parent)
if (op.right == null) {
parent.end = Y.utils.getLastId(op)
}
if (op.left == null) {
parent.start = op.id
}
yield* this.setOperation(parent)
}
}
}
yield* this.os.put(op)
}
for (var user in stateSet) {
yield* this.ss.put({
id: [user],
clock: stateSet[user]
})
}
}
/* this is what we used before.. use this as a reference..
* makeOperationReady (startSS, op) {
op = Y.Struct[op.struct].encode(op)

View File

@@ -26,6 +26,24 @@
module.exports = function (Y /* : any*/) {
Y.utils = {}
Y.utils.bubbleEvent = function (type, event) {
type.eventHandler.callEventListeners(event)
event.path = []
while (type != null && type._deepEventHandler != null) {
type._deepEventHandler.callEventListeners(event)
var parent = null
if (type._parent != null) {
parent = type.os.getType(type._parent)
}
if (parent != null && parent._getPathToChild != null) {
event.path = [parent._getPathToChild(type._model)].concat(event.path)
type = parent
} else {
type = null
}
}
}
class EventListenerHandler {
constructor () {
this.eventListeners = []
@@ -50,9 +68,13 @@ module.exports = function (Y /* : any*/) {
callEventListeners (event) {
for (var i = 0; i < this.eventListeners.length; i++) {
try {
this.eventListeners[i](event)
var _event = {}
for (var name in event) {
_event[name] = event[name]
}
this.eventListeners[i](_event)
} 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)
}
}
}
@@ -82,7 +104,6 @@ module.exports = function (Y /* : any*/) {
destroy () {
super.destroy()
this.waiting = null
this.awaiting = null
this.onevent = null
}
/*
@@ -238,7 +259,13 @@ module.exports = function (Y /* : any*/) {
// finished with remaining operations
self.waiting.push(d)
}
checkDelete(op)
if (op.key == null) {
// deletes in list
checkDelete(op)
} else {
// deletes in map
this.waiting.push(op)
}
} else {
this.waiting.push(op)
}
@@ -287,7 +314,11 @@ module.exports = function (Y /* : any*/) {
var o = this.waiting[i]
if (o.struct === 'Insert') {
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.left = [o.id[0], o.id[1] - 1]
} else if (_o.left == null) {
@@ -447,6 +478,27 @@ module.exports = function (Y /* : any*/) {
}
Y.utils.EventHandler = EventHandler
/*
Default class of custom types!
*/
class CustomType {
getPath () {
var parent = null
if (this._parent != null) {
parent = this.os.getType(this._parent)
}
if (parent != null && parent._getPathToChild != null) {
var firstKey = parent._getPathToChild(this._model)
var parentKeys = parent.getPath()
parentKeys.push(firstKey)
return parentKeys
} else {
return []
}
}
}
Y.utils.CustomType = CustomType
/*
A wrapper for the definition of a custom type.
Every custom type must have three properties:
@@ -458,7 +510,7 @@ module.exports = function (Y /* : any*/) {
* class
- 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;
initType: any;
@@ -469,12 +521,14 @@ module.exports = function (Y /* : any*/) {
if (def.struct == null ||
def.initType == null ||
def.class == null ||
def.name == null
def.name == null ||
def.createType == null
) {
throw new Error('Custom type was not initialized correctly!')
}
this.struct = def.struct
this.initType = def.initType
this.createType = def.createType
this.class = def.class
this.name = def.name
if (def.appendAdditionalInfo != null) {
@@ -486,13 +540,13 @@ module.exports = function (Y /* : any*/) {
this.parseArguments.typeDefinition = this
}
}
Y.utils.CustomType = CustomType
Y.utils.CustomTypeDefinition = CustomTypeDefinition
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
if (v != null) {
if (v instanceof Y.utils.CustomType) return [v]
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomType) return v
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomType) return [v.typeDefinition]
if (v instanceof Y.utils.CustomTypeDefinition) 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.CustomTypeDefinition) return [v.typeDefinition]
}
return false
}
@@ -733,4 +787,9 @@ module.exports = function (Y /* : any*/) {
return SmallLookupBuffer
}
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
// Generates a unique id, for use as a user id.
// Thx to @jed for this script https://gist.github.com/jed/982883
function generateGuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,generateGuid)} // eslint-disable-line
Y.utils.generateGuid = generateGuid
}

129
src/y.js
View File

@@ -8,25 +8,44 @@ require('./Struct.js')(Y)
require('./Utils.js')(Y)
require('./Connectors/Test.js')(Y)
Y.debug = require('debug')
var requiringModules = {}
module.exports = Y
Y.requiringModules = requiringModules
Y.extend = function (name, value) {
if (value instanceof Y.utils.CustomType) {
Y[name] = value.parseArguments
if (arguments.length === 2 && typeof name === 'string') {
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 {
Y[name] = value
}
if (requiringModules[name] != null) {
requiringModules[name].resolve()
delete requiringModules[name]
for (var i = 0; i < arguments.length; i++) {
var f = arguments[i]
if (typeof f === 'function') {
f(Y)
} else {
throw new Error('Expected function!')
}
}
}
}
Y.requestModules = requestModules
function requestModules (modules) {
var sourceDir
if (Y.sourceDir === null) {
sourceDir = null
} else {
sourceDir = Y.sourceDir || '/bower_components'
}
// 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..
// then load the es5(.js) files..
@@ -39,10 +58,11 @@ function requestModules (modules) {
if (requiringModules[module] == null) {
// module does not exist
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
var imported = document.createElement('script')
imported.src = Y.sourceDir + '/' + modulename + '/' + modulename + extention
document.head.appendChild(imported)
if (sourceDir != null) {
var imported = document.createElement('script')
imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
document.head.appendChild(imported)
}
let requireModule = {}
requiringModules[module] = requireModule
requireModule.promise = new Promise(function (resolve) {
@@ -91,31 +111,36 @@ type YOptions = {
*/
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
if (opts.hasOwnProperty('sourceDir')) {
Y.sourceDir = opts.sourceDir
}
opts.types = opts.types != null ? opts.types : []
var modules = [opts.db.name, opts.connector.name].concat(opts.types)
for (var name in opts.share) {
modules.push(opts.share[name])
}
Y.sourceDir = opts.sourceDir
return new Promise(function (resolve, reject) {
setTimeout(function () {
Y.requestModules(modules).then(function () {
if (opts == null) reject('An options object is expected! ')
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name 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 db name! (missing db.name property)')
else if (opts.share == null) reject('You must specify a set of shared types!')
else {
if (opts == null) reject('An options object is expected! ')
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name 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 db name! (missing db.name property)')
else {
opts = Y.utils.copyObject(opts)
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)
yconfig.db.whenUserIdSet(function () {
yconfig.init(function () {
resolve(yconfig)
})
})
}
}).catch(reject)
}, 0)
}).catch(reject)
}, 0)
}
})
}
@@ -130,6 +155,7 @@ class YConfig {
this.options = opts
this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector)
this.connected = true
}
init (callback) {
var opts = this.options
@@ -140,6 +166,9 @@ class YConfig {
for (var propertyname in opts.share) {
var typeConstructor = opts.share[propertyname].split('(')
var typeName = typeConstructor.splice(0, 1)
var type = Y[typeName]
var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
var args = []
if (typeConstructor.length === 1) {
try {
@@ -147,11 +176,13 @@ class YConfig {
} catch (e) {
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]
var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
share[propertyname] = yield* this.createType(type.apply(typedef, args), id)
share[propertyname] = yield* this.store.initType.call(this, id, args)
}
this.store.whenTransactionsFinished()
.then(callback)
@@ -161,26 +192,46 @@ class YConfig {
return this.connector.isSynced
}
disconnect () {
return this.connector.disconnect()
if (this.connected) {
this.connected = false
return this.connector.disconnect()
} else {
return Promise.resolve()
}
}
reconnect () {
return this.connector.reconnect()
if (!this.connected) {
this.connected = true
return this.connector.reconnect()
} else {
return Promise.resolve()
}
}
destroy () {
var self = this
return this.close().then(function () {
if (self.db.deleteDB != null) {
return self.db.deleteDB()
} else {
return Promise.resolve()
}
})
}
close () {
var self = this
this.share = null
if (this.connector.destroy != null) {
this.connector.destroy()
} else {
this.connector.disconnect()
}
var self = this
this.db.requestTransaction(function * () {
yield* self.db.destroy()
self.connector = null
self.db = null
return this.db.whenTransactionsFinished(function () {
this.db.destroyTypes()
// make sure to wait for all transactions before destroying the db
this.db.requestTransaction(function * () {
yield* self.db.destroy()
})
return this.db.whenTransactionsFinished()
})
}
}
if (typeof window !== 'undefined') {
window.Y = Y
}