Compare commits
1 Commits
v13.0.0-35
...
v11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9374c363c3 |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
["latest", {
|
|
||||||
"es2015": {
|
|
||||||
"modules": false
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"external-helpers"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
[include]
|
[include]
|
||||||
./src/
|
./src/
|
||||||
./tests-lib/
|
|
||||||
./test/
|
|
||||||
|
|
||||||
[libs]
|
[libs]
|
||||||
./declarations/
|
./declarations/
|
||||||
|
|||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,4 +1,15 @@
|
|||||||
node_modules
|
node_modules
|
||||||
bower_components
|
bower_components
|
||||||
/y.*
|
build
|
||||||
/examples/yjs-dist.js*
|
build_test
|
||||||
|
.directory
|
||||||
|
.codio
|
||||||
|
.settings
|
||||||
|
.jshintignore
|
||||||
|
.jshintrc
|
||||||
|
.validate.json
|
||||||
|
/y.js
|
||||||
|
/y.js.map
|
||||||
|
/y-*
|
||||||
|
.vscode
|
||||||
|
jsconfig.json
|
||||||
|
|||||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "dist"]
|
||||||
|
path = dist
|
||||||
|
url = https://github.com/y-js/yjs.git
|
||||||
|
branch = dist
|
||||||
329
README.md
329
README.md
@@ -1,216 +1,133 @@
|
|||||||
|
|
||||||
# 
|
# 
|
||||||
|
|
||||||
Yjs is a framework for offline-first p2p shared editing on structured data like
|
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data.
|
||||||
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer
|
||||||
most of the complexity of concurrent editing. For additional information, demos,
|
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data
|
||||||
and tutorials visit [y-js.org](http://y-js.org/).
|
like Text, Json, and XML. We also provide support for storing and manipulating your shared data offline.
|
||||||
|
For more information and demo applications visit our [homepage](http://y-js.org/).
|
||||||
|
|
||||||
### Extensions
|
You can create you own shared types easily.
|
||||||
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
|
Therefore, you can design the structure of your custom type,
|
||||||
* *Connector* - a communication protocol that propagates changes to the clients
|
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data).
|
||||||
* *Database* - a database to store your changes
|
We already provide abstract data types for
|
||||||
* one or more *Types* - that represent the shared data
|
|
||||||
|
|
||||||
Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
|
||||||
is a list of the modules we know of:
|
|
||||||
|
|
||||||
##### Connectors
|
|
||||||
|
|
||||||
|Name | Description |
|
|
||||||
|----------------|-----------------------------------|
|
|
||||||
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
|
||||||
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
|
||||||
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
|
||||||
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|
|
||||||
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
|
||||||
|
|
||||||
##### Database adapters
|
|
||||||
|
|
||||||
|Name | Description |
|
|
||||||
|----------------|-----------------------------------|
|
|
||||||
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|
|
||||||
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|
|
||||||
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
|
|
||||||
|
|
||||||
|
|
||||||
##### Types
|
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|----------|-------------------|
|
|----------|-------------------|
|
||||||
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|
||||||
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|
||||||
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|
||||||
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) |
|
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to textareas, input elements, or HTML elements (e.g. <*h1*>, or <*p*>). Also supports the [Ace Editor](https://ace.c9.io) |
|
||||||
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
||||||
|
|
||||||
##### Other
|
Yjs supports P2P message propagation, and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
|
||||||
|
|
||||||
| Name | Description |
|
We support several communication protocols as so called *Connectors*.
|
||||||
|-----------|-------------------|
|
You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors).
|
||||||
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
|
Currently, we support the following communication protocols:
|
||||||
|
|
||||||
|
|Name | Description |
|
||||||
|
|----------------|-----------------------------------|
|
||||||
|
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
||||||
|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
||||||
|
|[websockets](https://github.com/y-js/y-websockets-client) | Exchange updates efficiently in the classical client-server model |
|
||||||
|
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
||||||
|
|
||||||
|
You are not limited to use a specific database to store the shared data. We provide the following database adapters:
|
||||||
|
|
||||||
|
|Name | Description |
|
||||||
|
|----------------|-----------------------------------|
|
||||||
|
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|
||||||
|
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|
||||||
|
|
||||||
|
The advantages over similar frameworks are support for
|
||||||
|
* .. P2P message propagation and arbitrary communication protocols
|
||||||
|
* .. share any type of data. The types provide a convenient interface
|
||||||
|
* .. offline support: Changes are stored persistently and only relevant changes are propagated on rejoin
|
||||||
|
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline. Every type has a notion on how we define Intention Preservation on it.
|
||||||
|
|
||||||
## Use it!
|
## Use it!
|
||||||
Install Yjs, and its modules with [bower](http://bower.io/), or
|
Install yjs and its modules with [bower](http://bower.io/), or with [npm](https://www.npmjs.org/package/yjs).
|
||||||
[npm](https://www.npmjs.org/package/yjs).
|
|
||||||
|
|
||||||
### Bower
|
### Bower
|
||||||
```
|
```
|
||||||
bower install --save yjs y-array % add all y-* modules you want to use
|
bower install yjs --save
|
||||||
```
|
```
|
||||||
You only need to include the `y.js` file. Yjs is able to automatically require
|
Then you include the libraries directly from the installation folder.
|
||||||
missing modules.
|
|
||||||
```
|
```
|
||||||
<script src="./bower_components/yjs/y.js"></script>
|
<script src="./bower_components/yjs/y.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Npm
|
### Npm
|
||||||
```
|
```
|
||||||
npm install --save yjs % add all y-* modules you want to use
|
npm install yjs --save
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't include via script tag, you have to explicitly include all modules!
|
And use it like this with *npm*:
|
||||||
(Same goes for other module systems)
|
|
||||||
```
|
```
|
||||||
var Y = require('yjs')
|
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
|
|
||||||
```
|
```
|
||||||
bower i yjs y-memory y-webrtc y-array y-text
|
Y({
|
||||||
|
db: {
|
||||||
|
name: 'memory' // store in memory.
|
||||||
|
// name: 'indexeddb'
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'websockets-client', // choose the websockets connector
|
||||||
|
// name: 'webrtc'
|
||||||
|
// name: 'xmpp'
|
||||||
|
room: 'Textarea-example-dev'
|
||||||
|
},
|
||||||
|
sourceDir: '/bower_components', // location of the y-* modules
|
||||||
|
share: {
|
||||||
|
textarea: 'Text' // y.share.textarea is of type Y.Text
|
||||||
|
}
|
||||||
|
// types: ['Richtext', 'Array'] // optional list of types you want to import
|
||||||
|
}).then(function (y) {
|
||||||
|
// bind the textarea to a shared text element
|
||||||
|
y.share.textarea.bind(document.getElementById('textfield'))
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Here is a simple example of a shared textarea
|
# Api
|
||||||
```HTML
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<script src="./bower_components/yjs/y.js"></script>
|
|
||||||
<!-- Yjs automatically includes all missing dependencies (browser only) -->
|
|
||||||
<script>
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory' // use memory database adapter.
|
|
||||||
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'webrtc', // use webrtc connector
|
|
||||||
// name: 'websockets-client'
|
|
||||||
// name: 'xmpp'
|
|
||||||
room: 'my-room' // clients connecting to the same room share data
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components', // location of the y-* modules (browser only)
|
|
||||||
share: {
|
|
||||||
textarea: 'Text' // y.share.textarea is of type y-text
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
// The Yjs instance `y` is available
|
|
||||||
// y.share.* contains the shared types
|
|
||||||
|
|
||||||
// Bind `y.share.textarea` to `<textarea/>`
|
|
||||||
y.share.textarea.bind(document.querySelector('textarea'))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<textarea></textarea>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Get Help & Give Help
|
|
||||||
There are some friendly people on [](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
|
|
||||||
|
|
||||||
Report _any_ issues to the
|
|
||||||
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
|
|
||||||
soon, if possible.
|
|
||||||
|
|
||||||
# API
|
|
||||||
|
|
||||||
### Y(options)
|
### Y(options)
|
||||||
* Y.extend(module1, module2, ..)
|
|
||||||
* Add extensions to Y
|
|
||||||
* `Y.extend(require('y-webrtc'))` has the same semantics as
|
|
||||||
`require('y-webrtc')(Y)`
|
|
||||||
* options.db
|
* options.db
|
||||||
* Will be forwarded to the database adapter. Specify the database adaper on
|
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`.
|
||||||
`options.db.name`.
|
* Have a look at the used database adapter repository to see all available options.
|
||||||
* Have a look at the used database adapter repository to see all available
|
|
||||||
options.
|
|
||||||
* options.connector
|
* options.connector
|
||||||
* Will be forwarded to the connector adapter. Specify the connector adaper on
|
* Will be forwarded to the connector adapter. Specify the connector adaper on `options.connector.name`.
|
||||||
`options.connector.name`.
|
* All our connectors implement a `room` property. Clients that specify the same room share the same data.
|
||||||
* All our connectors implement a `room` property. Clients that specify the
|
* All of our connectors specify an `url` property that defines the connection endpoint of the used connector.
|
||||||
same room share the same data.
|
* All of our connectors also have a default connection endpoint that you can use for development.
|
||||||
* All of our connectors specify an `url` property that defines the connection
|
|
||||||
endpoint of the used connector.
|
|
||||||
* All of our connectors also have a default connection endpoint that you can
|
|
||||||
use for development.
|
|
||||||
* Set `options.connector.generateUserId = true` in order to genenerate a
|
|
||||||
userid, instead of receiving one from the server. This way the `Y(..)` is
|
|
||||||
immediately going to be resolved, without waiting for any confirmation from
|
|
||||||
the server. Use with caution.
|
|
||||||
* Have a look at the used connector repository to see all available options.
|
* Have a look at the used connector repository to see all available options.
|
||||||
* *Only if you know what you are doing:* Set
|
* options.sourceDir
|
||||||
`options.connector.preferUntransformed = true` in order receive the shared
|
* Path where all y-* modules are stored.
|
||||||
data untransformed. This is very efficient as the database content is simply
|
|
||||||
copied to this client. This does only work if this client receives content
|
|
||||||
from only one client.
|
|
||||||
* options.sourceDir (browser only)
|
|
||||||
* Path where all y-* modules are stored
|
|
||||||
* Defaults to `/bower_components`
|
* Defaults to `/bower_components`
|
||||||
* Not required when running on `nodejs` / `iojs`
|
* Not required when running on `nodejs` / `iojs`
|
||||||
* When using nodejs you need to manually extend Yjs:
|
* When using browserify you can specify all used modules like this:
|
||||||
```
|
```
|
||||||
var Y = require('yjs')
|
var Y = require('yjs')
|
||||||
// you have to require a db, connector, and *all* types you use!
|
// you need to require the db, connector, and *all* types you use!
|
||||||
require('y-memory')(Y)
|
require('y-memory')(Y)
|
||||||
require('y-webrtc')(Y)
|
require('y-webrtc')(Y)
|
||||||
require('y-map')(Y)
|
require('y-map')(Y)
|
||||||
// ..
|
// ..
|
||||||
```
|
```
|
||||||
* options.share
|
* options.share
|
||||||
* Specify on `options.share[arbitraryName]` types that are shared among all
|
* Specify on `options.share[arbitraryName]` types that are shared among all users.
|
||||||
users.
|
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and create an Y.Array type on `y.share[arbitraryName]`.
|
||||||
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and
|
* If userA doesn't specify `options.share[arbitraryName]`, it won't be available for userA.
|
||||||
create an y-array type on `y.share[arbitraryName]`.
|
* If userB specifies `options.share[arbitraryName]`, it still won't be available for userA. But all the updates are send from userB to userA.
|
||||||
* If userA doesn't specify `options.share[arbitraryName]`, it won't be
|
* In contrast to Y.Map, types on `y.share.*` cannot be overwritten or deleted. Instead, they are merged among all users. This feature is only available on `y.share.*`
|
||||||
available for userA.
|
* Weird behavior: It is supported that two users specify different types with the same property name.
|
||||||
* If userB specifies `options.share[arbitraryName]`, it still won't be
|
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies `options.share.x = 'Text'`. But they'll only share data if they specified the same type with the same property name
|
||||||
available for userA. But all the updates are send from userB to userA.
|
* options.type
|
||||||
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
|
* Array of modules that Yjs needs to require, before instantiating a shared type.
|
||||||
Instead, they are merged among all users. This feature is only available on
|
* By default Yjs requires the specified database adapter, the specified connector, and all modules that are used in `options.share.*`
|
||||||
`y.share.*`
|
|
||||||
* Weird behavior: It is supported that two users specify different types with
|
|
||||||
the same property name.
|
|
||||||
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies
|
|
||||||
`options.share.x = 'Text'`. But they only share data if they specified the
|
|
||||||
same type with the same property name
|
|
||||||
* options.type (browser only)
|
|
||||||
* Array of modules that Yjs needs to require, before instantiating a shared
|
|
||||||
type.
|
|
||||||
* By default Yjs requires the specified database adapter, the specified
|
|
||||||
connector, and all modules that are used in `options.share.*`
|
|
||||||
* Put all types here that you intend to use, but are not used in y.share.*
|
* Put all types here that you intend to use, but are not used in y.share.*
|
||||||
|
|
||||||
### Instantiated Y object (y)
|
### Instantiated Y object (y)
|
||||||
@@ -220,8 +137,7 @@ require('y-map')(Y)
|
|||||||
* The specified database adapter is loaded
|
* The specified database adapter is loaded
|
||||||
* The specified connector is loaded
|
* The specified connector is loaded
|
||||||
* All types are included
|
* All types are included
|
||||||
* The connector is initialized, and a unique user id is set (received from the
|
* The connector is initialized, and a unique user id is set (received from the server)
|
||||||
server)
|
|
||||||
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
|
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
|
||||||
|
|
||||||
The promise returns an instance of Y. We denote it with a lower case `y`.
|
The promise returns an instance of Y. We denote it with a lower case `y`.
|
||||||
@@ -239,57 +155,70 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
|
|||||||
* y.connector.disconnect()
|
* y.connector.disconnect()
|
||||||
* Force to disconnect this instance from the other instances
|
* Force to disconnect this instance from the other instances
|
||||||
* y.connector.reconnect()
|
* y.connector.reconnect()
|
||||||
* Try to reconnect to the other instances (needs to be supported by the
|
* Try to reconnect to the other instances (needs to be supported by the connector)
|
||||||
connector)
|
|
||||||
* Not supported by y-xmpp
|
* Not supported by y-xmpp
|
||||||
* y.close()
|
* y.destroy()
|
||||||
* Destroy this object.
|
* Destroy this object.
|
||||||
* Destroys all types (they will throw weird errors if you still use them)
|
* Destroys all types (they will throw weird errors if you still use them)
|
||||||
* Disconnects from the other instances (via connector)
|
* Disconnects from the other instances (via connector)
|
||||||
* Returns a promise
|
|
||||||
* y.destroy()
|
|
||||||
* calls y.close()
|
|
||||||
* Removes all data from the database
|
* Removes all data from the database
|
||||||
* Returns a promise
|
|
||||||
* y.db.stopGarbageCollector()
|
* y.db.stopGarbageCollector()
|
||||||
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
|
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage collection
|
||||||
collection
|
|
||||||
* y.db.gc :: Boolean
|
|
||||||
* Whether gc is turned on
|
|
||||||
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
||||||
* Time interval between two garbage collect cycles
|
* Time interval between two garbage collect cycles
|
||||||
* It is required that all instances exchanged all messages after two garbage
|
* It is required that all instances exchanged all messages after two garbage collect cycles (after 100000 ms per default)
|
||||||
collect cycles (after 100000 ms per default)
|
|
||||||
* y.db.userId :: String
|
* y.db.userId :: String
|
||||||
* The used user id for this client. **Never overwrite this**
|
* The used user id for this client. **Never overwrite this**
|
||||||
|
|
||||||
### Logging
|
## Get help
|
||||||
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
|
There are some friendly people on [](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who may help you with your problem, and answer your questions.
|
||||||
`y*` enables logging for all y-* components. You can selectively remove
|
|
||||||
components you are not interested in: E.g. The flag `y*,-y:connector-message`
|
|
||||||
will not log the long `y:connector-message` messages.
|
|
||||||
|
|
||||||
##### Enable logging in Node.js
|
Please report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
|
||||||
```sh
|
If you want to see an issue fixed, please subscribe to the thread (or remind me via gitter).
|
||||||
DEBUG=y* node app.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove the colors in order to log to a file:
|
|
||||||
```sh
|
|
||||||
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Enable logging in the browser
|
## Changelog
|
||||||
```js
|
|
||||||
localStorage.debug = 'y*'
|
### 11.0.0
|
||||||
```
|
|
||||||
|
* **All types now return a single event instead of list of events**
|
||||||
|
* Insert events contain a list of values
|
||||||
|
* Improved performance for large insertions & deletions
|
||||||
|
* Several bugfixes (offline editing related)
|
||||||
|
* Native support for node 4 (see #49)
|
||||||
|
|
||||||
|
### 10.0.0
|
||||||
|
|
||||||
|
* Support for more complex types (a type can be a composition of several types)
|
||||||
|
* Fixes several memory leaks
|
||||||
|
|
||||||
|
### 9.0.0
|
||||||
|
There were several rolling updates from 0.6 to 0.8. We consider Yjs stable since a long time,
|
||||||
|
and intend to continue stable releases. From this release forward y-* modules will implement peer-dependencies for npm, and dependencies for bower.
|
||||||
|
Furthermore, incompatible yjs instances will now throw errors when syncing - this feature was influenced by #48. The versioning jump was influenced by react (see [here](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html))
|
||||||
|
|
||||||
|
|
||||||
|
### 0.6.0
|
||||||
|
This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 0.6.0 it is possible to work asynchronously on a persistent database, which enables offline support.
|
||||||
|
* Switched to semver versioning
|
||||||
|
* Requires a promise implementation in environment (es6 promises suffice, included in all the major browsers). Otherwise you have to include a polyfill
|
||||||
|
* Y.Object has been renamed to Y.Map
|
||||||
|
* Y.Map exchanges `.val(name [, value])` in favor of `.set(name, value)` and `.get(name)`
|
||||||
|
* Y.Map `.get(name)` returns a promise, if the value is a custom type
|
||||||
|
* The Connector definition slightly changed (I'll update the wiki)
|
||||||
|
* The Type definitions completely changed, so you have to rewrite them (I'll rewrite the article in the wiki)
|
||||||
|
* Support for several packaging systems
|
||||||
|
* Flowtype
|
||||||
|
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
I created this framework during my bachelor thesis at the chair of computer
|
I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
|
||||||
science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since
|
|
||||||
December 2014 I'm working on Yjs as a part of my student worker job at the i5.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Yjs is licensed under the [MIT License](./LICENSE).
|
Yjs is licensed under the [MIT License](./LICENSE.txt).
|
||||||
|
|
||||||
<yjs@dbis.rwth-aachen.de>
|
<yjs@dbis.rwth-aachen.de>
|
||||||
|
|
||||||
|
[ShareJs]: https://github.com/share/ShareJS
|
||||||
|
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki
|
||||||
|
|
||||||
|
|||||||
72
declarations/Structs.js
Normal file
72
declarations/Structs.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* @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
|
||||||
|
|
||||||
0
declarations/Type.js
Normal file
0
declarations/Type.js
Normal file
34
declarations/Y.js
Normal file
34
declarations/Y.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* @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>;
|
||||||
|
}
|
||||||
1
dist
Submodule
1
dist
Submodule
Submodule dist added at 47bcec8bc7
@@ -1,32 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
})
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"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": {
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!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="../../y.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
|
||||||
var y = new Y({
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'chat-example'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
window.yChat = y
|
|
||||||
|
|
||||||
let chatprotocol = y.define('chatprotocol', Y.Array)
|
|
||||||
|
|
||||||
let chatcontainer = document.querySelector('#chat')
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
|
|
||||||
}
|
|
||||||
// This function makes sure that only 7 messages exist in the chat history.
|
|
||||||
// The rest is deleted
|
|
||||||
function cleanupChat () {
|
|
||||||
if (chatprotocol.length > 7) {
|
|
||||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Insert the initial content
|
|
||||||
chatprotocol.toArray().forEach(appendMessage)
|
|
||||||
cleanupChat()
|
|
||||||
|
|
||||||
// whenever content changes, make sure to reflect the changes in the DOM
|
|
||||||
chatprotocol.observe(function (event) {
|
|
||||||
if (event.type === 'insert') {
|
|
||||||
for (let i = 0; i < event.length; i++) {
|
|
||||||
appendMessage(event.values[i], event.index + i)
|
|
||||||
}
|
|
||||||
} else if (event.type === 'delete') {
|
|
||||||
for (let i = 0; i < event.length; i++) {
|
|
||||||
chatcontainer.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 (chatprotocol.length > 6) {
|
|
||||||
// If we are goint to insert the 8th element, make sure to delete first.
|
|
||||||
chatprotocol.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
|
|
||||||
chatprotocol.push([message])
|
|
||||||
this.querySelector('[name=message]').value = ''
|
|
||||||
}
|
|
||||||
// Do not send this form!
|
|
||||||
event.preventDefault()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
})
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!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="../../y.js"></script>
|
|
||||||
<script src="../../../y-array/y-array.js"></script>
|
|
||||||
<script src="../../../y-map/dist/y-map.js"></script>
|
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="../bower_components/d3/d3.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/* 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
</head>
|
|
||||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
|
||||||
<script src="../yjs-dist.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</head>
|
|
||||||
<body contenteditable="true">
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/* global Y, HTMLElement, customElements */
|
|
||||||
|
|
||||||
class MagicTable extends HTMLElement {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
var shadow = this.attachShadow({mode: 'open'})
|
|
||||||
setTimeout(() => {
|
|
||||||
shadow.append(this.childNodes[0])
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('magic-table', MagicTable)
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
|
||||||
let y = new Y({
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
url: 'http://127.0.0.1:1234',
|
|
||||||
room: 'html-editor-example6'
|
|
||||||
// maxBufferLength: 100
|
|
||||||
}
|
|
||||||
})
|
|
||||||
window.yXml = y
|
|
||||||
window.yXmlType = y.define('xml', Y.XmlFragment)
|
|
||||||
window.onload = function () {
|
|
||||||
console.log('start!')
|
|
||||||
// Bind children of XmlFragment to the document.body
|
|
||||||
window.yXmlType.bindToDom(document.body)
|
|
||||||
}
|
|
||||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
|
||||||
captureTimeout: 500
|
|
||||||
})
|
|
||||||
|
|
||||||
document.onkeydown = function interceptUndoRedo (e) {
|
|
||||||
if (e.keyCode === 90 && e.metaKey) {
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
window.undoManager.undo()
|
|
||||||
} else {
|
|
||||||
window.undoManager.redo()
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="codeMirrorContainer"></div>
|
|
||||||
<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 type="module" src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
})
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
grid-gap: 7px;
|
|
||||||
}
|
|
||||||
.one {
|
|
||||||
grid-column: 1 ;
|
|
||||||
}
|
|
||||||
.two {
|
|
||||||
grid-column: 2;
|
|
||||||
}
|
|
||||||
.three {
|
|
||||||
grid-column: 3;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
width: calc(100% - 10px)
|
|
||||||
}
|
|
||||||
.editor-container {
|
|
||||||
background-color: #4caf50;
|
|
||||||
padding: 4px 5px 10px 5px;
|
|
||||||
border-radius: 11px;
|
|
||||||
}
|
|
||||||
.editor-container[disconnected] {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
.disconnected-info {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.editor-container[disconnected] .disconnected-info {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="wrapper">
|
|
||||||
<div id="container1" class="one editor-container">
|
|
||||||
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
|
|
||||||
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
|
||||||
</div>
|
|
||||||
<div id="container2" class="two editor-container">
|
|
||||||
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
|
|
||||||
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
|
||||||
</div>
|
|
||||||
<div id="container3" class="three editor-container">
|
|
||||||
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
|
|
||||||
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="../../y.js"></script>
|
|
||||||
<script src="../../../y-array/y-array.js"></script>
|
|
||||||
<script src="../../../y-text/dist/y-text.js"></script>
|
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'Textarea-example',
|
|
||||||
url: 'https://yjs-v13.herokuapp.com/'
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
textarea: 'Text'
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.y1 = y
|
|
||||||
y.share.textarea.bind(document.getElementById('textarea1'))
|
|
||||||
})
|
|
||||||
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'Textarea-example',
|
|
||||||
url: 'https://yjs-v13-second.herokuapp.com/'
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
textarea: 'Text'
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.y2 = y
|
|
||||||
y.share.textarea.bind(document.getElementById('textarea2'))
|
|
||||||
y.connector.socket.on('connection', function () {
|
|
||||||
document.getElementById('container2').removeAttribute('disconnected')
|
|
||||||
})
|
|
||||||
y.connector.socket.on('disconnect', function () {
|
|
||||||
document.getElementById('container2').setAttribute('disconnected', true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'Textarea-example',
|
|
||||||
url: 'https://yjs-v13-third.herokuapp.com/'
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
textarea: 'Text'
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.y3 = y
|
|
||||||
y.share.textarea.bind(document.getElementById('textarea3'))
|
|
||||||
y.connector.socket.on('connection', function () {
|
|
||||||
document.getElementById('container3').removeAttribute('disconnected')
|
|
||||||
})
|
|
||||||
y.connector.socket.on('disconnect', function () {
|
|
||||||
document.getElementById('container3').setAttribute('disconnected', true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<!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="../../y.js"></script>
|
|
||||||
<script src="../../../y-map/dist/y-map.js"></script>
|
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="../bower_components/d3/d3.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/* @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',
|
|
||||||
url: 'http://localhost:1234'
|
|
||||||
},
|
|
||||||
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 + ')'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<!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-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/* global Y, monaco */
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
1173
examples/package-lock.json
generated
1173
examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "examples",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "",
|
|
||||||
"scripts": {
|
|
||||||
"dist": "rollup -c",
|
|
||||||
"watch": "rollup -cw"
|
|
||||||
},
|
|
||||||
"author": "Kevin Jahns",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"monaco-editor": "^0.8.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"standard": "^10.0.2"
|
|
||||||
},
|
|
||||||
"standard": {
|
|
||||||
"ignore": ["bower_components"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<!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="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
|
||||||
<link href="https://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="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
|
||||||
<script src="https://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="../../y.js"></script>
|
|
||||||
<script src="../../../y-array/y-array.js"></script>
|
|
||||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/* 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',
|
|
||||||
url: 'http://localhost:1234'
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
|
|
||||||
var pkg = require('./package.json')
|
|
||||||
|
|
||||||
export default {
|
|
||||||
entry: 'yjs-dist.esm',
|
|
||||||
dest: 'yjs-dist.js',
|
|
||||||
moduleName: 'Y',
|
|
||||||
format: 'umd',
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs()
|
|
||||||
],
|
|
||||||
sourceMap: true,
|
|
||||||
banner: `
|
|
||||||
/**
|
|
||||||
* ${pkg.name} - ${pkg.description}
|
|
||||||
* @version v${pkg.version}
|
|
||||||
* @license ${pkg.license}
|
|
||||||
*/
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/* 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)
|
|
||||||
})
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/* 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'
|
|
||||||
)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
|
||||||
<script src="../../y.js"></script>
|
|
||||||
<script src="../../../y-array/y-array.js"></script>
|
|
||||||
<script src="../../../y-text/y-text.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'Textarea-example2',
|
|
||||||
// url: '//localhost:1234',
|
|
||||||
url: 'https://yjs-v13.herokuapp.com/'
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
textarea: 'Text'
|
|
||||||
},
|
|
||||||
timeout: 5000 // reject if no connection was established within 5 seconds
|
|
||||||
}).then(function (y) {
|
|
||||||
window.yTextarea = y
|
|
||||||
|
|
||||||
// bind the textarea to a shared text element
|
|
||||||
y.share.textarea.bind(document.getElementById('textfield'))
|
|
||||||
})
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
</head>
|
|
||||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
|
||||||
<script src="../yjs-dist.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>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
// url: 'http://127.0.0.1:1234',
|
|
||||||
url: 'http://192.168.178.81:1234',
|
|
||||||
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,7 +0,0 @@
|
|||||||
|
|
||||||
import Y from '../src/Y.js'
|
|
||||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
|
||||||
|
|
||||||
Y.extend(yWebsocketsClient)
|
|
||||||
|
|
||||||
export default Y
|
|
||||||
202
gulpfile.helper.js
Normal file
202
gulpfile.helper.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
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
Normal file
104
gulpfile.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/* 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')
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
5245
package-lock.json
generated
5245
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
88
package.json
88
package.json
@@ -1,27 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-35",
|
"version": "11.2.6",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
|
||||||
"main": "./y.node.js",
|
"main": "./src/y.js",
|
||||||
"browser": "./y.js",
|
|
||||||
"module": "./src/y.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "node --harmony ./node_modules/.bin/gulp test",
|
||||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
"lint": "./node_modules/.bin/standard"
|
||||||
"lint": "standard",
|
|
||||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
|
||||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
|
||||||
"postversion": "npm run dist",
|
|
||||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"pre-commit": [
|
||||||
"y.*",
|
"lint",
|
||||||
"src/*"
|
"test"
|
||||||
],
|
],
|
||||||
"standard": {
|
"standard": {
|
||||||
|
"parser": "babel-eslint",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"/y.js",
|
"build/**",
|
||||||
"/y.js.map"
|
"dist/**",
|
||||||
|
"declarations/**",
|
||||||
|
"./y.js",
|
||||||
|
"./y.js.map"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -45,29 +42,38 @@
|
|||||||
},
|
},
|
||||||
"homepage": "http://y-js.org",
|
"homepage": "http://y-js.org",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.24.1",
|
"babel-eslint": "^5.0.0-beta6",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"babel-plugin-transform-runtime": "^6.1.18",
|
||||||
"babel-plugin-transform-regenerator": "^6.24.1",
|
"babel-preset-es2015": "^6.1.18",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babelify": "^7.2.0",
|
||||||
"babel-preset-latest": "^6.24.1",
|
"browserify": "^12.0.1",
|
||||||
"chance": "^1.0.9",
|
"eslint": "^1.10.2",
|
||||||
"concurrently": "^3.4.0",
|
"gulp": "^3.9.0",
|
||||||
"cutest": "^0.1.9",
|
"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",
|
||||||
"tag-dist-files": "^0.1.6"
|
"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",
|
|
||||||
"fast-diff": "^1.1.2",
|
|
||||||
"utf-8": "^1.0.0",
|
|
||||||
"utf8": "^2.1.2"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
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 {
|
|
||||||
input: 'src/Y.js',
|
|
||||||
name: 'Y',
|
|
||||||
sourcemap: true,
|
|
||||||
output: {
|
|
||||||
file: 'y.js',
|
|
||||||
format: 'umd'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
babel(),
|
|
||||||
uglify({
|
|
||||||
mangle: {
|
|
||||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
comments: function (node, comment) {
|
|
||||||
var text = comment.value
|
|
||||||
var type = comment.type
|
|
||||||
if (type === 'comment2') {
|
|
||||||
// multiline comment
|
|
||||||
return /@license/i.test(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
banner: `
|
|
||||||
/**
|
|
||||||
* ${pkg.name} - ${pkg.description}
|
|
||||||
* @version v${pkg.version}
|
|
||||||
* @license ${pkg.license}
|
|
||||||
*/
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
var pkg = require('./package.json')
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'src/y-dist.cjs.js',
|
|
||||||
nameame: 'Y',
|
|
||||||
sourcemap: true,
|
|
||||||
output: {
|
|
||||||
file: 'y.node.js',
|
|
||||||
format: 'cjs'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs()
|
|
||||||
],
|
|
||||||
banner: `
|
|
||||||
/**
|
|
||||||
* ${pkg.name} - ${pkg.description}
|
|
||||||
* @version v${pkg.version}
|
|
||||||
* @license ${pkg.license}
|
|
||||||
*/
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
import multiEntry from 'rollup-plugin-multi-entry'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'test/y-xml.tests.js',
|
|
||||||
name: 'y-tests',
|
|
||||||
sourcemap: true,
|
|
||||||
output: {
|
|
||||||
file: 'y.test.js',
|
|
||||||
format: 'umd'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
multiEntry()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import utf8 from 'utf-8'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
|
|
||||||
export default class BinaryDecoder {
|
|
||||||
constructor (buffer) {
|
|
||||||
if (buffer instanceof ArrayBuffer) {
|
|
||||||
this.uint8arr = new Uint8Array(buffer)
|
|
||||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
|
||||||
this.uint8arr = buffer
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
|
||||||
}
|
|
||||||
this.pos = 0
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Clone this decoder instance
|
|
||||||
* Optionally set a new position parameter
|
|
||||||
*/
|
|
||||||
clone (newPos = this.pos) {
|
|
||||||
let decoder = new BinaryDecoder(this.uint8arr)
|
|
||||||
decoder.pos = newPos
|
|
||||||
return decoder
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Number of bytes
|
|
||||||
*/
|
|
||||||
get length () {
|
|
||||||
return this.uint8arr.length
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Skip one byte, jump to the next position
|
|
||||||
*/
|
|
||||||
skip8 () {
|
|
||||||
this.pos++
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read one byte as unsigned integer
|
|
||||||
*/
|
|
||||||
readUint8 () {
|
|
||||||
return this.uint8arr[this.pos++]
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read 4 bytes as unsigned integer
|
|
||||||
*/
|
|
||||||
readUint32 () {
|
|
||||||
let uint =
|
|
||||||
this.uint8arr[this.pos] +
|
|
||||||
(this.uint8arr[this.pos + 1] << 8) +
|
|
||||||
(this.uint8arr[this.pos + 2] << 16) +
|
|
||||||
(this.uint8arr[this.pos + 3] << 24)
|
|
||||||
this.pos += 4
|
|
||||||
return uint
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Look ahead without incrementing position
|
|
||||||
* to the next byte and read it as unsigned integer
|
|
||||||
*/
|
|
||||||
peekUint8 () {
|
|
||||||
return this.uint8arr[this.pos]
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read unsigned integer (32bit) with variable length
|
|
||||||
* 1/8th of the storage is used as encoding overhead
|
|
||||||
* - numbers < 2^7 is stored in one byte
|
|
||||||
* - numbers < 2^14 is stored in two bytes
|
|
||||||
* ..
|
|
||||||
*/
|
|
||||||
readVarUint () {
|
|
||||||
let num = 0
|
|
||||||
let len = 0
|
|
||||||
while (true) {
|
|
||||||
let r = this.uint8arr[this.pos++]
|
|
||||||
num = num | ((r & 0b1111111) << len)
|
|
||||||
len += 7
|
|
||||||
if (r < 1 << 7) {
|
|
||||||
return num >>> 0 // return unsigned number!
|
|
||||||
}
|
|
||||||
if (len > 35) {
|
|
||||||
throw new Error('Integer out of range!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read string of variable length
|
|
||||||
* - varUint is used to store the length of the string
|
|
||||||
*/
|
|
||||||
readVarString () {
|
|
||||||
let len = this.readVarUint()
|
|
||||||
let bytes = new Array(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = this.uint8arr[this.pos++]
|
|
||||||
}
|
|
||||||
return utf8.getStringFromBytes(bytes)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Look ahead and read varString without incrementing position
|
|
||||||
*/
|
|
||||||
peekVarString () {
|
|
||||||
let pos = this.pos
|
|
||||||
let s = this.readVarString()
|
|
||||||
this.pos = pos
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read ID
|
|
||||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
|
||||||
* - Otherwise an ID is returned
|
|
||||||
*/
|
|
||||||
readID () {
|
|
||||||
let user = this.readVarUint()
|
|
||||||
if (user === RootFakeUserID) {
|
|
||||||
// read property name and type id
|
|
||||||
const rid = new RootID(this.readVarString(), null)
|
|
||||||
rid.type = this.readVarUint()
|
|
||||||
return rid
|
|
||||||
}
|
|
||||||
return new ID(user, this.readVarUint())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import utf8 from 'utf-8'
|
|
||||||
import { RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
|
|
||||||
const bits7 = 0b1111111
|
|
||||||
const bits8 = 0b11111111
|
|
||||||
|
|
||||||
export default class BinaryEncoder {
|
|
||||||
constructor () {
|
|
||||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
|
||||||
this.data = []
|
|
||||||
}
|
|
||||||
|
|
||||||
get length () {
|
|
||||||
return this.data.length
|
|
||||||
}
|
|
||||||
|
|
||||||
get pos () {
|
|
||||||
return this.data.length
|
|
||||||
}
|
|
||||||
|
|
||||||
createBuffer () {
|
|
||||||
return Uint8Array.from(this.data).buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
writeUint8 (num) {
|
|
||||||
this.data.push(num & bits8)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUint8 (pos, num) {
|
|
||||||
this.data[pos] = num & bits8
|
|
||||||
}
|
|
||||||
|
|
||||||
writeUint16 (num) {
|
|
||||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUint16 (pos, num) {
|
|
||||||
this.data[pos] = num & bits8
|
|
||||||
this.data[pos + 1] = (num >>> 8) & bits8
|
|
||||||
}
|
|
||||||
|
|
||||||
writeUint32 (num) {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.data.push(num & bits8)
|
|
||||||
num >>>= 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setUint32 (pos, num) {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.data[pos + i] = num & bits8
|
|
||||||
num >>>= 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeVarUint (num) {
|
|
||||||
while (num >= 0b10000000) {
|
|
||||||
this.data.push(0b10000000 | (bits7 & num))
|
|
||||||
num >>>= 7
|
|
||||||
}
|
|
||||||
this.data.push(bits7 & num)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeVarString (str) {
|
|
||||||
let bytes = utf8.setBytesFromString(str)
|
|
||||||
let len = bytes.length
|
|
||||||
this.writeVarUint(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
this.data.push(bytes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeID (id) {
|
|
||||||
const user = id.user
|
|
||||||
this.writeVarUint(user)
|
|
||||||
if (user !== RootFakeUserID) {
|
|
||||||
this.writeVarUint(id.clock)
|
|
||||||
} else {
|
|
||||||
this.writeVarString(id.name)
|
|
||||||
this.writeVarUint(id.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
668
src/Connector.js
668
src/Connector.js
@@ -1,294 +1,424 @@
|
|||||||
import BinaryEncoder from './Binary/Encoder.js'
|
/* @flow */
|
||||||
import BinaryDecoder from './Binary/Decoder.js'
|
'use strict'
|
||||||
|
|
||||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
module.exports = function (Y/* :any */) {
|
||||||
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
class AbstractConnector {
|
||||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
/* ::
|
||||||
|
y: YConfig;
|
||||||
import debug from 'debug'
|
role: SyncRole;
|
||||||
|
connections: Object;
|
||||||
export default class AbstractConnector {
|
isSynced: boolean;
|
||||||
constructor (y, opts) {
|
userEventListeners: Array<Function>;
|
||||||
this.y = y
|
whenSyncedListeners: Array<Function>;
|
||||||
this.opts = opts
|
currentSyncTarget: ?UserId;
|
||||||
if (opts.role == null || opts.role === 'master') {
|
syncingClients: Array<UserId>;
|
||||||
this.role = 'master'
|
forwardToSyncingClients: boolean;
|
||||||
} else if (opts.role === 'slave') {
|
debug: boolean;
|
||||||
this.role = 'slave'
|
broadcastedHB: boolean;
|
||||||
} else {
|
syncStep2: Promise;
|
||||||
throw new Error("Role must be either 'master' or 'slave'!")
|
userId: UserId;
|
||||||
|
send: Function;
|
||||||
|
broadcast: Function;
|
||||||
|
broadcastOpBuffer: Array<Operation>;
|
||||||
|
protocolVersion: number;
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
opts contains the following information:
|
||||||
|
role : String Role of this client ("master" or "slave")
|
||||||
|
userId : String Uniquely defines the user.
|
||||||
|
debug: Boolean Whether to print debug messages (optional)
|
||||||
|
*/
|
||||||
|
constructor (y, opts) {
|
||||||
|
this.y = y
|
||||||
|
if (opts == null) {
|
||||||
|
opts = {}
|
||||||
|
}
|
||||||
|
if (opts.role == null || opts.role === 'master') {
|
||||||
|
this.role = 'master'
|
||||||
|
} else if (opts.role === 'slave') {
|
||||||
|
this.role = 'slave'
|
||||||
|
} else {
|
||||||
|
throw new Error("Role must be either 'master' or 'slave'!")
|
||||||
|
}
|
||||||
|
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
|
||||||
|
this.role = opts.role
|
||||||
|
this.connections = {}
|
||||||
|
this.isSynced = false
|
||||||
|
this.userEventListeners = []
|
||||||
|
this.whenSyncedListeners = []
|
||||||
|
this.currentSyncTarget = null
|
||||||
|
this.syncingClients = []
|
||||||
|
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
||||||
|
this.debug = opts.debug === true
|
||||||
|
this.broadcastedHB = false
|
||||||
|
this.syncStep2 = Promise.resolve()
|
||||||
|
this.broadcastOpBuffer = []
|
||||||
|
this.protocolVersion = 11
|
||||||
}
|
}
|
||||||
this.log = debug('y:connector')
|
reconnect () {
|
||||||
this.logMessage = debug('y:connector-message')
|
|
||||||
this._forwardAppliedStructs = opts.forwardAppliedOperations || false // TODO: rename
|
|
||||||
this.role = opts.role
|
|
||||||
this.connections = new Map()
|
|
||||||
this.isSynced = false
|
|
||||||
this.userEventListeners = []
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.debug = opts.debug === true
|
|
||||||
this.broadcastBuffer = new BinaryEncoder()
|
|
||||||
this.broadcastBufferSize = 0
|
|
||||||
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.maxBufferLength == null) {
|
|
||||||
this.maxBufferLength = -1
|
|
||||||
} else {
|
|
||||||
this.maxBufferLength = opts.maxBufferLength
|
|
||||||
}
|
}
|
||||||
}
|
disconnect () {
|
||||||
|
this.connections = {}
|
||||||
reconnect () {
|
this.isSynced = false
|
||||||
this.log('reconnecting..')
|
this.currentSyncTarget = null
|
||||||
}
|
this.broadcastedHB = false
|
||||||
|
this.syncingClients = []
|
||||||
disconnect () {
|
this.whenSyncedListeners = []
|
||||||
this.log('discronnecting..')
|
return this.y.db.stopGarbageCollector()
|
||||||
this.connections = new Map()
|
}
|
||||||
this.isSynced = false
|
repair () {
|
||||||
this.currentSyncTarget = null
|
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.whenSyncedListeners = []
|
for (var name in this.connections) {
|
||||||
return Promise.resolve()
|
this.connections[name].isSynced = false
|
||||||
}
|
}
|
||||||
|
this.isSynced = false
|
||||||
onUserEvent (f) {
|
this.currentSyncTarget = null
|
||||||
this.userEventListeners.push(f)
|
this.broadcastedHB = false
|
||||||
}
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
removeUserEventListener (f) {
|
setUserId (userId) {
|
||||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
if (this.userId == null) {
|
||||||
}
|
this.userId = userId
|
||||||
|
return this.y.db.setUserId(userId)
|
||||||
userLeft (user) {
|
} else {
|
||||||
if (this.connections.has(user)) {
|
return null
|
||||||
this.log('%s: User left %s', this.y.userID, user)
|
}
|
||||||
this.connections.delete(user)
|
}
|
||||||
// check if isSynced event can be sent now
|
onUserEvent (f) {
|
||||||
this._setSyncedWith(null)
|
this.userEventListeners.push(f)
|
||||||
|
}
|
||||||
|
userLeft (user) {
|
||||||
|
if (this.connections[user] != null) {
|
||||||
|
delete this.connections[user]
|
||||||
|
if (user === this.currentSyncTarget) {
|
||||||
|
this.currentSyncTarget = null
|
||||||
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
|
this.syncingClients = this.syncingClients.filter(function (cli) {
|
||||||
|
return cli !== user
|
||||||
|
})
|
||||||
|
for (var f of this.userEventListeners) {
|
||||||
|
f({
|
||||||
|
action: 'userLeft',
|
||||||
|
user: user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userJoined (user, role) {
|
||||||
|
if (role == null) {
|
||||||
|
throw new Error('You must specify the role of the joined user!')
|
||||||
|
}
|
||||||
|
if (this.connections[user] != null) {
|
||||||
|
throw new Error('This user already joined!')
|
||||||
|
}
|
||||||
|
this.connections[user] = {
|
||||||
|
isSynced: false,
|
||||||
|
role: role
|
||||||
|
}
|
||||||
for (var f of this.userEventListeners) {
|
for (var f of this.userEventListeners) {
|
||||||
f({
|
f({
|
||||||
action: 'userLeft',
|
action: 'userJoined',
|
||||||
user: user
|
user: user,
|
||||||
|
role: role
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.currentSyncTarget == null) {
|
||||||
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Execute a function _when_ we are connected.
|
||||||
|
// If not connected, wait until connected
|
||||||
|
whenSynced (f) {
|
||||||
|
if (this.isSynced) {
|
||||||
|
f()
|
||||||
|
} else {
|
||||||
|
this.whenSyncedListeners.push(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
|
||||||
|
returns false, if there is no sync target
|
||||||
|
true otherwise
|
||||||
|
*/
|
||||||
|
findNextSyncTarget () {
|
||||||
|
if (this.currentSyncTarget != null || this.isSynced) {
|
||||||
|
return // "The current sync has not finished!"
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncUser = null
|
||||||
|
for (var uid in this.connections) {
|
||||||
|
if (!this.connections[uid].isSynced) {
|
||||||
|
syncUser = uid
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var conn = this
|
||||||
|
if (syncUser != null) {
|
||||||
|
this.currentSyncTarget = syncUser
|
||||||
|
this.y.db.requestTransaction(function *() {
|
||||||
|
var stateSet = yield* this.getStateSet()
|
||||||
|
var deleteSet = yield* this.getDeleteSet()
|
||||||
|
conn.send(syncUser, {
|
||||||
|
type: 'sync step 1',
|
||||||
|
stateSet: stateSet,
|
||||||
|
deleteSet: deleteSet,
|
||||||
|
protocolVersion: conn.protocolVersion
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.y.db.requestTransaction(function *() {
|
||||||
|
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||||
|
conn.isSynced = true
|
||||||
|
yield* this.garbageCollectAfterSync()
|
||||||
|
// call whensynced listeners
|
||||||
|
for (var f of conn.whenSyncedListeners) {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
conn.whenSyncedListeners = []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
send (uid, message) {
|
||||||
|
if (this.debug) {
|
||||||
userJoined (user, role, auth) {
|
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, message) // eslint-disable-line
|
||||||
if (role == null) {
|
|
||||||
throw new Error('You must specify the role of the joined user!')
|
|
||||||
}
|
|
||||||
if (this.connections.has(user)) {
|
|
||||||
throw new Error('This user already joined!')
|
|
||||||
}
|
|
||||||
this.log('%s: User joined %s', this.y.userID, user)
|
|
||||||
this.connections.set(user, {
|
|
||||||
uid: user,
|
|
||||||
isSynced: false,
|
|
||||||
role: role,
|
|
||||||
processAfterAuth: [],
|
|
||||||
processAfterSync: [],
|
|
||||||
auth: auth || null,
|
|
||||||
receivedSyncStep2: false
|
|
||||||
})
|
|
||||||
let defer = {}
|
|
||||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
|
||||||
this.connections.get(user).syncStep2 = defer
|
|
||||||
for (var f of this.userEventListeners) {
|
|
||||||
f({
|
|
||||||
action: 'userJoined',
|
|
||||||
user: user,
|
|
||||||
role: role
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this._syncWithUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute a function _when_ we are connected.
|
|
||||||
// If not connected, wait until connected
|
|
||||||
whenSynced (f) {
|
|
||||||
if (this.isSynced) {
|
|
||||||
f()
|
|
||||||
} else {
|
|
||||||
this.whenSyncedListeners.push(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_syncWithUser (userID) {
|
|
||||||
if (this.role === 'slave') {
|
|
||||||
return // "The current sync has not finished or this is controlled by a master!"
|
|
||||||
}
|
|
||||||
sendSyncStep1(this, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
_fireIsSyncedListeners () {
|
|
||||||
if (!this.isSynced) {
|
|
||||||
this.isSynced = true
|
|
||||||
// It is safer to remove this!
|
|
||||||
// call whensynced listeners
|
|
||||||
for (var f of this.whenSyncedListeners) {
|
|
||||||
f()
|
|
||||||
}
|
}
|
||||||
this.whenSyncedListeners = []
|
|
||||||
this.y.emit('synced')
|
|
||||||
}
|
}
|
||||||
}
|
/*
|
||||||
|
Buffer operations, and broadcast them when ready.
|
||||||
send (uid, buffer) {
|
*/
|
||||||
const y = this.y
|
broadcastOps (ops) {
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
ops = ops.map(function (op) {
|
||||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
return Y.Struct[op.struct].encode(op)
|
||||||
}
|
|
||||||
this.log('User%s to User%s: Send \'%y\'', y.userID, uid, buffer)
|
|
||||||
this.logMessage('User%s to User%s: Send %Y', y.userID, uid, [y, buffer])
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast (buffer) {
|
|
||||||
const y = this.y
|
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
|
||||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
|
||||||
}
|
|
||||||
this.log('User%s: Broadcast \'%y\'', y.userID, buffer)
|
|
||||||
this.logMessage('User%s: Broadcast: %Y', y.userID, [y, buffer])
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Buffer operations, and broadcast them when ready.
|
|
||||||
*/
|
|
||||||
broadcastStruct (struct) {
|
|
||||||
const firstContent = this.broadcastBuffer.length === 0
|
|
||||||
if (firstContent) {
|
|
||||||
this.broadcastBuffer.writeVarString(this.y.room)
|
|
||||||
this.broadcastBuffer.writeVarString('update')
|
|
||||||
this.broadcastBufferSize = 0
|
|
||||||
this.broadcastBufferSizePos = this.broadcastBuffer.pos
|
|
||||||
this.broadcastBuffer.writeUint32(0)
|
|
||||||
}
|
|
||||||
this.broadcastBufferSize++
|
|
||||||
struct._toBinary(this.broadcastBuffer)
|
|
||||||
if (this.maxBufferLength > 0 && this.broadcastBuffer.length > this.maxBufferLength) {
|
|
||||||
// it is necessary to send the buffer now
|
|
||||||
// cache the buffer and check if server is responsive
|
|
||||||
const buffer = this.broadcastBuffer
|
|
||||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
|
||||||
this.broadcastBuffer = new BinaryEncoder()
|
|
||||||
this.whenRemoteResponsive().then(() => {
|
|
||||||
this.broadcast(buffer.createBuffer())
|
|
||||||
})
|
})
|
||||||
} else if (firstContent) {
|
var self = this
|
||||||
// send the buffer when all transactions are finished
|
function broadcastOperations () {
|
||||||
// (or buffer exceeds maxBufferLength)
|
if (self.broadcastOpBuffer.length > 0) {
|
||||||
setTimeout(() => {
|
self.broadcast({
|
||||||
if (this.broadcastBuffer.length > 0) {
|
type: 'update',
|
||||||
const buffer = this.broadcastBuffer
|
ops: self.broadcastOpBuffer
|
||||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
})
|
||||||
this.broadcast(buffer.createBuffer())
|
self.broadcastOpBuffer = []
|
||||||
this.broadcastBuffer = new BinaryEncoder()
|
|
||||||
}
|
}
|
||||||
}, 0)
|
}
|
||||||
|
if (this.broadcastOpBuffer.length === 0) {
|
||||||
|
this.broadcastOpBuffer = ops
|
||||||
|
if (this.y.db.transactionInProgress) {
|
||||||
|
this.y.db.whenTransactionsFinished().then(broadcastOperations)
|
||||||
|
} else {
|
||||||
|
setTimeout(broadcastOperations, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
/*
|
||||||
|
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||||
|
*/
|
||||||
|
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
||||||
|
if (sender === this.userId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.debug) {
|
||||||
|
console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line
|
||||||
|
}
|
||||||
|
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
||||||
|
console.error(
|
||||||
|
`You tried to sync with a yjs instance that has a different protocol version
|
||||||
|
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
||||||
|
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
||||||
|
`)
|
||||||
|
this.send(sender, {
|
||||||
|
type: 'sync stop',
|
||||||
|
protocolVersion: this.protocolVersion
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (message.type === 'sync step 1') {
|
||||||
|
let conn = this
|
||||||
|
let m = message
|
||||||
|
this.y.db.requestTransaction(function *() {
|
||||||
|
var currentStateSet = yield* this.getStateSet()
|
||||||
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
|
|
||||||
/*
|
var ds = yield* this.getDeleteSet()
|
||||||
* Somehow check the responsiveness of the remote clients/server
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
* Default behavior:
|
conn.send(sender, {
|
||||||
* Wait 100ms before broadcasting the next batch of operations
|
type: 'sync step 2',
|
||||||
*
|
os: ops,
|
||||||
* Only used when maxBufferLength is set
|
stateSet: currentStateSet,
|
||||||
*
|
deleteSet: ds,
|
||||||
*/
|
protocolVersion: this.protocolVersion
|
||||||
whenRemoteResponsive () {
|
})
|
||||||
return new Promise(function (resolve) {
|
if (this.forwardToSyncingClients) {
|
||||||
setTimeout(resolve, 100)
|
conn.syncingClients.push(sender)
|
||||||
})
|
setTimeout(function () {
|
||||||
}
|
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
||||||
|
return cli !== sender
|
||||||
/*
|
})
|
||||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
conn.send(sender, {
|
||||||
*/
|
type: 'sync done'
|
||||||
receiveMessage (sender, buffer, skipAuth) {
|
})
|
||||||
const y = this.y
|
}, 5000) // TODO: conn.syncingClientDuration)
|
||||||
const userID = y.userID
|
} else {
|
||||||
skipAuth = skipAuth || false
|
conn.send(sender, {
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
type: 'sync done'
|
||||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
|
||||||
}
|
|
||||||
if (sender === userID) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
let roomname = decoder.readVarString() // read room name
|
|
||||||
encoder.writeVarString(roomname)
|
|
||||||
let messageType = decoder.readVarString()
|
|
||||||
let senderConn = this.connections.get(sender)
|
|
||||||
this.log('User%s from User%s: Receive \'%s\'', userID, sender, messageType)
|
|
||||||
this.logMessage('User%s from User%s: Receive %Y', userID, sender, [y, buffer])
|
|
||||||
if (senderConn == null && !skipAuth) {
|
|
||||||
throw new Error('Received message from unknown peer!')
|
|
||||||
}
|
|
||||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
|
||||||
let auth = decoder.readVarUint()
|
|
||||||
if (senderConn.auth == null) {
|
|
||||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
|
||||||
// check auth
|
|
||||||
return this.checkAuth(auth, y, sender).then(authPermissions => {
|
|
||||||
if (senderConn.auth == null) {
|
|
||||||
senderConn.auth = authPermissions
|
|
||||||
y.emit('userAuthenticated', {
|
|
||||||
user: senderConn.uid,
|
|
||||||
auth: authPermissions
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let messages = senderConn.processAfterAuth
|
conn._setSyncedWith(sender)
|
||||||
senderConn.processAfterAuth = []
|
|
||||||
|
|
||||||
messages.forEach(m =>
|
|
||||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
} else if (message.type === 'sync step 2') {
|
||||||
|
let conn = this
|
||||||
|
var broadcastHB = !this.broadcastedHB
|
||||||
|
this.broadcastedHB = true
|
||||||
|
var db = this.y.db
|
||||||
|
var defer = {}
|
||||||
|
defer.promise = new Promise(function (resolve) {
|
||||||
|
defer.resolve = resolve
|
||||||
|
})
|
||||||
|
this.syncStep2 = defer.promise
|
||||||
|
let m /* :MessageSyncStep2 */ = message
|
||||||
|
db.requestTransaction(function * () {
|
||||||
|
yield* this.applyDeleteSet(m.deleteSet)
|
||||||
|
this.store.apply(m.os)
|
||||||
|
db.requestTransaction(function * () {
|
||||||
|
var ops = yield* this.getOperations(m.stateSet)
|
||||||
|
if (ops.length > 0) {
|
||||||
|
if (!broadcastHB) { // TODO: consider to broadcast here..
|
||||||
|
conn.send(sender, {
|
||||||
|
type: 'update',
|
||||||
|
ops: ops
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// broadcast only once!
|
||||||
|
conn.broadcastOps(ops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer.resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (message.type === 'sync done') {
|
||||||
|
var self = this
|
||||||
|
this.syncStep2.then(function () {
|
||||||
|
self._setSyncedWith(sender)
|
||||||
|
})
|
||||||
|
} else if (message.type === 'update') {
|
||||||
|
if (this.forwardToSyncingClients) {
|
||||||
|
for (var client of this.syncingClients) {
|
||||||
|
this.send(client, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.y.db.forwardAppliedOperations) {
|
||||||
|
var delops = message.ops.filter(function (o) {
|
||||||
|
return o.struct === 'Delete'
|
||||||
|
})
|
||||||
|
if (delops.length > 0) {
|
||||||
|
this.broadcastOps(delops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.y.db.apply(message.ops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((skipAuth || senderConn.auth != null) && (messageType !== 'update' || senderConn.isSynced)) {
|
_setSyncedWith (user) {
|
||||||
this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
var conn = this.connections[user]
|
||||||
} else {
|
if (conn != null) {
|
||||||
senderConn.processAfterSync.push([messageType, senderConn, decoder, encoder, sender, false])
|
conn.isSynced = true
|
||||||
|
}
|
||||||
|
if (user === this.currentSyncTarget) {
|
||||||
|
this.currentSyncTarget = null
|
||||||
|
this.findNextSyncTarget()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
/*
|
||||||
|
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||||
|
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||||
|
too much overhead. Y is very likely to get changed a lot in the future
|
||||||
|
|
||||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
|
||||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
we encode the JSON as XML.
|
||||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
|
||||||
readSyncStep1(decoder, encoder, this.y, senderConn, sender)
|
When the HB support encoding as XML, the format should look pretty much like this.
|
||||||
} else {
|
|
||||||
const y = this.y
|
does not support primitive values as array elements
|
||||||
y.transact(function () {
|
expects an ltx (less than xml) object
|
||||||
if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
*/
|
||||||
readSyncStep2(decoder, encoder, y, senderConn, sender)
|
parseMessageFromXml (m/* :any */) {
|
||||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
function parseArray (node) {
|
||||||
integrateRemoteStructs(decoder, encoder, y, senderConn, sender)
|
for (var n of node.children) {
|
||||||
} else {
|
if (n.getAttribute('isArray') === 'true') {
|
||||||
throw new Error('Unable to receive message')
|
return parseArray(n)
|
||||||
|
} else {
|
||||||
|
return parseObject(n)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, true)
|
}
|
||||||
}
|
function parseObject (node/* :any */) {
|
||||||
}
|
var json = {}
|
||||||
|
for (var attrName in node.attrs) {
|
||||||
_setSyncedWith (user) {
|
var value = node.attrs[attrName]
|
||||||
if (user != null) {
|
var int = parseInt(value, 10)
|
||||||
const userConn = this.connections.get(user)
|
if (isNaN(int) || ('' + int) !== value) {
|
||||||
userConn.isSynced = true
|
json[attrName] = value
|
||||||
const messages = userConn.processAfterSync
|
} else {
|
||||||
userConn.processAfterSync = []
|
json[attrName] = int
|
||||||
messages.forEach(m => {
|
}
|
||||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
}
|
||||||
})
|
for (var n/* :any */ in node.children) {
|
||||||
}
|
var name = n.name
|
||||||
const conns = Array.from(this.connections.values())
|
if (n.getAttribute('isArray') === 'true') {
|
||||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
json[name] = parseArray(n)
|
||||||
this._fireIsSyncedListeners()
|
} else {
|
||||||
|
json[name] = parseObject(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
parseObject(m)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
encode message in xml
|
||||||
|
we use string because Strophe only accepts an "xml-string"..
|
||||||
|
So {a:4,b:{c:5}} will look like
|
||||||
|
<y a="4">
|
||||||
|
<b c="5"></b>
|
||||||
|
</y>
|
||||||
|
m - ltx element
|
||||||
|
json - Object
|
||||||
|
*/
|
||||||
|
encodeMessageToXml (msg, obj) {
|
||||||
|
// attributes is optional
|
||||||
|
function encodeObject (m, json) {
|
||||||
|
for (var name in json) {
|
||||||
|
var value = json[name]
|
||||||
|
if (name == null) {
|
||||||
|
// nop
|
||||||
|
} else if (value.constructor === Object) {
|
||||||
|
encodeObject(m.c(name), value)
|
||||||
|
} else if (value.constructor === Array) {
|
||||||
|
encodeArray(m.c(name), value)
|
||||||
|
} else {
|
||||||
|
m.setAttribute(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function encodeArray (m, array) {
|
||||||
|
m.setAttribute('isArray', 'true')
|
||||||
|
for (var e of array) {
|
||||||
|
if (e.constructor === Object) {
|
||||||
|
encodeObject(m.c('array-element'), e)
|
||||||
|
} else {
|
||||||
|
encodeArray(m.c('array-element'), e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.constructor === Object) {
|
||||||
|
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||||
|
} else if (obj.constructor === Array) {
|
||||||
|
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||||
|
} else {
|
||||||
|
throw new Error("I can't encode this json!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Y.AbstractConnector = AbstractConnector
|
||||||
}
|
}
|
||||||
|
|||||||
165
src/Connectors/Test.js
Normal file
165
src/Connectors/Test.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/* global getRandom, async */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = function (Y) {
|
||||||
|
var globalRoom = {
|
||||||
|
users: {},
|
||||||
|
buffers: {},
|
||||||
|
removeUser: function (user) {
|
||||||
|
for (var i in this.users) {
|
||||||
|
this.users[i].userLeft(user)
|
||||||
|
}
|
||||||
|
delete this.users[user]
|
||||||
|
delete this.buffers[user]
|
||||||
|
},
|
||||||
|
addUser: function (connector) {
|
||||||
|
this.users[connector.userId] = connector
|
||||||
|
this.buffers[connector.userId] = {}
|
||||||
|
for (var uname in this.users) {
|
||||||
|
if (uname !== connector.userId) {
|
||||||
|
var u = this.users[uname]
|
||||||
|
u.userJoined(connector.userId, 'master')
|
||||||
|
connector.userJoined(u.userId, 'master')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
whenTransactionsFinished: function () {
|
||||||
|
var ps = []
|
||||||
|
for (var name in this.users) {
|
||||||
|
ps.push(this.users[name].y.db.whenTransactionsFinished())
|
||||||
|
}
|
||||||
|
return Promise.all(ps)
|
||||||
|
},
|
||||||
|
flushOne: function flushOne () {
|
||||||
|
var bufs = []
|
||||||
|
for (var receiver in globalRoom.buffers) {
|
||||||
|
let buff = globalRoom.buffers[receiver]
|
||||||
|
var push = false
|
||||||
|
for (let sender in buff) {
|
||||||
|
if (buff[sender].length > 0) {
|
||||||
|
push = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (push) {
|
||||||
|
bufs.push(receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bufs.length > 0) {
|
||||||
|
var userId = getRandom(bufs)
|
||||||
|
let buff = globalRoom.buffers[userId]
|
||||||
|
let sender = getRandom(Object.keys(buff))
|
||||||
|
var m = buff[sender].shift()
|
||||||
|
if (buff[sender].length === 0) {
|
||||||
|
delete buff[sender]
|
||||||
|
}
|
||||||
|
var user = globalRoom.users[userId]
|
||||||
|
user.receiveMessage(m[0], m[1])
|
||||||
|
return user.y.db.whenTransactionsFinished()
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flushAll: function () {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
// flushes may result in more created operations,
|
||||||
|
// flush until there is nothing more to flush
|
||||||
|
function nextFlush () {
|
||||||
|
var c = globalRoom.flushOne()
|
||||||
|
if (c) {
|
||||||
|
while (c) {
|
||||||
|
c = globalRoom.flushOne()
|
||||||
|
}
|
||||||
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
|
} else {
|
||||||
|
setTimeout(function () {
|
||||||
|
var c = globalRoom.flushOne()
|
||||||
|
if (c) {
|
||||||
|
c.then(function () {
|
||||||
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.globalRoom = globalRoom
|
||||||
|
|
||||||
|
var userIdCounter = 0
|
||||||
|
|
||||||
|
class Test extends Y.AbstractConnector {
|
||||||
|
constructor (y, options) {
|
||||||
|
if (options === undefined) {
|
||||||
|
throw new Error('Options must not be undefined!')
|
||||||
|
}
|
||||||
|
options.role = 'master'
|
||||||
|
options.forwardToSyncingClients = false
|
||||||
|
super(y, options)
|
||||||
|
this.setUserId((userIdCounter++) + '').then(() => {
|
||||||
|
globalRoom.addUser(this)
|
||||||
|
})
|
||||||
|
this.globalRoom = globalRoom
|
||||||
|
this.syncingClientDuration = 0
|
||||||
|
}
|
||||||
|
receiveMessage (sender, m) {
|
||||||
|
super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
||||||
|
}
|
||||||
|
send (userId, message) {
|
||||||
|
var buffer = globalRoom.buffers[userId]
|
||||||
|
if (buffer != null) {
|
||||||
|
if (buffer[this.userId] == null) {
|
||||||
|
buffer[this.userId] = []
|
||||||
|
}
|
||||||
|
buffer[this.userId].push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
broadcast (message) {
|
||||||
|
for (var key in globalRoom.buffers) {
|
||||||
|
var buff = globalRoom.buffers[key]
|
||||||
|
if (buff[this.userId] == null) {
|
||||||
|
buff[this.userId] = []
|
||||||
|
}
|
||||||
|
buff[this.userId].push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDisconnected () {
|
||||||
|
return globalRoom.users[this.userId] == null
|
||||||
|
}
|
||||||
|
reconnect () {
|
||||||
|
if (this.isDisconnected()) {
|
||||||
|
globalRoom.addUser(this)
|
||||||
|
super.reconnect()
|
||||||
|
}
|
||||||
|
return Y.utils.globalRoom.flushAll()
|
||||||
|
}
|
||||||
|
disconnect () {
|
||||||
|
if (!this.isDisconnected()) {
|
||||||
|
globalRoom.removeUser(this.userId)
|
||||||
|
super.disconnect()
|
||||||
|
}
|
||||||
|
return this.y.db.whenTransactionsFinished()
|
||||||
|
}
|
||||||
|
flush () {
|
||||||
|
var self = this
|
||||||
|
return async(function * () {
|
||||||
|
var buff = globalRoom.buffers[self.userId]
|
||||||
|
while (Object.keys(buff).length > 0) {
|
||||||
|
var sender = getRandom(Object.keys(buff))
|
||||||
|
var m = buff[sender].shift()
|
||||||
|
if (buff[sender].length === 0) {
|
||||||
|
delete buff[sender]
|
||||||
|
}
|
||||||
|
this.receiveMessage(m[0], m[1])
|
||||||
|
}
|
||||||
|
yield self.whenTransactionsFinished()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Y.Test = Test
|
||||||
|
}
|
||||||
548
src/Database.js
Normal file
548
src/Database.js
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
/* @flow */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = function (Y /* :any */) {
|
||||||
|
/*
|
||||||
|
Partial definition of an OperationStore.
|
||||||
|
TODO: name it Database, operation store only holds operations.
|
||||||
|
|
||||||
|
A database definition must alse define the following methods:
|
||||||
|
* logTable() (optional)
|
||||||
|
- show relevant information information in a table
|
||||||
|
* requestTransaction(makeGen)
|
||||||
|
- request a transaction
|
||||||
|
* destroy()
|
||||||
|
- destroy the database
|
||||||
|
*/
|
||||||
|
class AbstractDatabase {
|
||||||
|
/* ::
|
||||||
|
y: YConfig;
|
||||||
|
forwardAppliedOperations: boolean;
|
||||||
|
listenersById: Object;
|
||||||
|
listenersByIdExecuteNow: Array<Object>;
|
||||||
|
listenersByIdRequestPending: boolean;
|
||||||
|
initializedTypes: Object;
|
||||||
|
whenUserIdSetListener: ?Function;
|
||||||
|
waitingTransactions: Array<Transaction>;
|
||||||
|
transactionInProgress: boolean;
|
||||||
|
executeOrder: Array<Object>;
|
||||||
|
gc1: Array<Struct>;
|
||||||
|
gc2: Array<Struct>;
|
||||||
|
gcTimeout: number;
|
||||||
|
gcInterval: any;
|
||||||
|
garbageCollect: Function;
|
||||||
|
executeOrder: Array<any>; // for debugging only
|
||||||
|
userId: UserId;
|
||||||
|
opClock: number;
|
||||||
|
transactionsFinished: ?{promise: Promise, resolve: any};
|
||||||
|
transact: (x: ?Generator) => any;
|
||||||
|
*/
|
||||||
|
constructor (y, opts) {
|
||||||
|
this.y = y
|
||||||
|
var os = this
|
||||||
|
this.userId = null
|
||||||
|
var resolve
|
||||||
|
this.userIdPromise = new Promise(function (r) {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
this.userIdPromise.resolve = resolve
|
||||||
|
// whether to broadcast all applied operations (insert & delete hook)
|
||||||
|
this.forwardAppliedOperations = false
|
||||||
|
// E.g. this.listenersById[id] : Array<Listener>
|
||||||
|
this.listenersById = {}
|
||||||
|
// Execute the next time a transaction is requested
|
||||||
|
this.listenersByIdExecuteNow = []
|
||||||
|
// A transaction is requested
|
||||||
|
this.listenersByIdRequestPending = false
|
||||||
|
/* To make things more clear, the following naming conventions:
|
||||||
|
* ls : we put this.listenersById on ls
|
||||||
|
* l : Array<Listener>
|
||||||
|
* id : Id (can't use as property name)
|
||||||
|
* sid : String (converted from id via JSON.stringify
|
||||||
|
so we can use it as a property name)
|
||||||
|
|
||||||
|
Always remember to first overwrite
|
||||||
|
a property before you iterate over it!
|
||||||
|
*/
|
||||||
|
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
||||||
|
// wont be kept in memory.
|
||||||
|
this.initializedTypes = {}
|
||||||
|
this.waitingTransactions = []
|
||||||
|
this.transactionInProgress = false
|
||||||
|
this.transactionIsFlushed = false
|
||||||
|
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
||||||
|
this.executeOrder = []
|
||||||
|
}
|
||||||
|
this.gc1 = [] // first stage
|
||||||
|
this.gc2 = [] // second stage -> after that, remove the op
|
||||||
|
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeouts
|
||||||
|
function garbageCollect () {
|
||||||
|
return os.whenTransactionsFinished().then(function () {
|
||||||
|
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
||||||
|
if (!os.y.isConnected()) {
|
||||||
|
console.warn('gc should be empty when disconnected!')
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
os.requestTransaction(function * () {
|
||||||
|
if (os.y.connector != null && os.y.connector.isSynced) {
|
||||||
|
for (var i = 0; i < os.gc2.length; i++) {
|
||||||
|
var oid = os.gc2[i]
|
||||||
|
yield* this.garbageCollectOperation(oid)
|
||||||
|
}
|
||||||
|
os.gc2 = os.gc1
|
||||||
|
os.gc1 = []
|
||||||
|
}
|
||||||
|
// TODO: Use setInterval here instead (when garbageCollect is called several times there will be several timeouts..)
|
||||||
|
if (os.gcTimeout > 0) {
|
||||||
|
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// TODO: see above
|
||||||
|
if (os.gcTimeout > 0) {
|
||||||
|
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.garbageCollect = garbageCollect
|
||||||
|
if (this.gcTimeout > 0) {
|
||||||
|
garbageCollect()
|
||||||
|
}
|
||||||
|
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
|
||||||
|
this.opsReceivedTimestamp = new Date()
|
||||||
|
this.startRepairCheck()
|
||||||
|
}
|
||||||
|
startRepairCheck () {
|
||||||
|
var os = this
|
||||||
|
if (this.repairCheckInterval > 0) {
|
||||||
|
this.repairCheckIntervalHandler = setInterval(function repairOnMissingOperations () {
|
||||||
|
/*
|
||||||
|
Case 1. No ops have been received in a while (new Date() - os.opsReceivedTimestamp > os.repairCheckInterval)
|
||||||
|
- 1.1 os.listenersById is empty. Then the state was correct the whole time. -> Nothing to do (nor to update)
|
||||||
|
- 1.2 os.listenersById is not empty.
|
||||||
|
* Then the state was incorrect for at least {os.repairCheckInterval} seconds.
|
||||||
|
* -> Remove everything in os.listenersById and sync again (connector.repair())
|
||||||
|
Case 2. An op has been received in the last {os.repairCheckInterval } seconds.
|
||||||
|
It is not yet necessary to check for faulty behavior. Everything can still resolve itself. Wait for more messages.
|
||||||
|
If nothing was received for a while and os.listenersById is still not emty, we are in case 1.2
|
||||||
|
-> Do nothing
|
||||||
|
|
||||||
|
Baseline here is: we really only have to catch case 1.2..
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
new Date() - os.opsReceivedTimestamp > os.repairCheckInterval &&
|
||||||
|
Object.keys(os.listenersById).length > 0 // os.listenersById is not empty
|
||||||
|
) {
|
||||||
|
// haven't received operations for over {os.repairCheckInterval} seconds, resend state vector
|
||||||
|
os.listenersById = {}
|
||||||
|
os.opsReceivedTimestamp = new Date() // update so you don't send repair several times in a row
|
||||||
|
os.y.connector.repair()
|
||||||
|
}
|
||||||
|
}, this.repairCheckInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopRepairCheck () {
|
||||||
|
clearInterval(this.repairCheckIntervalHandler)
|
||||||
|
}
|
||||||
|
queueGarbageCollector (id) {
|
||||||
|
if (this.y.isConnected()) {
|
||||||
|
this.gc1.push(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emptyGarbageCollector () {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
var check = () => {
|
||||||
|
if (this.gc1.length > 0 || this.gc2.length > 0) {
|
||||||
|
this.garbageCollect().then(check)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(check, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addToDebug () {
|
||||||
|
if (typeof YConcurrency_TestingMode !== 'undefined') {
|
||||||
|
var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
|
||||||
|
if (typeof s === 'string') {
|
||||||
|
return s
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(s)
|
||||||
|
}
|
||||||
|
}).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
|
||||||
|
this.executeOrder.push(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getDebugData () {
|
||||||
|
console.log(this.executeOrder.join('\n'))
|
||||||
|
}
|
||||||
|
stopGarbageCollector () {
|
||||||
|
var self = this
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
self.requestTransaction(function * () {
|
||||||
|
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
|
||||||
|
self.gc1 = []
|
||||||
|
self.gc2 = []
|
||||||
|
for (var i = 0; i < ungc.length; i++) {
|
||||||
|
var op = yield* this.getOperation(ungc[i])
|
||||||
|
if (op != null) {
|
||||||
|
delete op.gc
|
||||||
|
yield* this.setOperation(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Try to add to GC.
|
||||||
|
|
||||||
|
TODO: rename this function
|
||||||
|
|
||||||
|
Rulez:
|
||||||
|
* Only gc if this user is online
|
||||||
|
* The most left element in a list must not be gc'd.
|
||||||
|
=> There is at least one element in the list
|
||||||
|
|
||||||
|
returns true iff op was added to GC
|
||||||
|
*/
|
||||||
|
* addToGarbageCollector (op, left) {
|
||||||
|
if (
|
||||||
|
op.gc == null &&
|
||||||
|
op.deleted === true
|
||||||
|
) {
|
||||||
|
var gc = false
|
||||||
|
if (left != null && left.deleted === true) {
|
||||||
|
gc = true
|
||||||
|
} else if (op.content != null && op.content.length > 1) {
|
||||||
|
op = yield* this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
|
||||||
|
gc = true
|
||||||
|
}
|
||||||
|
if (gc) {
|
||||||
|
op.gc = true
|
||||||
|
yield* this.setOperation(op)
|
||||||
|
this.store.queueGarbageCollector(op.id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
removeFromGarbageCollector (op) {
|
||||||
|
function filter (o) {
|
||||||
|
return !Y.utils.compareIds(o, op.id)
|
||||||
|
}
|
||||||
|
this.gc1 = this.gc1.filter(filter)
|
||||||
|
this.gc2 = this.gc2.filter(filter)
|
||||||
|
delete op.gc
|
||||||
|
}
|
||||||
|
* destroy () {
|
||||||
|
clearInterval(this.gcInterval)
|
||||||
|
this.gcInterval = null
|
||||||
|
this.stopRepairCheck()
|
||||||
|
for (var key in this.initializedTypes) {
|
||||||
|
var type = this.initializedTypes[key]
|
||||||
|
if (type._destroy != null) {
|
||||||
|
type._destroy()
|
||||||
|
} else {
|
||||||
|
console.error('The type you included does not provide destroy functionality, it will remain in memory (updating your packages will help).')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUserId (userId) {
|
||||||
|
if (!this.userIdPromise.inProgress) {
|
||||||
|
this.userIdPromise.inProgress = true
|
||||||
|
var self = this
|
||||||
|
self.requestTransaction(function * () {
|
||||||
|
self.userId = userId
|
||||||
|
var state = yield* this.getState(userId)
|
||||||
|
self.opClock = state.clock
|
||||||
|
self.userIdPromise.resolve(userId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return this.userIdPromise
|
||||||
|
}
|
||||||
|
whenUserIdSet (f) {
|
||||||
|
this.userIdPromise.then(f)
|
||||||
|
}
|
||||||
|
getNextOpId (numberOfIds) {
|
||||||
|
if (numberOfIds == null) {
|
||||||
|
throw new Error('getNextOpId expects the number of created ids to create!')
|
||||||
|
} else if (this.userId == null) {
|
||||||
|
throw new Error('OperationStore not yet initialized!')
|
||||||
|
} else {
|
||||||
|
var id = [this.userId, this.opClock]
|
||||||
|
this.opClock += numberOfIds
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Apply a list of operations.
|
||||||
|
|
||||||
|
* we save a timestamp, because we received new operations that could resolve ops in this.listenersById (see this.startRepairCheck)
|
||||||
|
* get a transaction
|
||||||
|
* check whether all Struct.*.requiredOps are in the OS
|
||||||
|
* check if it is an expected op (otherwise wait for it)
|
||||||
|
* check if was deleted, apply a delete operation after op was applied
|
||||||
|
*/
|
||||||
|
apply (ops) {
|
||||||
|
this.opsReceivedTimestamp = new Date()
|
||||||
|
for (var i = 0; i < ops.length; i++) {
|
||||||
|
var o = ops[i]
|
||||||
|
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
||||||
|
var required = Y.Struct[o.struct].requiredOps(o)
|
||||||
|
if (o.requires != null) {
|
||||||
|
required = required.concat(o.requires)
|
||||||
|
}
|
||||||
|
this.whenOperationsExist(required, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
op is executed as soon as every operation requested is available.
|
||||||
|
Note that Transaction can (and should) buffer requests.
|
||||||
|
*/
|
||||||
|
whenOperationsExist (ids, op) {
|
||||||
|
if (ids.length > 0) {
|
||||||
|
let listener = {
|
||||||
|
op: op,
|
||||||
|
missing: ids.length
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
let id = ids[i]
|
||||||
|
let sid = JSON.stringify(id)
|
||||||
|
let l = this.listenersById[sid]
|
||||||
|
if (l == null) {
|
||||||
|
l = []
|
||||||
|
this.listenersById[sid] = l
|
||||||
|
}
|
||||||
|
l.push(listener)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.listenersByIdExecuteNow.push({
|
||||||
|
op: op
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.listenersByIdRequestPending) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listenersByIdRequestPending = true
|
||||||
|
var store = this
|
||||||
|
|
||||||
|
this.requestTransaction(function * () {
|
||||||
|
var exeNow = store.listenersByIdExecuteNow
|
||||||
|
store.listenersByIdExecuteNow = []
|
||||||
|
|
||||||
|
var ls = store.listenersById
|
||||||
|
store.listenersById = {}
|
||||||
|
|
||||||
|
store.listenersByIdRequestPending = false
|
||||||
|
|
||||||
|
for (let key = 0; key < exeNow.length; key++) {
|
||||||
|
let o = exeNow[key].op
|
||||||
|
yield* store.tryExecute.call(this, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var sid in ls) {
|
||||||
|
var l = ls[sid]
|
||||||
|
var id = JSON.parse(sid)
|
||||||
|
var op
|
||||||
|
if (typeof id[1] === 'string') {
|
||||||
|
op = yield* this.getOperation(id)
|
||||||
|
} else {
|
||||||
|
op = yield* this.getInsertion(id)
|
||||||
|
}
|
||||||
|
if (op == null) {
|
||||||
|
store.listenersById[sid] = l
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < l.length; i++) {
|
||||||
|
let listener = l[i]
|
||||||
|
let o = listener.op
|
||||||
|
if (--listener.missing === 0) {
|
||||||
|
yield* store.tryExecute.call(this, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Actually execute an operation, when all expected operations are available.
|
||||||
|
*/
|
||||||
|
/* :: // TODO: this belongs somehow to transaction
|
||||||
|
store: Object;
|
||||||
|
getOperation: any;
|
||||||
|
isGarbageCollected: any;
|
||||||
|
addOperation: any;
|
||||||
|
whenOperationsExist: any;
|
||||||
|
*/
|
||||||
|
* tryExecute (op) {
|
||||||
|
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||||
|
if (op.struct === 'Delete') {
|
||||||
|
yield* Y.Struct.Delete.execute.call(this, op)
|
||||||
|
// this is now called in Transaction.deleteOperation!
|
||||||
|
// yield* this.store.operationAdded(this, op)
|
||||||
|
} else {
|
||||||
|
// check if this op was defined
|
||||||
|
var defined = yield* this.getInsertion(op.id)
|
||||||
|
while (defined != null && defined.content != null) {
|
||||||
|
// 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) {
|
||||||
|
var overlapSize = defined.content.length - (op.id[1] - defined.id[1])
|
||||||
|
op.content.splice(0, overlapSize)
|
||||||
|
op.id = [op.id[0], op.id[1] + overlapSize]
|
||||||
|
op.left = Y.utils.getLastId(defined)
|
||||||
|
op.origin = op.left
|
||||||
|
defined = yield* this.getOperation(op.id) // getOperation suffices here
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defined == null) {
|
||||||
|
var opid = op.id
|
||||||
|
var isGarbageCollected = yield* this.isGarbageCollected(opid)
|
||||||
|
if (!isGarbageCollected) {
|
||||||
|
// TODO: reduce number of get / put calls for op ..
|
||||||
|
yield* Y.Struct[op.struct].execute.call(this, op)
|
||||||
|
yield* this.addOperation(op)
|
||||||
|
yield* this.store.operationAdded(this, op)
|
||||||
|
// operationAdded can change op..
|
||||||
|
op = yield* this.getOperation(opid)
|
||||||
|
// if insertion, try to combine with left
|
||||||
|
yield* this.tryCombineWithLeft(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Called by a transaction when an operation is added.
|
||||||
|
* This function is especially important for y-indexeddb, where several instances may share a single database.
|
||||||
|
* Every time an operation is created by one instance, it is send to all other instances and operationAdded is called
|
||||||
|
*
|
||||||
|
* If it's not a Delete operation:
|
||||||
|
* * Checks if another operation is executable (listenersById)
|
||||||
|
* * Update state, if possible
|
||||||
|
*
|
||||||
|
* Always:
|
||||||
|
* * Call type
|
||||||
|
*/
|
||||||
|
* operationAdded (transaction, op) {
|
||||||
|
if (op.struct === 'Delete') {
|
||||||
|
var target = yield* transaction.getInsertion(op.target)
|
||||||
|
var type = this.initializedTypes[JSON.stringify(target.parent)]
|
||||||
|
if (type != null) {
|
||||||
|
yield* type._changed(transaction, op)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// increase SS
|
||||||
|
yield* transaction.updateState(op.id[0])
|
||||||
|
var opLen = op.content != null ? op.content.length : 1
|
||||||
|
for (let i = 0; i < opLen; i++) {
|
||||||
|
// notify whenOperation listeners (by id)
|
||||||
|
var sid = JSON.stringify([op.id[0], op.id[1] + i])
|
||||||
|
var l = this.listenersById[sid]
|
||||||
|
delete this.listenersById[sid]
|
||||||
|
if (l != null) {
|
||||||
|
for (var key in l) {
|
||||||
|
var listener = l[key]
|
||||||
|
if (--listener.missing === 0) {
|
||||||
|
this.whenOperationsExist([], listener.op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var t = this.initializedTypes[JSON.stringify(op.parent)]
|
||||||
|
|
||||||
|
// if parent is deleted, mark as gc'd and return
|
||||||
|
if (op.parent != null) {
|
||||||
|
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
|
||||||
|
if (parentIsDeleted) {
|
||||||
|
yield* transaction.deleteList(op.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify parent, if it was instanciated as a custom type
|
||||||
|
if (t != null) {
|
||||||
|
let o = Y.utils.copyOperation(op)
|
||||||
|
yield* t._changed(transaction, o)
|
||||||
|
}
|
||||||
|
if (!op.deleted) {
|
||||||
|
// Delete if DS says this is actually deleted
|
||||||
|
var len = op.content != null ? op.content.length : 1
|
||||||
|
var startId = op.id // You must not use op.id in the following loop, because op will change when deleted
|
||||||
|
// TODO: !! console.log('TODO: change this before commiting')
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
var id = [startId[0], startId[1] + i]
|
||||||
|
var opIsDeleted = yield* transaction.isDeleted(id)
|
||||||
|
if (opIsDeleted) {
|
||||||
|
var delop = {
|
||||||
|
struct: 'Delete',
|
||||||
|
target: id
|
||||||
|
}
|
||||||
|
yield* this.tryExecute.call(transaction, delop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
whenTransactionsFinished () {
|
||||||
|
if (this.transactionInProgress) {
|
||||||
|
if (this.transactionsFinished == null) {
|
||||||
|
var resolve
|
||||||
|
var promise = new Promise(function (r) {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
this.transactionsFinished = {
|
||||||
|
resolve: resolve,
|
||||||
|
promise: promise
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
} else {
|
||||||
|
return this.transactionsFinished.promise
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if there is another transaction request.
|
||||||
|
// * the last transaction is always a flush :)
|
||||||
|
getNextRequest () {
|
||||||
|
if (this.waitingTransactions.length === 0) {
|
||||||
|
if (this.transactionIsFlushed) {
|
||||||
|
this.transactionInProgress = false
|
||||||
|
this.transactionIsFlushed = false
|
||||||
|
if (this.transactionsFinished != null) {
|
||||||
|
this.transactionsFinished.resolve()
|
||||||
|
this.transactionsFinished = null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
this.transactionIsFlushed = true
|
||||||
|
return function * () {
|
||||||
|
yield* this.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.transactionIsFlushed = false
|
||||||
|
return this.waitingTransactions.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestTransaction (makeGen/* :any */, callImmediately) {
|
||||||
|
this.waitingTransactions.push(makeGen)
|
||||||
|
if (!this.transactionInProgress) {
|
||||||
|
this.transactionInProgress = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.transact(this.getNextRequest())
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.AbstractDatabase = AbstractDatabase
|
||||||
|
}
|
||||||
354
src/Database.spec.js
Normal file
354
src/Database.spec.js
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/* global async, databases, describe, beforeEach, afterEach */
|
||||||
|
/* eslint-env browser,jasmine,console */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
var Y = require('./SpecHelper.js')
|
||||||
|
|
||||||
|
for (let database of databases) {
|
||||||
|
describe(`Database (${database})`, function () {
|
||||||
|
var store
|
||||||
|
describe('DeleteStore', function () {
|
||||||
|
describe('Basic', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
store = new Y[database](null, {
|
||||||
|
gcTimeout: -1,
|
||||||
|
namespace: 'testing'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterEach(function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.store.destroy()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('Deleted operation is deleted', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['u1', 10], 1)
|
||||||
|
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['u1', 10], 1)
|
||||||
|
yield* this.markDeleted(['u1', 11], 1)
|
||||||
|
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
||||||
|
expect(yield* this.isDeleted(['u1', 11])).toBeTruthy()
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['0', 3], 1)
|
||||||
|
yield* this.markDeleted(['0', 4], 1)
|
||||||
|
yield* this.markDeleted(['0', 2], 1)
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #1', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['166', 0], 1)
|
||||||
|
yield* this.markDeleted(['166', 2], 1)
|
||||||
|
yield* this.markDeleted(['166', 0], 1)
|
||||||
|
yield* this.markDeleted(['166', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['166', 2], 1)
|
||||||
|
yield* this.markDeleted(['166', 1], 1)
|
||||||
|
yield* this.markDeleted(['166', 3], 1)
|
||||||
|
yield* this.markGarbageCollected(['166', 3], 1)
|
||||||
|
yield* this.markDeleted(['166', 0], 1)
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #2', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['293', 0], 1)
|
||||||
|
yield* this.markDeleted(['291', 2], 1)
|
||||||
|
yield* this.markDeleted(['291', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['293', 0], 1)
|
||||||
|
yield* this.markDeleted(['293', 1], 1)
|
||||||
|
yield* this.markGarbageCollected(['291', 2], 1)
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #3', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['581', 0], 1)
|
||||||
|
yield* this.markDeleted(['581', 1], 1)
|
||||||
|
yield* this.markDeleted(['580', 0], 1)
|
||||||
|
yield* this.markDeleted(['580', 0], 1)
|
||||||
|
yield* this.markGarbageCollected(['581', 0], 1)
|
||||||
|
yield* this.markDeleted(['581', 2], 1)
|
||||||
|
yield* this.markDeleted(['580', 1], 1)
|
||||||
|
yield* this.markDeleted(['580', 2], 1)
|
||||||
|
yield* this.markDeleted(['580', 1], 1)
|
||||||
|
yield* this.markDeleted(['580', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['581', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['581', 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]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #4', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['544', 0], 1)
|
||||||
|
yield* this.markDeleted(['543', 2], 1)
|
||||||
|
yield* this.markDeleted(['544', 0], 1)
|
||||||
|
yield* this.markDeleted(['543', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['544', 0], 1)
|
||||||
|
yield* this.markDeleted(['545', 1], 1)
|
||||||
|
yield* this.markDeleted(['543', 4], 1)
|
||||||
|
yield* this.markDeleted(['543', 3], 1)
|
||||||
|
yield* this.markDeleted(['544', 1], 1)
|
||||||
|
yield* this.markDeleted(['544', 2], 1)
|
||||||
|
yield* this.markDeleted(['544', 1], 1)
|
||||||
|
yield* this.markDeleted(['544', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['543', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['543', 4], 1)
|
||||||
|
yield* this.markGarbageCollected(['544', 2], 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]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #5', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
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]]})
|
||||||
|
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #6', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.applyDeleteSet({'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]]})
|
||||||
|
expect(yield* this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
it('Debug #7', async(function * (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.markDeleted(['9', 2], 1)
|
||||||
|
yield* this.markDeleted(['11', 2], 1)
|
||||||
|
yield* this.markDeleted(['11', 4], 1)
|
||||||
|
yield* this.markDeleted(['11', 1], 1)
|
||||||
|
yield* this.markDeleted(['9', 4], 1)
|
||||||
|
yield* this.markDeleted(['10', 0], 1)
|
||||||
|
yield* this.markGarbageCollected(['11', 2], 1)
|
||||||
|
yield* this.markDeleted(['11', 2], 1)
|
||||||
|
yield* this.markGarbageCollected(['11', 3], 1)
|
||||||
|
yield* this.markDeleted(['11', 3], 1)
|
||||||
|
yield* this.markDeleted(['11', 3], 1)
|
||||||
|
yield* this.markDeleted(['9', 4], 1)
|
||||||
|
yield* this.markDeleted(['10', 0], 1)
|
||||||
|
yield* this.markGarbageCollected(['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]]})
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('OperationStore', function () {
|
||||||
|
describe('Basic Tests', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
store = new Y[database](null, {
|
||||||
|
gcTimeout: -1,
|
||||||
|
namespace: 'testing'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterEach(function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.store.destroy()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('debug #1', function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.put({id: [2]})
|
||||||
|
yield* this.os.put({id: [0]})
|
||||||
|
yield* this.os.delete([2])
|
||||||
|
yield* this.os.put({id: [1]})
|
||||||
|
expect(yield* this.os.find([0])).toBeTruthy()
|
||||||
|
expect(yield* this.os.find([1])).toBeTruthy()
|
||||||
|
expect(yield* this.os.find([2])).toBeFalsy()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('can add&retrieve 5 elements', function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.put({val: 'four', id: [4]})
|
||||||
|
yield* this.os.put({val: 'one', id: [1]})
|
||||||
|
yield* this.os.put({val: 'three', id: [3]})
|
||||||
|
yield* this.os.put({val: 'two', id: [2]})
|
||||||
|
yield* this.os.put({val: 'five', id: [5]})
|
||||||
|
expect((yield* this.os.find([1])).val).toEqual('one')
|
||||||
|
expect((yield* this.os.find([2])).val).toEqual('two')
|
||||||
|
expect((yield* this.os.find([3])).val).toEqual('three')
|
||||||
|
expect((yield* this.os.find([4])).val).toEqual('four')
|
||||||
|
expect((yield* this.os.find([5])).val).toEqual('five')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('5 elements do not exist anymore after deleting them', function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.put({val: 'four', id: [4]})
|
||||||
|
yield* this.os.put({val: 'one', id: [1]})
|
||||||
|
yield* this.os.put({val: 'three', id: [3]})
|
||||||
|
yield* this.os.put({val: 'two', id: [2]})
|
||||||
|
yield* this.os.put({val: 'five', id: [5]})
|
||||||
|
yield* this.os.delete([4])
|
||||||
|
expect(yield* this.os.find([4])).not.toBeTruthy()
|
||||||
|
yield* this.os.delete([3])
|
||||||
|
expect(yield* this.os.find([3])).not.toBeTruthy()
|
||||||
|
yield* this.os.delete([2])
|
||||||
|
expect(yield* this.os.find([2])).not.toBeTruthy()
|
||||||
|
yield* this.os.delete([1])
|
||||||
|
expect(yield* this.os.find([1])).not.toBeTruthy()
|
||||||
|
yield* this.os.delete([5])
|
||||||
|
expect(yield* this.os.find([5])).not.toBeTruthy()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
var numberOfOSTests = 1000
|
||||||
|
describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () {
|
||||||
|
var elements = []
|
||||||
|
beforeAll(function (done) {
|
||||||
|
store = new Y[database](null, {
|
||||||
|
gcTimeout: -1,
|
||||||
|
namespace: 'testing'
|
||||||
|
})
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
for (var i = 0; i < numberOfOSTests; i++) {
|
||||||
|
var r = Math.random()
|
||||||
|
if (r < 0.8) {
|
||||||
|
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
|
||||||
|
if (!(yield* this.os.find(obj))) {
|
||||||
|
elements.push(obj)
|
||||||
|
yield* this.os.put({id: obj})
|
||||||
|
}
|
||||||
|
} else if (elements.length > 0) {
|
||||||
|
var elemid = Math.floor(Math.random() * elements.length)
|
||||||
|
var elem = elements[elemid]
|
||||||
|
elements = elements.filter(function (e) {
|
||||||
|
return !Y.utils.compareIds(e, elem)
|
||||||
|
})
|
||||||
|
yield* this.os.delete(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
afterAll(function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.store.destroy()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('can find every object', function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
for (var id of elements) {
|
||||||
|
expect((yield* this.os.find(id)).id).toEqual(id)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can find every object with lower bound search', function (done) {
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
for (var id of elements) {
|
||||||
|
var e = yield* this.os.findWithLowerBound(id)
|
||||||
|
expect(e.id).toEqual(id)
|
||||||
|
}
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('iterating over a tree with lower bound yields the right amount of results', function (done) {
|
||||||
|
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
|
||||||
|
var expectedResults = elements.filter(function (e, pos) {
|
||||||
|
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos
|
||||||
|
}).length
|
||||||
|
|
||||||
|
var actualResults = 0
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.iterate(this, lowerBound, null, function * (val) {
|
||||||
|
expect(val).toBeDefined()
|
||||||
|
actualResults++
|
||||||
|
})
|
||||||
|
expect(expectedResults).toEqual(actualResults)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('iterating over a tree without bounds yield the right amount of results', function (done) {
|
||||||
|
var lowerBound = null
|
||||||
|
var expectedResults = elements.filter(function (e, pos) {
|
||||||
|
return elements.indexOf(e) === pos
|
||||||
|
}).length
|
||||||
|
var actualResults = 0
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.iterate(this, lowerBound, null, function * (val) {
|
||||||
|
expect(val).toBeDefined()
|
||||||
|
actualResults++
|
||||||
|
})
|
||||||
|
expect(expectedResults).toEqual(actualResults)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('iterating over a tree with upper bound yields the right amount of results', function (done) {
|
||||||
|
var upperBound = elements[Math.floor(Math.random() * elements.length)]
|
||||||
|
var expectedResults = elements.filter(function (e, pos) {
|
||||||
|
return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
||||||
|
}).length
|
||||||
|
|
||||||
|
var actualResults = 0
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.iterate(this, null, upperBound, function * (val) {
|
||||||
|
expect(val).toBeDefined()
|
||||||
|
actualResults++
|
||||||
|
})
|
||||||
|
expect(expectedResults).toEqual(actualResults)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) {
|
||||||
|
var b1 = elements[Math.floor(Math.random() * elements.length)]
|
||||||
|
var b2 = elements[Math.floor(Math.random() * elements.length)]
|
||||||
|
var upperBound, lowerBound
|
||||||
|
if (Y.utils.smaller(b1, b2)) {
|
||||||
|
lowerBound = b1
|
||||||
|
upperBound = b2
|
||||||
|
} else {
|
||||||
|
lowerBound = b2
|
||||||
|
upperBound = b1
|
||||||
|
}
|
||||||
|
var expectedResults = elements.filter(function (e, pos) {
|
||||||
|
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
|
||||||
|
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
||||||
|
}).length
|
||||||
|
var actualResults = 0
|
||||||
|
store.requestTransaction(function * () {
|
||||||
|
yield* this.os.iterate(this, lowerBound, upperBound, function * (val) {
|
||||||
|
expect(val).toBeDefined()
|
||||||
|
actualResults++
|
||||||
|
})
|
||||||
|
expect(expectedResults).toEqual(actualResults)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { deleteItemRange } from '../Struct/Delete.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
|
||||||
let dsLength = decoder.readUint32()
|
|
||||||
for (let i = 0; i < dsLength; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
strBuilder.push(' -' + user + ':')
|
|
||||||
let dvLength = decoder.readVarUint()
|
|
||||||
for (let j = 0; j < dvLength; j++) {
|
|
||||||
let from = decoder.readVarUint()
|
|
||||||
let len = decoder.readVarUint()
|
|
||||||
let gc = decoder.readUint8() === 1
|
|
||||||
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strBuilder
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeDeleteSet (y, encoder) {
|
|
||||||
let currentUser = null
|
|
||||||
let currentLength
|
|
||||||
let lastLenPos
|
|
||||||
|
|
||||||
let numberOfUsers = 0
|
|
||||||
let laterDSLenPus = encoder.pos
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
|
|
||||||
y.ds.iterate(null, null, function (n) {
|
|
||||||
var user = n._id.user
|
|
||||||
var clock = n._id.clock
|
|
||||||
var len = n.len
|
|
||||||
var gc = n.gc
|
|
||||||
if (currentUser !== user) {
|
|
||||||
numberOfUsers++
|
|
||||||
// a new user was found
|
|
||||||
if (currentUser !== null) { // happens on first iteration
|
|
||||||
encoder.setUint32(lastLenPos, currentLength)
|
|
||||||
}
|
|
||||||
currentUser = user
|
|
||||||
encoder.writeVarUint(user)
|
|
||||||
// pseudo-fill pos
|
|
||||||
lastLenPos = encoder.pos
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
currentLength = 0
|
|
||||||
}
|
|
||||||
encoder.writeVarUint(clock)
|
|
||||||
encoder.writeVarUint(len)
|
|
||||||
encoder.writeUint8(gc ? 1 : 0)
|
|
||||||
currentLength++
|
|
||||||
})
|
|
||||||
if (currentUser !== null) { // happens on first iteration
|
|
||||||
encoder.setUint32(lastLenPos, currentLength)
|
|
||||||
}
|
|
||||||
encoder.setUint32(laterDSLenPus, numberOfUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readDeleteSet (y, decoder) {
|
|
||||||
let dsLength = decoder.readUint32()
|
|
||||||
for (let i = 0; i < dsLength; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
let dv = []
|
|
||||||
let dvLength = decoder.readUint32()
|
|
||||||
for (let j = 0; j < dvLength; j++) {
|
|
||||||
let from = decoder.readVarUint()
|
|
||||||
let len = decoder.readVarUint()
|
|
||||||
let gc = decoder.readUint8() === 1
|
|
||||||
dv.push([from, len, gc])
|
|
||||||
}
|
|
||||||
if (dvLength > 0) {
|
|
||||||
let pos = 0
|
|
||||||
let d = dv[pos]
|
|
||||||
let deletions = []
|
|
||||||
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
|
|
||||||
// cases:
|
|
||||||
// 1. d deletes something to the right of n
|
|
||||||
// => go to next n (break)
|
|
||||||
// 2. d deletes something to the left of n
|
|
||||||
// => create deletions
|
|
||||||
// => reset d accordingly
|
|
||||||
// *)=> if d doesn't delete anything anymore, go to next d (continue)
|
|
||||||
// 3. not 2) and d deletes something that also n deletes
|
|
||||||
// => reset d so that it doesn't contain n's deletion
|
|
||||||
// *)=> if d does not delete anything anymore, go to next d (continue)
|
|
||||||
while (d != null) {
|
|
||||||
var diff = 0 // describe the diff of length in 1) and 2)
|
|
||||||
if (n._id.clock + n.len <= d[0]) {
|
|
||||||
// 1)
|
|
||||||
break
|
|
||||||
} else if (d[0] < n._id.clock) {
|
|
||||||
// 2)
|
|
||||||
// delete maximum the len of d
|
|
||||||
// else delete as much as possible
|
|
||||||
diff = Math.min(n._id.clock - d[0], d[1])
|
|
||||||
// deleteItemRange(y, user, d[0], diff)
|
|
||||||
deletions.push([user, d[0], diff])
|
|
||||||
} else {
|
|
||||||
// 3)
|
|
||||||
diff = n._id.clock + n.len - d[0] // never null (see 1)
|
|
||||||
if (d[2] && !n.gc) {
|
|
||||||
// d marks as gc'd but n does not
|
|
||||||
// then delete either way
|
|
||||||
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
|
|
||||||
deletions.push([user, d[0], Math.min(diff, d[1])])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (d[1] <= diff) {
|
|
||||||
// d doesn't delete anything anymore
|
|
||||||
d = dv[++pos]
|
|
||||||
} else {
|
|
||||||
d[0] = d[0] + diff // reset pos
|
|
||||||
d[1] = d[1] - diff // reset length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// TODO: It would be more performant to apply the deletes in the above loop
|
|
||||||
// Adapt the Tree implementation to support delete while iterating
|
|
||||||
for (let i = deletions.length - 1; i >= 0; i--) {
|
|
||||||
const del = deletions[i]
|
|
||||||
deleteItemRange(y, del[0], del[1], del[2])
|
|
||||||
}
|
|
||||||
// for the rest.. just apply it
|
|
||||||
for (; pos < dv.length; pos++) {
|
|
||||||
d = dv[pos]
|
|
||||||
deleteItemRange(y, user, d[0], d[1])
|
|
||||||
// deletions.push([user, d[0], d[1], d[2]])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { getStruct } from '../Util/structReferences.js'
|
|
||||||
import BinaryDecoder from '../Binary/Decoder.js'
|
|
||||||
import { logID } from './messageToString.js'
|
|
||||||
|
|
||||||
class MissingEntry {
|
|
||||||
constructor (decoder, missing, struct) {
|
|
||||||
this.decoder = decoder
|
|
||||||
this.missing = missing.length
|
|
||||||
this.struct = struct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integrate remote struct
|
|
||||||
* When a remote struct is integrated, other structs might be ready to ready to
|
|
||||||
* integrate.
|
|
||||||
*/
|
|
||||||
function _integrateRemoteStructHelper (y, struct) {
|
|
||||||
const id = struct._id
|
|
||||||
if (id === undefined) {
|
|
||||||
struct._integrate(y)
|
|
||||||
} else {
|
|
||||||
if (y.ss.getState(id.user) > id.clock) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
struct._integrate(y)
|
|
||||||
let msu = y._missingStructs.get(id.user)
|
|
||||||
if (msu != null) {
|
|
||||||
let clock = id.clock
|
|
||||||
const finalClock = clock + struct._length
|
|
||||||
for (;clock < finalClock; clock++) {
|
|
||||||
const missingStructs = msu.get(clock)
|
|
||||||
if (missingStructs !== undefined) {
|
|
||||||
missingStructs.forEach(missingDef => {
|
|
||||||
missingDef.missing--
|
|
||||||
if (missingDef.missing === 0) {
|
|
||||||
const decoder = missingDef.decoder
|
|
||||||
let oldPos = decoder.pos
|
|
||||||
let missing = missingDef.struct._fromBinary(y, decoder)
|
|
||||||
decoder.pos = oldPos
|
|
||||||
if (missing.length === 0) {
|
|
||||||
y._readyToIntegrate.push(missingDef.struct)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
msu.delete(clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringifyStructs (y, decoder, strBuilder) {
|
|
||||||
const len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let reference = decoder.readVarUint()
|
|
||||||
let Constr = getStruct(reference)
|
|
||||||
let struct = new Constr()
|
|
||||||
let missing = struct._fromBinary(y, decoder)
|
|
||||||
let logMessage = ' ' + struct._logString()
|
|
||||||
if (missing.length > 0) {
|
|
||||||
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
|
|
||||||
}
|
|
||||||
strBuilder.push(logMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function integrateRemoteStructs (decoder, encoder, y) {
|
|
||||||
const len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let reference = decoder.readVarUint()
|
|
||||||
let Constr = getStruct(reference)
|
|
||||||
let struct = new Constr()
|
|
||||||
let decoderPos = decoder.pos
|
|
||||||
let missing = struct._fromBinary(y, decoder)
|
|
||||||
if (missing.length === 0) {
|
|
||||||
while (struct != null) {
|
|
||||||
_integrateRemoteStructHelper(y, struct)
|
|
||||||
struct = y._readyToIntegrate.shift()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
|
||||||
_decoder.pos = decoderPos
|
|
||||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
|
||||||
let missingStructs = y._missingStructs
|
|
||||||
for (let i = missing.length - 1; i >= 0; i--) {
|
|
||||||
let m = missing[i]
|
|
||||||
if (!missingStructs.has(m.user)) {
|
|
||||||
missingStructs.set(m.user, new Map())
|
|
||||||
}
|
|
||||||
let msu = missingStructs.get(m.user)
|
|
||||||
if (!msu.has(m.clock)) {
|
|
||||||
msu.set(m.clock, [])
|
|
||||||
}
|
|
||||||
let mArray = msu = msu.get(m.clock)
|
|
||||||
mArray.push(missingEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import BinaryDecoder from '../Binary/Decoder.js'
|
|
||||||
import { stringifyStructs } from './integrateRemoteStructs.js'
|
|
||||||
import { stringifySyncStep1 } from './syncStep1.js'
|
|
||||||
import { stringifySyncStep2 } from './syncStep2.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import RootID from '../Util/RootID.js'
|
|
||||||
import Y from '../Y.js'
|
|
||||||
|
|
||||||
export function messageToString ([y, buffer]) {
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
decoder.readVarString() // read roomname
|
|
||||||
let type = decoder.readVarString()
|
|
||||||
let strBuilder = []
|
|
||||||
strBuilder.push('\n === ' + type + ' ===')
|
|
||||||
if (type === 'update') {
|
|
||||||
stringifyStructs(y, decoder, strBuilder)
|
|
||||||
} else if (type === 'sync step 1') {
|
|
||||||
stringifySyncStep1(y, decoder, strBuilder)
|
|
||||||
} else if (type === 'sync step 2') {
|
|
||||||
stringifySyncStep2(y, decoder, strBuilder)
|
|
||||||
} else {
|
|
||||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
|
||||||
}
|
|
||||||
return strBuilder.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function messageToRoomname (buffer) {
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
decoder.readVarString() // roomname
|
|
||||||
return decoder.readVarString() // messageType
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logID (id) {
|
|
||||||
if (id !== null && id._id != null) {
|
|
||||||
id = id._id
|
|
||||||
}
|
|
||||||
if (id === null) {
|
|
||||||
return '()'
|
|
||||||
} else if (id instanceof ID) {
|
|
||||||
return `(${id.user},${id.clock})`
|
|
||||||
} else if (id instanceof RootID) {
|
|
||||||
return `(${id.name},${id.type})`
|
|
||||||
} else if (id.constructor === Y) {
|
|
||||||
return `y`
|
|
||||||
} else {
|
|
||||||
throw new Error('This is not a valid ID!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
export function readStateSet (decoder) {
|
|
||||||
let ss = new Map()
|
|
||||||
let ssLength = decoder.readUint32()
|
|
||||||
for (let i = 0; i < ssLength; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
let clock = decoder.readVarUint()
|
|
||||||
ss.set(user, clock)
|
|
||||||
}
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeStateSet (y, encoder) {
|
|
||||||
let lenPosition = encoder.pos
|
|
||||||
let len = 0
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
for (let [user, clock] of y.ss.state) {
|
|
||||||
encoder.writeVarUint(user)
|
|
||||||
encoder.writeVarUint(clock)
|
|
||||||
len++
|
|
||||||
}
|
|
||||||
encoder.setUint32(lenPosition, len)
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import BinaryEncoder from '../Binary/Encoder.js'
|
|
||||||
import { readStateSet, writeStateSet } from './stateSet.js'
|
|
||||||
import { writeDeleteSet } from './deleteSet.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
|
|
||||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
|
||||||
let auth = decoder.readVarString()
|
|
||||||
let protocolVersion = decoder.readVarUint()
|
|
||||||
strBuilder.push(` - auth: "${auth}"`)
|
|
||||||
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
|
|
||||||
// write SS
|
|
||||||
let ssBuilder = []
|
|
||||||
let len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
let clock = decoder.readVarUint()
|
|
||||||
ssBuilder.push(`(${user}:${clock})`)
|
|
||||||
}
|
|
||||||
strBuilder.push(' == SS: ' + ssBuilder.join(','))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendSyncStep1 (connector, syncUser) {
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarString(connector.y.room)
|
|
||||||
encoder.writeVarString('sync step 1')
|
|
||||||
encoder.writeVarString(connector.authInfo || '')
|
|
||||||
encoder.writeVarUint(connector.protocolVersion)
|
|
||||||
writeStateSet(connector.y, encoder)
|
|
||||||
connector.send(syncUser, encoder.createBuffer())
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function writeStructs (encoder, decoder, y, ss) {
|
|
||||||
const lenPos = encoder.pos
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
let len = 0
|
|
||||||
for (let user of y.ss.state.keys()) {
|
|
||||||
let clock = ss.get(user) || 0
|
|
||||||
if (user !== RootFakeUserID) {
|
|
||||||
y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) {
|
|
||||||
struct._toBinary(encoder)
|
|
||||||
len++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
encoder.setUint32(lenPos, len)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
|
||||||
let protocolVersion = decoder.readVarUint()
|
|
||||||
// check protocol version
|
|
||||||
if (protocolVersion !== y.connector.protocolVersion) {
|
|
||||||
console.warn(
|
|
||||||
`You tried to sync with a Yjs instance that has a different protocol version
|
|
||||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
|
||||||
`)
|
|
||||||
y.destroy()
|
|
||||||
}
|
|
||||||
// write sync step 2
|
|
||||||
encoder.writeVarString('sync step 2')
|
|
||||||
encoder.writeVarString(y.connector.authInfo || '')
|
|
||||||
const ss = readStateSet(decoder)
|
|
||||||
writeStructs(encoder, decoder, y, ss)
|
|
||||||
writeDeleteSet(y, encoder)
|
|
||||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
|
||||||
senderConn.receivedSyncStep2 = true
|
|
||||||
if (y.connector.role === 'slave') {
|
|
||||||
sendSyncStep1(y.connector, sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
|
||||||
import { readDeleteSet } from './deleteSet.js'
|
|
||||||
|
|
||||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
|
||||||
strBuilder.push(' - auth: ' + decoder.readVarString())
|
|
||||||
strBuilder.push(' == OS:')
|
|
||||||
stringifyStructs(y, decoder, strBuilder)
|
|
||||||
// write DS to string
|
|
||||||
strBuilder.push(' == DS:')
|
|
||||||
let len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
strBuilder.push(` User: ${user}: `)
|
|
||||||
let len2 = decoder.readUint32()
|
|
||||||
for (let j = 0; j < len2; j++) {
|
|
||||||
let from = decoder.readVarUint()
|
|
||||||
let to = decoder.readVarUint()
|
|
||||||
let gc = decoder.readUint8() === 1
|
|
||||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
|
||||||
integrateRemoteStructs(decoder, encoder, y)
|
|
||||||
readDeleteSet(y, decoder)
|
|
||||||
y.connector._setSyncedWith(sender)
|
|
||||||
}
|
|
||||||
12
src/Notes.md
Normal file
12
src/Notes.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
# Notes
|
||||||
|
|
||||||
|
### Terminology
|
||||||
|
|
||||||
|
* DB: DataBase that holds all the information of the shared object. It is devided into the OS, DS, and SS. This can be a persistent database or an in-memory database. Depending on the type of database, it could make sense to store OS, DS, and SS in different tables, or maybe different databases.
|
||||||
|
* OS: OperationStore holds all the operations. An operation is a js object with a fixed number of name fields.
|
||||||
|
* DS: DeleteStore holds the information about which operations are deleted and which operations were garbage collected (no longer available in the OS).
|
||||||
|
* SS: StateSet holds the current state of the OS. SS.getState(username) refers to the amount of operations that were received by that respective user.
|
||||||
|
* Op: Operation defines an action on a shared type. But it is also the format in which we store the model of a type. This is why it is also called a Struct/Structure.
|
||||||
|
* Type and Structure: We crearly distinguish between type and structure. Short explanation: A type (e.g. Strings, Numbers) have a number of functions that you can apply on them. (+) is well defined on both of them. They are *modeled* by a structure - the functions really change the structure of a type. Types can be implemented differently but still provide the same functionality. In Yjs, almost all types are realized as a doubly linked list (on which Yjs can provide eventual convergence)
|
||||||
|
*
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// import BinaryEncoder from './Binary/Encoder.js'
|
|
||||||
|
|
||||||
export default function extendPersistence (Y) {
|
|
||||||
class AbstractPersistence {
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y
|
|
||||||
this.opts = opts
|
|
||||||
this.saveOperationsBuffer = []
|
|
||||||
this.log = Y.debug('y:persistence')
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToMessageQueue (binary) {
|
|
||||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveOperations (ops) {
|
|
||||||
ops = ops.map(function (op) {
|
|
||||||
return Y.Struct[op.struct].encode(op)
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
const saveOperations = () => {
|
|
||||||
if (this.saveOperationsBuffer.length > 0) {
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarString(this.opts.room)
|
|
||||||
encoder.writeVarString('update')
|
|
||||||
let ops = this.saveOperationsBuffer
|
|
||||||
this.saveOperationsBuffer = []
|
|
||||||
let length = ops.length
|
|
||||||
encoder.writeUint32(length)
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
let op = ops[i]
|
|
||||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
|
||||||
}
|
|
||||||
this.saveToMessageQueue(encoder.createBuffer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (this.saveOperationsBuffer.length === 0) {
|
|
||||||
this.saveOperationsBuffer = ops
|
|
||||||
} else {
|
|
||||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.AbstractPersistence = AbstractPersistence
|
|
||||||
}
|
|
||||||
385
src/SpecHelper.js
Normal file
385
src/SpecHelper.js
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
/* eslint-env browser, jasmine */
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is just a compilation of functions that help to test this library!
|
||||||
|
*/
|
||||||
|
|
||||||
|
// When testing, you store everything on the global object. We call it g
|
||||||
|
|
||||||
|
var Y = require('./y.js')
|
||||||
|
require('../../y-memory/src/Memory.js')(Y)
|
||||||
|
require('../../y-array/src/Array.js')(Y)
|
||||||
|
require('../../y-map/src/Map.js')(Y)
|
||||||
|
require('../../y-indexeddb/src/IndexedDB.js')(Y)
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
require('../../y-leveldb/src/LevelDB.js')(Y)
|
||||||
|
}
|
||||||
|
module.exports = Y
|
||||||
|
|
||||||
|
var g
|
||||||
|
if (typeof global !== 'undefined') {
|
||||||
|
g = global
|
||||||
|
} else if (typeof window !== 'undefined') {
|
||||||
|
g = window
|
||||||
|
} else {
|
||||||
|
throw new Error('No global object?')
|
||||||
|
}
|
||||||
|
g.g = g
|
||||||
|
|
||||||
|
// Helper methods for the random number generator
|
||||||
|
Math.seedrandom = require('seedrandom')
|
||||||
|
|
||||||
|
g.generateRandomSeed = function generateRandomSeed () {
|
||||||
|
var seed
|
||||||
|
if (typeof window !== 'undefined' && window.location.hash.length > 1) {
|
||||||
|
seed = window.location.hash.slice(1) // first character is the hash!
|
||||||
|
console.warn('Using random seed that was specified in the url!')
|
||||||
|
} else {
|
||||||
|
seed = JSON.stringify(Math.random())
|
||||||
|
}
|
||||||
|
console.info('Using random seed: ' + seed)
|
||||||
|
g.setRandomSeed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setRandomSeed = function setRandomSeed (seed) {
|
||||||
|
Math.seedrandom.currentSeed = seed
|
||||||
|
Math.seedrandom(Math.seedrandom.currentSeed, { global: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
g.generateRandomSeed()
|
||||||
|
|
||||||
|
g.YConcurrency_TestingMode = true
|
||||||
|
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
|
||||||
|
|
||||||
|
g.describeManyTimes = function describeManyTimes (times, name, f) {
|
||||||
|
for (var i = 0; i < times; i++) {
|
||||||
|
describe(name, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Wait for a specified amount of time (in ms). defaults to 5ms
|
||||||
|
*/
|
||||||
|
function wait (t) {
|
||||||
|
if (t == null) {
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
setTimeout(function () {
|
||||||
|
resolve()
|
||||||
|
}, t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
g.wait = wait
|
||||||
|
|
||||||
|
g.databases = ['memory']
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
g.databases.push('indexeddb')
|
||||||
|
} else {
|
||||||
|
g.databases.push('leveldb')
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
returns a random element of o.
|
||||||
|
works on Object, and Array
|
||||||
|
*/
|
||||||
|
function getRandom (o) {
|
||||||
|
if (o instanceof Array) {
|
||||||
|
return o[Math.floor(Math.random() * o.length)]
|
||||||
|
} else if (o.constructor === Object) {
|
||||||
|
return o[getRandom(Object.keys(o))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.getRandom = getRandom
|
||||||
|
|
||||||
|
function getRandomNumber (n) {
|
||||||
|
if (n == null) {
|
||||||
|
n = 9999
|
||||||
|
}
|
||||||
|
return Math.floor(Math.random() * n)
|
||||||
|
}
|
||||||
|
g.getRandomNumber = getRandomNumber
|
||||||
|
|
||||||
|
function getRandomString () {
|
||||||
|
var chars = 'abcdefghijklmnopqrstuvwxyzäüöABCDEFGHIJKLMNOPQRSTUVWXYZÄÜÖ'
|
||||||
|
var char = chars[getRandomNumber(chars.length)] // ü\n\n\n\n\n\n\n'
|
||||||
|
var length = getRandomNumber(7)
|
||||||
|
var string = ''
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
string += char
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
g.getRandomString = getRandomString
|
||||||
|
|
||||||
|
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) {
|
||||||
|
g.generateRandomSeed() // create a new seed, so we can re-create the behavior
|
||||||
|
for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
|
||||||
|
var r = Math.random()
|
||||||
|
if (r > 0.95) {
|
||||||
|
// 10% chance of toggling concurrent user interactions.
|
||||||
|
// There will be an artificial delay until ops can be executed by the type,
|
||||||
|
// therefore, operations of the database will be (pre)transformed until user operations arrive
|
||||||
|
yield (function simulateConcurrentUserInteractions (type) {
|
||||||
|
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
|
||||||
|
type.eventHandler.awaiting = 1
|
||||||
|
type.eventHandler._debuggingAwaiting = true
|
||||||
|
} else {
|
||||||
|
// fixAwaitingInType will handle _debuggingAwaiting
|
||||||
|
return fixAwaitingInType(type)
|
||||||
|
}
|
||||||
|
})(getRandom(objects))
|
||||||
|
} else if (r >= 0.5) {
|
||||||
|
// 40% chance to flush
|
||||||
|
yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0)
|
||||||
|
} else if (noReconnect || r >= 0.05) {
|
||||||
|
// 45% chance to create operation
|
||||||
|
var done = getRandom(transactions)(getRandom(objects))
|
||||||
|
if (done != null) {
|
||||||
|
yield done
|
||||||
|
} else {
|
||||||
|
yield wait()
|
||||||
|
}
|
||||||
|
yield Y.utils.globalRoom.whenTransactionsFinished()
|
||||||
|
} else {
|
||||||
|
// 5% chance to disconnect/reconnect
|
||||||
|
var u = getRandom(users)
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
if (u.connector.isDisconnected()) {
|
||||||
|
yield u.reconnect()
|
||||||
|
} else {
|
||||||
|
yield u.disconnect()
|
||||||
|
}
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixAwaitingInType (type) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
type.os.whenTransactionsFinished().then(function () {
|
||||||
|
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
|
||||||
|
type.os.requestTransaction(function * () {
|
||||||
|
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
|
||||||
|
type.eventHandler._debuggingAwaiting = false
|
||||||
|
yield* type.eventHandler.awaitOps(this, function * () { /* mock function */ })
|
||||||
|
}
|
||||||
|
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
g.fixAwaitingInType = fixAwaitingInType
|
||||||
|
|
||||||
|
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||||
|
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
})
|
||||||
|
|
||||||
|
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||||
|
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
for (var u in users) {
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
yield users[u].reconnect()
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
}
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
yield g.garbageCollectAllUsers(users)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||||
|
yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
for (var u in users) {
|
||||||
|
// TODO: here, we enforce that two users never sync at the same time with u[0]
|
||||||
|
// enforce that in the connector itself!
|
||||||
|
yield users[u].reconnect()
|
||||||
|
}
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield Promise.all(objects.map(fixAwaitingInType))
|
||||||
|
yield g.garbageCollectAllUsers(users)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
for (var i in users) {
|
||||||
|
yield users[i].db.emptyGarbageCollector()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
g.compareAllUsers = async(function * compareAllUsers (users) {
|
||||||
|
var s1, s2 // state sets
|
||||||
|
var ds1, ds2 // delete sets
|
||||||
|
var allDels1, allDels2 // all deletions
|
||||||
|
var db1 = [] // operation store of user1
|
||||||
|
|
||||||
|
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
||||||
|
function * t1 () {
|
||||||
|
s1 = yield* this.getStateSet()
|
||||||
|
ds1 = yield* this.getDeleteSet()
|
||||||
|
allDels1 = []
|
||||||
|
yield* this.ds.iterate(this, null, null, function * (d) {
|
||||||
|
allDels1.push(d)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function * t2 () {
|
||||||
|
s2 = yield* this.getStateSet()
|
||||||
|
ds2 = yield* this.getDeleteSet()
|
||||||
|
allDels2 = []
|
||||||
|
yield* this.ds.iterate(this, null, null, function * (d) {
|
||||||
|
allDels2.push(d)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
yield g.garbageCollectAllUsers(users)
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
var buffer = Y.utils.globalRoom.buffers
|
||||||
|
for (var name in buffer) {
|
||||||
|
if (buffer[name].length > 0) {
|
||||||
|
// not all ops were transmitted..
|
||||||
|
debugger // eslint-disable-line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var uid = 0; uid < users.length; uid++) {
|
||||||
|
var u = users[uid]
|
||||||
|
u.db.requestTransaction(function * () {
|
||||||
|
var sv = yield* this.getStateVector()
|
||||||
|
for (var s of sv) {
|
||||||
|
yield* this.updateState(s.user)
|
||||||
|
}
|
||||||
|
// compare deleted ops against deleteStore
|
||||||
|
yield* this.os.iterate(this, null, null, function * (o) {
|
||||||
|
if (o.deleted === true) {
|
||||||
|
expect(yield* this.isDeleted(o.id)).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// compare deleteStore against deleted ops
|
||||||
|
var ds = []
|
||||||
|
yield* this.ds.iterate(this, null, null, function * (d) {
|
||||||
|
ds.push(d)
|
||||||
|
})
|
||||||
|
for (var j in ds) {
|
||||||
|
var d = ds[j]
|
||||||
|
for (var i = 0; i < d.len; i++) {
|
||||||
|
var o = yield* this.getInsertion([d.id[0], d.id[1] + i])
|
||||||
|
// gc'd or deleted
|
||||||
|
if (d.gc) {
|
||||||
|
expect(o).toBeFalsy()
|
||||||
|
} else {
|
||||||
|
expect(o.deleted).toBeTruthy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// compare allDels tree
|
||||||
|
if (s1 == null) {
|
||||||
|
u.db.requestTransaction(function * () {
|
||||||
|
yield* t1.call(this)
|
||||||
|
yield* this.os.iterate(this, null, null, function * (o) {
|
||||||
|
o = Y.utils.copyObject(o)
|
||||||
|
delete o.origin
|
||||||
|
delete o.originOf
|
||||||
|
db1.push(o)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
u.db.requestTransaction(function * () {
|
||||||
|
yield* t2.call(this)
|
||||||
|
var db2 = []
|
||||||
|
yield* this.os.iterate(this, null, null, function * (o) {
|
||||||
|
o = Y.utils.copyObject(o)
|
||||||
|
delete o.origin
|
||||||
|
delete o.originOf
|
||||||
|
db2.push(o)
|
||||||
|
})
|
||||||
|
expect(s1).toEqual(s2)
|
||||||
|
expect(allDels1).toEqual(allDels2) // inner structure
|
||||||
|
expect(ds1).toEqual(ds2) // exported structure
|
||||||
|
db2.forEach((o, i) => {
|
||||||
|
expect(db1[i]).toEqual(o)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
yield u.db.whenTransactionsFinished()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
|
||||||
|
if (Y.utils.globalRoom.users[0] != null) {
|
||||||
|
yield Y.utils.globalRoom.flushAll()
|
||||||
|
}
|
||||||
|
// destroy old users
|
||||||
|
for (var u in Y.utils.globalRoom.users) {
|
||||||
|
Y.utils.globalRoom.users[u].y.destroy()
|
||||||
|
}
|
||||||
|
self.users = null
|
||||||
|
|
||||||
|
var promises = []
|
||||||
|
for (var i = 0; i < numberOfUsers; i++) {
|
||||||
|
promises.push(Y({
|
||||||
|
db: {
|
||||||
|
name: database,
|
||||||
|
namespace: 'User ' + i,
|
||||||
|
cleanStart: true,
|
||||||
|
gcTimeout: -1,
|
||||||
|
repairCheckInterval: -1
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: 'Test',
|
||||||
|
debug: false
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
root: initType || 'Map'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
self.users = yield Promise.all(promises)
|
||||||
|
self.types = self.users.map(function (u) { return u.share.root })
|
||||||
|
return self.users
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
Until async/await arrives in js, we use this function to wait for promises
|
||||||
|
by yielding them.
|
||||||
|
*/
|
||||||
|
function async (makeGenerator) {
|
||||||
|
return function (arg) {
|
||||||
|
var generator = makeGenerator.apply(this, arguments)
|
||||||
|
|
||||||
|
function handle (result) {
|
||||||
|
if (result.done) return Promise.resolve(result.value)
|
||||||
|
|
||||||
|
return Promise.resolve(result.value).then(function (res) {
|
||||||
|
return handle(generator.next(res))
|
||||||
|
}, function (err) {
|
||||||
|
return handle(generator.throw(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return handle(generator.next())
|
||||||
|
} catch (ex) {
|
||||||
|
generator.throw(ex)
|
||||||
|
// return Promise.reject(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.async = async
|
||||||
|
|
||||||
|
function logUsers (self) {
|
||||||
|
if (self.constructor === Array) {
|
||||||
|
self = {users: self}
|
||||||
|
}
|
||||||
|
self.users[0].db.logTable()
|
||||||
|
self.users[1].db.logTable()
|
||||||
|
self.users[2].db.logTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
g.logUsers = logUsers
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import Tree from '../Util/Tree.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
class DSNode {
|
|
||||||
constructor (id, len, gc) {
|
|
||||||
this._id = id
|
|
||||||
this.len = len
|
|
||||||
this.gc = gc
|
|
||||||
}
|
|
||||||
clone () {
|
|
||||||
return new DSNode(this._id, this.len, this.gc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeleteStore extends Tree {
|
|
||||||
logTable () {
|
|
||||||
const deletes = []
|
|
||||||
this.iterate(null, null, function (n) {
|
|
||||||
deletes.push({
|
|
||||||
user: n._id.user,
|
|
||||||
clock: n._id.clock,
|
|
||||||
len: n.len,
|
|
||||||
gc: n.gc
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.table(deletes)
|
|
||||||
}
|
|
||||||
isDeleted (id) {
|
|
||||||
var n = this.findWithUpperBound(id)
|
|
||||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Mark an operation as deleted. returns the deleted node
|
|
||||||
*/
|
|
||||||
markDeleted (id, length) {
|
|
||||||
if (length == null) {
|
|
||||||
throw new Error('length must be defined')
|
|
||||||
}
|
|
||||||
var n = this.findWithUpperBound(id)
|
|
||||||
if (n != null && n._id.user === id.user) {
|
|
||||||
if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
|
|
||||||
// id is in n's range
|
|
||||||
var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
|
|
||||||
if (diff > 0) {
|
|
||||||
// id+length overlaps n
|
|
||||||
if (!n.gc) {
|
|
||||||
n.len += diff
|
|
||||||
} else {
|
|
||||||
diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
|
|
||||||
if (diff < length) {
|
|
||||||
// a partial deletion
|
|
||||||
let nId = id.clone()
|
|
||||||
nId.clock += diff
|
|
||||||
n = new DSNode(nId, length - diff, false)
|
|
||||||
this.put(n)
|
|
||||||
} else {
|
|
||||||
// already gc'd
|
|
||||||
throw new Error(
|
|
||||||
'DS reached an inconsistent state. Please report this issue!'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no overlapping, already deleted
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// cannot extend left (there is no left!)
|
|
||||||
n = new DSNode(id, length, false)
|
|
||||||
this.put(n) // TODO: you double-put !!
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// cannot extend left
|
|
||||||
n = new DSNode(id, length, false)
|
|
||||||
this.put(n)
|
|
||||||
}
|
|
||||||
// can extend right?
|
|
||||||
var next = this.findNext(n._id)
|
|
||||||
if (
|
|
||||||
next != null &&
|
|
||||||
n._id.user === next._id.user &&
|
|
||||||
n._id.clock + n.len >= next._id.clock
|
|
||||||
) {
|
|
||||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
|
||||||
while (diff >= 0) {
|
|
||||||
// n overlaps with next
|
|
||||||
if (next.gc) {
|
|
||||||
// gc is stronger, so reduce length of n
|
|
||||||
n.len -= diff
|
|
||||||
if (diff >= next.len) {
|
|
||||||
// delete the missing range after next
|
|
||||||
diff = diff - next.len // missing range after next
|
|
||||||
if (diff > 0) {
|
|
||||||
this.put(n) // unneccessary? TODO!
|
|
||||||
this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
// we can extend n with next
|
|
||||||
if (diff > next.len) {
|
|
||||||
// n is even longer than next
|
|
||||||
// get next.next, and try to extend it
|
|
||||||
var _next = this.findNext(next._id)
|
|
||||||
this.delete(next._id)
|
|
||||||
if (_next == null || n._id.user !== _next._id.user) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
next = _next
|
|
||||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
|
||||||
// continue!
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// n just partially overlaps with next. extend n, delete next, and break this loop
|
|
||||||
n.len += next.len - diff
|
|
||||||
this.delete(next._id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.put(n)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import Tree from '../Util/Tree.js'
|
|
||||||
import RootID from '../Util/RootID.js'
|
|
||||||
import { getStruct } from '../Util/structReferences.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class OperationStore extends Tree {
|
|
||||||
constructor (y) {
|
|
||||||
super()
|
|
||||||
this.y = y
|
|
||||||
}
|
|
||||||
logTable () {
|
|
||||||
const items = []
|
|
||||||
this.iterate(null, null, function (item) {
|
|
||||||
items.push({
|
|
||||||
id: logID(item),
|
|
||||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
|
||||||
left: logID(item._left === null ? null : item._left._lastId),
|
|
||||||
right: logID(item._right),
|
|
||||||
right_origin: logID(item._right_origin),
|
|
||||||
parent: logID(item._parent),
|
|
||||||
parentSub: item._parentSub,
|
|
||||||
deleted: item._deleted,
|
|
||||||
content: JSON.stringify(item._content)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.table(items)
|
|
||||||
}
|
|
||||||
get (id) {
|
|
||||||
let struct = this.find(id)
|
|
||||||
if (struct === null && id instanceof RootID) {
|
|
||||||
const Constr = getStruct(id.type)
|
|
||||||
const y = this.y
|
|
||||||
struct = new Constr()
|
|
||||||
struct._id = id
|
|
||||||
struct._parent = y
|
|
||||||
y.transact(() => {
|
|
||||||
struct._integrate(y)
|
|
||||||
})
|
|
||||||
this.put(struct)
|
|
||||||
}
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
// Use getItem for structs with _length > 1
|
|
||||||
getItem (id) {
|
|
||||||
var item = this.findWithUpperBound(id)
|
|
||||||
if (item === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const itemID = item._id
|
|
||||||
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
|
|
||||||
return item
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return an insertion such that id is the first element of content
|
|
||||||
// This function manipulates an item, if necessary
|
|
||||||
getItemCleanStart (id) {
|
|
||||||
var ins = this.getItem(id)
|
|
||||||
if (ins === null || ins._length === 1) {
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
const insID = ins._id
|
|
||||||
if (insID.clock === id.clock) {
|
|
||||||
return ins
|
|
||||||
} else {
|
|
||||||
return ins._splitAt(this.y, id.clock - insID.clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return an insertion such that id is the last element of content
|
|
||||||
// This function manipulates an operation, if necessary
|
|
||||||
getItemCleanEnd (id) {
|
|
||||||
var ins = this.getItem(id)
|
|
||||||
if (ins === null || ins._length === 1) {
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
const insID = ins._id
|
|
||||||
if (insID.clock + ins._length - 1 === id.clock) {
|
|
||||||
return ins
|
|
||||||
} else {
|
|
||||||
ins._splitAt(this.y, id.clock - insID.clock + 1)
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
export default class StateStore {
|
|
||||||
constructor (y) {
|
|
||||||
this.y = y
|
|
||||||
this.state = new Map()
|
|
||||||
}
|
|
||||||
logTable () {
|
|
||||||
const entries = []
|
|
||||||
for (let [user, state] of this.state) {
|
|
||||||
entries.push({
|
|
||||||
user, state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.table(entries)
|
|
||||||
}
|
|
||||||
getNextID (len) {
|
|
||||||
const user = this.y.userID
|
|
||||||
const state = this.getState(user)
|
|
||||||
this.setState(user, state + len)
|
|
||||||
return new ID(user, state)
|
|
||||||
}
|
|
||||||
updateRemoteState (struct) {
|
|
||||||
let user = struct._id.user
|
|
||||||
let userState = this.state.get(user)
|
|
||||||
while (struct !== null && struct._id.clock === userState) {
|
|
||||||
userState += struct._length
|
|
||||||
struct = this.y.os.get(new ID(user, userState))
|
|
||||||
}
|
|
||||||
this.state.set(user, userState)
|
|
||||||
}
|
|
||||||
getState (user) {
|
|
||||||
let state = this.state.get(user)
|
|
||||||
if (state == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
setState (user, state) {
|
|
||||||
// TODO: modify missingi structs here
|
|
||||||
const beforeState = this.y._transaction.beforeState
|
|
||||||
if (!beforeState.has(user)) {
|
|
||||||
beforeState.set(user, this.getState(user))
|
|
||||||
}
|
|
||||||
this.state.set(user, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
410
src/Struct.js
Normal file
410
src/Struct.js
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
/* @flow */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/*
|
||||||
|
An operation also defines the structure of a type. This is why operation and
|
||||||
|
structure are used interchangeably here.
|
||||||
|
|
||||||
|
It must be of the type Object. I hope to achieve some performance
|
||||||
|
improvements when working on databases that support the json format.
|
||||||
|
|
||||||
|
An operation must have the following properties:
|
||||||
|
|
||||||
|
* encode
|
||||||
|
- Encode the structure in a readable format (preferably string- todo)
|
||||||
|
* decode (todo)
|
||||||
|
- decode structure to json
|
||||||
|
* execute
|
||||||
|
- Execute the semantics of an operation.
|
||||||
|
* requiredOps
|
||||||
|
- Operations that are required to execute this operation.
|
||||||
|
*/
|
||||||
|
module.exports = function (Y/* :any */) {
|
||||||
|
var Struct = {
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
op = {
|
||||||
|
target: Id
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
Delete: {
|
||||||
|
encode: function (op) {
|
||||||
|
return op
|
||||||
|
},
|
||||||
|
requiredOps: function (op) {
|
||||||
|
return [] // [op.target]
|
||||||
|
},
|
||||||
|
execute: function * (op) {
|
||||||
|
return yield* this.deleteOperation(op.target, op.length || 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Insert: {
|
||||||
|
/* {
|
||||||
|
content: [any],
|
||||||
|
opContent: Id,
|
||||||
|
id: Id,
|
||||||
|
left: Id,
|
||||||
|
origin: Id,
|
||||||
|
right: Id,
|
||||||
|
parent: Id,
|
||||||
|
parentSub: string (optional), // child of Map type
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||||
|
// TODO: you could not send the "left" property, then you also have to
|
||||||
|
// "op.left = null" in $execute or $decode
|
||||||
|
var e/* :any */ = {
|
||||||
|
id: op.id,
|
||||||
|
left: op.left,
|
||||||
|
right: op.right,
|
||||||
|
origin: op.origin,
|
||||||
|
parent: op.parent,
|
||||||
|
struct: op.struct
|
||||||
|
}
|
||||||
|
if (op.parentSub != null) {
|
||||||
|
e.parentSub = op.parentSub
|
||||||
|
}
|
||||||
|
if (op.hasOwnProperty('opContent')) {
|
||||||
|
e.opContent = op.opContent
|
||||||
|
} else {
|
||||||
|
e.content = op.content.slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
requiredOps: function (op) {
|
||||||
|
var ids = []
|
||||||
|
if (op.left != null) {
|
||||||
|
ids.push(op.left)
|
||||||
|
}
|
||||||
|
if (op.right != null) {
|
||||||
|
ids.push(op.right)
|
||||||
|
}
|
||||||
|
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
||||||
|
ids.push(op.origin)
|
||||||
|
}
|
||||||
|
// if (op.right == null && op.left == null) {
|
||||||
|
ids.push(op.parent)
|
||||||
|
|
||||||
|
if (op.opContent != null) {
|
||||||
|
ids.push(op.opContent)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
},
|
||||||
|
getDistanceToOrigin: function * (op) {
|
||||||
|
if (op.left == null) {
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
var d = 0
|
||||||
|
var o = yield* this.getInsertion(op.left)
|
||||||
|
while (!Y.utils.matchesId(o, op.origin)) {
|
||||||
|
d++
|
||||||
|
if (o.left == null) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
o = yield* this.getInsertion(o.left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
# $this has to find a unique position between origin and the next known character
|
||||||
|
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||||
|
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||||
|
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||||
|
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||||
|
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||||
|
# therefore $this would be always to the right of o3
|
||||||
|
# case 2: $origin < $o.origin
|
||||||
|
# if current $this insert_position > $o origin: $this ins
|
||||||
|
# else $insert_position will not change
|
||||||
|
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||||
|
# case 3: $origin > $o.origin
|
||||||
|
# $this insert_position is to the left of $o (forever!)
|
||||||
|
*/
|
||||||
|
execute: function * (op) {
|
||||||
|
var i // loop counter
|
||||||
|
|
||||||
|
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
||||||
|
// We try to merge them later, if possible
|
||||||
|
var tryToRemergeLater = []
|
||||||
|
|
||||||
|
if (op.origin != null) { // TODO: !== instead of !=
|
||||||
|
// we save in origin that op originates in it
|
||||||
|
// we need that later when we eventually garbage collect origin (see transaction)
|
||||||
|
var origin = yield* this.getInsertionCleanEnd(op.origin)
|
||||||
|
if (origin.originOf == null) {
|
||||||
|
origin.originOf = []
|
||||||
|
}
|
||||||
|
origin.originOf.push(op.id)
|
||||||
|
yield* this.setOperation(origin)
|
||||||
|
if (origin.right != null) {
|
||||||
|
tryToRemergeLater.push(origin.right)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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..
|
||||||
|
var o
|
||||||
|
var parent
|
||||||
|
var start
|
||||||
|
|
||||||
|
// find o. o is the first conflicting operation
|
||||||
|
if (op.left != null) {
|
||||||
|
o = yield* this.getInsertionCleanEnd(op.left)
|
||||||
|
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
||||||
|
// only if not added previously
|
||||||
|
tryToRemergeLater.push(o.right)
|
||||||
|
}
|
||||||
|
o = (o.right == null) ? null : yield* this.getOperation(o.right)
|
||||||
|
} else { // left == null
|
||||||
|
parent = yield* this.getOperation(op.parent)
|
||||||
|
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||||
|
start = startId == null ? null : yield* this.getOperation(startId)
|
||||||
|
o = start
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
||||||
|
if (op.right != null) {
|
||||||
|
tryToRemergeLater.push(op.right)
|
||||||
|
yield* this.getInsertionCleanStart(op.right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle conflicts
|
||||||
|
while (true) {
|
||||||
|
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||||
|
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||||
|
if (oOriginDistance === i) {
|
||||||
|
// case 1
|
||||||
|
if (o.id[0] < op.id[0]) {
|
||||||
|
op.left = Y.utils.getLastId(o)
|
||||||
|
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||||
|
}
|
||||||
|
} else if (oOriginDistance < i) {
|
||||||
|
// case 2
|
||||||
|
if (i - distanceToOrigin <= oOriginDistance) {
|
||||||
|
op.left = Y.utils.getLastId(o)
|
||||||
|
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
if (o.right != null) {
|
||||||
|
o = yield* this.getInsertion(o.right)
|
||||||
|
} else {
|
||||||
|
o = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconnect..
|
||||||
|
var left = null
|
||||||
|
var right = null
|
||||||
|
if (parent == null) {
|
||||||
|
parent = yield* this.getOperation(op.parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconnect left and set right of op
|
||||||
|
if (op.left != null) {
|
||||||
|
left = yield* this.getInsertion(op.left)
|
||||||
|
// link left
|
||||||
|
op.right = left.right
|
||||||
|
left.right = op.id
|
||||||
|
|
||||||
|
yield* this.setOperation(left)
|
||||||
|
} else {
|
||||||
|
// set op.right from parent, if necessary
|
||||||
|
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
||||||
|
}
|
||||||
|
// reconnect right
|
||||||
|
if (op.right != null) {
|
||||||
|
// TODO: wanna connect right too?
|
||||||
|
right = yield* this.getOperation(op.right)
|
||||||
|
right.left = Y.utils.getLastId(op)
|
||||||
|
|
||||||
|
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
||||||
|
if (right.gc != null) {
|
||||||
|
if (right.content != null && right.content.length > 1) {
|
||||||
|
right = yield* this.getInsertionCleanEnd(right.id)
|
||||||
|
}
|
||||||
|
this.store.removeFromGarbageCollector(right)
|
||||||
|
}
|
||||||
|
yield* this.setOperation(right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update parents .map/start/end properties
|
||||||
|
if (op.parentSub != null) {
|
||||||
|
if (left == null) {
|
||||||
|
parent.map[op.parentSub] = op.id
|
||||||
|
yield* this.setOperation(parent)
|
||||||
|
}
|
||||||
|
// is a child of a map struct.
|
||||||
|
// 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)
|
||||||
|
if (op.right != null) {
|
||||||
|
yield* this.deleteOperation(op.right, 1, true)
|
||||||
|
}
|
||||||
|
if (op.left != null) {
|
||||||
|
yield* this.deleteOperation(op.id, 1, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (right == null || left == null) {
|
||||||
|
if (right == null) {
|
||||||
|
parent.end = Y.utils.getLastId(op)
|
||||||
|
}
|
||||||
|
if (left == null) {
|
||||||
|
parent.start = op.id
|
||||||
|
}
|
||||||
|
yield* this.setOperation(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to merge original op.left and op.origin
|
||||||
|
for (let i = 0; i < tryToRemergeLater.length; i++) {
|
||||||
|
var m = yield* this.getOperation(tryToRemergeLater[i])
|
||||||
|
yield* this.tryCombineWithLeft(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
List: {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
struct: "List",
|
||||||
|
type: "",
|
||||||
|
id: this.os.getNextOpId(1)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
create: function (id) {
|
||||||
|
return {
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
struct: 'List',
|
||||||
|
id: id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encode: function (op) {
|
||||||
|
var e = {
|
||||||
|
struct: 'List',
|
||||||
|
id: op.id,
|
||||||
|
type: op.type
|
||||||
|
}
|
||||||
|
if (op.requires != null) {
|
||||||
|
e.requires = op.requires
|
||||||
|
}
|
||||||
|
if (op.info != null) {
|
||||||
|
e.info = op.info
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
requiredOps: function () {
|
||||||
|
/*
|
||||||
|
var ids = []
|
||||||
|
if (op.start != null) {
|
||||||
|
ids.push(op.start)
|
||||||
|
}
|
||||||
|
if (op.end != null){
|
||||||
|
ids.push(op.end)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
*/
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
execute: function * (op) {
|
||||||
|
op.start = null
|
||||||
|
op.end = null
|
||||||
|
},
|
||||||
|
ref: function * (op, pos) {
|
||||||
|
if (op.start == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var res = null
|
||||||
|
var o = yield* this.getOperation(op.start)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!o.deleted) {
|
||||||
|
res = o
|
||||||
|
pos--
|
||||||
|
}
|
||||||
|
if (pos >= 0 && o.right != null) {
|
||||||
|
o = yield* this.getOperation(o.right)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
map: function * (o, f) {
|
||||||
|
o = o.start
|
||||||
|
var res = []
|
||||||
|
while (o != null) { // TODO: change to != (at least some convention)
|
||||||
|
var operation = yield* this.getOperation(o)
|
||||||
|
if (!operation.deleted) {
|
||||||
|
res.push(f(operation))
|
||||||
|
}
|
||||||
|
o = operation.right
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Map: {
|
||||||
|
/*
|
||||||
|
{
|
||||||
|
map: {},
|
||||||
|
struct: "Map",
|
||||||
|
type: "",
|
||||||
|
id: this.os.getNextOpId(1)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
create: function (id) {
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
map: {},
|
||||||
|
struct: 'Map'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encode: function (op) {
|
||||||
|
var e = {
|
||||||
|
struct: 'Map',
|
||||||
|
type: op.type,
|
||||||
|
id: op.id,
|
||||||
|
map: {} // overwrite map!!
|
||||||
|
}
|
||||||
|
if (op.requires != null) {
|
||||||
|
e.requires = op.requires
|
||||||
|
}
|
||||||
|
if (op.info != null) {
|
||||||
|
e.info = op.info
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
requiredOps: function () {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
execute: function * () {},
|
||||||
|
/*
|
||||||
|
Get a property by name
|
||||||
|
*/
|
||||||
|
get: function * (op, name) {
|
||||||
|
var oid = op.map[name]
|
||||||
|
if (oid != null) {
|
||||||
|
var res = yield* this.getOperation(oid)
|
||||||
|
if (res == null || res.deleted) {
|
||||||
|
return void 0
|
||||||
|
} else if (res.opContent == null) {
|
||||||
|
return res.content[0]
|
||||||
|
} else {
|
||||||
|
return yield* this.getType(res.opContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.Struct = Struct
|
||||||
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { getReference } from '../Util/structReferences.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all items in an ID-range
|
|
||||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
|
||||||
*/
|
|
||||||
export function deleteItemRange (y, user, clock, range) {
|
|
||||||
const createDelete = y.connector._forwardAppliedStructs
|
|
||||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
|
||||||
if (item !== null) {
|
|
||||||
if (!item._deleted) {
|
|
||||||
item._splitAt(y, range)
|
|
||||||
item._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
let itemLen = item._length
|
|
||||||
range -= itemLen
|
|
||||||
clock += itemLen
|
|
||||||
if (range > 0) {
|
|
||||||
let node = y.os.findNode(new ID(user, clock))
|
|
||||||
while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
|
||||||
const nodeVal = node.val
|
|
||||||
if (!nodeVal._deleted) {
|
|
||||||
nodeVal._splitAt(y, range)
|
|
||||||
nodeVal._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
const nodeLen = nodeVal._length
|
|
||||||
range -= nodeLen
|
|
||||||
clock += nodeLen
|
|
||||||
node = node.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete is not a real struct. It will not be saved in OS
|
|
||||||
*/
|
|
||||||
export default class Delete {
|
|
||||||
constructor () {
|
|
||||||
this._target = null
|
|
||||||
this._length = null
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
// TODO: set target, and add it to missing if not found
|
|
||||||
// There is an edge case in p2p networks!
|
|
||||||
const targetID = decoder.readID()
|
|
||||||
this._targetID = targetID
|
|
||||||
this._length = decoder.readVarUint()
|
|
||||||
if (y.os.getItem(targetID) === null) {
|
|
||||||
return [targetID]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
encoder.writeUint8(getReference(this.constructor))
|
|
||||||
encoder.writeID(this._targetID)
|
|
||||||
encoder.writeVarUint(this._length)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* - If created remotely (a remote user deleted something),
|
|
||||||
* this Delete is applied to all structs in id-range.
|
|
||||||
* - If created lokally (e.g. when y-array deletes a range of elements),
|
|
||||||
* this struct is broadcasted only (it is already executed)
|
|
||||||
*/
|
|
||||||
_integrate (y, locallyCreated = false) {
|
|
||||||
if (!locallyCreated) {
|
|
||||||
// from remote
|
|
||||||
const id = this._targetID
|
|
||||||
deleteItemRange(y, id.user, id.clock, this._length)
|
|
||||||
} else {
|
|
||||||
// from local
|
|
||||||
y.connector.broadcastStruct(this)
|
|
||||||
}
|
|
||||||
if (y.persistence !== null) {
|
|
||||||
y.persistence.saveOperations(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
import { getReference } from '../Util/structReferences.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
import Delete from './Delete.js'
|
|
||||||
import { transactionTypeChanged } from '../Transaction.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper utility to split an Item (see _splitAt)
|
|
||||||
* - copy all properties from a to b
|
|
||||||
* - connect a to b
|
|
||||||
* - assigns the correct _id
|
|
||||||
* - save b to os
|
|
||||||
*/
|
|
||||||
export function splitHelper (y, a, b, diff) {
|
|
||||||
const aID = a._id
|
|
||||||
b._id = new ID(aID.user, aID.clock + diff)
|
|
||||||
b._origin = a
|
|
||||||
b._left = a
|
|
||||||
b._right = a._right
|
|
||||||
if (b._right !== null) {
|
|
||||||
b._right._left = b
|
|
||||||
}
|
|
||||||
b._right_origin = a._right_origin
|
|
||||||
// do not set a._right_origin, as this will lead to problems when syncing
|
|
||||||
a._right = b
|
|
||||||
b._parent = a._parent
|
|
||||||
b._parentSub = a._parentSub
|
|
||||||
b._deleted = a._deleted
|
|
||||||
// now search all relevant items to the right and update origin
|
|
||||||
// if origin is not it foundOrigins, we don't have to search any longer
|
|
||||||
let foundOrigins = new Set()
|
|
||||||
foundOrigins.add(a)
|
|
||||||
let o = b._right
|
|
||||||
while (o !== null && foundOrigins.has(o._origin)) {
|
|
||||||
if (o._origin === a) {
|
|
||||||
o._origin = b
|
|
||||||
}
|
|
||||||
foundOrigins.add(o)
|
|
||||||
o = o._right
|
|
||||||
}
|
|
||||||
y.os.put(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Item {
|
|
||||||
constructor () {
|
|
||||||
this._id = null
|
|
||||||
this._origin = null
|
|
||||||
this._left = null
|
|
||||||
this._right = null
|
|
||||||
this._right_origin = null
|
|
||||||
this._parent = null
|
|
||||||
this._parentSub = null
|
|
||||||
this._deleted = false
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Copy the effect of struct
|
|
||||||
*/
|
|
||||||
_copy () {
|
|
||||||
let struct = new this.constructor()
|
|
||||||
struct._origin = this._left
|
|
||||||
struct._left = this._left
|
|
||||||
struct._right = this
|
|
||||||
struct._right_origin = this
|
|
||||||
struct._parent = this._parent
|
|
||||||
struct._parentSub = this._parentSub
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _lastId () {
|
|
||||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Splits this struct so that another struct can be inserted in-between.
|
|
||||||
* This must be overwritten if _length > 1
|
|
||||||
* Returns right part after split
|
|
||||||
* - diff === 0 => this
|
|
||||||
* - diff === length => this._right
|
|
||||||
* - otherwise => split _content and return right part of split
|
|
||||||
* (see ItemJSON/ItemString for implementation)
|
|
||||||
*/
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
_delete (y, createDelete = true) {
|
|
||||||
this._deleted = true
|
|
||||||
y.ds.markDeleted(this._id, this._length)
|
|
||||||
if (createDelete) {
|
|
||||||
let del = new Delete()
|
|
||||||
del._targetID = this._id
|
|
||||||
del._length = this._length
|
|
||||||
del._integrate(y, true)
|
|
||||||
}
|
|
||||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
|
||||||
y._transaction.deletedStructs.add(this)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This is called right before this struct receives any children.
|
|
||||||
* It can be overwritten to apply pending changes before applying remote changes
|
|
||||||
*/
|
|
||||||
_beforeChange () {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* - Integrate the struct so that other types/structs can see it
|
|
||||||
* - Add this struct to y.os
|
|
||||||
* - Check if this is struct deleted
|
|
||||||
*/
|
|
||||||
_integrate (y) {
|
|
||||||
const parent = this._parent
|
|
||||||
const selfID = this._id
|
|
||||||
const userState = selfID === null ? 0 : y.ss.getState(selfID.user)
|
|
||||||
if (selfID === null) {
|
|
||||||
this._id = y.ss.getNextID(this._length)
|
|
||||||
} else if (selfID.user === RootFakeUserID) {
|
|
||||||
// nop
|
|
||||||
} else if (selfID.clock < userState) {
|
|
||||||
// already applied..
|
|
||||||
return []
|
|
||||||
} else if (selfID.clock === userState) {
|
|
||||||
y.ss.setState(selfID.user, userState + this._length)
|
|
||||||
} else {
|
|
||||||
// missing content from user
|
|
||||||
throw new Error('Can not apply yet!')
|
|
||||||
}
|
|
||||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
|
||||||
// this is the first time parent is updated
|
|
||||||
// or this types is new
|
|
||||||
this._parent._beforeChange()
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
# $this has to find a unique position between origin and the next known character
|
|
||||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
|
||||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
|
||||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
|
||||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
|
||||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
|
||||||
# therefore $this would be always to the right of o3
|
|
||||||
# case 2: $origin < $o.origin
|
|
||||||
# if current $this insert_position > $o origin: $this ins
|
|
||||||
# else $insert_position will not change
|
|
||||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
|
||||||
# case 3: $origin > $o.origin
|
|
||||||
# $this insert_position is to the left of $o (forever!)
|
|
||||||
*/
|
|
||||||
// handle conflicts
|
|
||||||
let o
|
|
||||||
// set o to the first conflicting item
|
|
||||||
if (this._left !== null) {
|
|
||||||
o = this._left._right
|
|
||||||
} else if (this._parentSub !== null) {
|
|
||||||
o = this._parent._map.get(this._parentSub) || null
|
|
||||||
} else {
|
|
||||||
o = this._parent._start
|
|
||||||
}
|
|
||||||
let conflictingItems = new Set()
|
|
||||||
let itemsBeforeOrigin = new Set()
|
|
||||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
|
||||||
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
|
||||||
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
|
||||||
while (o !== null && o !== this._right) {
|
|
||||||
itemsBeforeOrigin.add(o)
|
|
||||||
conflictingItems.add(o)
|
|
||||||
if (this._origin === o._origin) {
|
|
||||||
// case 1
|
|
||||||
if (o._id.user < this._id.user) {
|
|
||||||
this._left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else if (itemsBeforeOrigin.has(o._origin)) {
|
|
||||||
// case 2
|
|
||||||
if (!conflictingItems.has(o._origin)) {
|
|
||||||
this._left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// TODO: try to use right_origin instead.
|
|
||||||
// Then you could basically omit conflictingItems!
|
|
||||||
// Note: you probably can't use right_origin in every case.. only when setting _left
|
|
||||||
o = o._right
|
|
||||||
}
|
|
||||||
// reconnect left/right + update parent map/start if necessary
|
|
||||||
const parentSub = this._parentSub
|
|
||||||
if (this._left === null) {
|
|
||||||
let right
|
|
||||||
if (parentSub !== null) {
|
|
||||||
const pmap = parent._map
|
|
||||||
right = pmap.get(parentSub) || null
|
|
||||||
pmap.set(parentSub, this)
|
|
||||||
} else {
|
|
||||||
right = parent._start
|
|
||||||
parent._start = this
|
|
||||||
}
|
|
||||||
this._right = right
|
|
||||||
if (right !== null) {
|
|
||||||
right._left = this
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const left = this._left
|
|
||||||
const right = left._right
|
|
||||||
this._right = right
|
|
||||||
left._right = this
|
|
||||||
if (right !== null) {
|
|
||||||
right._left = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parent._deleted) {
|
|
||||||
this._delete(y, false)
|
|
||||||
}
|
|
||||||
y.os.put(this)
|
|
||||||
transactionTypeChanged(y, parent, parentSub)
|
|
||||||
if (this._id.user !== RootFakeUserID) {
|
|
||||||
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
|
|
||||||
y.connector.broadcastStruct(this)
|
|
||||||
}
|
|
||||||
if (y.persistence !== null) {
|
|
||||||
y.persistence.saveOperations(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
encoder.writeUint8(getReference(this.constructor))
|
|
||||||
let info = 0
|
|
||||||
if (this._origin !== null) {
|
|
||||||
info += 0b1 // origin is defined
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
/* no longer send _left
|
|
||||||
if (this._left !== this._origin) {
|
|
||||||
info += 0b10 // do not copy origin to left
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (this._right_origin !== null) {
|
|
||||||
info += 0b100
|
|
||||||
}
|
|
||||||
if (this._parentSub !== null) {
|
|
||||||
info += 0b1000
|
|
||||||
}
|
|
||||||
encoder.writeUint8(info)
|
|
||||||
encoder.writeID(this._id)
|
|
||||||
if (info & 0b1) {
|
|
||||||
encoder.writeID(this._origin._lastId)
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
/* see above
|
|
||||||
if (info & 0b10) {
|
|
||||||
encoder.writeID(this._left._lastId)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (info & 0b100) {
|
|
||||||
encoder.writeID(this._right_origin._id)
|
|
||||||
}
|
|
||||||
if ((info & 0b101) === 0) {
|
|
||||||
// neither origin nor right is defined
|
|
||||||
encoder.writeID(this._parent._id)
|
|
||||||
}
|
|
||||||
if (info & 0b1000) {
|
|
||||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = []
|
|
||||||
const info = decoder.readUint8()
|
|
||||||
const id = decoder.readID()
|
|
||||||
this._id = id
|
|
||||||
// read origin
|
|
||||||
if (info & 0b1) {
|
|
||||||
// origin != null
|
|
||||||
const originID = decoder.readID()
|
|
||||||
// we have to query for left again because it might have been split/merged..
|
|
||||||
const origin = y.os.getItemCleanEnd(originID)
|
|
||||||
if (origin === null) {
|
|
||||||
missing.push(originID)
|
|
||||||
} else {
|
|
||||||
this._origin = origin
|
|
||||||
this._left = this._origin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// read right
|
|
||||||
if (info & 0b100) {
|
|
||||||
// right != null
|
|
||||||
const rightID = decoder.readID()
|
|
||||||
// we have to query for right again because it might have been split/merged..
|
|
||||||
const right = y.os.getItemCleanStart(rightID)
|
|
||||||
if (right === null) {
|
|
||||||
missing.push(rightID)
|
|
||||||
} else {
|
|
||||||
this._right = right
|
|
||||||
this._right_origin = right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// read parent
|
|
||||||
if ((info & 0b101) === 0) {
|
|
||||||
// neither origin nor right is defined
|
|
||||||
const parentID = decoder.readID()
|
|
||||||
// parent does not change, so we don't have to search for it again
|
|
||||||
if (this._parent === null) {
|
|
||||||
const parent = y.os.get(parentID)
|
|
||||||
if (parent === null) {
|
|
||||||
missing.push(parentID)
|
|
||||||
} else {
|
|
||||||
this._parent = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this._parent === null) {
|
|
||||||
if (this._origin !== null) {
|
|
||||||
this._parent = this._origin._parent
|
|
||||||
} else if (this._right_origin !== null) {
|
|
||||||
this._parent = this._right_origin._parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (info & 0b1000) {
|
|
||||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
|
||||||
this._parentSub = JSON.parse(decoder.readVarString())
|
|
||||||
}
|
|
||||||
if (y.ss.getState(id.user) < id.clock) {
|
|
||||||
missing.push(new ID(id.user, id.clock - 1))
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { splitHelper, default as Item } from './Item.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class ItemJSON extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return this._content.length
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = super._fromBinary(y, decoder)
|
|
||||||
let len = decoder.readVarUint()
|
|
||||||
this._content = new Array(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const ctnt = decoder.readVarString()
|
|
||||||
let parsed
|
|
||||||
if (ctnt === 'undefined') {
|
|
||||||
parsed = undefined
|
|
||||||
} else {
|
|
||||||
parsed = JSON.parse(ctnt)
|
|
||||||
}
|
|
||||||
this._content[i] = parsed
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
super._toBinary(encoder)
|
|
||||||
let len = this._content.length
|
|
||||||
encoder.writeVarUint(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let encoded
|
|
||||||
let content = this._content[i]
|
|
||||||
if (content === undefined) {
|
|
||||||
encoded = 'undefined'
|
|
||||||
} else {
|
|
||||||
encoded = JSON.stringify(content)
|
|
||||||
}
|
|
||||||
encoder.writeVarString(encoded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
} else if (diff >= this._length) {
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
let item = new ItemJSON()
|
|
||||||
item._content = this._content.splice(diff)
|
|
||||||
splitHelper(y, this, item, diff)
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { splitHelper, default as Item } from './Item.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class ItemString extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return this._content.length
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = super._fromBinary(y, decoder)
|
|
||||||
this._content = decoder.readVarString()
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
super._toBinary(encoder)
|
|
||||||
encoder.writeVarString(this._content)
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
} else if (diff >= this._length) {
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
let item = new ItemString()
|
|
||||||
item._content = this._content.slice(diff)
|
|
||||||
this._content = this._content.slice(0, diff)
|
|
||||||
splitHelper(y, this, item, diff)
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import Item from './Item.js'
|
|
||||||
import EventHandler from '../Util/EventHandler.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
// restructure children as if they were inserted one after another
|
|
||||||
function integrateChildren (y, start) {
|
|
||||||
let right
|
|
||||||
do {
|
|
||||||
right = start._right
|
|
||||||
start._right = null
|
|
||||||
start._right_origin = null
|
|
||||||
start._origin = start._left
|
|
||||||
start._integrate(y)
|
|
||||||
start = right
|
|
||||||
} while (right !== null)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getListItemIDByPosition (type, i) {
|
|
||||||
let pos = 0
|
|
||||||
let n = type._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (pos <= i && i < pos + n._length) {
|
|
||||||
const id = n._id
|
|
||||||
return new ID(id.user, id.clock + i - pos)
|
|
||||||
}
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Type extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._map = new Map()
|
|
||||||
this._start = null
|
|
||||||
this._y = null
|
|
||||||
this._eventHandler = new EventHandler()
|
|
||||||
this._deepEventHandler = new EventHandler()
|
|
||||||
}
|
|
||||||
getPathTo (type) {
|
|
||||||
if (type === this) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const path = []
|
|
||||||
const y = this._y
|
|
||||||
while (type._parent !== this && this._parent !== y) {
|
|
||||||
let parent = type._parent
|
|
||||||
if (type._parentSub !== null) {
|
|
||||||
path.push(type._parentSub)
|
|
||||||
} else {
|
|
||||||
// parent is array-ish
|
|
||||||
for (let [i, child] of parent) {
|
|
||||||
if (child === type) {
|
|
||||||
path.push(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type = parent
|
|
||||||
}
|
|
||||||
if (this._parent !== this) {
|
|
||||||
throw new Error('The type is not a child of this node')
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
_callEventHandler (transaction, event) {
|
|
||||||
const changedParentTypes = transaction.changedParentTypes
|
|
||||||
this._eventHandler.callEventListeners(transaction, event)
|
|
||||||
let type = this
|
|
||||||
while (type !== this._y) {
|
|
||||||
let events = changedParentTypes.get(type)
|
|
||||||
if (events === undefined) {
|
|
||||||
events = []
|
|
||||||
changedParentTypes.set(type, events)
|
|
||||||
}
|
|
||||||
events.push(event)
|
|
||||||
type = type._parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_copy (undeleteChildren) {
|
|
||||||
let copy = super._copy()
|
|
||||||
let map = new Map()
|
|
||||||
copy._map = map
|
|
||||||
for (let [key, value] of this._map) {
|
|
||||||
if (undeleteChildren.has(value) || !value.deleted) {
|
|
||||||
let _item = value._copy(undeleteChildren)
|
|
||||||
_item._parent = copy
|
|
||||||
map.set(key, value._copy(undeleteChildren))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let prevUndeleted = null
|
|
||||||
copy._start = null
|
|
||||||
let item = this._start
|
|
||||||
while (item !== null) {
|
|
||||||
if (undeleteChildren.has(item) || !item.deleted) {
|
|
||||||
let _item = item._copy(undeleteChildren)
|
|
||||||
_item._left = prevUndeleted
|
|
||||||
_item._origin = prevUndeleted
|
|
||||||
_item._right = null
|
|
||||||
_item._right_origin = null
|
|
||||||
_item._parent = copy
|
|
||||||
if (prevUndeleted === null) {
|
|
||||||
copy._start = _item
|
|
||||||
} else {
|
|
||||||
prevUndeleted._right = _item
|
|
||||||
}
|
|
||||||
prevUndeleted = _item
|
|
||||||
}
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
_transact (f) {
|
|
||||||
const y = this._y
|
|
||||||
if (y !== null) {
|
|
||||||
y.transact(f)
|
|
||||||
} else {
|
|
||||||
f(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observe (f) {
|
|
||||||
this._eventHandler.addEventListener(f)
|
|
||||||
}
|
|
||||||
observeDeep (f) {
|
|
||||||
this._deepEventHandler.addEventListener(f)
|
|
||||||
}
|
|
||||||
unobserve (f) {
|
|
||||||
this._eventHandler.removeEventListener(f)
|
|
||||||
}
|
|
||||||
unobserveDeep (f) {
|
|
||||||
this._deepEventHandler.removeEventListener(f)
|
|
||||||
}
|
|
||||||
_integrate (y) {
|
|
||||||
y._transaction.newTypes.add(this)
|
|
||||||
super._integrate(y)
|
|
||||||
this._y = y
|
|
||||||
// when integrating children we must make sure to
|
|
||||||
// integrate start
|
|
||||||
const start = this._start
|
|
||||||
if (start !== null) {
|
|
||||||
this._start = null
|
|
||||||
integrateChildren(y, start)
|
|
||||||
}
|
|
||||||
// integrate map children
|
|
||||||
const map = this._map
|
|
||||||
this._map = new Map()
|
|
||||||
for (let t of map.values()) {
|
|
||||||
// TODO make sure that right elements are deleted!
|
|
||||||
integrateChildren(y, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_delete (y, createDelete) {
|
|
||||||
super._delete(y, createDelete)
|
|
||||||
y._transaction.changedTypes.delete(this)
|
|
||||||
// delete map types
|
|
||||||
for (let value of this._map.values()) {
|
|
||||||
if (value instanceof Item && !value._deleted) {
|
|
||||||
value._delete(y, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// delete array types
|
|
||||||
let t = this._start
|
|
||||||
while (t !== null) {
|
|
||||||
if (!t._deleted) {
|
|
||||||
t._delete(y, false)
|
|
||||||
}
|
|
||||||
t = t._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1112
src/Transaction.js
1112
src/Transaction.js
File diff suppressed because it is too large
Load Diff
@@ -1,222 +0,0 @@
|
|||||||
import Type from '../Struct/Type.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
import YEvent from '../Util/YEvent.js'
|
|
||||||
|
|
||||||
class YArrayEvent extends YEvent {
|
|
||||||
constructor (yarray, remote) {
|
|
||||||
super(yarray)
|
|
||||||
this.remote = remote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class YArray extends Type {
|
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, new YArrayEvent(this, remote))
|
|
||||||
}
|
|
||||||
get (pos) {
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (pos < n._length) {
|
|
||||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
|
||||||
return n._content[pos]
|
|
||||||
} else {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pos -= n._length
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toArray () {
|
|
||||||
return this.map(c => c)
|
|
||||||
}
|
|
||||||
toJSON () {
|
|
||||||
return this.map(c => {
|
|
||||||
if (c instanceof Type) {
|
|
||||||
if (c.toJSON !== null) {
|
|
||||||
return c.toJSON()
|
|
||||||
} else {
|
|
||||||
return c.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
})
|
|
||||||
}
|
|
||||||
map (f) {
|
|
||||||
const res = []
|
|
||||||
this.forEach((c, i) => {
|
|
||||||
res.push(f(c, i, this))
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
forEach (f) {
|
|
||||||
let pos = 0
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (n instanceof Type) {
|
|
||||||
f(n, pos++, this)
|
|
||||||
} else {
|
|
||||||
const content = n._content
|
|
||||||
const contentLen = content.length
|
|
||||||
for (let i = 0; i < contentLen; i++) {
|
|
||||||
pos++
|
|
||||||
f(content[i], pos, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get length () {
|
|
||||||
let length = 0
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
length += n._length
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
[Symbol.iterator] () {
|
|
||||||
return {
|
|
||||||
next: function () {
|
|
||||||
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
|
||||||
// item is deleted or itemElement does not exist (is deleted)
|
|
||||||
this._item = this._item._right
|
|
||||||
this._itemElement = 0
|
|
||||||
}
|
|
||||||
if (this._item === null) {
|
|
||||||
return {
|
|
||||||
done: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let content
|
|
||||||
if (this._item instanceof Type) {
|
|
||||||
content = this._item
|
|
||||||
} else {
|
|
||||||
content = this._item._content[this._itemElement++]
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: [this._count, content],
|
|
||||||
done: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_item: this._start,
|
|
||||||
_itemElement: 0,
|
|
||||||
_count: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete (pos, length = 1) {
|
|
||||||
this._y.transact(() => {
|
|
||||||
let item = this._start
|
|
||||||
let count = 0
|
|
||||||
while (item !== null && length > 0) {
|
|
||||||
if (!item._deleted) {
|
|
||||||
if (count <= pos && pos < count + item._length) {
|
|
||||||
const diffDel = pos - count
|
|
||||||
item = item._splitAt(this._y, diffDel)
|
|
||||||
item._splitAt(this._y, length)
|
|
||||||
length -= item._length
|
|
||||||
item._delete(this._y)
|
|
||||||
count += diffDel
|
|
||||||
} else {
|
|
||||||
count += item._length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (length > 0) {
|
|
||||||
throw new Error('Delete exceeds the range of the YArray')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertAfter (left, content) {
|
|
||||||
this._transact(y => {
|
|
||||||
let right
|
|
||||||
if (left === null) {
|
|
||||||
right = this._start
|
|
||||||
} else {
|
|
||||||
right = left._right
|
|
||||||
}
|
|
||||||
let prevJsonIns = null
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
let c = content[i]
|
|
||||||
if (typeof c === 'function') {
|
|
||||||
c = new c() // eslint-disable-line new-cap
|
|
||||||
}
|
|
||||||
if (c instanceof Type) {
|
|
||||||
if (prevJsonIns !== null) {
|
|
||||||
if (y !== null) {
|
|
||||||
prevJsonIns._integrate(y)
|
|
||||||
}
|
|
||||||
left = prevJsonIns
|
|
||||||
prevJsonIns = null
|
|
||||||
}
|
|
||||||
c._origin = left
|
|
||||||
c._left = left
|
|
||||||
c._right = right
|
|
||||||
c._right_origin = right
|
|
||||||
c._parent = this
|
|
||||||
if (y !== null) {
|
|
||||||
c._integrate(y)
|
|
||||||
} else if (left === null) {
|
|
||||||
this._start = c
|
|
||||||
} else {
|
|
||||||
left._right = c
|
|
||||||
}
|
|
||||||
left = c
|
|
||||||
} else {
|
|
||||||
if (prevJsonIns === null) {
|
|
||||||
prevJsonIns = new ItemJSON()
|
|
||||||
prevJsonIns._origin = left
|
|
||||||
prevJsonIns._left = left
|
|
||||||
prevJsonIns._right = right
|
|
||||||
prevJsonIns._right_origin = right
|
|
||||||
prevJsonIns._parent = this
|
|
||||||
prevJsonIns._content = []
|
|
||||||
}
|
|
||||||
prevJsonIns._content.push(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (prevJsonIns !== null && y !== null) {
|
|
||||||
prevJsonIns._integrate(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
insert (pos, content) {
|
|
||||||
let left = null
|
|
||||||
let right = this._start
|
|
||||||
let count = 0
|
|
||||||
const y = this._y
|
|
||||||
while (right !== null) {
|
|
||||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
|
||||||
if (count <= pos && pos <= count + rightLen) {
|
|
||||||
const splitDiff = pos - count
|
|
||||||
right = right._splitAt(y, splitDiff)
|
|
||||||
left = right._left
|
|
||||||
count += splitDiff
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (!right._deleted) {
|
|
||||||
count += right._length
|
|
||||||
}
|
|
||||||
left = right
|
|
||||||
right = right._right
|
|
||||||
}
|
|
||||||
if (pos > count) {
|
|
||||||
throw new Error('Position exceeds array range!')
|
|
||||||
}
|
|
||||||
this.insertAfter(left, content)
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `YArray(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
src/Type/YMap.js
114
src/Type/YMap.js
@@ -1,114 +0,0 @@
|
|||||||
import Type from '../Struct/Type.js'
|
|
||||||
import Item from '../Struct/Item.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
import YEvent from '../Util/YEvent.js'
|
|
||||||
|
|
||||||
class YMapEvent extends YEvent {
|
|
||||||
constructor (ymap, subs, remote) {
|
|
||||||
super(ymap)
|
|
||||||
this.keysChanged = subs
|
|
||||||
this.remote = remote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class YMap extends Type {
|
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
|
|
||||||
}
|
|
||||||
toJSON () {
|
|
||||||
const map = {}
|
|
||||||
for (let [key, item] of this._map) {
|
|
||||||
if (!item._deleted) {
|
|
||||||
let res
|
|
||||||
if (item instanceof Type) {
|
|
||||||
if (item.toJSON !== undefined) {
|
|
||||||
res = item.toJSON()
|
|
||||||
} else {
|
|
||||||
res = item.toString()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res = item._content[0]
|
|
||||||
}
|
|
||||||
map[key] = res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
keys () {
|
|
||||||
let keys = []
|
|
||||||
for (let [key, value] of this._map) {
|
|
||||||
if (!value._deleted) {
|
|
||||||
keys.push(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
delete (key) {
|
|
||||||
this._transact((y) => {
|
|
||||||
let c = this._map.get(key)
|
|
||||||
if (y !== null && c !== undefined) {
|
|
||||||
c._delete(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
set (key, value) {
|
|
||||||
this._transact(y => {
|
|
||||||
const old = this._map.get(key) || null
|
|
||||||
if (old !== null) {
|
|
||||||
if (old instanceof ItemJSON && old._content[0] === value) {
|
|
||||||
// Trying to overwrite with same value
|
|
||||||
// break here
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
if (y !== null) {
|
|
||||||
old._delete(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let v
|
|
||||||
if (typeof value === 'function') {
|
|
||||||
v = new value() // eslint-disable-line new-cap
|
|
||||||
value = v
|
|
||||||
} else if (value instanceof Item) {
|
|
||||||
v = value
|
|
||||||
} else {
|
|
||||||
v = new ItemJSON()
|
|
||||||
v._content = [value]
|
|
||||||
}
|
|
||||||
v._right = old
|
|
||||||
v._right_origin = old
|
|
||||||
v._parent = this
|
|
||||||
v._parentSub = key
|
|
||||||
if (y !== null) {
|
|
||||||
v._integrate(y)
|
|
||||||
} else {
|
|
||||||
this._map.set(key, v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
get (key) {
|
|
||||||
let v = this._map.get(key)
|
|
||||||
if (v === undefined || v._deleted) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (v instanceof Type) {
|
|
||||||
return v
|
|
||||||
} else {
|
|
||||||
return v._content[v._content.length - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has (key) {
|
|
||||||
let v = this._map.get(key)
|
|
||||||
if (v === undefined || v._deleted) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `YMap(id:${logID(this._id)},mapSize:${this._map.size},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
import YArray from './YArray.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class YText extends YArray {
|
|
||||||
constructor (string) {
|
|
||||||
super()
|
|
||||||
if (typeof string === 'string') {
|
|
||||||
const start = new ItemString()
|
|
||||||
start._parent = this
|
|
||||||
start._content = string
|
|
||||||
this._start = start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toString () {
|
|
||||||
const strBuilder = []
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
strBuilder.push(n._content)
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
return strBuilder.join('')
|
|
||||||
}
|
|
||||||
insert (pos, text) {
|
|
||||||
this._transact(y => {
|
|
||||||
let left = null
|
|
||||||
let right = this._start
|
|
||||||
let count = 0
|
|
||||||
while (right !== null) {
|
|
||||||
if (count <= pos && pos < count + right._length) {
|
|
||||||
const splitDiff = pos - count
|
|
||||||
right = right._splitAt(this._y, pos - count)
|
|
||||||
left = right._left
|
|
||||||
count += splitDiff
|
|
||||||
break
|
|
||||||
}
|
|
||||||
count += right._length
|
|
||||||
left = right
|
|
||||||
right = right._right
|
|
||||||
}
|
|
||||||
if (pos > count) {
|
|
||||||
throw new Error('Position exceeds array range!')
|
|
||||||
}
|
|
||||||
let item = new ItemString()
|
|
||||||
item._origin = left
|
|
||||||
item._left = left
|
|
||||||
item._right = right
|
|
||||||
item._right_origin = right
|
|
||||||
item._parent = this
|
|
||||||
item._content = text
|
|
||||||
if (y !== null) {
|
|
||||||
item._integrate(this._y)
|
|
||||||
} else if (left === null) {
|
|
||||||
this._start = item
|
|
||||||
} else {
|
|
||||||
left._right = item
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `YText(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
// import diff from 'fast-diff'
|
|
||||||
import { defaultDomFilter } from './utils.js'
|
|
||||||
|
|
||||||
import YMap from '../YMap.js'
|
|
||||||
import YXmlFragment from './YXmlFragment.js'
|
|
||||||
|
|
||||||
export default class YXmlElement extends YXmlFragment {
|
|
||||||
constructor (arg1, arg2, _document) {
|
|
||||||
super()
|
|
||||||
this.nodeName = null
|
|
||||||
this._scrollElement = null
|
|
||||||
if (typeof arg1 === 'string') {
|
|
||||||
this.nodeName = arg1.toUpperCase()
|
|
||||||
} else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === arg1.ELEMENT_NODE) {
|
|
||||||
this.nodeName = arg1.nodeName
|
|
||||||
this._setDom(arg1, _document)
|
|
||||||
} else {
|
|
||||||
this.nodeName = 'UNDEFINED'
|
|
||||||
}
|
|
||||||
if (typeof arg2 === 'function') {
|
|
||||||
this._domFilter = arg2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_copy (undeleteChildren) {
|
|
||||||
let struct = super._copy(undeleteChildren)
|
|
||||||
struct.nodeName = this.nodeName
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
_setDom (dom, _document) {
|
|
||||||
if (this._dom != null) {
|
|
||||||
throw new Error('Only call this method if you know what you are doing ;)')
|
|
||||||
} else if (dom._yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
|
|
||||||
throw new Error('Already bound to an YXml type')
|
|
||||||
} else {
|
|
||||||
// tag is already set in constructor
|
|
||||||
// set attributes
|
|
||||||
let attributes = new Map()
|
|
||||||
for (let i = 0; i < dom.attributes.length; i++) {
|
|
||||||
let attr = dom.attributes[i]
|
|
||||||
attributes.set(attr.name, attr.value)
|
|
||||||
}
|
|
||||||
attributes = this._domFilter(dom, attributes)
|
|
||||||
attributes.forEach((value, name) => {
|
|
||||||
this.setAttribute(name, value)
|
|
||||||
})
|
|
||||||
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes), _document)
|
|
||||||
this._bindToDom(dom, _document)
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
const missing = super._fromBinary(y, decoder)
|
|
||||||
this.nodeName = decoder.readVarString()
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
super._toBinary(encoder)
|
|
||||||
encoder.writeVarString(this.nodeName)
|
|
||||||
}
|
|
||||||
_integrate (y) {
|
|
||||||
if (this.nodeName === null) {
|
|
||||||
throw new Error('nodeName must be defined!')
|
|
||||||
}
|
|
||||||
if (this._domFilter === defaultDomFilter && this._parent instanceof YXmlFragment) {
|
|
||||||
this._domFilter = this._parent._domFilter
|
|
||||||
}
|
|
||||||
super._integrate(y)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Returns the string representation of the XML document.
|
|
||||||
* The attributes are ordered by attribute-name, so you can easily use this
|
|
||||||
* method to compare YXmlElements
|
|
||||||
*/
|
|
||||||
toString () {
|
|
||||||
const attrs = this.getAttributes()
|
|
||||||
const stringBuilder = []
|
|
||||||
const keys = []
|
|
||||||
for (let key in attrs) {
|
|
||||||
keys.push(key)
|
|
||||||
}
|
|
||||||
keys.sort()
|
|
||||||
const keysLen = keys.length
|
|
||||||
for (let i = 0; i < keysLen; i++) {
|
|
||||||
const key = keys[i]
|
|
||||||
stringBuilder.push(key + '="' + attrs[key] + '"')
|
|
||||||
}
|
|
||||||
const nodeName = this.nodeName.toLocaleLowerCase()
|
|
||||||
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
|
||||||
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
|
||||||
}
|
|
||||||
removeAttribute () {
|
|
||||||
return YMap.prototype.delete.apply(this, arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttribute () {
|
|
||||||
return YMap.prototype.set.apply(this, arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttribute () {
|
|
||||||
return YMap.prototype.get.apply(this, arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttributes () {
|
|
||||||
const obj = {}
|
|
||||||
for (let [key, value] of this._map) {
|
|
||||||
if (!value._deleted) {
|
|
||||||
obj[key] = value._content[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
getDom (_document) {
|
|
||||||
_document = _document || document
|
|
||||||
let dom = this._dom
|
|
||||||
if (dom == null) {
|
|
||||||
dom = _document.createElement(this.nodeName)
|
|
||||||
dom._yxml = this
|
|
||||||
let attrs = this.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
dom.setAttribute(key, attrs[key])
|
|
||||||
}
|
|
||||||
this.forEach(yxml => {
|
|
||||||
dom.appendChild(yxml.getDom(_document))
|
|
||||||
})
|
|
||||||
this._bindToDom(dom, _document)
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import YEvent from '../../Util/YEvent.js'
|
|
||||||
|
|
||||||
export default class YXmlEvent extends YEvent {
|
|
||||||
constructor (target, subs, remote) {
|
|
||||||
super(target)
|
|
||||||
this.childListChanged = false
|
|
||||||
this.attributesChanged = new Set()
|
|
||||||
this.remote = remote
|
|
||||||
subs.forEach((sub) => {
|
|
||||||
if (sub === null) {
|
|
||||||
this.childListChanged = true
|
|
||||||
} else {
|
|
||||||
this.attributesChanged.add(sub)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
/* global MutationObserver */
|
|
||||||
|
|
||||||
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
|
||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
|
||||||
|
|
||||||
import YArray from '../YArray.js'
|
|
||||||
import YXmlText from './YXmlText.js'
|
|
||||||
import YXmlEvent from './YXmlEvent.js'
|
|
||||||
import { logID } from '../../MessageHandler/messageToString.js'
|
|
||||||
import diff from 'fast-diff'
|
|
||||||
|
|
||||||
function domToYXml (parent, doms, _document) {
|
|
||||||
const types = []
|
|
||||||
doms.forEach(d => {
|
|
||||||
if (d._yxml != null && d._yxml !== false) {
|
|
||||||
d._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (parent._domFilter(d.nodeName, new Map()) !== null) {
|
|
||||||
let type
|
|
||||||
if (d.nodeType === d.TEXT_NODE) {
|
|
||||||
type = new YXmlText(d)
|
|
||||||
} else if (d.nodeType === d.ELEMENT_NODE) {
|
|
||||||
type = new YXmlFragment._YXmlElement(d, parent._domFilter, _document)
|
|
||||||
} else {
|
|
||||||
throw new Error('Unsupported node!')
|
|
||||||
}
|
|
||||||
type.enableSmartScrolling(parent._scrollElement)
|
|
||||||
types.push(type)
|
|
||||||
} else {
|
|
||||||
d._yxml = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
class YXmlTreeWalker {
|
|
||||||
constructor (root, f) {
|
|
||||||
this._filter = f || (() => true)
|
|
||||||
this._root = root
|
|
||||||
this._currentNode = root
|
|
||||||
this._firstCall = true
|
|
||||||
}
|
|
||||||
[Symbol.iterator] () {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
next () {
|
|
||||||
let n = this._currentNode
|
|
||||||
if (this._firstCall) {
|
|
||||||
this._firstCall = false
|
|
||||||
if (!n._deleted && this._filter(n)) {
|
|
||||||
return { value: n, done: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
if (!n._deleted && n.constructor === YXmlFragment._YXmlElement && n._start !== null) {
|
|
||||||
// walk down in the tree
|
|
||||||
n = n._start
|
|
||||||
} else {
|
|
||||||
// walk right or up in the tree
|
|
||||||
while (n !== this._root) {
|
|
||||||
if (n._right !== null) {
|
|
||||||
n = n._right
|
|
||||||
break
|
|
||||||
}
|
|
||||||
n = n._parent
|
|
||||||
}
|
|
||||||
if (n === this._root) {
|
|
||||||
n = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (n === this._root) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} while (n !== null && (n._deleted || !this._filter(n)))
|
|
||||||
this._currentNode = n
|
|
||||||
if (n === null) {
|
|
||||||
return { done: true }
|
|
||||||
} else {
|
|
||||||
return { value: n, done: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class YXmlFragment extends YArray {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._dom = null
|
|
||||||
this._domFilter = defaultDomFilter
|
|
||||||
this._domObserver = null
|
|
||||||
// this function makes sure that either the
|
|
||||||
// dom event is executed, or the yjs observer is executed
|
|
||||||
var token = true
|
|
||||||
this._mutualExclude = f => {
|
|
||||||
if (token) {
|
|
||||||
token = false
|
|
||||||
try {
|
|
||||||
f()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
if (this._domObserver !== null) {
|
|
||||||
this._domObserver.takeRecords()
|
|
||||||
}
|
|
||||||
token = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createTreeWalker (filter) {
|
|
||||||
return new YXmlTreeWalker(this, filter)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Retrieve first element that matches *query*
|
|
||||||
* Similar to DOM's querySelector, but only accepts a subset of its queries
|
|
||||||
*
|
|
||||||
* Query support:
|
|
||||||
* - tagname
|
|
||||||
* TODO:
|
|
||||||
* - id
|
|
||||||
* - attribute
|
|
||||||
*/
|
|
||||||
querySelector (query) {
|
|
||||||
query = query.toUpperCase()
|
|
||||||
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
|
|
||||||
const next = iterator.next()
|
|
||||||
if (next.done) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
return next.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
querySelectorAll (query) {
|
|
||||||
query = query.toUpperCase()
|
|
||||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
|
||||||
}
|
|
||||||
enableSmartScrolling (scrollElement) {
|
|
||||||
this._scrollElement = scrollElement
|
|
||||||
this.forEach(xml => {
|
|
||||||
xml.enableSmartScrolling(scrollElement)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setDomFilter (f) {
|
|
||||||
this._domFilter = f
|
|
||||||
this.forEach(xml => {
|
|
||||||
xml.setDomFilter(f)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote))
|
|
||||||
}
|
|
||||||
toString () {
|
|
||||||
return this.map(xml => xml.toString()).join('')
|
|
||||||
}
|
|
||||||
_delete (y, createDelete) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
super._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
_unbindFromDom () {
|
|
||||||
if (this._domObserver != null) {
|
|
||||||
this._domObserver.disconnect()
|
|
||||||
this._domObserver = null
|
|
||||||
}
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._dom._yxml = null
|
|
||||||
this._dom = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertDomElementsAfter (prev, doms, _document) {
|
|
||||||
const types = domToYXml(this, doms, _document)
|
|
||||||
this.insertAfter(prev, types)
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
insertDomElements (pos, doms, _document) {
|
|
||||||
const types = domToYXml(this, doms, _document)
|
|
||||||
this.insert(pos, types)
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
getDom () {
|
|
||||||
return this._dom
|
|
||||||
}
|
|
||||||
bindToDom (dom, _document) {
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (dom._yxml != null) {
|
|
||||||
dom._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
dom.innerHTML = ''
|
|
||||||
this.forEach(t => {
|
|
||||||
dom.insertBefore(t.getDom(_document), null)
|
|
||||||
})
|
|
||||||
this._bindToDom(dom, _document)
|
|
||||||
}
|
|
||||||
// binds to a dom element
|
|
||||||
// Only call if dom and YXml are isomorph
|
|
||||||
_bindToDom (dom, _document) {
|
|
||||||
_document = _document || document
|
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
// TODO: refine this..
|
|
||||||
if ((this.constructor !== YXmlFragment && this._parent !== this._y) || this._parent === null) {
|
|
||||||
// TODO: only top level YXmlFragment can bind. Also allow YXmlElements..
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
|
|
||||||
this._y.on('afterTransaction', afterTransactionSelectionFixer)
|
|
||||||
const applyFilter = (type) => {
|
|
||||||
if (type._deleted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if type is a child of this
|
|
||||||
let isChild = false
|
|
||||||
let p = type
|
|
||||||
while (p !== this._y) {
|
|
||||||
if (p === this) {
|
|
||||||
isChild = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p = p._parent
|
|
||||||
}
|
|
||||||
if (!isChild) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// filter attributes
|
|
||||||
let attributes = new Map()
|
|
||||||
if (type.getAttributes !== undefined) {
|
|
||||||
let attrs = type.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let result = this._domFilter(type.nodeName, new Map(attributes))
|
|
||||||
if (result === null) {
|
|
||||||
type._delete(this._y)
|
|
||||||
} else {
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
if (!result.has(key)) {
|
|
||||||
type.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._y.on('beforeObserverCalls', function (y, transaction) {
|
|
||||||
// apply dom filter to new and changed types
|
|
||||||
transaction.changedTypes.forEach(function (subs, type) {
|
|
||||||
if (subs.size > 1 || !subs.has(null)) {
|
|
||||||
// only apply changes on attributes
|
|
||||||
applyFilter(type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
transaction.newTypes.forEach(applyFilter)
|
|
||||||
})
|
|
||||||
// Apply Y.Xml events to dom
|
|
||||||
this.observeDeep(events => {
|
|
||||||
reflectChangesOnDom.call(this, events, _document)
|
|
||||||
})
|
|
||||||
// Apply Dom changes on Y.Xml
|
|
||||||
if (typeof MutationObserver !== 'undefined') {
|
|
||||||
this._y.on('beforeTransaction', () => {
|
|
||||||
this._domObserverListener(this._domObserver.takeRecords())
|
|
||||||
})
|
|
||||||
this._domObserverListener = mutations => {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
this._y.transact(() => {
|
|
||||||
let diffChildren = new Set()
|
|
||||||
mutations.forEach(mutation => {
|
|
||||||
const dom = mutation.target
|
|
||||||
const yxml = dom._yxml
|
|
||||||
if (yxml == null) {
|
|
||||||
// dom element is filtered
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (mutation.type) {
|
|
||||||
case 'characterData':
|
|
||||||
var diffs = diff(yxml.toString(), dom.nodeValue)
|
|
||||||
var pos = 0
|
|
||||||
for (var i = 0; i < diffs.length; i++) {
|
|
||||||
var d = diffs[i]
|
|
||||||
if (d[0] === 0) { // EQUAL
|
|
||||||
pos += d[1].length
|
|
||||||
} else if (d[0] === -1) { // DELETE
|
|
||||||
yxml.delete(pos, d[1].length)
|
|
||||||
} else { // INSERT
|
|
||||||
yxml.insert(pos, d[1])
|
|
||||||
pos += d[1].length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'attributes':
|
|
||||||
if (yxml.constructor === YXmlFragment) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let name = mutation.attributeName
|
|
||||||
let val = dom.getAttribute(name)
|
|
||||||
// check if filter accepts attribute
|
|
||||||
let attributes = new Map()
|
|
||||||
attributes.set(name, val)
|
|
||||||
if (this._domFilter(dom.nodeName, attributes).size > 0 && yxml.constructor !== YXmlFragment) {
|
|
||||||
if (yxml.getAttribute(name) !== val) {
|
|
||||||
if (val == null) {
|
|
||||||
yxml.removeAttribute(name)
|
|
||||||
} else {
|
|
||||||
yxml.setAttribute(name, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'childList':
|
|
||||||
diffChildren.add(mutation.target)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (let dom of diffChildren) {
|
|
||||||
if (dom._yxml != null && dom._yxml !== false) {
|
|
||||||
applyChangesFromDom(dom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
|
||||||
this._domObserver.observe(dom, {
|
|
||||||
childList: true,
|
|
||||||
attributes: true,
|
|
||||||
characterData: true,
|
|
||||||
subtree: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `YXml(id:${logID(this._id)},left:${logID(left)},origin:${logID(origin)},right:${this._right},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import YText from '../YText.js'
|
|
||||||
|
|
||||||
export default class YXmlText extends YText {
|
|
||||||
constructor (arg1) {
|
|
||||||
let dom = null
|
|
||||||
let initialText = null
|
|
||||||
if (arg1 != null) {
|
|
||||||
if (arg1.nodeType != null && arg1.nodeType === arg1.TEXT_NODE) {
|
|
||||||
dom = arg1
|
|
||||||
initialText = dom.nodeValue
|
|
||||||
} else if (typeof arg1 === 'string') {
|
|
||||||
initialText = arg1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super(initialText)
|
|
||||||
this._dom = null
|
|
||||||
this._domObserver = null
|
|
||||||
this._domObserverListener = null
|
|
||||||
this._scrollElement = null
|
|
||||||
if (dom !== null) {
|
|
||||||
this._setDom(arg1)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var token = true
|
|
||||||
this._mutualExclude = f => {
|
|
||||||
if (token) {
|
|
||||||
token = false
|
|
||||||
try {
|
|
||||||
f()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
this._domObserver.takeRecords()
|
|
||||||
token = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.observe(event => {
|
|
||||||
if (this._dom != null) {
|
|
||||||
const dom = this._dom
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
let anchorViewPosition = getAnchorViewPosition(this._scrollElement)
|
|
||||||
let anchorViewFix
|
|
||||||
if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) {
|
|
||||||
anchorViewFix = anchorViewPosition
|
|
||||||
} else {
|
|
||||||
anchorViewFix = null
|
|
||||||
}
|
|
||||||
dom.nodeValue = this.toString()
|
|
||||||
fixScrollPosition(this._scrollElement, anchorViewFix)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
setDomFilter () {}
|
|
||||||
enableSmartScrolling (scrollElement) {
|
|
||||||
this._scrollElement = scrollElement
|
|
||||||
}
|
|
||||||
_setDom (dom) {
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (dom._yxml != null) {
|
|
||||||
dom._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
// set marker
|
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
}
|
|
||||||
getDom (_document) {
|
|
||||||
_document = _document || document
|
|
||||||
if (this._dom === null) {
|
|
||||||
const dom = _document.createTextNode(this.toString())
|
|
||||||
this._setDom(dom)
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
return this._dom
|
|
||||||
}
|
|
||||||
_delete (y, createDelete) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
super._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
_unbindFromDom () {
|
|
||||||
if (this._domObserver != null) {
|
|
||||||
this._domObserver.disconnect()
|
|
||||||
this._domObserver = null
|
|
||||||
}
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._dom._yxml = null
|
|
||||||
this._dom = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
const filterMap = new Map()
|
|
||||||
|
|
||||||
export function addFilter (type, filter) {
|
|
||||||
if (!filterMap.has(type)) {
|
|
||||||
filterMap.set(type, new Set())
|
|
||||||
}
|
|
||||||
const filters = filterMap.get(type)
|
|
||||||
filters.add(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function executeFilter (type) {
|
|
||||||
const y = type._y
|
|
||||||
let parent = type
|
|
||||||
const nodeName = type.nodeName
|
|
||||||
let attributes = new Map()
|
|
||||||
if (type.getAttributes !== undefined) {
|
|
||||||
let attrs = type.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let filteredAttributes = new Map(attributes)
|
|
||||||
// is not y, supports dom filtering
|
|
||||||
while (parent !== y && parent.setDomFilter != null) {
|
|
||||||
const filters = filterMap.get(parent)
|
|
||||||
if (filters !== undefined) {
|
|
||||||
for (let f of filters) {
|
|
||||||
filteredAttributes = f(nodeName, filteredAttributes)
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent = parent._parent
|
|
||||||
}
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
type._delete(y)
|
|
||||||
} else {
|
|
||||||
// iterate original attributes
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
// delete all attributes that are not in filteredAttributes
|
|
||||||
if (!filteredAttributes.has(key)) {
|
|
||||||
type.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/* globals getSelection */
|
|
||||||
|
|
||||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
|
||||||
|
|
||||||
let browserSelection = null
|
|
||||||
let relativeSelection = null
|
|
||||||
|
|
||||||
export let beforeTransactionSelectionFixer
|
|
||||||
if (typeof getSelection !== 'undefined') {
|
|
||||||
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) {
|
|
||||||
if (!remote) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
relativeSelection = { from: null, to: null, fromY: null, toY: null }
|
|
||||||
browserSelection = getSelection()
|
|
||||||
const anchorNode = browserSelection.anchorNode
|
|
||||||
if (anchorNode !== null && anchorNode._yxml != null) {
|
|
||||||
const yxml = anchorNode._yxml
|
|
||||||
relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset)
|
|
||||||
relativeSelection.fromY = yxml._y
|
|
||||||
}
|
|
||||||
const focusNode = browserSelection.focusNode
|
|
||||||
if (focusNode !== null && focusNode._yxml != null) {
|
|
||||||
const yxml = focusNode._yxml
|
|
||||||
relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset)
|
|
||||||
relativeSelection.toY = yxml._y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function afterTransactionSelectionFixer (y, transaction, remote) {
|
|
||||||
if (relativeSelection === null || !remote) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const to = relativeSelection.to
|
|
||||||
const from = relativeSelection.from
|
|
||||||
const fromY = relativeSelection.fromY
|
|
||||||
const toY = relativeSelection.toY
|
|
||||||
let shouldUpdate = false
|
|
||||||
let anchorNode = browserSelection.anchorNode
|
|
||||||
let anchorOffset = browserSelection.anchorOffset
|
|
||||||
let focusNode = browserSelection.focusNode
|
|
||||||
let focusOffset = browserSelection.focusOffset
|
|
||||||
if (from !== null) {
|
|
||||||
let sel = fromRelativePosition(fromY, from)
|
|
||||||
if (sel !== null) {
|
|
||||||
let node = sel.type.getDom()
|
|
||||||
let offset = sel.offset
|
|
||||||
if (node !== anchorNode || offset !== anchorOffset) {
|
|
||||||
anchorNode = node
|
|
||||||
anchorOffset = offset
|
|
||||||
shouldUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (to !== null) {
|
|
||||||
let sel = fromRelativePosition(toY, to)
|
|
||||||
if (sel !== null) {
|
|
||||||
let node = sel.type.getDom()
|
|
||||||
let offset = sel.offset
|
|
||||||
if (node !== focusNode || offset !== focusOffset) {
|
|
||||||
focusNode = node
|
|
||||||
focusOffset = offset
|
|
||||||
shouldUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldUpdate) {
|
|
||||||
console.info('updating selection!!')
|
|
||||||
browserSelection.setBaseAndExtent(
|
|
||||||
anchorNode,
|
|
||||||
anchorOffset,
|
|
||||||
focusNode,
|
|
||||||
focusOffset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// delete, so the objects can be gc'd
|
|
||||||
relativeSelection = null
|
|
||||||
browserSelection = null
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
import YXmlText from './YXmlText.js'
|
|
||||||
|
|
||||||
export function defaultDomFilter (node, attributes) {
|
|
||||||
return attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAnchorViewPosition (scrollElement) {
|
|
||||||
if (scrollElement == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
let anchor = document.getSelection().anchorNode
|
|
||||||
if (anchor != null) {
|
|
||||||
let top = getBoundingClientRect(anchor).top
|
|
||||||
if (top >= 0 && top <= document.documentElement.clientHeight) {
|
|
||||||
return {
|
|
||||||
anchor: anchor,
|
|
||||||
top: top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
anchor: null,
|
|
||||||
scrollTop: scrollElement.scrollTop,
|
|
||||||
scrollHeight: scrollElement.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get BoundingClientRect that works on text nodes
|
|
||||||
export function getBoundingClientRect (element) {
|
|
||||||
if (element.getBoundingClientRect != null) {
|
|
||||||
// is element node
|
|
||||||
return element.getBoundingClientRect()
|
|
||||||
} else {
|
|
||||||
// is text node
|
|
||||||
if (element.parentNode == null) {
|
|
||||||
// range requires that text nodes have a parent
|
|
||||||
let span = document.createElement('span')
|
|
||||||
span.appendChild(element)
|
|
||||||
}
|
|
||||||
let range = document.createRange()
|
|
||||||
range.selectNode(element)
|
|
||||||
return range.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fixScrollPosition (scrollElement, fix) {
|
|
||||||
if (scrollElement !== null && fix !== null) {
|
|
||||||
if (fix.anchor === null) {
|
|
||||||
if (scrollElement.scrollTop === fix.scrollTop) {
|
|
||||||
scrollElement.scrollTop = scrollElement.scrollHeight - fix.scrollHeight
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scrollElement.scrollTop = getBoundingClientRect(fix.anchor).top - fix.top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function iterateUntilUndeleted (item) {
|
|
||||||
while (item !== null && item._deleted) {
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
function _insertNodeHelper (yxml, prevExpectedNode, child) {
|
|
||||||
let insertedNodes = yxml.insertDomElementsAfter(prevExpectedNode, [child])
|
|
||||||
if (insertedNodes.length > 0) {
|
|
||||||
return insertedNodes[0]
|
|
||||||
} else {
|
|
||||||
return prevExpectedNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Check if any of the nodes was deleted
|
|
||||||
* 2. Iterate over the children.
|
|
||||||
* 2.1 If a node exists without _yxml property, insert a new node
|
|
||||||
* 2.2 If _contents.length < dom.childNodes.length, fill the
|
|
||||||
* rest of _content with childNodes
|
|
||||||
* 2.3 If a node was moved, delete it and
|
|
||||||
* recreate a new yxml element that is bound to that node.
|
|
||||||
* You can detect that a node was moved because expectedId
|
|
||||||
* !== actualId in the list
|
|
||||||
*/
|
|
||||||
export function applyChangesFromDom (dom) {
|
|
||||||
const yxml = dom._yxml
|
|
||||||
const y = yxml._y
|
|
||||||
let knownChildren =
|
|
||||||
new Set(
|
|
||||||
Array.prototype.map.call(dom.childNodes, child => child._yxml)
|
|
||||||
.filter(id => id !== undefined)
|
|
||||||
)
|
|
||||||
// 1. Check if any of the nodes was deleted
|
|
||||||
yxml.forEach(function (childType, i) {
|
|
||||||
if (!knownChildren.has(childType)) {
|
|
||||||
childType._delete(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 2. iterate
|
|
||||||
let childNodes = dom.childNodes
|
|
||||||
let len = childNodes.length
|
|
||||||
let prevExpectedNode = null
|
|
||||||
let expectedNode = iterateUntilUndeleted(yxml._start)
|
|
||||||
for (let domCnt = 0; domCnt < len; domCnt++) {
|
|
||||||
const child = childNodes[domCnt]
|
|
||||||
const childYXml = child._yxml
|
|
||||||
if (childYXml != null) {
|
|
||||||
if (childYXml === false) {
|
|
||||||
// should be ignored or is going to be deleted
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (expectedNode !== null) {
|
|
||||||
if (expectedNode !== childYXml) {
|
|
||||||
// 2.3 Not expected node
|
|
||||||
if (childYXml._parent !== this) {
|
|
||||||
// element is going to be deleted by its previous parent
|
|
||||||
child._yxml = null
|
|
||||||
} else {
|
|
||||||
childYXml._delete(y)
|
|
||||||
}
|
|
||||||
prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child)
|
|
||||||
} else {
|
|
||||||
prevExpectedNode = expectedNode
|
|
||||||
expectedNode = iterateUntilUndeleted(expectedNode._right)
|
|
||||||
}
|
|
||||||
// if this is the expected node id, just continue
|
|
||||||
} else {
|
|
||||||
// 2.2 fill _conten with child nodes
|
|
||||||
prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 2.1 A new node was found
|
|
||||||
prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reflectChangesOnDom (events, _document) {
|
|
||||||
// Make sure that no filtered attributes are applied to the structure
|
|
||||||
// if they were, delete them
|
|
||||||
/*
|
|
||||||
events.forEach(event => {
|
|
||||||
const target = event.target
|
|
||||||
if (event.attributesChanged === undefined) {
|
|
||||||
// event.target is Y.XmlText
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const keys = this._domFilter(target.nodeName, Array.from(event.attributesChanged))
|
|
||||||
if (keys === null) {
|
|
||||||
target._delete()
|
|
||||||
} else {
|
|
||||||
const removeKeys = new Set() // is a copy of event.attributesChanged
|
|
||||||
event.attributesChanged.forEach(key => { removeKeys.add(key) })
|
|
||||||
keys.forEach(key => {
|
|
||||||
// remove all accepted keys from removeKeys
|
|
||||||
removeKeys.delete(key)
|
|
||||||
})
|
|
||||||
// remove the filtered attribute
|
|
||||||
removeKeys.forEach(key => {
|
|
||||||
target.removeAttribute(key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
events.forEach(event => {
|
|
||||||
const yxml = event.target
|
|
||||||
const dom = yxml._dom
|
|
||||||
if (dom != null) {
|
|
||||||
// TODO: do this once before applying stuff
|
|
||||||
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
|
||||||
if (yxml.constructor === YXmlText) {
|
|
||||||
yxml._dom.nodeValue = yxml.toString()
|
|
||||||
} else {
|
|
||||||
// update attributes
|
|
||||||
event.attributesChanged.forEach(attributeName => {
|
|
||||||
const value = yxml.getAttribute(attributeName)
|
|
||||||
if (value === undefined) {
|
|
||||||
dom.removeAttribute(attributeName)
|
|
||||||
} else {
|
|
||||||
dom.setAttribute(attributeName, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (event.childListChanged) {
|
|
||||||
// create fragment of undeleted nodes
|
|
||||||
const fragment = _document.createDocumentFragment()
|
|
||||||
yxml.forEach(function (t) {
|
|
||||||
fragment.appendChild(t.getDom(_document))
|
|
||||||
})
|
|
||||||
// remove remainding nodes
|
|
||||||
let lastChild = dom.lastChild
|
|
||||||
while (lastChild !== null) {
|
|
||||||
dom.removeChild(lastChild)
|
|
||||||
lastChild = dom.lastChild
|
|
||||||
}
|
|
||||||
// insert fragment of undeleted nodes
|
|
||||||
dom.appendChild(fragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* TODO: smartscrolling
|
|
||||||
.. else if (event.type === 'childInserted' || event.type === 'insert') {
|
|
||||||
let nodes = event.values
|
|
||||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
||||||
let node = nodes[i]
|
|
||||||
node.setDomFilter(yxml._domFilter)
|
|
||||||
node.enableSmartScrolling(yxml._scrollElement)
|
|
||||||
let dom = node.getDom()
|
|
||||||
let fixPosition = null
|
|
||||||
let nextDom = null
|
|
||||||
if (yxml._content.length > event.index + i + 1) {
|
|
||||||
nextDom = yxml.get(event.index + i + 1).getDom()
|
|
||||||
}
|
|
||||||
yxml._dom.insertBefore(dom, nextDom)
|
|
||||||
if (anchorViewPosition === null) {
|
|
||||||
// nop
|
|
||||||
} else if (anchorViewPosition.anchor !== null) {
|
|
||||||
// no scrolling when current selection
|
|
||||||
if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
} else if (getBoundingClientRect(dom).top <= 0) {
|
|
||||||
// adjust scrolling if modified element is out of view,
|
|
||||||
// there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
fixScrollPosition(yxml._scrollElement, fixPosition)
|
|
||||||
}
|
|
||||||
} else if (event.type === 'childRemoved' || event.type === 'delete') {
|
|
||||||
for (let i = event.values.length - 1; i >= 0; i--) {
|
|
||||||
let dom = event.values[i]._dom
|
|
||||||
let fixPosition = null
|
|
||||||
if (anchorViewPosition === null) {
|
|
||||||
// nop
|
|
||||||
} else if (anchorViewPosition.anchor !== null) {
|
|
||||||
// no scrolling when current selection
|
|
||||||
if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
} else if (getBoundingClientRect(dom).top <= 0) {
|
|
||||||
// adjust scrolling if modified element is out of view,
|
|
||||||
// there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
dom.remove()
|
|
||||||
fixScrollPosition(yxml._scrollElement, fixPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
import YXmlFragment from './YXmlFragment.js'
|
|
||||||
import YXmlElement from './YXmlElement.js'
|
|
||||||
|
|
||||||
export { default as YXmlFragment } from './YXmlFragment.js'
|
|
||||||
export { default as YXmlElement } from './YXmlElement.js'
|
|
||||||
export { default as YXmlText } from './YXmlText.js'
|
|
||||||
|
|
||||||
YXmlFragment._YXmlElement = YXmlElement
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
export default class EventHandler {
|
|
||||||
constructor () {
|
|
||||||
this.eventListeners = []
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this.eventListeners = null
|
|
||||||
}
|
|
||||||
addEventListener (f) {
|
|
||||||
this.eventListeners.push(f)
|
|
||||||
}
|
|
||||||
removeEventListener (f) {
|
|
||||||
this.eventListeners = this.eventListeners.filter(function (g) {
|
|
||||||
return f !== g
|
|
||||||
})
|
|
||||||
}
|
|
||||||
removeAllEventListeners () {
|
|
||||||
this.eventListeners = []
|
|
||||||
}
|
|
||||||
callEventListeners (transaction, event) {
|
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
|
||||||
try {
|
|
||||||
const f = this.eventListeners[i]
|
|
||||||
f(event)
|
|
||||||
} catch (e) {
|
|
||||||
/*
|
|
||||||
Your observer threw an error. This error was caught so that Yjs
|
|
||||||
can ensure data consistency! In order to debug this error you
|
|
||||||
have to check "Pause On Caught Exceptions" in developer tools.
|
|
||||||
*/
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
export default class ID {
|
|
||||||
constructor (user, clock) {
|
|
||||||
this.user = user
|
|
||||||
this.clock = clock
|
|
||||||
}
|
|
||||||
clone () {
|
|
||||||
return new ID(this.user, this.clock)
|
|
||||||
}
|
|
||||||
equals (id) {
|
|
||||||
return id !== null && id.user === this.user && id.clock === this.clock
|
|
||||||
}
|
|
||||||
lessThan (id) {
|
|
||||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
export default class NamedEventHandler {
|
|
||||||
constructor () {
|
|
||||||
this._eventListener = new Map()
|
|
||||||
}
|
|
||||||
_getListener (name) {
|
|
||||||
let listeners = this._eventListener.get(name)
|
|
||||||
if (listeners === undefined) {
|
|
||||||
listeners = {
|
|
||||||
once: new Set(),
|
|
||||||
on: new Set()
|
|
||||||
}
|
|
||||||
this._eventListener.set(name, listeners)
|
|
||||||
}
|
|
||||||
return listeners
|
|
||||||
}
|
|
||||||
once (name, f) {
|
|
||||||
let listeners = this._getListener(name)
|
|
||||||
listeners.once.add(f)
|
|
||||||
}
|
|
||||||
on (name, f) {
|
|
||||||
let listeners = this._getListener(name)
|
|
||||||
listeners.on.add(f)
|
|
||||||
}
|
|
||||||
off (name, f) {
|
|
||||||
if (name == null || f == null) {
|
|
||||||
throw new Error('You must specify event name and function!')
|
|
||||||
}
|
|
||||||
const listener = this._eventListener.get(name)
|
|
||||||
if (listener !== undefined) {
|
|
||||||
listener.remove(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit (name, ...args) {
|
|
||||||
const listener = this._eventListener.get(name)
|
|
||||||
if (listener !== undefined) {
|
|
||||||
listener.on.forEach(f => f.apply(null, args))
|
|
||||||
listener.once.forEach(f => f.apply(null, args))
|
|
||||||
listener.once = new Set()
|
|
||||||
} else if (name === 'error') {
|
|
||||||
console.error(args[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this._eventListener = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { getReference } from './structReferences.js'
|
|
||||||
|
|
||||||
export const RootFakeUserID = 0xFFFFFF
|
|
||||||
|
|
||||||
export default class RootID {
|
|
||||||
constructor (name, typeConstructor) {
|
|
||||||
this.user = RootFakeUserID
|
|
||||||
this.name = name
|
|
||||||
this.type = getReference(typeConstructor)
|
|
||||||
}
|
|
||||||
equals (id) {
|
|
||||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
|
||||||
}
|
|
||||||
lessThan (id) {
|
|
||||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
471
src/Util/Tree.js
471
src/Util/Tree.js
@@ -1,471 +0,0 @@
|
|||||||
|
|
||||||
class N {
|
|
||||||
// A created node is always red!
|
|
||||||
constructor (val) {
|
|
||||||
this.val = val
|
|
||||||
this.color = true
|
|
||||||
this._left = null
|
|
||||||
this._right = null
|
|
||||||
this._parent = null
|
|
||||||
}
|
|
||||||
isRed () { return this.color }
|
|
||||||
isBlack () { return !this.color }
|
|
||||||
redden () { this.color = true; return this }
|
|
||||||
blacken () { this.color = false; return this }
|
|
||||||
get grandparent () {
|
|
||||||
return this.parent.parent
|
|
||||||
}
|
|
||||||
get parent () {
|
|
||||||
return this._parent
|
|
||||||
}
|
|
||||||
get sibling () {
|
|
||||||
return (this === this.parent.left)
|
|
||||||
? this.parent.right : this.parent.left
|
|
||||||
}
|
|
||||||
get left () {
|
|
||||||
return this._left
|
|
||||||
}
|
|
||||||
get right () {
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
set left (n) {
|
|
||||||
if (n !== null) {
|
|
||||||
n._parent = this
|
|
||||||
}
|
|
||||||
this._left = n
|
|
||||||
}
|
|
||||||
set right (n) {
|
|
||||||
if (n !== null) {
|
|
||||||
n._parent = this
|
|
||||||
}
|
|
||||||
this._right = n
|
|
||||||
}
|
|
||||||
rotateLeft (tree) {
|
|
||||||
var parent = this.parent
|
|
||||||
var newParent = this.right
|
|
||||||
var newRight = this.right.left
|
|
||||||
newParent.left = this
|
|
||||||
this.right = newRight
|
|
||||||
if (parent === null) {
|
|
||||||
tree.root = newParent
|
|
||||||
newParent._parent = null
|
|
||||||
} else if (parent.left === this) {
|
|
||||||
parent.left = newParent
|
|
||||||
} else if (parent.right === this) {
|
|
||||||
parent.right = newParent
|
|
||||||
} else {
|
|
||||||
throw new Error('The elements are wrongly connected!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next () {
|
|
||||||
if (this.right !== null) {
|
|
||||||
// search the most left node in the right tree
|
|
||||||
var o = this.right
|
|
||||||
while (o.left !== null) {
|
|
||||||
o = o.left
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
} else {
|
|
||||||
var p = this
|
|
||||||
while (p.parent !== null && p !== p.parent.left) {
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return p.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev () {
|
|
||||||
if (this.left !== null) {
|
|
||||||
// search the most right node in the left tree
|
|
||||||
var o = this.left
|
|
||||||
while (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
} else {
|
|
||||||
var p = this
|
|
||||||
while (p.parent !== null && p !== p.parent.right) {
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return p.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rotateRight (tree) {
|
|
||||||
var parent = this.parent
|
|
||||||
var newParent = this.left
|
|
||||||
var newLeft = this.left.right
|
|
||||||
newParent.right = this
|
|
||||||
this.left = newLeft
|
|
||||||
if (parent === null) {
|
|
||||||
tree.root = newParent
|
|
||||||
newParent._parent = null
|
|
||||||
} else if (parent.left === this) {
|
|
||||||
parent.left = newParent
|
|
||||||
} else if (parent.right === this) {
|
|
||||||
parent.right = newParent
|
|
||||||
} else {
|
|
||||||
throw new Error('The elements are wrongly connected!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getUncle () {
|
|
||||||
// we can assume that grandparent exists when this is called!
|
|
||||||
if (this.parent === this.parent.parent.left) {
|
|
||||||
return this.parent.parent.right
|
|
||||||
} else {
|
|
||||||
return this.parent.parent.left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is a Red Black Tree implementation
|
|
||||||
*/
|
|
||||||
export default class Tree {
|
|
||||||
constructor () {
|
|
||||||
this.root = null
|
|
||||||
this.length = 0
|
|
||||||
}
|
|
||||||
findNext (id) {
|
|
||||||
var nextID = id.clone()
|
|
||||||
nextID.clock += 1
|
|
||||||
return this.findWithLowerBound(nextID)
|
|
||||||
}
|
|
||||||
findPrev (id) {
|
|
||||||
let prevID = id.clone()
|
|
||||||
prevID.clock -= 1
|
|
||||||
return this.findWithUpperBound(prevID)
|
|
||||||
}
|
|
||||||
findNodeWithLowerBound (from) {
|
|
||||||
var o = this.root
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
while (true) {
|
|
||||||
if (from === null || (from.lessThan(o.val._id) && o.left !== null)) {
|
|
||||||
// o is included in the bound
|
|
||||||
// try to find an element that is closer to the bound
|
|
||||||
o = o.left
|
|
||||||
} else if (from !== null && o.val._id.lessThan(from)) {
|
|
||||||
// o is not within the bound, maybe one of the right elements is..
|
|
||||||
if (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
} else {
|
|
||||||
// there is no right element. Search for the next bigger element,
|
|
||||||
// this should be within the bounds
|
|
||||||
return o.next()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findNodeWithUpperBound (to) {
|
|
||||||
if (to === void 0) {
|
|
||||||
throw new Error('You must define from!')
|
|
||||||
}
|
|
||||||
var o = this.root
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
while (true) {
|
|
||||||
if ((to === null || o.val._id.lessThan(to)) && o.right !== null) {
|
|
||||||
// o is included in the bound
|
|
||||||
// try to find an element that is closer to the bound
|
|
||||||
o = o.right
|
|
||||||
} else if (to !== null && to.lessThan(o.val._id)) {
|
|
||||||
// o is not within the bound, maybe one of the left elements is..
|
|
||||||
if (o.left !== null) {
|
|
||||||
o = o.left
|
|
||||||
} else {
|
|
||||||
// there is no left element. Search for the prev smaller element,
|
|
||||||
// this should be within the bounds
|
|
||||||
return o.prev()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findSmallestNode () {
|
|
||||||
var o = this.root
|
|
||||||
while (o != null && o.left != null) {
|
|
||||||
o = o.left
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
findWithLowerBound (from) {
|
|
||||||
var n = this.findNodeWithLowerBound(from)
|
|
||||||
return n == null ? null : n.val
|
|
||||||
}
|
|
||||||
findWithUpperBound (to) {
|
|
||||||
var n = this.findNodeWithUpperBound(to)
|
|
||||||
return n == null ? null : n.val
|
|
||||||
}
|
|
||||||
iterate (from, to, f) {
|
|
||||||
var o
|
|
||||||
if (from === null) {
|
|
||||||
o = this.findSmallestNode()
|
|
||||||
} else {
|
|
||||||
o = this.findNodeWithLowerBound(from)
|
|
||||||
}
|
|
||||||
while (
|
|
||||||
o !== null &&
|
|
||||||
(
|
|
||||||
to === null || // eslint-disable-line no-unmodified-loop-condition
|
|
||||||
o.val._id.lessThan(to) ||
|
|
||||||
o.val._id.equals(to)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
f(o.val)
|
|
||||||
o = o.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
find (id) {
|
|
||||||
let n = this.findNode(id)
|
|
||||||
if (n !== null) {
|
|
||||||
return n.val
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findNode (id) {
|
|
||||||
var o = this.root
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
while (true) {
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (id.lessThan(o.val._id)) {
|
|
||||||
o = o.left
|
|
||||||
} else if (o.val._id.lessThan(id)) {
|
|
||||||
o = o.right
|
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete (id) {
|
|
||||||
var d = this.findNode(id)
|
|
||||||
if (d == null) {
|
|
||||||
// throw new Error('Element does not exist!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.length--
|
|
||||||
if (d.left !== null && d.right !== null) {
|
|
||||||
// switch d with the greates element in the left subtree.
|
|
||||||
// o should have at most one child.
|
|
||||||
var o = d.left
|
|
||||||
// find
|
|
||||||
while (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
}
|
|
||||||
// switch
|
|
||||||
d.val = o.val
|
|
||||||
d = o
|
|
||||||
}
|
|
||||||
// d has at most one child
|
|
||||||
// let n be the node that replaces d
|
|
||||||
var isFakeChild
|
|
||||||
var child = d.left || d.right
|
|
||||||
if (child === null) {
|
|
||||||
isFakeChild = true
|
|
||||||
child = new N(null)
|
|
||||||
child.blacken()
|
|
||||||
d.right = child
|
|
||||||
} else {
|
|
||||||
isFakeChild = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (d.parent === null) {
|
|
||||||
if (!isFakeChild) {
|
|
||||||
this.root = child
|
|
||||||
child.blacken()
|
|
||||||
child._parent = null
|
|
||||||
} else {
|
|
||||||
this.root = null
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else if (d.parent.left === d) {
|
|
||||||
d.parent.left = child
|
|
||||||
} else if (d.parent.right === d) {
|
|
||||||
d.parent.right = child
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible!')
|
|
||||||
}
|
|
||||||
if (d.isBlack()) {
|
|
||||||
if (child.isRed()) {
|
|
||||||
child.blacken()
|
|
||||||
} else {
|
|
||||||
this._fixDelete(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.root.blacken()
|
|
||||||
if (isFakeChild) {
|
|
||||||
if (child.parent.left === child) {
|
|
||||||
child.parent.left = null
|
|
||||||
} else if (child.parent.right === child) {
|
|
||||||
child.parent.right = null
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible #3')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_fixDelete (n) {
|
|
||||||
function isBlack (node) {
|
|
||||||
return node !== null ? node.isBlack() : true
|
|
||||||
}
|
|
||||||
function isRed (node) {
|
|
||||||
return node !== null ? node.isRed() : false
|
|
||||||
}
|
|
||||||
if (n.parent === null) {
|
|
||||||
// this can only be called after the first iteration of fixDelete.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// d was already replaced by the child
|
|
||||||
// d is not the root
|
|
||||||
// d and child are black
|
|
||||||
var sibling = n.sibling
|
|
||||||
if (isRed(sibling)) {
|
|
||||||
// make sibling the grandfather
|
|
||||||
n.parent.redden()
|
|
||||||
sibling.blacken()
|
|
||||||
if (n === n.parent.left) {
|
|
||||||
n.parent.rotateLeft(this)
|
|
||||||
} else if (n === n.parent.right) {
|
|
||||||
n.parent.rotateRight(this)
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible #2')
|
|
||||||
}
|
|
||||||
sibling = n.sibling
|
|
||||||
}
|
|
||||||
// parent, sibling, and children of n are black
|
|
||||||
if (n.parent.isBlack() &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isBlack(sibling.left) &&
|
|
||||||
isBlack(sibling.right)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
this._fixDelete(n.parent)
|
|
||||||
} else if (n.parent.isRed() &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isBlack(sibling.left) &&
|
|
||||||
isBlack(sibling.right)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
n.parent.blacken()
|
|
||||||
} else {
|
|
||||||
if (n === n.parent.left &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isRed(sibling.left) &&
|
|
||||||
isBlack(sibling.right)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
sibling.left.blacken()
|
|
||||||
sibling.rotateRight(this)
|
|
||||||
sibling = n.sibling
|
|
||||||
} else if (n === n.parent.right &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isRed(sibling.right) &&
|
|
||||||
isBlack(sibling.left)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
sibling.right.blacken()
|
|
||||||
sibling.rotateLeft(this)
|
|
||||||
sibling = n.sibling
|
|
||||||
}
|
|
||||||
sibling.color = n.parent.color
|
|
||||||
n.parent.blacken()
|
|
||||||
if (n === n.parent.left) {
|
|
||||||
sibling.right.blacken()
|
|
||||||
n.parent.rotateLeft(this)
|
|
||||||
} else {
|
|
||||||
sibling.left.blacken()
|
|
||||||
n.parent.rotateRight(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put (v) {
|
|
||||||
var node = new N(v)
|
|
||||||
if (this.root !== null) {
|
|
||||||
var p = this.root // p abbrev. parent
|
|
||||||
while (true) {
|
|
||||||
if (node.val._id.lessThan(p.val._id)) {
|
|
||||||
if (p.left === null) {
|
|
||||||
p.left = node
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
p = p.left
|
|
||||||
}
|
|
||||||
} else if (p.val._id.lessThan(node.val._id)) {
|
|
||||||
if (p.right === null) {
|
|
||||||
p.right = node
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
p = p.right
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.val = node.val
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._fixInsert(node)
|
|
||||||
} else {
|
|
||||||
this.root = node
|
|
||||||
}
|
|
||||||
this.length++
|
|
||||||
this.root.blacken()
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
_fixInsert (n) {
|
|
||||||
if (n.parent === null) {
|
|
||||||
n.blacken()
|
|
||||||
return
|
|
||||||
} else if (n.parent.isBlack()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var uncle = n.getUncle()
|
|
||||||
if (uncle !== null && uncle.isRed()) {
|
|
||||||
// Note: parent: red, uncle: red
|
|
||||||
n.parent.blacken()
|
|
||||||
uncle.blacken()
|
|
||||||
n.grandparent.redden()
|
|
||||||
this._fixInsert(n.grandparent)
|
|
||||||
} else {
|
|
||||||
// Note: parent: red, uncle: black or null
|
|
||||||
// Now we transform the tree in such a way that
|
|
||||||
// either of these holds:
|
|
||||||
// 1) grandparent.left.isRed
|
|
||||||
// and grandparent.left.left.isRed
|
|
||||||
// 2) grandparent.right.isRed
|
|
||||||
// and grandparent.right.right.isRed
|
|
||||||
if (n === n.parent.right && n.parent === n.grandparent.left) {
|
|
||||||
n.parent.rotateLeft(this)
|
|
||||||
// Since we rotated and want to use the previous
|
|
||||||
// cases, we need to set n in such a way that
|
|
||||||
// n.parent.isRed again
|
|
||||||
n = n.left
|
|
||||||
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
|
|
||||||
n.parent.rotateRight(this)
|
|
||||||
// see above
|
|
||||||
n = n.right
|
|
||||||
}
|
|
||||||
// Case 1) or 2) hold from here on.
|
|
||||||
// Now traverse grandparent, make parent a black node
|
|
||||||
// on the highest level which holds two red nodes.
|
|
||||||
n.parent.blacken()
|
|
||||||
n.grandparent.redden()
|
|
||||||
if (n === n.parent.left) {
|
|
||||||
// Case 1
|
|
||||||
n.grandparent.rotateRight(this)
|
|
||||||
} else {
|
|
||||||
// Case 2
|
|
||||||
n.grandparent.rotateLeft(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flush () {}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import ID from './ID.js'
|
|
||||||
|
|
||||||
class ReverseOperation {
|
|
||||||
constructor (y, transaction) {
|
|
||||||
this.created = new Date()
|
|
||||||
const beforeState = transaction.beforeState
|
|
||||||
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1)
|
|
||||||
if (beforeState.has(y.userID)) {
|
|
||||||
this.fromState = new ID(y.userID, beforeState.get(y.userID))
|
|
||||||
} else {
|
|
||||||
this.fromState = this.toState
|
|
||||||
}
|
|
||||||
this.deletedStructs = transaction.deletedStructs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStructInScope (y, struct, scope) {
|
|
||||||
while (struct !== y) {
|
|
||||||
if (struct === scope) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
struct = struct._parent
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyReverseOperation (y, scope, reverseBuffer) {
|
|
||||||
let performedUndo = false
|
|
||||||
y.transact(() => {
|
|
||||||
while (!performedUndo && reverseBuffer.length > 0) {
|
|
||||||
let undoOp = reverseBuffer.pop()
|
|
||||||
// make sure that it is possible to iterate {from}-{to}
|
|
||||||
y.os.getItemCleanStart(undoOp.fromState)
|
|
||||||
y.os.getItemCleanEnd(undoOp.toState)
|
|
||||||
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
|
||||||
if (!op._deleted && isStructInScope(y, op, scope)) {
|
|
||||||
performedUndo = true
|
|
||||||
op._delete(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (let op of undoOp.deletedStructs) {
|
|
||||||
if (
|
|
||||||
isStructInScope(y, op, scope) &&
|
|
||||||
op._parent !== y &&
|
|
||||||
!op._parent._deleted &&
|
|
||||||
(
|
|
||||||
op._parent._id.user !== y.userID ||
|
|
||||||
op._parent._id.clock < undoOp.fromState.clock ||
|
|
||||||
op._parent._id.clock > undoOp.fromState.clock
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
performedUndo = true
|
|
||||||
op = op._copy(undoOp.deletedStructs)
|
|
||||||
op._integrate(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return performedUndo
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UndoManager {
|
|
||||||
constructor (scope, options = {}) {
|
|
||||||
this.options = options
|
|
||||||
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
|
||||||
this._undoBuffer = []
|
|
||||||
this._redoBuffer = []
|
|
||||||
this._scope = scope
|
|
||||||
this._undoing = false
|
|
||||||
this._redoing = false
|
|
||||||
const y = scope._y
|
|
||||||
this.y = y
|
|
||||||
y.on('afterTransaction', (y, transaction, remote) => {
|
|
||||||
if (!remote && transaction.changedParentTypes.has(scope)) {
|
|
||||||
let reverseOperation = new ReverseOperation(y, transaction)
|
|
||||||
if (!this._undoing) {
|
|
||||||
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
|
|
||||||
if (lastUndoOp !== null && reverseOperation.created - lastUndoOp.created <= options.captureTimeout) {
|
|
||||||
lastUndoOp.created = reverseOperation.created
|
|
||||||
lastUndoOp.toState = reverseOperation.toState
|
|
||||||
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
|
|
||||||
} else {
|
|
||||||
this._undoBuffer.push(reverseOperation)
|
|
||||||
}
|
|
||||||
if (!this._redoing) {
|
|
||||||
this._redoBuffer = []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._redoBuffer.push(reverseOperation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
undo () {
|
|
||||||
this._undoing = true
|
|
||||||
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
|
||||||
this._undoing = false
|
|
||||||
return performedUndo
|
|
||||||
}
|
|
||||||
redo () {
|
|
||||||
this._redoing = true
|
|
||||||
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
|
||||||
this._redoing = false
|
|
||||||
return performedRedo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
export default class YEvent {
|
|
||||||
constructor (target) {
|
|
||||||
this.target = target
|
|
||||||
this.currentTarget = target
|
|
||||||
}
|
|
||||||
get path () {
|
|
||||||
const path = []
|
|
||||||
let type = this.target
|
|
||||||
const y = type._y
|
|
||||||
while (type !== this.currentTarget && type !== y) {
|
|
||||||
let parent = type._parent
|
|
||||||
if (type._parentSub !== null) {
|
|
||||||
path.unshift(type._parentSub)
|
|
||||||
} else {
|
|
||||||
// parent is array-ish
|
|
||||||
for (let [i, child] of parent) {
|
|
||||||
if (child === type) {
|
|
||||||
path.unshift(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type = parent
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to merge all items in os with their successors.
|
|
||||||
*
|
|
||||||
* Some transformations (like delete) fragment items.
|
|
||||||
* Item(c: 'ab') + Delete(1,1) + Delete(0, 1) -> Item(c: 'a',deleted);Item(c: 'b',deleted)
|
|
||||||
*
|
|
||||||
* This functions merges the fragmented nodes together:
|
|
||||||
* Item(c: 'a',deleted);Item(c: 'b',deleted) -> Item(c: 'ab', deleted)
|
|
||||||
*
|
|
||||||
* TODO: The Tree implementation does not support deletions in-spot.
|
|
||||||
* This is why all deletions must be performed after the traversal.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function defragmentItemContent (y) {
|
|
||||||
const os = y.os
|
|
||||||
if (os.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let deletes = []
|
|
||||||
let node = os.findSmallestNode()
|
|
||||||
let next = node.next()
|
|
||||||
while (next !== null) {
|
|
||||||
let a = node.val
|
|
||||||
let b = next.val
|
|
||||||
if (
|
|
||||||
(a instanceof ItemJSON || a instanceof ItemString) &&
|
|
||||||
a.constructor === b.constructor &&
|
|
||||||
a._deleted === b._deleted &&
|
|
||||||
a._right === b &&
|
|
||||||
(new ID(a._id.user, a._id.clock + a._length)).equals(b._id)
|
|
||||||
) {
|
|
||||||
a._right = b._right
|
|
||||||
if (a instanceof ItemJSON) {
|
|
||||||
a._content = a._content.concat(b._content)
|
|
||||||
} else if (a instanceof ItemString) {
|
|
||||||
a._content += b._content
|
|
||||||
}
|
|
||||||
// delete b later
|
|
||||||
deletes.push(b._id)
|
|
||||||
// do not iterate node!
|
|
||||||
// !(node = next)
|
|
||||||
} else {
|
|
||||||
// not able to merge node, get next node
|
|
||||||
node = next
|
|
||||||
}
|
|
||||||
// update next
|
|
||||||
next = next.next()
|
|
||||||
}
|
|
||||||
for (let i = deletes.length - 1; i >= 0; i--) {
|
|
||||||
os.delete(deletes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/* global crypto */
|
|
||||||
|
|
||||||
export function generateUserID () {
|
|
||||||
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
|
|
||||||
// browser
|
|
||||||
let arr = new Uint32Array(1)
|
|
||||||
crypto.getRandomValues(arr)
|
|
||||||
return arr[0]
|
|
||||||
} else if (typeof crypto !== 'undefined' && crypto.randomBytes != null) {
|
|
||||||
// node
|
|
||||||
let buf = crypto.randomBytes(4)
|
|
||||||
return new Uint32Array(buf.buffer)[0]
|
|
||||||
} else {
|
|
||||||
return Math.ceil(Math.random() * 0xFFFFFFFF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import ID from './ID.js'
|
|
||||||
import RootID from './RootID.js'
|
|
||||||
|
|
||||||
export function getRelativePosition (type, offset) {
|
|
||||||
if (offset === 0) {
|
|
||||||
return ['startof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
|
|
||||||
} else {
|
|
||||||
let t = type._start
|
|
||||||
while (t !== null) {
|
|
||||||
if (t._deleted === false) {
|
|
||||||
if (t._length >= offset) {
|
|
||||||
return [t._id.user, t._id.clock + offset - 1]
|
|
||||||
}
|
|
||||||
if (t._right === null) {
|
|
||||||
return [t._id.user, t._id.clock + t._length - 1]
|
|
||||||
}
|
|
||||||
offset -= t._length
|
|
||||||
}
|
|
||||||
t = t._right
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromRelativePosition (y, rpos) {
|
|
||||||
if (rpos[0] === 'startof') {
|
|
||||||
let id
|
|
||||||
if (rpos[3] === null) {
|
|
||||||
id = new ID(rpos[1], rpos[2])
|
|
||||||
} else {
|
|
||||||
id = new RootID(rpos[3], rpos[4])
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: y.os.get(id),
|
|
||||||
offset: 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let offset = 0
|
|
||||||
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
|
|
||||||
const parent = struct._parent
|
|
||||||
if (parent._deleted) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!struct._deleted) {
|
|
||||||
offset = rpos[1] - struct._id.clock + 1
|
|
||||||
}
|
|
||||||
struct = struct._left
|
|
||||||
while (struct !== null) {
|
|
||||||
if (!struct._deleted) {
|
|
||||||
offset += struct._length
|
|
||||||
}
|
|
||||||
struct = struct._left
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: parent,
|
|
||||||
offset: offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import YArray from '../Type/YArray.js'
|
|
||||||
import YMap from '../Type/YMap.js'
|
|
||||||
import YText from '../Type/YText.js'
|
|
||||||
import YXmlFragment from '../Type/y-xml/YXmlFragment.js'
|
|
||||||
import YXmlElement from '../Type/y-xml/YXmlElement.js'
|
|
||||||
import YXmlText from '../Type/y-xml/YXmlText.js'
|
|
||||||
|
|
||||||
import Delete from '../Struct/Delete.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
|
|
||||||
const structs = new Map()
|
|
||||||
const references = new Map()
|
|
||||||
|
|
||||||
function addStruct (reference, structConstructor) {
|
|
||||||
structs.set(reference, structConstructor)
|
|
||||||
references.set(structConstructor, reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStruct (reference) {
|
|
||||||
return structs.get(reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReference (typeConstructor) {
|
|
||||||
return references.get(typeConstructor)
|
|
||||||
}
|
|
||||||
|
|
||||||
addStruct(0, ItemJSON)
|
|
||||||
addStruct(1, ItemString)
|
|
||||||
addStruct(2, Delete)
|
|
||||||
|
|
||||||
addStruct(3, YArray)
|
|
||||||
addStruct(4, YMap)
|
|
||||||
addStruct(5, YText)
|
|
||||||
addStruct(6, YXmlFragment)
|
|
||||||
addStruct(7, YXmlElement)
|
|
||||||
addStruct(8, YXmlText)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
import YMap from '../Type/YMap'
|
|
||||||
import YArray from '../Type/YArray'
|
|
||||||
|
|
||||||
export function writeObjectToYMap (object, type) {
|
|
||||||
for (var key in object) {
|
|
||||||
var val = object[key]
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
type.set(key, YArray)
|
|
||||||
writeArrayToYArray(val, type.get(key))
|
|
||||||
} else if (typeof val === 'object') {
|
|
||||||
type.set(key, YMap)
|
|
||||||
writeObjectToYMap(val, type.get(key))
|
|
||||||
} else {
|
|
||||||
type.set(key, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeArrayToYArray (array, type) {
|
|
||||||
for (var i = array.length - 1; i >= 0; i--) {
|
|
||||||
var val = array[i]
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
type.insert(0, [YArray])
|
|
||||||
writeArrayToYArray(val, type.get(0))
|
|
||||||
} else if (typeof val === 'object') {
|
|
||||||
type.insert(0, [YMap])
|
|
||||||
writeObjectToYMap(val, type.get(0))
|
|
||||||
} else {
|
|
||||||
type.insert(0, [val])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
736
src/Utils.js
Normal file
736
src/Utils.js
Normal file
@@ -0,0 +1,736 @@
|
|||||||
|
/* @flow */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
/*
|
||||||
|
EventHandler is an helper class for constructing custom types.
|
||||||
|
|
||||||
|
Why: When constructing custom types, you sometimes want your types to work
|
||||||
|
synchronous: E.g.
|
||||||
|
``` Synchronous
|
||||||
|
mytype.setSomething("yay")
|
||||||
|
mytype.getSomething() === "yay"
|
||||||
|
```
|
||||||
|
versus
|
||||||
|
``` Asynchronous
|
||||||
|
mytype.setSomething("yay")
|
||||||
|
mytype.getSomething() === undefined
|
||||||
|
mytype.waitForSomething().then(function(){
|
||||||
|
mytype.getSomething() === "yay"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The structures usually work asynchronously (you have to wait for the
|
||||||
|
database request to finish). EventHandler helps you to make your type
|
||||||
|
synchronous.
|
||||||
|
*/
|
||||||
|
module.exports = function (Y /* : any*/) {
|
||||||
|
Y.utils = {}
|
||||||
|
|
||||||
|
class EventListenerHandler {
|
||||||
|
constructor () {
|
||||||
|
this.eventListeners = []
|
||||||
|
}
|
||||||
|
destroy () {
|
||||||
|
this.eventListeners = null
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Basic event listener boilerplate...
|
||||||
|
*/
|
||||||
|
addEventListener (f) {
|
||||||
|
this.eventListeners.push(f)
|
||||||
|
}
|
||||||
|
removeEventListener (f) {
|
||||||
|
this.eventListeners = this.eventListeners.filter(function (g) {
|
||||||
|
return f !== g
|
||||||
|
})
|
||||||
|
}
|
||||||
|
removeAllEventListeners () {
|
||||||
|
this.eventListeners = []
|
||||||
|
}
|
||||||
|
callEventListeners (event) {
|
||||||
|
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||||
|
try {
|
||||||
|
this.eventListeners[i](event)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('User events must not throw Errors!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.EventListenerHandler = EventListenerHandler
|
||||||
|
|
||||||
|
class EventHandler extends EventListenerHandler {
|
||||||
|
/* ::
|
||||||
|
waiting: Array<Insertion | Deletion>;
|
||||||
|
awaiting: number;
|
||||||
|
onevent: Function;
|
||||||
|
eventListeners: Array<Function>;
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
onevent: is called when the structure changes.
|
||||||
|
|
||||||
|
Note: "awaiting opertations" is used to denote operations that were
|
||||||
|
prematurely called. Events for received operations can not be executed until
|
||||||
|
all prematurely called operations were executed ("waiting operations")
|
||||||
|
*/
|
||||||
|
constructor (onevent /* : Function */) {
|
||||||
|
super()
|
||||||
|
this.waiting = []
|
||||||
|
this.awaiting = 0
|
||||||
|
this.onevent = onevent
|
||||||
|
}
|
||||||
|
destroy () {
|
||||||
|
super.destroy()
|
||||||
|
this.waiting = null
|
||||||
|
this.awaiting = null
|
||||||
|
this.onevent = null
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Call this when a new operation arrives. It will be executed right away if
|
||||||
|
there are no waiting operations, that you prematurely executed
|
||||||
|
*/
|
||||||
|
receivedOp (op) {
|
||||||
|
if (this.awaiting <= 0) {
|
||||||
|
this.onevent(op)
|
||||||
|
} else if (op.struct === 'Delete') {
|
||||||
|
var self = this
|
||||||
|
var checkDelete = function checkDelete (d) {
|
||||||
|
if (d.length == null) {
|
||||||
|
throw new Error('This shouldn\'t happen! d.length must be defined!')
|
||||||
|
}
|
||||||
|
// we check if o deletes something in self.waiting
|
||||||
|
// if so, we remove the deleted operation
|
||||||
|
for (var w = 0; w < self.waiting.length; w++) {
|
||||||
|
var i = self.waiting[w]
|
||||||
|
if (i.struct === 'Insert' && i.id[0] === d.target[0]) {
|
||||||
|
var iLength = i.hasOwnProperty('content') ? i.content.length : 1
|
||||||
|
var dStart = d.target[1]
|
||||||
|
var dEnd = d.target[1] + (d.length || 1)
|
||||||
|
var iStart = i.id[1]
|
||||||
|
var iEnd = i.id[1] + iLength
|
||||||
|
// Check if they don't overlap
|
||||||
|
if (iEnd <= dStart || dEnd <= iStart) {
|
||||||
|
// no overlapping
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// we check all overlapping cases. All cases:
|
||||||
|
/*
|
||||||
|
1) iiiii
|
||||||
|
ddddd
|
||||||
|
--> modify i and d
|
||||||
|
2) iiiiiii
|
||||||
|
ddddd
|
||||||
|
--> modify i, remove d
|
||||||
|
3) iiiiiii
|
||||||
|
ddd
|
||||||
|
--> remove d, modify i, and create another i (for the right hand side)
|
||||||
|
4) iiiii
|
||||||
|
ddddddd
|
||||||
|
--> remove i, modify d
|
||||||
|
5) iiiiiii
|
||||||
|
ddddddd
|
||||||
|
--> remove both i and d (**)
|
||||||
|
6) iiiiiii
|
||||||
|
ddddd
|
||||||
|
--> modify i, remove d
|
||||||
|
7) iii
|
||||||
|
ddddddd
|
||||||
|
--> remove i, create and apply two d with checkDelete(d) (**)
|
||||||
|
8) iiiii
|
||||||
|
ddddddd
|
||||||
|
--> remove i, modify d (**)
|
||||||
|
9) iiiii
|
||||||
|
ddddd
|
||||||
|
--> modify i and d
|
||||||
|
(**) (also check if i contains content or type)
|
||||||
|
*/
|
||||||
|
// TODO: I left some debugger statements, because I want to debug all cases once in production. REMEMBER END TODO
|
||||||
|
if (iStart < dStart) {
|
||||||
|
if (dStart < iEnd) {
|
||||||
|
if (iEnd < dEnd) {
|
||||||
|
// Case 1
|
||||||
|
// remove the right part of i's content
|
||||||
|
i.content.splice(dStart - iStart)
|
||||||
|
// remove the start of d's deletion
|
||||||
|
d.length = dEnd - iEnd
|
||||||
|
d.target = [d.target[0], iEnd]
|
||||||
|
continue
|
||||||
|
} else if (iEnd === dEnd) {
|
||||||
|
// Case 2
|
||||||
|
i.content.splice(dStart - iStart)
|
||||||
|
// remove d, we do that by simply ending this function
|
||||||
|
return
|
||||||
|
} else { // (dEnd < iEnd)
|
||||||
|
// Case 3
|
||||||
|
var newI = {
|
||||||
|
id: [i.id[0], dEnd],
|
||||||
|
content: i.content.slice(dEnd - iStart),
|
||||||
|
struct: 'Insert'
|
||||||
|
}
|
||||||
|
self.waiting.push(newI)
|
||||||
|
i.content.splice(dStart - iStart)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (dStart === iStart) {
|
||||||
|
if (iEnd < dEnd) {
|
||||||
|
// Case 4
|
||||||
|
d.length = dEnd - iEnd
|
||||||
|
d.target = [d.target[0], iEnd]
|
||||||
|
i.content = []
|
||||||
|
continue
|
||||||
|
} else if (iEnd === dEnd) {
|
||||||
|
// Case 5
|
||||||
|
self.waiting.splice(w, 1)
|
||||||
|
return
|
||||||
|
} else { // (dEnd < iEnd)
|
||||||
|
// Case 6
|
||||||
|
i.content = i.content.slice(dEnd - iStart)
|
||||||
|
i.id = [i.id[0], dEnd]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else { // (dStart < iStart)
|
||||||
|
if (iStart < dEnd) {
|
||||||
|
// they overlap
|
||||||
|
/*
|
||||||
|
7) iii
|
||||||
|
ddddddd
|
||||||
|
--> remove i, create and apply two d with checkDelete(d) (**)
|
||||||
|
8) iiiii
|
||||||
|
ddddddd
|
||||||
|
--> remove i, modify d (**)
|
||||||
|
9) iiiii
|
||||||
|
ddddd
|
||||||
|
--> modify i and d
|
||||||
|
*/
|
||||||
|
if (iEnd < dEnd) {
|
||||||
|
// Case 7
|
||||||
|
// debugger // TODO: You did not test this case yet!!!! (add the debugger here)
|
||||||
|
self.waiting.splice(w, 1)
|
||||||
|
checkDelete({
|
||||||
|
target: [d.target[0], dStart],
|
||||||
|
length: iStart - dStart,
|
||||||
|
struct: 'Delete'
|
||||||
|
})
|
||||||
|
checkDelete({
|
||||||
|
target: [d.target[0], iEnd],
|
||||||
|
length: iEnd - dEnd,
|
||||||
|
struct: 'Delete'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if (iEnd === dEnd) {
|
||||||
|
// Case 8
|
||||||
|
self.waiting.splice(w, 1)
|
||||||
|
w--
|
||||||
|
d.length -= iLength
|
||||||
|
continue
|
||||||
|
} else { // dEnd < iEnd
|
||||||
|
// Case 9
|
||||||
|
d.length = iStart - dStart
|
||||||
|
i.content.splice(0, dEnd - iStart)
|
||||||
|
i.id = [i.id[0], dEnd]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// finished with remaining operations
|
||||||
|
self.waiting.push(d)
|
||||||
|
}
|
||||||
|
checkDelete(op)
|
||||||
|
} else {
|
||||||
|
this.waiting.push(op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
You created some operations, and you want the `onevent` function to be
|
||||||
|
called right away. Received operations will not be executed untill all
|
||||||
|
prematurely called operations are executed
|
||||||
|
*/
|
||||||
|
awaitAndPrematurelyCall (ops) {
|
||||||
|
this.awaiting++
|
||||||
|
ops.map(Y.utils.copyOperation).forEach(this.onevent)
|
||||||
|
}
|
||||||
|
* awaitOps (transaction, f, args) {
|
||||||
|
function notSoSmartSort (array) {
|
||||||
|
// this function sorts insertions in a executable order
|
||||||
|
var result = []
|
||||||
|
while (array.length > 0) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
var independent = true
|
||||||
|
for (var j = 0; j < array.length; j++) {
|
||||||
|
if (Y.utils.matchesId(array[j], array[i].left)) {
|
||||||
|
// array[i] depends on array[j]
|
||||||
|
independent = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (independent) {
|
||||||
|
result.push(array.splice(i, 1)[0])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
var before = this.waiting.length
|
||||||
|
// somehow create new operations
|
||||||
|
yield* f.apply(transaction, args)
|
||||||
|
// remove all appended ops / awaited ops
|
||||||
|
this.waiting.splice(before)
|
||||||
|
if (this.awaiting > 0) this.awaiting--
|
||||||
|
// if there are no awaited ops anymore, we can update all waiting ops, and send execute them (if there are still no awaited ops)
|
||||||
|
if (this.awaiting === 0 && this.waiting.length > 0) {
|
||||||
|
// update all waiting ops
|
||||||
|
for (let i = 0; i < this.waiting.length; i++) {
|
||||||
|
var o = this.waiting[i]
|
||||||
|
if (o.struct === 'Insert') {
|
||||||
|
var _o = yield* transaction.getInsertion(o.id)
|
||||||
|
if (!Y.utils.compareIds(_o.id, o.id)) {
|
||||||
|
// o got extended
|
||||||
|
o.left = [o.id[0], o.id[1] - 1]
|
||||||
|
} else if (_o.left == null) {
|
||||||
|
o.left = null
|
||||||
|
} else {
|
||||||
|
// find next undeleted op
|
||||||
|
var left = yield* transaction.getInsertion(_o.left)
|
||||||
|
while (left.deleted != null) {
|
||||||
|
if (left.left != null) {
|
||||||
|
left = yield* transaction.getInsertion(left.left)
|
||||||
|
} else {
|
||||||
|
left = null
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.left = left != null ? Y.utils.getLastId(left) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the previous stuff was async, so we have to check again!
|
||||||
|
// We also pull changes from the bindings, if there exists such a method, this could increase awaiting too
|
||||||
|
if (this._pullChanges != null) {
|
||||||
|
this._pullChanges()
|
||||||
|
}
|
||||||
|
if (this.awaiting === 0) {
|
||||||
|
// sort by type, execute inserts first
|
||||||
|
var ins = []
|
||||||
|
var dels = []
|
||||||
|
this.waiting.forEach(function (o) {
|
||||||
|
if (o.struct === 'Delete') {
|
||||||
|
dels.push(o)
|
||||||
|
} else {
|
||||||
|
ins.push(o)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.waiting = []
|
||||||
|
// put in executable order
|
||||||
|
ins = notSoSmartSort(ins)
|
||||||
|
// this.onevent can trigger the creation of another operation
|
||||||
|
// -> check if this.awaiting increased & stop computation if it does
|
||||||
|
for (var i = 0; i < ins.length; i++) {
|
||||||
|
if (this.awaiting === 0) {
|
||||||
|
this.onevent(ins[i])
|
||||||
|
} else {
|
||||||
|
this.waiting = this.waiting.concat(ins.slice(i))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < dels.length; i++) {
|
||||||
|
if (this.awaiting === 0) {
|
||||||
|
this.onevent(dels[i])
|
||||||
|
} else {
|
||||||
|
this.waiting = this.waiting.concat(dels.slice(i))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Remove awaitedInserts and awaitedDeletes in favor of awaitedOps, as they are deprecated and do not always work
|
||||||
|
// Do this in one of the coming releases that are breaking anyway
|
||||||
|
/*
|
||||||
|
Call this when you successfully awaited the execution of n Insert operations
|
||||||
|
*/
|
||||||
|
awaitedInserts (n) {
|
||||||
|
var ops = this.waiting.splice(this.waiting.length - n)
|
||||||
|
for (var oid = 0; oid < ops.length; oid++) {
|
||||||
|
var op = ops[oid]
|
||||||
|
if (op.struct === 'Insert') {
|
||||||
|
for (var i = this.waiting.length - 1; i >= 0; i--) {
|
||||||
|
let w = this.waiting[i]
|
||||||
|
// TODO: do I handle split operations correctly here? Super unlikely, but yeah..
|
||||||
|
// Also: can this case happen? Can op be inserted in the middle of a larger op that is in $waiting?
|
||||||
|
if (w.struct === 'Insert') {
|
||||||
|
if (Y.utils.matchesId(w, op.left)) {
|
||||||
|
// include the effect of op in w
|
||||||
|
w.right = op.id
|
||||||
|
// exclude the effect of w in op
|
||||||
|
op.left = w.left
|
||||||
|
} else if (Y.utils.compareIds(w.id, op.right)) {
|
||||||
|
// similar..
|
||||||
|
w.left = Y.utils.getLastId(op)
|
||||||
|
op.right = w.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Expected Insert Operation!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._tryCallEvents(n)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Call this when you successfully awaited the execution of n Delete operations
|
||||||
|
*/
|
||||||
|
awaitedDeletes (n, newLeft) {
|
||||||
|
var ops = this.waiting.splice(this.waiting.length - n)
|
||||||
|
for (var j = 0; j < ops.length; j++) {
|
||||||
|
var del = ops[j]
|
||||||
|
if (del.struct === 'Delete') {
|
||||||
|
if (newLeft != null) {
|
||||||
|
for (var i = 0; i < this.waiting.length; i++) {
|
||||||
|
let w = this.waiting[i]
|
||||||
|
// We will just care about w.left
|
||||||
|
if (w.struct === 'Insert' && Y.utils.compareIds(del.target, w.left)) {
|
||||||
|
w.left = newLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Expected Delete Operation!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._tryCallEvents(n)
|
||||||
|
}
|
||||||
|
/* (private)
|
||||||
|
Try to execute the events for the waiting operations
|
||||||
|
*/
|
||||||
|
_tryCallEvents () {
|
||||||
|
function notSoSmartSort (array) {
|
||||||
|
var result = []
|
||||||
|
while (array.length > 0) {
|
||||||
|
for (var i = 0; i < array.length; i++) {
|
||||||
|
var independent = true
|
||||||
|
for (var j = 0; j < array.length; j++) {
|
||||||
|
if (Y.utils.matchesId(array[j], array[i].left)) {
|
||||||
|
// array[i] depends on array[j]
|
||||||
|
independent = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (independent) {
|
||||||
|
result.push(array.splice(i, 1)[0])
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if (this.awaiting > 0) this.awaiting--
|
||||||
|
if (this.awaiting === 0 && this.waiting.length > 0) {
|
||||||
|
var ins = []
|
||||||
|
var dels = []
|
||||||
|
this.waiting.forEach(function (o) {
|
||||||
|
if (o.struct === 'Delete') {
|
||||||
|
dels.push(o)
|
||||||
|
} else {
|
||||||
|
ins.push(o)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ins = notSoSmartSort(ins)
|
||||||
|
ins.forEach(this.onevent)
|
||||||
|
dels.forEach(this.onevent)
|
||||||
|
this.waiting = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.EventHandler = EventHandler
|
||||||
|
|
||||||
|
/*
|
||||||
|
A wrapper for the definition of a custom type.
|
||||||
|
Every custom type must have three properties:
|
||||||
|
|
||||||
|
* struct
|
||||||
|
- Structname of this type
|
||||||
|
* initType
|
||||||
|
- Given a model, creates a custom type
|
||||||
|
* class
|
||||||
|
- the constructor of the custom type (e.g. in order to inherit from a type)
|
||||||
|
*/
|
||||||
|
class CustomType { // eslint-disable-line
|
||||||
|
/* ::
|
||||||
|
struct: any;
|
||||||
|
initType: any;
|
||||||
|
class: Function;
|
||||||
|
name: String;
|
||||||
|
*/
|
||||||
|
constructor (def) {
|
||||||
|
if (def.struct == null ||
|
||||||
|
def.initType == null ||
|
||||||
|
def.class == null ||
|
||||||
|
def.name == null
|
||||||
|
) {
|
||||||
|
throw new Error('Custom type was not initialized correctly!')
|
||||||
|
}
|
||||||
|
this.struct = def.struct
|
||||||
|
this.initType = def.initType
|
||||||
|
this.class = def.class
|
||||||
|
this.name = def.name
|
||||||
|
if (def.appendAdditionalInfo != null) {
|
||||||
|
this.appendAdditionalInfo = def.appendAdditionalInfo
|
||||||
|
}
|
||||||
|
this.parseArguments = (def.parseArguments || function () {
|
||||||
|
return [this]
|
||||||
|
}).bind(this)
|
||||||
|
this.parseArguments.typeDefinition = this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.CustomType = CustomType
|
||||||
|
|
||||||
|
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
|
||||||
|
if (v != null) {
|
||||||
|
if (v instanceof Y.utils.CustomType) return [v]
|
||||||
|
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomType) return v
|
||||||
|
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomType) return [v.typeDefinition]
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make a flat copy of an object
|
||||||
|
(just copy properties)
|
||||||
|
*/
|
||||||
|
function copyObject (o) {
|
||||||
|
var c = {}
|
||||||
|
for (var key in o) {
|
||||||
|
c[key] = o[key]
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
Y.utils.copyObject = copyObject
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copy an operation, so that it can be manipulated.
|
||||||
|
Note: You must not change subproperties (except o.content)!
|
||||||
|
*/
|
||||||
|
function copyOperation (o) {
|
||||||
|
o = copyObject(o)
|
||||||
|
if (o.content != null) {
|
||||||
|
o.content = o.content.map(function (c) { return c })
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
Y.utils.copyOperation = copyOperation
|
||||||
|
|
||||||
|
/*
|
||||||
|
Defines a smaller relation on Id's
|
||||||
|
*/
|
||||||
|
function smaller (a, b) {
|
||||||
|
return a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || typeof a[1] < typeof b[1]))
|
||||||
|
}
|
||||||
|
Y.utils.smaller = smaller
|
||||||
|
|
||||||
|
function inDeletionRange (del, ins) {
|
||||||
|
return del.target[0] === ins[0] && del.target[1] <= ins[1] && ins[1] < del.target[1] + (del.length || 1)
|
||||||
|
}
|
||||||
|
Y.utils.inDeletionRange = inDeletionRange
|
||||||
|
|
||||||
|
function compareIds (id1, id2) {
|
||||||
|
if (id1 == null || id2 == null) {
|
||||||
|
return id1 === id2
|
||||||
|
} else {
|
||||||
|
return id1[0] === id2[0] && id1[1] === id2[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.compareIds = compareIds
|
||||||
|
|
||||||
|
function matchesId (op, id) {
|
||||||
|
if (id == null || op == null) {
|
||||||
|
return id === op
|
||||||
|
} else {
|
||||||
|
if (id[0] === op.id[0]) {
|
||||||
|
if (op.content == null) {
|
||||||
|
return id[1] === op.id[1]
|
||||||
|
} else {
|
||||||
|
return id[1] >= op.id[1] && id[1] < op.id[1] + op.content.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.matchesId = matchesId
|
||||||
|
|
||||||
|
function getLastId (op) {
|
||||||
|
if (op.content == null || op.content.length === 1) {
|
||||||
|
return op.id
|
||||||
|
} else {
|
||||||
|
return [op.id[0], op.id[1] + op.content.length - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.utils.getLastId = getLastId
|
||||||
|
|
||||||
|
function createEmptyOpsArray (n) {
|
||||||
|
var a = new Array(n)
|
||||||
|
for (var i = 0; i < a.length; i++) {
|
||||||
|
a[i] = {
|
||||||
|
id: [null, null]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSmallLookupBuffer (Store) {
|
||||||
|
/*
|
||||||
|
This buffer implements a very small buffer that temporarily stores operations
|
||||||
|
after they are read / before they are written.
|
||||||
|
The buffer basically implements FIFO. Often requested lookups will be re-queued every time they are looked up / written.
|
||||||
|
|
||||||
|
It can speed up lookups on Operation Stores and State Stores. But it does not require notable use of memory or processing power.
|
||||||
|
|
||||||
|
Good for os and ss, bot not for ds (because it often uses methods that require a flush)
|
||||||
|
|
||||||
|
I tried to optimize this for performance, therefore no highlevel operations.
|
||||||
|
*/
|
||||||
|
class SmallLookupBuffer extends Store {
|
||||||
|
constructor (arg1, arg2) {
|
||||||
|
// super(...arguments) -- do this when this is supported by stable nodejs
|
||||||
|
super(arg1, arg2)
|
||||||
|
this.writeBuffer = createEmptyOpsArray(5)
|
||||||
|
this.readBuffer = createEmptyOpsArray(10)
|
||||||
|
}
|
||||||
|
* find (id, noSuperCall) {
|
||||||
|
var i, r
|
||||||
|
for (i = this.readBuffer.length - 1; i >= 0; i--) {
|
||||||
|
r = this.readBuffer[i]
|
||||||
|
// we don't have to use compareids, because id is always defined!
|
||||||
|
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
||||||
|
// found r
|
||||||
|
// move r to the end of readBuffer
|
||||||
|
for (; i < this.readBuffer.length - 1; i++) {
|
||||||
|
this.readBuffer[i] = this.readBuffer[i + 1]
|
||||||
|
}
|
||||||
|
this.readBuffer[this.readBuffer.length - 1] = r
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var o
|
||||||
|
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
|
||||||
|
r = this.writeBuffer[i]
|
||||||
|
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
||||||
|
o = r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < 0 && noSuperCall === undefined) {
|
||||||
|
// did not reach break in last loop
|
||||||
|
// read id and put it to the end of readBuffer
|
||||||
|
o = yield* super.find(id)
|
||||||
|
}
|
||||||
|
if (o != null) {
|
||||||
|
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
||||||
|
this.readBuffer[i] = this.readBuffer[i + 1]
|
||||||
|
}
|
||||||
|
this.readBuffer[this.readBuffer.length - 1] = o
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
* put (o) {
|
||||||
|
var id = o.id
|
||||||
|
var i, r // helper variables
|
||||||
|
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
|
||||||
|
r = this.writeBuffer[i]
|
||||||
|
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
||||||
|
// is already in buffer
|
||||||
|
// forget r, and move o to the end of writeBuffer
|
||||||
|
for (; i < this.writeBuffer.length - 1; i++) {
|
||||||
|
this.writeBuffer[i] = this.writeBuffer[i + 1]
|
||||||
|
}
|
||||||
|
this.writeBuffer[this.writeBuffer.length - 1] = o
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < 0) {
|
||||||
|
// did not reach break in last loop
|
||||||
|
// write writeBuffer[0]
|
||||||
|
var write = this.writeBuffer[0]
|
||||||
|
if (write.id[0] !== null) {
|
||||||
|
yield* super.put(write)
|
||||||
|
}
|
||||||
|
// put o to the end of writeBuffer
|
||||||
|
for (i = 0; i < this.writeBuffer.length - 1; i++) {
|
||||||
|
this.writeBuffer[i] = this.writeBuffer[i + 1]
|
||||||
|
}
|
||||||
|
this.writeBuffer[this.writeBuffer.length - 1] = o
|
||||||
|
}
|
||||||
|
// check readBuffer for every occurence of o.id, overwrite if found
|
||||||
|
// whether found or not, we'll append o to the readbuffer
|
||||||
|
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
||||||
|
r = this.readBuffer[i + 1]
|
||||||
|
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
||||||
|
this.readBuffer[i] = o
|
||||||
|
} else {
|
||||||
|
this.readBuffer[i] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.readBuffer[this.readBuffer.length - 1] = o
|
||||||
|
}
|
||||||
|
* delete (id) {
|
||||||
|
var i, r
|
||||||
|
for (i = 0; i < this.readBuffer.length; i++) {
|
||||||
|
r = this.readBuffer[i]
|
||||||
|
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
||||||
|
this.readBuffer[i] = {
|
||||||
|
id: [null, null]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield* this.flush()
|
||||||
|
yield* super.delete(id)
|
||||||
|
}
|
||||||
|
* findWithLowerBound (id) {
|
||||||
|
var o = yield* this.find(id, true)
|
||||||
|
if (o != null) {
|
||||||
|
return o
|
||||||
|
} else {
|
||||||
|
yield* this.flush()
|
||||||
|
return yield* super.findWithLowerBound.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* findWithUpperBound (id) {
|
||||||
|
var o = yield* this.find(id, true)
|
||||||
|
if (o != null) {
|
||||||
|
return o
|
||||||
|
} else {
|
||||||
|
yield* this.flush()
|
||||||
|
return yield* super.findWithUpperBound.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* findNext () {
|
||||||
|
yield* this.flush()
|
||||||
|
return yield* super.findNext.apply(this, arguments)
|
||||||
|
}
|
||||||
|
* findPrev () {
|
||||||
|
yield* this.flush()
|
||||||
|
return yield* super.findPrev.apply(this, arguments)
|
||||||
|
}
|
||||||
|
* iterate () {
|
||||||
|
yield* this.flush()
|
||||||
|
yield* super.iterate.apply(this, arguments)
|
||||||
|
}
|
||||||
|
* flush () {
|
||||||
|
for (var i = 0; i < this.writeBuffer.length; i++) {
|
||||||
|
var write = this.writeBuffer[i]
|
||||||
|
if (write.id[0] !== null) {
|
||||||
|
yield* super.put(write)
|
||||||
|
this.writeBuffer[i] = {
|
||||||
|
id: [null, null]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SmallLookupBuffer
|
||||||
|
}
|
||||||
|
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
||||||
|
}
|
||||||
174
src/Y.js
174
src/Y.js
@@ -1,174 +0,0 @@
|
|||||||
import DeleteStore from './Store/DeleteStore.js'
|
|
||||||
import OperationStore from './Store/OperationStore.js'
|
|
||||||
import StateStore from './Store/StateStore.js'
|
|
||||||
import { generateUserID } from './Util/generateUserID.js'
|
|
||||||
import RootID from './Util/RootID.js'
|
|
||||||
import NamedEventHandler from './Util/NamedEventHandler.js'
|
|
||||||
import UndoManager from './Util/UndoManager.js'
|
|
||||||
|
|
||||||
import { messageToString, messageToRoomname } from './MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
import Connector from './Connector.js'
|
|
||||||
import Persistence from './Persistence.js'
|
|
||||||
import YArray from './Type/YArray.js'
|
|
||||||
import YMap from './Type/YMap.js'
|
|
||||||
import YText from './Type/YText.js'
|
|
||||||
import { YXmlFragment, YXmlElement, YXmlText } from './Type/y-xml/y-xml.js'
|
|
||||||
import BinaryDecoder from './Binary/Decoder.js'
|
|
||||||
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
|
|
||||||
|
|
||||||
import debug from 'debug'
|
|
||||||
import Transaction from './Transaction.js'
|
|
||||||
|
|
||||||
export default class Y extends NamedEventHandler {
|
|
||||||
constructor (opts) {
|
|
||||||
super()
|
|
||||||
this._opts = opts
|
|
||||||
this.userID = opts._userID != null ? opts._userID : generateUserID()
|
|
||||||
this.share = {}
|
|
||||||
this.ds = new DeleteStore(this)
|
|
||||||
this.os = new OperationStore(this)
|
|
||||||
this.ss = new StateStore(this)
|
|
||||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
|
||||||
if (opts.persistence != null) {
|
|
||||||
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
|
|
||||||
this.persistence.retrieveContent()
|
|
||||||
} else {
|
|
||||||
this.persistence = null
|
|
||||||
}
|
|
||||||
this.connected = true
|
|
||||||
this._missingStructs = new Map()
|
|
||||||
this._readyToIntegrate = []
|
|
||||||
this._transaction = null
|
|
||||||
}
|
|
||||||
_beforeChange () {}
|
|
||||||
transact (f, remote = false) {
|
|
||||||
let initialCall = this._transaction === null
|
|
||||||
if (initialCall) {
|
|
||||||
this._transaction = new Transaction(this)
|
|
||||||
this.emit('beforeTransaction', this, this._transaction, remote)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
f(this)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
if (initialCall) {
|
|
||||||
this.emit('beforeObserverCalls', this, this._transaction, remote)
|
|
||||||
const transaction = this._transaction
|
|
||||||
this._transaction = null
|
|
||||||
// emit change events on changed types
|
|
||||||
transaction.changedTypes.forEach(function (subs, type) {
|
|
||||||
if (!type._deleted) {
|
|
||||||
type._callObserver(transaction, subs, remote)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
transaction.changedParentTypes.forEach(function (events, type) {
|
|
||||||
if (!type._deleted) {
|
|
||||||
events = events
|
|
||||||
.filter(event =>
|
|
||||||
!event.target._deleted
|
|
||||||
)
|
|
||||||
events
|
|
||||||
.forEach(event => {
|
|
||||||
event.currentTarget = type
|
|
||||||
})
|
|
||||||
// we don't have to check for events.length
|
|
||||||
// because there is no way events is empty..
|
|
||||||
type._deepEventHandler.callEventListeners(transaction, events)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// when all changes & events are processed, emit afterTransaction event
|
|
||||||
this.emit('afterTransaction', this, transaction, remote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// fake _start for root properties (y.set('name', type))
|
|
||||||
get _start () {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
set _start (start) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
get room () {
|
|
||||||
return this._opts.connector.room
|
|
||||||
}
|
|
||||||
define (name, TypeConstructor) {
|
|
||||||
let id = new RootID(name, TypeConstructor)
|
|
||||||
let type = this.os.get(id)
|
|
||||||
if (this.share[name] === undefined) {
|
|
||||||
this.share[name] = type
|
|
||||||
} else if (this.share[name] !== type) {
|
|
||||||
throw new Error('Type is already defined with a different constructor')
|
|
||||||
}
|
|
||||||
return type
|
|
||||||
}
|
|
||||||
get (name) {
|
|
||||||
return this.share[name]
|
|
||||||
}
|
|
||||||
disconnect () {
|
|
||||||
if (this.connected) {
|
|
||||||
this.connected = false
|
|
||||||
return this.connector.disconnect()
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reconnect () {
|
|
||||||
if (!this.connected) {
|
|
||||||
this.connected = true
|
|
||||||
return this.connector.reconnect()
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this.share = null
|
|
||||||
if (this.connector.destroy != null) {
|
|
||||||
this.connector.destroy()
|
|
||||||
} else {
|
|
||||||
this.connector.disconnect()
|
|
||||||
}
|
|
||||||
this.os = null
|
|
||||||
this.ds = null
|
|
||||||
this.ss = null
|
|
||||||
}
|
|
||||||
whenSynced () {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.once('synced', () => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.extend = function extendYjs () {
|
|
||||||
for (var i = 0; i < arguments.length; i++) {
|
|
||||||
var f = arguments[i]
|
|
||||||
if (typeof f === 'function') {
|
|
||||||
f(Y)
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected a function!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The following assignments should be moved to yjs-dist
|
|
||||||
Y.AbstractConnector = Connector
|
|
||||||
Y.Persisence = Persistence
|
|
||||||
Y.Array = YArray
|
|
||||||
Y.Map = YMap
|
|
||||||
Y.Text = YText
|
|
||||||
Y.XmlElement = YXmlElement
|
|
||||||
Y.XmlFragment = YXmlFragment
|
|
||||||
Y.XmlText = YXmlText
|
|
||||||
|
|
||||||
Y.utils = {
|
|
||||||
BinaryDecoder,
|
|
||||||
UndoManager,
|
|
||||||
getRelativePosition,
|
|
||||||
fromRelativePosition
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.debug = debug
|
|
||||||
debug.formatters.Y = messageToString
|
|
||||||
debug.formatters.y = messageToRoomname
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
import Y from './Y.js'
|
|
||||||
export default Y
|
|
||||||
186
src/y.js
Normal file
186
src/y.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/* @flow */
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
require('./Connector.js')(Y)
|
||||||
|
require('./Database.js')(Y)
|
||||||
|
require('./Transaction.js')(Y)
|
||||||
|
require('./Struct.js')(Y)
|
||||||
|
require('./Utils.js')(Y)
|
||||||
|
require('./Connectors/Test.js')(Y)
|
||||||
|
|
||||||
|
var requiringModules = {}
|
||||||
|
|
||||||
|
module.exports = Y
|
||||||
|
Y.requiringModules = requiringModules
|
||||||
|
|
||||||
|
Y.extend = function (name, value) {
|
||||||
|
if (value instanceof Y.utils.CustomType) {
|
||||||
|
Y[name] = value.parseArguments
|
||||||
|
} else {
|
||||||
|
Y[name] = value
|
||||||
|
}
|
||||||
|
if (requiringModules[name] != null) {
|
||||||
|
requiringModules[name].resolve()
|
||||||
|
delete requiringModules[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Y.requestModules = requestModules
|
||||||
|
function requestModules (modules) {
|
||||||
|
// determine if this module was compiled for es5 or es6 (y.js vs. y.es6)
|
||||||
|
// if Insert.execute is a Function, then it isnt a generator..
|
||||||
|
// then load the es5(.js) files..
|
||||||
|
var extention = typeof regeneratorRuntime !== 'undefined' ? '.js' : '.es6'
|
||||||
|
var promises = []
|
||||||
|
for (var i = 0; i < modules.length; i++) {
|
||||||
|
var module = modules[i].split('(')[0]
|
||||||
|
var modulename = 'y-' + module.toLowerCase()
|
||||||
|
if (Y[module] == null) {
|
||||||
|
if (requiringModules[module] == null) {
|
||||||
|
// module does not exist
|
||||||
|
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
|
||||||
|
var imported = document.createElement('script')
|
||||||
|
imported.src = Y.sourceDir + '/' + modulename + '/' + modulename + extention
|
||||||
|
document.head.appendChild(imported)
|
||||||
|
|
||||||
|
let requireModule = {}
|
||||||
|
requiringModules[module] = requireModule
|
||||||
|
requireModule.promise = new Promise(function (resolve) {
|
||||||
|
requireModule.resolve = resolve
|
||||||
|
})
|
||||||
|
promises.push(requireModule.promise)
|
||||||
|
} else {
|
||||||
|
console.info('YJS: Please do not depend on automatic requiring of modules anymore! Extend modules as follows `require(\'y-modulename\')(Y)`')
|
||||||
|
require(modulename)(Y)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
promises.push(requiringModules[modules[i]].promise)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ::
|
||||||
|
type MemoryOptions = {
|
||||||
|
name: 'memory'
|
||||||
|
}
|
||||||
|
type IndexedDBOptions = {
|
||||||
|
name: 'indexeddb',
|
||||||
|
namespace: string
|
||||||
|
}
|
||||||
|
type DbOptions = MemoryOptions | IndexedDBOptions
|
||||||
|
|
||||||
|
type WebRTCOptions = {
|
||||||
|
name: 'webrtc',
|
||||||
|
room: string
|
||||||
|
}
|
||||||
|
type WebsocketsClientOptions = {
|
||||||
|
name: 'websockets-client',
|
||||||
|
room: string
|
||||||
|
}
|
||||||
|
type ConnectionOptions = WebRTCOptions | WebsocketsClientOptions
|
||||||
|
|
||||||
|
type YOptions = {
|
||||||
|
connector: ConnectionOptions,
|
||||||
|
db: DbOptions,
|
||||||
|
types: Array<TypeName>,
|
||||||
|
sourceDir: string,
|
||||||
|
share: {[key: string]: TypeName}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||||
|
opts.types = opts.types != null ? opts.types : []
|
||||||
|
var modules = [opts.db.name, opts.connector.name].concat(opts.types)
|
||||||
|
for (var name in opts.share) {
|
||||||
|
modules.push(opts.share[name])
|
||||||
|
}
|
||||||
|
Y.sourceDir = opts.sourceDir
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
setTimeout(function () {
|
||||||
|
Y.requestModules(modules).then(function () {
|
||||||
|
if (opts == null) reject('An options object is expected! ')
|
||||||
|
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
||||||
|
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
||||||
|
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
||||||
|
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
||||||
|
else if (opts.share == null) reject('You must specify a set of shared types!')
|
||||||
|
else {
|
||||||
|
var yconfig = new YConfig(opts)
|
||||||
|
yconfig.db.whenUserIdSet(function () {
|
||||||
|
yconfig.init(function () {
|
||||||
|
resolve(yconfig)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(reject)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
class YConfig {
|
||||||
|
/* ::
|
||||||
|
db: Y.AbstractDatabase;
|
||||||
|
connector: Y.AbstractConnector;
|
||||||
|
share: {[key: string]: any};
|
||||||
|
options: Object;
|
||||||
|
*/
|
||||||
|
constructor (opts, callback) {
|
||||||
|
this.options = opts
|
||||||
|
this.db = new Y[opts.db.name](this, opts.db)
|
||||||
|
this.connector = new Y[opts.connector.name](this, opts.connector)
|
||||||
|
}
|
||||||
|
init (callback) {
|
||||||
|
var opts = this.options
|
||||||
|
var share = {}
|
||||||
|
this.share = share
|
||||||
|
this.db.requestTransaction(function * requestTransaction () {
|
||||||
|
// create shared object
|
||||||
|
for (var propertyname in opts.share) {
|
||||||
|
var typeConstructor = opts.share[propertyname].split('(')
|
||||||
|
var typeName = typeConstructor.splice(0, 1)
|
||||||
|
var args = []
|
||||||
|
if (typeConstructor.length === 1) {
|
||||||
|
try {
|
||||||
|
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var type = Y[typeName]
|
||||||
|
var typedef = type.typeDefinition
|
||||||
|
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
||||||
|
share[propertyname] = yield* this.createType(type.apply(typedef, args), id)
|
||||||
|
}
|
||||||
|
this.store.whenTransactionsFinished()
|
||||||
|
.then(callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isConnected () {
|
||||||
|
return this.connector.isSynced
|
||||||
|
}
|
||||||
|
disconnect () {
|
||||||
|
return this.connector.disconnect()
|
||||||
|
}
|
||||||
|
reconnect () {
|
||||||
|
return this.connector.reconnect()
|
||||||
|
}
|
||||||
|
destroy () {
|
||||||
|
if (this.connector.destroy != null) {
|
||||||
|
this.connector.destroy()
|
||||||
|
} else {
|
||||||
|
this.connector.disconnect()
|
||||||
|
}
|
||||||
|
var self = this
|
||||||
|
this.db.requestTransaction(function * () {
|
||||||
|
yield* self.db.destroy()
|
||||||
|
self.connector = null
|
||||||
|
self.db = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.Y = Y
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user