Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9374c363c3 |
358
README.md
358
README.md
@@ -1,235 +1,133 @@
|
|||||||
|
|
||||||
# 
|
# 
|
||||||
|
|
||||||
Yjs is a framework for offline-first p2p shared editing on structured data like
|
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data.
|
||||||
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer
|
||||||
most of the complexity of concurrent editing. For additional information, demos,
|
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data
|
||||||
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/).
|
||||||
|
|
||||||
### Extensions
|
You can create you own shared types easily.
|
||||||
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
|
Therefore, you can design the structure of your custom type,
|
||||||
* *Connector* - a communication protocol that propagates changes to the clients
|
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data).
|
||||||
* *Database* - a database to store your changes
|
We already provide abstract data types for
|
||||||
* 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 |
|
| 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 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*>) |
|
|[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) |
|
||||||
|[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/)|
|
||||||
|
|
||||||
##### Other
|
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.
|
||||||
|
|
||||||
| Name | Description |
|
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).
|
||||||
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
|
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!
|
## Use it!
|
||||||
Install Yjs, and its modules with [bower](http://bower.io/), or
|
Install yjs and its modules with [bower](http://bower.io/), or with [npm](https://www.npmjs.org/package/yjs).
|
||||||
[npm](https://www.npmjs.org/package/yjs).
|
|
||||||
|
|
||||||
### Bower
|
### Bower
|
||||||
```sh
|
|
||||||
bower install --save yjs y-array % add all y-* modules you want to use
|
|
||||||
```
|
```
|
||||||
You only need to include the `y.js` file. Yjs is able to automatically require
|
bower install yjs --save
|
||||||
missing modules.
|
```
|
||||||
```html
|
Then you include the libraries directly from the installation folder.
|
||||||
|
```
|
||||||
<script src="./bower_components/yjs/y.js"></script>
|
<script src="./bower_components/yjs/y.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Npm
|
### Npm
|
||||||
```sh
|
```
|
||||||
npm install --save yjs % add all y-* modules you want to use
|
npm install yjs --save
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't include via script tag, you have to explicitly include all modules!
|
And use it like this with *npm*:
|
||||||
(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
|
# Text editing example
|
||||||
Install dependencies
|
```
|
||||||
```sh
|
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'))
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a simple example of a shared textarea
|
# Api
|
||||||
```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 [](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
|
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`.
|
||||||
`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.
|
|
||||||
* options.connector
|
* options.connector
|
||||||
* Will be forwarded to the connector adapter. Specify the connector adaper on
|
* Will be forwarded to the connector adapter. Specify the connector adaper on `options.connector.name`.
|
||||||
`options.connector.name`.
|
* All our connectors implement a `room` property. Clients that specify the same room share the same data.
|
||||||
* All our connectors implement a `room` property. Clients that specify the
|
* All of our connectors specify an `url` property that defines the connection endpoint of the used connector.
|
||||||
same room share the same data.
|
* All of our connectors also have a default connection endpoint that you can use for development.
|
||||||
* 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.
|
* Have a look at the used connector repository to see all available options.
|
||||||
* *Only if you know what you are doing:* Set
|
* options.sourceDir
|
||||||
`options.connector.preferUntransformed = true` in order receive the shared
|
* Path where all y-* modules are stored.
|
||||||
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`
|
* Defaults to `/bower_components`
|
||||||
* Not required when running on `nodejs` / `iojs`
|
* Not required when running on `nodejs` / `iojs`
|
||||||
* When using nodejs you need to manually extend Yjs:
|
* When using browserify you can specify all used modules like this:
|
||||||
```js
|
```
|
||||||
var Y = require('yjs')
|
var Y = require('yjs')
|
||||||
// you have to require a db, connector, and *all* types you use!
|
// you need to require the 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)
|
||||||
// ..
|
// ..
|
||||||
```
|
```
|
||||||
* options.share
|
* options.share
|
||||||
* Specify on `options.share[arbitraryName]` types that are shared among all
|
* Specify on `options.share[arbitraryName]` types that are shared among all users.
|
||||||
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
|
* If userA doesn't specify `options.share[arbitraryName]`, it won't be available for userA.
|
||||||
create an y-array type on `y.share[arbitraryName]`.
|
* 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 userA doesn't specify `options.share[arbitraryName]`, it won't be
|
* 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.*`
|
||||||
available for userA.
|
* Weird behavior: It is supported that two users specify different types with the same property name.
|
||||||
* If userB specifies `options.share[arbitraryName]`, it still won't be
|
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
|
||||||
available for userA. But all the updates are send from userB to userA.
|
* options.type
|
||||||
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
|
* Array of modules that Yjs needs to require, before instantiating a shared type.
|
||||||
Instead, they are merged among all users. This feature is only available on
|
* By default Yjs requires the specified database adapter, the specified connector, and all modules that are used in `options.share.*`
|
||||||
`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.*
|
* Put all types here that you intend to use, but are not used in y.share.*
|
||||||
|
|
||||||
### Instantiated Y object (y)
|
### Instantiated Y object (y)
|
||||||
@@ -239,8 +137,7 @@ require('y-map')(Y)
|
|||||||
* The specified database adapter is loaded
|
* The specified database adapter is loaded
|
||||||
* The specified connector is loaded
|
* The specified connector is loaded
|
||||||
* All types are included
|
* All types are included
|
||||||
* The connector is initialized, and a unique user id is set (received from the
|
* The connector is initialized, and a unique user id is set (received from the server)
|
||||||
server)
|
|
||||||
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
|
* 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`.
|
The promise returns an instance of Y. We denote it with a lower case `y`.
|
||||||
@@ -258,57 +155,70 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
|
|||||||
* y.connector.disconnect()
|
* y.connector.disconnect()
|
||||||
* Force to disconnect this instance from the other instances
|
* Force to disconnect this instance from the other instances
|
||||||
* y.connector.reconnect()
|
* y.connector.reconnect()
|
||||||
* Try to reconnect to the other instances (needs to be supported by the
|
* Try to reconnect to the other instances (needs to be supported by the connector)
|
||||||
connector)
|
|
||||||
* Not supported by y-xmpp
|
* Not supported by y-xmpp
|
||||||
* y.close()
|
* y.destroy()
|
||||||
* Destroy this object.
|
* Destroy this object.
|
||||||
* Destroys all types (they will throw weird errors if you still use them)
|
* Destroys all types (they will throw weird errors if you still use them)
|
||||||
* Disconnects from the other instances (via connector)
|
* Disconnects from the other instances (via connector)
|
||||||
* Returns a promise
|
|
||||||
* y.destroy()
|
|
||||||
* calls y.close()
|
|
||||||
* Removes all data from the database
|
* Removes all data from the database
|
||||||
* Returns a promise
|
|
||||||
* y.db.stopGarbageCollector()
|
* y.db.stopGarbageCollector()
|
||||||
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
|
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage collection
|
||||||
collection
|
|
||||||
* y.db.gc :: Boolean
|
|
||||||
* Whether gc is turned on
|
|
||||||
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
||||||
* Time interval between two garbage collect cycles
|
* Time interval between two garbage collect cycles
|
||||||
* It is required that all instances exchanged all messages after two garbage
|
* It is required that all instances exchanged all messages after two garbage collect cycles (after 100000 ms per default)
|
||||||
collect cycles (after 100000 ms per default)
|
|
||||||
* 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**
|
||||||
|
|
||||||
### Logging
|
## Get help
|
||||||
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
|
There are some friendly people on [](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.
|
||||||
`y*` enables logging for all y-* components. You can selectively remove
|
|
||||||
components you are not interested in: E.g. The flag `y*,-y:connector-message`
|
|
||||||
will not log the long `y:connector-message` messages.
|
|
||||||
|
|
||||||
##### Enable logging in Node.js
|
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.
|
||||||
```sh
|
If you want to see an issue fixed, please subscribe to the thread (or remind me via gitter).
|
||||||
DEBUG=y* node app.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove the colors in order to log to a file:
|
|
||||||
```sh
|
|
||||||
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Enable logging in the browser
|
## Changelog
|
||||||
```js
|
|
||||||
localStorage.debug = 'y*'
|
### 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
|
||||||
|
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
I created this framework during my bachelor thesis at the chair of computer
|
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.
|
||||||
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).
|
Yjs is licensed under the [MIT License](./LICENSE.txt).
|
||||||
|
|
||||||
<yjs@dbis.rwth-aachen.de>
|
<yjs@dbis.rwth-aachen.de>
|
||||||
|
|
||||||
|
[ShareJs]: https://github.com/share/ShareJS
|
||||||
|
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki
|
||||||
|
|
||||||
|
|||||||
2
dist
2
dist
Submodule dist updated: 8739fd3a9c...47bcec8bc7
@@ -10,7 +10,7 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
var options = minimist(process.argv.slice(2), {
|
var options = minimist(process.argv.slice(2), {
|
||||||
string: ['modulename', 'export', 'name', 'port', 'testfiles', 'es6'],
|
string: ['modulename', 'export', 'name', 'port', 'testfiles', 'es6'],
|
||||||
default: {
|
default: {
|
||||||
moduleName: helperOptions.moduleName,
|
modulename: helperOptions.moduleName,
|
||||||
targetName: helperOptions.targetName,
|
targetName: helperOptions.targetName,
|
||||||
export: 'ignore',
|
export: 'ignore',
|
||||||
port: '8888',
|
port: '8888',
|
||||||
@@ -36,23 +36,13 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
files.distEs5 = [files.dist]
|
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 () {
|
gulp.task('dist:es5', function () {
|
||||||
var babelOptions = {
|
var babelOptions = {
|
||||||
presets: ['es2015']
|
presets: ['es2015']
|
||||||
}
|
}
|
||||||
return (browserify({
|
return (browserify({
|
||||||
entries: files.distEs5,
|
entries: files.distEs5,
|
||||||
debug: true,
|
debug: true
|
||||||
standalone: options.moduleName
|
|
||||||
}).transform('babelify', babelOptions)
|
}).transform('babelify', babelOptions)
|
||||||
.bundle()
|
.bundle()
|
||||||
.pipe(source(options.targetName))
|
.pipe(source(options.targetName))
|
||||||
@@ -61,7 +51,6 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
.pipe($.if(!options.debug, $.uglify().on('error', function (e) {
|
.pipe($.if(!options.debug, $.uglify().on('error', function (e) {
|
||||||
console.log('\x07', e.message, JSON.stringify(e)); return this.end()
|
console.log('\x07', e.message, JSON.stringify(e)); return this.end()
|
||||||
})))
|
})))
|
||||||
.pipe(header(banner, { pkg: require('./package.json') }))
|
|
||||||
.pipe($.sourcemaps.write('.'))
|
.pipe($.sourcemaps.write('.'))
|
||||||
.pipe(gulp.dest('./dist/')))
|
.pipe(gulp.dest('./dist/')))
|
||||||
})
|
})
|
||||||
@@ -69,8 +58,7 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
gulp.task('dist:es6', function () {
|
gulp.task('dist:es6', function () {
|
||||||
return (browserify({
|
return (browserify({
|
||||||
entries: files.dist,
|
entries: files.dist,
|
||||||
debug: true,
|
debug: true
|
||||||
standalone: options.moduleName
|
|
||||||
}).bundle()
|
}).bundle()
|
||||||
.pipe(source(options.targetName))
|
.pipe(source(options.targetName))
|
||||||
.pipe(buffer())
|
.pipe(buffer())
|
||||||
@@ -79,8 +67,8 @@ module.exports = function (gulp, helperOptions) {
|
|||||||
.pipe($.rename({
|
.pipe($.rename({
|
||||||
extname: '.es6'
|
extname: '.es6'
|
||||||
}))
|
}))
|
||||||
.pipe(header(banner, { pkg: require('./package.json') }))
|
|
||||||
.pipe($.sourcemaps.write('.'))
|
.pipe($.sourcemaps.write('.'))
|
||||||
|
|
||||||
.pipe(gulp.dest('./dist/')))
|
.pipe(gulp.dest('./dist/')))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ require('./gulpfile.helper.js')(gulp, {
|
|||||||
polyfills: [],
|
polyfills: [],
|
||||||
entry: './src/y.js',
|
entry: './src/y.js',
|
||||||
targetName: 'y.js',
|
targetName: 'y.js',
|
||||||
moduleName: 'Y',
|
moduleName: 'yjs',
|
||||||
includeRuntime: true,
|
includeRuntime: true,
|
||||||
specs: [
|
specs: [
|
||||||
'./src/Database.spec.js',
|
'./src/Database.spec.js',
|
||||||
|
|||||||
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "12.3.3",
|
"version": "11.2.6",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
|
||||||
"main": "./src/y.js",
|
"main": "./src/y.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --harmony ./node_modules/.bin/gulp test",
|
"test": "node --harmony ./node_modules/.bin/gulp test",
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
"gulp-concat": "^2.6.0",
|
"gulp-concat": "^2.6.0",
|
||||||
"gulp-filter": "^3.0.1",
|
"gulp-filter": "^3.0.1",
|
||||||
"gulp-git": "^1.6.0",
|
"gulp-git": "^1.6.0",
|
||||||
"gulp-header": "^1.8.8",
|
|
||||||
"gulp-if": "^2.0.0",
|
"gulp-if": "^2.0.0",
|
||||||
"gulp-jasmine": "^2.0.1",
|
"gulp-jasmine": "^2.0.1",
|
||||||
"gulp-jasmine-browser": "^0.2.3",
|
"gulp-jasmine-browser": "^0.2.3",
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
"gulp-shell": "^0.5.1",
|
"gulp-shell": "^0.5.1",
|
||||||
"gulp-sourcemaps": "^1.5.2",
|
"gulp-sourcemaps": "^1.5.2",
|
||||||
"gulp-tag-version": "^1.3.0",
|
"gulp-tag-version": "^1.3.0",
|
||||||
"gulp-uglify": "^2.0.0",
|
"gulp-uglify": "1.4.*",
|
||||||
"gulp-util": "^3.0.6",
|
"gulp-util": "^3.0.6",
|
||||||
"gulp-watch": "^4.3.5",
|
"gulp-watch": "^4.3.5",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
@@ -76,7 +75,5 @@
|
|||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0"
|
"vinyl-source-stream": "^1.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
"debug": "^2.6.3"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
281
src/Connector.js
281
src/Connector.js
@@ -1,5 +1,5 @@
|
|||||||
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
/* @flow */
|
||||||
function canWrite (auth) { return auth === 'write' }
|
'use strict'
|
||||||
|
|
||||||
module.exports = function (Y/* :any */) {
|
module.exports = function (Y/* :any */) {
|
||||||
class AbstractConnector {
|
class AbstractConnector {
|
||||||
@@ -14,6 +14,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
syncingClients: Array<UserId>;
|
syncingClients: Array<UserId>;
|
||||||
forwardToSyncingClients: boolean;
|
forwardToSyncingClients: boolean;
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
|
broadcastedHB: boolean;
|
||||||
syncStep2: Promise;
|
syncStep2: Promise;
|
||||||
userId: UserId;
|
userId: UserId;
|
||||||
send: Function;
|
send: Function;
|
||||||
@@ -32,10 +33,6 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (opts == null) {
|
if (opts == null) {
|
||||||
opts = {}
|
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') {
|
if (opts.role == null || opts.role === 'master') {
|
||||||
this.role = 'master'
|
this.role = 'master'
|
||||||
} else if (opts.role === 'slave') {
|
} else if (opts.role === 'slave') {
|
||||||
@@ -43,8 +40,6 @@ module.exports = function (Y/* :any */) {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error("Role must be either 'master' or 'slave'!")
|
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.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
|
||||||
this.role = opts.role
|
this.role = opts.role
|
||||||
this.connections = {}
|
this.connections = {}
|
||||||
@@ -55,50 +50,34 @@ module.exports = function (Y/* :any */) {
|
|||||||
this.syncingClients = []
|
this.syncingClients = []
|
||||||
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
||||||
this.debug = opts.debug === true
|
this.debug = opts.debug === true
|
||||||
|
this.broadcastedHB = false
|
||||||
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
|
|
||||||
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 () {
|
reconnect () {
|
||||||
this.log('reconnecting..')
|
|
||||||
return this.y.db.startGarbageCollector()
|
|
||||||
}
|
}
|
||||||
disconnect () {
|
disconnect () {
|
||||||
this.log('discronnecting..')
|
|
||||||
this.connections = {}
|
this.connections = {}
|
||||||
this.isSynced = false
|
this.isSynced = false
|
||||||
this.currentSyncTarget = null
|
this.currentSyncTarget = null
|
||||||
|
this.broadcastedHB = false
|
||||||
this.syncingClients = []
|
this.syncingClients = []
|
||||||
this.whenSyncedListeners = []
|
this.whenSyncedListeners = []
|
||||||
this.y.db.stopGarbageCollector()
|
return this.y.db.stopGarbageCollector()
|
||||||
return this.y.db.whenTransactionsFinished()
|
|
||||||
}
|
}
|
||||||
repair () {
|
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')
|
console.info('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) {
|
for (var name in this.connections) {
|
||||||
this.connections[name].isSynced = false
|
this.connections[name].isSynced = false
|
||||||
}
|
}
|
||||||
this.isSynced = false
|
this.isSynced = false
|
||||||
this.currentSyncTarget = null
|
this.currentSyncTarget = null
|
||||||
|
this.broadcastedHB = false
|
||||||
this.findNextSyncTarget()
|
this.findNextSyncTarget()
|
||||||
}
|
}
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
if (this.userId == null) {
|
if (this.userId == null) {
|
||||||
this.log('Set userId to "%s"', userId)
|
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
return this.y.db.setUserId(userId)
|
return this.y.db.setUserId(userId)
|
||||||
} else {
|
} else {
|
||||||
@@ -108,12 +87,8 @@ 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) {
|
||||||
this.log('User left: %s', user)
|
|
||||||
delete this.connections[user]
|
delete this.connections[user]
|
||||||
if (user === this.currentSyncTarget) {
|
if (user === this.currentSyncTarget) {
|
||||||
this.currentSyncTarget = null
|
this.currentSyncTarget = null
|
||||||
@@ -137,7 +112,6 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (this.connections[user] != null) {
|
if (this.connections[user] != null) {
|
||||||
throw new Error('This user already joined!')
|
throw new Error('This user already joined!')
|
||||||
}
|
}
|
||||||
this.log('User joined: %s', user)
|
|
||||||
this.connections[user] = {
|
this.connections[user] = {
|
||||||
isSynced: false,
|
isSynced: false,
|
||||||
role: role
|
role: role
|
||||||
@@ -162,8 +136,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
this.whenSyncedListeners.push(f)
|
this.whenSyncedListeners.push(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
|
||||||
|
returns false, if there is no sync target
|
||||||
|
true otherwise
|
||||||
|
*/
|
||||||
findNextSyncTarget () {
|
findNextSyncTarget () {
|
||||||
if (this.currentSyncTarget != null) {
|
if (this.currentSyncTarget != null || this.isSynced) {
|
||||||
return // "The current sync has not finished!"
|
return // "The current sync has not finished!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,39 +159,30 @@ module.exports = function (Y/* :any */) {
|
|||||||
this.y.db.requestTransaction(function *() {
|
this.y.db.requestTransaction(function *() {
|
||||||
var stateSet = yield* this.getStateSet()
|
var stateSet = yield* this.getStateSet()
|
||||||
var deleteSet = yield* this.getDeleteSet()
|
var deleteSet = yield* this.getDeleteSet()
|
||||||
var answer = {
|
conn.send(syncUser, {
|
||||||
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
|
})
|
||||||
}
|
|
||||||
conn.send(syncUser, answer)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (!conn.isSynced) {
|
this.y.db.requestTransaction(function *() {
|
||||||
this.y.db.requestTransaction(function *() {
|
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||||
if (!conn.isSynced) {
|
conn.isSynced = true
|
||||||
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
yield* this.garbageCollectAfterSync()
|
||||||
conn.isSynced = true
|
// call whensynced listeners
|
||||||
yield* this.garbageCollectAfterSync()
|
for (var f of conn.whenSyncedListeners) {
|
||||||
// call whensynced listeners
|
f()
|
||||||
for (var f of conn.whenSyncedListeners) {
|
}
|
||||||
f()
|
conn.whenSyncedListeners = []
|
||||||
}
|
})
|
||||||
conn.whenSyncedListeners = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
send (uid, message) {
|
send (uid, message) {
|
||||||
this.log('Send \'%s\' to %s', message.type, uid)
|
if (this.debug) {
|
||||||
this.logMessage('Message: %j', message)
|
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, message) // eslint-disable-line
|
||||||
}
|
}
|
||||||
broadcast (message) {
|
|
||||||
this.log('Broadcast \'%s\'', message.type)
|
|
||||||
this.logMessage('Message: %j', message)
|
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Buffer operations, and broadcast them when ready.
|
Buffer operations, and broadcast them when ready.
|
||||||
@@ -247,12 +217,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
||||||
if (sender === this.userId) {
|
if (sender === this.userId) {
|
||||||
return Promise.resolve()
|
return
|
||||||
|
}
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line
|
||||||
}
|
}
|
||||||
this.log('Receive \'%s\' from %s', message.type, sender)
|
|
||||||
this.logMessage('Message: %j', message)
|
|
||||||
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
||||||
this.log(
|
console.error(
|
||||||
`You tried to sync with a yjs instance that has a different protocol version
|
`You tried to sync with a yjs instance that has a different protocol version
|
||||||
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
||||||
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
||||||
@@ -261,123 +232,91 @@ module.exports = function (Y/* :any */) {
|
|||||||
type: 'sync stop',
|
type: 'sync stop',
|
||||||
protocolVersion: this.protocolVersion
|
protocolVersion: this.protocolVersion
|
||||||
})
|
})
|
||||||
return Promise.reject('Incompatible protocol version')
|
return
|
||||||
}
|
}
|
||||||
if (message.auth != null && this.connections[sender] != null) {
|
if (message.type === 'sync step 1') {
|
||||||
// authenticate using auth in message
|
let conn = this
|
||||||
var auth = this.checkAuth(message.auth, this.y, sender)
|
let m = message
|
||||||
this.connections[sender].auth = auth
|
this.y.db.requestTransaction(function *() {
|
||||||
auth.then(auth => {
|
var currentStateSet = yield* this.getStateSet()
|
||||||
for (var f of this.userEventListeners) {
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
f({
|
|
||||||
action: 'userAuthenticated',
|
var ds = yield* this.getDeleteSet()
|
||||||
user: sender,
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
auth: auth
|
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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
conn._setSyncedWith(sender)
|
||||||
})
|
})
|
||||||
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
|
} else if (message.type === 'sync step 2') {
|
||||||
// authenticate without otherwise
|
let conn = this
|
||||||
this.connections[sender].auth = this.checkAuth(null, this.y, sender)
|
var broadcastHB = !this.broadcastedHB
|
||||||
}
|
this.broadcastedHB = true
|
||||||
if (this.connections[sender] != null && this.connections[sender].auth != null) {
|
var db = this.y.db
|
||||||
return this.connections[sender].auth.then((auth) => {
|
var defer = {}
|
||||||
if (message.type === 'sync step 1' && canRead(auth)) {
|
defer.promise = new Promise(function (resolve) {
|
||||||
let conn = this
|
defer.resolve = resolve
|
||||||
let m = message
|
})
|
||||||
|
this.syncStep2 = defer.promise
|
||||||
this.y.db.requestTransaction(function *() {
|
let m /* :MessageSyncStep2 */ = message
|
||||||
var currentStateSet = yield* this.getStateSet()
|
db.requestTransaction(function * () {
|
||||||
if (canWrite(auth)) {
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
yield* this.applyDeleteSet(m.deleteSet)
|
this.store.apply(m.os)
|
||||||
}
|
db.requestTransaction(function * () {
|
||||||
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
var ds = yield* this.getDeleteSet()
|
if (ops.length > 0) {
|
||||||
var answer = {
|
if (!broadcastHB) { // TODO: consider to broadcast here..
|
||||||
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 {
|
|
||||||
conn.send(sender, {
|
conn.send(sender, {
|
||||||
type: 'sync done'
|
type: 'update',
|
||||||
|
ops: ops
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
} 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 {
|
} else {
|
||||||
this.store.apply(m.os)
|
// broadcast only once!
|
||||||
}
|
conn.broadcastOps(ops)
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.y.db.forwardAppliedOperations) {
|
defer.resolve()
|
||||||
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 {
|
} else if (message.type === 'sync done') {
|
||||||
return Promise.reject('Unable to deliver message')
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_setSyncedWith (user) {
|
_setSyncedWith (user) {
|
||||||
|
|||||||
@@ -24,21 +24,11 @@ module.exports = function (Y) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
whenTransactionsFinished: function () {
|
whenTransactionsFinished: function () {
|
||||||
var self = this
|
var ps = []
|
||||||
return new Promise(function (resolve, reject) {
|
for (var name in this.users) {
|
||||||
// The connector first has to send the messages to the db.
|
ps.push(this.users[name].y.db.whenTransactionsFinished())
|
||||||
// Wait for the checkAuth-function to resolve
|
}
|
||||||
// The test lib only has a simple checkAuth function: `() => Promise.resolve()`
|
return Promise.all(ps)
|
||||||
// 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 () {
|
flushOne: function flushOne () {
|
||||||
var bufs = []
|
var bufs = []
|
||||||
@@ -64,9 +54,8 @@ module.exports = function (Y) {
|
|||||||
delete buff[sender]
|
delete buff[sender]
|
||||||
}
|
}
|
||||||
var user = globalRoom.users[userId]
|
var user = globalRoom.users[userId]
|
||||||
return user.receiveMessage(m[0], m[1]).then(function () {
|
user.receiveMessage(m[0], m[1])
|
||||||
return user.y.db.whenTransactionsFinished()
|
return user.y.db.whenTransactionsFinished()
|
||||||
}, function () {})
|
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -83,14 +72,16 @@ module.exports = function (Y) {
|
|||||||
}
|
}
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
} else {
|
} else {
|
||||||
c = globalRoom.flushOne()
|
setTimeout(function () {
|
||||||
if (c) {
|
var c = globalRoom.flushOne()
|
||||||
c.then(function () {
|
if (c) {
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
c.then(function () {
|
||||||
})
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
} else {
|
})
|
||||||
resolve()
|
} else {
|
||||||
}
|
resolve()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
@@ -116,7 +107,7 @@ module.exports = function (Y) {
|
|||||||
this.syncingClientDuration = 0
|
this.syncingClientDuration = 0
|
||||||
}
|
}
|
||||||
receiveMessage (sender, m) {
|
receiveMessage (sender, m) {
|
||||||
return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
||||||
}
|
}
|
||||||
send (userId, message) {
|
send (userId, message) {
|
||||||
var buffer = globalRoom.buffers[userId]
|
var buffer = globalRoom.buffers[userId]
|
||||||
@@ -147,15 +138,11 @@ module.exports = function (Y) {
|
|||||||
return Y.utils.globalRoom.flushAll()
|
return Y.utils.globalRoom.flushAll()
|
||||||
}
|
}
|
||||||
disconnect () {
|
disconnect () {
|
||||||
var waitForMe = Promise.resolve()
|
|
||||||
if (!this.isDisconnected()) {
|
if (!this.isDisconnected()) {
|
||||||
globalRoom.removeUser(this.userId)
|
globalRoom.removeUser(this.userId)
|
||||||
waitForMe = super.disconnect()
|
super.disconnect()
|
||||||
}
|
}
|
||||||
var self = this
|
return this.y.db.whenTransactionsFinished()
|
||||||
return waitForMe.then(function () {
|
|
||||||
return self.y.db.whenTransactionsFinished()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
flush () {
|
flush () {
|
||||||
var self = this
|
var self = this
|
||||||
@@ -167,7 +154,7 @@ module.exports = function (Y) {
|
|||||||
if (buff[sender].length === 0) {
|
if (buff[sender].length === 0) {
|
||||||
delete buff[sender]
|
delete buff[sender]
|
||||||
}
|
}
|
||||||
yield this.receiveMessage(m[0], m[1])
|
this.receiveMessage(m[0], m[1])
|
||||||
}
|
}
|
||||||
yield self.whenTransactionsFinished()
|
yield self.whenTransactionsFinished()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ module.exports = function (Y /* :any */) {
|
|||||||
*/
|
*/
|
||||||
constructor (y, opts) {
|
constructor (y, opts) {
|
||||||
this.y = y
|
this.y = y
|
||||||
this.dbOpts = opts
|
|
||||||
var os = this
|
var os = this
|
||||||
this.userId = null
|
this.userId = null
|
||||||
var resolve
|
var resolve
|
||||||
@@ -76,12 +75,12 @@ module.exports = function (Y /* :any */) {
|
|||||||
}
|
}
|
||||||
this.gc1 = [] // first stage
|
this.gc1 = [] // first stage
|
||||||
this.gc2 = [] // second stage -> after that, remove the op
|
this.gc2 = [] // second stage -> after that, remove the op
|
||||||
|
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeouts
|
||||||
function garbageCollect () {
|
function garbageCollect () {
|
||||||
return os.whenTransactionsFinished().then(function () {
|
return os.whenTransactionsFinished().then(function () {
|
||||||
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
||||||
if (!os.y.connector.isSynced) {
|
if (!os.y.isConnected()) {
|
||||||
console.warn('gc should be empty when not synced!')
|
console.warn('gc should be empty when disconnected!')
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
os.requestTransaction(function * () {
|
os.requestTransaction(function * () {
|
||||||
@@ -110,23 +109,13 @@ module.exports = function (Y /* :any */) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.garbageCollect = garbageCollect
|
this.garbageCollect = garbageCollect
|
||||||
this.startGarbageCollector()
|
if (this.gcTimeout > 0) {
|
||||||
|
garbageCollect()
|
||||||
|
}
|
||||||
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
|
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
|
||||||
this.opsReceivedTimestamp = new Date()
|
this.opsReceivedTimestamp = new Date()
|
||||||
this.startRepairCheck()
|
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) {
|
|
||||||
this.garbageCollect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startRepairCheck () {
|
startRepairCheck () {
|
||||||
var os = this
|
var os = this
|
||||||
if (this.repairCheckInterval > 0) {
|
if (this.repairCheckInterval > 0) {
|
||||||
@@ -160,7 +149,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
clearInterval(this.repairCheckIntervalHandler)
|
clearInterval(this.repairCheckIntervalHandler)
|
||||||
}
|
}
|
||||||
queueGarbageCollector (id) {
|
queueGarbageCollector (id) {
|
||||||
if (this.y.connector.isSynced && this.gc) {
|
if (this.y.isConnected()) {
|
||||||
this.gc1.push(id)
|
this.gc1.push(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,8 +182,6 @@ module.exports = function (Y /* :any */) {
|
|||||||
}
|
}
|
||||||
stopGarbageCollector () {
|
stopGarbageCollector () {
|
||||||
var self = this
|
var self = this
|
||||||
this.gc = false
|
|
||||||
this.gcTimeout = -1
|
|
||||||
return new Promise(function (resolve) {
|
return new Promise(function (resolve) {
|
||||||
self.requestTransaction(function * () {
|
self.requestTransaction(function * () {
|
||||||
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
|
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
|
||||||
@@ -217,7 +204,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
TODO: rename this function
|
TODO: rename this function
|
||||||
|
|
||||||
Rulez:
|
Rulez:
|
||||||
* Only gc if this user is online & gc turned on
|
* Only gc if this user is online
|
||||||
* The most left element in a list must not be gc'd.
|
* The most left element in a list must not be gc'd.
|
||||||
=> There is at least one element in the list
|
=> There is at least one element in the list
|
||||||
|
|
||||||
@@ -226,9 +213,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
* addToGarbageCollector (op, left) {
|
* addToGarbageCollector (op, left) {
|
||||||
if (
|
if (
|
||||||
op.gc == null &&
|
op.gc == null &&
|
||||||
op.deleted === true &&
|
op.deleted === true
|
||||||
this.store.gc &&
|
|
||||||
this.store.y.connector.isSynced
|
|
||||||
) {
|
) {
|
||||||
var gc = false
|
var gc = false
|
||||||
if (left != null && left.deleted === true) {
|
if (left != null && left.deleted === true) {
|
||||||
@@ -254,7 +239,10 @@ module.exports = function (Y /* :any */) {
|
|||||||
this.gc2 = this.gc2.filter(filter)
|
this.gc2 = this.gc2.filter(filter)
|
||||||
delete op.gc
|
delete op.gc
|
||||||
}
|
}
|
||||||
destroyTypes () {
|
* destroy () {
|
||||||
|
clearInterval(this.gcInterval)
|
||||||
|
this.gcInterval = null
|
||||||
|
this.stopRepairCheck()
|
||||||
for (var key in this.initializedTypes) {
|
for (var key in this.initializedTypes) {
|
||||||
var type = this.initializedTypes[key]
|
var type = this.initializedTypes[key]
|
||||||
if (type._destroy != null) {
|
if (type._destroy != null) {
|
||||||
@@ -264,11 +252,6 @@ module.exports = function (Y /* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* destroy () {
|
|
||||||
clearInterval(this.gcInterval)
|
|
||||||
this.gcInterval = null
|
|
||||||
this.stopRepairCheck()
|
|
||||||
}
|
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
if (!this.userIdPromise.inProgress) {
|
if (!this.userIdPromise.inProgress) {
|
||||||
this.userIdPromise.inProgress = true
|
this.userIdPromise.inProgress = true
|
||||||
@@ -451,7 +434,8 @@ module.exports = function (Y /* :any */) {
|
|||||||
*/
|
*/
|
||||||
* operationAdded (transaction, op) {
|
* operationAdded (transaction, op) {
|
||||||
if (op.struct === 'Delete') {
|
if (op.struct === 'Delete') {
|
||||||
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
|
var target = yield* transaction.getInsertion(op.target)
|
||||||
|
var type = this.initializedTypes[JSON.stringify(target.parent)]
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
yield* type._changed(transaction, op)
|
yield* type._changed(transaction, op)
|
||||||
}
|
}
|
||||||
@@ -519,8 +503,10 @@ module.exports = function (Y /* :any */) {
|
|||||||
resolve: resolve,
|
resolve: resolve,
|
||||||
promise: promise
|
promise: promise
|
||||||
}
|
}
|
||||||
|
return promise
|
||||||
|
} else {
|
||||||
|
return this.transactionsFinished.promise
|
||||||
}
|
}
|
||||||
return this.transactionsFinished.promise
|
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
@@ -557,48 +543,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ require('../../y-memory/src/Memory.js')(Y)
|
|||||||
require('../../y-array/src/Array.js')(Y)
|
require('../../y-array/src/Array.js')(Y)
|
||||||
require('../../y-map/src/Map.js')(Y)
|
require('../../y-map/src/Map.js')(Y)
|
||||||
require('../../y-indexeddb/src/IndexedDB.js')(Y)
|
require('../../y-indexeddb/src/IndexedDB.js')(Y)
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
require('../../y-leveldb/src/LevelDB.js')(Y)
|
||||||
|
}
|
||||||
module.exports = Y
|
module.exports = Y
|
||||||
|
|
||||||
var g
|
var g
|
||||||
@@ -119,10 +121,6 @@ function * applyTransactions (relAmount, numberOfTransactions, objects, users, t
|
|||||||
// There will be an artificial delay until ops can be executed by the type,
|
// 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
|
// therefore, operations of the database will be (pre)transformed until user operations arrive
|
||||||
yield (function simulateConcurrentUserInteractions (type) {
|
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) {
|
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
|
||||||
type.eventHandler.awaiting = 1
|
type.eventHandler.awaiting = 1
|
||||||
type.eventHandler._debuggingAwaiting = true
|
type.eventHandler._debuggingAwaiting = true
|
||||||
@@ -158,10 +156,6 @@ function * applyTransactions (relAmount, numberOfTransactions, objects, users, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fixAwaitingInType (type) {
|
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) {
|
return new Promise(function (resolve) {
|
||||||
type.os.whenTransactionsFinished().then(function () {
|
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
|
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
|
||||||
@@ -226,20 +220,6 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
|
|||||||
var allDels1, allDels2 // all deletions
|
var allDels1, allDels2 // all deletions
|
||||||
var db1 = [] // operation store of user1
|
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]
|
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
||||||
function * t1 () {
|
function * t1 () {
|
||||||
s1 = yield* this.getStateSet()
|
s1 = yield* this.getStateSet()
|
||||||
@@ -257,7 +237,9 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
|
|||||||
allDels2.push(d)
|
allDels2.push(d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield g.garbageCollectAllUsers(users)
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
var buffer = Y.utils.globalRoom.buffers
|
var buffer = Y.utils.globalRoom.buffers
|
||||||
for (var name in buffer) {
|
for (var name in buffer) {
|
||||||
if (buffer[name].length > 0) {
|
if (buffer[name].length > 0) {
|
||||||
@@ -348,7 +330,6 @@ g.createUsers = async(function * createUsers (self, numberOfUsers, database, ini
|
|||||||
namespace: 'User ' + i,
|
namespace: 'User ' + i,
|
||||||
cleanStart: true,
|
cleanStart: true,
|
||||||
gcTimeout: -1,
|
gcTimeout: -1,
|
||||||
gc: true,
|
|
||||||
repairCheckInterval: -1
|
repairCheckInterval: -1
|
||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
|
|||||||
@@ -30,11 +30,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
Delete: {
|
Delete: {
|
||||||
encode: function (op) {
|
encode: function (op) {
|
||||||
return {
|
return op
|
||||||
target: op.target,
|
|
||||||
length: op.length || 0,
|
|
||||||
struct: 'Delete'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
requiredOps: function (op) {
|
requiredOps: function (op) {
|
||||||
return [] // [op.target]
|
return [] // [op.target]
|
||||||
@@ -269,7 +265,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try to merge original op.left and op.origin
|
// try to merge original op.left and op.origin
|
||||||
for (i = 0; i < tryToRemergeLater.length; i++) {
|
for (let i = 0; i < tryToRemergeLater.length; i++) {
|
||||||
var m = yield* this.getOperation(tryToRemergeLater[i])
|
var m = yield* this.getOperation(tryToRemergeLater[i])
|
||||||
yield* this.tryCombineWithLeft(m)
|
yield* this.tryCombineWithLeft(m)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,57 @@ 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()
|
||||||
@@ -96,7 +147,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
send.push(Y.Struct[op.struct].encode(op))
|
send.push(Y.Struct[op.struct].encode(op))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
if (!this.store.y.connector.isDisconnected() && 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
|
// is connected, and this is not going to be send in addOperation
|
||||||
this.store.y.connector.broadcastOps(send)
|
this.store.y.connector.broadcastOps(send)
|
||||||
}
|
}
|
||||||
@@ -206,8 +257,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
yield* this.store.operationAdded(this, {
|
yield* this.store.operationAdded(this, {
|
||||||
struct: 'Delete',
|
struct: 'Delete',
|
||||||
target: target.id,
|
target: target.id,
|
||||||
length: targetLength,
|
length: targetLength
|
||||||
targetParent: target.parent
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// need to gc in the end!
|
// need to gc in the end!
|
||||||
@@ -369,9 +419,6 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
|
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
|
||||||
console.warn('gc should be empty after sync')
|
console.warn('gc should be empty after sync')
|
||||||
}
|
}
|
||||||
if (!this.store.gc) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
yield* this.os.iterate(this, null, null, function * (op) {
|
yield* this.os.iterate(this, null, null, function * (op) {
|
||||||
if (op.gc) {
|
if (op.gc) {
|
||||||
delete op.gc
|
delete op.gc
|
||||||
@@ -679,7 +726,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
if (this.store.forwardAppliedOperations) {
|
if (this.store.forwardAppliedOperations) {
|
||||||
var ops = []
|
var ops = []
|
||||||
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
ops.push({struct: 'Delete', target: [d[0], d[1]], length: del[2]})
|
||||||
this.store.y.connector.broadcastOps(ops)
|
this.store.y.connector.broadcastOps(ops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -717,7 +764,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
yield* this.os.put(op)
|
yield* this.os.put(op)
|
||||||
if (this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
if (!this.store.y.connector.isDisconnected() && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
||||||
// is connected, and this is not going to be send in addOperation
|
// is connected, and this is not going to be send in addOperation
|
||||||
this.store.y.connector.broadcastOps([op])
|
this.store.y.connector.broadcastOps([op])
|
||||||
}
|
}
|
||||||
@@ -1013,56 +1060,6 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
return send.reverse()
|
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..
|
/* this is what we used before.. use this as a reference..
|
||||||
* makeOperationReady (startSS, op) {
|
* makeOperationReady (startSS, op) {
|
||||||
op = Y.Struct[op.struct].encode(op)
|
op = Y.Struct[op.struct].encode(op)
|
||||||
|
|||||||
81
src/Utils.js
81
src/Utils.js
@@ -26,24 +26,6 @@
|
|||||||
module.exports = function (Y /* : any*/) {
|
module.exports = function (Y /* : any*/) {
|
||||||
Y.utils = {}
|
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 {
|
class EventListenerHandler {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.eventListeners = []
|
this.eventListeners = []
|
||||||
@@ -68,13 +50,9 @@ module.exports = function (Y /* : any*/) {
|
|||||||
callEventListeners (event) {
|
callEventListeners (event) {
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||||
try {
|
try {
|
||||||
var _event = {}
|
this.eventListeners[i](event)
|
||||||
for (var name in event) {
|
|
||||||
_event[name] = event[name]
|
|
||||||
}
|
|
||||||
this.eventListeners[i](_event)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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)
|
console.error('User events must not throw Errors!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +82,7 @@ module.exports = function (Y /* : any*/) {
|
|||||||
destroy () {
|
destroy () {
|
||||||
super.destroy()
|
super.destroy()
|
||||||
this.waiting = null
|
this.waiting = null
|
||||||
|
this.awaiting = null
|
||||||
this.onevent = null
|
this.onevent = null
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -259,13 +238,7 @@ module.exports = function (Y /* : any*/) {
|
|||||||
// finished with remaining operations
|
// finished with remaining operations
|
||||||
self.waiting.push(d)
|
self.waiting.push(d)
|
||||||
}
|
}
|
||||||
if (op.key == null) {
|
checkDelete(op)
|
||||||
// deletes in list
|
|
||||||
checkDelete(op)
|
|
||||||
} else {
|
|
||||||
// deletes in map
|
|
||||||
this.waiting.push(op)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.waiting.push(op)
|
this.waiting.push(op)
|
||||||
}
|
}
|
||||||
@@ -314,11 +287,7 @@ 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 (_o.parentSub != null && _o.left != null) {
|
if (!Y.utils.compareIds(_o.id, o.id)) {
|
||||||
// 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) {
|
||||||
@@ -478,27 +447,6 @@ module.exports = function (Y /* : any*/) {
|
|||||||
}
|
}
|
||||||
Y.utils.EventHandler = EventHandler
|
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.
|
A wrapper for the definition of a custom type.
|
||||||
Every custom type must have three properties:
|
Every custom type must have three properties:
|
||||||
@@ -510,7 +458,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 CustomTypeDefinition { // eslint-disable-line
|
class CustomType { // eslint-disable-line
|
||||||
/* ::
|
/* ::
|
||||||
struct: any;
|
struct: any;
|
||||||
initType: any;
|
initType: any;
|
||||||
@@ -521,14 +469,12 @@ 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) {
|
||||||
@@ -540,13 +486,13 @@ module.exports = function (Y /* : any*/) {
|
|||||||
this.parseArguments.typeDefinition = this
|
this.parseArguments.typeDefinition = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Y.utils.CustomTypeDefinition = CustomTypeDefinition
|
Y.utils.CustomType = CustomType
|
||||||
|
|
||||||
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
|
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
if (v instanceof Y.utils.CustomTypeDefinition) return [v]
|
if (v instanceof Y.utils.CustomType) return [v]
|
||||||
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomTypeDefinition) 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.CustomTypeDefinition) return [v.typeDefinition]
|
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomType) return [v.typeDefinition]
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -787,9 +733,4 @@ module.exports = function (Y /* : any*/) {
|
|||||||
return SmallLookupBuffer
|
return SmallLookupBuffer
|
||||||
}
|
}
|
||||||
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
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
129
src/y.js
@@ -8,44 +8,25 @@ require('./Struct.js')(Y)
|
|||||||
require('./Utils.js')(Y)
|
require('./Utils.js')(Y)
|
||||||
require('./Connectors/Test.js')(Y)
|
require('./Connectors/Test.js')(Y)
|
||||||
|
|
||||||
Y.debug = require('debug')
|
|
||||||
|
|
||||||
var requiringModules = {}
|
var requiringModules = {}
|
||||||
|
|
||||||
module.exports = Y
|
module.exports = Y
|
||||||
Y.requiringModules = requiringModules
|
Y.requiringModules = requiringModules
|
||||||
|
|
||||||
Y.extend = function (name, value) {
|
Y.extend = function (name, value) {
|
||||||
if (arguments.length === 2 && typeof name === 'string') {
|
if (value instanceof Y.utils.CustomType) {
|
||||||
if (value instanceof Y.utils.CustomTypeDefinition) {
|
Y[name] = value.parseArguments
|
||||||
Y[name] = value.parseArguments
|
|
||||||
} else {
|
|
||||||
Y[name] = value
|
|
||||||
}
|
|
||||||
if (requiringModules[name] != null) {
|
|
||||||
requiringModules[name].resolve()
|
|
||||||
delete requiringModules[name]
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for (var i = 0; i < arguments.length; i++) {
|
Y[name] = value
|
||||||
var f = arguments[i]
|
}
|
||||||
if (typeof f === 'function') {
|
if (requiringModules[name] != null) {
|
||||||
f(Y)
|
requiringModules[name].resolve()
|
||||||
} else {
|
delete requiringModules[name]
|
||||||
throw new Error('Expected function!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Y.requestModules = requestModules
|
Y.requestModules = requestModules
|
||||||
function requestModules (modules) {
|
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)
|
// 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..
|
||||||
@@ -58,11 +39,10 @@ function requestModules (modules) {
|
|||||||
if (requiringModules[module] == null) {
|
if (requiringModules[module] == null) {
|
||||||
// module does not exist
|
// module does not exist
|
||||||
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
|
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
|
||||||
if (sourceDir != null) {
|
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 = {}
|
||||||
requiringModules[module] = requireModule
|
requiringModules[module] = requireModule
|
||||||
requireModule.promise = new Promise(function (resolve) {
|
requireModule.promise = new Promise(function (resolve) {
|
||||||
@@ -111,36 +91,31 @@ type YOptions = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||||
if (opts.hasOwnProperty('sourceDir')) {
|
|
||||||
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) {
|
||||||
if (opts == null) reject('An options object is expected! ')
|
setTimeout(function () {
|
||||||
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
Y.requestModules(modules).then(function () {
|
||||||
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
if (opts == null) reject('An options object is expected! ')
|
||||||
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
||||||
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
||||||
else {
|
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
||||||
opts = Y.utils.copyObject(opts)
|
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
||||||
opts.connector = Y.utils.copyObject(opts.connector)
|
else if (opts.share == null) reject('You must specify a set of shared types!')
|
||||||
opts.db = Y.utils.copyObject(opts.db)
|
else {
|
||||||
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)
|
}
|
||||||
}, 0)
|
}).catch(reject)
|
||||||
}
|
}, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +130,6 @@ class YConfig {
|
|||||||
this.options = opts
|
this.options = opts
|
||||||
this.db = new Y[opts.db.name](this, opts.db)
|
this.db = new Y[opts.db.name](this, opts.db)
|
||||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
this.connector = new Y[opts.connector.name](this, opts.connector)
|
||||||
this.connected = true
|
|
||||||
}
|
}
|
||||||
init (callback) {
|
init (callback) {
|
||||||
var opts = this.options
|
var opts = this.options
|
||||||
@@ -166,9 +140,6 @@ 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 {
|
||||||
@@ -176,13 +147,11 @@ 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]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
share[propertyname] = yield* this.store.initType.call(this, id, args)
|
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)
|
||||||
}
|
}
|
||||||
this.store.whenTransactionsFinished()
|
this.store.whenTransactionsFinished()
|
||||||
.then(callback)
|
.then(callback)
|
||||||
@@ -192,46 +161,26 @@ class YConfig {
|
|||||||
return this.connector.isSynced
|
return this.connector.isSynced
|
||||||
}
|
}
|
||||||
disconnect () {
|
disconnect () {
|
||||||
if (this.connected) {
|
return this.connector.disconnect()
|
||||||
this.connected = false
|
|
||||||
return this.connector.disconnect()
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
reconnect () {
|
reconnect () {
|
||||||
if (!this.connected) {
|
return this.connector.reconnect()
|
||||||
this.connected = true
|
|
||||||
return this.connector.reconnect()
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
destroy () {
|
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) {
|
if (this.connector.destroy != null) {
|
||||||
this.connector.destroy()
|
this.connector.destroy()
|
||||||
} else {
|
} else {
|
||||||
this.connector.disconnect()
|
this.connector.disconnect()
|
||||||
}
|
}
|
||||||
return this.db.whenTransactionsFinished(function () {
|
var self = this
|
||||||
this.db.destroyTypes()
|
this.db.requestTransaction(function * () {
|
||||||
// make sure to wait for all transactions before destroying the db
|
yield* self.db.destroy()
|
||||||
this.db.requestTransaction(function * () {
|
self.connector = null
|
||||||
yield* self.db.destroy()
|
self.db = null
|
||||||
})
|
|
||||||
return this.db.whenTransactionsFinished()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.Y = Y
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user