Compare commits

..

1 Commits

Author SHA1 Message Date
Kevin Jahns
9374c363c3 Release v11.2.6 (hot fix for #54) 2016-09-28 13:35:09 +02:00
60 changed files with 1913 additions and 14036 deletions

View File

@@ -1,12 +0,0 @@
{
"presets": [
["latest", {
"es2015": {
"modules": false
}
}]
],
"plugins": [
"external-helpers"
]
}

14
.gitignore vendored
View File

@@ -1,3 +1,15 @@
node_modules
bower_components
/y.*
build
build_test
.directory
.codio
.settings
.jshintignore
.jshintrc
.validate.json
/y.js
/y.js.map
/y-*
.vscode
jsconfig.json

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "dist"]
path = dist
url = https://github.com/y-js/yjs.git
branch = dist

333
README.md
View File

@@ -1,216 +1,133 @@
# ![Yjs](http://y-js.org/images/yjs.png)
Yjs is a framework for offline-first p2p shared editing on structured data like
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
most of the complexity of concurrent editing. For additional information, demos,
and tutorials visit [y-js.org](http://y-js.org/).
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data.
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data
like Text, Json, and XML. We also provide support for storing and manipulating your shared data offline.
For more information and demo applications visit our [homepage](http://y-js.org/).
### Extensions
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
* *Connector* - a communication protocol that propagates changes to the clients
* *Database* - a database to store your changes
* one or more *Types* - that represent the shared data
Connectors, Databases, and Types are available as modules that extend Yjs. Here
is a list of the modules we know of:
##### Connectors
|Name | Description |
|----------------|-----------------------------------|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
##### Database adapters
|Name | Description |
|----------------|-----------------------------------|
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
##### Types
You can create you own shared types easily.
Therefore, you can design the structure of your custom type,
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data).
We already provide abstract data types for
| Name | Description |
|----------|-------------------|
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to 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/)|
##### 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 |
|-----------|-------------------|
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
We support several communication protocols as so called *Connectors*.
You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors).
Currently, we support the following communication protocols:
|Name | Description |
|----------------|-----------------------------------|
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|[websockets](https://github.com/y-js/y-websockets-client) | Exchange updates efficiently in the classical client-server model |
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
You are not limited to use a specific database to store the shared data. We provide the following database adapters:
|Name | Description |
|----------------|-----------------------------------|
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
The advantages over similar frameworks are support for
* .. P2P message propagation and arbitrary communication protocols
* .. share any type of data. The types provide a convenient interface
* .. offline support: Changes are stored persistently and only relevant changes are propagated on rejoin
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline. Every type has a notion on how we define Intention Preservation on it.
## Use it!
Install Yjs, and its modules with [bower](http://bower.io/), or
[npm](https://www.npmjs.org/package/yjs).
Install yjs and its modules with [bower](http://bower.io/), or with [npm](https://www.npmjs.org/package/yjs).
### 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
missing modules.
Then you include the libraries directly from the installation folder.
```
<script src="./bower_components/yjs/y.js"></script>
```
### 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!
(Same goes for other module systems)
And use it like this with *npm*:
```
var Y = require('yjs')
require('y-array')(Y) // add the y-array type to Yjs
require('y-websockets-client')(Y)
require('y-memory')(Y)
require('y-array')(Y)
require('y-map')(Y)
require('y-text')(Y)
// ..
// do the same for all modules you want to use
```
### ES6 Syntax
```
import Y from 'yjs'
import yArray from 'y-array'
import yWebsocketsClient from 'y-webrtc'
import yMemory from 'y-memory'
import yArray from 'y-array'
import yMap from 'y-map'
import yText from 'y-text'
// ..
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
Y = require("yjs");
```
# 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
```HTML
<!DOCTYPE html>
<html>
<body>
<script src="./bower_components/yjs/y.js"></script>
<!-- Yjs automatically includes all missing dependencies (browser only) -->
<script>
Y({
db: {
name: 'memory' // use memory database adapter.
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
},
connector: {
name: 'webrtc', // use webrtc connector
// name: 'websockets-client'
// name: 'xmpp'
room: 'my-room' // clients connecting to the same room share data
},
sourceDir: '/bower_components', // location of the y-* modules (browser only)
share: {
textarea: 'Text' // y.share.textarea is of type y-text
}
}).then(function (y) {
// The Yjs instance `y` is available
// y.share.* contains the shared types
// Bind `y.share.textarea` to `<textarea/>`
y.share.textarea.bind(document.querySelector('textarea'))
})
</script>
<textarea></textarea>
</body>
</html>
```
## Get Help & Give Help
There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
Report _any_ issues to the
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
soon, if possible.
# API
# Api
### Y(options)
* Y.extend(module1, module2, ..)
* Add extensions to Y
* `Y.extend(require('y-webrtc'))` has the same semantics as
`require('y-webrtc')(Y)`
* options.db
* Will be forwarded to the database adapter. Specify the database adaper on
`options.db.name`.
* Have a look at the used database adapter repository to see all available
options.
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`.
* Have a look at the used database adapter repository to see all available options.
* options.connector
* Will be forwarded to the connector adapter. Specify the connector adaper on
`options.connector.name`.
* All our connectors implement a `room` property. Clients that specify the
same room share the same data.
* All of our connectors specify an `url` property that defines the connection
endpoint of the used connector.
* All of our connectors also have a default connection endpoint that you can
use for development.
* 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.
* Will be forwarded to the connector adapter. Specify the connector adaper on `options.connector.name`.
* All our connectors implement a `room` property. Clients that specify the same room share the same data.
* All of our connectors specify an `url` property that defines the connection endpoint of the used connector.
* All of our connectors also have a default connection endpoint that you can use for development.
* Have a look at the used connector repository to see all available options.
* *Only if you know what you are doing:* Set
`options.connector.preferUntransformed = true` in order receive the shared
data untransformed. This is very efficient as the database content is simply
copied to this client. This does only work if this client receives content
from only one client.
* options.sourceDir (browser only)
* Path where all y-* modules are stored
* options.sourceDir
* Path where all y-* modules are stored.
* Defaults to `/bower_components`
* 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')
// 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-webrtc')(Y)
require('y-map')(Y)
// ..
```
```
* options.share
* Specify on `options.share[arbitraryName]` types that are shared among all
users.
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and
create an y-array type on `y.share[arbitraryName]`.
* If userA doesn't specify `options.share[arbitraryName]`, it won't be
available for userA.
* If userB specifies `options.share[arbitraryName]`, it still won't be
available for userA. But all the updates are send from userB to userA.
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
Instead, they are merged among all users. This feature is only available on
`y.share.*`
* Weird behavior: It is supported that two users specify different types with
the same property name.
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies
`options.share.x = 'Text'`. But they 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.*`
* Specify on `options.share[arbitraryName]` types that are shared among all users.
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and create an Y.Array type on `y.share[arbitraryName]`.
* If userA doesn't specify `options.share[arbitraryName]`, it won't be available for userA.
* If userB specifies `options.share[arbitraryName]`, it still won't be available for userA. But all the updates are send from userB to userA.
* In contrast to Y.Map, types on `y.share.*` cannot be overwritten or deleted. Instead, they are merged among all users. This feature is only available on `y.share.*`
* Weird behavior: It is supported that two users specify different types with the same property name.
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies `options.share.x = 'Text'`. But they'll only share data if they specified the same type with the same property name
* options.type
* Array of modules that Yjs needs to require, before instantiating a shared type.
* By default Yjs requires the specified database adapter, the specified connector, and all modules that are used in `options.share.*`
* Put all types here that you intend to use, but are not used in y.share.*
### Instantiated Y object (y)
@@ -220,8 +137,7 @@ require('y-map')(Y)
* The specified database adapter is loaded
* The specified connector is loaded
* All types are included
* The connector is initialized, and a unique user id is set (received from the
server)
* The connector is initialized, and a unique user id is set (received from the server)
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
The promise returns an instance of Y. We denote it with a lower case `y`.
@@ -239,57 +155,70 @@ The promise returns an instance of Y. We denote it with a lower case `y`.
* y.connector.disconnect()
* Force to disconnect this instance from the other instances
* y.connector.reconnect()
* Try to reconnect to the other instances (needs to be supported by the
connector)
* Not supported by y-xmpp
* y.close()
* Try to reconnect to the other instances (needs to be supported by the connector)
* Not supported by y-xmpp
* y.destroy()
* Destroy this object.
* Destroys all types (they will throw weird errors if you still use them)
* Disconnects from the other instances (via connector)
* Returns a promise
* y.destroy()
* calls y.close()
* Removes all data from the database
* Returns a promise
* y.db.stopGarbageCollector()
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
collection
* y.db.gc :: Boolean
* Whether gc is turned on
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage collection
* y.db.gcTimeout :: Number (defaults to 50000 ms)
* Time interval between two garbage collect cycles
* It is required that all instances exchanged all messages after two garbage
collect cycles (after 100000 ms per default)
* It is required that all instances exchanged all messages after two garbage collect cycles (after 100000 ms per default)
* y.db.userId :: String
* The used user id for this client. **Never overwrite this**
### Logging
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
`y*` enables logging for all y-* components. You can selectively remove
components you are not interested in: E.g. The flag `y*,-y:connector-message`
will not log the long `y:connector-message` messages.
## Get help
There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who may help you with your problem, and answer your questions.
##### Enable logging in Node.js
```sh
DEBUG=y* node app.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.
If you want to see an issue fixed, please subscribe to the thread (or remind me via gitter).
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
```js
localStorage.debug = 'y*'
```
## Changelog
### 11.0.0
* **All types now return a single event instead of list of events**
* Insert events contain a list of values
* Improved performance for large insertions & deletions
* Several bugfixes (offline editing related)
* Native support for node 4 (see #49)
### 10.0.0
* Support for more complex types (a type can be a composition of several types)
* Fixes several memory leaks
### 9.0.0
There were several rolling updates from 0.6 to 0.8. We consider Yjs stable since a long time,
and intend to continue stable releases. From this release forward y-* modules will implement peer-dependencies for npm, and dependencies for bower.
Furthermore, incompatible yjs instances will now throw errors when syncing - this feature was influenced by #48. The versioning jump was influenced by react (see [here](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html))
### 0.6.0
This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 0.6.0 it is possible to work asynchronously on a persistent database, which enables offline support.
* Switched to semver versioning
* Requires a promise implementation in environment (es6 promises suffice, included in all the major browsers). Otherwise you have to include a polyfill
* Y.Object has been renamed to Y.Map
* Y.Map exchanges `.val(name [, value])` in favor of `.set(name, value)` and `.get(name)`
* Y.Map `.get(name)` returns a promise, if the value is a custom type
* The Connector definition slightly changed (I'll update the wiki)
* The Type definitions completely changed, so you have to rewrite them (I'll rewrite the article in the wiki)
* Support for several packaging systems
* Flowtype
## Contribution
I created this framework during my bachelor thesis at the chair of computer
science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since
December 2014 I'm working on Yjs as a part of my student worker job at the i5.
I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
## License
Yjs is licensed under the [MIT License](./LICENSE).
Yjs is licensed under the [MIT License](./LICENSE.txt).
<yjs@dbis.rwth-aachen.de>
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki

72
declarations/Structs.js Normal file
View 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
View File

34
declarations/Y.js Normal file
View 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

Submodule dist added at 47bcec8bc7

View File

@@ -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>

View File

@@ -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)
})

View File

@@ -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"
}
}

View File

@@ -1,23 +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-array/y-array.js"></script>
<script src="../../../y-map/dist/y-map.js"></script>
<script src="../../../y-text/dist/y-text.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,73 +0,0 @@
/* global Y, chat */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'chat-example'
},
sourceDir: '/bower_components',
share: {
chat: 'Array'
}
}).then(function (y) {
window.yChat = y
// This functions inserts a message at the specified position in the DOM
function appendMessage (message, position) {
var p = document.createElement('p')
var uname = document.createElement('span')
uname.appendChild(document.createTextNode(message.username + ': '))
p.appendChild(uname)
p.appendChild(document.createTextNode(message.message))
document.querySelector('#chat').insertBefore(p, chat.children[position] || null)
}
// This function makes sure that only 7 messages exist in the chat history.
// The rest is deleted
function cleanupChat () {
if (y.share.chat.length > 7) {
y.share.chat.delete(0, y.chat.length - 7)
}
}
// Insert the initial content
y.share.chat.toArray().forEach(appendMessage)
cleanupChat()
// whenever content changes, make sure to reflect the changes in the DOM
y.share.chat.observe(function (event) {
if (event.type === 'insert') {
for (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++) {
chat.children[event.index].remove()
}
}
// concurrent insertions may result in a history > 7, so cleanup here
cleanupChat()
})
document.querySelector('#chatform').onsubmit = function (event) {
// the form is submitted
var message = {
username: this.querySelector('[name=username]').value,
message: this.querySelector('[name=message]').value
}
if (message.username.length > 0 && message.message.length > 0) {
if (y.share.chat.length > 6) {
// If we are goint to insert the 8th element, make sure to delete first.
y.share.chat.delete(0)
}
// Here we insert a message in the shared chat type.
// This will call the observe function (see line 40)
// and reflect the change in the DOM
y.share.chat.push([message])
this.querySelector('[name=message]').value = ''
}
// Do not send this form!
event.preventDefault()
return false
}
})

View File

@@ -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>

View File

@@ -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)
})

View File

@@ -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>

View File

@@ -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
}
})

View File

@@ -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>

View File

@@ -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 + ')'
})
})
})
})

View File

@@ -1,24 +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-array/y-array.js"></script>
<script src="../bower_components/y-text/y-text.js"></script>
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
<script src="../bower_components/y-memory/y-memory.js"></script>
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -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)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
{
"name": "examples",
"version": "0.0.0",
"description": "",
"author": "Kevin Jahns",
"license": "MIT",
"dependencies": {
"monaco-editor": "^0.8.3"
},
"devDependencies": {
"standard": "^10.0.2"
},
"standard": {
"ignore": ["bower_components"]
}
}

View File

@@ -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>

View File

@@ -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)
})

View File

@@ -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>

View File

@@ -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)
})

View File

@@ -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'
)

View File

@@ -1,12 +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/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>

View File

@@ -1,30 +0,0 @@
/* global Y */
// eslint-disable-next-line
let search = new URLSearchParams(location.search)
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'https://yjs-v13.herokuapp.com/'
},
sourceDir: '/bower_components',
share: {
textarea: 'Text', // y.share.textarea is of type Y.Text
test: 'Array'
},
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'))
// thats it..
}).catch(() => {
console.log('Something went wrong while creating the instance..')
})

View File

@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
</head>
<script src="../bower_components/yjs/y.js"></script>
<script src="../bower_components/jquery/dist/jquery.min.js"></script>
<script src="./index.js"></script>
</head>
<body>
<h1> Shared DOM Example </h1>
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
</div>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
</div>
<div class="command">
<button type="button">Execute</button>
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
</div>
<script>
var commands = document.querySelectorAll(".command");
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
var execute = function(){
eval(command.querySelector("input").value);
}
command.querySelector("button").onclick = execute
$(command.querySelector("input")).keyup(function (e) {
if (e.keyCode == 13) {
execute()
}
})
})
</script>
</body>
</html>

View File

@@ -1,21 +0,0 @@
/* global Y */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Xml-example'
},
sourceDir: '/bower_components',
share: {
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
}
}).then(function (y) {
window.yXml = y
// bind xml type to a dom, and put it in body
window.sharedDom = y.share.xml.getDom()
document.body.appendChild(window.sharedDom)
})

202
gulpfile.helper.js Normal file
View 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
View 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')
}
}))
})

3649
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,24 @@
{
"name": "yjs",
"version": "13.0.0-15",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",
"module": "./src/y.js",
"version": "11.2.6",
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
"main": "./src/y.js",
"scripts": {
"test": "npm run lint",
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
"lint": "standard",
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
"postversion": "npm run dist",
"postpublish": "tag-dist-files --overwrite-existing-tag"
"test": "node --harmony ./node_modules/.bin/gulp test",
"lint": "./node_modules/.bin/standard"
},
"files": [
"y.*"
"pre-commit": [
"lint",
"test"
],
"standard": {
"parser": "babel-eslint",
"ignore": [
"/y.js",
"/y.js.map"
"build/**",
"dist/**",
"declarations/**",
"./y.js",
"./y.js.map"
]
},
"repository": {
@@ -43,27 +42,38 @@
},
"homepage": "http://y-js.org",
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-plugin-external-helpers": "^6.22.0",
"babel-plugin-transform-regenerator": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-latest": "^6.24.1",
"chance": "^1.0.9",
"concurrently": "^3.4.0",
"cutest": "^0.1.9",
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-inject": "^2.0.0",
"rollup-plugin-multi-entry": "^2.0.1",
"rollup-plugin-node-resolve": "^3.0.0",
"rollup-plugin-uglify": "^1.0.2",
"rollup-regenerator-runtime": "^6.23.1",
"rollup-watch": "^3.2.2",
"standard": "^10.0.2",
"tag-dist-files": "^0.1.6"
"babel-eslint": "^5.0.0-beta6",
"babel-plugin-transform-runtime": "^6.1.18",
"babel-preset-es2015": "^6.1.18",
"babelify": "^7.2.0",
"browserify": "^12.0.1",
"eslint": "^1.10.2",
"gulp": "^3.9.0",
"gulp-bump": "^1.0.0",
"gulp-concat": "^2.6.0",
"gulp-filter": "^3.0.1",
"gulp-git": "^1.6.0",
"gulp-if": "^2.0.0",
"gulp-jasmine": "^2.0.1",
"gulp-jasmine-browser": "^0.2.3",
"gulp-load-plugins": "^1.0.0",
"gulp-prompt": "^0.1.2",
"gulp-rename": "^1.2.2",
"gulp-serve": "^1.2.0",
"gulp-shell": "^0.5.1",
"gulp-sourcemaps": "^1.5.2",
"gulp-tag-version": "^1.3.0",
"gulp-uglify": "1.4.*",
"gulp-util": "^3.0.6",
"gulp-watch": "^4.3.5",
"minimist": "^1.2.0",
"pre-commit": "^1.1.1",
"regenerator": "^0.8.42",
"run-sequence": "^1.1.4",
"seedrandom": "^2.4.2",
"standard": "^5.2.2",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
"dependencies": {
"debug": "^2.6.8",
"utf-8": "^1.0.0"
}
"dependencies": {}
}

View File

@@ -1,47 +0,0 @@
import inject from 'rollup-plugin-inject'
import babel from 'rollup-plugin-babel'
import uglify from 'rollup-plugin-uglify'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
entry: 'src/y.js',
moduleName: 'Y',
format: 'umd',
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs(),
babel({
runtimeHelpers: true
}),
inject({
regeneratorRuntime: 'regenerator-runtime'
}),
uglify({
output: {
comments: function (node, comment) {
var text = comment.value
var type = comment.type
if (type === 'comment2') {
// multiline comment
return /@license/i.test(text)
}
}
}
})
],
dest: 'y.js',
sourceMap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}

View File

@@ -1,26 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
entry: 'src/y.js',
moduleName: 'Y',
format: 'umd',
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs()
],
dest: 'y.node.js',
sourceMap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}
* @version v${pkg.version}
* @license ${pkg.license}
*/
`
}

View File

@@ -1,20 +0,0 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
entry: 'test/*',
moduleName: 'y-tests',
format: 'umd',
plugins: [
nodeResolve({
main: true,
module: true,
browser: true
}),
commonjs(),
multiEntry()
],
dest: 'y.test.js',
sourceMap: true
}

View File

@@ -1,23 +1,38 @@
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
import { sendSyncStep1, computeMessageSyncStep1, computeMessageSyncStep2, computeMessageUpdate } from './MessageHandler.js'
/* @flow */
'use strict'
export default function extendConnector (Y/* :any */) {
module.exports = function (Y/* :any */) {
class AbstractConnector {
/* ::
y: YConfig;
role: SyncRole;
connections: Object;
isSynced: boolean;
userEventListeners: Array<Function>;
whenSyncedListeners: Array<Function>;
currentSyncTarget: ?UserId;
syncingClients: Array<UserId>;
forwardToSyncingClients: boolean;
debug: boolean;
broadcastedHB: boolean;
syncStep2: Promise;
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 = {}
}
this.opts = opts
// Prefer to receive untransformed operations. This does only work if
// this client receives operations from only one other client.
// In particular, this does not work with y-webrtc.
// It will work with y-websockets-client
this.preferUntransformed = opts.preferUntransformed || false
if (opts.role == null || opts.role === 'master') {
this.role = 'master'
} else if (opts.role === 'slave') {
@@ -25,78 +40,63 @@ export default function extendConnector (Y/* :any */) {
} else {
throw new Error("Role must be either 'master' or 'slave'!")
}
this.log = Y.debug('y:connector')
this.logMessage = Y.debug('y:connector-message')
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
this.role = opts.role
this.connections = new Map()
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.authInfo = opts.auth || null
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
if (opts.generateUserId !== false) {
this.setUserId(Y.utils.generateUserId())
}
}
reconnect () {
this.log('reconnecting..')
return this.y.db.startGarbageCollector()
}
disconnect () {
this.log('discronnecting..')
this.connections = new Map()
this.connections = {}
this.isSynced = false
this.currentSyncTarget = null
this.broadcastedHB = false
this.syncingClients = []
this.whenSyncedListeners = []
this.y.db.stopGarbageCollector()
return this.y.db.whenTransactionsFinished()
return this.y.db.stopGarbageCollector()
}
repair () {
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
console.info('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
for (var name in this.connections) {
this.connections[name].isSynced = false
}
this.isSynced = false
this.connections.forEach((user, userId) => {
user.isSynced = false
this._syncWithUser(userId)
})
this.currentSyncTarget = null
this.broadcastedHB = false
this.findNextSyncTarget()
}
setUserId (userId) {
if (this.userId == null) {
if (!Number.isInteger(userId)) {
let err = new Error('UserId must be an integer!')
this.y.emit('error', err)
throw err
}
this.log('Set userId to "%s"', userId)
this.userId = userId
return this.y.db.setUserId(userId)
} else {
return null
}
}
onUserEvent (f) {
this.userEventListeners.push(f)
}
removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
}
userLeft (user) {
if (this.connections.has(user)) {
this.log('%s: User left %s', this.userId, user)
this.connections.delete(user)
// check if isSynced event can be sent now
this._setSyncedWith(null)
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',
@@ -105,25 +105,17 @@ export default function extendConnector (Y/* :any */) {
}
}
}
userJoined (user, role, auth) {
userJoined (user, role) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
}
if (this.connections.has(user)) {
if (this.connections[user] != null) {
throw new Error('This user already joined!')
}
this.log('%s: User joined %s', this.userId, user)
this.connections.set(user, {
uid: user,
this.connections[user] = {
isSynced: false,
role: role,
processAfterAuth: [],
auth: auth || null,
receivedSyncStep2: false
})
let defer = {}
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
this.connections.get(user).syncStep2 = defer
role: role
}
for (var f of this.userEventListeners) {
f({
action: 'userJoined',
@@ -131,7 +123,9 @@ export default function extendConnector (Y/* :any */) {
role: role
})
}
this._syncWithUser(user)
if (this.currentSyncTarget == null) {
this.findNextSyncTarget()
}
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
@@ -142,39 +136,53 @@ export default function extendConnector (Y/* :any */) {
this.whenSyncedListeners.push(f)
}
}
_syncWithUser (userid) {
if (this.role === 'slave') {
return // "The current sync has not finished or this is controlled by a master!"
/*
returns false, if there is no sync target
true otherwise
*/
findNextSyncTarget () {
if (this.currentSyncTarget != null || this.isSynced) {
return // "The current sync has not finished!"
}
sendSyncStep1(this, userid)
}
_fireIsSyncedListeners () {
this.y.db.whenTransactionsFinished().then(() => {
if (!this.isSynced) {
this.isSynced = true
// It is safer to remove this!
// TODO: remove: yield * this.garbageCollectAfterSync()
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 this.whenSyncedListeners) {
for (var f of conn.whenSyncedListeners) {
f()
}
this.whenSyncedListeners = []
}
})
}
send (uid, buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
conn.whenSyncedListeners = []
})
}
this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid)
this.logMessage('Message: %Y', buffer)
}
broadcast (buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
send (uid, message) {
if (this.debug) {
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, message) // eslint-disable-line
}
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
this.logMessage('Message: %Y', buffer)
}
/*
Buffer operations, and broadcast them when ready.
@@ -186,23 +194,20 @@ export default function extendConnector (Y/* :any */) {
var self = this
function broadcastOperations () {
if (self.broadcastOpBuffer.length > 0) {
let encoder = new BinaryEncoder()
encoder.writeVarString(self.opts.room)
encoder.writeVarString('update')
let ops = self.broadcastOpBuffer
self.broadcast({
type: 'update',
ops: self.broadcastOpBuffer
})
self.broadcastOpBuffer = []
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)
}
self.broadcast(encoder.createBuffer())
}
}
if (this.broadcastOpBuffer.length === 0) {
this.broadcastOpBuffer = ops
this.y.db.whenTransactionsFinished().then(broadcastOperations)
if (this.y.db.transactionInProgress) {
this.y.db.whenTransactionsFinished().then(broadcastOperations)
} else {
setTimeout(broadcastOperations, 0)
}
} else {
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
}
@@ -210,81 +215,120 @@ export default function extendConnector (Y/* :any */) {
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender, buffer, skipAuth) {
skipAuth = skipAuth || false
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
}
receiveMessage (sender/* :UserId */, message/* :Message */) {
if (sender === this.userId) {
return Promise.resolve()
return
}
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('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
this.logMessage('Message: %Y', buffer)
if (senderConn == null && !skipAuth) {
throw new Error('Received message from unknown peer!')
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)
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, this.y, sender).then(authPermissions => {
if (senderConn.auth == null) {
senderConn.auth = authPermissions
this.y.emit('userAuthenticated', {
user: senderConn.uid,
auth: authPermissions
})
}
let messages = senderConn.processAfterAuth
senderConn.processAfterAuth = []
return messages.reduce((p, m) =>
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
, Promise.resolve())
var ds = yield* this.getDeleteSet()
var ops = yield* this.getOperations(m.stateSet)
conn.send(sender, {
type: 'sync step 2',
os: ops,
stateSet: currentStateSet,
deleteSet: ds,
protocolVersion: this.protocolVersion
})
if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender)
setTimeout(function () {
conn.syncingClients = conn.syncingClients.filter(function (cli) {
return cli !== sender
})
conn.send(sender, {
type: 'sync done'
})
}, 5000) // TODO: conn.syncingClientDuration)
} else {
conn.send(sender, {
type: 'sync done'
})
}
conn._setSyncedWith(sender)
})
} else if (message.type === 'sync step 2') {
let conn = this
var broadcastHB = !this.broadcastedHB
this.broadcastedHB = true
var db = this.y.db
var defer = {}
defer.promise = new Promise(function (resolve) {
defer.resolve = resolve
})
this.syncStep2 = defer.promise
let m /* :MessageSyncStep2 */ = message
db.requestTransaction(function * () {
yield* this.applyDeleteSet(m.deleteSet)
this.store.apply(m.os)
db.requestTransaction(function * () {
var ops = yield* this.getOperations(m.stateSet)
if (ops.length > 0) {
if (!broadcastHB) { // TODO: consider to broadcast here..
conn.send(sender, {
type: 'update',
ops: ops
})
} else {
// 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 (skipAuth || senderConn.auth != null) {
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
} else {
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
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)
}
}
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
return this.y.db.whenTransactionsFinished()
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
} else {
return Promise.reject(new Error('Unable to receive message'))
}
}
_setSyncedWith (user) {
if (user != null) {
this.connections.get(user).isSynced = true
var conn = this.connections[user]
if (conn != null) {
conn.isSynced = true
}
let conns = Array.from(this.connections.values())
if (conns.length > 0 && conns.every(u => u.isSynced)) {
this._fireIsSyncedListeners()
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

165
src/Connectors/Test.js Normal file
View 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
}

View File

@@ -1,7 +1,7 @@
/* @flow */
'use strict'
export default function extendDatabase (Y /* :any */) {
module.exports = function (Y /* :any */) {
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
@@ -39,15 +39,13 @@ export default function extendDatabase (Y /* :any */) {
*/
constructor (y, opts) {
this.y = y
opts.gc = opts.gc === true
this.dbOpts = opts
var os = this
this.userId = null
var resolve_
this.userIdPromise = new Promise(function (resolve) {
resolve_ = resolve
var resolve
this.userIdPromise = new Promise(function (r) {
resolve = r
})
this.userIdPromise.resolve = resolve_
this.userIdPromise.resolve = resolve
// whether to broadcast all applied operations (insert & delete hook)
this.forwardAppliedOperations = false
// E.g. this.listenersById[id] : Array<Listener>
@@ -72,24 +70,24 @@ export default function extendDatabase (Y /* :any */) {
this.waitingTransactions = []
this.transactionInProgress = false
this.transactionIsFlushed = false
if (typeof YConcurrencyTestingMode !== 'undefined') {
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.gcTimeout > 0 && (os.gc1.length > 0 || os.gc2.length > 0)) {
if (!os.y.connector.isSynced) {
console.warn('gc should be empty when not synced!')
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)
yield* this.garbageCollectOperation(oid)
}
os.gc2 = os.gc1
os.gc1 = []
@@ -111,23 +109,13 @@ export default function extendDatabase (Y /* :any */) {
})
}
this.garbageCollect = garbageCollect
this.startGarbageCollector()
if (this.gcTimeout > 0) {
garbageCollect()
}
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
this.opsReceivedTimestamp = new Date()
this.startRepairCheck()
}
startGarbageCollector () {
this.gc = this.dbOpts.gc
if (this.gc) {
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
} else {
this.gcTimeout = -1
}
if (this.gcTimeout > 0) {
this.garbageCollect()
}
}
startRepairCheck () {
var os = this
if (this.repairCheckInterval > 0) {
@@ -161,7 +149,7 @@ export default function extendDatabase (Y /* :any */) {
clearInterval(this.repairCheckIntervalHandler)
}
queueGarbageCollector (id) {
if (this.y.connector.isSynced && this.gc) {
if (this.y.isConnected()) {
this.gc1.push(id)
}
}
@@ -178,7 +166,7 @@ export default function extendDatabase (Y /* :any */) {
})
}
addToDebug () {
if (typeof YConcurrencyTestingMode !== 'undefined') {
if (typeof YConcurrency_TestingMode !== 'undefined') {
var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
if (typeof s === 'string') {
return s
@@ -194,18 +182,16 @@ export default function extendDatabase (Y /* :any */) {
}
stopGarbageCollector () {
var self = this
this.gc = false
this.gcTimeout = -1
return new Promise(function (resolve) {
self.requestTransaction(function * () {
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i = 0; i < ungc.length; i++) {
var op = yield * this.getOperation(ungc[i])
var op = yield* this.getOperation(ungc[i])
if (op != null) {
delete op.gc
yield * this.setOperation(op)
yield* this.setOperation(op)
}
}
resolve()
@@ -218,7 +204,7 @@ export default function extendDatabase (Y /* :any */) {
TODO: rename this function
Rulez:
* Only gc if this user is online & gc turned on
* Only gc if this user is online
* The most left element in a list must not be gc'd.
=> There is at least one element in the list
@@ -227,20 +213,18 @@ export default function extendDatabase (Y /* :any */) {
* addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
this.store.gc &&
this.store.y.connector.isSynced
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])
op = yield* this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
gc = true
}
if (gc) {
op.gc = true
yield * this.setOperation(op)
yield* this.setOperation(op)
this.store.queueGarbageCollector(op.id)
return true
}
@@ -255,7 +239,10 @@ export default function extendDatabase (Y /* :any */) {
this.gc2 = this.gc2.filter(filter)
delete op.gc
}
destroyTypes () {
* destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
this.stopRepairCheck()
for (var key in this.initializedTypes) {
var type = this.initializedTypes[key]
if (type._destroy != null) {
@@ -265,18 +252,13 @@ export default function extendDatabase (Y /* :any */) {
}
}
}
* destroy () {
clearTimeout(this.gcInterval)
this.gcInterval = null
this.stopRepairCheck()
}
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)
var state = yield* this.getState(userId)
self.opClock = state.clock
self.userIdPromise.resolve(userId)
})
@@ -306,12 +288,10 @@ export default function extendDatabase (Y /* :any */) {
* check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied
*/
applyOperations (decoder) {
apply (ops) {
this.opsReceivedTimestamp = new Date()
let length = decoder.readUint32()
for (var i = 0; i < length; i++) {
let o = Y.Struct.binaryDecodeOperation(decoder)
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) {
@@ -366,7 +346,7 @@ export default function extendDatabase (Y /* :any */) {
for (let key = 0; key < exeNow.length; key++) {
let o = exeNow[key].op
yield * store.tryExecute.call(this, o)
yield* store.tryExecute.call(this, o)
}
for (var sid in ls) {
@@ -374,9 +354,9 @@ export default function extendDatabase (Y /* :any */) {
var id = JSON.parse(sid)
var op
if (typeof id[1] === 'string') {
op = yield * this.getOperation(id)
op = yield* this.getOperation(id)
} else {
op = yield * this.getInsertion(id)
op = yield* this.getInsertion(id)
}
if (op == null) {
store.listenersById[sid] = l
@@ -385,7 +365,7 @@ export default function extendDatabase (Y /* :any */) {
let listener = l[i]
let o = listener.op
if (--listener.missing === 0) {
yield * store.tryExecute.call(this, o)
yield* store.tryExecute.call(this, o)
}
}
}
@@ -403,14 +383,14 @@ export default function extendDatabase (Y /* :any */) {
whenOperationsExist: any;
*/
* tryExecute (op) {
this.store.addToDebug('yield * this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
if (op.struct === 'Delete') {
yield * Y.Struct.Delete.execute.call(this, op)
yield* Y.Struct.Delete.execute.call(this, op)
// this is now called in Transaction.deleteOperation!
// yield * this.store.operationAdded(this, op)
// yield* this.store.operationAdded(this, op)
} else {
// check if this op was defined
var defined = yield * this.getInsertion(op.id)
var defined = yield* this.getInsertion(op.id)
while (defined != null && defined.content != null) {
// 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) {
@@ -419,23 +399,23 @@ export default function extendDatabase (Y /* :any */) {
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
defined = yield* this.getOperation(op.id) // getOperation suffices here
} else {
break
}
}
if (defined == null) {
var opid = op.id
var isGarbageCollected = yield * this.isGarbageCollected(opid)
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)
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)
op = yield* this.getOperation(opid)
// if insertion, try to combine with left
yield * this.tryCombineWithLeft(op)
yield* this.tryCombineWithLeft(op)
}
}
}
@@ -454,13 +434,14 @@ export default function extendDatabase (Y /* :any */) {
*/
* operationAdded (transaction, op) {
if (op.struct === 'Delete') {
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
var target = yield* transaction.getInsertion(op.target)
var type = this.initializedTypes[JSON.stringify(target.parent)]
if (type != null) {
yield * type._changed(transaction, op)
yield* type._changed(transaction, op)
}
} else {
// increase SS
yield * transaction.updateState(op.id[0])
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)
@@ -480,9 +461,9 @@ export default function extendDatabase (Y /* :any */) {
// if parent is deleted, mark as gc'd and return
if (op.parent != null) {
var parentIsDeleted = yield * transaction.isDeleted(op.parent)
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
if (parentIsDeleted) {
yield * transaction.deleteList(op.id)
yield* transaction.deleteList(op.id)
return
}
}
@@ -490,7 +471,7 @@ export default function extendDatabase (Y /* :any */) {
// notify parent, if it was instanciated as a custom type
if (t != null) {
let o = Y.utils.copyOperation(op)
yield * t._changed(transaction, o)
yield* t._changed(transaction, o)
}
if (!op.deleted) {
// Delete if DS says this is actually deleted
@@ -499,13 +480,13 @@ export default function extendDatabase (Y /* :any */) {
// 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)
var opIsDeleted = yield* transaction.isDeleted(id)
if (opIsDeleted) {
var delop = {
struct: 'Delete',
target: id
}
yield * this.tryExecute.call(transaction, delop)
yield* this.tryExecute.call(transaction, delop)
}
}
}
@@ -514,16 +495,18 @@ export default function extendDatabase (Y /* :any */) {
whenTransactionsFinished () {
if (this.transactionInProgress) {
if (this.transactionsFinished == null) {
var resolve_
var promise = new Promise(function (resolve) {
resolve_ = resolve
var resolve
var promise = new Promise(function (r) {
resolve = r
})
this.transactionsFinished = {
resolve: resolve_,
resolve: resolve,
promise: promise
}
return promise
} else {
return this.transactionsFinished.promise
}
return this.transactionsFinished.promise
} else {
return Promise.resolve()
}
@@ -543,7 +526,7 @@ export default function extendDatabase (Y /* :any */) {
} else {
this.transactionIsFlushed = true
return function * () {
yield * this.flush()
yield* this.flush()
}
}
} else {
@@ -560,48 +543,6 @@ export default function extendDatabase (Y /* :any */) {
}, 0)
}
}
/*
Get a created/initialized type.
*/
getType (id) {
return this.initializedTypes[JSON.stringify(id)]
}
/*
Init type. This is called when a remote operation is retrieved, and transformed to a type
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
*/
* initType (id, args) {
var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid]
if (t == null) {
var op/* :MapStruct | ListStruct */ = yield * this.getOperation(id)
if (op != null) {
t = yield * Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
this.store.initializedTypes[sid] = t
}
}
return t
}
/*
Create type. This is called when the local user creates a type (which is a synchronous action)
*/
createType (typedefinition, id) {
var structname = typedefinition[0].struct
id = id || this.getNextOpId(1)
var op = Y.Struct[structname].create(id)
op.type = typedefinition[0].name
this.requestTransaction(function * () {
if (op.id[0] === 0xFFFFFF) {
yield * this.setOperation(op)
} else {
yield * this.applyCreatedOperations([op])
}
})
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])
this.initializedTypes[JSON.stringify(op.id)] = t
return t
}
}
Y.AbstractDatabase = AbstractDatabase
}

View File

@@ -17,141 +17,141 @@ for (let database of databases) {
})
afterEach(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
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]]})
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]]})
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]]})
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]]})
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]]})
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]]})
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]]})
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]]})
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]]})
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]]})
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()
})
}))
@@ -167,54 +167,54 @@ for (let database of databases) {
})
afterEach(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
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()
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')
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()
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()
})
})
@@ -232,9 +232,9 @@ for (let database of databases) {
var r = Math.random()
if (r < 0.8) {
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
if (!(yield * this.os.find(obj))) {
if (!(yield* this.os.find(obj))) {
elements.push(obj)
yield * this.os.put({id: obj})
yield* this.os.put({id: obj})
}
} else if (elements.length > 0) {
var elemid = Math.floor(Math.random() * elements.length)
@@ -242,7 +242,7 @@ for (let database of databases) {
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
yield * this.os.delete(elem)
yield* this.os.delete(elem)
}
}
done()
@@ -250,14 +250,14 @@ for (let database of databases) {
})
afterAll(function (done) {
store.requestTransaction(function * () {
yield * this.store.destroy()
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)
expect((yield* this.os.find(id)).id).toEqual(id)
}
done()
})
@@ -266,7 +266,7 @@ for (let database of databases) {
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)
var e = yield* this.os.findWithLowerBound(id)
expect(e.id).toEqual(id)
}
done()
@@ -281,7 +281,7 @@ for (let database of databases) {
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, null, function * (val) {
yield* this.os.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
@@ -297,7 +297,7 @@ for (let database of databases) {
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, null, function * (val) {
yield* this.os.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
@@ -314,7 +314,7 @@ for (let database of databases) {
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, null, upperBound, function * (val) {
yield* this.os.iterate(this, null, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
@@ -340,7 +340,7 @@ for (let database of databases) {
}).length
var actualResults = 0
store.requestTransaction(function * () {
yield * this.os.iterate(this, lowerBound, upperBound, function * (val) {
yield* this.os.iterate(this, lowerBound, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})

View File

@@ -1,152 +0,0 @@
import utf8 from 'utf-8'
const bits7 = 0b1111111
const bits8 = 0b11111111
export class BinaryEncoder {
constructor () {
this.data = []
}
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])
}
}
writeOpID (id) {
let user = id[0]
this.writeVarUint(user)
if (user !== 0xFFFFFF) {
this.writeVarUint(id[1])
} else {
this.writeVarString(id[1])
}
}
}
export 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
}
skip8 () {
this.pos++
}
readUint8 () {
return this.uint8arr[this.pos++]
}
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
}
peekUint8 () {
return this.uint8arr[this.pos]
}
readVarUint () {
let num = 0
let len = 0
while (true) {
let r = this.uint8arr[this.pos++]
num = num | ((r & bits7) << len)
len += 7
if (r < 1 << 7) {
return num >>> 0 // return unsigned number!
}
if (len > 35) {
throw new Error('Integer out of range!')
}
}
}
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)
}
peekVarString () {
let pos = this.pos
let s = this.readVarString()
this.pos = pos
return s
}
readOpID () {
let user = this.readVarUint()
if (user !== 0xFFFFFF) {
return [user, this.readVarUint()]
} else {
return [user, this.readVarString()]
}
}
}

View File

@@ -1,193 +0,0 @@
import Y from './y.js'
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
export function formatYjsMessage (buffer) {
let decoder = new BinaryDecoder(buffer)
decoder.readVarString() // read roomname
let type = decoder.readVarString()
let strBuilder = []
strBuilder.push('\n === ' + type + ' ===\n')
if (type === 'update') {
logMessageUpdate(decoder, strBuilder)
} else if (type === 'sync step 1') {
logMessageSyncStep1(decoder, strBuilder)
} else if (type === 'sync step 2') {
logMessageSyncStep2(decoder, strBuilder)
} else {
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
}
return strBuilder.join('')
}
export function formatYjsMessageType (buffer) {
let decoder = new BinaryDecoder(buffer)
decoder.readVarString() // roomname
return decoder.readVarString()
}
export function logMessageUpdate (decoder, strBuilder) {
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n')
}
}
export function computeMessageUpdate (decoder, encoder, conn) {
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
let messagePosition = decoder.pos
let len = decoder.readUint32()
let delops = []
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder)
if (op.struct === 'Delete') {
delops.push(op)
}
}
if (delops.length > 0) {
if (conn.y.db.forwardAppliedOperations) {
conn.broadcastOps(delops)
}
if (conn.y.persistence) {
conn.y.persistence.saveOperations(delops)
}
}
decoder.pos = messagePosition
}
conn.y.db.applyOperations(decoder)
}
export function sendSyncStep1 (conn, syncUser) {
conn.y.db.requestTransaction(function * () {
let encoder = new BinaryEncoder()
encoder.writeVarString(conn.opts.room || '')
encoder.writeVarString('sync step 1')
encoder.writeVarString(conn.authInfo || '')
encoder.writeVarUint(conn.protocolVersion)
let preferUntransformed = conn.preferUntransformed && this.os.length === 0 // TODO: length may not be defined
encoder.writeUint8(preferUntransformed ? 1 : 0)
yield * this.writeStateSet(encoder)
conn.send(syncUser, encoder.createBuffer())
})
}
export function logMessageSyncStep1 (decoder, strBuilder) {
let auth = decoder.readVarString()
let protocolVersion = decoder.readVarUint()
let preferUntransformed = decoder.readUint8() === 1
strBuilder.push(`
- auth: "${auth}"
- protocolVersion: ${protocolVersion}
- preferUntransformed: ${preferUntransformed}
`)
logSS(decoder, strBuilder)
}
export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
let protocolVersion = decoder.readVarUint()
let preferUntransformed = decoder.readUint8() === 1
// check protocol version
if (protocolVersion !== conn.protocolVersion) {
console.warn(
`You tried to sync with a yjs instance that has a different protocol version
(You: ${protocolVersion}, Client: ${protocolVersion}).
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
`)
conn.y.destroy()
}
return conn.y.db.whenTransactionsFinished().then(() => {
// send sync step 2
conn.y.db.requestTransaction(function * () {
encoder.writeVarString('sync step 2')
encoder.writeVarString(conn.authInfo || '')
if (preferUntransformed) {
encoder.writeUint8(1)
yield * this.writeOperationsUntransformed(encoder)
} else {
encoder.writeUint8(0)
yield * this.writeOperations(encoder, decoder)
}
yield * this.writeDeleteSet(encoder)
conn.send(senderConn.uid, encoder.createBuffer())
senderConn.receivedSyncStep2 = true
})
return conn.y.db.whenTransactionsFinished().then(() => {
if (conn.role === 'slave') {
sendSyncStep1(conn, sender)
}
})
})
}
export function logSS (decoder, strBuilder) {
strBuilder.push(' == SS: \n')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
strBuilder.push(` ${user}: ${clock}\n`)
}
}
export function logOS (decoder, strBuilder) {
strBuilder.push(' == OS: \n')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder)
strBuilder.push(JSON.stringify(op) + '\n')
}
}
export function logDS (decoder, strBuilder) {
strBuilder.push(' == DS: \n')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint()
strBuilder.push(` User: ${user}: `)
let len2 = decoder.readVarUint()
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 logMessageSyncStep2 (decoder, strBuilder) {
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n')
let osTransformed = decoder.readUint8() === 1
strBuilder.push(' - osUntransformed: ' + osTransformed + '\n')
logOS(decoder, strBuilder)
if (osTransformed) {
logSS(decoder, strBuilder)
}
logDS(decoder, strBuilder)
}
export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
var db = conn.y.db
let defer = senderConn.syncStep2
// apply operations first
db.requestTransaction(function * () {
let osUntransformed = decoder.readUint8()
if (osUntransformed === 1) {
yield * this.applyOperationsUntransformed(decoder)
} else {
this.store.applyOperations(decoder)
}
})
// then apply ds
db.requestTransaction(function * () {
yield * this.applyDeleteSet(decoder)
})
return db.whenTransactionsFinished().then(() => {
conn._setSyncedWith(sender)
defer.resolve()
})
}

View File

@@ -1,43 +0,0 @@
import { BinaryEncoder } from './Encoding.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
this.y.db.whenTransactionsFinished().then(saveOperations)
} else {
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
}
}
}
Y.AbstractPersistence = AbstractPersistence
}

385
src/SpecHelper.js Normal file
View 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

View File

@@ -1,7 +1,5 @@
const CDELETE = 0
const CINSERT = 1
const CLIST = 2
const CMAP = 3
/* @flow */
'use strict'
/*
An operation also defines the structure of a type. This is why operation and
@@ -21,22 +19,8 @@ const CMAP = 3
* requiredOps
- Operations that are required to execute this operation.
*/
export default function extendStruct (Y) {
module.exports = function (Y/* :any */) {
var Struct = {
binaryDecodeOperation: function (decoder) {
let code = decoder.peekUint8()
if (code === CDELETE) {
return Y.Struct.Delete.binaryDecode(decoder)
} else if (code === CINSERT) {
return Y.Struct.Insert.binaryDecode(decoder)
} else if (code === CLIST) {
return Y.Struct.List.binaryDecode(decoder)
} else if (code === CMAP) {
return Y.Struct.Map.binaryDecode(decoder)
} else {
throw new Error('Unable to decode operation!')
}
},
/* 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
@@ -46,30 +30,13 @@ export default function extendStruct (Y) {
*/
Delete: {
encode: function (op) {
return {
target: op.target,
length: op.length || 0,
struct: 'Delete'
}
},
binaryEncode: function (encoder, op) {
encoder.writeUint8(CDELETE)
encoder.writeOpID(op.target)
encoder.writeVarUint(op.length || 0)
},
binaryDecode: function (decoder) {
decoder.skip8()
return {
target: decoder.readOpID(),
length: decoder.readVarUint(),
struct: 'Delete'
}
return op
},
requiredOps: function (op) {
return [] // [op.target]
},
execute: function * (op) {
return yield * this.deleteOperation(op.target, op.length || 1)
return yield* this.deleteOperation(op.target, op.length || 1)
}
},
Insert: {
@@ -106,91 +73,6 @@ export default function extendStruct (Y) {
return e
},
binaryEncode: function (encoder, op) {
encoder.writeUint8(CINSERT)
// compute info property
let contentIsText = op.content != null && op.content.every(c => typeof c === 'string' && c.length === 1)
let originIsLeft = Y.utils.compareIds(op.left, op.origin)
let info =
(op.parentSub != null ? 1 : 0) |
(op.opContent != null ? 2 : 0) |
(contentIsText ? 4 : 0) |
(originIsLeft ? 8 : 0) |
(op.left != null ? 16 : 0) |
(op.right != null ? 32 : 0) |
(op.origin != null ? 64 : 0)
encoder.writeUint8(info)
encoder.writeOpID(op.id)
encoder.writeOpID(op.parent)
if (info & 16) {
encoder.writeOpID(op.left)
}
if (info & 32) {
encoder.writeOpID(op.right)
}
if (!originIsLeft && info & 64) {
encoder.writeOpID(op.origin)
}
if (info & 1) {
// write parentSub
encoder.writeVarString(op.parentSub)
}
if (info & 2) {
// write opContent
encoder.writeOpID(op.opContent)
} else if (info & 4) {
// write text
encoder.writeVarString(op.content.join(''))
} else {
// convert to JSON and write
encoder.writeVarString(JSON.stringify(op.content))
}
},
binaryDecode: function (decoder) {
let op = {
struct: 'Insert'
}
decoder.skip8()
// get info property
let info = decoder.readUint8()
op.id = decoder.readOpID()
op.parent = decoder.readOpID()
if (info & 16) {
op.left = decoder.readOpID()
} else {
op.left = null
}
if (info & 32) {
op.right = decoder.readOpID()
} else {
op.right = null
}
if (info & 8) {
// origin is left
op.origin = op.left
} else if (info & 64) {
op.origin = decoder.readOpID()
} else {
op.origin = null
}
if (info & 1) {
// has parentSub
op.parentSub = decoder.readVarString()
}
if (info & 2) {
// has opContent
op.opContent = decoder.readOpID()
} else if (info & 4) {
// has pure text content
op.content = decoder.readVarString().split('')
} else {
// has mixed content
let s = decoder.readVarString()
op.content = JSON.parse(s)
}
return op
},
requiredOps: function (op) {
var ids = []
if (op.left != null) {
@@ -215,13 +97,13 @@ export default function extendStruct (Y) {
return 0
} else {
var d = 0
var o = yield * this.getInsertion(op.left)
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)
o = yield* this.getInsertion(o.left)
}
}
return d
@@ -252,17 +134,17 @@ export default function extendStruct (Y) {
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)
var origin = yield* this.getInsertionCleanEnd(op.origin)
if (origin.originOf == null) {
origin.originOf = []
}
origin.originOf.push(op.id)
yield * this.setOperation(origin)
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)
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
@@ -271,29 +153,29 @@ export default function extendStruct (Y) {
// find o. o is the first conflicting operation
if (op.left != null) {
o = yield * this.getInsertionCleanEnd(op.left)
o = yield* this.getInsertionCleanEnd(op.left)
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
// only if not added previously
tryToRemergeLater.push(o.right)
}
o = (o.right == null) ? null : yield * this.getOperation(o.right)
o = (o.right == null) ? null : yield* this.getOperation(o.right)
} else { // left == null
parent = yield * this.getOperation(op.parent)
parent = yield* this.getOperation(op.parent)
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
start = startId == null ? null : yield * this.getOperation(startId)
start = startId == null ? null : yield* this.getOperation(startId)
o = start
}
// 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)
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)
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
if (oOriginDistance === i) {
// case 1
if (o.id[0] < op.id[0]) {
@@ -311,7 +193,7 @@ export default function extendStruct (Y) {
}
i++
if (o.right != null) {
o = yield * this.getInsertion(o.right)
o = yield* this.getInsertion(o.right)
} else {
o = null
}
@@ -324,17 +206,17 @@ export default function extendStruct (Y) {
var left = null
var right = null
if (parent == null) {
parent = yield * this.getOperation(op.parent)
parent = yield* this.getOperation(op.parent)
}
// reconnect left and set right of op
if (op.left != null) {
left = yield * this.getInsertion(op.left)
left = yield* this.getInsertion(op.left)
// link left
op.right = left.right
left.right = op.id
yield * this.setOperation(left)
yield* this.setOperation(left)
} else {
// set op.right from parent, if necessary
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
@@ -342,33 +224,33 @@ export default function extendStruct (Y) {
// reconnect right
if (op.right != null) {
// TODO: wanna connect right too?
right = yield * this.getOperation(op.right)
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)
right = yield* this.getInsertionCleanEnd(right.id)
}
this.store.removeFromGarbageCollector(right)
}
yield * this.setOperation(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)
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)
yield* this.deleteOperation(op.right, 1, true)
}
if (op.left != null) {
yield * this.deleteOperation(op.id, 1, true)
yield* this.deleteOperation(op.id, 1, true)
}
} else {
if (right == null || left == null) {
@@ -378,14 +260,14 @@ export default function extendStruct (Y) {
if (left == null) {
parent.start = op.id
}
yield * this.setOperation(parent)
yield* this.setOperation(parent)
}
}
// try to merge original op.left and op.origin
for (i = 0; i < tryToRemergeLater.length; i++) {
var m = yield * this.getOperation(tryToRemergeLater[i])
yield * this.tryCombineWithLeft(m)
for (let i = 0; i < tryToRemergeLater.length; i++) {
var m = yield* this.getOperation(tryToRemergeLater[i])
yield* this.tryCombineWithLeft(m)
}
}
},
@@ -413,31 +295,14 @@ export default function extendStruct (Y) {
id: op.id,
type: op.type
}
if (op.requires != null) {
e.requires = op.requires
}
if (op.info != null) {
e.info = op.info
}
return e
},
binaryEncode: function (encoder, op) {
encoder.writeUint8(CLIST)
encoder.writeOpID(op.id)
encoder.writeVarString(op.type)
let info = op.info != null ? JSON.stringify(op.info) : ''
encoder.writeVarString(info)
},
binaryDecode: function (decoder) {
decoder.skip8()
let op = {
id: decoder.readOpID(),
type: decoder.readVarString(),
struct: 'List'
}
let info = decoder.readVarString()
if (info.length > 0) {
op.info = JSON.parse(info)
}
return op
},
requiredOps: function () {
/*
var ids = []
@@ -460,7 +325,7 @@ export default function extendStruct (Y) {
return null
}
var res = null
var o = yield * this.getOperation(op.start)
var o = yield* this.getOperation(op.start)
while (true) {
if (!o.deleted) {
@@ -468,7 +333,7 @@ export default function extendStruct (Y) {
pos--
}
if (pos >= 0 && o.right != null) {
o = yield * this.getOperation(o.right)
o = yield* this.getOperation(o.right)
} else {
break
}
@@ -479,7 +344,7 @@ export default function extendStruct (Y) {
o = o.start
var res = []
while (o != null) { // TODO: change to != (at least some convention)
var operation = yield * this.getOperation(o)
var operation = yield* this.getOperation(o)
if (!operation.deleted) {
res.push(f(operation))
}
@@ -512,36 +377,13 @@ export default function extendStruct (Y) {
map: {} // overwrite map!!
}
if (op.requires != null) {
e.requires = op.require
// TODO: !!
console.warn('requires is used! see same note above for List')
e.requires = op.requires
}
if (op.info != null) {
e.info = op.info
}
return e
},
binaryEncode: function (encoder, op) {
encoder.writeUint8(CMAP)
encoder.writeOpID(op.id)
encoder.writeVarString(op.type)
let info = op.info != null ? JSON.stringify(op.info) : ''
encoder.writeVarString(info)
},
binaryDecode: function (decoder) {
decoder.skip8()
let op = {
id: decoder.readOpID(),
type: decoder.readVarString(),
struct: 'Map',
map: {}
}
let info = decoder.readVarString()
if (info.length > 0) {
op.info = JSON.parse(info)
}
return op
},
requiredOps: function () {
return []
},
@@ -552,13 +394,13 @@ export default function extendStruct (Y) {
get: function * (op, name) {
var oid = op.map[name]
if (oid != null) {
var res = yield * this.getOperation(oid)
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)
return yield* this.getType(res.opContent)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
/* globals crypto */
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
/* @flow */
'use strict'
/*
EventHandler is an helper class for constructing custom types.
@@ -24,56 +23,8 @@ import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
database request to finish). EventHandler helps you to make your type
synchronous.
*/
export default function Utils (Y) {
Y.utils = {
BinaryDecoder: BinaryDecoder,
BinaryEncoder: BinaryEncoder
}
Y.utils.bubbleEvent = function (type, event) {
type.eventHandler.callEventListeners(event)
event.path = []
while (type != null && type._deepEventHandler != null) {
type._deepEventHandler.callEventListeners(event)
var parent = null
if (type._parent != null) {
parent = type.os.getType(type._parent)
}
if (parent != null && parent._getPathToChild != null) {
event.path = [parent._getPathToChild(type._model)].concat(event.path)
type = parent
} else {
type = null
}
}
}
class NamedEventHandler {
constructor () {
this._eventListener = {}
}
on (name, f) {
if (this._eventListener[name] == null) {
this._eventListener[name] = []
}
this._eventListener[name].push(f)
}
off (name, f) {
if (name == null || f == null) {
throw new Error('You must specify event name and function!')
}
let listener = this._eventListener[name] || []
this._eventListener[name] = listener.filter(e => e !== f)
}
emit (name, value) {
(this._eventListener[name] || []).forEach(l => l(value))
}
destroy () {
this._eventListener = null
}
}
Y.utils.NamedEventHandler = NamedEventHandler
module.exports = function (Y /* : any*/) {
Y.utils = {}
class EventListenerHandler {
constructor () {
@@ -99,18 +50,9 @@ export default function Utils (Y) {
callEventListeners (event) {
for (var i = 0; i < this.eventListeners.length; i++) {
try {
var _event = {}
for (var name in event) {
_event[name] = event[name]
}
this.eventListeners[i](_event)
this.eventListeners[i](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)
console.error('User events must not throw Errors!')
}
}
}
@@ -140,6 +82,7 @@ export default function Utils (Y) {
destroy () {
super.destroy()
this.waiting = null
this.awaiting = null
this.onevent = null
}
/*
@@ -295,13 +238,7 @@ export default function Utils (Y) {
// finished with remaining operations
self.waiting.push(d)
}
if (op.key == null) {
// deletes in list
checkDelete(op)
} else {
// deletes in map
this.waiting.push(op)
}
checkDelete(op)
} else {
this.waiting.push(op)
}
@@ -339,7 +276,7 @@ export default function Utils (Y) {
}
var before = this.waiting.length
// somehow create new operations
yield * f.apply(transaction, args)
yield* f.apply(transaction, args)
// remove all appended ops / awaited ops
this.waiting.splice(before)
if (this.awaiting > 0) this.awaiting--
@@ -349,22 +286,18 @@ export default function Utils (Y) {
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 (_o.parentSub != null && _o.left != null) {
// if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left
this.waiting.splice(i, 1)
i-- // update index
} else if (!Y.utils.compareIds(_o.id, o.id)) {
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)
var left = yield* transaction.getInsertion(_o.left)
while (left.deleted != null) {
if (left.left != null) {
left = yield * transaction.getInsertion(left.left)
left = yield* transaction.getInsertion(left.left)
} else {
left = null
break
@@ -514,27 +447,6 @@ export default function Utils (Y) {
}
Y.utils.EventHandler = EventHandler
/*
Default class of custom types!
*/
class CustomType {
getPath () {
var parent = null
if (this._parent != null) {
parent = this.os.getType(this._parent)
}
if (parent != null && parent._getPathToChild != null) {
var firstKey = parent._getPathToChild(this._model)
var parentKeys = parent.getPath()
parentKeys.push(firstKey)
return parentKeys
} else {
return []
}
}
}
Y.utils.CustomType = CustomType
/*
A wrapper for the definition of a custom type.
Every custom type must have three properties:
@@ -546,7 +458,7 @@ export default function Utils (Y) {
* class
- the constructor of the custom type (e.g. in order to inherit from a type)
*/
class CustomTypeDefinition { // eslint-disable-line
class CustomType { // eslint-disable-line
/* ::
struct: any;
initType: any;
@@ -557,14 +469,12 @@ export default function Utils (Y) {
if (def.struct == null ||
def.initType == null ||
def.class == null ||
def.name == null ||
def.createType == null
def.name == null
) {
throw new Error('Custom type was not initialized correctly!')
}
this.struct = def.struct
this.initType = def.initType
this.createType = def.createType
this.class = def.class
this.name = def.name
if (def.appendAdditionalInfo != null) {
@@ -576,13 +486,13 @@ export default function Utils (Y) {
this.parseArguments.typeDefinition = this
}
}
Y.utils.CustomTypeDefinition = CustomTypeDefinition
Y.utils.CustomType = CustomType
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
if (v != null) {
if (v instanceof Y.utils.CustomTypeDefinition) return [v]
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomTypeDefinition) return v
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomTypeDefinition) return [v.typeDefinition]
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
}
@@ -648,7 +558,6 @@ export default function Utils (Y) {
}
}
}
return false
}
Y.utils.matchesId = matchesId
@@ -716,7 +625,7 @@ export default function Utils (Y) {
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)
o = yield* super.find(id)
}
if (o != null) {
for (i = 0; i < this.readBuffer.length - 1; i++) {
@@ -746,7 +655,7 @@ export default function Utils (Y) {
// write writeBuffer[0]
var write = this.writeBuffer[0]
if (write.id[0] !== null) {
yield * super.put(write)
yield* super.put(write)
}
// put o to the end of writeBuffer
for (i = 0; i < this.writeBuffer.length - 1; i++) {
@@ -776,44 +685,44 @@ export default function Utils (Y) {
}
}
}
yield * this.flush()
yield * super.delete(id)
yield* this.flush()
yield* super.delete(id)
}
* findWithLowerBound (id) {
var o = yield * this.find(id, true)
var o = yield* this.find(id, true)
if (o != null) {
return o
} else {
yield * this.flush()
return yield * super.findWithLowerBound.apply(this, arguments)
yield* this.flush()
return yield* super.findWithLowerBound.apply(this, arguments)
}
}
* findWithUpperBound (id) {
var o = yield * this.find(id, true)
var o = yield* this.find(id, true)
if (o != null) {
return o
} else {
yield * this.flush()
return yield * super.findWithUpperBound.apply(this, arguments)
yield* this.flush()
return yield* super.findWithUpperBound.apply(this, arguments)
}
}
* findNext () {
yield * this.flush()
return yield * super.findNext.apply(this, arguments)
yield* this.flush()
return yield* super.findNext.apply(this, arguments)
}
* findPrev () {
yield * this.flush()
return yield * super.findPrev.apply(this, arguments)
yield* this.flush()
return yield* super.findPrev.apply(this, arguments)
}
* iterate () {
yield * this.flush()
yield * super.iterate.apply(this, arguments)
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)
yield* super.put(write)
this.writeBuffer[i] = {
id: [null, null]
}
@@ -824,20 +733,4 @@ export default function Utils (Y) {
return SmallLookupBuffer
}
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
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)
}
}
Y.utils.generateUserId = generateUserId
}

191
src/y.js
View File

@@ -1,58 +1,32 @@
import extendConnector from './Connector.js'
import extendPersistence from './Persistence.js'
import extendDatabase from './Database.js'
import extendTransaction from './Transaction.js'
import extendStruct from './Struct.js'
import extendUtils from './Utils.js'
import debug from 'debug'
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
/* @flow */
'use strict'
extendConnector(Y)
extendPersistence(Y)
extendDatabase(Y)
extendTransaction(Y)
extendStruct(Y)
extendUtils(Y)
Y.debug = debug
debug.formatters.Y = formatYjsMessage
debug.formatters.y = formatYjsMessageType
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 (arguments.length === 2 && typeof name === 'string') {
if (value instanceof Y.utils.CustomTypeDefinition) {
Y[name] = value.parseArguments
} else {
Y[name] = value
}
if (requiringModules[name] != null) {
requiringModules[name].resolve()
delete requiringModules[name]
}
if (value instanceof Y.utils.CustomType) {
Y[name] = value.parseArguments
} else {
for (var i = 0; i < arguments.length; i++) {
var f = arguments[i]
if (typeof f === 'function') {
f(Y)
} else {
throw new Error('Expected function!')
}
}
Y[name] = value
}
if (requiringModules[name] != null) {
requiringModules[name].resolve()
delete requiringModules[name]
}
}
Y.requestModules = requestModules
function requestModules (modules) {
var sourceDir
if (Y.sourceDir === null) {
sourceDir = null
} else {
sourceDir = Y.sourceDir || '/bower_components'
}
// determine if this module was compiled for es5 or es6 (y.js vs. y.es6)
// if Insert.execute is a Function, then it isnt a generator..
// then load the es5(.js) files..
@@ -65,11 +39,10 @@ function requestModules (modules) {
if (requiringModules[module] == null) {
// module does not exist
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
if (sourceDir != null) {
var imported = document.createElement('script')
imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
document.head.appendChild(imported)
}
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) {
@@ -117,49 +90,36 @@ type YOptions = {
}
*/
export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
if (opts.hasOwnProperty('sourceDir')) {
Y.sourceDir = opts.sourceDir
}
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) {
if (opts == null) reject(new Error('An options object is expected!'))
else if (opts.connector == null) reject(new Error('You must specify a connector! (missing connector property)'))
else if (opts.connector.name == null) reject(new Error('You must specify connector name! (missing connector.name property)'))
else if (opts.db == null) reject(new Error('You must specify a database! (missing db property)'))
else if (opts.connector.name == null) reject(new Error('You must specify db name! (missing db.name property)'))
else {
opts = Y.utils.copyObject(opts)
opts.connector = Y.utils.copyObject(opts.connector)
opts.db = Y.utils.copyObject(opts.db)
opts.share = Y.utils.copyObject(opts.share)
setTimeout(function () {
Y.requestModules(modules).then(function () {
var yconfig = new YConfig(opts)
let resolved = false
if (opts.timeout != null && opts.timeout >= 0) {
setTimeout(function () {
if (!resolved) {
reject(new Error('Yjs init timeout'))
yconfig.destroy()
}
}, opts.timeout)
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)
})
})
}
yconfig.db.whenUserIdSet(function () {
yconfig.init(function () {
resolved = true
resolve(yconfig)
}, reject)
})
}).catch(reject)
}
}, 0)
})
}
class YConfig extends Y.utils.NamedEventHandler {
class YConfig {
/* ::
db: Y.AbstractDatabase;
connector: Y.AbstractConnector;
@@ -167,16 +127,9 @@ class YConfig extends Y.utils.NamedEventHandler {
options: Object;
*/
constructor (opts, callback) {
super()
this.options = opts
this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector)
if (opts.persistence != null) {
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
} else {
this.persistence = null
}
this.connected = true
}
init (callback) {
var opts = this.options
@@ -187,9 +140,6 @@ class YConfig extends Y.utils.NamedEventHandler {
for (var propertyname in opts.share) {
var typeConstructor = opts.share[propertyname].split('(')
var typeName = typeConstructor.splice(0, 1)
var type = Y[typeName]
var typedef = type.typeDefinition
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
var args = []
if (typeConstructor.length === 1) {
try {
@@ -197,71 +147,40 @@ class YConfig extends Y.utils.NamedEventHandler {
} catch (e) {
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
}
if (type.typeDefinition.parseArguments == null) {
throw new Error(typeName + ' does not expect arguments!')
} else {
args = typedef.parseArguments(args[0])[1]
}
}
share[propertyname] = yield * this.store.initType.call(this, id, args)
var type = Y[typeName]
var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
share[propertyname] = yield* this.createType(type.apply(typedef, args), id)
}
this.store.whenTransactionsFinished()
.then(callback)
})
if (this.persistence != null) {
this.persistence.retrieveContent()
.then(() => this.db.whenTransactionsFinished())
.then(callback)
} else {
this.db.whenTransactionsFinished()
.then(callback)
}
}
isConnected () {
return this.connector.isSynced
}
disconnect () {
if (this.connected) {
this.connected = false
return this.connector.disconnect()
} else {
return Promise.resolve()
}
return this.connector.disconnect()
}
reconnect () {
if (!this.connected) {
this.connected = true
return this.connector.reconnect()
} else {
return Promise.resolve()
}
return this.connector.reconnect()
}
destroy () {
var self = this
return this.close().then(function () {
if (self.db.deleteDB != null) {
return self.db.deleteDB()
} else {
return Promise.resolve()
}
}).then(() => {
// remove existing event listener
super.destroy()
})
}
close () {
var self = this
this.share = null
if (this.connector.destroy != null) {
this.connector.destroy()
} else {
this.connector.disconnect()
}
return this.db.whenTransactionsFinished().then(function () {
self.db.destroyTypes()
// make sure to wait for all transactions before destroying the db
self.db.requestTransaction(function * () {
yield * self.db.destroy()
})
return self.db.whenTransactionsFinished()
var self = this
this.db.requestTransaction(function * () {
yield* self.db.destroy()
self.connector = null
self.db = null
})
}
}
if (typeof window !== 'undefined') {
window.Y = Y
}

View File

@@ -1,229 +0,0 @@
import { test } from 'cutest'
import Chance from 'chance'
import Y from '../src/y.js'
import { BinaryEncoder, BinaryDecoder } from '../src/Encoding.js'
function testEncoding (t, write, read, val) {
let encoder = new BinaryEncoder()
write(encoder, val)
let reader = new BinaryDecoder(encoder.createBuffer())
let result = read(reader)
t.log(`string encode: ${JSON.stringify(val).length} bytes / binary encode: ${encoder.data.length} bytes`)
t.compare(val, result, 'Compare results')
}
const writeVarUint = (encoder, val) => encoder.writeVarUint(val)
const readVarUint = decoder => decoder.readVarUint()
test('varUint 1 byte', async function varUint1 (t) {
testEncoding(t, writeVarUint, readVarUint, 42)
})
test('varUint 2 bytes', async function varUint2 (t) {
testEncoding(t, writeVarUint, readVarUint, 1 << 9 | 3)
testEncoding(t, writeVarUint, readVarUint, 1 << 9 | 3)
})
test('varUint 3 bytes', async function varUint3 (t) {
testEncoding(t, writeVarUint, readVarUint, 1 << 17 | 1 << 9 | 3)
})
test('varUint 4 bytes', async function varUint4 (t) {
testEncoding(t, writeVarUint, readVarUint, 1 << 25 | 1 << 17 | 1 << 9 | 3)
})
test('varUint of 2839012934', async function varUint2839012934 (t) {
testEncoding(t, writeVarUint, readVarUint, 2839012934)
})
test('varUint random', async function varUintRandom (t) {
const chance = new Chance(t.getSeed() * Math.pow(Number.MAX_SAFE_INTEGER))
testEncoding(t, writeVarUint, readVarUint, chance.integer({min: 0, max: (1 << 28) - 1}))
})
test('varUint random user id', async function varUintRandomUserId (t) {
t.getSeed() // enforces that this test is repeated
testEncoding(t, writeVarUint, readVarUint, Y.utils.generateUserId())
})
const writeVarString = (encoder, val) => encoder.writeVarString(val)
const readVarString = decoder => decoder.readVarString()
test('varString', async function varString (t) {
testEncoding(t, writeVarString, readVarString, 'hello')
testEncoding(t, writeVarString, readVarString, 'test!')
testEncoding(t, writeVarString, readVarString, '☺☺☺')
testEncoding(t, writeVarString, readVarString, '1234')
})
test('varString random', async function varStringRandom (t) {
const chance = new Chance(t.getSeed() * 1000000000)
testEncoding(t, writeVarString, readVarString, chance.string())
})
const writeDelete = Y.Struct.Delete.binaryEncode
const readDelete = Y.Struct.Delete.binaryDecode
test('encode/decode Delete operation', async function binDelete (t) {
let op = {
target: [10, 3000],
length: 40000,
struct: 'Delete'
}
testEncoding(t, writeDelete, readDelete, op)
})
const writeInsert = Y.Struct.Insert.binaryEncode
const readInsert = Y.Struct.Insert.binaryDecode
test('encode/decode Insert operations', async function binInsert (t) {
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: [5, 6],
left: [3, 4],
origin: [7, 8],
parent: [9, 10],
struct: 'Insert',
content: ['a']
})
t.log('left === origin')
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: [5, 6],
left: [3, 4],
origin: [3, 4],
parent: [9, 10],
struct: 'Insert',
content: ['a']
})
t.log('parentsub')
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: [5, 6],
left: [3, 4],
origin: [3, 4],
parent: [9, 10],
parentSub: 'sub',
struct: 'Insert',
content: ['a']
})
t.log('opContent')
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: [5, 6],
left: [3, 4],
origin: [3, 4],
parent: [9, 10],
struct: 'Insert',
opContent: [1000, 10000]
})
t.log('mixed content')
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: [5, 6],
left: [3, 4],
origin: [3, 4],
parent: [9, 10],
struct: 'Insert',
content: ['a', 1]
})
t.log('origin is null')
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: [5, 6],
left: [3, 4],
origin: null,
parent: [9, 10],
struct: 'Insert',
content: ['a']
})
t.log('left = origin = right = null')
testEncoding(t, writeInsert, readInsert, {
id: [1, 2],
right: null,
left: null,
origin: null,
parent: [9, 10],
struct: 'Insert',
content: ['a']
})
})
const writeList = Y.Struct.List.binaryEncode
const readList = Y.Struct.List.binaryDecode
test('encode/decode List operations', async function binList (t) {
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array'
})
t.log('info is an object')
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array',
info: { prop: 'yay' }
})
t.log('info is a string')
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array',
info: 'hi'
})
t.log('info is a number')
testEncoding(t, writeList, readList, {
struct: 'List',
id: [100, 33],
type: 'Array',
info: 400
})
})
const writeMap = Y.Struct.Map.binaryEncode
const readMap = Y.Struct.Map.binaryDecode
test('encode/decode Map operations', async function binMap (t) {
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
map: {}
})
t.log('info is an object')
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
info: { prop: 'yay' },
map: {}
})
t.log('info is a string')
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
map: {},
info: 'hi'
})
t.log('info is a number')
testEncoding(t, writeMap, readMap, {
struct: 'Map',
id: [100, 33],
type: 'Map',
map: {},
info: 400
})
})

View File

@@ -1,374 +0,0 @@
import { wait, initArrays, compareUsers, Y, flushAll, garbageCollectUsers, applyRandomTests } from '../tests-lib/helper.js'
import { test, proxyConsole } from 'cutest'
proxyConsole()
test('basic spec', async function array0 (t) {
let { users, array0 } = await initArrays(t, { users: 2 })
array0.delete(0, 0)
t.assert(true, 'Does not throw when deleting zero elements with position 0')
let throwInvalidPosition = false
try {
array0.delete(1, 0)
} catch (e) {
throwInvalidPosition = true
}
t.assert(throwInvalidPosition, 'Throws when deleting zero elements with an invalid position')
array0.insert(0, ['A'])
array0.delete(1, 0)
t.assert(true, 'Does not throw when deleting zero elements with valid position 1')
await compareUsers(t, users)
})
test('insert three elements, try re-get property', async function array1 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, [1, 2, 3])
t.compare(array0.toArray(), [1, 2, 3], '.toArray() works')
await flushAll(t, users)
t.compare(array1.toArray(), [1, 2, 3], '.toArray() works after sync')
await compareUsers(t, users)
})
test('concurrent insert (handle three conflicts)', async function array2 (t) {
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
array0.insert(0, [0])
array1.insert(0, [1])
array2.insert(0, [2])
await compareUsers(t, users)
})
test('concurrent insert&delete (handle three conflicts)', async function array3 (t) {
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y', 'z'])
await flushAll(t, users)
array0.insert(1, [0])
array1.delete(0)
array1.delete(1, 1)
array2.insert(1, [2])
await compareUsers(t, users)
})
test('insertions work in late sync', async function array4 (t) {
var { users, array0, array1, array2 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y'])
await flushAll(t, users)
users[1].disconnect()
users[2].disconnect()
array0.insert(1, ['user0'])
array1.insert(1, ['user1'])
array2.insert(1, ['user2'])
await users[1].reconnect()
await users[2].reconnect()
await compareUsers(t, users)
})
test('disconnect really prevents sending messages', async function array5 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y'])
await flushAll(t, users)
users[1].disconnect()
users[2].disconnect()
array0.insert(1, ['user0'])
array1.insert(1, ['user1'])
await wait(1000)
t.compare(array0.toArray(), ['x', 'user0', 'y'])
t.compare(array1.toArray(), ['x', 'user1', 'y'])
await users[1].reconnect()
await users[2].reconnect()
await compareUsers(t, users)
})
test('deletions in late sync', async function array6 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, ['x', 'y'])
await flushAll(t, users)
await users[1].disconnect()
array1.delete(1, 1)
array0.delete(0, 2)
await wait()
await users[1].reconnect()
await compareUsers(t, users)
})
test('insert, then marge delete on sync', async function array7 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 2 })
array0.insert(0, ['x', 'y', 'z'])
await flushAll(t, users)
await wait()
await users[0].disconnect()
array1.delete(0, 3)
await wait()
await users[0].reconnect()
await compareUsers(t, users)
})
function compareEvent (t, is, should) {
for (var key in should) {
t.assert(
should[key] === is[key] ||
JSON.stringify(should[key]) === JSON.stringify(is[key])
, 'event works as expected'
)
}
}
test('insert & delete events', async function array8 (t) {
var { array0, users } = await initArrays(t, { users: 2 })
var event
array0.observe(function (e) {
event = e
})
array0.insert(0, [0, 1, 2])
compareEvent(t, event, {
type: 'insert',
index: 0,
values: [0, 1, 2],
length: 3
})
array0.delete(0)
compareEvent(t, event, {
type: 'delete',
index: 0,
length: 1,
values: [0]
})
array0.delete(0, 2)
compareEvent(t, event, {
type: 'delete',
index: 0,
length: 2,
values: [1, 2]
})
await compareUsers(t, users)
})
test('insert & delete events for types', async function array9 (t) {
var { array0, users } = await initArrays(t, { users: 2 })
var event
array0.observe(function (e) {
event = e
})
array0.insert(0, [Y.Array])
compareEvent(t, event, {
type: 'insert',
object: array0,
index: 0,
length: 1
})
var type = array0.get(0)
t.assert(type._model != null, 'Model of type is defined')
array0.delete(0)
compareEvent(t, event, {
type: 'delete',
object: array0,
index: 0,
length: 1
})
await compareUsers(t, users)
})
test('insert & delete events for types (2)', async function array10 (t) {
var { array0, users } = await initArrays(t, { users: 2 })
var events = []
array0.observe(function (e) {
events.push(e)
})
array0.insert(0, ['hi', Y.Map])
compareEvent(t, events[0], {
type: 'insert',
object: array0,
index: 0,
length: 1,
values: ['hi']
})
compareEvent(t, events[1], {
type: 'insert',
object: array0,
index: 1,
length: 1
})
array0.delete(1)
compareEvent(t, events[2], {
type: 'delete',
object: array0,
index: 1,
length: 1
})
await compareUsers(t, users)
})
test('garbage collector', async function gc1 (t) {
var { users, array0 } = await initArrays(t, { users: 3 })
array0.insert(0, ['x', 'y', 'z'])
await flushAll(t, users)
users[0].disconnect()
array0.delete(0, 3)
await wait()
await users[0].reconnect()
await flushAll(t, users)
await garbageCollectUsers(t, users)
await compareUsers(t, users)
})
test('event has correct value when setting a primitive on a YArray (same user)', async function array11 (t) {
var { array0, users } = await initArrays(t, { users: 3 })
var event
array0.observe(function (e) {
event = e
})
array0.insert(0, ['stuff'])
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] === 'stuff', 'check that value is actually present')
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
await compareUsers(t, users)
})
test('event has correct value when setting a primitive on a YArray (received from another user)', async function array12 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 3 })
var event
array0.observe(function (e) {
event = e
})
array1.insert(0, ['stuff'])
await flushAll(t, users)
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] === 'stuff', 'check that value is actually present')
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YArray (same user)', async function array13 (t) {
var { array0, users } = await initArrays(t, { users: 3 })
var event
array0.observe(function (e) {
event = e
})
array0.insert(0, [Y.Array])
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] != null, 'event.value exists')
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YArray (ops received from another user)', async function array14 (t) {
var { users, array0, array1 } = await initArrays(t, { users: 3 })
var event
array0.observe(function (e) {
event = e
})
array1.insert(0, [Y.Array])
await flushAll(t, users)
t.assert(event.values[0] === event.object.get(0), 'compare value with get method')
t.assert(event.values[0] != null, 'event.value exists')
t.assert(event.values[0] === array0.toArray()[0], '.toArray works as expected')
await compareUsers(t, users)
})
var _uniqueNumber = 0
function getUniqueNumber () {
return _uniqueNumber++
}
var arrayTransactions = [
function insert (t, user, chance) {
var uniqueNumber = getUniqueNumber()
var content = []
var len = chance.integer({ min: 1, max: 4 })
for (var i = 0; i < len; i++) {
content.push(uniqueNumber)
}
var pos = chance.integer({ min: 0, max: user.share.array.length })
user.share.array.insert(pos, content)
},
function insertTypeArray (t, user, chance) {
var pos = chance.integer({ min: 0, max: user.share.array.length })
user.share.array.insert(pos, [Y.Array])
var array2 = user.share.array.get(pos)
array2.insert(0, [1, 2, 3, 4])
},
function insertTypeMap (t, user, chance) {
var pos = chance.integer({ min: 0, max: user.share.array.length })
user.share.array.insert(pos, [Y.Map])
var map = user.share.array.get(pos)
map.set('someprop', 42)
map.set('someprop', 43)
map.set('someprop', 44)
},
function _delete (t, user, chance) {
var length = user.share.array._content.length
if (length > 0) {
var pos = chance.integer({ min: 0, max: length - 1 })
var delLength = chance.integer({ min: 1, max: Math.min(2, length - pos) })
if (user.share.array._content[pos].type != null) {
if (chance.bool()) {
var type = user.share.array.get(pos)
if (type instanceof Y.Array.typeDefinition.class) {
if (type._content.length > 0) {
pos = chance.integer({ min: 0, max: type._content.length - 1 })
delLength = chance.integer({ min: 0, max: Math.min(2, type._content.length - pos) })
type.delete(pos, delLength)
}
} else {
type.delete('someprop')
}
} else {
user.share.array.delete(pos, delLength)
}
} else {
user.share.array.delete(pos, delLength)
}
}
}
]
test('y-array: Random tests (42)', async function randomArray42 (t) {
await applyRandomTests(t, arrayTransactions, 42)
})
test('y-array: Random tests (43)', async function randomArray43 (t) {
await applyRandomTests(t, arrayTransactions, 43)
})
test('y-array: Random tests (44)', async function randomArray44 (t) {
await applyRandomTests(t, arrayTransactions, 44)
})
test('y-array: Random tests (45)', async function randomArray45 (t) {
await applyRandomTests(t, arrayTransactions, 45)
})
test('y-array: Random tests (46)', async function randomArray46 (t) {
await applyRandomTests(t, arrayTransactions, 46)
})
test('y-array: Random tests (47)', async function randomArray47 (t) {
await applyRandomTests(t, arrayTransactions, 47)
})
/*
test('y-array: Random tests (200)', async function randomArray200 (t) {
await applyRandomTests(t, arrayTransactions, 200)
})
test('y-array: Random tests (300)', async function randomArray300 (t) {
await applyRandomTests(t, arrayTransactions, 300)
})
test('y-array: Random tests (400)', async function randomArray400 (t) {
await applyRandomTests(t, arrayTransactions, 400)
})
test('y-array: Random tests (500)', async function randomArray500 (t) {
await applyRandomTests(t, arrayTransactions, 500)
})
*/

View File

@@ -1,371 +0,0 @@
import { initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../tests-lib/helper.js'
import { test, proxyConsole } from 'cutest'
proxyConsole()
test('basic map tests', async function map0 (t) {
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
users[2].disconnect()
map0.set('number', 1)
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
map0.set('y-map', Y.Map)
let map = map0.get('y-map')
map.set('y-array', Y.Array)
let array = map.get('y-array')
array.insert(0, [0])
array.insert(0, [-1])
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
await users[2].reconnect()
await flushAll(t, users)
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
// compare disconnected user
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
await compareUsers(t, users)
})
test('Basic get&set of Map property (converge via sync)', async function map1 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'stuffy')
}
await compareUsers(t, users)
})
test('Map can set custom types (Map)', async function map2 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var map = map0.set('Map', Y.Map)
map.set('one', 1)
map = map0.get('Map')
t.compare(map.get('one'), 1)
await compareUsers(t, users)
})
test('Map can set custom types (Map) - get also returns the type', async function map3 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('Map', Y.Map)
var map = map0.get('Map')
map.set('one', 1)
map = map0.get('Map')
t.compare(map.get('one'), 1)
await compareUsers(t, users)
})
test('Map can set custom types (Array)', async function map4 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var array = map0.set('Array', Y.Array)
array.insert(0, [1, 2, 3])
array = map0.get('Array')
t.compare(array.toArray(), [1, 2, 3])
await compareUsers(t, users)
})
test('Basic get&set of Map property (converge via update)', async function map5 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'stuffy')
}
await compareUsers(t, users)
})
test('Basic get&set of Map property (handle conflict)', async function map6 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'c0')
}
await compareUsers(t, users)
})
test('Basic get&set&delete of Map property (handle conflict)', async function map7 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
map0.set('stuff', 'c0')
map0.delete('stuff')
map1.set('stuff', 'c1')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.assert(u.get('stuff') === undefined)
}
await compareUsers(t, users)
})
test('Basic get&set of Map property (handle three conflicts)', async function map8 (t) {
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.compare(u.get('stuff'), 'c0')
}
await compareUsers(t, users)
})
test('Basic get&set&delete of Map property (handle three conflicts)', async function map9 (t) {
let { users, map0, map1, map2, map3 } = await initArrays(t, { users: 4 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
await flushAll(t, users)
map0.set('stuff', 'deleteme')
map0.delete('stuff')
map1.set('stuff', 'c1')
map2.set('stuff', 'c2')
map3.set('stuff', 'c3')
await flushAll(t, users)
for (let user of users) {
var u = user.share.map
t.assert(u.get('stuff') === undefined)
}
await compareUsers(t, users)
})
test('observePath properties', async function map10 (t) {
let { users, map0, map1, map2 } = await initArrays(t, { users: 3 })
let map
map0.observePath(['map'], function (map) {
if (map != null) {
map.set('yay', 4)
}
})
map1.set('map', Y.Map)
await flushAll(t, users)
map = map2.get('map')
t.compare(map.get('yay'), 4)
await compareUsers(t, users)
})
test('observe deep properties', async function map11 (t) {
let { users, map1, map2, map3 } = await initArrays(t, { users: 4 })
var _map1 = map1.set('map', Y.Map)
var calls = 0
var dmapid
_map1.observe(function (event) {
calls++
t.compare(event.name, 'deepmap')
dmapid = event.object.opContents.deepmap
})
await flushAll(t, users)
var _map3 = map3.get('map')
_map3.set('deepmap', Y.Map)
await flushAll(t, users)
var _map2 = map2.get('map')
_map2.set('deepmap', Y.Map)
await flushAll(t, users)
var dmap1 = _map1.get('deepmap')
var dmap2 = _map2.get('deepmap')
var dmap3 = _map3.get('deepmap')
t.assert(calls > 0)
t.compare(dmap1._model, dmap2._model)
t.compare(dmap1._model, dmap3._model)
t.compare(dmap1._model, dmapid)
await compareUsers(t, users)
})
test('observes using observePath', async function map12 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var pathes = []
var calls = 0
map0.observeDeep(function (event) {
pathes.push(event.path)
calls++
})
map0.set('map', Y.Map)
map0.get('map').set('array', Y.Array)
map0.get('map').get('array').insert(0, ['content'])
t.assert(calls === 3)
t.compare(pathes, [[], ['map'], ['map', 'array']])
await compareUsers(t, users)
})
function compareEvent (t, is, should) {
for (var key in should) {
t.assert(should[key] === is[key])
}
}
test('throws add & update & delete events (with type and primitive content)', async function map13 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e // just put it on event, should be thrown synchronously anyway
})
map0.set('stuff', 4)
compareEvent(t, event, {
type: 'add',
object: map0,
name: 'stuff'
})
// update, oldValue is in contents
map0.set('stuff', Y.Array)
compareEvent(t, event, {
type: 'update',
object: map0,
name: 'stuff',
oldValue: 4
})
var replacedArray = map0.get('stuff')
// update, oldValue is in opContents
map0.set('stuff', 5)
var array = event.oldValue
t.compare(array._model, replacedArray._model)
// delete
map0.delete('stuff')
compareEvent(t, event, {
type: 'delete',
name: 'stuff',
object: map0,
oldValue: 5
})
await compareUsers(t, users)
})
test('event has correct value when setting a primitive on a YMap (same user)', async function map14 (t) {
let { users, map0 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map0.set('stuff', 2)
t.compare(event.value, event.object.get(event.name))
await compareUsers(t, users)
})
test('event has correct value when setting a primitive on a YMap (received from another user)', async function map15 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map1.set('stuff', 2)
await flushAll(t, users)
t.compare(event.value, event.object.get(event.name))
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YMap (same user)', async function map16 (t) {
let { users, map0 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map0.set('stuff', Y.Map)
t.compare(event.value._model, event.object.get(event.name)._model)
await compareUsers(t, users)
})
test('event has correct value when setting a type on a YMap (ops received from another user)', async function map17 (t) {
let { users, map0, map1 } = await initArrays(t, { users: 3 })
var event
await flushAll(t, users)
map0.observe(function (e) {
event = e
})
map1.set('stuff', Y.Map)
await flushAll(t, users)
t.compare(event.value._model, event.object.get(event.name)._model)
await compareUsers(t, users)
})
var mapTransactions = [
function set (t, user, chance) {
let key = chance.pickone(['one', 'two'])
var value = chance.string()
user.share.map.set(key, value)
},
function setType (t, user, chance) {
let key = chance.pickone(['one', 'two'])
var value = chance.pickone([Y.Array, Y.Map])
let type = user.share.map.set(key, value)
if (value === Y.Array) {
type.insert(0, [1, 2, 3, 4])
} else {
type.set('deepkey', 'deepvalue')
}
},
function _delete (t, user, chance) {
let key = chance.pickone(['one', 'two'])
user.share.map.delete(key)
}
]
test('y-map: Random tests (42)', async function randomMap42 (t) {
await applyRandomTests(t, mapTransactions, 42)
})
test('y-map: Random tests (43)', async function randomMap43 (t) {
await applyRandomTests(t, mapTransactions, 43)
})
test('y-map: Random tests (44)', async function randomMap44 (t) {
await applyRandomTests(t, mapTransactions, 44)
})
test('y-map: Random tests (45)', async function randomMap45 (t) {
await applyRandomTests(t, mapTransactions, 45)
})
test('y-map: Random tests (46)', async function randomMap46 (t) {
await applyRandomTests(t, mapTransactions, 46)
})
test('y-map: Random tests (47)', async function randomMap47 (t) {
await applyRandomTests(t, mapTransactions, 47)
})
/*
test('y-map: Random tests (200)', async function randomMap200 (t) {
await applyRandomTests(t, mapTransactions, 200)
})
test('y-map: Random tests (300)', async function randomMap300 (t) {
await applyRandomTests(t, mapTransactions, 300)
})
test('y-map: Random tests (400)', async function randomMap400 (t) {
await applyRandomTests(t, mapTransactions, 400)
})
test('y-map: Random tests (500)', async function randomMap500 (t) {
await applyRandomTests(t, mapTransactions, 500)
})
*/

View File

@@ -1,281 +0,0 @@
import _Y from '../../yjs/src/y.js'
import yMemory from '../../y-memory/src/y-memory.js'
import yArray from '../../y-array/src/y-array.js'
import yMap from '../../y-map/src/Map.js'
import yTest from './test-connector.js'
import Chance from 'chance'
export let Y = _Y
Y.extend(yMemory, yArray, yMap, yTest)
export var database = { name: 'memory' }
export var connector = { name: 'test', url: 'http://localhost:1234' }
function * getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
var user = n.id[0]
var clock = n.clock
ss[user] = clock
})
return ss
}
function * getDeleteSet () {
var ds = {}
yield * this.ds.iterate(this, null, null, function * (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
var gc = n.gc
var dv = ds[user]
if (dv === void 0) {
dv = []
ds[user] = dv
}
dv.push([counter, len, gc])
})
return ds
}
export async function garbageCollectUsers (t, users) {
await flushAll(t, users)
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
}
/*
* 1. reconnect and flush all
* 2. user 0 gc
* 3. get type content
* 4. disconnect & reconnect all (so gc is propagated)
* 5. compare os, ds, ss
*/
export async function compareUsers (t, users) {
await Promise.all(users.map(u => u.reconnect()))
if (users[0].connector.testRoom == null) {
await wait(100)
}
await flushAll(t, users)
await wait()
await flushAll(t, users)
var userArrayValues = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
function valueToComparable (v) {
if (v != null && v._model != null) {
return v._model
} else {
return v || null
}
}
var userMapOneValues = users.map(u => u.share.map.get('one')).map(valueToComparable)
var userMapTwoValues = users.map(u => u.share.map.get('two')).map(valueToComparable)
await users[0].db.garbageCollect()
await users[0].db.garbageCollect()
// disconnect all except user 0
await Promise.all(users.slice(1).map(async u =>
u.disconnect()
))
if (users[0].connector.testRoom == null) {
await wait(100)
}
// reconnect all
await Promise.all(users.map(u => u.reconnect()))
if (users[0].connector.testRoom == null) {
await wait(100)
}
await users[0].connector.testRoom.flushAll(users)
await Promise.all(users.map(u =>
new Promise(function (resolve) {
u.connector.whenSynced(resolve)
})
))
let filterDeletedOps = users.every(u => u.db.gc === false)
var data = await Promise.all(users.map(async (u) => {
var data = {}
u.db.requestTransaction(function * () {
let ops = []
yield * this.os.iterate(this, null, null, function * (op) {
ops.push(Y.Struct[op.struct].encode(op))
})
data.os = {}
for (let i = 0; i < ops.length; i++) {
let op = ops[i]
op = Y.Struct[op.struct].encode(op)
delete op.origin
/*
If gc = false, it is necessary to filter deleted ops
as they might have been split up differently..
*/
if (filterDeletedOps) {
let opIsDeleted = yield * this.isDeleted(op.id)
if (!opIsDeleted) {
data.os[JSON.stringify(op.id)] = op
}
} else {
data.os[JSON.stringify(op.id)] = op
}
}
data.ds = yield * getDeleteSet.apply(this)
data.ss = yield * getStateSet.apply(this)
})
await u.db.whenTransactionsFinished()
return data
}))
for (var i = 0; i < data.length - 1; i++) {
await t.asyncGroup(async () => {
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
t.compare(userMapOneValues[i], userMapOneValues[i + 1], 'map types (propery "one")')
t.compare(userMapTwoValues[i], userMapTwoValues[i + 1], 'map types (propery "two")')
t.compare(data[i].os, data[i + 1].os, 'os')
t.compare(data[i].ds, data[i + 1].ds, 'ds')
t.compare(data[i].ss, data[i + 1].ss, 'ss')
}, `Compare user${i} with user${i + 1}`)
}
await Promise.all(users.map(async (u) => {
await u.close()
}))
}
export async function initArrays (t, opts) {
var result = {
users: []
}
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map' }, opts.share)
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector)
for (let i = 0; i < opts.users; i++) {
let dbOpts
let connOpts
if (i === 0) {
// Only one instance can gc!
dbOpts = Object.assign({ gc: false }, database)
connOpts = Object.assign({ role: 'master' }, conn)
} else {
dbOpts = Object.assign({ gc: false }, database)
connOpts = Object.assign({ role: 'slave' }, conn)
}
let y = await Y({
connector: connOpts,
db: dbOpts,
share: share
})
result.users.push(y)
for (let name in share) {
result[name + i] = y.share[name]
}
}
result.array0.delete(0, result.array0.length)
if (result.users[0].connector.testRoom != null) {
// flush for sync if test-connector
await result.users[0].connector.testRoom.flushAll(result.users)
}
await Promise.all(result.users.map(u => {
return new Promise(function (resolve) {
u.connector.whenSynced(resolve)
})
}))
await flushAll(t, result.users)
return result
}
export async function flushAll (t, users) {
// users = users.filter(u => u.connector.isSynced)
if (users.length === 0) {
return
}
await wait(0)
if (users[0].connector.testRoom != null) {
// use flushAll method specified in Test Connector
await users[0].connector.testRoom.flushAll(users)
} else {
// flush for any connector
await Promise.all(users.map(u => { return u.db.whenTransactionsFinished() }))
var flushCounter = users[0].share.flushHelper.get('0') || 0
flushCounter++
await Promise.all(users.map(async (u, i) => {
// wait for all users to set the flush counter to the same value
await new Promise(resolve => {
function observer () {
var allUsersReceivedUpdate = true
for (var i = 0; i < users.length; i++) {
if (u.share.flushHelper.get(i + '') !== flushCounter) {
allUsersReceivedUpdate = false
break
}
}
if (allUsersReceivedUpdate) {
resolve()
}
}
u.share.flushHelper.observe(observer)
u.share.flushHelper.set(i + '', flushCounter)
})
}))
}
}
export async function flushSome (t, users) {
if (users[0].connector.testRoom == null) {
// if not test-connector, wait for some time for operations to arrive
await wait(100)
}
}
export function wait (t) {
return new Promise(function (resolve) {
setTimeout(resolve, t != null ? t : 100)
})
}
export async function applyRandomTests (t, mods, iterations) {
const chance = new Chance(t.getSeed() * 1000000000)
var initInformation = await initArrays(t, { users: 5, chance: chance })
let { users } = initInformation
for (var i = 0; i < iterations; i++) {
if (chance.bool({likelihood: 10})) {
// 10% chance to disconnect/reconnect a user
// we make sure that the first users always is connected
let user = chance.pickone(users.slice(1))
if (user.connector.isSynced) {
if (users.filter(u => u.connector.isSynced).length > 1) {
// make sure that at least one user remains in the room
await user.disconnect()
if (users[0].connector.testRoom == null) {
await wait(100)
}
}
} else {
await user.reconnect()
if (users[0].connector.testRoom == null) {
await wait(100)
}
await new Promise(function (resolve) {
user.connector.whenSynced(resolve)
})
}
} else if (chance.bool({likelihood: 5})) {
// 20%*!prev chance to flush all & garbagecollect
// TODO: We do not gc all users as this does not work yet
// await garbageCollectUsers(t, users)
await flushAll(t, users)
await users[0].db.emptyGarbageCollector()
await flushAll(t, users)
} else if (chance.bool({likelihood: 10})) {
// 20%*!prev chance to flush some operations
await flushSome(t, users)
}
let user = chance.pickone(users)
var test = chance.pickone(mods)
test(t, user, chance)
}
await compareUsers(t, users)
return initInformation
}

View File

@@ -1,166 +0,0 @@
/* global Y */
import { wait } from './helper.js'
import { formatYjsMessage } from '../src/MessageHandler.js'
var rooms = {}
export class TestRoom {
constructor (roomname) {
this.room = roomname
this.users = new Map()
this.nextUserId = 0
}
join (connector) {
if (connector.userId == null) {
connector.setUserId(this.nextUserId++)
}
this.users.forEach((user, uid) => {
if (user.role === 'master' || connector.role === 'master') {
this.users.get(uid).userJoined(connector.userId, connector.role)
connector.userJoined(uid, this.users.get(uid).role)
}
})
this.users.set(connector.userId, connector)
}
leave (connector) {
this.users.delete(connector.userId)
this.users.forEach(user => {
user.userLeft(connector.userId)
})
}
send (sender, receiver, m) {
var user = this.users.get(receiver)
if (user != null) {
user.receiveMessage(sender, m)
}
}
broadcast (sender, m) {
this.users.forEach((user, receiver) => {
this.send(sender, receiver, m)
})
}
async flushAll (users) {
let flushing = true
let allUserIds = Array.from(this.users.keys())
if (users == null) {
users = allUserIds.map(id => this.users.get(id).y)
}
while (flushing) {
await wait(10)
let res = await Promise.all(allUserIds.map(id => this.users.get(id)._flushAll(users)))
flushing = res.some(status => status === 'flushing')
}
}
}
function getTestRoom (roomname) {
if (rooms[roomname] == null) {
rooms[roomname] = new TestRoom(roomname)
}
return rooms[roomname]
}
export default function extendTestConnector (Y) {
class TestConnector extends Y.AbstractConnector {
constructor (y, options) {
if (options === undefined) {
throw new Error('Options must not be undefined!')
}
if (options.room == null) {
throw new Error('You must define a room name!')
}
options.forwardAppliedOperations = options.role === 'master'
super(y, options)
this.options = options
this.room = options.room
this.chance = options.chance
this.testRoom = getTestRoom(this.room)
this.testRoom.join(this)
}
disconnect () {
this.testRoom.leave(this)
return super.disconnect()
}
logBufferParsed () {
console.log(' === Logging buffer of user ' + this.userId + ' === ')
for (let [user, conn] of this.connections) {
console.log(` ${user}:`)
for (let i = 0; i < conn.buffer.length; i++) {
console.log(formatYjsMessage(conn.buffer[i]))
}
}
}
reconnect () {
this.testRoom.join(this)
return super.reconnect()
}
send (uid, message) {
super.send(uid, message)
this.testRoom.send(this.userId, uid, message)
}
broadcast (message) {
super.broadcast(message)
this.testRoom.broadcast(this.userId, message)
}
async whenSynced (f) {
var synced = false
var periodicFlushTillSync = () => {
if (synced) {
f()
} else {
this.testRoom.flushAll([this.y]).then(function () {
setTimeout(periodicFlushTillSync, 10)
})
}
}
periodicFlushTillSync()
return super.whenSynced(function () {
synced = true
})
}
receiveMessage (sender, m) {
if (this.userId !== sender && this.connections.has(sender)) {
var buffer = this.connections.get(sender).buffer
if (buffer == null) {
buffer = this.connections.get(sender).buffer = []
}
buffer.push(m)
if (this.chance.bool({likelihood: 30})) {
// flush 1/2 with 30% chance
var flushLength = Math.round(buffer.length / 2)
buffer.splice(0, flushLength).forEach(m => {
super.receiveMessage(sender, m)
})
}
}
}
async _flushAll (flushUsers) {
if (flushUsers.some(u => u.connector.userId === this.userId)) {
// this one needs to sync with every other user
flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y)
}
var finished = []
for (let i = 0; i < flushUsers.length; i++) {
let userId = flushUsers[i].connector.userId
if (userId !== this.userId && this.connections.has(userId)) {
let buffer = this.connections.get(userId).buffer
if (buffer != null) {
var messages = buffer.splice(0)
for (let j = 0; j < messages.length; j++) {
let p = super.receiveMessage(userId, messages[j])
finished.push(p)
}
}
}
}
await Promise.all(finished)
await this.y.db.whenTransactionsFinished()
return finished.length > 0 ? 'flushing' : 'done'
}
}
Y.extend('test', TestConnector)
}
if (typeof Y !== 'undefined') {
extendTestConnector(Y)
}

10
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5062
y.node.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long