Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8bca15d72 | ||
|
|
a64730e651 | ||
|
|
409a9414f1 | ||
|
|
24facaab09 | ||
|
|
060549f2cb | ||
|
|
dfe3b0b1d1 | ||
|
|
a5506a5ded | ||
|
|
361d4a48e1 | ||
|
|
e23154bec2 | ||
|
|
1682d43c26 | ||
|
|
68c417fe6f | ||
|
|
2ea163a5cf | ||
|
|
020dacdad4 | ||
|
|
42abcc897c | ||
|
|
0a321610aa | ||
|
|
edf47d3491 | ||
|
|
14ee42cad5 | ||
|
|
f990927d3e | ||
|
|
a1cef4662f | ||
|
|
2c343970c4 | ||
|
|
74b41e03e3 | ||
|
|
b242aab955 | ||
|
|
8e4efd9bba | ||
|
|
47d5899058 | ||
|
|
a126a29876 | ||
|
|
4aa720116f | ||
|
|
e29162c3fc | ||
|
|
aa40855953 | ||
|
|
b6545d62fc | ||
|
|
3425d95507 | ||
|
|
53682c17fb | ||
|
|
a492a83f0c | ||
|
|
d340e557c1 | ||
|
|
d5cd9d94d5 | ||
|
|
e1a160b894 | ||
|
|
f996ac83d2 | ||
|
|
922637930f | ||
|
|
ff7e9cdef2 | ||
|
|
f02641deb7 | ||
|
|
f97144356c | ||
|
|
a9fdd5df66 | ||
|
|
e90f241ae0 | ||
|
|
102bef4f92 | ||
|
|
96e9c3c166 | ||
|
|
1080f83990 | ||
|
|
66b6b2a568 | ||
|
|
7415f27fbc | ||
|
|
c9d1f34864 | ||
|
|
34997f940b | ||
|
|
4e9e21e75e | ||
|
|
6c375a37c8 | ||
|
|
cd0cddaf35 | ||
|
|
93c23ddc09 | ||
|
|
480dfdfb77 | ||
|
|
dda2a1ef82 | ||
|
|
f32ff1b613 | ||
|
|
8ab16f4ada | ||
|
|
3fdcf82bcc | ||
|
|
6dd33f4f90 | ||
|
|
0521fac8d8 | ||
|
|
666ab8285c | ||
|
|
675c7f6638 | ||
|
|
463608cb5c | ||
|
|
d1059b5d04 | ||
|
|
8b24284e25 | ||
|
|
08bcdfb008 | ||
|
|
f93d7b1e70 | ||
|
|
4d024883bc | ||
|
|
ecd412c6f6 | ||
|
|
b939cdd086 | ||
|
|
17803266d4 | ||
|
|
f0e88d192c | ||
|
|
e66c0f8a4e | ||
|
|
eba3d590cc | ||
|
|
0b31e63b82 | ||
|
|
d22fbca6cc | ||
|
|
330434ee24 | ||
|
|
2f0216bf89 | ||
|
|
f9d0625bd2 | ||
|
|
7a9d60770a | ||
|
|
059f72ffe1 | ||
|
|
d2d74a64ab | ||
|
|
a1f0140069 | ||
|
|
7bd8e81342 | ||
|
|
34f365cd8f | ||
|
|
b3ba8e7546 | ||
|
|
e1e94bcf5d | ||
|
|
4a83ff8514 |
12
.babelrc
Normal file
12
.babelrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["latest", {
|
||||||
|
"es2015": {
|
||||||
|
"modules": false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"external-helpers"
|
||||||
|
]
|
||||||
|
}
|
||||||
328
README.md
328
README.md
@@ -1,133 +1,215 @@
|
|||||||
|
|
||||||
# 
|
# 
|
||||||
|
|
||||||
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data.
|
Yjs is a framework for offline-first p2p shared editing on structured data like
|
||||||
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer
|
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
||||||
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data
|
most of the complexity of concurrent editing. For additional information, demos,
|
||||||
like Text, Json, and XML. We also provide support for storing and manipulating your shared data offline.
|
and tutorials visit [y-js.org](http://y-js.org/).
|
||||||
For more information and demo applications visit our [homepage](http://y-js.org/).
|
|
||||||
|
|
||||||
You can create you own shared types easily.
|
### Extensions
|
||||||
Therefore, you can design the structure of your custom type,
|
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
|
||||||
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data).
|
* *Connector* - a communication protocol that propagates changes to the clients
|
||||||
We already provide abstract data types for
|
* *Database* - a database to store your changes
|
||||||
|
* one or more *Types* - that represent the shared data
|
||||||
|
|
||||||
|
Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
||||||
|
is a list of the modules we know of:
|
||||||
|
|
||||||
|
##### Connectors
|
||||||
|
|
||||||
|
|Name | Description |
|
||||||
|
|----------------|-----------------------------------|
|
||||||
|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
||||||
|
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
||||||
|
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
||||||
|
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
||||||
|
|
||||||
|
##### Database adapters
|
||||||
|
|
||||||
|
|Name | Description |
|
||||||
|
|----------------|-----------------------------------|
|
||||||
|
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|
||||||
|
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|
||||||
|
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
|
||||||
|
|
||||||
|
|
||||||
|
##### Types
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|----------|-------------------|
|
|----------|-------------------|
|
||||||
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|
||||||
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|
||||||
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|
||||||
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to textareas, input elements, or HTML elements (e.g. <*h1*>, or <*p*>). Also supports the [Ace Editor](https://ace.c9.io) |
|
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) |
|
||||||
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
||||||
|
|
||||||
Yjs supports P2P message propagation, and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
|
##### Other
|
||||||
|
|
||||||
We support several communication protocols as so called *Connectors*.
|
| Name | Description |
|
||||||
You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors).
|
|-----------|-------------------|
|
||||||
Currently, we support the following communication protocols:
|
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
|
||||||
|
|
||||||
|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 with [npm](https://www.npmjs.org/package/yjs).
|
Install Yjs, and its modules with [bower](http://bower.io/), or
|
||||||
|
[npm](https://www.npmjs.org/package/yjs).
|
||||||
|
|
||||||
### Bower
|
### Bower
|
||||||
```
|
```
|
||||||
bower install yjs --save
|
bower install --save yjs y-array % add all y-* modules you want to use
|
||||||
```
|
```
|
||||||
Then you include the libraries directly from the installation folder.
|
You only need to include the `y.js` file. Yjs is able to automatically require
|
||||||
|
missing modules.
|
||||||
```
|
```
|
||||||
<script src="./bower_components/yjs/y.js"></script>
|
<script src="./bower_components/yjs/y.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Npm
|
### Npm
|
||||||
```
|
```
|
||||||
npm install yjs --save
|
npm install --save yjs % add all y-* modules you want to use
|
||||||
```
|
```
|
||||||
|
|
||||||
And use it like this with *npm*:
|
If you don't include via script tag, you have to explicitly include all modules!
|
||||||
|
(Same goes for other module systems)
|
||||||
```
|
```
|
||||||
Y = require("yjs");
|
var Y = require('yjs')
|
||||||
|
require('y-array')(Y) // add the y-array type to Yjs
|
||||||
|
require('y-websockets-client')(Y)
|
||||||
|
require('y-memory')(Y)
|
||||||
|
require('y-array')(Y)
|
||||||
|
require('y-map')(Y)
|
||||||
|
require('y-text')(Y)
|
||||||
|
// ..
|
||||||
|
// do the same for all modules you want to use
|
||||||
|
```
|
||||||
|
|
||||||
|
### ES6 Syntax
|
||||||
|
```
|
||||||
|
import Y from 'yjs'
|
||||||
|
import yArray from 'y-array'
|
||||||
|
import yWebsocketsClient from 'y-webrtc'
|
||||||
|
import yMemory from 'y-memory'
|
||||||
|
import yArray from 'y-array'
|
||||||
|
import yMap from 'y-map'
|
||||||
|
import yText from 'y-text'
|
||||||
|
// ..
|
||||||
|
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
|
||||||
```
|
```
|
||||||
|
|
||||||
# Text editing example
|
# Text editing example
|
||||||
|
Install dependencies
|
||||||
```
|
```
|
||||||
Y({
|
bower i yjs y-memory y-webrtc y-array y-text
|
||||||
db: {
|
|
||||||
name: 'memory' // store in memory.
|
|
||||||
// name: 'indexeddb'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client', // choose the websockets connector
|
|
||||||
// name: 'webrtc'
|
|
||||||
// name: 'xmpp'
|
|
||||||
room: 'Textarea-example-dev'
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components', // location of the y-* modules
|
|
||||||
share: {
|
|
||||||
textarea: 'Text' // y.share.textarea is of type Y.Text
|
|
||||||
}
|
|
||||||
// types: ['Richtext', 'Array'] // optional list of types you want to import
|
|
||||||
}).then(function (y) {
|
|
||||||
// bind the textarea to a shared text element
|
|
||||||
y.share.textarea.bind(document.getElementById('textfield'))
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Api
|
Here is a simple example of a shared textarea
|
||||||
|
```HTML
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script src="./bower_components/yjs/y.js"></script>
|
||||||
|
<!-- Yjs automatically includes all missing dependencies (browser only) -->
|
||||||
|
<script>
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory' // use memory database adapter.
|
||||||
|
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'webrtc', // use webrtc connector
|
||||||
|
// name: 'websockets-client'
|
||||||
|
// name: 'xmpp'
|
||||||
|
room: 'my-room' // clients connecting to the same room share data
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components', // location of the y-* modules (browser only)
|
||||||
|
share: {
|
||||||
|
textarea: 'Text' // y.share.textarea is of type y-text
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
// The Yjs instance `y` is available
|
||||||
|
// y.share.* contains the shared types
|
||||||
|
|
||||||
|
// Bind `y.share.textarea` to `<textarea/>`
|
||||||
|
y.share.textarea.bind(document.querySelector('textarea'))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<textarea></textarea>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get Help & Give Help
|
||||||
|
There are some friendly people on [](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
|
||||||
|
|
||||||
|
Report _any_ issues to the
|
||||||
|
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
|
||||||
|
soon, if possible.
|
||||||
|
|
||||||
|
# API
|
||||||
|
|
||||||
### Y(options)
|
### Y(options)
|
||||||
|
* Y.extend(module1, module2, ..)
|
||||||
|
* Add extensions to Y
|
||||||
|
* `Y.extend(require('y-webrtc'))` has the same semantics as
|
||||||
|
`require('y-webrtc')(Y)`
|
||||||
* options.db
|
* options.db
|
||||||
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`.
|
* Will be forwarded to the database adapter. Specify the database adaper on
|
||||||
* Have a look at the used database adapter repository to see all available options.
|
`options.db.name`.
|
||||||
|
* 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 `options.connector.name`.
|
* Will be forwarded to the connector adapter. Specify the connector adaper on
|
||||||
* All our connectors implement a `room` property. Clients that specify the same room share the same data.
|
`options.connector.name`.
|
||||||
* All of our connectors specify an `url` property that defines the connection endpoint of the used connector.
|
* All our connectors implement a `room` property. Clients that specify the
|
||||||
* All of our connectors also have a default connection endpoint that you can use for development.
|
same room share the same data.
|
||||||
|
* All of our connectors specify an `url` property that defines the connection
|
||||||
|
endpoint of the used connector.
|
||||||
|
* All of our connectors also have a default connection endpoint that you can
|
||||||
|
use for development.
|
||||||
|
* Set `options.connector.generateUserId = true` in order to genenerate a
|
||||||
|
userid, instead of receiving one from the server. This way the `Y(..)` is
|
||||||
|
immediately going to be resolved, without waiting for any confirmation from
|
||||||
|
the server. Use with caution.
|
||||||
* Have a look at the used connector repository to see all available options.
|
* Have a look at the used connector repository to see all available options.
|
||||||
* options.sourceDir
|
* *Only if you know what you are doing:* Set
|
||||||
* Path where all y-* modules are stored.
|
`options.connector.preferUntransformed = true` in order receive the shared
|
||||||
|
data untransformed. This is very efficient as the database content is simply
|
||||||
|
copied to this client. This does only work if this client receives content
|
||||||
|
from only one client.
|
||||||
|
* options.sourceDir (browser only)
|
||||||
|
* Path where all y-* modules are stored
|
||||||
* Defaults to `/bower_components`
|
* Defaults to `/bower_components`
|
||||||
* Not required when running on `nodejs` / `iojs`
|
* Not required when running on `nodejs` / `iojs`
|
||||||
* When using browserify you can specify all used modules like this:
|
* When using nodejs you need to manually extend Yjs:
|
||||||
```
|
```
|
||||||
var Y = require('yjs')
|
var Y = require('yjs')
|
||||||
// you need to require the db, connector, and *all* types you use!
|
// you have to require a db, connector, and *all* types you use!
|
||||||
require('y-memory')(Y)
|
require('y-memory')(Y)
|
||||||
require('y-webrtc')(Y)
|
require('y-webrtc')(Y)
|
||||||
require('y-map')(Y)
|
require('y-map')(Y)
|
||||||
// ..
|
// ..
|
||||||
```
|
```
|
||||||
* options.share
|
* options.share
|
||||||
* Specify on `options.share[arbitraryName]` types that are shared among all users.
|
* Specify on `options.share[arbitraryName]` types that are shared among all
|
||||||
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and create an Y.Array type on `y.share[arbitraryName]`.
|
users.
|
||||||
* If userA doesn't specify `options.share[arbitraryName]`, it won't be available for userA.
|
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and
|
||||||
* If userB specifies `options.share[arbitraryName]`, it still won't be available for userA. But all the updates are send from userB to userA.
|
create an y-array type on `y.share[arbitraryName]`.
|
||||||
* 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.*`
|
* If userA doesn't specify `options.share[arbitraryName]`, it won't be
|
||||||
* Weird behavior: It is supported that two users specify different types with the same property name.
|
available for userA.
|
||||||
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
|
* If userB specifies `options.share[arbitraryName]`, it still won't be
|
||||||
* options.type
|
available for userA. But all the updates are send from userB to userA.
|
||||||
* Array of modules that Yjs needs to require, before instantiating a shared type.
|
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
|
||||||
* By default Yjs requires the specified database adapter, the specified connector, and all modules that are used in `options.share.*`
|
Instead, they are merged among all users. This feature is only available on
|
||||||
|
`y.share.*`
|
||||||
|
* Weird behavior: It is supported that two users specify different types with
|
||||||
|
the same property name.
|
||||||
|
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies
|
||||||
|
`options.share.x = 'Text'`. But they only share data if they specified the
|
||||||
|
same type with the same property name
|
||||||
|
* options.type (browser only)
|
||||||
|
* Array of modules that Yjs needs to require, before instantiating a shared
|
||||||
|
type.
|
||||||
|
* By default Yjs requires the specified database adapter, the specified
|
||||||
|
connector, and all modules that are used in `options.share.*`
|
||||||
* Put all types here that you intend to use, but are not used in y.share.*
|
* Put all types here that you intend to use, but are not used in y.share.*
|
||||||
|
|
||||||
### Instantiated Y object (y)
|
### Instantiated Y object (y)
|
||||||
@@ -137,7 +219,8 @@ 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 server)
|
* The connector is initialized, and a unique user id is set (received from the
|
||||||
|
server)
|
||||||
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
|
* 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`.
|
||||||
@@ -155,70 +238,57 @@ 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 connector)
|
* Try to reconnect to the other instances (needs to be supported by the
|
||||||
|
connector)
|
||||||
* Not supported by y-xmpp
|
* Not supported by y-xmpp
|
||||||
* y.destroy()
|
* y.close()
|
||||||
* 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 collection
|
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
|
||||||
|
collection
|
||||||
|
* y.db.gc :: Boolean
|
||||||
|
* Whether gc is turned on
|
||||||
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
* 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 collect cycles (after 100000 ms per default)
|
* It is required that all instances exchanged all messages after two garbage
|
||||||
|
collect cycles (after 100000 ms per default)
|
||||||
* y.db.userId :: String
|
* y.db.userId :: String
|
||||||
* The used user id for this client. **Never overwrite this**
|
* The used user id for this client. **Never overwrite this**
|
||||||
|
|
||||||
## Get help
|
### Logging
|
||||||
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.
|
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
|
||||||
|
`y*` enables logging for all y-* components. You can selectively remove
|
||||||
|
components you are not interested in: E.g. The flag `y*,-y:connector-message`
|
||||||
|
will not log the long `y:connector-message` messages.
|
||||||
|
|
||||||
Please report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
|
##### Enable logging in Node.js
|
||||||
If you want to see an issue fixed, please subscribe to the thread (or remind me via gitter).
|
```sh
|
||||||
|
DEBUG=y* node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove the colors in order to log to a file:
|
||||||
|
```sh
|
||||||
|
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
||||||
|
```
|
||||||
|
|
||||||
## Changelog
|
##### Enable logging in the browser
|
||||||
|
```js
|
||||||
### 11.0.0
|
localStorage.debug = 'y*'
|
||||||
|
```
|
||||||
* **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 science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
|
I created this framework during my bachelor thesis at the chair of computer
|
||||||
|
science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since
|
||||||
|
December 2014 I'm working on Yjs as a part of my student worker job at the i5.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Yjs is licensed under the [MIT License](./LICENSE.txt).
|
Yjs is licensed under the [MIT License](./LICENSE).
|
||||||
|
|
||||||
<yjs@dbis.rwth-aachen.de>
|
<yjs@dbis.rwth-aachen.de>
|
||||||
|
|
||||||
[ShareJs]: https://github.com/share/ShareJS
|
|
||||||
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
/* @flow */
|
|
||||||
|
|
||||||
type UserId = string
|
|
||||||
type Id = [UserId, number|string]
|
|
||||||
|
|
||||||
/*
|
|
||||||
type Struct = {
|
|
||||||
id: Id,
|
|
||||||
left?: Id,
|
|
||||||
right?: Id,
|
|
||||||
target?: Id,
|
|
||||||
struct: 'Insert' | 'Delete'
|
|
||||||
}*/
|
|
||||||
|
|
||||||
type Struct = Insertion | Deletion
|
|
||||||
type Operation = Struct
|
|
||||||
|
|
||||||
type Insertion = {
|
|
||||||
id: Id,
|
|
||||||
left: ?Id,
|
|
||||||
origin: ?Id,
|
|
||||||
right: ?Id,
|
|
||||||
parent: Id,
|
|
||||||
parentSub: ?Id,
|
|
||||||
opContent: ?Id,
|
|
||||||
content: ?any,
|
|
||||||
struct: 'Insert'
|
|
||||||
}
|
|
||||||
|
|
||||||
type Deletion = {
|
|
||||||
target: Id,
|
|
||||||
struct: 'Delete'
|
|
||||||
}
|
|
||||||
|
|
||||||
type MapStruct = {
|
|
||||||
id: Id,
|
|
||||||
type: TypeNames,
|
|
||||||
map: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListStruct = {
|
|
||||||
id: Id,
|
|
||||||
type: TypeNames,
|
|
||||||
start: Id,
|
|
||||||
end: Id
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type MessageSyncStep1 = {
|
|
||||||
type: 'sync step 1',
|
|
||||||
deleteSet: any,
|
|
||||||
stateSet: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageSyncStep2 = {
|
|
||||||
type: 'sync step 2',
|
|
||||||
os: Array<Operation>,
|
|
||||||
deleteSet: any,
|
|
||||||
stateSet: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageUpdate = {
|
|
||||||
type: 'update',
|
|
||||||
ops: Array<Operation>
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageSyncDone = {
|
|
||||||
type: 'sync done'
|
|
||||||
}
|
|
||||||
|
|
||||||
type Message = MessageSyncStep1 | MessageSyncStep2 | MessageUpdate | MessageSyncDone
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/* @flow */
|
|
||||||
|
|
||||||
type YGlobal = {
|
|
||||||
utils: Object,
|
|
||||||
Struct: any,
|
|
||||||
AbstractDatabase: any,
|
|
||||||
AbstractConnector: any,
|
|
||||||
Transaction: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type YConfig = {
|
|
||||||
db: Object,
|
|
||||||
connector: Object,
|
|
||||||
root: Object
|
|
||||||
}
|
|
||||||
|
|
||||||
type TypeName = 'array' | 'map' | 'text'
|
|
||||||
|
|
||||||
declare var YConcurrency_TestingMode : boolean
|
|
||||||
|
|
||||||
type Transaction<A> = Generator<any, A, any>
|
|
||||||
|
|
||||||
type SyncRole = 'master' | 'slave'
|
|
||||||
|
|
||||||
declare class Store {
|
|
||||||
find: (id:Id) => Transaction<any>;
|
|
||||||
put: (n:any) => Transaction<void>;
|
|
||||||
delete: (id:Id) => Transaction<void>;
|
|
||||||
findWithLowerBound: (start:Id) => Transaction<any>;
|
|
||||||
findWithUpperBound: (end:Id) => Transaction<any>;
|
|
||||||
findNext: (id:Id) => Transaction<any>;
|
|
||||||
findPrev: (id:Id) => Transaction<any>;
|
|
||||||
iterate: (t:any,start:?Id,end:?Id,gen:any) => Transaction<any>;
|
|
||||||
}
|
|
||||||
2
dist
2
dist
Submodule dist updated: 47bcec8bc7...42aa7ec5c9
32
examples/ace/index.html
Normal file
32
examples/ace/index.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style type="text/css" media="screen">
|
||||||
|
#aceContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.inserted {
|
||||||
|
position:absolute;
|
||||||
|
z-index:20;
|
||||||
|
background-color: #FFC107;
|
||||||
|
}
|
||||||
|
.deleted {
|
||||||
|
position:absolute;
|
||||||
|
z-index:20;
|
||||||
|
background-color: #FFC107;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="aceContainer"></div>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||||
|
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
examples/ace/index.js
Normal file
24
examples/ace/index.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* global Y, ace */
|
||||||
|
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'ace-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
ace: 'Text' // y.share.textarea is of type Y.Text
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yAce = y
|
||||||
|
|
||||||
|
// bind the textarea to a shared text element
|
||||||
|
var editor = ace.edit('aceContainer')
|
||||||
|
editor.setTheme('ace/theme/chrome')
|
||||||
|
editor.getSession().setMode('ace/mode/javascript')
|
||||||
|
|
||||||
|
y.share.ace.bindAce(editor)
|
||||||
|
})
|
||||||
29
examples/bower.json
Normal file
29
examples/bower.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "yjs-examples",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"homepage": "y-js.org",
|
||||||
|
"authors": [
|
||||||
|
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
||||||
|
],
|
||||||
|
"description": "Examples for Yjs",
|
||||||
|
"license": "MIT",
|
||||||
|
"ignore": [],
|
||||||
|
"dependencies": {
|
||||||
|
"yjs": "latest",
|
||||||
|
"y-array": "latest",
|
||||||
|
"y-map": "latest",
|
||||||
|
"y-memory": "latest",
|
||||||
|
"y-richtext": "latest",
|
||||||
|
"y-webrtc": "latest",
|
||||||
|
"y-websockets-client": "latest",
|
||||||
|
"y-text": "latest",
|
||||||
|
"y-indexeddb": "latest",
|
||||||
|
"y-xml": "latest",
|
||||||
|
"quill": "^1.0.0-rc.2",
|
||||||
|
"ace": "~1.2.3",
|
||||||
|
"ace-builds": "~1.2.3",
|
||||||
|
"jquery": "~2.2.2",
|
||||||
|
"d3": "^3.5.16",
|
||||||
|
"codemirror": "^5.25.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
examples/chat/index.html
Normal file
18
examples/chat/index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
#chat p span {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="chat"></div>
|
||||||
|
<form id="chatform">
|
||||||
|
<input name="username" type="text" style="width:15%;">
|
||||||
|
<input name="message" type="text" style="width:60%;">
|
||||||
|
<input type="submit" value="Send">
|
||||||
|
</form>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
75
examples/chat/index.js
Normal file
75
examples/chat/index.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* @flow */
|
||||||
|
/* global Y */
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'chat-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
chat: 'Array'
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yChat = y
|
||||||
|
// This functions inserts a message at the specified position in the DOM
|
||||||
|
function appendMessage(message, position) {
|
||||||
|
var p = document.createElement('p')
|
||||||
|
var uname = document.createElement('span')
|
||||||
|
uname.appendChild(document.createTextNode(message.username + ": "))
|
||||||
|
p.appendChild(uname)
|
||||||
|
p.appendChild(document.createTextNode(message.message))
|
||||||
|
document.querySelector('#chat').insertBefore(p, chat.children[position] || null)
|
||||||
|
}
|
||||||
|
// This function makes sure that only 7 messages exist in the chat history.
|
||||||
|
// The rest is deleted
|
||||||
|
function cleanupChat () {
|
||||||
|
var len
|
||||||
|
while ((len = y.share.chat.length) > 7) {
|
||||||
|
y.share.chat.delete(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Insert the initial content
|
||||||
|
y.share.chat.toArray().forEach(appendMessage)
|
||||||
|
cleanupChat()
|
||||||
|
|
||||||
|
// whenever content changes, make sure to reflect the changes in the DOM
|
||||||
|
y.share.chat.observe(function (event) {
|
||||||
|
if (event.type === 'insert') {
|
||||||
|
for (var i = 0; i < event.length; i++) {
|
||||||
|
appendMessage(event.values[i], event.index + i)
|
||||||
|
}
|
||||||
|
} else if (event.type === 'delete') {
|
||||||
|
for (var i = 0; i < event.length; i++) {
|
||||||
|
chat.children[event.index].remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// concurrent insertions may result in a history > 7, so cleanup here
|
||||||
|
cleanupChat()
|
||||||
|
})
|
||||||
|
document.querySelector('#chatform').onsubmit = function (event) {
|
||||||
|
// the form is submitted
|
||||||
|
var message = {
|
||||||
|
username: this.querySelector("[name=username]").value,
|
||||||
|
message: this.querySelector("[name=message]").value
|
||||||
|
}
|
||||||
|
if (message.username.length > 0 && message.message.length > 0) {
|
||||||
|
if (y.share.chat.length > 6) {
|
||||||
|
// If we are goint to insert the 8th element, make sure to delete first.
|
||||||
|
y.share.chat.delete(0)
|
||||||
|
}
|
||||||
|
// Here we insert a message in the shared chat type.
|
||||||
|
// This will call the observe function (see line 40)
|
||||||
|
// and reflect the change in the DOM
|
||||||
|
y.share.chat.push([message])
|
||||||
|
this.querySelector("[name=message]").value = ""
|
||||||
|
}
|
||||||
|
// Do not send this form!
|
||||||
|
event.preventDefault()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
23
examples/codemirror/index.html
Normal file
23
examples/codemirror/index.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="codeMirrorContainer"></div>
|
||||||
|
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||||
|
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||||
|
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||||
|
<style>
|
||||||
|
.CodeMirror {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
examples/codemirror/index.js
Normal file
24
examples/codemirror/index.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* global Y, CodeMirror */
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'codemirror-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yCodeMirror = y
|
||||||
|
|
||||||
|
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||||
|
mode: 'javascript',
|
||||||
|
lineNumbers: true
|
||||||
|
})
|
||||||
|
y.share.codemirror.bindCodeMirror(editor)
|
||||||
|
})
|
||||||
19
examples/drawing/index.html
Normal file
19
examples/drawing/index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<style>
|
||||||
|
path {
|
||||||
|
fill: none;
|
||||||
|
stroke: blue;
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||||
|
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="../bower_components/d3/d3.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
84
examples/drawing/index.js
Normal file
84
examples/drawing/index.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/* globals Y, d3 */
|
||||||
|
'strict mode'
|
||||||
|
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'drawing-example'
|
||||||
|
// url: 'localhost:1234'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
drawing: 'Array'
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yDrawing = y
|
||||||
|
var drawing = y.share.drawing
|
||||||
|
var renderPath = d3.svg.line()
|
||||||
|
.x(function (d) { return d[0] })
|
||||||
|
.y(function (d) { return d[1] })
|
||||||
|
.interpolate('basis')
|
||||||
|
|
||||||
|
var svg = d3.select('#drawingCanvas')
|
||||||
|
.call(d3.behavior.drag()
|
||||||
|
.on('dragstart', dragstart)
|
||||||
|
.on('drag', drag)
|
||||||
|
.on('dragend', dragend))
|
||||||
|
|
||||||
|
// create line from a shared array object and update the line when the array changes
|
||||||
|
function drawLine (yarray) {
|
||||||
|
var line = svg.append('path').datum(yarray.toArray())
|
||||||
|
line.attr('d', renderPath)
|
||||||
|
yarray.observe(function (event) {
|
||||||
|
// we only implement insert events that are appended to the end of the array
|
||||||
|
event.values.forEach(function (value) {
|
||||||
|
line.datum().push(value)
|
||||||
|
})
|
||||||
|
line.attr('d', renderPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// call drawLine every time an array is appended
|
||||||
|
y.share.drawing.observe(function (event) {
|
||||||
|
if (event.type === 'insert') {
|
||||||
|
event.values.forEach(drawLine)
|
||||||
|
} else {
|
||||||
|
// just remove all elements (thats what we do anyway)
|
||||||
|
svg.selectAll('path').remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// draw all existing content
|
||||||
|
for (var i = 0; i < drawing.length; i++) {
|
||||||
|
drawLine(drawing.get(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear canvas on request
|
||||||
|
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||||
|
drawing.delete(0, drawing.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sharedLine = null
|
||||||
|
function dragstart () {
|
||||||
|
drawing.insert(drawing.length, [Y.Array])
|
||||||
|
sharedLine = drawing.get(drawing.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After one dragged event is recognized, we ignore them for 33ms.
|
||||||
|
var ignoreDrag = null
|
||||||
|
function drag () {
|
||||||
|
if (sharedLine != null && ignoreDrag == null) {
|
||||||
|
ignoreDrag = window.setTimeout(function () {
|
||||||
|
ignoreDrag = null
|
||||||
|
}, 33)
|
||||||
|
sharedLine.push([d3.mouse(this)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragend () {
|
||||||
|
sharedLine = null
|
||||||
|
window.clearTimeout(ignoreDrag)
|
||||||
|
ignoreDrag = null
|
||||||
|
}
|
||||||
|
})
|
||||||
23
examples/jigsaw/index.html
Normal file
23
examples/jigsaw/index.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style type="text/css">
|
||||||
|
.draggable {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
|
||||||
|
<g>
|
||||||
|
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
|
||||||
|
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
|
||||||
|
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
|
||||||
|
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="../bower_components/d3/d3.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
69
examples/jigsaw/index.js
Normal file
69
examples/jigsaw/index.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/* @flow */
|
||||||
|
/* global Y, d3 */
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'Puzzle-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
piece1: 'Map',
|
||||||
|
piece2: 'Map',
|
||||||
|
piece3: 'Map',
|
||||||
|
piece4: 'Map'
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yJigsaw = y
|
||||||
|
var origin // mouse start position - translation of piece
|
||||||
|
var drag = d3.behavior.drag()
|
||||||
|
.on('dragstart', function (params) {
|
||||||
|
// get the translation of the element
|
||||||
|
var translation = d3.select(this).attr('transform').slice(10,-1).split(',').map(Number)
|
||||||
|
// mouse coordinates
|
||||||
|
var mouse = d3.mouse(this.parentNode)
|
||||||
|
origin = {
|
||||||
|
x: mouse[0] - translation[0],
|
||||||
|
y: mouse[1] - translation[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("drag", function(){
|
||||||
|
var mouse = d3.mouse(this.parentNode)
|
||||||
|
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||||
|
var y = mouse[1] - origin.y
|
||||||
|
d3.select(this).attr("transform", "translate(" + x + "," + y + ")")
|
||||||
|
})
|
||||||
|
.on('dragend', function (piece, i) {
|
||||||
|
// save the current translation of the puzzle piece
|
||||||
|
var mouse = d3.mouse(this.parentNode)
|
||||||
|
var x = mouse[0] - origin.x
|
||||||
|
var y = mouse[1] - origin.y
|
||||||
|
piece.set('translation', {x: x, y: y})
|
||||||
|
})
|
||||||
|
|
||||||
|
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
||||||
|
var pieces = d3.select(document.querySelector("#puzzle-example")).selectAll("path").data(data)
|
||||||
|
|
||||||
|
pieces
|
||||||
|
.classed('draggable', true)
|
||||||
|
.attr("transform", function (piece) {
|
||||||
|
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||||
|
return "translate(" + translation.x + "," + translation.y + ")"
|
||||||
|
}).call(drag)
|
||||||
|
|
||||||
|
data.forEach(function(piece){
|
||||||
|
piece.observe(function () {
|
||||||
|
// whenever a property of a piece changes, update the translation of the pieces
|
||||||
|
pieces
|
||||||
|
.transition()
|
||||||
|
.attr("transform", function (piece) {
|
||||||
|
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||||
|
return "translate(" + translation.x + "," + translation.y + ")"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
24
examples/monaco/index.html
Normal file
24
examples/monaco/index.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="monacoContainer"></div>
|
||||||
|
<style>
|
||||||
|
#monacoContainer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="../bower_components/y-array/y-array.js"></script>
|
||||||
|
<script src="../bower_components/y-text/y-text.js"></script>
|
||||||
|
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
|
||||||
|
<script src="../bower_components/y-memory/y-memory.js"></script>
|
||||||
|
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
examples/monaco/index.js
Normal file
31
examples/monaco/index.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* global Y */
|
||||||
|
|
||||||
|
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' }})
|
||||||
|
require(['vs/editor/editor.main'], function() {
|
||||||
|
|
||||||
|
// Initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'monaco-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
monaco: 'Text' // y.share.monaco is of type Y.Text
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yMonaco = y
|
||||||
|
|
||||||
|
// Create Monaco editor
|
||||||
|
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||||
|
language: 'javascript'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Bind to y.share.monaco
|
||||||
|
y.share.monaco.bindMonaco(editor)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
10
examples/package.json
Normal file
10
examples/package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "examples",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "",
|
||||||
|
"author": "Kevin Jahns",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"monaco-editor": "^0.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
examples/quill/index.html
Normal file
31
examples/quill/index.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||||
|
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||||
|
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||||
|
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||||
|
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
#quill-container {
|
||||||
|
border: 1px solid gray;
|
||||||
|
box-shadow: 0px 0px 10px gray;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="quill-container">
|
||||||
|
<div id="quill">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||||
|
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||||
|
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||||
|
-->
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
examples/quill/index.js
Normal file
39
examples/quill/index.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/* global Y, Quill */
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'richtext-example-quill-1.0-test'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yQuill = y
|
||||||
|
|
||||||
|
// create quill element
|
||||||
|
window.quill = new Quill('#quill', {
|
||||||
|
modules: {
|
||||||
|
formula: true,
|
||||||
|
syntax: true,
|
||||||
|
toolbar: [
|
||||||
|
[{ size: ['small', false, 'large', 'huge'] }],
|
||||||
|
['bold', 'italic', 'underline'],
|
||||||
|
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||||
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
|
['link', 'image'],
|
||||||
|
['link', 'code-block'],
|
||||||
|
[{list: 'ordered' }]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
theme: 'snow'
|
||||||
|
});
|
||||||
|
// bind quill to richtext type
|
||||||
|
y.share.richtext.bind(window.quill)
|
||||||
|
})
|
||||||
31
examples/serviceworker/index.html
Normal file
31
examples/serviceworker/index.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||||
|
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||||
|
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||||
|
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||||
|
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
#quill-container {
|
||||||
|
border: 1px solid gray;
|
||||||
|
box-shadow: 0px 0px 10px gray;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="quill-container">
|
||||||
|
<div id="quill">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||||
|
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||||
|
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||||
|
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||||
|
-->
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
49
examples/serviceworker/index.js
Normal file
49
examples/serviceworker/index.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/* global Y, Quill */
|
||||||
|
|
||||||
|
// register yjs service worker
|
||||||
|
if('serviceWorker' in navigator){
|
||||||
|
// Register service worker
|
||||||
|
// it is important to copy yjs-sw-template to the root directory!
|
||||||
|
navigator.serviceWorker.register('./yjs-sw-template.js').then(function(reg){
|
||||||
|
console.log("Yjs service worker registration succeeded. Scope is " + reg.scope);
|
||||||
|
}).catch(function(err){
|
||||||
|
console.error("Yjs service worker registration failed with error " + err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'serviceworker',
|
||||||
|
room: 'ServiceWorkerExample2'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yServiceWorker = y
|
||||||
|
|
||||||
|
// create quill element
|
||||||
|
window.quill = new Quill('#quill', {
|
||||||
|
modules: {
|
||||||
|
formula: true,
|
||||||
|
syntax: true,
|
||||||
|
toolbar: [
|
||||||
|
[{ size: ['small', false, 'large', 'huge'] }],
|
||||||
|
['bold', 'italic', 'underline'],
|
||||||
|
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||||
|
[{ script: 'sub' }, { script: 'super' }],
|
||||||
|
['link', 'image'],
|
||||||
|
['link', 'code-block'],
|
||||||
|
[{list: 'ordered' }]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
theme: 'snow'
|
||||||
|
})
|
||||||
|
// bind quill to richtext type
|
||||||
|
y.share.richtext.bind(window.quill)
|
||||||
|
})
|
||||||
22
examples/serviceworker/yjs-sw-template.js
Normal file
22
examples/serviceworker/yjs-sw-template.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-env worker */
|
||||||
|
|
||||||
|
// copy and modify this file
|
||||||
|
|
||||||
|
self.DBConfig = {
|
||||||
|
name: 'indexeddb'
|
||||||
|
}
|
||||||
|
self.ConnectorConfig = {
|
||||||
|
name: 'websockets-client',
|
||||||
|
// url: '..',
|
||||||
|
options: {
|
||||||
|
jsonp: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importScripts(
|
||||||
|
'/bower_components/yjs/y.js',
|
||||||
|
'/bower_components/y-memory/y-memory.js',
|
||||||
|
'/bower_components/y-indexeddb/y-indexeddb.js',
|
||||||
|
'/bower_components/y-websockets-client/y-websockets-client.js',
|
||||||
|
'/bower_components/y-serviceworker/yjs-sw-include.js'
|
||||||
|
)
|
||||||
8
examples/textarea/index.html
Normal file
8
examples/textarea/index.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
examples/textarea/index.js
Normal file
23
examples/textarea/index.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/* global Y */
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'Textarea-example'
|
||||||
|
// url: '127.0.0.1:1234'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
textarea: 'Text' // y.share.textarea is of type Y.Text
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yTextarea = y
|
||||||
|
|
||||||
|
// bind the textarea to a shared text element
|
||||||
|
y.share.textarea.bind(document.getElementById('textfield'))
|
||||||
|
// thats it..
|
||||||
|
})
|
||||||
39
examples/xml/index.html
Normal file
39
examples/xml/index.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
</head>
|
||||||
|
<script src="../bower_components/yjs/y.js"></script>
|
||||||
|
<script src="../bower_components/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1> Shared DOM Example </h1>
|
||||||
|
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
|
||||||
|
<div class="command">
|
||||||
|
<button type="button">Execute</button>
|
||||||
|
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
|
||||||
|
</div>
|
||||||
|
<div class="command">
|
||||||
|
<button type="button">Execute</button>
|
||||||
|
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
|
||||||
|
</div>
|
||||||
|
<div class="command">
|
||||||
|
<button type="button">Execute</button>
|
||||||
|
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var commands = document.querySelectorAll(".command");
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
|
||||||
|
var execute = function(){
|
||||||
|
eval(command.querySelector("input").value);
|
||||||
|
}
|
||||||
|
command.querySelector("button").onclick = execute
|
||||||
|
$(command.querySelector("input")).keyup(function (e) {
|
||||||
|
if (e.keyCode == 13) {
|
||||||
|
execute()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
examples/xml/index.js
Normal file
21
examples/xml/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* global Y */
|
||||||
|
|
||||||
|
// initialize a shared object. This function call returns a promise!
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: 'Xml-example'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components',
|
||||||
|
share: {
|
||||||
|
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
|
||||||
|
}
|
||||||
|
}).then(function (y) {
|
||||||
|
window.yXml = y
|
||||||
|
// bind xml type to a dom, and put it in body
|
||||||
|
window.sharedDom = y.share.xml.getDom()
|
||||||
|
document.body.appendChild(window.sharedDom)
|
||||||
|
})
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
var $ = require('gulp-load-plugins')()
|
|
||||||
var minimist = require('minimist')
|
|
||||||
var browserify = require('browserify')
|
|
||||||
var source = require('vinyl-source-stream')
|
|
||||||
var buffer = require('vinyl-buffer')
|
|
||||||
|
|
||||||
module.exports = function (gulp, helperOptions) {
|
|
||||||
var runSequence = require('run-sequence').use(gulp)
|
|
||||||
var options = minimist(process.argv.slice(2), {
|
|
||||||
string: ['modulename', 'export', 'name', 'port', 'testfiles', 'es6'],
|
|
||||||
default: {
|
|
||||||
modulename: helperOptions.moduleName,
|
|
||||||
targetName: helperOptions.targetName,
|
|
||||||
export: 'ignore',
|
|
||||||
port: '8888',
|
|
||||||
testfiles: '**/*.spec.js',
|
|
||||||
es6: false,
|
|
||||||
browserify: helperOptions.browserify != null ? helperOptions.browserify : false,
|
|
||||||
includeRuntime: helperOptions.includeRuntime || false,
|
|
||||||
debug: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (options.es6 !== false) {
|
|
||||||
options.es6 = true
|
|
||||||
}
|
|
||||||
var files = {
|
|
||||||
dist: helperOptions.entry,
|
|
||||||
specs: helperOptions.specs,
|
|
||||||
src: './src/**/*.js'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.includeRuntime) {
|
|
||||||
files.distEs5 = ['node_modules/regenerator/runtime.js', files.dist]
|
|
||||||
} else {
|
|
||||||
files.distEs5 = [files.dist]
|
|
||||||
}
|
|
||||||
|
|
||||||
gulp.task('dist:es5', function () {
|
|
||||||
var babelOptions = {
|
|
||||||
presets: ['es2015']
|
|
||||||
}
|
|
||||||
return (browserify({
|
|
||||||
entries: files.distEs5,
|
|
||||||
debug: true
|
|
||||||
}).transform('babelify', babelOptions)
|
|
||||||
.bundle()
|
|
||||||
.pipe(source(options.targetName))
|
|
||||||
.pipe(buffer())
|
|
||||||
.pipe($.sourcemaps.init({loadMaps: true}))
|
|
||||||
.pipe($.if(!options.debug, $.uglify().on('error', function (e) {
|
|
||||||
console.log('\x07', e.message, JSON.stringify(e)); return this.end()
|
|
||||||
})))
|
|
||||||
.pipe($.sourcemaps.write('.'))
|
|
||||||
.pipe(gulp.dest('./dist/')))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('dist:es6', function () {
|
|
||||||
return (browserify({
|
|
||||||
entries: files.dist,
|
|
||||||
debug: true
|
|
||||||
}).bundle()
|
|
||||||
.pipe(source(options.targetName))
|
|
||||||
.pipe(buffer())
|
|
||||||
.pipe($.sourcemaps.init({loadMaps: true}))
|
|
||||||
// .pipe($.uglify()) -- generators not yet supported see #448
|
|
||||||
.pipe($.rename({
|
|
||||||
extname: '.es6'
|
|
||||||
}))
|
|
||||||
.pipe($.sourcemaps.write('.'))
|
|
||||||
|
|
||||||
.pipe(gulp.dest('./dist/')))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('dist', ['dist:es6', 'dist:es5'])
|
|
||||||
|
|
||||||
gulp.task('watch:dist', function (cb) {
|
|
||||||
options.debug = true
|
|
||||||
gulp.src(['./README.md'])
|
|
||||||
.pipe($.watch('./README.md'))
|
|
||||||
.pipe(gulp.dest('./dist/'))
|
|
||||||
runSequence('dist', function () {
|
|
||||||
gulp.watch(files.src.concat('./README.md'), ['dist'])
|
|
||||||
cb()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('dev:node', ['test'], function () {
|
|
||||||
gulp.watch(files.src, ['test'])
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('spec-build', function () {
|
|
||||||
var browserify = require('browserify')
|
|
||||||
var source = require('vinyl-source-stream')
|
|
||||||
var buffer = require('vinyl-buffer')
|
|
||||||
|
|
||||||
return browserify({
|
|
||||||
entries: files.specs, // .concat(files.distEs5),
|
|
||||||
debug: true
|
|
||||||
})// .transform('babelify', { presets: ['es2015'] })
|
|
||||||
.bundle()
|
|
||||||
.pipe(source('specs.js'))
|
|
||||||
.pipe(buffer())
|
|
||||||
// .pipe($.sourcemaps.init({loadMaps: true}))
|
|
||||||
// .pipe($.sourcemaps.write('.'))
|
|
||||||
.pipe(gulp.dest('./build/'))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('dev:browser', ['spec-build'], function () {
|
|
||||||
gulp.watch(files.src, ['spec-build'])
|
|
||||||
return gulp.src('./build/specs.js')
|
|
||||||
.pipe($.jasmineBrowser.specRunner())
|
|
||||||
.pipe($.jasmineBrowser.server({port: options.port}))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('test', function () {
|
|
||||||
return gulp.src(files.specs)
|
|
||||||
.pipe($.jasmine({
|
|
||||||
verbose: true,
|
|
||||||
includeStuckTrace: true
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('updateSubmodule', function () {
|
|
||||||
return gulp.src('./package.json', {read: false})
|
|
||||||
.pipe($.shell([
|
|
||||||
'git submodule update --init',
|
|
||||||
'cd dist && git pull origin dist'
|
|
||||||
]))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('bump', function (cb) {
|
|
||||||
gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
|
||||||
.pipe($.prompt.prompt({
|
|
||||||
type: 'checkbox',
|
|
||||||
name: 'bump',
|
|
||||||
message: 'What type of bump would you like to do?',
|
|
||||||
choices: ['patch', 'minor', 'major']
|
|
||||||
}, function (res) {
|
|
||||||
if (res.bump.length === 0) {
|
|
||||||
console.info('You have to select a bump type. Now I\'m going to use "patch" as bump type..')
|
|
||||||
}
|
|
||||||
var bumptype = res.bump[0]
|
|
||||||
if (bumptype === 'major') {
|
|
||||||
runSequence('bump_major', cb)
|
|
||||||
} else if (bumptype === 'minor') {
|
|
||||||
runSequence('bump_minor', cb)
|
|
||||||
} else {
|
|
||||||
runSequence('bump_patch', cb)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
gulp.task('bump_patch', function () {
|
|
||||||
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
|
||||||
.pipe($.bump({type: 'patch'}))
|
|
||||||
.pipe(gulp.dest('./'))
|
|
||||||
})
|
|
||||||
gulp.task('bump_minor', function () {
|
|
||||||
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
|
||||||
.pipe($.bump({type: 'minor'}))
|
|
||||||
.pipe(gulp.dest('./'))
|
|
||||||
})
|
|
||||||
gulp.task('bump_major', function () {
|
|
||||||
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
|
||||||
.pipe($.bump({type: 'major'}))
|
|
||||||
.pipe(gulp.dest('./'))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('publish_commits', function () {
|
|
||||||
return gulp.src('./package.json')
|
|
||||||
.pipe($.prompt.confirm({
|
|
||||||
message: 'Are you sure you want to publish this release?',
|
|
||||||
default: false
|
|
||||||
}))
|
|
||||||
.pipe($.shell([
|
|
||||||
'cp README.md dist',
|
|
||||||
'standard',
|
|
||||||
'echo "Deploying version <%= getVersion(file.path) %>"',
|
|
||||||
'git pull',
|
|
||||||
'cd ./dist/ && git add -A',
|
|
||||||
'cd ./dist/ && git commit -am "Deploy <%= getVersion(file.path) %>" -n',
|
|
||||||
'cd ./dist/ && git push origin HEAD:dist',
|
|
||||||
'cd ./dist/ && git tag -a v<%= getVersion(file.path) %> -m "Release <%= getVersion(file.path) %>"',
|
|
||||||
'cd ./dist/ && git push origin --tags',
|
|
||||||
'git commit -am "Release <%= getVersion(file.path) %>" -n',
|
|
||||||
'git push',
|
|
||||||
'npm publish',
|
|
||||||
'echo Finished'
|
|
||||||
], {
|
|
||||||
templateData: {
|
|
||||||
getVersion: function () {
|
|
||||||
return JSON.parse(String.fromCharCode.apply(null, this.file._contents)).version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('publish', function (cb) {
|
|
||||||
/* TODO: include 'test',*/
|
|
||||||
runSequence('updateSubmodule', 'bump', 'dist', 'publish_commits', cb)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
104
gulpfile.js
104
gulpfile.js
@@ -1,104 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
/** Gulp Commands
|
|
||||||
|
|
||||||
gulp command*
|
|
||||||
[--export ModuleType]
|
|
||||||
[--name ModuleName]
|
|
||||||
[--testport TestPort]
|
|
||||||
[--testfiles TestFiles]
|
|
||||||
|
|
||||||
Module name (ModuleName):
|
|
||||||
Compile this to "y.js" (default)
|
|
||||||
|
|
||||||
Supported module types (ModuleType):
|
|
||||||
- amd
|
|
||||||
- amdStrict
|
|
||||||
- common
|
|
||||||
- commonStrict
|
|
||||||
- ignore (default)
|
|
||||||
- system
|
|
||||||
- umd
|
|
||||||
- umdStrict
|
|
||||||
|
|
||||||
Test port (TestPort):
|
|
||||||
Serve the specs on port 8888 (default)
|
|
||||||
|
|
||||||
Test files (TestFiles):
|
|
||||||
Specify which specs to use!
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
- build:deploy
|
|
||||||
Build this library for deployment (es6->es5, minified)
|
|
||||||
- dev:browser
|
|
||||||
Watch the ./src directory.
|
|
||||||
Builds the library on changes.
|
|
||||||
Starts an http-server and serves the test suite on http://127.0.0.1:8888.
|
|
||||||
- dev:node
|
|
||||||
Watch the ./src directory.
|
|
||||||
Builds and specs the library on changes.
|
|
||||||
Usefull to run with node-inspector.
|
|
||||||
`node-debug $(which gulp) dev:node
|
|
||||||
- test:
|
|
||||||
Test this library
|
|
||||||
*/
|
|
||||||
|
|
||||||
var gulp = require('gulp')
|
|
||||||
var $ = require('gulp-load-plugins')()
|
|
||||||
var runSequence = require('run-sequence').use(gulp)
|
|
||||||
|
|
||||||
require('./gulpfile.helper.js')(gulp, {
|
|
||||||
polyfills: [],
|
|
||||||
entry: './src/y.js',
|
|
||||||
targetName: 'y.js',
|
|
||||||
moduleName: 'yjs',
|
|
||||||
includeRuntime: true,
|
|
||||||
specs: [
|
|
||||||
'./src/Database.spec.js',
|
|
||||||
'../y-array/src/Array.spec.js',
|
|
||||||
'../y-map/src/Map.spec.js'
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('dev:examples', ['watch:dist'], function () {
|
|
||||||
// watch all distfiles and copy them to bower_components
|
|
||||||
var distfiles = ['./dist/*.{js,es6}', './dist/*.{js,es6}.map', '../y-*/dist/*.{js,es6}', '../y-*/dist/*.{js,es6}.map']
|
|
||||||
gulp.src(distfiles)
|
|
||||||
.pipe($.watch(distfiles))
|
|
||||||
.pipe($.rename(function (path) {
|
|
||||||
var dir = path.dirname.split(/[\\\/]/)[0]
|
|
||||||
console.log(JSON.stringify(path))
|
|
||||||
path.dirname = dir === '.' ? 'yjs' : dir
|
|
||||||
}))
|
|
||||||
.pipe(gulp.dest('./dist/Examples/bower_components/'))
|
|
||||||
|
|
||||||
return $.serve('dist/Examples/')()
|
|
||||||
})
|
|
||||||
|
|
||||||
gulp.task('default', ['updateSubmodule'], function (cb) {
|
|
||||||
gulp.src('package.json')
|
|
||||||
.pipe($.prompt.prompt({
|
|
||||||
type: 'checkbox',
|
|
||||||
name: 'tasks',
|
|
||||||
message: 'Which tasks would you like to run?',
|
|
||||||
choices: [
|
|
||||||
'test Test this project',
|
|
||||||
'dev:examples Serve the examples directory in ./dist/',
|
|
||||||
'dev:browser Watch files & serve the testsuite for the browser',
|
|
||||||
'dev:nodejs Watch filse & test this project with nodejs',
|
|
||||||
'bump Bump the current state of the project',
|
|
||||||
'publish Publish this project. Creates a github tag',
|
|
||||||
'dist Build the distribution files'
|
|
||||||
]
|
|
||||||
}, function (res) {
|
|
||||||
var tasks = res.tasks.map(function (task) {
|
|
||||||
return task.split(' ')[0]
|
|
||||||
})
|
|
||||||
if (tasks.length > 0) {
|
|
||||||
console.info('gulp ' + tasks.join(' '))
|
|
||||||
runSequence(tasks, cb)
|
|
||||||
} else {
|
|
||||||
console.info('Ok, .. goodbye')
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
3119
package-lock.json
generated
Normal file
3119
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@@ -1,22 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "11.2.6",
|
"version": "13.0.0-0",
|
||||||
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
|
"description": "A framework for real-time p2p shared editing on any data",
|
||||||
"main": "./src/y.js",
|
"main": "./src/y.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --harmony ./node_modules/.bin/gulp test",
|
"lint": "standard",
|
||||||
"lint": "./node_modules/.bin/standard"
|
"dist": "rollup -c rollup.dist.js",
|
||||||
|
"serve": "concurrently 'serve examples' 'rollup -wc rollup.dist.js -o examples/bower_components/yjs/y.js'"
|
||||||
},
|
},
|
||||||
"pre-commit": [
|
"pre-commit": [
|
||||||
"lint",
|
"lint",
|
||||||
"test"
|
"test"
|
||||||
],
|
],
|
||||||
"standard": {
|
"standard": {
|
||||||
"parser": "babel-eslint",
|
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"build/**",
|
|
||||||
"dist/**",
|
|
||||||
"declarations/**",
|
|
||||||
"./y.js",
|
"./y.js",
|
||||||
"./y.js.map"
|
"./y.js.map"
|
||||||
]
|
]
|
||||||
@@ -42,38 +39,24 @@
|
|||||||
},
|
},
|
||||||
"homepage": "http://y-js.org",
|
"homepage": "http://y-js.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^5.0.0-beta6",
|
"babel-cli": "^6.24.1",
|
||||||
"babel-plugin-transform-runtime": "^6.1.18",
|
"babel-preset-latest": "^6.24.1",
|
||||||
"babel-preset-es2015": "^6.1.18",
|
"babel-plugin-external-helpers": "^6.22.0",
|
||||||
"babelify": "^7.2.0",
|
"babel-plugin-transform-regenerator": "^6.24.1",
|
||||||
"browserify": "^12.0.1",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"eslint": "^1.10.2",
|
"chance": "^1.0.9",
|
||||||
"gulp": "^3.9.0",
|
"concurrently": "^3.4.0",
|
||||||
"gulp-bump": "^1.0.0",
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
"gulp-concat": "^2.6.0",
|
"rollup-plugin-commonjs": "^8.0.2",
|
||||||
"gulp-filter": "^3.0.1",
|
"rollup-plugin-inject": "^2.0.0",
|
||||||
"gulp-git": "^1.6.0",
|
"rollup-plugin-multi-entry": "^2.0.1",
|
||||||
"gulp-if": "^2.0.0",
|
"rollup-plugin-node-resolve": "^3.0.0",
|
||||||
"gulp-jasmine": "^2.0.1",
|
"rollup-plugin-uglify": "^1.0.2",
|
||||||
"gulp-jasmine-browser": "^0.2.3",
|
"rollup-regenerator-runtime": "^6.23.1",
|
||||||
"gulp-load-plugins": "^1.0.0",
|
"rollup-watch": "^3.2.2",
|
||||||
"gulp-prompt": "^0.1.2",
|
"standard": "^10.0.2"
|
||||||
"gulp-rename": "^1.2.2",
|
|
||||||
"gulp-serve": "^1.2.0",
|
|
||||||
"gulp-shell": "^0.5.1",
|
|
||||||
"gulp-sourcemaps": "^1.5.2",
|
|
||||||
"gulp-tag-version": "^1.3.0",
|
|
||||||
"gulp-uglify": "1.4.*",
|
|
||||||
"gulp-util": "^3.0.6",
|
|
||||||
"gulp-watch": "^4.3.5",
|
|
||||||
"minimist": "^1.2.0",
|
|
||||||
"pre-commit": "^1.1.1",
|
|
||||||
"regenerator": "^0.8.42",
|
|
||||||
"run-sequence": "^1.1.4",
|
|
||||||
"seedrandom": "^2.4.2",
|
|
||||||
"standard": "^5.2.2",
|
|
||||||
"vinyl-buffer": "^1.0.0",
|
|
||||||
"vinyl-source-stream": "^1.1.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"debug": "^2.6.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
rollup.dist.js
Normal file
47
rollup.dist.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import inject from 'rollup-plugin-inject'
|
||||||
|
import babel from 'rollup-plugin-babel'
|
||||||
|
import uglify from 'rollup-plugin-uglify'
|
||||||
|
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||||
|
import commonjs from 'rollup-plugin-commonjs'
|
||||||
|
var pkg = require('./package.json')
|
||||||
|
|
||||||
|
export default {
|
||||||
|
entry: 'src/y.js',
|
||||||
|
moduleName: 'Y',
|
||||||
|
format: 'umd',
|
||||||
|
plugins: [
|
||||||
|
nodeResolve({
|
||||||
|
main: true,
|
||||||
|
module: true,
|
||||||
|
browser: true
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
babel({
|
||||||
|
runtimeHelpers: true
|
||||||
|
}),
|
||||||
|
inject({
|
||||||
|
regeneratorRuntime: 'regenerator-runtime'
|
||||||
|
}),
|
||||||
|
uglify({
|
||||||
|
output: {
|
||||||
|
comments: function (node, comment) {
|
||||||
|
var text = comment.value
|
||||||
|
var type = comment.type
|
||||||
|
if (type === 'comment2') {
|
||||||
|
// multiline comment
|
||||||
|
return /@license/i.test(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
dest: 'y.js',
|
||||||
|
sourceMap: true,
|
||||||
|
banner: `
|
||||||
|
/**
|
||||||
|
* ${pkg.name} - ${pkg.description}
|
||||||
|
* @version v${pkg.version}
|
||||||
|
* @license ${pkg.license}
|
||||||
|
*/
|
||||||
|
`
|
||||||
|
}
|
||||||
20
rollup.test.js
Normal file
20
rollup.test.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||||
|
import commonjs from 'rollup-plugin-commonjs'
|
||||||
|
import multiEntry from 'rollup-plugin-multi-entry'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
entry: 'tests/*.js',
|
||||||
|
moduleName: 'y-array-tests',
|
||||||
|
format: 'umd',
|
||||||
|
plugins: [
|
||||||
|
nodeResolve({
|
||||||
|
main: true,
|
||||||
|
module: true,
|
||||||
|
browser: true
|
||||||
|
}),
|
||||||
|
commonjs(),
|
||||||
|
multiEntry()
|
||||||
|
],
|
||||||
|
dest: 'y-array.test.js',
|
||||||
|
sourceMap: true
|
||||||
|
}
|
||||||
311
src/Connector.js
311
src/Connector.js
@@ -1,7 +1,10 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
module.exports = function (Y/* :any */) {
|
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
||||||
|
function canWrite (auth) { return auth === 'write' }
|
||||||
|
|
||||||
|
export default function extendConnector (Y/* :any */) {
|
||||||
class AbstractConnector {
|
class AbstractConnector {
|
||||||
/* ::
|
/* ::
|
||||||
y: YConfig;
|
y: YConfig;
|
||||||
@@ -14,7 +17,6 @@ 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;
|
||||||
@@ -33,6 +35,11 @@ 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
|
||||||
|
this.preferUntransformed = opts.preferUntransformed || false
|
||||||
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') {
|
||||||
@@ -40,6 +47,8 @@ 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 = {}
|
||||||
@@ -50,34 +59,49 @@ 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.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 = []
|
||||||
return this.y.db.stopGarbageCollector()
|
this.y.db.stopGarbageCollector()
|
||||||
|
return this.y.db.whenTransactionsFinished()
|
||||||
}
|
}
|
||||||
repair () {
|
repair () {
|
||||||
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')
|
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
|
||||||
for (var name in this.connections) {
|
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 {
|
||||||
@@ -87,8 +111,12 @@ 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
|
||||||
@@ -112,10 +140,14 @@ 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
|
||||||
}
|
}
|
||||||
|
let defer = {}
|
||||||
|
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
||||||
|
this.connections[user].syncStep2 = defer
|
||||||
for (var f of this.userEventListeners) {
|
for (var f of this.userEventListeners) {
|
||||||
f({
|
f({
|
||||||
action: 'userJoined',
|
action: 'userJoined',
|
||||||
@@ -136,13 +168,8 @@ 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 || this.isSynced) {
|
if (this.currentSyncTarget != null) {
|
||||||
return // "The current sync has not finished!"
|
return // "The current sync has not finished!"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,33 +183,46 @@ module.exports = function (Y/* :any */) {
|
|||||||
var conn = this
|
var conn = this
|
||||||
if (syncUser != null) {
|
if (syncUser != null) {
|
||||||
this.currentSyncTarget = syncUser
|
this.currentSyncTarget = syncUser
|
||||||
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()
|
||||||
conn.send(syncUser, {
|
var answer = {
|
||||||
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
|
||||||
|
}
|
||||||
|
if (conn.preferUntransformed && Object.keys(stateSet).length === 0) {
|
||||||
|
answer.preferUntransformed = true
|
||||||
|
}
|
||||||
|
conn.send(syncUser, answer)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.y.db.requestTransaction(function *() {
|
if (!conn.isSynced) {
|
||||||
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
this.y.db.requestTransaction(function * () {
|
||||||
conn.isSynced = true
|
if (!conn.isSynced) {
|
||||||
yield* this.garbageCollectAfterSync()
|
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||||
// call whensynced listeners
|
conn.isSynced = true
|
||||||
for (var f of conn.whenSyncedListeners) {
|
// It is safer to remove this!
|
||||||
f()
|
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||||
}
|
// call whensynced listeners
|
||||||
conn.whenSyncedListeners = []
|
for (var f of conn.whenSyncedListeners) {
|
||||||
})
|
f()
|
||||||
|
}
|
||||||
|
conn.whenSyncedListeners = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
send (uid, message) {
|
send (uid, message) {
|
||||||
if (this.debug) {
|
this.log('Send \'%s\' to %s', message.type, uid)
|
||||||
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, message) // eslint-disable-line
|
this.logMessage('Message: %j', message)
|
||||||
}
|
}
|
||||||
|
broadcast (message) {
|
||||||
|
this.log('Broadcast \'%s\'', message.type)
|
||||||
|
this.logMessage('Message: %j', message)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Buffer operations, and broadcast them when ready.
|
Buffer operations, and broadcast them when ready.
|
||||||
@@ -203,11 +243,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
if (this.broadcastOpBuffer.length === 0) {
|
if (this.broadcastOpBuffer.length === 0) {
|
||||||
this.broadcastOpBuffer = ops
|
this.broadcastOpBuffer = ops
|
||||||
if (this.y.db.transactionInProgress) {
|
this.y.db.whenTransactionsFinished().then(broadcastOperations)
|
||||||
this.y.db.whenTransactionsFinished().then(broadcastOperations)
|
|
||||||
} else {
|
|
||||||
setTimeout(broadcastOperations, 0)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
|
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
|
||||||
}
|
}
|
||||||
@@ -217,13 +253,12 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
||||||
if (sender === this.userId) {
|
if (sender === this.userId) {
|
||||||
return
|
return Promise.resolve()
|
||||||
}
|
|
||||||
if (this.debug) {
|
|
||||||
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) {
|
||||||
console.error(
|
this.log(
|
||||||
`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)!
|
||||||
@@ -232,91 +267,125 @@ module.exports = function (Y/* :any */) {
|
|||||||
type: 'sync stop',
|
type: 'sync stop',
|
||||||
protocolVersion: this.protocolVersion
|
protocolVersion: this.protocolVersion
|
||||||
})
|
})
|
||||||
return
|
return Promise.reject(new Error('Incompatible protocol version'))
|
||||||
}
|
}
|
||||||
if (message.type === 'sync step 1') {
|
if (message.auth != null && this.connections[sender] != null) {
|
||||||
let conn = this
|
// authenticate using auth in message
|
||||||
let m = message
|
var auth = this.checkAuth(message.auth, this.y)
|
||||||
this.y.db.requestTransaction(function *() {
|
this.connections[sender].auth = auth
|
||||||
var currentStateSet = yield* this.getStateSet()
|
auth.then(auth => {
|
||||||
yield* this.applyDeleteSet(m.deleteSet)
|
for (var f of this.userEventListeners) {
|
||||||
|
f({
|
||||||
var ds = yield* this.getDeleteSet()
|
action: 'userAuthenticated',
|
||||||
var ops = yield* this.getOperations(m.stateSet)
|
user: sender,
|
||||||
conn.send(sender, {
|
auth: auth
|
||||||
type: 'sync step 2',
|
|
||||||
os: ops,
|
|
||||||
stateSet: currentStateSet,
|
|
||||||
deleteSet: ds,
|
|
||||||
protocolVersion: this.protocolVersion
|
|
||||||
})
|
|
||||||
if (this.forwardToSyncingClients) {
|
|
||||||
conn.syncingClients.push(sender)
|
|
||||||
setTimeout(function () {
|
|
||||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
|
||||||
return cli !== sender
|
|
||||||
})
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
|
||||||
}, 5000) // TODO: conn.syncingClientDuration)
|
|
||||||
} else {
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
conn._setSyncedWith(sender)
|
|
||||||
})
|
})
|
||||||
} else if (message.type === 'sync step 2') {
|
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
|
||||||
let conn = this
|
// authenticate without otherwise
|
||||||
var broadcastHB = !this.broadcastedHB
|
this.connections[sender].auth = this.checkAuth(null, this.y)
|
||||||
this.broadcastedHB = true
|
}
|
||||||
var db = this.y.db
|
if (this.connections[sender] != null && this.connections[sender].auth != null) {
|
||||||
var defer = {}
|
return this.connections[sender].auth.then((auth) => {
|
||||||
defer.promise = new Promise(function (resolve) {
|
if (message.type === 'sync step 1' && canRead(auth)) {
|
||||||
defer.resolve = resolve
|
let conn = this
|
||||||
})
|
let m = message
|
||||||
this.syncStep2 = defer.promise
|
let wait // wait for sync step 2 to complete
|
||||||
let m /* :MessageSyncStep2 */ = message
|
if (this.role === 'slave') {
|
||||||
db.requestTransaction(function * () {
|
wait = Promise.all(Object.keys(this.connections)
|
||||||
yield* this.applyDeleteSet(m.deleteSet)
|
.map(uid => this.connections[uid])
|
||||||
this.store.apply(m.os)
|
.filter(conn => conn.role === 'master')
|
||||||
db.requestTransaction(function * () {
|
.map(conn => conn.syncStep2.promise)
|
||||||
var ops = yield* this.getOperations(m.stateSet)
|
)
|
||||||
if (ops.length > 0) {
|
} else {
|
||||||
if (!broadcastHB) { // TODO: consider to broadcast here..
|
wait = Promise.resolve()
|
||||||
conn.send(sender, {
|
}
|
||||||
type: 'update',
|
wait.then(() => {
|
||||||
ops: ops
|
this.y.db.requestTransaction(function * () {
|
||||||
})
|
var currentStateSet = yield * this.getStateSet()
|
||||||
|
// TODO: remove
|
||||||
|
// if (canWrite(auth)) {
|
||||||
|
// yield * this.applyDeleteSet(m.deleteSet)
|
||||||
|
// }
|
||||||
|
|
||||||
|
var ds = yield * this.getDeleteSet()
|
||||||
|
var answer = {
|
||||||
|
type: 'sync step 2',
|
||||||
|
stateSet: currentStateSet,
|
||||||
|
deleteSet: ds,
|
||||||
|
protocolVersion: this.protocolVersion,
|
||||||
|
auth: this.authInfo
|
||||||
|
}
|
||||||
|
if (message.preferUntransformed === true && Object.keys(m.stateSet).length === 0) {
|
||||||
|
answer.osUntransformed = yield * this.getOperationsUntransformed()
|
||||||
|
} else {
|
||||||
|
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, {
|
||||||
|
type: 'sync done'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
||||||
|
var db = this.y.db
|
||||||
|
let defer = this.connections[sender].syncStep2
|
||||||
|
let m = message
|
||||||
|
// apply operations first
|
||||||
|
db.requestTransaction(function * () {
|
||||||
|
yield * this.applyDeleteSet(m.deleteSet)
|
||||||
|
if (m.osUntransformed != null) {
|
||||||
|
yield * this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
|
||||||
} else {
|
} else {
|
||||||
// broadcast only once!
|
this.store.apply(m.os)
|
||||||
conn.broadcastOps(ops)
|
}
|
||||||
|
defer.resolve()
|
||||||
|
})
|
||||||
|
/*/ then apply ds
|
||||||
|
db.whenTransactionsFinished().then(() => {
|
||||||
|
db.requestTransaction(function * () {
|
||||||
|
yield * this.applyDeleteSet(m.deleteSet)
|
||||||
|
})
|
||||||
|
defer.resolve()
|
||||||
|
})*/
|
||||||
|
return defer.promise
|
||||||
|
} else if (message.type === 'sync done') {
|
||||||
|
var self = this
|
||||||
|
this.connections[sender].syncStep2.promise.then(function () {
|
||||||
|
self._setSyncedWith(sender)
|
||||||
|
})
|
||||||
|
} else if (message.type === 'update' && canWrite(auth)) {
|
||||||
|
if (this.forwardToSyncingClients) {
|
||||||
|
for (var client of this.syncingClients) {
|
||||||
|
this.send(client, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer.resolve()
|
if (this.y.db.forwardAppliedOperations) {
|
||||||
})
|
var delops = message.ops.filter(function (o) {
|
||||||
})
|
return o.struct === 'Delete'
|
||||||
} else if (message.type === 'sync done') {
|
})
|
||||||
var self = this
|
if (delops.length > 0) {
|
||||||
this.syncStep2.then(function () {
|
this.broadcastOps(delops)
|
||||||
self._setSyncedWith(sender)
|
}
|
||||||
})
|
}
|
||||||
} else if (message.type === 'update') {
|
this.y.db.apply(message.ops)
|
||||||
if (this.forwardToSyncingClients) {
|
|
||||||
for (var client of this.syncingClients) {
|
|
||||||
this.send(client, message)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
if (this.y.db.forwardAppliedOperations) {
|
} else {
|
||||||
var delops = message.ops.filter(function (o) {
|
return Promise.reject(new Error('Unable to deliver message'))
|
||||||
return o.struct === 'Delete'
|
|
||||||
})
|
|
||||||
if (delops.length > 0) {
|
|
||||||
this.broadcastOps(delops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.y.db.apply(message.ops)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_setSyncedWith (user) {
|
_setSyncedWith (user) {
|
||||||
|
|||||||
@@ -24,11 +24,21 @@ module.exports = function (Y) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
whenTransactionsFinished: function () {
|
whenTransactionsFinished: function () {
|
||||||
var ps = []
|
var self = this
|
||||||
for (var name in this.users) {
|
return new Promise(function (resolve, reject) {
|
||||||
ps.push(this.users[name].y.db.whenTransactionsFinished())
|
// The connector first has to send the messages to the db.
|
||||||
}
|
// Wait for the checkAuth-function to resolve
|
||||||
return Promise.all(ps)
|
// The test lib only has a simple checkAuth function: `() => Promise.resolve()`
|
||||||
|
// Just add a function to the event-queue, in order to wait for the event.
|
||||||
|
// TODO: this may be buggy in test applications (but it isn't be for real-life apps)
|
||||||
|
setTimeout(function () {
|
||||||
|
var ps = []
|
||||||
|
for (var name in self.users) {
|
||||||
|
ps.push(self.users[name].y.db.whenTransactionsFinished())
|
||||||
|
}
|
||||||
|
Promise.all(ps).then(resolve, reject)
|
||||||
|
}, 10)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
flushOne: function flushOne () {
|
flushOne: function flushOne () {
|
||||||
var bufs = []
|
var bufs = []
|
||||||
@@ -54,8 +64,9 @@ module.exports = function (Y) {
|
|||||||
delete buff[sender]
|
delete buff[sender]
|
||||||
}
|
}
|
||||||
var user = globalRoom.users[userId]
|
var user = globalRoom.users[userId]
|
||||||
user.receiveMessage(m[0], m[1])
|
return user.receiveMessage(m[0], m[1]).then(function () {
|
||||||
return user.y.db.whenTransactionsFinished()
|
return user.y.db.whenTransactionsFinished()
|
||||||
|
}, function () {})
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -72,16 +83,14 @@ module.exports = function (Y) {
|
|||||||
}
|
}
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
} else {
|
} else {
|
||||||
setTimeout(function () {
|
c = globalRoom.flushOne()
|
||||||
var c = globalRoom.flushOne()
|
if (c) {
|
||||||
if (c) {
|
c.then(function () {
|
||||||
c.then(function () {
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
})
|
||||||
})
|
} else {
|
||||||
} else {
|
resolve()
|
||||||
resolve()
|
}
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
@@ -107,7 +116,7 @@ module.exports = function (Y) {
|
|||||||
this.syncingClientDuration = 0
|
this.syncingClientDuration = 0
|
||||||
}
|
}
|
||||||
receiveMessage (sender, m) {
|
receiveMessage (sender, m) {
|
||||||
super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
||||||
}
|
}
|
||||||
send (userId, message) {
|
send (userId, message) {
|
||||||
var buffer = globalRoom.buffers[userId]
|
var buffer = globalRoom.buffers[userId]
|
||||||
@@ -138,11 +147,15 @@ 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)
|
||||||
super.disconnect()
|
waitForMe = super.disconnect()
|
||||||
}
|
}
|
||||||
return this.y.db.whenTransactionsFinished()
|
var self = this
|
||||||
|
return waitForMe.then(function () {
|
||||||
|
return self.y.db.whenTransactionsFinished()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
flush () {
|
flush () {
|
||||||
var self = this
|
var self = this
|
||||||
@@ -154,7 +167,7 @@ module.exports = function (Y) {
|
|||||||
if (buff[sender].length === 0) {
|
if (buff[sender].length === 0) {
|
||||||
delete buff[sender]
|
delete buff[sender]
|
||||||
}
|
}
|
||||||
this.receiveMessage(m[0], m[1])
|
yield this.receiveMessage(m[0], m[1])
|
||||||
}
|
}
|
||||||
yield self.whenTransactionsFinished()
|
yield self.whenTransactionsFinished()
|
||||||
})
|
})
|
||||||
|
|||||||
175
src/Database.js
175
src/Database.js
@@ -1,7 +1,7 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
module.exports = function (Y /* :any */) {
|
export default function extendDatabase (Y /* :any */) {
|
||||||
/*
|
/*
|
||||||
Partial definition of an OperationStore.
|
Partial definition of an OperationStore.
|
||||||
TODO: name it Database, operation store only holds operations.
|
TODO: name it Database, operation store only holds operations.
|
||||||
@@ -39,13 +39,15 @@ module.exports = function (Y /* :any */) {
|
|||||||
*/
|
*/
|
||||||
constructor (y, opts) {
|
constructor (y, opts) {
|
||||||
this.y = y
|
this.y = y
|
||||||
|
opts.gc = opts.gc === true
|
||||||
|
this.dbOpts = opts
|
||||||
var os = this
|
var os = this
|
||||||
this.userId = null
|
this.userId = null
|
||||||
var resolve
|
var resolve_
|
||||||
this.userIdPromise = new Promise(function (r) {
|
this.userIdPromise = new Promise(function (resolve) {
|
||||||
resolve = r
|
resolve_ = resolve
|
||||||
})
|
})
|
||||||
this.userIdPromise.resolve = resolve
|
this.userIdPromise.resolve = resolve_
|
||||||
// whether to broadcast all applied operations (insert & delete hook)
|
// whether to broadcast all applied operations (insert & delete hook)
|
||||||
this.forwardAppliedOperations = false
|
this.forwardAppliedOperations = false
|
||||||
// E.g. this.listenersById[id] : Array<Listener>
|
// E.g. this.listenersById[id] : Array<Listener>
|
||||||
@@ -70,24 +72,28 @@ module.exports = function (Y /* :any */) {
|
|||||||
this.waitingTransactions = []
|
this.waitingTransactions = []
|
||||||
this.transactionInProgress = false
|
this.transactionInProgress = false
|
||||||
this.transactionIsFlushed = false
|
this.transactionIsFlushed = false
|
||||||
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
if (typeof YConcurrencyTestingMode !== 'undefined') {
|
||||||
this.executeOrder = []
|
this.executeOrder = []
|
||||||
}
|
}
|
||||||
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.gcTimeout > 0 && (os.gc1.length > 0 || os.gc2.length > 0)) {
|
||||||
if (!os.y.isConnected()) {
|
// debug
|
||||||
console.warn('gc should be empty when disconnected!')
|
if (os.y.connector.isSynced === false) {
|
||||||
|
debugger
|
||||||
|
}
|
||||||
|
if (!os.y.connector.isSynced) {
|
||||||
|
console.warn('gc should be empty when not synced!')
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
os.requestTransaction(function * () {
|
os.requestTransaction(function * () {
|
||||||
if (os.y.connector != null && os.y.connector.isSynced) {
|
if (os.y.connector != null && os.y.connector.isSynced) {
|
||||||
for (var i = 0; i < os.gc2.length; i++) {
|
for (var i = 0; i < os.gc2.length; i++) {
|
||||||
var oid = os.gc2[i]
|
var oid = os.gc2[i]
|
||||||
yield* this.garbageCollectOperation(oid)
|
yield * this.garbageCollectOperation(oid)
|
||||||
}
|
}
|
||||||
os.gc2 = os.gc1
|
os.gc2 = os.gc1
|
||||||
os.gc1 = []
|
os.gc1 = []
|
||||||
@@ -109,13 +115,23 @@ module.exports = function (Y /* :any */) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.garbageCollect = garbageCollect
|
this.garbageCollect = garbageCollect
|
||||||
if (this.gcTimeout > 0) {
|
this.startGarbageCollector()
|
||||||
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
|
||||||
|
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) {
|
||||||
@@ -149,7 +165,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
clearInterval(this.repairCheckIntervalHandler)
|
clearInterval(this.repairCheckIntervalHandler)
|
||||||
}
|
}
|
||||||
queueGarbageCollector (id) {
|
queueGarbageCollector (id) {
|
||||||
if (this.y.isConnected()) {
|
if (this.y.connector.isSynced && this.gc) {
|
||||||
this.gc1.push(id)
|
this.gc1.push(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +182,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
addToDebug () {
|
addToDebug () {
|
||||||
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
if (typeof YConcurrencyTestingMode !== 'undefined') {
|
||||||
var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
|
var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
|
||||||
if (typeof s === 'string') {
|
if (typeof s === 'string') {
|
||||||
return s
|
return s
|
||||||
@@ -182,16 +198,18 @@ 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)
|
||||||
self.gc1 = []
|
self.gc1 = []
|
||||||
self.gc2 = []
|
self.gc2 = []
|
||||||
for (var i = 0; i < ungc.length; i++) {
|
for (var i = 0; i < ungc.length; i++) {
|
||||||
var op = yield* this.getOperation(ungc[i])
|
var op = yield * this.getOperation(ungc[i])
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
delete op.gc
|
delete op.gc
|
||||||
yield* this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve()
|
resolve()
|
||||||
@@ -204,7 +222,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
TODO: rename this function
|
TODO: rename this function
|
||||||
|
|
||||||
Rulez:
|
Rulez:
|
||||||
* Only gc if this user is online
|
* Only gc if this user is online & gc turned on
|
||||||
* The most left element in a list must not be gc'd.
|
* 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
|
||||||
|
|
||||||
@@ -213,18 +231,20 @@ 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) {
|
||||||
gc = true
|
gc = true
|
||||||
} else if (op.content != null && op.content.length > 1) {
|
} else if (op.content != null && op.content.length > 1) {
|
||||||
op = yield* this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
|
op = yield * this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
|
||||||
gc = true
|
gc = true
|
||||||
}
|
}
|
||||||
if (gc) {
|
if (gc) {
|
||||||
op.gc = true
|
op.gc = true
|
||||||
yield* this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
this.store.queueGarbageCollector(op.id)
|
this.store.queueGarbageCollector(op.id)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -239,10 +259,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
this.gc2 = this.gc2.filter(filter)
|
this.gc2 = this.gc2.filter(filter)
|
||||||
delete op.gc
|
delete op.gc
|
||||||
}
|
}
|
||||||
* destroy () {
|
destroyTypes () {
|
||||||
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) {
|
||||||
@@ -252,13 +269,18 @@ module.exports = function (Y /* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
* destroy () {
|
||||||
|
clearTimeout(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
|
||||||
var self = this
|
var self = this
|
||||||
self.requestTransaction(function * () {
|
self.requestTransaction(function * () {
|
||||||
self.userId = userId
|
self.userId = userId
|
||||||
var state = yield* this.getState(userId)
|
var state = yield * this.getState(userId)
|
||||||
self.opClock = state.clock
|
self.opClock = state.clock
|
||||||
self.userIdPromise.resolve(userId)
|
self.userIdPromise.resolve(userId)
|
||||||
})
|
})
|
||||||
@@ -346,7 +368,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
|
|
||||||
for (let key = 0; key < exeNow.length; key++) {
|
for (let key = 0; key < exeNow.length; key++) {
|
||||||
let o = exeNow[key].op
|
let o = exeNow[key].op
|
||||||
yield* store.tryExecute.call(this, o)
|
yield * store.tryExecute.call(this, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var sid in ls) {
|
for (var sid in ls) {
|
||||||
@@ -354,9 +376,9 @@ module.exports = function (Y /* :any */) {
|
|||||||
var id = JSON.parse(sid)
|
var id = JSON.parse(sid)
|
||||||
var op
|
var op
|
||||||
if (typeof id[1] === 'string') {
|
if (typeof id[1] === 'string') {
|
||||||
op = yield* this.getOperation(id)
|
op = yield * this.getOperation(id)
|
||||||
} else {
|
} else {
|
||||||
op = yield* this.getInsertion(id)
|
op = yield * this.getInsertion(id)
|
||||||
}
|
}
|
||||||
if (op == null) {
|
if (op == null) {
|
||||||
store.listenersById[sid] = l
|
store.listenersById[sid] = l
|
||||||
@@ -365,7 +387,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
let listener = l[i]
|
let listener = l[i]
|
||||||
let o = listener.op
|
let o = listener.op
|
||||||
if (--listener.missing === 0) {
|
if (--listener.missing === 0) {
|
||||||
yield* store.tryExecute.call(this, o)
|
yield * store.tryExecute.call(this, o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,12 +407,12 @@ module.exports = function (Y /* :any */) {
|
|||||||
* tryExecute (op) {
|
* tryExecute (op) {
|
||||||
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||||
if (op.struct === 'Delete') {
|
if (op.struct === 'Delete') {
|
||||||
yield* Y.Struct.Delete.execute.call(this, op)
|
yield * Y.Struct.Delete.execute.call(this, op)
|
||||||
// this is now called in Transaction.deleteOperation!
|
// this is now called in Transaction.deleteOperation!
|
||||||
// yield* this.store.operationAdded(this, op)
|
// yield* this.store.operationAdded(this, op)
|
||||||
} else {
|
} else {
|
||||||
// check if this op was defined
|
// check if this op was defined
|
||||||
var defined = yield* this.getInsertion(op.id)
|
var defined = yield * this.getInsertion(op.id)
|
||||||
while (defined != null && defined.content != null) {
|
while (defined != null && defined.content != null) {
|
||||||
// check if this op has a longer content in the case it is defined
|
// check if this op has a longer content in the case it is defined
|
||||||
if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
|
if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
|
||||||
@@ -399,23 +421,23 @@ module.exports = function (Y /* :any */) {
|
|||||||
op.id = [op.id[0], op.id[1] + overlapSize]
|
op.id = [op.id[0], op.id[1] + overlapSize]
|
||||||
op.left = Y.utils.getLastId(defined)
|
op.left = Y.utils.getLastId(defined)
|
||||||
op.origin = op.left
|
op.origin = op.left
|
||||||
defined = yield* this.getOperation(op.id) // getOperation suffices here
|
defined = yield * this.getOperation(op.id) // getOperation suffices here
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (defined == null) {
|
if (defined == null) {
|
||||||
var opid = op.id
|
var opid = op.id
|
||||||
var isGarbageCollected = yield* this.isGarbageCollected(opid)
|
var isGarbageCollected = yield * this.isGarbageCollected(opid)
|
||||||
if (!isGarbageCollected) {
|
if (!isGarbageCollected) {
|
||||||
// TODO: reduce number of get / put calls for op ..
|
// TODO: reduce number of get / put calls for op ..
|
||||||
yield* Y.Struct[op.struct].execute.call(this, op)
|
yield * Y.Struct[op.struct].execute.call(this, op)
|
||||||
yield* this.addOperation(op)
|
yield * this.addOperation(op)
|
||||||
yield* this.store.operationAdded(this, op)
|
yield * this.store.operationAdded(this, op)
|
||||||
// operationAdded can change op..
|
// operationAdded can change op..
|
||||||
op = yield* this.getOperation(opid)
|
op = yield * this.getOperation(opid)
|
||||||
// if insertion, try to combine with left
|
// if insertion, try to combine with left
|
||||||
yield* this.tryCombineWithLeft(op)
|
yield * this.tryCombineWithLeft(op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,14 +456,13 @@ module.exports = function (Y /* :any */) {
|
|||||||
*/
|
*/
|
||||||
* operationAdded (transaction, op) {
|
* operationAdded (transaction, op) {
|
||||||
if (op.struct === 'Delete') {
|
if (op.struct === 'Delete') {
|
||||||
var target = yield* transaction.getInsertion(op.target)
|
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
|
||||||
var type = this.initializedTypes[JSON.stringify(target.parent)]
|
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
yield* type._changed(transaction, op)
|
yield * type._changed(transaction, op)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// increase SS
|
// increase SS
|
||||||
yield* transaction.updateState(op.id[0])
|
yield * transaction.updateState(op.id[0])
|
||||||
var opLen = op.content != null ? op.content.length : 1
|
var opLen = op.content != null ? op.content.length : 1
|
||||||
for (let i = 0; i < opLen; i++) {
|
for (let i = 0; i < opLen; i++) {
|
||||||
// notify whenOperation listeners (by id)
|
// notify whenOperation listeners (by id)
|
||||||
@@ -461,9 +482,9 @@ module.exports = function (Y /* :any */) {
|
|||||||
|
|
||||||
// if parent is deleted, mark as gc'd and return
|
// if parent is deleted, mark as gc'd and return
|
||||||
if (op.parent != null) {
|
if (op.parent != null) {
|
||||||
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
|
var parentIsDeleted = yield * transaction.isDeleted(op.parent)
|
||||||
if (parentIsDeleted) {
|
if (parentIsDeleted) {
|
||||||
yield* transaction.deleteList(op.id)
|
yield * transaction.deleteList(op.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,7 +492,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
// notify parent, if it was instanciated as a custom type
|
// notify parent, if it was instanciated as a custom type
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
let o = Y.utils.copyOperation(op)
|
let o = Y.utils.copyOperation(op)
|
||||||
yield* t._changed(transaction, o)
|
yield * t._changed(transaction, o)
|
||||||
}
|
}
|
||||||
if (!op.deleted) {
|
if (!op.deleted) {
|
||||||
// Delete if DS says this is actually deleted
|
// Delete if DS says this is actually deleted
|
||||||
@@ -480,13 +501,13 @@ module.exports = function (Y /* :any */) {
|
|||||||
// TODO: !! console.log('TODO: change this before commiting')
|
// TODO: !! console.log('TODO: change this before commiting')
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
var id = [startId[0], startId[1] + i]
|
var id = [startId[0], startId[1] + i]
|
||||||
var opIsDeleted = yield* transaction.isDeleted(id)
|
var opIsDeleted = yield * transaction.isDeleted(id)
|
||||||
if (opIsDeleted) {
|
if (opIsDeleted) {
|
||||||
var delop = {
|
var delop = {
|
||||||
struct: 'Delete',
|
struct: 'Delete',
|
||||||
target: id
|
target: id
|
||||||
}
|
}
|
||||||
yield* this.tryExecute.call(transaction, delop)
|
yield * this.tryExecute.call(transaction, delop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,18 +516,16 @@ module.exports = function (Y /* :any */) {
|
|||||||
whenTransactionsFinished () {
|
whenTransactionsFinished () {
|
||||||
if (this.transactionInProgress) {
|
if (this.transactionInProgress) {
|
||||||
if (this.transactionsFinished == null) {
|
if (this.transactionsFinished == null) {
|
||||||
var resolve
|
var resolve_
|
||||||
var promise = new Promise(function (r) {
|
var promise = new Promise(function (resolve) {
|
||||||
resolve = r
|
resolve_ = resolve
|
||||||
})
|
})
|
||||||
this.transactionsFinished = {
|
this.transactionsFinished = {
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -526,7 +545,7 @@ module.exports = function (Y /* :any */) {
|
|||||||
} else {
|
} else {
|
||||||
this.transactionIsFlushed = true
|
this.transactionIsFlushed = true
|
||||||
return function * () {
|
return function * () {
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -543,6 +562,48 @@ module.exports = function (Y /* :any */) {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
Get a created/initialized type.
|
||||||
|
*/
|
||||||
|
getType (id) {
|
||||||
|
return this.initializedTypes[JSON.stringify(id)]
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Init type. This is called when a remote operation is retrieved, and transformed to a type
|
||||||
|
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
|
||||||
|
*/
|
||||||
|
* initType (id, args) {
|
||||||
|
var sid = JSON.stringify(id)
|
||||||
|
var t = this.store.initializedTypes[sid]
|
||||||
|
if (t == null) {
|
||||||
|
var op/* :MapStruct | ListStruct */ = yield * this.getOperation(id)
|
||||||
|
if (op != null) {
|
||||||
|
t = yield * Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
|
||||||
|
this.store.initializedTypes[sid] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Create type. This is called when the local user creates a type (which is a synchronous action)
|
||||||
|
*/
|
||||||
|
createType (typedefinition, id) {
|
||||||
|
var structname = typedefinition[0].struct
|
||||||
|
id = id || this.getNextOpId(1)
|
||||||
|
var op = Y.Struct[structname].create(id)
|
||||||
|
op.type = typedefinition[0].name
|
||||||
|
|
||||||
|
this.requestTransaction(function * () {
|
||||||
|
if (op.id[0] === '_') {
|
||||||
|
yield * this.setOperation(op)
|
||||||
|
} else {
|
||||||
|
yield * this.applyCreatedOperations([op])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])
|
||||||
|
this.initializedTypes[JSON.stringify(op.id)] = t
|
||||||
|
return t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Y.AbstractDatabase = AbstractDatabase
|
Y.AbstractDatabase = AbstractDatabase
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,141 +17,141 @@ for (let database of databases) {
|
|||||||
})
|
})
|
||||||
afterEach(function (done) {
|
afterEach(function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.store.destroy()
|
yield * this.store.destroy()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('Deleted operation is deleted', async(function * (done) {
|
it('Deleted operation is deleted', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['u1', 10], 1)
|
yield * this.markDeleted(['u1', 10], 1)
|
||||||
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['u1', 10], 1)
|
yield * this.markDeleted(['u1', 10], 1)
|
||||||
yield* this.markDeleted(['u1', 11], 1)
|
yield * this.markDeleted(['u1', 11], 1)
|
||||||
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
|
||||||
expect(yield* this.isDeleted(['u1', 11])).toBeTruthy()
|
expect(yield * this.isDeleted(['u1', 11])).toBeTruthy()
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['0', 3], 1)
|
yield * this.markDeleted(['0', 3], 1)
|
||||||
yield* this.markDeleted(['0', 4], 1)
|
yield * this.markDeleted(['0', 4], 1)
|
||||||
yield* this.markDeleted(['0', 2], 1)
|
yield * this.markDeleted(['0', 2], 1)
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #1', async(function * (done) {
|
it('Debug #1', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['166', 0], 1)
|
yield * this.markDeleted(['166', 0], 1)
|
||||||
yield* this.markDeleted(['166', 2], 1)
|
yield * this.markDeleted(['166', 2], 1)
|
||||||
yield* this.markDeleted(['166', 0], 1)
|
yield * this.markDeleted(['166', 0], 1)
|
||||||
yield* this.markDeleted(['166', 2], 1)
|
yield * this.markDeleted(['166', 2], 1)
|
||||||
yield* this.markGarbageCollected(['166', 2], 1)
|
yield * this.markGarbageCollected(['166', 2], 1)
|
||||||
yield* this.markDeleted(['166', 1], 1)
|
yield * this.markDeleted(['166', 1], 1)
|
||||||
yield* this.markDeleted(['166', 3], 1)
|
yield * this.markDeleted(['166', 3], 1)
|
||||||
yield* this.markGarbageCollected(['166', 3], 1)
|
yield * this.markGarbageCollected(['166', 3], 1)
|
||||||
yield* this.markDeleted(['166', 0], 1)
|
yield * this.markDeleted(['166', 0], 1)
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
|
expect(yield * this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #2', async(function * (done) {
|
it('Debug #2', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['293', 0], 1)
|
yield * this.markDeleted(['293', 0], 1)
|
||||||
yield* this.markDeleted(['291', 2], 1)
|
yield * this.markDeleted(['291', 2], 1)
|
||||||
yield* this.markDeleted(['291', 2], 1)
|
yield * this.markDeleted(['291', 2], 1)
|
||||||
yield* this.markGarbageCollected(['293', 0], 1)
|
yield * this.markGarbageCollected(['293', 0], 1)
|
||||||
yield* this.markDeleted(['293', 1], 1)
|
yield * this.markDeleted(['293', 1], 1)
|
||||||
yield* this.markGarbageCollected(['291', 2], 1)
|
yield * this.markGarbageCollected(['291', 2], 1)
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #3', async(function * (done) {
|
it('Debug #3', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['581', 0], 1)
|
yield * this.markDeleted(['581', 0], 1)
|
||||||
yield* this.markDeleted(['581', 1], 1)
|
yield * this.markDeleted(['581', 1], 1)
|
||||||
yield* this.markDeleted(['580', 0], 1)
|
yield * this.markDeleted(['580', 0], 1)
|
||||||
yield* this.markDeleted(['580', 0], 1)
|
yield * this.markDeleted(['580', 0], 1)
|
||||||
yield* this.markGarbageCollected(['581', 0], 1)
|
yield * this.markGarbageCollected(['581', 0], 1)
|
||||||
yield* this.markDeleted(['581', 2], 1)
|
yield * this.markDeleted(['581', 2], 1)
|
||||||
yield* this.markDeleted(['580', 1], 1)
|
yield * this.markDeleted(['580', 1], 1)
|
||||||
yield* this.markDeleted(['580', 2], 1)
|
yield * this.markDeleted(['580', 2], 1)
|
||||||
yield* this.markDeleted(['580', 1], 1)
|
yield * this.markDeleted(['580', 1], 1)
|
||||||
yield* this.markDeleted(['580', 2], 1)
|
yield * this.markDeleted(['580', 2], 1)
|
||||||
yield* this.markGarbageCollected(['581', 2], 1)
|
yield * this.markGarbageCollected(['581', 2], 1)
|
||||||
yield* this.markGarbageCollected(['581', 1], 1)
|
yield * this.markGarbageCollected(['581', 1], 1)
|
||||||
yield* this.markGarbageCollected(['580', 1], 1)
|
yield * this.markGarbageCollected(['580', 1], 1)
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
|
expect(yield * this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #4', async(function * (done) {
|
it('Debug #4', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['544', 0], 1)
|
yield * this.markDeleted(['544', 0], 1)
|
||||||
yield* this.markDeleted(['543', 2], 1)
|
yield * this.markDeleted(['543', 2], 1)
|
||||||
yield* this.markDeleted(['544', 0], 1)
|
yield * this.markDeleted(['544', 0], 1)
|
||||||
yield* this.markDeleted(['543', 2], 1)
|
yield * this.markDeleted(['543', 2], 1)
|
||||||
yield* this.markGarbageCollected(['544', 0], 1)
|
yield * this.markGarbageCollected(['544', 0], 1)
|
||||||
yield* this.markDeleted(['545', 1], 1)
|
yield * this.markDeleted(['545', 1], 1)
|
||||||
yield* this.markDeleted(['543', 4], 1)
|
yield * this.markDeleted(['543', 4], 1)
|
||||||
yield* this.markDeleted(['543', 3], 1)
|
yield * this.markDeleted(['543', 3], 1)
|
||||||
yield* this.markDeleted(['544', 1], 1)
|
yield * this.markDeleted(['544', 1], 1)
|
||||||
yield* this.markDeleted(['544', 2], 1)
|
yield * this.markDeleted(['544', 2], 1)
|
||||||
yield* this.markDeleted(['544', 1], 1)
|
yield * this.markDeleted(['544', 1], 1)
|
||||||
yield* this.markDeleted(['544', 2], 1)
|
yield * this.markDeleted(['544', 2], 1)
|
||||||
yield* this.markGarbageCollected(['543', 2], 1)
|
yield * this.markGarbageCollected(['543', 2], 1)
|
||||||
yield* this.markGarbageCollected(['543', 4], 1)
|
yield * this.markGarbageCollected(['543', 4], 1)
|
||||||
yield* this.markGarbageCollected(['544', 2], 1)
|
yield * this.markGarbageCollected(['544', 2], 1)
|
||||||
yield* this.markGarbageCollected(['543', 3], 1)
|
yield * this.markGarbageCollected(['543', 3], 1)
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #5', async(function * (done) {
|
it('Debug #5', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
||||||
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #6', async(function * (done) {
|
it('Debug #6', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.applyDeleteSet({'40': [[0, 3, false]]})
|
yield * this.applyDeleteSet({'40': [[0, 3, false]]})
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
|
||||||
yield* this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
yield * this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
it('Debug #7', async(function * (done) {
|
it('Debug #7', async(function * (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.markDeleted(['9', 2], 1)
|
yield * this.markDeleted(['9', 2], 1)
|
||||||
yield* this.markDeleted(['11', 2], 1)
|
yield * this.markDeleted(['11', 2], 1)
|
||||||
yield* this.markDeleted(['11', 4], 1)
|
yield * this.markDeleted(['11', 4], 1)
|
||||||
yield* this.markDeleted(['11', 1], 1)
|
yield * this.markDeleted(['11', 1], 1)
|
||||||
yield* this.markDeleted(['9', 4], 1)
|
yield * this.markDeleted(['9', 4], 1)
|
||||||
yield* this.markDeleted(['10', 0], 1)
|
yield * this.markDeleted(['10', 0], 1)
|
||||||
yield* this.markGarbageCollected(['11', 2], 1)
|
yield * this.markGarbageCollected(['11', 2], 1)
|
||||||
yield* this.markDeleted(['11', 2], 1)
|
yield * this.markDeleted(['11', 2], 1)
|
||||||
yield* this.markGarbageCollected(['11', 3], 1)
|
yield * this.markGarbageCollected(['11', 3], 1)
|
||||||
yield* this.markDeleted(['11', 3], 1)
|
yield * this.markDeleted(['11', 3], 1)
|
||||||
yield* this.markDeleted(['11', 3], 1)
|
yield * this.markDeleted(['11', 3], 1)
|
||||||
yield* this.markDeleted(['9', 4], 1)
|
yield * this.markDeleted(['9', 4], 1)
|
||||||
yield* this.markDeleted(['10', 0], 1)
|
yield * this.markDeleted(['10', 0], 1)
|
||||||
yield* this.markGarbageCollected(['11', 1], 1)
|
yield * this.markGarbageCollected(['11', 1], 1)
|
||||||
yield* this.markDeleted(['11', 1], 1)
|
yield * this.markDeleted(['11', 1], 1)
|
||||||
expect(yield* this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
|
expect(yield * this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
@@ -167,54 +167,54 @@ for (let database of databases) {
|
|||||||
})
|
})
|
||||||
afterEach(function (done) {
|
afterEach(function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.store.destroy()
|
yield * this.store.destroy()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('debug #1', function (done) {
|
it('debug #1', function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.put({id: [2]})
|
yield * this.os.put({id: [2]})
|
||||||
yield* this.os.put({id: [0]})
|
yield * this.os.put({id: [0]})
|
||||||
yield* this.os.delete([2])
|
yield * this.os.delete([2])
|
||||||
yield* this.os.put({id: [1]})
|
yield * this.os.put({id: [1]})
|
||||||
expect(yield* this.os.find([0])).toBeTruthy()
|
expect(yield * this.os.find([0])).toBeTruthy()
|
||||||
expect(yield* this.os.find([1])).toBeTruthy()
|
expect(yield * this.os.find([1])).toBeTruthy()
|
||||||
expect(yield* this.os.find([2])).toBeFalsy()
|
expect(yield * this.os.find([2])).toBeFalsy()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('can add&retrieve 5 elements', function (done) {
|
it('can add&retrieve 5 elements', function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.put({val: 'four', id: [4]})
|
yield * this.os.put({val: 'four', id: [4]})
|
||||||
yield* this.os.put({val: 'one', id: [1]})
|
yield * this.os.put({val: 'one', id: [1]})
|
||||||
yield* this.os.put({val: 'three', id: [3]})
|
yield * this.os.put({val: 'three', id: [3]})
|
||||||
yield* this.os.put({val: 'two', id: [2]})
|
yield * this.os.put({val: 'two', id: [2]})
|
||||||
yield* this.os.put({val: 'five', id: [5]})
|
yield * this.os.put({val: 'five', id: [5]})
|
||||||
expect((yield* this.os.find([1])).val).toEqual('one')
|
expect((yield * this.os.find([1])).val).toEqual('one')
|
||||||
expect((yield* this.os.find([2])).val).toEqual('two')
|
expect((yield * this.os.find([2])).val).toEqual('two')
|
||||||
expect((yield* this.os.find([3])).val).toEqual('three')
|
expect((yield * this.os.find([3])).val).toEqual('three')
|
||||||
expect((yield* this.os.find([4])).val).toEqual('four')
|
expect((yield * this.os.find([4])).val).toEqual('four')
|
||||||
expect((yield* this.os.find([5])).val).toEqual('five')
|
expect((yield * this.os.find([5])).val).toEqual('five')
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('5 elements do not exist anymore after deleting them', function (done) {
|
it('5 elements do not exist anymore after deleting them', function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.put({val: 'four', id: [4]})
|
yield * this.os.put({val: 'four', id: [4]})
|
||||||
yield* this.os.put({val: 'one', id: [1]})
|
yield * this.os.put({val: 'one', id: [1]})
|
||||||
yield* this.os.put({val: 'three', id: [3]})
|
yield * this.os.put({val: 'three', id: [3]})
|
||||||
yield* this.os.put({val: 'two', id: [2]})
|
yield * this.os.put({val: 'two', id: [2]})
|
||||||
yield* this.os.put({val: 'five', id: [5]})
|
yield * this.os.put({val: 'five', id: [5]})
|
||||||
yield* this.os.delete([4])
|
yield * this.os.delete([4])
|
||||||
expect(yield* this.os.find([4])).not.toBeTruthy()
|
expect(yield * this.os.find([4])).not.toBeTruthy()
|
||||||
yield* this.os.delete([3])
|
yield * this.os.delete([3])
|
||||||
expect(yield* this.os.find([3])).not.toBeTruthy()
|
expect(yield * this.os.find([3])).not.toBeTruthy()
|
||||||
yield* this.os.delete([2])
|
yield * this.os.delete([2])
|
||||||
expect(yield* this.os.find([2])).not.toBeTruthy()
|
expect(yield * this.os.find([2])).not.toBeTruthy()
|
||||||
yield* this.os.delete([1])
|
yield * this.os.delete([1])
|
||||||
expect(yield* this.os.find([1])).not.toBeTruthy()
|
expect(yield * this.os.find([1])).not.toBeTruthy()
|
||||||
yield* this.os.delete([5])
|
yield * this.os.delete([5])
|
||||||
expect(yield* this.os.find([5])).not.toBeTruthy()
|
expect(yield * this.os.find([5])).not.toBeTruthy()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -232,9 +232,9 @@ for (let database of databases) {
|
|||||||
var r = Math.random()
|
var r = Math.random()
|
||||||
if (r < 0.8) {
|
if (r < 0.8) {
|
||||||
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
|
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
|
||||||
if (!(yield* this.os.find(obj))) {
|
if (!(yield * this.os.find(obj))) {
|
||||||
elements.push(obj)
|
elements.push(obj)
|
||||||
yield* this.os.put({id: obj})
|
yield * this.os.put({id: obj})
|
||||||
}
|
}
|
||||||
} else if (elements.length > 0) {
|
} else if (elements.length > 0) {
|
||||||
var elemid = Math.floor(Math.random() * elements.length)
|
var elemid = Math.floor(Math.random() * elements.length)
|
||||||
@@ -242,7 +242,7 @@ for (let database of databases) {
|
|||||||
elements = elements.filter(function (e) {
|
elements = elements.filter(function (e) {
|
||||||
return !Y.utils.compareIds(e, elem)
|
return !Y.utils.compareIds(e, elem)
|
||||||
})
|
})
|
||||||
yield* this.os.delete(elem)
|
yield * this.os.delete(elem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
done()
|
done()
|
||||||
@@ -250,14 +250,14 @@ for (let database of databases) {
|
|||||||
})
|
})
|
||||||
afterAll(function (done) {
|
afterAll(function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.store.destroy()
|
yield * this.store.destroy()
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('can find every object', function (done) {
|
it('can find every object', function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
for (var id of elements) {
|
for (var id of elements) {
|
||||||
expect((yield* this.os.find(id)).id).toEqual(id)
|
expect((yield * this.os.find(id)).id).toEqual(id)
|
||||||
}
|
}
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
@@ -266,7 +266,7 @@ for (let database of databases) {
|
|||||||
it('can find every object with lower bound search', function (done) {
|
it('can find every object with lower bound search', function (done) {
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
for (var id of elements) {
|
for (var id of elements) {
|
||||||
var e = yield* this.os.findWithLowerBound(id)
|
var e = yield * this.os.findWithLowerBound(id)
|
||||||
expect(e.id).toEqual(id)
|
expect(e.id).toEqual(id)
|
||||||
}
|
}
|
||||||
done()
|
done()
|
||||||
@@ -281,7 +281,7 @@ for (let database of databases) {
|
|||||||
|
|
||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.iterate(this, lowerBound, null, function * (val) {
|
yield * this.os.iterate(this, lowerBound, null, function * (val) {
|
||||||
expect(val).toBeDefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
@@ -297,7 +297,7 @@ for (let database of databases) {
|
|||||||
}).length
|
}).length
|
||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.iterate(this, lowerBound, null, function * (val) {
|
yield * this.os.iterate(this, lowerBound, null, function * (val) {
|
||||||
expect(val).toBeDefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
@@ -314,7 +314,7 @@ for (let database of databases) {
|
|||||||
|
|
||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.iterate(this, null, upperBound, function * (val) {
|
yield * this.os.iterate(this, null, upperBound, function * (val) {
|
||||||
expect(val).toBeDefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
@@ -340,7 +340,7 @@ for (let database of databases) {
|
|||||||
}).length
|
}).length
|
||||||
var actualResults = 0
|
var actualResults = 0
|
||||||
store.requestTransaction(function * () {
|
store.requestTransaction(function * () {
|
||||||
yield* this.os.iterate(this, lowerBound, upperBound, function * (val) {
|
yield * this.os.iterate(this, lowerBound, upperBound, function * (val) {
|
||||||
expect(val).toBeDefined()
|
expect(val).toBeDefined()
|
||||||
actualResults++
|
actualResults++
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ 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
|
||||||
@@ -48,7 +46,7 @@ g.setRandomSeed = function setRandomSeed (seed) {
|
|||||||
|
|
||||||
g.generateRandomSeed()
|
g.generateRandomSeed()
|
||||||
|
|
||||||
g.YConcurrency_TestingMode = true
|
g.YConcurrencyTestingMode = true
|
||||||
|
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
|
||||||
|
|
||||||
@@ -121,6 +119,10 @@ function * applyTransactions (relAmount, numberOfTransactions, objects, users, t
|
|||||||
// There will be an artificial delay until ops can be executed by the type,
|
// 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
|
||||||
@@ -156,13 +158,17 @@ 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
|
||||||
type.os.requestTransaction(function * () {
|
type.os.requestTransaction(function * () {
|
||||||
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
|
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
|
||||||
type.eventHandler._debuggingAwaiting = false
|
type.eventHandler._debuggingAwaiting = false
|
||||||
yield* type.eventHandler.awaitOps(this, function * () { /* mock function */ })
|
yield * type.eventHandler.awaitOps(this, function * () { /* mock function */ })
|
||||||
}
|
}
|
||||||
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
|
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
|
||||||
})
|
})
|
||||||
@@ -172,13 +178,13 @@ function fixAwaitingInType (type) {
|
|||||||
g.fixAwaitingInType = fixAwaitingInType
|
g.fixAwaitingInType = fixAwaitingInType
|
||||||
|
|
||||||
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||||
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
|
yield * applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
|
||||||
yield Y.utils.globalRoom.flushAll()
|
yield Y.utils.globalRoom.flushAll()
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
})
|
})
|
||||||
|
|
||||||
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||||
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
yield * applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
yield Y.utils.globalRoom.flushAll()
|
yield Y.utils.globalRoom.flushAll()
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
@@ -194,7 +200,7 @@ g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransaction
|
|||||||
})
|
})
|
||||||
|
|
||||||
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||||
yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
yield * applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
||||||
yield Y.utils.globalRoom.flushAll()
|
yield Y.utils.globalRoom.flushAll()
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
for (var u in users) {
|
for (var u in users) {
|
||||||
@@ -220,26 +226,38 @@ 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()
|
||||||
ds1 = yield* this.getDeleteSet()
|
ds1 = yield * this.getDeleteSet()
|
||||||
allDels1 = []
|
allDels1 = []
|
||||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
yield * this.ds.iterate(this, null, null, function * (d) {
|
||||||
allDels1.push(d)
|
allDels1.push(d)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function * t2 () {
|
function * t2 () {
|
||||||
s2 = yield* this.getStateSet()
|
s2 = yield * this.getStateSet()
|
||||||
ds2 = yield* this.getDeleteSet()
|
ds2 = yield * this.getDeleteSet()
|
||||||
allDels2 = []
|
allDels2 = []
|
||||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
yield * this.ds.iterate(this, null, null, function * (d) {
|
||||||
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) {
|
||||||
@@ -251,25 +269,25 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
|
|||||||
for (var uid = 0; uid < users.length; uid++) {
|
for (var uid = 0; uid < users.length; uid++) {
|
||||||
var u = users[uid]
|
var u = users[uid]
|
||||||
u.db.requestTransaction(function * () {
|
u.db.requestTransaction(function * () {
|
||||||
var sv = yield* this.getStateVector()
|
var sv = yield * this.getStateVector()
|
||||||
for (var s of sv) {
|
for (var s of sv) {
|
||||||
yield* this.updateState(s.user)
|
yield * this.updateState(s.user)
|
||||||
}
|
}
|
||||||
// compare deleted ops against deleteStore
|
// compare deleted ops against deleteStore
|
||||||
yield* this.os.iterate(this, null, null, function * (o) {
|
yield * this.os.iterate(this, null, null, function * (o) {
|
||||||
if (o.deleted === true) {
|
if (o.deleted === true) {
|
||||||
expect(yield* this.isDeleted(o.id)).toBeTruthy()
|
expect(yield * this.isDeleted(o.id)).toBeTruthy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// compare deleteStore against deleted ops
|
// compare deleteStore against deleted ops
|
||||||
var ds = []
|
var ds = []
|
||||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
yield * this.ds.iterate(this, null, null, function * (d) {
|
||||||
ds.push(d)
|
ds.push(d)
|
||||||
})
|
})
|
||||||
for (var j in ds) {
|
for (var j in ds) {
|
||||||
var d = ds[j]
|
var d = ds[j]
|
||||||
for (var i = 0; i < d.len; i++) {
|
for (var i = 0; i < d.len; i++) {
|
||||||
var o = yield* this.getInsertion([d.id[0], d.id[1] + i])
|
var o = yield * this.getInsertion([d.id[0], d.id[1] + i])
|
||||||
// gc'd or deleted
|
// gc'd or deleted
|
||||||
if (d.gc) {
|
if (d.gc) {
|
||||||
expect(o).toBeFalsy()
|
expect(o).toBeFalsy()
|
||||||
@@ -282,8 +300,8 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
|
|||||||
// compare allDels tree
|
// compare allDels tree
|
||||||
if (s1 == null) {
|
if (s1 == null) {
|
||||||
u.db.requestTransaction(function * () {
|
u.db.requestTransaction(function * () {
|
||||||
yield* t1.call(this)
|
yield * t1.call(this)
|
||||||
yield* this.os.iterate(this, null, null, function * (o) {
|
yield * this.os.iterate(this, null, null, function * (o) {
|
||||||
o = Y.utils.copyObject(o)
|
o = Y.utils.copyObject(o)
|
||||||
delete o.origin
|
delete o.origin
|
||||||
delete o.originOf
|
delete o.originOf
|
||||||
@@ -292,9 +310,9 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
u.db.requestTransaction(function * () {
|
u.db.requestTransaction(function * () {
|
||||||
yield* t2.call(this)
|
yield * t2.call(this)
|
||||||
var db2 = []
|
var db2 = []
|
||||||
yield* this.os.iterate(this, null, null, function * (o) {
|
yield * this.os.iterate(this, null, null, function * (o) {
|
||||||
o = Y.utils.copyObject(o)
|
o = Y.utils.copyObject(o)
|
||||||
delete o.origin
|
delete o.origin
|
||||||
delete o.originOf
|
delete o.originOf
|
||||||
@@ -330,6 +348,7 @@ 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: {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
* requiredOps
|
* requiredOps
|
||||||
- Operations that are required to execute this operation.
|
- Operations that are required to execute this operation.
|
||||||
*/
|
*/
|
||||||
module.exports = function (Y/* :any */) {
|
export default function extendStruct (Y) {
|
||||||
var Struct = {
|
var Struct = {
|
||||||
/* This is the only operation that is actually not a structure, because
|
/* This is the only operation that is actually not a structure, because
|
||||||
it is not stored in the OS. This is why it _does not_ have an id
|
it is not stored in the OS. This is why it _does not_ have an id
|
||||||
@@ -30,13 +30,17 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
Delete: {
|
Delete: {
|
||||||
encode: function (op) {
|
encode: function (op) {
|
||||||
return op
|
return {
|
||||||
|
target: op.target,
|
||||||
|
length: op.length || 0,
|
||||||
|
struct: 'Delete'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
requiredOps: function (op) {
|
requiredOps: function (op) {
|
||||||
return [] // [op.target]
|
return [] // [op.target]
|
||||||
},
|
},
|
||||||
execute: function * (op) {
|
execute: function * (op) {
|
||||||
return yield* this.deleteOperation(op.target, op.length || 1)
|
return yield * this.deleteOperation(op.target, op.length || 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -97,13 +101,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
return 0
|
return 0
|
||||||
} else {
|
} else {
|
||||||
var d = 0
|
var d = 0
|
||||||
var o = yield* this.getInsertion(op.left)
|
var o = yield * this.getInsertion(op.left)
|
||||||
while (!Y.utils.matchesId(o, op.origin)) {
|
while (!Y.utils.matchesId(o, op.origin)) {
|
||||||
d++
|
d++
|
||||||
if (o.left == null) {
|
if (o.left == null) {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
o = yield* this.getInsertion(o.left)
|
o = yield * this.getInsertion(o.left)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
@@ -134,17 +138,17 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (op.origin != null) { // TODO: !== instead of !=
|
if (op.origin != null) { // TODO: !== instead of !=
|
||||||
// we save in origin that op originates in it
|
// we save in origin that op originates in it
|
||||||
// we need that later when we eventually garbage collect origin (see transaction)
|
// we need that later when we eventually garbage collect origin (see transaction)
|
||||||
var origin = yield* this.getInsertionCleanEnd(op.origin)
|
var origin = yield * this.getInsertionCleanEnd(op.origin)
|
||||||
if (origin.originOf == null) {
|
if (origin.originOf == null) {
|
||||||
origin.originOf = []
|
origin.originOf = []
|
||||||
}
|
}
|
||||||
origin.originOf.push(op.id)
|
origin.originOf.push(op.id)
|
||||||
yield* this.setOperation(origin)
|
yield * this.setOperation(origin)
|
||||||
if (origin.right != null) {
|
if (origin.right != null) {
|
||||||
tryToRemergeLater.push(origin.right)
|
tryToRemergeLater.push(origin.right)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
||||||
|
|
||||||
// now we begin to insert op in the list of insertions..
|
// now we begin to insert op in the list of insertions..
|
||||||
var o
|
var o
|
||||||
@@ -153,29 +157,29 @@ module.exports = function (Y/* :any */) {
|
|||||||
|
|
||||||
// find o. o is the first conflicting operation
|
// find o. o is the first conflicting operation
|
||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
o = yield* this.getInsertionCleanEnd(op.left)
|
o = yield * this.getInsertionCleanEnd(op.left)
|
||||||
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
||||||
// only if not added previously
|
// only if not added previously
|
||||||
tryToRemergeLater.push(o.right)
|
tryToRemergeLater.push(o.right)
|
||||||
}
|
}
|
||||||
o = (o.right == null) ? null : yield* this.getOperation(o.right)
|
o = (o.right == null) ? null : yield * this.getOperation(o.right)
|
||||||
} else { // left == null
|
} else { // left == null
|
||||||
parent = yield* this.getOperation(op.parent)
|
parent = yield * this.getOperation(op.parent)
|
||||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||||
start = startId == null ? null : yield* this.getOperation(startId)
|
start = startId == null ? null : yield * this.getOperation(startId)
|
||||||
o = start
|
o = start
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
||||||
if (op.right != null) {
|
if (op.right != null) {
|
||||||
tryToRemergeLater.push(op.right)
|
tryToRemergeLater.push(op.right)
|
||||||
yield* this.getInsertionCleanStart(op.right)
|
yield * this.getInsertionCleanStart(op.right)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle conflicts
|
// handle conflicts
|
||||||
while (true) {
|
while (true) {
|
||||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||||
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
|
var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||||
if (oOriginDistance === i) {
|
if (oOriginDistance === i) {
|
||||||
// case 1
|
// case 1
|
||||||
if (o.id[0] < op.id[0]) {
|
if (o.id[0] < op.id[0]) {
|
||||||
@@ -193,7 +197,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
if (o.right != null) {
|
if (o.right != null) {
|
||||||
o = yield* this.getInsertion(o.right)
|
o = yield * this.getInsertion(o.right)
|
||||||
} else {
|
} else {
|
||||||
o = null
|
o = null
|
||||||
}
|
}
|
||||||
@@ -206,17 +210,17 @@ module.exports = function (Y/* :any */) {
|
|||||||
var left = null
|
var left = null
|
||||||
var right = null
|
var right = null
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
parent = yield* this.getOperation(op.parent)
|
parent = yield * this.getOperation(op.parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconnect left and set right of op
|
// reconnect left and set right of op
|
||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
left = yield* this.getInsertion(op.left)
|
left = yield * this.getInsertion(op.left)
|
||||||
// link left
|
// link left
|
||||||
op.right = left.right
|
op.right = left.right
|
||||||
left.right = op.id
|
left.right = op.id
|
||||||
|
|
||||||
yield* this.setOperation(left)
|
yield * this.setOperation(left)
|
||||||
} else {
|
} else {
|
||||||
// set op.right from parent, if necessary
|
// set op.right from parent, if necessary
|
||||||
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
||||||
@@ -224,33 +228,33 @@ module.exports = function (Y/* :any */) {
|
|||||||
// reconnect right
|
// reconnect right
|
||||||
if (op.right != null) {
|
if (op.right != null) {
|
||||||
// TODO: wanna connect right too?
|
// TODO: wanna connect right too?
|
||||||
right = yield* this.getOperation(op.right)
|
right = yield * this.getOperation(op.right)
|
||||||
right.left = Y.utils.getLastId(op)
|
right.left = Y.utils.getLastId(op)
|
||||||
|
|
||||||
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
||||||
if (right.gc != null) {
|
if (right.gc != null) {
|
||||||
if (right.content != null && right.content.length > 1) {
|
if (right.content != null && right.content.length > 1) {
|
||||||
right = yield* this.getInsertionCleanEnd(right.id)
|
right = yield * this.getInsertionCleanEnd(right.id)
|
||||||
}
|
}
|
||||||
this.store.removeFromGarbageCollector(right)
|
this.store.removeFromGarbageCollector(right)
|
||||||
}
|
}
|
||||||
yield* this.setOperation(right)
|
yield * this.setOperation(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update parents .map/start/end properties
|
// update parents .map/start/end properties
|
||||||
if (op.parentSub != null) {
|
if (op.parentSub != null) {
|
||||||
if (left == null) {
|
if (left == null) {
|
||||||
parent.map[op.parentSub] = op.id
|
parent.map[op.parentSub] = op.id
|
||||||
yield* this.setOperation(parent)
|
yield * this.setOperation(parent)
|
||||||
}
|
}
|
||||||
// is a child of a map struct.
|
// is a child of a map struct.
|
||||||
// Then also make sure that only the most left element is not deleted
|
// Then also make sure that only the most left element is not deleted
|
||||||
// We do not call the type in this case (this is what the third parameter is for)
|
// We do not call the type in this case (this is what the third parameter is for)
|
||||||
if (op.right != null) {
|
if (op.right != null) {
|
||||||
yield* this.deleteOperation(op.right, 1, true)
|
yield * this.deleteOperation(op.right, 1, true)
|
||||||
}
|
}
|
||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
yield* this.deleteOperation(op.id, 1, true)
|
yield * this.deleteOperation(op.id, 1, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (right == null || left == null) {
|
if (right == null || left == null) {
|
||||||
@@ -260,14 +264,14 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (left == null) {
|
if (left == null) {
|
||||||
parent.start = op.id
|
parent.start = op.id
|
||||||
}
|
}
|
||||||
yield* this.setOperation(parent)
|
yield * this.setOperation(parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to merge original op.left and op.origin
|
// try to merge original op.left and op.origin
|
||||||
for (let i = 0; i < tryToRemergeLater.length; i++) {
|
for (i = 0; i < tryToRemergeLater.length; i++) {
|
||||||
var m = yield* this.getOperation(tryToRemergeLater[i])
|
var m = yield * this.getOperation(tryToRemergeLater[i])
|
||||||
yield* this.tryCombineWithLeft(m)
|
yield * this.tryCombineWithLeft(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -325,7 +329,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
var res = null
|
var res = null
|
||||||
var o = yield* this.getOperation(op.start)
|
var o = yield * this.getOperation(op.start)
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!o.deleted) {
|
if (!o.deleted) {
|
||||||
@@ -333,7 +337,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
pos--
|
pos--
|
||||||
}
|
}
|
||||||
if (pos >= 0 && o.right != null) {
|
if (pos >= 0 && o.right != null) {
|
||||||
o = yield* this.getOperation(o.right)
|
o = yield * this.getOperation(o.right)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -344,7 +348,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
o = o.start
|
o = o.start
|
||||||
var res = []
|
var res = []
|
||||||
while (o != null) { // TODO: change to != (at least some convention)
|
while (o != null) { // TODO: change to != (at least some convention)
|
||||||
var operation = yield* this.getOperation(o)
|
var operation = yield * this.getOperation(o)
|
||||||
if (!operation.deleted) {
|
if (!operation.deleted) {
|
||||||
res.push(f(operation))
|
res.push(f(operation))
|
||||||
}
|
}
|
||||||
@@ -394,13 +398,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
get: function * (op, name) {
|
get: function * (op, name) {
|
||||||
var oid = op.map[name]
|
var oid = op.map[name]
|
||||||
if (oid != null) {
|
if (oid != null) {
|
||||||
var res = yield* this.getOperation(oid)
|
var res = yield * this.getOperation(oid)
|
||||||
if (res == null || res.deleted) {
|
if (res == null || res.deleted) {
|
||||||
return void 0
|
return void 0
|
||||||
} else if (res.opContent == null) {
|
} else if (res.opContent == null) {
|
||||||
return res.content[0]
|
return res.content[0]
|
||||||
} else {
|
} else {
|
||||||
return yield* this.getType(res.opContent)
|
return yield * this.getType(res.opContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
- this is called only by `getOperations(startSS)`. It makes an operation
|
- this is called only by `getOperations(startSS)`. It makes an operation
|
||||||
applyable on a given SS.
|
applyable on a given SS.
|
||||||
*/
|
*/
|
||||||
module.exports = function (Y/* :any */) {
|
export default function extendTransaction (Y) {
|
||||||
class TransactionInterface {
|
class TransactionInterface {
|
||||||
/* ::
|
/* ::
|
||||||
store: Y.AbstractDatabase;
|
store: Y.AbstractDatabase;
|
||||||
@@ -82,57 +82,6 @@ module.exports = function (Y/* :any */) {
|
|||||||
os: Store;
|
os: Store;
|
||||||
ss: Store;
|
ss: Store;
|
||||||
*/
|
*/
|
||||||
/*
|
|
||||||
Get a type based on the id of its model.
|
|
||||||
If it does not exist yes, create it.
|
|
||||||
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
|
|
||||||
*/
|
|
||||||
* getType (id, args) {
|
|
||||||
var sid = JSON.stringify(id)
|
|
||||||
var t = this.store.initializedTypes[sid]
|
|
||||||
if (t == null) {
|
|
||||||
var op/* :MapStruct | ListStruct */ = yield* this.getOperation(id)
|
|
||||||
if (op != null) {
|
|
||||||
t = yield* Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
|
|
||||||
this.store.initializedTypes[sid] = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
* createType (typedefinition, id) {
|
|
||||||
var structname = typedefinition[0].struct
|
|
||||||
id = id || this.store.getNextOpId(1)
|
|
||||||
var op
|
|
||||||
if (id[0] === '_') {
|
|
||||||
op = yield* this.getOperation(id)
|
|
||||||
} else {
|
|
||||||
op = Y.Struct[structname].create(id)
|
|
||||||
op.type = typedefinition[0].name
|
|
||||||
}
|
|
||||||
if (typedefinition[0].appendAdditionalInfo != null) {
|
|
||||||
yield* typedefinition[0].appendAdditionalInfo.call(this, op, typedefinition[1])
|
|
||||||
}
|
|
||||||
if (op[0] === '_') {
|
|
||||||
yield* this.setOperation(op)
|
|
||||||
} else {
|
|
||||||
yield* this.applyCreatedOperations([op])
|
|
||||||
}
|
|
||||||
return yield* this.getType(id, typedefinition[1])
|
|
||||||
}
|
|
||||||
/* createType (typedefinition, id) {
|
|
||||||
var structname = typedefinition[0].struct
|
|
||||||
id = id || this.store.getNextOpId(1)
|
|
||||||
var op = Y.Struct[structname].create(id)
|
|
||||||
op.type = typedefinition[0].name
|
|
||||||
if (typedefinition[0].appendAdditionalInfo != null) {
|
|
||||||
yield* typedefinition[0].appendAdditionalInfo.call(this, op, typedefinition[1])
|
|
||||||
}
|
|
||||||
// yield* this.applyCreatedOperations([op])
|
|
||||||
yield* Y.Struct[op.struct].execute.call(this, op)
|
|
||||||
yield* this.addOperation(op)
|
|
||||||
yield* this.store.operationAdded(this, op)
|
|
||||||
return yield* this.getType(id, typedefinition[1])
|
|
||||||
}*/
|
|
||||||
/*
|
/*
|
||||||
Apply operations that this user created (no remote ones!)
|
Apply operations that this user created (no remote ones!)
|
||||||
* does not check for Struct.*.requiredOps()
|
* does not check for Struct.*.requiredOps()
|
||||||
@@ -142,12 +91,12 @@ module.exports = function (Y/* :any */) {
|
|||||||
var send = []
|
var send = []
|
||||||
for (var i = 0; i < ops.length; i++) {
|
for (var i = 0; i < ops.length; i++) {
|
||||||
var op = ops[i]
|
var op = ops[i]
|
||||||
yield* this.store.tryExecute.call(this, op)
|
yield * this.store.tryExecute.call(this, op)
|
||||||
if (op.id == null || typeof op.id[1] !== 'string') {
|
if (op.id == null || typeof op.id[1] !== 'string') {
|
||||||
send.push(Y.Struct[op.struct].encode(op))
|
send.push(Y.Struct[op.struct].encode(op))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.store.y.connector.isDisconnected() && send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
if (this.store.y.connector.isSynced && 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)
|
||||||
}
|
}
|
||||||
@@ -155,15 +104,15 @@ module.exports = function (Y/* :any */) {
|
|||||||
|
|
||||||
* deleteList (start) {
|
* deleteList (start) {
|
||||||
while (start != null) {
|
while (start != null) {
|
||||||
start = yield* this.getOperation(start)
|
start = yield * this.getOperation(start)
|
||||||
if (!start.gc) {
|
if (!start.gc) {
|
||||||
start.gc = true
|
start.gc = true
|
||||||
start.deleted = true
|
start.deleted = true
|
||||||
yield* this.setOperation(start)
|
yield * this.setOperation(start)
|
||||||
var delLength = start.content != null ? start.content.length : 1
|
var delLength = start.content != null ? start.content.length : 1
|
||||||
yield* this.markDeleted(start.id, delLength)
|
yield * this.markDeleted(start.id, delLength)
|
||||||
if (start.opContent != null) {
|
if (start.opContent != null) {
|
||||||
yield* this.deleteOperation(start.opContent)
|
yield * this.deleteOperation(start.opContent)
|
||||||
}
|
}
|
||||||
this.store.queueGarbageCollector(start.id)
|
this.store.queueGarbageCollector(start.id)
|
||||||
}
|
}
|
||||||
@@ -178,10 +127,10 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (length == null) {
|
if (length == null) {
|
||||||
length = 1
|
length = 1
|
||||||
}
|
}
|
||||||
yield* this.markDeleted(targetId, length)
|
yield * this.markDeleted(targetId, length)
|
||||||
while (length > 0) {
|
while (length > 0) {
|
||||||
var callType = false
|
var callType = false
|
||||||
var target = yield* this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1])
|
var target = yield * this.os.findWithUpperBound([targetId[0], targetId[1] + length - 1])
|
||||||
var targetLength = target != null && target.content != null ? target.content.length : 1
|
var targetLength = target != null && target.content != null ? target.content.length : 1
|
||||||
if (target == null || target.id[0] !== targetId[0] || target.id[1] + targetLength <= targetId[1]) {
|
if (target == null || target.id[0] !== targetId[0] || target.id[1] + targetLength <= targetId[1]) {
|
||||||
// does not exist or is not in the range of the deletion
|
// does not exist or is not in the range of the deletion
|
||||||
@@ -192,12 +141,12 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (!target.deleted) {
|
if (!target.deleted) {
|
||||||
if (target.id[1] < targetId[1]) {
|
if (target.id[1] < targetId[1]) {
|
||||||
// starts to the left of the deletion range
|
// starts to the left of the deletion range
|
||||||
target = yield* this.getInsertionCleanStart(targetId)
|
target = yield * this.getInsertionCleanStart(targetId)
|
||||||
targetLength = target.content.length // must have content property!
|
targetLength = target.content.length // must have content property!
|
||||||
}
|
}
|
||||||
if (target.id[1] + targetLength > targetId[1] + length) {
|
if (target.id[1] + targetLength > targetId[1] + length) {
|
||||||
// ends to the right of the deletion range
|
// ends to the right of the deletion range
|
||||||
target = yield* this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1])
|
target = yield * this.getInsertionCleanEnd([targetId[0], targetId[1] + length - 1])
|
||||||
targetLength = target.content.length
|
targetLength = target.content.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,35 +161,35 @@ module.exports = function (Y/* :any */) {
|
|||||||
// delete containing lists
|
// delete containing lists
|
||||||
if (target.start != null) {
|
if (target.start != null) {
|
||||||
// TODO: don't do it like this .. -.-
|
// TODO: don't do it like this .. -.-
|
||||||
yield* this.deleteList(target.start)
|
yield * this.deleteList(target.start)
|
||||||
// yield* this.deleteList(target.id) -- do not gc itself because this may still get referenced
|
// yield* this.deleteList(target.id) -- do not gc itself because this may still get referenced
|
||||||
}
|
}
|
||||||
if (target.map != null) {
|
if (target.map != null) {
|
||||||
for (var name in target.map) {
|
for (var name in target.map) {
|
||||||
yield* this.deleteList(target.map[name])
|
yield * this.deleteList(target.map[name])
|
||||||
}
|
}
|
||||||
// TODO: here to.. (see above)
|
// TODO: here to.. (see above)
|
||||||
// yield* this.deleteList(target.id) -- see above
|
// yield* this.deleteList(target.id) -- see above
|
||||||
}
|
}
|
||||||
if (target.opContent != null) {
|
if (target.opContent != null) {
|
||||||
yield* this.deleteOperation(target.opContent)
|
yield * this.deleteOperation(target.opContent)
|
||||||
// target.opContent = null
|
// target.opContent = null
|
||||||
}
|
}
|
||||||
if (target.requires != null) {
|
if (target.requires != null) {
|
||||||
for (var i = 0; i < target.requires.length; i++) {
|
for (var i = 0; i < target.requires.length; i++) {
|
||||||
yield* this.deleteOperation(target.requires[i])
|
yield * this.deleteOperation(target.requires[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var left
|
var left
|
||||||
if (target.left != null) {
|
if (target.left != null) {
|
||||||
left = yield* this.getInsertion(target.left)
|
left = yield * this.getInsertion(target.left)
|
||||||
} else {
|
} else {
|
||||||
left = null
|
left = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// set here because it was deleted and/or gc'd
|
// set here because it was deleted and/or gc'd
|
||||||
yield* this.setOperation(target)
|
yield * this.setOperation(target)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Check if it is possible to add right to the gc.
|
Check if it is possible to add right to the gc.
|
||||||
@@ -249,21 +198,22 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
var right
|
var right
|
||||||
if (target.right != null) {
|
if (target.right != null) {
|
||||||
right = yield* this.getOperation(target.right)
|
right = yield * this.getOperation(target.right)
|
||||||
} else {
|
} else {
|
||||||
right = null
|
right = null
|
||||||
}
|
}
|
||||||
if (callType && !preventCallType) {
|
if (callType && !preventCallType) {
|
||||||
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!
|
||||||
yield* this.store.addToGarbageCollector.call(this, target, left)
|
yield * this.store.addToGarbageCollector.call(this, target, left)
|
||||||
if (right != null) {
|
if (right != null) {
|
||||||
yield* this.store.addToGarbageCollector.call(this, right, target)
|
yield * this.store.addToGarbageCollector.call(this, right, target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,22 +224,22 @@ module.exports = function (Y/* :any */) {
|
|||||||
* markGarbageCollected (id, len) {
|
* markGarbageCollected (id, len) {
|
||||||
// this.mem.push(["gc", id]);
|
// this.mem.push(["gc", id]);
|
||||||
this.store.addToDebug('yield* this.markGarbageCollected(', id, ', ', len, ')')
|
this.store.addToDebug('yield* this.markGarbageCollected(', id, ', ', len, ')')
|
||||||
var n = yield* this.markDeleted(id, len)
|
var n = yield * this.markDeleted(id, len)
|
||||||
if (n.id[1] < id[1] && !n.gc) {
|
if (n.id[1] < id[1] && !n.gc) {
|
||||||
// un-extend left
|
// un-extend left
|
||||||
var newlen = n.len - (id[1] - n.id[1])
|
var newlen = n.len - (id[1] - n.id[1])
|
||||||
n.len -= newlen
|
n.len -= newlen
|
||||||
yield* this.ds.put(n)
|
yield * this.ds.put(n)
|
||||||
n = {id: id, len: newlen, gc: false}
|
n = {id: id, len: newlen, gc: false}
|
||||||
yield* this.ds.put(n)
|
yield * this.ds.put(n)
|
||||||
}
|
}
|
||||||
// get prev&next before adding a new operation
|
// get prev&next before adding a new operation
|
||||||
var prev = yield* this.ds.findPrev(id)
|
var prev = yield * this.ds.findPrev(id)
|
||||||
var next = yield* this.ds.findNext(id)
|
var next = yield * this.ds.findNext(id)
|
||||||
|
|
||||||
if (id[1] + len < n.id[1] + n.len && !n.gc) {
|
if (id[1] + len < n.id[1] + n.len && !n.gc) {
|
||||||
// un-extend right
|
// un-extend right
|
||||||
yield* this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false})
|
yield * this.ds.put({id: [id[0], id[1] + len], len: n.len - len, gc: false})
|
||||||
n.len = len
|
n.len = len
|
||||||
}
|
}
|
||||||
// set gc'd
|
// set gc'd
|
||||||
@@ -301,7 +251,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id)
|
Y.utils.compareIds([prev.id[0], prev.id[1] + prev.len], n.id)
|
||||||
) {
|
) {
|
||||||
prev.len += n.len
|
prev.len += n.len
|
||||||
yield* this.ds.delete(n.id)
|
yield * this.ds.delete(n.id)
|
||||||
n = prev
|
n = prev
|
||||||
// ds.put n here?
|
// ds.put n here?
|
||||||
}
|
}
|
||||||
@@ -312,10 +262,10 @@ module.exports = function (Y/* :any */) {
|
|||||||
Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id)
|
Y.utils.compareIds([n.id[0], n.id[1] + n.len], next.id)
|
||||||
) {
|
) {
|
||||||
n.len += next.len
|
n.len += next.len
|
||||||
yield* this.ds.delete(next.id)
|
yield * this.ds.delete(next.id)
|
||||||
}
|
}
|
||||||
yield* this.ds.put(n)
|
yield * this.ds.put(n)
|
||||||
yield* this.updateState(n.id[0])
|
yield * this.updateState(n.id[0])
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Mark an operation as deleted.
|
Mark an operation as deleted.
|
||||||
@@ -327,7 +277,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
length = 1
|
length = 1
|
||||||
}
|
}
|
||||||
// this.mem.push(["del", id]);
|
// this.mem.push(["del", id]);
|
||||||
var n = yield* this.ds.findWithUpperBound(id)
|
var n = yield * this.ds.findWithUpperBound(id)
|
||||||
if (n != null && n.id[0] === id[0]) {
|
if (n != null && n.id[0] === id[0]) {
|
||||||
if (n.id[1] <= id[1] && id[1] <= n.id[1] + n.len) {
|
if (n.id[1] <= id[1] && id[1] <= n.id[1] + n.len) {
|
||||||
// id is in n's range
|
// id is in n's range
|
||||||
@@ -341,7 +291,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (diff < length) {
|
if (diff < length) {
|
||||||
// a partial deletion
|
// a partial deletion
|
||||||
n = {id: [id[0], id[1] + diff], len: length - diff, gc: false}
|
n = {id: [id[0], id[1] + diff], len: length - diff, gc: false}
|
||||||
yield* this.ds.put(n)
|
yield * this.ds.put(n)
|
||||||
} else {
|
} else {
|
||||||
// already gc'd
|
// already gc'd
|
||||||
throw new Error('Cannot happen! (it dit though.. :()')
|
throw new Error('Cannot happen! (it dit though.. :()')
|
||||||
@@ -355,15 +305,15 @@ module.exports = function (Y/* :any */) {
|
|||||||
} else {
|
} else {
|
||||||
// cannot extend left (there is no left!)
|
// cannot extend left (there is no left!)
|
||||||
n = {id: id, len: length, gc: false}
|
n = {id: id, len: length, gc: false}
|
||||||
yield* this.ds.put(n) // TODO: you double-put !!
|
yield * this.ds.put(n) // TODO: you double-put !!
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// cannot extend left
|
// cannot extend left
|
||||||
n = {id: id, len: length, gc: false}
|
n = {id: id, len: length, gc: false}
|
||||||
yield* this.ds.put(n)
|
yield * this.ds.put(n)
|
||||||
}
|
}
|
||||||
// can extend right?
|
// can extend right?
|
||||||
var next = yield* this.ds.findNext(n.id)
|
var next = yield * this.ds.findNext(n.id)
|
||||||
if (
|
if (
|
||||||
next != null &&
|
next != null &&
|
||||||
n.id[0] === next.id[0] &&
|
n.id[0] === next.id[0] &&
|
||||||
@@ -379,8 +329,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
// delete the missing range after next
|
// delete the missing range after next
|
||||||
diff = diff - next.len // missing range after next
|
diff = diff - next.len // missing range after next
|
||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
yield* this.ds.put(n) // unneccessary? TODO!
|
yield * this.ds.put(n) // unneccessary? TODO!
|
||||||
yield* this.markDeleted([next.id[0], next.id[1] + next.len], diff)
|
yield * this.markDeleted([next.id[0], next.id[1] + next.len], diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -389,8 +339,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (diff > next.len) {
|
if (diff > next.len) {
|
||||||
// n is even longer than next
|
// n is even longer than next
|
||||||
// get next.next, and try to extend it
|
// get next.next, and try to extend it
|
||||||
var _next = yield* this.ds.findNext(next.id)
|
var _next = yield * this.ds.findNext(next.id)
|
||||||
yield* this.ds.delete(next.id)
|
yield * this.ds.delete(next.id)
|
||||||
if (_next == null || n.id[0] !== _next.id[0]) {
|
if (_next == null || n.id[0] !== _next.id[0]) {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
@@ -401,13 +351,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
} else {
|
} else {
|
||||||
// n just partially overlaps with next. extend n, delete next, and break this loop
|
// n just partially overlaps with next. extend n, delete next, and break this loop
|
||||||
n.len += next.len - diff
|
n.len += next.len - diff
|
||||||
yield* this.ds.delete(next.id)
|
yield * this.ds.delete(next.id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield* this.ds.put(n)
|
yield * this.ds.put(n)
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -419,28 +369,31 @@ 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')
|
||||||
}
|
}
|
||||||
yield* this.os.iterate(this, null, null, function * (op) {
|
if (!this.store.gc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
yield * this.os.iterate(this, null, null, function * (op) {
|
||||||
if (op.gc) {
|
if (op.gc) {
|
||||||
delete op.gc
|
delete op.gc
|
||||||
yield* this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
}
|
}
|
||||||
if (op.parent != null) {
|
if (op.parent != null) {
|
||||||
var parentDeleted = yield* this.isDeleted(op.parent)
|
var parentDeleted = yield * this.isDeleted(op.parent)
|
||||||
if (parentDeleted) {
|
if (parentDeleted) {
|
||||||
op.gc = true
|
op.gc = true
|
||||||
if (!op.deleted) {
|
if (!op.deleted) {
|
||||||
yield* this.markDeleted(op.id, op.content != null ? op.content.length : 1)
|
yield * this.markDeleted(op.id, op.content != null ? op.content.length : 1)
|
||||||
op.deleted = true
|
op.deleted = true
|
||||||
if (op.opContent != null) {
|
if (op.opContent != null) {
|
||||||
yield* this.deleteOperation(op.opContent)
|
yield * this.deleteOperation(op.opContent)
|
||||||
}
|
}
|
||||||
if (op.requires != null) {
|
if (op.requires != null) {
|
||||||
for (var i = 0; i < op.requires.length; i++) {
|
for (var i = 0; i < op.requires.length; i++) {
|
||||||
yield* this.deleteOperation(op.requires[i])
|
yield * this.deleteOperation(op.requires[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield* this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
this.store.gc1.push(op.id) // this is ok becaues its shortly before sync (otherwise use queueGarbageCollector!)
|
this.store.gc1.push(op.id) // this is ok becaues its shortly before sync (otherwise use queueGarbageCollector!)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -448,9 +401,9 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (op.deleted) {
|
if (op.deleted) {
|
||||||
var left = null
|
var left = null
|
||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
left = yield* this.getInsertion(op.left)
|
left = yield * this.getInsertion(op.left)
|
||||||
}
|
}
|
||||||
yield* this.store.addToGarbageCollector.call(this, op, left)
|
yield * this.store.addToGarbageCollector.call(this, op, left)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -465,8 +418,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
* garbageCollectOperation (id) {
|
* garbageCollectOperation (id) {
|
||||||
this.store.addToDebug('yield* this.garbageCollectOperation(', id, ')')
|
this.store.addToDebug('yield* this.garbageCollectOperation(', id, ')')
|
||||||
var o = yield* this.getOperation(id)
|
var o = yield * this.getOperation(id)
|
||||||
yield* this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
|
yield * this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
|
||||||
// if op exists, then clean that mess up..
|
// if op exists, then clean that mess up..
|
||||||
if (o != null) {
|
if (o != null) {
|
||||||
var deps = []
|
var deps = []
|
||||||
@@ -477,32 +430,32 @@ module.exports = function (Y/* :any */) {
|
|||||||
deps = deps.concat(o.requires)
|
deps = deps.concat(o.requires)
|
||||||
}
|
}
|
||||||
for (var i = 0; i < deps.length; i++) {
|
for (var i = 0; i < deps.length; i++) {
|
||||||
var dep = yield* this.getOperation(deps[i])
|
var dep = yield * this.getOperation(deps[i])
|
||||||
if (dep != null) {
|
if (dep != null) {
|
||||||
if (!dep.deleted) {
|
if (!dep.deleted) {
|
||||||
yield* this.deleteOperation(dep.id)
|
yield * this.deleteOperation(dep.id)
|
||||||
dep = yield* this.getOperation(dep.id)
|
dep = yield * this.getOperation(dep.id)
|
||||||
}
|
}
|
||||||
dep.gc = true
|
dep.gc = true
|
||||||
yield* this.setOperation(dep)
|
yield * this.setOperation(dep)
|
||||||
this.store.queueGarbageCollector(dep.id)
|
this.store.queueGarbageCollector(dep.id)
|
||||||
} else {
|
} else {
|
||||||
yield* this.markGarbageCollected(deps[i], 1)
|
yield * this.markGarbageCollected(deps[i], 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove gc'd op from the left op, if it exists
|
// remove gc'd op from the left op, if it exists
|
||||||
if (o.left != null) {
|
if (o.left != null) {
|
||||||
var left = yield* this.getInsertion(o.left)
|
var left = yield * this.getInsertion(o.left)
|
||||||
left.right = o.right
|
left.right = o.right
|
||||||
yield* this.setOperation(left)
|
yield * this.setOperation(left)
|
||||||
}
|
}
|
||||||
// remove gc'd op from the right op, if it exists
|
// remove gc'd op from the right op, if it exists
|
||||||
// also reset origins of right ops
|
// also reset origins of right ops
|
||||||
if (o.right != null) {
|
if (o.right != null) {
|
||||||
var right = yield* this.getOperation(o.right)
|
var right = yield * this.getOperation(o.right)
|
||||||
right.left = o.left
|
right.left = o.left
|
||||||
yield* this.setOperation(right)
|
yield * this.setOperation(right)
|
||||||
|
|
||||||
if (o.originOf != null && o.originOf.length > 0) {
|
if (o.originOf != null && o.originOf.length > 0) {
|
||||||
// find new origin of right ops
|
// find new origin of right ops
|
||||||
@@ -510,7 +463,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
var neworigin = o.left
|
var neworigin = o.left
|
||||||
var neworigin_ = null
|
var neworigin_ = null
|
||||||
while (neworigin != null) {
|
while (neworigin != null) {
|
||||||
neworigin_ = yield* this.getInsertion(neworigin)
|
neworigin_ = yield * this.getInsertion(neworigin)
|
||||||
if (neworigin_.deleted) {
|
if (neworigin_.deleted) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -553,10 +506,10 @@ module.exports = function (Y/* :any */) {
|
|||||||
// ** Now the new implementation starts **
|
// ** Now the new implementation starts **
|
||||||
// reset neworigin of all originOf[*]
|
// reset neworigin of all originOf[*]
|
||||||
for (var _i in o.originOf) {
|
for (var _i in o.originOf) {
|
||||||
var originsIn = yield* this.getOperation(o.originOf[_i])
|
var originsIn = yield * this.getOperation(o.originOf[_i])
|
||||||
if (originsIn != null) {
|
if (originsIn != null) {
|
||||||
originsIn.origin = neworigin
|
originsIn.origin = neworigin
|
||||||
yield* this.setOperation(originsIn)
|
yield * this.setOperation(originsIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (neworigin != null) {
|
if (neworigin != null) {
|
||||||
@@ -565,7 +518,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
} else {
|
} else {
|
||||||
neworigin_.originOf = o.originOf.concat(neworigin_.originOf)
|
neworigin_.originOf = o.originOf.concat(neworigin_.originOf)
|
||||||
}
|
}
|
||||||
yield* this.setOperation(neworigin_)
|
yield * this.setOperation(neworigin_)
|
||||||
}
|
}
|
||||||
// we don't need to set right here, because
|
// we don't need to set right here, because
|
||||||
// right should be in o.originOf => it is set it the previous for loop
|
// right should be in o.originOf => it is set it the previous for loop
|
||||||
@@ -574,15 +527,15 @@ module.exports = function (Y/* :any */) {
|
|||||||
// o may originate in another operation.
|
// o may originate in another operation.
|
||||||
// Since o is deleted, we have to reset o.origin's `originOf` property
|
// Since o is deleted, we have to reset o.origin's `originOf` property
|
||||||
if (o.origin != null) {
|
if (o.origin != null) {
|
||||||
var origin = yield* this.getInsertion(o.origin)
|
var origin = yield * this.getInsertion(o.origin)
|
||||||
origin.originOf = origin.originOf.filter(function (_id) {
|
origin.originOf = origin.originOf.filter(function (_id) {
|
||||||
return !Y.utils.compareIds(id, _id)
|
return !Y.utils.compareIds(id, _id)
|
||||||
})
|
})
|
||||||
yield* this.setOperation(origin)
|
yield * this.setOperation(origin)
|
||||||
}
|
}
|
||||||
var parent
|
var parent
|
||||||
if (o.parent != null) {
|
if (o.parent != null) {
|
||||||
parent = yield* this.getOperation(o.parent)
|
parent = yield * this.getOperation(o.parent)
|
||||||
}
|
}
|
||||||
// remove gc'd op from parent, if it exists
|
// remove gc'd op from parent, if it exists
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
@@ -609,32 +562,32 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (setParent) {
|
if (setParent) {
|
||||||
yield* this.setOperation(parent)
|
yield * this.setOperation(parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// finally remove it from the os
|
// finally remove it from the os
|
||||||
yield* this.removeOperation(o.id)
|
yield * this.removeOperation(o.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* checkDeleteStoreForState (state) {
|
* checkDeleteStoreForState (state) {
|
||||||
var n = yield* this.ds.findWithUpperBound([state.user, state.clock])
|
var n = yield * this.ds.findWithUpperBound([state.user, state.clock])
|
||||||
if (n != null && n.id[0] === state.user && n.gc) {
|
if (n != null && n.id[0] === state.user && n.gc) {
|
||||||
state.clock = Math.max(state.clock, n.id[1] + n.len)
|
state.clock = Math.max(state.clock, n.id[1] + n.len)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* updateState (user) {
|
* updateState (user) {
|
||||||
var state = yield* this.getState(user)
|
var state = yield * this.getState(user)
|
||||||
yield* this.checkDeleteStoreForState(state)
|
yield * this.checkDeleteStoreForState(state)
|
||||||
var o = yield* this.getInsertion([user, state.clock])
|
var o = yield * this.getInsertion([user, state.clock])
|
||||||
var oLength = (o != null && o.content != null) ? o.content.length : 1
|
var oLength = (o != null && o.content != null) ? o.content.length : 1
|
||||||
while (o != null && user === o.id[0] && o.id[1] <= state.clock && o.id[1] + oLength > state.clock) {
|
while (o != null && user === o.id[0] && o.id[1] <= state.clock && o.id[1] + oLength > state.clock) {
|
||||||
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
|
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
|
||||||
state.clock += oLength
|
state.clock += oLength
|
||||||
yield* this.checkDeleteStoreForState(state)
|
yield * this.checkDeleteStoreForState(state)
|
||||||
o = yield* this.os.findNext(o.id)
|
o = yield * this.os.findNext(o.id)
|
||||||
oLength = (o != null && o.content != null) ? o.content.length : 1
|
oLength = (o != null && o.content != null) ? o.content.length : 1
|
||||||
}
|
}
|
||||||
yield* this.setState(state)
|
yield * this.setState(state)
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
apply a delete set in order to get
|
apply a delete set in order to get
|
||||||
@@ -647,7 +600,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
var dv = ds[user]
|
var dv = ds[user]
|
||||||
var pos = 0
|
var pos = 0
|
||||||
var d = dv[pos]
|
var d = dv[pos]
|
||||||
yield* this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
|
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
|
||||||
// cases:
|
// cases:
|
||||||
// 1. d deletes something to the right of n
|
// 1. d deletes something to the right of n
|
||||||
// => go to next n (break)
|
// => go to next n (break)
|
||||||
@@ -696,14 +649,14 @@ module.exports = function (Y/* :any */) {
|
|||||||
for (var i = 0; i < deletions.length; i++) {
|
for (var i = 0; i < deletions.length; i++) {
|
||||||
var del = deletions[i]
|
var del = deletions[i]
|
||||||
// always try to delete..
|
// always try to delete..
|
||||||
yield* this.deleteOperation([del[0], del[1]], del[2])
|
yield * this.deleteOperation([del[0], del[1]], del[2])
|
||||||
if (del[3]) {
|
if (del[3]) {
|
||||||
// gc..
|
// gc..
|
||||||
yield* this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
|
yield * this.markGarbageCollected([del[0], del[1]], del[2]) // always mark gc'd
|
||||||
// remove operation..
|
// remove operation..
|
||||||
var counter = del[1] + del[2]
|
var counter = del[1] + del[2]
|
||||||
while (counter >= del[1]) {
|
while (counter >= del[1]) {
|
||||||
var o = yield* this.os.findWithUpperBound([del[0], counter - 1])
|
var o = yield * this.os.findWithUpperBound([del[0], counter - 1])
|
||||||
if (o == null) {
|
if (o == null) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -714,25 +667,25 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
if (o.id[1] + oLen > del[1] + del[2]) {
|
if (o.id[1] + oLen > del[1] + del[2]) {
|
||||||
// overlaps right
|
// overlaps right
|
||||||
o = yield* this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
|
o = yield * this.getInsertionCleanEnd([del[0], del[1] + del[2] - 1])
|
||||||
}
|
}
|
||||||
if (o.id[1] < del[1]) {
|
if (o.id[1] < del[1]) {
|
||||||
// overlaps left
|
// overlaps left
|
||||||
o = yield* this.getInsertionCleanStart([del[0], del[1]])
|
o = yield * this.getInsertionCleanStart([del[0], del[1]])
|
||||||
}
|
}
|
||||||
counter = o.id[1]
|
counter = o.id[1]
|
||||||
yield* this.garbageCollectOperation(o.id)
|
yield * this.garbageCollectOperation(o.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.store.forwardAppliedOperations) {
|
if (this.store.forwardAppliedOperations) {
|
||||||
var ops = []
|
var ops = []
|
||||||
ops.push({struct: 'Delete', target: [d[0], d[1]], length: del[2]})
|
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
||||||
this.store.y.connector.broadcastOps(ops)
|
this.store.y.connector.broadcastOps(ops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* isGarbageCollected (id) {
|
* isGarbageCollected (id) {
|
||||||
var n = yield* this.ds.findWithUpperBound(id)
|
var n = yield * this.ds.findWithUpperBound(id)
|
||||||
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc
|
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len && n.gc
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -740,7 +693,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
*/
|
*/
|
||||||
* getDeleteSet () {
|
* getDeleteSet () {
|
||||||
var ds = {}
|
var ds = {}
|
||||||
yield* this.ds.iterate(this, null, null, function * (n) {
|
yield * this.ds.iterate(this, null, null, function * (n) {
|
||||||
var user = n.id[0]
|
var user = n.id[0]
|
||||||
var counter = n.id[1]
|
var counter = n.id[1]
|
||||||
var len = n.len
|
var len = n.len
|
||||||
@@ -755,16 +708,16 @@ module.exports = function (Y/* :any */) {
|
|||||||
return ds
|
return ds
|
||||||
}
|
}
|
||||||
* isDeleted (id) {
|
* isDeleted (id) {
|
||||||
var n = yield* this.ds.findWithUpperBound(id)
|
var n = yield * this.ds.findWithUpperBound(id)
|
||||||
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
|
return n != null && n.id[0] === id[0] && id[1] < n.id[1] + n.len
|
||||||
}
|
}
|
||||||
* setOperation (op) {
|
* setOperation (op) {
|
||||||
yield* this.os.put(op)
|
yield * this.os.put(op)
|
||||||
return op
|
return op
|
||||||
}
|
}
|
||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
yield* this.os.put(op)
|
yield * this.os.put(op)
|
||||||
if (!this.store.y.connector.isDisconnected() && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
if (this.store.y.connector.isSynced && 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])
|
||||||
}
|
}
|
||||||
@@ -778,7 +731,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
op.left[0] === op.id[0] &&
|
op.left[0] === op.id[0] &&
|
||||||
Y.utils.compareIds(op.left, op.origin)
|
Y.utils.compareIds(op.left, op.origin)
|
||||||
) {
|
) {
|
||||||
var left = yield* this.getInsertion(op.left)
|
var left = yield * this.getInsertion(op.left)
|
||||||
if (left.content != null &&
|
if (left.content != null &&
|
||||||
left.id[1] + left.content.length === op.id[1] &&
|
left.id[1] + left.content.length === op.id[1] &&
|
||||||
left.originOf.length === 1 &&
|
left.originOf.length === 1 &&
|
||||||
@@ -793,13 +746,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
left.content = left.content.concat(op.content)
|
left.content = left.content.concat(op.content)
|
||||||
left.right = op.right
|
left.right = op.right
|
||||||
yield* this.os.delete(op.id)
|
yield * this.os.delete(op.id)
|
||||||
yield* this.setOperation(left)
|
yield * this.setOperation(left)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* getInsertion (id) {
|
* getInsertion (id) {
|
||||||
var ins = yield* this.os.findWithUpperBound(id)
|
var ins = yield * this.os.findWithUpperBound(id)
|
||||||
if (ins == null) {
|
if (ins == null) {
|
||||||
return null
|
return null
|
||||||
} else {
|
} else {
|
||||||
@@ -812,13 +765,13 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
* getInsertionCleanStartEnd (id) {
|
* getInsertionCleanStartEnd (id) {
|
||||||
yield* this.getInsertionCleanStart(id)
|
yield * this.getInsertionCleanStart(id)
|
||||||
return yield* this.getInsertionCleanEnd(id)
|
return yield * this.getInsertionCleanEnd(id)
|
||||||
}
|
}
|
||||||
// Return an insertion such that id is the first element of content
|
// Return an insertion such that id is the first element of content
|
||||||
// This function manipulates an operation, if necessary
|
// This function manipulates an operation, if necessary
|
||||||
* getInsertionCleanStart (id) {
|
* getInsertionCleanStart (id) {
|
||||||
var ins = yield* this.getInsertion(id)
|
var ins = yield * this.getInsertion(id)
|
||||||
if (ins != null) {
|
if (ins != null) {
|
||||||
if (ins.id[1] === id[1]) {
|
if (ins.id[1] === id[1]) {
|
||||||
return ins
|
return ins
|
||||||
@@ -832,8 +785,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
left.right = ins.id
|
left.right = ins.id
|
||||||
ins.left = leftLid
|
ins.left = leftLid
|
||||||
// debugger // check
|
// debugger // check
|
||||||
yield* this.setOperation(left)
|
yield * this.setOperation(left)
|
||||||
yield* this.setOperation(ins)
|
yield * this.setOperation(ins)
|
||||||
if (left.gc) {
|
if (left.gc) {
|
||||||
this.store.queueGarbageCollector(ins.id)
|
this.store.queueGarbageCollector(ins.id)
|
||||||
}
|
}
|
||||||
@@ -846,7 +799,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
// Return an insertion such that id is the last element of content
|
// Return an insertion such that id is the last element of content
|
||||||
// This function manipulates an operation, if necessary
|
// This function manipulates an operation, if necessary
|
||||||
* getInsertionCleanEnd (id) {
|
* getInsertionCleanEnd (id) {
|
||||||
var ins = yield* this.getInsertion(id)
|
var ins = yield * this.getInsertion(id)
|
||||||
if (ins != null) {
|
if (ins != null) {
|
||||||
if (ins.content == null || (ins.id[1] + ins.content.length - 1 === id[1])) {
|
if (ins.content == null || (ins.id[1] + ins.content.length - 1 === id[1])) {
|
||||||
return ins
|
return ins
|
||||||
@@ -860,8 +813,8 @@ module.exports = function (Y/* :any */) {
|
|||||||
ins.right = right.id
|
ins.right = right.id
|
||||||
right.left = insLid
|
right.left = insLid
|
||||||
// debugger // check
|
// debugger // check
|
||||||
yield* this.setOperation(right)
|
yield * this.setOperation(right)
|
||||||
yield* this.setOperation(ins)
|
yield * this.setOperation(ins)
|
||||||
if (ins.gc) {
|
if (ins.gc) {
|
||||||
this.store.queueGarbageCollector(right.id)
|
this.store.queueGarbageCollector(right.id)
|
||||||
}
|
}
|
||||||
@@ -872,7 +825,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
* getOperation (id/* :any */)/* :Transaction<any> */ {
|
* getOperation (id/* :any */)/* :Transaction<any> */ {
|
||||||
var o = yield* this.os.find(id)
|
var o = yield * this.os.find(id)
|
||||||
if (id[0] !== '_' || o != null) {
|
if (id[0] !== '_' || o != null) {
|
||||||
return o
|
return o
|
||||||
} else { // type is string
|
} else { // type is string
|
||||||
@@ -882,7 +835,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
var struct = comp[0]
|
var struct = comp[0]
|
||||||
var op = Y.Struct[struct].create(id)
|
var op = Y.Struct[struct].create(id)
|
||||||
op.type = comp[1]
|
op.type = comp[1]
|
||||||
yield* this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
return op
|
return op
|
||||||
} else {
|
} else {
|
||||||
// won't be called. but just in case..
|
// won't be called. but just in case..
|
||||||
@@ -893,17 +846,17 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
* removeOperation (id) {
|
* removeOperation (id) {
|
||||||
yield* this.os.delete(id)
|
yield * this.os.delete(id)
|
||||||
}
|
}
|
||||||
* setState (state) {
|
* setState (state) {
|
||||||
var val = {
|
var val = {
|
||||||
id: [state.user],
|
id: [state.user],
|
||||||
clock: state.clock
|
clock: state.clock
|
||||||
}
|
}
|
||||||
yield* this.ss.put(val)
|
yield * this.ss.put(val)
|
||||||
}
|
}
|
||||||
* getState (user) {
|
* getState (user) {
|
||||||
var n = yield* this.ss.find([user])
|
var n = yield * this.ss.find([user])
|
||||||
var clock = n == null ? null : n.clock
|
var clock = n == null ? null : n.clock
|
||||||
if (clock == null) {
|
if (clock == null) {
|
||||||
clock = 0
|
clock = 0
|
||||||
@@ -915,7 +868,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
* getStateVector () {
|
* getStateVector () {
|
||||||
var stateVector = []
|
var stateVector = []
|
||||||
yield* this.ss.iterate(this, null, null, function * (n) {
|
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||||
stateVector.push({
|
stateVector.push({
|
||||||
user: n.id[0],
|
user: n.id[0],
|
||||||
clock: n.clock
|
clock: n.clock
|
||||||
@@ -925,7 +878,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
* getStateSet () {
|
* getStateSet () {
|
||||||
var ss = {}
|
var ss = {}
|
||||||
yield* this.ss.iterate(this, null, null, function * (n) {
|
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||||
ss[n.id[0]] = n.clock
|
ss[n.id[0]] = n.clock
|
||||||
})
|
})
|
||||||
return ss
|
return ss
|
||||||
@@ -983,7 +936,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
var send = []
|
var send = []
|
||||||
|
|
||||||
var endSV = yield* this.getStateVector()
|
var endSV = yield * this.getStateVector()
|
||||||
for (var endState of endSV) {
|
for (var endState of endSV) {
|
||||||
var user = endState.user
|
var user = endState.user
|
||||||
if (user === '_') {
|
if (user === '_') {
|
||||||
@@ -993,14 +946,14 @@ module.exports = function (Y/* :any */) {
|
|||||||
if (startPos > 0) {
|
if (startPos > 0) {
|
||||||
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
|
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
|
||||||
// find out if that is the case
|
// find out if that is the case
|
||||||
var firstMissing = yield* this.getInsertion([user, startPos])
|
var firstMissing = yield * this.getInsertion([user, startPos])
|
||||||
if (firstMissing != null) {
|
if (firstMissing != null) {
|
||||||
// update startPos
|
// update startPos
|
||||||
startPos = firstMissing.id[1]
|
startPos = firstMissing.id[1]
|
||||||
startSS[user] = startPos
|
startSS[user] = startPos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield* this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
||||||
op = Y.Struct[op.struct].encode(op)
|
op = Y.Struct[op.struct].encode(op)
|
||||||
if (op.struct !== 'Insert') {
|
if (op.struct !== 'Insert') {
|
||||||
send.push(op)
|
send.push(op)
|
||||||
@@ -1013,7 +966,7 @@ module.exports = function (Y/* :any */) {
|
|||||||
// 2. or to the first operation that has an origin that is not to the
|
// 2. or to the first operation that has an origin that is not to the
|
||||||
// right of op.
|
// right of op.
|
||||||
// For this we maintain a list of ops which origins are not found yet.
|
// For this we maintain a list of ops which origins are not found yet.
|
||||||
var missing_origins = [op]
|
var missingOrigins = [op]
|
||||||
var newright = op.right
|
var newright = op.right
|
||||||
while (true) {
|
while (true) {
|
||||||
if (o.left == null) {
|
if (o.left == null) {
|
||||||
@@ -1021,15 +974,15 @@ module.exports = function (Y/* :any */) {
|
|||||||
send.push(op)
|
send.push(op)
|
||||||
if (!Y.utils.compareIds(o.id, op.id)) {
|
if (!Y.utils.compareIds(o.id, op.id)) {
|
||||||
o = Y.Struct[op.struct].encode(o)
|
o = Y.Struct[op.struct].encode(o)
|
||||||
o.right = missing_origins[missing_origins.length - 1].id
|
o.right = missingOrigins[missingOrigins.length - 1].id
|
||||||
send.push(o)
|
send.push(o)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
o = yield* this.getInsertion(o.left)
|
o = yield * this.getInsertion(o.left)
|
||||||
// we set another o, check if we can reduce $missing_origins
|
// we set another o, check if we can reduce $missingOrigins
|
||||||
while (missing_origins.length > 0 && Y.utils.matchesId(o, missing_origins[missing_origins.length - 1].origin)) {
|
while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
|
||||||
missing_origins.pop()
|
missingOrigins.pop()
|
||||||
}
|
}
|
||||||
if (o.id[1] < (startSS[o.id[0]] || 0)) {
|
if (o.id[1] < (startSS[o.id[0]] || 0)) {
|
||||||
// case 2. o is known
|
// case 2. o is known
|
||||||
@@ -1042,17 +995,18 @@ module.exports = function (Y/* :any */) {
|
|||||||
send.push(op)
|
send.push(op)
|
||||||
op = Y.Struct[op.struct].encode(o)
|
op = Y.Struct[op.struct].encode(o)
|
||||||
op.right = newright
|
op.right = newright
|
||||||
if (missing_origins.length > 0) {
|
if (missingOrigins.length > 0) {
|
||||||
|
debugger
|
||||||
console.log('This should not happen .. :( please report this')
|
console.log('This should not happen .. :( please report this')
|
||||||
}
|
}
|
||||||
missing_origins = [op]
|
missingOrigins = [op]
|
||||||
} else {
|
} else {
|
||||||
// case 4. send o, continue to find op.origin
|
// case 4. send o, continue to find op.origin
|
||||||
var s = Y.Struct[op.struct].encode(o)
|
var s = Y.Struct[op.struct].encode(o)
|
||||||
s.right = missing_origins[missing_origins.length - 1].id
|
s.right = missingOrigins[missingOrigins.length - 1].id
|
||||||
s.left = s.origin
|
s.left = s.origin
|
||||||
send.push(s)
|
send.push(s)
|
||||||
missing_origins.push(o)
|
missingOrigins.push(o)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1060,6 +1014,56 @@ 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)
|
||||||
@@ -1086,9 +1090,9 @@ module.exports = function (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
* flush () {
|
* flush () {
|
||||||
yield* this.os.flush()
|
yield * this.os.flush()
|
||||||
yield* this.ss.flush()
|
yield * this.ss.flush()
|
||||||
yield* this.ds.flush()
|
yield * this.ds.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Y.Transaction = TransactionInterface
|
Y.Transaction = TransactionInterface
|
||||||
|
|||||||
129
src/Utils.js
129
src/Utils.js
@@ -1,6 +1,3 @@
|
|||||||
/* @flow */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
EventHandler is an helper class for constructing custom types.
|
EventHandler is an helper class for constructing custom types.
|
||||||
|
|
||||||
@@ -23,9 +20,28 @@
|
|||||||
database request to finish). EventHandler helps you to make your type
|
database request to finish). EventHandler helps you to make your type
|
||||||
synchronous.
|
synchronous.
|
||||||
*/
|
*/
|
||||||
module.exports = function (Y /* : any*/) {
|
|
||||||
|
export default function Utils (Y) {
|
||||||
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 = []
|
||||||
@@ -50,9 +66,13 @@ 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 {
|
||||||
this.eventListeners[i](event)
|
var _event = {}
|
||||||
|
for (var name in event) {
|
||||||
|
_event[name] = event[name]
|
||||||
|
}
|
||||||
|
this.eventListeners[i](_event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('User events must not throw Errors!')
|
console.error('Your observer threw an error. This error was caught so that Yjs still can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions"', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +102,6 @@ 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
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -238,7 +257,13 @@ module.exports = function (Y /* : any*/) {
|
|||||||
// finished with remaining operations
|
// finished with remaining operations
|
||||||
self.waiting.push(d)
|
self.waiting.push(d)
|
||||||
}
|
}
|
||||||
checkDelete(op)
|
if (op.key == null) {
|
||||||
|
// deletes in list
|
||||||
|
checkDelete(op)
|
||||||
|
} else {
|
||||||
|
// deletes in map
|
||||||
|
this.waiting.push(op)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.waiting.push(op)
|
this.waiting.push(op)
|
||||||
}
|
}
|
||||||
@@ -276,7 +301,7 @@ module.exports = function (Y /* : any*/) {
|
|||||||
}
|
}
|
||||||
var before = this.waiting.length
|
var before = this.waiting.length
|
||||||
// somehow create new operations
|
// somehow create new operations
|
||||||
yield* f.apply(transaction, args)
|
yield * f.apply(transaction, args)
|
||||||
// remove all appended ops / awaited ops
|
// remove all appended ops / awaited ops
|
||||||
this.waiting.splice(before)
|
this.waiting.splice(before)
|
||||||
if (this.awaiting > 0) this.awaiting--
|
if (this.awaiting > 0) this.awaiting--
|
||||||
@@ -286,18 +311,22 @@ module.exports = function (Y /* : any*/) {
|
|||||||
for (let i = 0; i < this.waiting.length; i++) {
|
for (let i = 0; i < this.waiting.length; i++) {
|
||||||
var o = this.waiting[i]
|
var o = this.waiting[i]
|
||||||
if (o.struct === 'Insert') {
|
if (o.struct === 'Insert') {
|
||||||
var _o = yield* transaction.getInsertion(o.id)
|
var _o = yield * transaction.getInsertion(o.id)
|
||||||
if (!Y.utils.compareIds(_o.id, o.id)) {
|
if (_o.parentSub != null && _o.left != null) {
|
||||||
|
// if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left
|
||||||
|
this.waiting.splice(i, 1)
|
||||||
|
i-- // update index
|
||||||
|
} else if (!Y.utils.compareIds(_o.id, o.id)) {
|
||||||
// o got extended
|
// o got extended
|
||||||
o.left = [o.id[0], o.id[1] - 1]
|
o.left = [o.id[0], o.id[1] - 1]
|
||||||
} else if (_o.left == null) {
|
} else if (_o.left == null) {
|
||||||
o.left = null
|
o.left = null
|
||||||
} else {
|
} else {
|
||||||
// find next undeleted op
|
// find next undeleted op
|
||||||
var left = yield* transaction.getInsertion(_o.left)
|
var left = yield * transaction.getInsertion(_o.left)
|
||||||
while (left.deleted != null) {
|
while (left.deleted != null) {
|
||||||
if (left.left != null) {
|
if (left.left != null) {
|
||||||
left = yield* transaction.getInsertion(left.left)
|
left = yield * transaction.getInsertion(left.left)
|
||||||
} else {
|
} else {
|
||||||
left = null
|
left = null
|
||||||
break
|
break
|
||||||
@@ -447,6 +476,27 @@ 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:
|
||||||
@@ -458,7 +508,7 @@ module.exports = function (Y /* : any*/) {
|
|||||||
* class
|
* class
|
||||||
- the constructor of the custom type (e.g. in order to inherit from a type)
|
- the constructor of the custom type (e.g. in order to inherit from a type)
|
||||||
*/
|
*/
|
||||||
class CustomType { // eslint-disable-line
|
class CustomTypeDefinition { // eslint-disable-line
|
||||||
/* ::
|
/* ::
|
||||||
struct: any;
|
struct: any;
|
||||||
initType: any;
|
initType: any;
|
||||||
@@ -469,12 +519,14 @@ module.exports = function (Y /* : any*/) {
|
|||||||
if (def.struct == null ||
|
if (def.struct == null ||
|
||||||
def.initType == null ||
|
def.initType == null ||
|
||||||
def.class == null ||
|
def.class == null ||
|
||||||
def.name == null
|
def.name == null ||
|
||||||
|
def.createType == null
|
||||||
) {
|
) {
|
||||||
throw new Error('Custom type was not initialized correctly!')
|
throw new Error('Custom type was not initialized correctly!')
|
||||||
}
|
}
|
||||||
this.struct = def.struct
|
this.struct = def.struct
|
||||||
this.initType = def.initType
|
this.initType = def.initType
|
||||||
|
this.createType = def.createType
|
||||||
this.class = def.class
|
this.class = def.class
|
||||||
this.name = def.name
|
this.name = def.name
|
||||||
if (def.appendAdditionalInfo != null) {
|
if (def.appendAdditionalInfo != null) {
|
||||||
@@ -486,13 +538,13 @@ module.exports = function (Y /* : any*/) {
|
|||||||
this.parseArguments.typeDefinition = this
|
this.parseArguments.typeDefinition = this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Y.utils.CustomType = CustomType
|
Y.utils.CustomTypeDefinition = CustomTypeDefinition
|
||||||
|
|
||||||
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
|
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
if (v instanceof Y.utils.CustomType) return [v]
|
if (v instanceof Y.utils.CustomTypeDefinition) return [v]
|
||||||
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomType) return v
|
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomTypeDefinition) return v
|
||||||
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomType) return [v.typeDefinition]
|
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomTypeDefinition) return [v.typeDefinition]
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -625,7 +677,7 @@ module.exports = function (Y /* : any*/) {
|
|||||||
if (i < 0 && noSuperCall === undefined) {
|
if (i < 0 && noSuperCall === undefined) {
|
||||||
// did not reach break in last loop
|
// did not reach break in last loop
|
||||||
// read id and put it to the end of readBuffer
|
// read id and put it to the end of readBuffer
|
||||||
o = yield* super.find(id)
|
o = yield * super.find(id)
|
||||||
}
|
}
|
||||||
if (o != null) {
|
if (o != null) {
|
||||||
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
||||||
@@ -655,7 +707,7 @@ module.exports = function (Y /* : any*/) {
|
|||||||
// write writeBuffer[0]
|
// write writeBuffer[0]
|
||||||
var write = this.writeBuffer[0]
|
var write = this.writeBuffer[0]
|
||||||
if (write.id[0] !== null) {
|
if (write.id[0] !== null) {
|
||||||
yield* super.put(write)
|
yield * super.put(write)
|
||||||
}
|
}
|
||||||
// put o to the end of writeBuffer
|
// put o to the end of writeBuffer
|
||||||
for (i = 0; i < this.writeBuffer.length - 1; i++) {
|
for (i = 0; i < this.writeBuffer.length - 1; i++) {
|
||||||
@@ -685,44 +737,44 @@ module.exports = function (Y /* : any*/) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
yield* super.delete(id)
|
yield * super.delete(id)
|
||||||
}
|
}
|
||||||
* findWithLowerBound (id) {
|
* findWithLowerBound (id) {
|
||||||
var o = yield* this.find(id, true)
|
var o = yield * this.find(id, true)
|
||||||
if (o != null) {
|
if (o != null) {
|
||||||
return o
|
return o
|
||||||
} else {
|
} else {
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
return yield* super.findWithLowerBound.apply(this, arguments)
|
return yield * super.findWithLowerBound.apply(this, arguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* findWithUpperBound (id) {
|
* findWithUpperBound (id) {
|
||||||
var o = yield* this.find(id, true)
|
var o = yield * this.find(id, true)
|
||||||
if (o != null) {
|
if (o != null) {
|
||||||
return o
|
return o
|
||||||
} else {
|
} else {
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
return yield* super.findWithUpperBound.apply(this, arguments)
|
return yield * super.findWithUpperBound.apply(this, arguments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
* findNext () {
|
* findNext () {
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
return yield* super.findNext.apply(this, arguments)
|
return yield * super.findNext.apply(this, arguments)
|
||||||
}
|
}
|
||||||
* findPrev () {
|
* findPrev () {
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
return yield* super.findPrev.apply(this, arguments)
|
return yield * super.findPrev.apply(this, arguments)
|
||||||
}
|
}
|
||||||
* iterate () {
|
* iterate () {
|
||||||
yield* this.flush()
|
yield * this.flush()
|
||||||
yield* super.iterate.apply(this, arguments)
|
yield * super.iterate.apply(this, arguments)
|
||||||
}
|
}
|
||||||
* flush () {
|
* flush () {
|
||||||
for (var i = 0; i < this.writeBuffer.length; i++) {
|
for (var i = 0; i < this.writeBuffer.length; i++) {
|
||||||
var write = this.writeBuffer[i]
|
var write = this.writeBuffer[i]
|
||||||
if (write.id[0] !== null) {
|
if (write.id[0] !== null) {
|
||||||
yield* super.put(write)
|
yield * super.put(write)
|
||||||
this.writeBuffer[i] = {
|
this.writeBuffer[i] = {
|
||||||
id: [null, null]
|
id: [null, null]
|
||||||
}
|
}
|
||||||
@@ -733,4 +785,9 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
155
src/y.js
155
src/y.js
@@ -1,32 +1,53 @@
|
|||||||
/* @flow */
|
import debug from 'debug'
|
||||||
'use strict'
|
import extendConnector from './Connector.js'
|
||||||
|
import extendDatabase from './Database.js'
|
||||||
|
import extendTransaction from './Transaction.js'
|
||||||
|
import extendStruct from './Struct.js'
|
||||||
|
import extendUtils from './Utils.js'
|
||||||
|
|
||||||
require('./Connector.js')(Y)
|
extendConnector(Y)
|
||||||
require('./Database.js')(Y)
|
extendDatabase(Y)
|
||||||
require('./Transaction.js')(Y)
|
extendTransaction(Y)
|
||||||
require('./Struct.js')(Y)
|
extendStruct(Y)
|
||||||
require('./Utils.js')(Y)
|
extendUtils(Y)
|
||||||
require('./Connectors/Test.js')(Y)
|
|
||||||
|
Y.debug = debug
|
||||||
|
|
||||||
var requiringModules = {}
|
var requiringModules = {}
|
||||||
|
|
||||||
module.exports = Y
|
|
||||||
Y.requiringModules = requiringModules
|
Y.requiringModules = requiringModules
|
||||||
|
|
||||||
Y.extend = function (name, value) {
|
Y.extend = function (name, value) {
|
||||||
if (value instanceof Y.utils.CustomType) {
|
if (arguments.length === 2 && typeof name === 'string') {
|
||||||
Y[name] = value.parseArguments
|
if (value instanceof Y.utils.CustomTypeDefinition) {
|
||||||
|
Y[name] = value.parseArguments
|
||||||
|
} else {
|
||||||
|
Y[name] = value
|
||||||
|
}
|
||||||
|
if (requiringModules[name] != null) {
|
||||||
|
requiringModules[name].resolve()
|
||||||
|
delete requiringModules[name]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Y[name] = value
|
for (var i = 0; i < arguments.length; i++) {
|
||||||
}
|
var f = arguments[i]
|
||||||
if (requiringModules[name] != null) {
|
if (typeof f === 'function') {
|
||||||
requiringModules[name].resolve()
|
f(Y)
|
||||||
delete requiringModules[name]
|
} else {
|
||||||
|
throw new Error('Expected function!')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Y.requestModules = requestModules
|
Y.requestModules = requestModules
|
||||||
function requestModules (modules) {
|
function requestModules (modules) {
|
||||||
|
var sourceDir
|
||||||
|
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..
|
||||||
@@ -39,10 +60,11 @@ 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') {
|
||||||
var imported = document.createElement('script')
|
if (sourceDir != null) {
|
||||||
imported.src = Y.sourceDir + '/' + modulename + '/' + modulename + extention
|
var imported = document.createElement('script')
|
||||||
document.head.appendChild(imported)
|
imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
|
||||||
|
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) {
|
||||||
@@ -90,32 +112,35 @@ type YOptions = {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
export default 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) {
|
||||||
setTimeout(function () {
|
if (opts == null) reject(new Error('An options object is expected!'))
|
||||||
|
else if (opts.connector == null) reject(new Error('You must specify a connector! (missing connector property)'))
|
||||||
|
else if (opts.connector.name == null) reject(new Error('You must specify connector name! (missing connector.name property)'))
|
||||||
|
else if (opts.db == null) reject(new Error('You must specify a database! (missing db property)'))
|
||||||
|
else if (opts.connector.name == null) reject(new Error('You must specify db name! (missing db.name property)'))
|
||||||
|
else {
|
||||||
|
opts = Y.utils.copyObject(opts)
|
||||||
|
opts.connector = Y.utils.copyObject(opts.connector)
|
||||||
|
opts.db = Y.utils.copyObject(opts.db)
|
||||||
|
opts.share = Y.utils.copyObject(opts.share)
|
||||||
Y.requestModules(modules).then(function () {
|
Y.requestModules(modules).then(function () {
|
||||||
if (opts == null) reject('An options object is expected! ')
|
var yconfig = new YConfig(opts)
|
||||||
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
yconfig.db.whenUserIdSet(function () {
|
||||||
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
yconfig.init(function () {
|
||||||
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
resolve(yconfig)
|
||||||
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
|
||||||
else if (opts.share == null) reject('You must specify a set of shared types!')
|
|
||||||
else {
|
|
||||||
var yconfig = new YConfig(opts)
|
|
||||||
yconfig.db.whenUserIdSet(function () {
|
|
||||||
yconfig.init(function () {
|
|
||||||
resolve(yconfig)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
}).catch(reject)
|
}).catch(reject)
|
||||||
}, 0)
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +155,7 @@ 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
|
||||||
@@ -140,6 +166,9 @@ class YConfig {
|
|||||||
for (var propertyname in opts.share) {
|
for (var propertyname in opts.share) {
|
||||||
var typeConstructor = opts.share[propertyname].split('(')
|
var typeConstructor = opts.share[propertyname].split('(')
|
||||||
var typeName = typeConstructor.splice(0, 1)
|
var typeName = typeConstructor.splice(0, 1)
|
||||||
|
var type = Y[typeName]
|
||||||
|
var typedef = type.typeDefinition
|
||||||
|
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
||||||
var args = []
|
var args = []
|
||||||
if (typeConstructor.length === 1) {
|
if (typeConstructor.length === 1) {
|
||||||
try {
|
try {
|
||||||
@@ -147,11 +176,13 @@ class YConfig {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
||||||
}
|
}
|
||||||
|
if (type.typeDefinition.parseArguments == null) {
|
||||||
|
throw new Error(typeName + ' does not expect arguments!')
|
||||||
|
} else {
|
||||||
|
args = typedef.parseArguments(args[0])[1]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var type = Y[typeName]
|
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
||||||
var typedef = type.typeDefinition
|
|
||||||
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
|
||||||
share[propertyname] = yield* this.createType(type.apply(typedef, args), id)
|
|
||||||
}
|
}
|
||||||
this.store.whenTransactionsFinished()
|
this.store.whenTransactionsFinished()
|
||||||
.then(callback)
|
.then(callback)
|
||||||
@@ -161,26 +192,46 @@ class YConfig {
|
|||||||
return this.connector.isSynced
|
return this.connector.isSynced
|
||||||
}
|
}
|
||||||
disconnect () {
|
disconnect () {
|
||||||
return this.connector.disconnect()
|
if (this.connected) {
|
||||||
|
this.connected = false
|
||||||
|
return this.connector.disconnect()
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
reconnect () {
|
reconnect () {
|
||||||
return this.connector.reconnect()
|
if (!this.connected) {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
var self = this
|
return this.db.whenTransactionsFinished().then(function () {
|
||||||
this.db.requestTransaction(function * () {
|
self.db.destroyTypes()
|
||||||
yield* self.db.destroy()
|
// make sure to wait for all transactions before destroying the db
|
||||||
self.connector = null
|
self.db.requestTransaction(function * () {
|
||||||
self.db = null
|
yield * self.db.destroy()
|
||||||
|
})
|
||||||
|
return self.db.whenTransactionsFinished()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.Y = Y
|
|
||||||
}
|
|
||||||
|
|||||||
178
tests-lib/helper.js
Normal file
178
tests-lib/helper.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
|
||||||
|
import _Y from '../../yjs/src/y.js'
|
||||||
|
|
||||||
|
import yMemory from '../../y-memory/src/Memory.js'
|
||||||
|
import yArray from '../../y-array/src/y-array.js'
|
||||||
|
import yMap from '../../y-map/src/Map.js'
|
||||||
|
import yTest from './test-connector.js'
|
||||||
|
|
||||||
|
import Chance from 'chance'
|
||||||
|
|
||||||
|
export let Y = _Y
|
||||||
|
|
||||||
|
Y.extend(yMemory, yArray, yMap, yTest)
|
||||||
|
|
||||||
|
export async function garbageCollectUsers (t, users) {
|
||||||
|
await flushAll(t, users)
|
||||||
|
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. reconnect and flush all
|
||||||
|
* 2. user 0 gc
|
||||||
|
* 3. get type content
|
||||||
|
* 4. disconnect & reconnect all (so gc is propagated)
|
||||||
|
* 5. compare os, ds, ss
|
||||||
|
*/
|
||||||
|
export async function compareUsers (t, users) {
|
||||||
|
await Promise.all(users.map(u => u.reconnect()))
|
||||||
|
if (users[0].connector.testRoom == null) {
|
||||||
|
await wait(100)
|
||||||
|
}
|
||||||
|
await flushAll(t, users)
|
||||||
|
|
||||||
|
await users[0].db.garbageCollect()
|
||||||
|
await users[0].db.garbageCollect()
|
||||||
|
|
||||||
|
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
|
||||||
|
|
||||||
|
// disconnect all except user 0
|
||||||
|
await Promise.all(users.slice(1).map(async u =>
|
||||||
|
u.disconnect()
|
||||||
|
))
|
||||||
|
if (users[0].connector.testRoom == null) {
|
||||||
|
await wait(100)
|
||||||
|
}
|
||||||
|
// reconnect all
|
||||||
|
await Promise.all(users.map(u => u.reconnect()))
|
||||||
|
if (users[0].connector.testRoom == null) {
|
||||||
|
await wait(100)
|
||||||
|
}
|
||||||
|
await users[0].connector.testRoom.flushAll(users)
|
||||||
|
await Promise.all(users.map(u =>
|
||||||
|
new Promise(function (resolve) {
|
||||||
|
u.connector.whenSynced(resolve)
|
||||||
|
})
|
||||||
|
))
|
||||||
|
|
||||||
|
var data = await Promise.all(users.map(async (u) => {
|
||||||
|
var data = {}
|
||||||
|
u.db.requestTransaction(function * () {
|
||||||
|
var os = yield * this.getOperationsUntransformed()
|
||||||
|
data.os = {}
|
||||||
|
os.untransformed.forEach((op) => {
|
||||||
|
op = Y.Struct[op.struct].encode(op)
|
||||||
|
delete op.origin
|
||||||
|
data.os[JSON.stringify(op.id)] = op
|
||||||
|
return op
|
||||||
|
})
|
||||||
|
data.ds = yield * this.getDeleteSet()
|
||||||
|
data.ss = yield * this.getStateSet()
|
||||||
|
})
|
||||||
|
await u.db.whenTransactionsFinished()
|
||||||
|
return data
|
||||||
|
}))
|
||||||
|
for (var i = 0; i < data.length - 1; i++) {
|
||||||
|
await t.asyncGroup(async () => {
|
||||||
|
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
|
||||||
|
t.compare(data[i].os, data[i + 1].os, 'os')
|
||||||
|
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||||
|
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||||
|
}, `Compare user${i} with user${i + 1}`)
|
||||||
|
}
|
||||||
|
await Promise.all(users.map(async (u) => {
|
||||||
|
await u.close()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initArrays (t, opts) {
|
||||||
|
var result = {
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
var share = Object.assign({ flushHelper: 'Map', array: 'Array' }, opts.share)
|
||||||
|
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
||||||
|
var connector = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, opts.connector)
|
||||||
|
for (let i = 0; i < opts.users; i++) {
|
||||||
|
let dbOpts
|
||||||
|
let connOpts
|
||||||
|
if (i === 0) {
|
||||||
|
// Only one instance can gc!
|
||||||
|
dbOpts = Object.assign({ gc: true }, opts.db)
|
||||||
|
connOpts = Object.assign({ role: 'master' }, connector)
|
||||||
|
} else {
|
||||||
|
dbOpts = Object.assign({ gc: false }, opts.db)
|
||||||
|
connOpts = Object.assign({ role: 'slave' }, connector)
|
||||||
|
}
|
||||||
|
let y = await Y({
|
||||||
|
connector: connOpts,
|
||||||
|
db: dbOpts,
|
||||||
|
share: share
|
||||||
|
})
|
||||||
|
result.users.push(y)
|
||||||
|
for (let name in share) {
|
||||||
|
result[name + i] = y.share[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.array0.delete(0, result.array0.length)
|
||||||
|
if (result.users[0].connector.testRoom != null) {
|
||||||
|
// flush for sync if test-connector
|
||||||
|
await result.users[0].connector.testRoom.flushAll(result.users)
|
||||||
|
}
|
||||||
|
await Promise.all(result.users.map(u => {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
u.connector.whenSynced(resolve)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
await flushAll(t, result.users)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushAll (t, users) {
|
||||||
|
// users = users.filter(u => u.connector.isSynced)
|
||||||
|
if (users.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await wait(0)
|
||||||
|
if (users[0].connector.testRoom != null) {
|
||||||
|
// use flushAll method specified in Test Connector
|
||||||
|
await users[0].connector.testRoom.flushAll(users)
|
||||||
|
} else {
|
||||||
|
// flush for any connector
|
||||||
|
await Promise.all(users.map(u => { return u.db.whenTransactionsFinished() }))
|
||||||
|
|
||||||
|
var flushCounter = users[0].share.flushHelper.get('0') || 0
|
||||||
|
flushCounter++
|
||||||
|
await Promise.all(users.map(async (u, i) => {
|
||||||
|
// wait for all users to set the flush counter to the same value
|
||||||
|
await new Promise(resolve => {
|
||||||
|
function observer () {
|
||||||
|
var allUsersReceivedUpdate = true
|
||||||
|
for (var i = 0; i < users.length; i++) {
|
||||||
|
if (u.share.flushHelper.get(i + '') !== flushCounter) {
|
||||||
|
allUsersReceivedUpdate = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allUsersReceivedUpdate) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.share.flushHelper.observe(observer)
|
||||||
|
u.share.flushHelper.set(i + '', flushCounter)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushSome (t, users) {
|
||||||
|
if (users[0].connector.testRoom == null) {
|
||||||
|
// if not test-connector, wait for some time for operations to arrive
|
||||||
|
await wait(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wait (t) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
setTimeout(resolve, t != null ? t : 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
151
tests-lib/test-connector.js
Normal file
151
tests-lib/test-connector.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/* global Y */
|
||||||
|
import { wait } from './helper.js'
|
||||||
|
|
||||||
|
var rooms = {}
|
||||||
|
|
||||||
|
export class TestRoom {
|
||||||
|
constructor (roomname) {
|
||||||
|
this.room = roomname
|
||||||
|
this.users = {}
|
||||||
|
this.nextUserId = 0
|
||||||
|
}
|
||||||
|
join (connector) {
|
||||||
|
if (connector.userId == null) {
|
||||||
|
connector.setUserId('' + (this.nextUserId++))
|
||||||
|
}
|
||||||
|
Object.keys(this.users).forEach(uid => {
|
||||||
|
this.users[uid].userJoined(connector.userId, connector.role)
|
||||||
|
connector.userJoined(uid, this.users[uid].role)
|
||||||
|
})
|
||||||
|
this.users[connector.userId] = connector
|
||||||
|
}
|
||||||
|
leave (connector) {
|
||||||
|
delete this.users[connector.userId]
|
||||||
|
Object.keys(this.users).forEach(uid => {
|
||||||
|
this.users[uid].userLeft(connector.userId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
send (sender, receiver, m) {
|
||||||
|
m = JSON.parse(JSON.stringify(m))
|
||||||
|
var user = this.users[receiver]
|
||||||
|
if (user != null) {
|
||||||
|
user.receiveMessage(sender, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcast (sender, m) {
|
||||||
|
Object.keys(this.users).forEach(receiver => {
|
||||||
|
this.send(sender, receiver, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async flushAll (users) {
|
||||||
|
let flushing = true
|
||||||
|
let allUserIds = Object.keys(this.users)
|
||||||
|
if (users == null) {
|
||||||
|
users = allUserIds.map(id => this.users[id].y)
|
||||||
|
}
|
||||||
|
while (flushing) {
|
||||||
|
await wait(10)
|
||||||
|
let res = await Promise.all(allUserIds.map(id => this.users[id]._flushAll(users)))
|
||||||
|
flushing = res.some(status => status === 'flushing')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTestRoom (roomname) {
|
||||||
|
if (rooms[roomname] == null) {
|
||||||
|
rooms[roomname] = new TestRoom(roomname)
|
||||||
|
}
|
||||||
|
return rooms[roomname]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function extendTestConnector (Y) {
|
||||||
|
class TestConnector extends Y.AbstractConnector {
|
||||||
|
constructor (y, options) {
|
||||||
|
if (options === undefined) {
|
||||||
|
throw new Error('Options must not be undefined!')
|
||||||
|
}
|
||||||
|
if (options.room == null) {
|
||||||
|
throw new Error('You must define a room name!')
|
||||||
|
}
|
||||||
|
super(y, options)
|
||||||
|
this.options = options
|
||||||
|
this.room = options.room
|
||||||
|
this.chance = options.chance
|
||||||
|
this.testRoom = getTestRoom(this.room)
|
||||||
|
this.testRoom.join(this)
|
||||||
|
}
|
||||||
|
disconnect () {
|
||||||
|
this.testRoom.leave(this)
|
||||||
|
return super.disconnect()
|
||||||
|
}
|
||||||
|
reconnect () {
|
||||||
|
this.testRoom.join(this)
|
||||||
|
return super.reconnect()
|
||||||
|
}
|
||||||
|
send (uid, message) {
|
||||||
|
this.testRoom.send(this.userId, uid, message)
|
||||||
|
}
|
||||||
|
broadcast (message) {
|
||||||
|
this.testRoom.broadcast(this.userId, message)
|
||||||
|
}
|
||||||
|
async whenSynced (f) {
|
||||||
|
var synced = false
|
||||||
|
var periodicFlushTillSync = () => {
|
||||||
|
if (synced) {
|
||||||
|
f()
|
||||||
|
} else {
|
||||||
|
this.testRoom.flushAll([this.y]).then(function () {
|
||||||
|
setTimeout(periodicFlushTillSync, 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
periodicFlushTillSync()
|
||||||
|
return super.whenSynced(function () {
|
||||||
|
synced = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
receiveMessage (sender, m) {
|
||||||
|
if (this.userId !== sender && this.connections[sender] != null) {
|
||||||
|
var buffer = this.connections[sender].buffer
|
||||||
|
if (buffer == null) {
|
||||||
|
buffer = this.connections[sender].buffer = []
|
||||||
|
}
|
||||||
|
buffer.push(m)
|
||||||
|
if (this.chance.bool({likelihood: 30})) {
|
||||||
|
// flush 1/2 with 30% chance
|
||||||
|
var flushLength = Math.round(buffer.length / 2)
|
||||||
|
buffer.splice(0, flushLength).forEach(m => {
|
||||||
|
super.receiveMessage(sender, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _flushAll (flushUsers) {
|
||||||
|
if (flushUsers.some(u => u.connector.userId === this.userId)) {
|
||||||
|
// this one needs to sync with every other user
|
||||||
|
flushUsers = Object.keys(this.connections).map(id => this.testRoom.users[id].y)
|
||||||
|
}
|
||||||
|
var finished = []
|
||||||
|
for (let i = 0; i < flushUsers.length; i++) {
|
||||||
|
let userId = flushUsers[i].connector.userId
|
||||||
|
if (userId === this.userId) continue
|
||||||
|
let buffer = this.connections[userId].buffer
|
||||||
|
if (buffer != null) {
|
||||||
|
var messages = buffer.splice(0)
|
||||||
|
for (let j = 0; j < messages.length; j++) {
|
||||||
|
let p = super.receiveMessage(userId, messages[j])
|
||||||
|
finished.push(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(finished)
|
||||||
|
await this.y.db.whenTransactionsFinished()
|
||||||
|
return finished.length > 0 ? 'flushing' : 'done'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.extend('test', TestConnector)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof Y !== 'undefined') {
|
||||||
|
extendTestConnector(Y)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user