Compare commits
5 Commits
v13.0.0-19
...
v12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03685ac84a | ||
|
|
6fa79320a5 | ||
|
|
4e3a297efc | ||
|
|
ef2cbf210b | ||
|
|
3a1c7a71d4 |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers"
|
||||
]
|
||||
}
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,4 +1,15 @@
|
||||
node_modules
|
||||
bower_components
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
build
|
||||
build_test
|
||||
.directory
|
||||
.codio
|
||||
.settings
|
||||
.jshintignore
|
||||
.jshintrc
|
||||
.validate.json
|
||||
/y.js
|
||||
/y.js.map
|
||||
/y-*
|
||||
.vscode
|
||||
jsconfig.json
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "dist"]
|
||||
path = dist
|
||||
url = https://github.com/y-js/yjs.git
|
||||
branch = dist
|
||||
33
README.md
33
README.md
@@ -55,23 +55,23 @@ Install Yjs, and its modules with [bower](http://bower.io/), or
|
||||
[npm](https://www.npmjs.org/package/yjs).
|
||||
|
||||
### Bower
|
||||
```
|
||||
```sh
|
||||
bower install --save yjs y-array % add all y-* modules you want to use
|
||||
```
|
||||
You only need to include the `y.js` file. Yjs is able to automatically require
|
||||
missing modules.
|
||||
```
|
||||
```html
|
||||
<script src="./bower_components/yjs/y.js"></script>
|
||||
```
|
||||
|
||||
### Npm
|
||||
```
|
||||
```sh
|
||||
npm install --save yjs % add all y-* modules you want to use
|
||||
```
|
||||
|
||||
If you don't include via script tag, you have to explicitly include all modules!
|
||||
(Same goes for other module systems)
|
||||
```
|
||||
```js
|
||||
var Y = require('yjs')
|
||||
require('y-array')(Y) // add the y-array type to Yjs
|
||||
require('y-websockets-client')(Y)
|
||||
@@ -84,7 +84,7 @@ require('y-text')(Y)
|
||||
```
|
||||
|
||||
### ES6 Syntax
|
||||
```
|
||||
```js
|
||||
import Y from 'yjs'
|
||||
import yArray from 'y-array'
|
||||
import yWebsocketsClient from 'y-webrtc'
|
||||
@@ -98,7 +98,7 @@ Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
|
||||
|
||||
# Text editing example
|
||||
Install dependencies
|
||||
```
|
||||
```sh
|
||||
bower i yjs y-memory y-webrtc y-array y-text
|
||||
```
|
||||
|
||||
@@ -166,6 +166,25 @@ soon, if possible.
|
||||
endpoint of the used connector.
|
||||
* All of our connectors also have a default connection endpoint that you can
|
||||
use for development.
|
||||
* We provide basic authentification for all connectors. The value of
|
||||
`options.connector.auth` (this can be a passphase) is sent to all connected
|
||||
Yjs instances. `options.connector.checkAuth` may grant read or write access
|
||||
depending on the `auth` information.
|
||||
Example: A client specifies `options.connector.auth = 'superSecretPassword`.
|
||||
A server specifies
|
||||
```js
|
||||
options.connector.checkAuth = function (auth, yjsInstance, sender) {
|
||||
return new Promise(function (resolve, reject){
|
||||
if (auth === 'superSecretPassword') {
|
||||
resolve('write') // grant read-write access
|
||||
} else if (auth === 'different password') {
|
||||
resolve('read') // grant read-only access
|
||||
} else {
|
||||
reject('wrong password!') // reject connection
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
* Set `options.connector.generateUserId = true` in order to genenerate a
|
||||
userid, instead of receiving one from the server. This way the `Y(..)` is
|
||||
immediately going to be resolved, without waiting for any confirmation from
|
||||
@@ -181,7 +200,7 @@ soon, if possible.
|
||||
* Defaults to `/bower_components`
|
||||
* Not required when running on `nodejs` / `iojs`
|
||||
* When using nodejs you need to manually extend Yjs:
|
||||
```
|
||||
```js
|
||||
var Y = require('yjs')
|
||||
// you have to require a db, connector, and *all* types you use!
|
||||
require('y-memory')(Y)
|
||||
|
||||
72
declarations/Structs.js
Normal file
72
declarations/Structs.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* @flow */
|
||||
|
||||
type UserId = string
|
||||
type Id = [UserId, number|string]
|
||||
|
||||
/*
|
||||
type Struct = {
|
||||
id: Id,
|
||||
left?: Id,
|
||||
right?: Id,
|
||||
target?: Id,
|
||||
struct: 'Insert' | 'Delete'
|
||||
}*/
|
||||
|
||||
type Struct = Insertion | Deletion
|
||||
type Operation = Struct
|
||||
|
||||
type Insertion = {
|
||||
id: Id,
|
||||
left: ?Id,
|
||||
origin: ?Id,
|
||||
right: ?Id,
|
||||
parent: Id,
|
||||
parentSub: ?Id,
|
||||
opContent: ?Id,
|
||||
content: ?any,
|
||||
struct: 'Insert'
|
||||
}
|
||||
|
||||
type Deletion = {
|
||||
target: Id,
|
||||
struct: 'Delete'
|
||||
}
|
||||
|
||||
type MapStruct = {
|
||||
id: Id,
|
||||
type: TypeNames,
|
||||
map: any
|
||||
}
|
||||
|
||||
type ListStruct = {
|
||||
id: Id,
|
||||
type: TypeNames,
|
||||
start: Id,
|
||||
end: Id
|
||||
}
|
||||
|
||||
|
||||
type MessageSyncStep1 = {
|
||||
type: 'sync step 1',
|
||||
deleteSet: any,
|
||||
stateSet: any
|
||||
}
|
||||
|
||||
type MessageSyncStep2 = {
|
||||
type: 'sync step 2',
|
||||
os: Array<Operation>,
|
||||
deleteSet: any,
|
||||
stateSet: any
|
||||
}
|
||||
|
||||
type MessageUpdate = {
|
||||
type: 'update',
|
||||
ops: Array<Operation>
|
||||
}
|
||||
|
||||
type MessageSyncDone = {
|
||||
type: 'sync done'
|
||||
}
|
||||
|
||||
type Message = MessageSyncStep1 | MessageSyncStep2 | MessageUpdate | MessageSyncDone
|
||||
|
||||
0
declarations/Type.js
Normal file
0
declarations/Type.js
Normal file
34
declarations/Y.js
Normal file
34
declarations/Y.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* @flow */
|
||||
|
||||
type YGlobal = {
|
||||
utils: Object,
|
||||
Struct: any,
|
||||
AbstractDatabase: any,
|
||||
AbstractConnector: any,
|
||||
Transaction: any
|
||||
}
|
||||
|
||||
type YConfig = {
|
||||
db: Object,
|
||||
connector: Object,
|
||||
root: Object
|
||||
}
|
||||
|
||||
type TypeName = 'array' | 'map' | 'text'
|
||||
|
||||
declare var YConcurrency_TestingMode : boolean
|
||||
|
||||
type Transaction<A> = Generator<any, A, any>
|
||||
|
||||
type SyncRole = 'master' | 'slave'
|
||||
|
||||
declare class Store {
|
||||
find: (id:Id) => Transaction<any>;
|
||||
put: (n:any) => Transaction<void>;
|
||||
delete: (id:Id) => Transaction<void>;
|
||||
findWithLowerBound: (start:Id) => Transaction<any>;
|
||||
findWithUpperBound: (end:Id) => Transaction<any>;
|
||||
findNext: (id:Id) => Transaction<any>;
|
||||
findPrev: (id:Id) => Transaction<any>;
|
||||
iterate: (t:any,start:?Id,end:?Id,gen:any) => Transaction<any>;
|
||||
}
|
||||
1
dist
Submodule
1
dist
Submodule
Submodule dist added at 8739fd3a9c
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css" media="screen">
|
||||
#aceContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
.inserted {
|
||||
position:absolute;
|
||||
z-index:20;
|
||||
background-color: #FFC107;
|
||||
}
|
||||
.deleted {
|
||||
position:absolute;
|
||||
z-index:20;
|
||||
background-color: #FFC107;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="aceContainer"></div>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
/* global Y, ace */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'ace-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
ace: 'Text' // y.share.textarea is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yAce = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
var editor = ace.edit('aceContainer')
|
||||
editor.setTheme('ace/theme/chrome')
|
||||
editor.getSession().setMode('ace/mode/javascript')
|
||||
|
||||
y.share.ace.bindAce(editor)
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "yjs-examples",
|
||||
"version": "0.0.0",
|
||||
"homepage": "y-js.org",
|
||||
"authors": [
|
||||
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
||||
],
|
||||
"description": "Examples for Yjs",
|
||||
"license": "MIT",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"quill": "^1.0.0-rc.2",
|
||||
"ace": "~1.2.3",
|
||||
"ace-builds": "~1.2.3",
|
||||
"jquery": "~2.2.2",
|
||||
"d3": "^3.5.16",
|
||||
"codemirror": "^5.25.0"
|
||||
}
|
||||
}
|
||||
@@ -1,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>
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
<style>
|
||||
.CodeMirror {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
path {
|
||||
fill: none;
|
||||
stroke: blue;
|
||||
stroke-width: 1px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
</style>
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
/* globals Y, d3 */
|
||||
'strict mode'
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example',
|
||||
url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
drawing: 'Array'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yDrawing = y
|
||||
var drawing = y.share.drawing
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basis')
|
||||
|
||||
var svg = d3.select('#drawingCanvas')
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
// we only implement insert events that are appended to the end of the array
|
||||
event.values.forEach(function (value) {
|
||||
line.datum().push(value)
|
||||
})
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
y.share.drawing.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
event.values.forEach(drawLine)
|
||||
} else {
|
||||
// just remove all elements (thats what we do anyway)
|
||||
svg.selectAll('path').remove()
|
||||
}
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i))
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
sharedLine = drawing.get(drawing.length - 1)
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 33)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'html-editor-example6'
|
||||
},
|
||||
share: {
|
||||
xml: 'XmlFragment()' // y.share.xml is of type Y.Xml with tagname "p"
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yXml = y
|
||||
// Bind children of XmlFragment to the document.body
|
||||
window.yXml.share.xml.bindToDom(document.body)
|
||||
})
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: 7px;
|
||||
}
|
||||
.one {
|
||||
grid-column: 1 ;
|
||||
}
|
||||
.two {
|
||||
grid-column: 2;
|
||||
}
|
||||
.three {
|
||||
grid-column: 3;
|
||||
}
|
||||
textarea {
|
||||
width: calc(100% - 10px)
|
||||
}
|
||||
.editor-container {
|
||||
background-color: #4caf50;
|
||||
padding: 4px 5px 10px 5px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
.editor-container[disconnected] {
|
||||
background-color: red;
|
||||
}
|
||||
.disconnected-info {
|
||||
display: none;
|
||||
}
|
||||
.editor-container[disconnected] .disconnected-info {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
<div class="wrapper">
|
||||
<div id="container1" class="one editor-container">
|
||||
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container2" class="two editor-container">
|
||||
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container3" class="three editor-container">
|
||||
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/dist/y-text.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,64 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y1 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea1'))
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-second.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y2 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea2'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container2').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container2').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-third.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y3 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea3'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container3').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container3').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
|
||||
<g>
|
||||
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
|
||||
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
|
||||
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
|
||||
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
/* @flow */
|
||||
/* global Y, d3 */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Puzzle-example',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
share: {
|
||||
piece1: 'Map',
|
||||
piece2: 'Map',
|
||||
piece3: 'Map',
|
||||
piece4: 'Map'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yJigsaw = y
|
||||
var origin // mouse start position - translation of piece
|
||||
var drag = d3.behavior.drag()
|
||||
.on('dragstart', function (params) {
|
||||
// get the translation of the element
|
||||
var translation = d3
|
||||
.select(this)
|
||||
.attr('transform')
|
||||
.slice(10, -1)
|
||||
.split(',')
|
||||
.map(Number)
|
||||
// mouse coordinates
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
origin = {
|
||||
x: mouse[0] - translation[0],
|
||||
y: mouse[1] - translation[1]
|
||||
}
|
||||
})
|
||||
.on('drag', function () {
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||
var y = mouse[1] - origin.y
|
||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||
})
|
||||
.on('dragend', function (piece, i) {
|
||||
// save the current translation of the puzzle piece
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x
|
||||
var y = mouse[1] - origin.y
|
||||
piece.set('translation', {x: x, y: y})
|
||||
})
|
||||
|
||||
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||
|
||||
pieces
|
||||
.classed('draggable', true)
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
}).call(drag)
|
||||
|
||||
data.forEach(function (piece) {
|
||||
piece.observe(function () {
|
||||
// whenever a property of a piece changes, update the translation of the pieces
|
||||
pieces
|
||||
.transition()
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,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>
|
||||
@@ -1,30 +0,0 @@
|
||||
/* global Y, monaco */
|
||||
|
||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// Initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'monaco-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
monaco: 'Text' // y.share.monaco is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yMonaco = y
|
||||
|
||||
// Create Monaco editor
|
||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||
language: 'javascript'
|
||||
})
|
||||
|
||||
// Bind to y.share.monaco
|
||||
y.share.monaco.bindMonaco(editor)
|
||||
})
|
||||
})
|
||||
1173
examples/package-lock.json
generated
1173
examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "rollup -c",
|
||||
"watch": "rollup -cw"
|
||||
},
|
||||
"author": "Kevin Jahns",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"monaco-editor": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^10.0.2"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": ["bower_components"]
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
-->
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,40 +0,0 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example-quill-1.0-test',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yQuill = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'yjs-dist.esm',
|
||||
dest: 'yjs-dist.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
-->
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,49 +0,0 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// register yjs service worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Register service worker
|
||||
// it is important to copy yjs-sw-template to the root directory!
|
||||
navigator.serviceWorker.register('./yjs-sw-template.js').then(function (reg) {
|
||||
console.log('Yjs service worker registration succeeded. Scope is ' + reg.scope)
|
||||
}).catch(function (err) {
|
||||
console.error('Yjs service worker registration failed with error ' + err)
|
||||
})
|
||||
}
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'serviceworker',
|
||||
room: 'ServiceWorkerExample2'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yServiceWorker = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
/* eslint-env worker */
|
||||
|
||||
// copy and modify this file
|
||||
|
||||
self.DBConfig = {
|
||||
name: 'indexeddb'
|
||||
}
|
||||
self.ConnectorConfig = {
|
||||
name: 'websockets-client',
|
||||
// url: '..',
|
||||
options: {
|
||||
jsonp: false
|
||||
}
|
||||
}
|
||||
|
||||
importScripts(
|
||||
'/bower_components/yjs/y.js',
|
||||
'/bower_components/y-memory/y-memory.js',
|
||||
'/bower_components/y-indexeddb/y-indexeddb.js',
|
||||
'/bower_components/y-websockets-client/y-websockets-client.js',
|
||||
'/bower_components/y-serviceworker/yjs-sw-include.js'
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/y-text.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example2',
|
||||
// url: '//localhost:1234',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
},
|
||||
timeout: 5000 // reject if no connection was established within 5 seconds
|
||||
}).then(function (y) {
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
y.share.textarea.bind(document.getElementById('textfield'))
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Shared DOM Example </h1>
|
||||
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
|
||||
</div>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
|
||||
</div>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var commands = document.querySelectorAll(".command");
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
|
||||
var execute = function(){
|
||||
eval(command.querySelector("input").value);
|
||||
}
|
||||
command.querySelector("button").onclick = execute
|
||||
$(command.querySelector("input")).keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
execute()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'Xml-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yXml = y
|
||||
// bind xml type to a dom, and put it in body
|
||||
window.sharedDom = y.share.xml.getDom()
|
||||
document.body.appendChild(window.sharedDom)
|
||||
})
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
import Y from '../src/y.js'
|
||||
import yArray from '../../y-array/src/y-array.js'
|
||||
import yIndexedDB from '../../y-indexeddb/src/y-indexeddb.js'
|
||||
import yMap from '../../y-map/src/y-map.js'
|
||||
import yText from '../../y-text/src/Text.js'
|
||||
import yXml from '../../y-xml/src/y-xml.js'
|
||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||
|
||||
Y.extend(yArray, yIndexedDB, yMap, yText, yXml, yWebsocketsClient)
|
||||
|
||||
export default Y
|
||||
214
gulpfile.helper.js
Normal file
214
gulpfile.helper.js
Normal file
@@ -0,0 +1,214 @@
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
var header = require('gulp-header')
|
||||
var banner = ['/**',
|
||||
' * <%= pkg.name %> - <%= pkg.description %>',
|
||||
' * @version v<%= pkg.version %>',
|
||||
' * @link <%= pkg.homepage %>',
|
||||
' * @license <%= pkg.license %>',
|
||||
' */',
|
||||
''].join('\n')
|
||||
|
||||
gulp.task('dist:es5', function () {
|
||||
var babelOptions = {
|
||||
presets: ['es2015']
|
||||
}
|
||||
return (browserify({
|
||||
entries: files.distEs5,
|
||||
debug: true,
|
||||
standalone: options.moduleName
|
||||
}).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(header(banner, { pkg: require('./package.json') }))
|
||||
.pipe($.sourcemaps.write('.'))
|
||||
.pipe(gulp.dest('./dist/')))
|
||||
})
|
||||
|
||||
gulp.task('dist:es6', function () {
|
||||
return (browserify({
|
||||
entries: files.dist,
|
||||
debug: true,
|
||||
standalone: options.moduleName
|
||||
}).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(header(banner, { pkg: require('./package.json') }))
|
||||
.pipe($.sourcemaps.write('.'))
|
||||
.pipe(gulp.dest('./dist/')))
|
||||
})
|
||||
|
||||
gulp.task('dist', ['dist:es6', 'dist:es5'])
|
||||
|
||||
gulp.task('watch:dist', function (cb) {
|
||||
options.debug = true
|
||||
gulp.src(['./README.md'])
|
||||
.pipe($.watch('./README.md'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
runSequence('dist', function () {
|
||||
gulp.watch(files.src.concat('./README.md'), ['dist'])
|
||||
cb()
|
||||
})
|
||||
})
|
||||
|
||||
gulp.task('dev:node', ['test'], function () {
|
||||
gulp.watch(files.src, ['test'])
|
||||
})
|
||||
|
||||
gulp.task('spec-build', function () {
|
||||
var browserify = require('browserify')
|
||||
var source = require('vinyl-source-stream')
|
||||
var buffer = require('vinyl-buffer')
|
||||
|
||||
return browserify({
|
||||
entries: files.specs, // .concat(files.distEs5),
|
||||
debug: true
|
||||
})// .transform('babelify', { presets: ['es2015'] })
|
||||
.bundle()
|
||||
.pipe(source('specs.js'))
|
||||
.pipe(buffer())
|
||||
// .pipe($.sourcemaps.init({loadMaps: true}))
|
||||
// .pipe($.sourcemaps.write('.'))
|
||||
.pipe(gulp.dest('./build/'))
|
||||
})
|
||||
|
||||
gulp.task('dev:browser', ['spec-build'], function () {
|
||||
gulp.watch(files.src, ['spec-build'])
|
||||
return gulp.src('./build/specs.js')
|
||||
.pipe($.jasmineBrowser.specRunner())
|
||||
.pipe($.jasmineBrowser.server({port: options.port}))
|
||||
})
|
||||
|
||||
gulp.task('test', function () {
|
||||
return gulp.src(files.specs)
|
||||
.pipe($.jasmine({
|
||||
verbose: true,
|
||||
includeStuckTrace: true
|
||||
}))
|
||||
})
|
||||
|
||||
gulp.task('updateSubmodule', function () {
|
||||
return gulp.src('./package.json', {read: false})
|
||||
.pipe($.shell([
|
||||
'git submodule update --init',
|
||||
'cd dist && git pull origin dist'
|
||||
]))
|
||||
})
|
||||
|
||||
gulp.task('bump', function (cb) {
|
||||
gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
||||
.pipe($.prompt.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'bump',
|
||||
message: 'What type of bump would you like to do?',
|
||||
choices: ['patch', 'minor', 'major']
|
||||
}, function (res) {
|
||||
if (res.bump.length === 0) {
|
||||
console.info('You have to select a bump type. Now I\'m going to use "patch" as bump type..')
|
||||
}
|
||||
var bumptype = res.bump[0]
|
||||
if (bumptype === 'major') {
|
||||
runSequence('bump_major', cb)
|
||||
} else if (bumptype === 'minor') {
|
||||
runSequence('bump_minor', cb)
|
||||
} else {
|
||||
runSequence('bump_patch', cb)
|
||||
}
|
||||
}))
|
||||
})
|
||||
gulp.task('bump_patch', function () {
|
||||
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
||||
.pipe($.bump({type: 'patch'}))
|
||||
.pipe(gulp.dest('./'))
|
||||
})
|
||||
gulp.task('bump_minor', function () {
|
||||
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
||||
.pipe($.bump({type: 'minor'}))
|
||||
.pipe(gulp.dest('./'))
|
||||
})
|
||||
gulp.task('bump_major', function () {
|
||||
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
|
||||
.pipe($.bump({type: 'major'}))
|
||||
.pipe(gulp.dest('./'))
|
||||
})
|
||||
|
||||
gulp.task('publish_commits', function () {
|
||||
return gulp.src('./package.json')
|
||||
.pipe($.prompt.confirm({
|
||||
message: 'Are you sure you want to publish this release?',
|
||||
default: false
|
||||
}))
|
||||
.pipe($.shell([
|
||||
'cp README.md dist',
|
||||
'standard',
|
||||
'echo "Deploying version <%= getVersion(file.path) %>"',
|
||||
'git pull',
|
||||
'cd ./dist/ && git add -A',
|
||||
'cd ./dist/ && git commit -am "Deploy <%= getVersion(file.path) %>" -n',
|
||||
'cd ./dist/ && git push origin HEAD:dist',
|
||||
'cd ./dist/ && git tag -a v<%= getVersion(file.path) %> -m "Release <%= getVersion(file.path) %>"',
|
||||
'cd ./dist/ && git push origin --tags',
|
||||
'git commit -am "Release <%= getVersion(file.path) %>" -n',
|
||||
'git push',
|
||||
'npm publish',
|
||||
'echo Finished'
|
||||
], {
|
||||
templateData: {
|
||||
getVersion: function () {
|
||||
return JSON.parse(String.fromCharCode.apply(null, this.file._contents)).version
|
||||
}
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
gulp.task('publish', function (cb) {
|
||||
/* TODO: include 'test',*/
|
||||
runSequence('updateSubmodule', 'bump', 'dist', 'publish_commits', cb)
|
||||
})
|
||||
}
|
||||
104
gulpfile.js
Normal file
104
gulpfile.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/* eslint-env node */
|
||||
|
||||
/** Gulp Commands
|
||||
|
||||
gulp command*
|
||||
[--export ModuleType]
|
||||
[--name ModuleName]
|
||||
[--testport TestPort]
|
||||
[--testfiles TestFiles]
|
||||
|
||||
Module name (ModuleName):
|
||||
Compile this to "y.js" (default)
|
||||
|
||||
Supported module types (ModuleType):
|
||||
- amd
|
||||
- amdStrict
|
||||
- common
|
||||
- commonStrict
|
||||
- ignore (default)
|
||||
- system
|
||||
- umd
|
||||
- umdStrict
|
||||
|
||||
Test port (TestPort):
|
||||
Serve the specs on port 8888 (default)
|
||||
|
||||
Test files (TestFiles):
|
||||
Specify which specs to use!
|
||||
|
||||
Commands:
|
||||
- build:deploy
|
||||
Build this library for deployment (es6->es5, minified)
|
||||
- dev:browser
|
||||
Watch the ./src directory.
|
||||
Builds the library on changes.
|
||||
Starts an http-server and serves the test suite on http://127.0.0.1:8888.
|
||||
- dev:node
|
||||
Watch the ./src directory.
|
||||
Builds and specs the library on changes.
|
||||
Usefull to run with node-inspector.
|
||||
`node-debug $(which gulp) dev:node
|
||||
- test:
|
||||
Test this library
|
||||
*/
|
||||
|
||||
var gulp = require('gulp')
|
||||
var $ = require('gulp-load-plugins')()
|
||||
var runSequence = require('run-sequence').use(gulp)
|
||||
|
||||
require('./gulpfile.helper.js')(gulp, {
|
||||
polyfills: [],
|
||||
entry: './src/y.js',
|
||||
targetName: 'y.js',
|
||||
moduleName: 'Y',
|
||||
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')
|
||||
}
|
||||
}))
|
||||
})
|
||||
4328
package-lock.json
generated
4328
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
82
package.json
82
package.json
@@ -1,26 +1,24 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-19",
|
||||
"version": "12.3.3",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"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",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc 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": {
|
||||
@@ -44,27 +42,41 @@
|
||||
},
|
||||
"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-header": "^1.8.8",
|
||||
"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": "^2.0.0",
|
||||
"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"
|
||||
"debug": "^2.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import uglify from 'rollup-plugin-uglify'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'src/y.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
babel(),
|
||||
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}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -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}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -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/*.js',
|
||||
moduleName: 'y-tests',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
],
|
||||
dest: 'y.test.js',
|
||||
sourceMap: true
|
||||
}
|
||||
507
src/Connector.js
507
src/Connector.js
@@ -1,23 +1,41 @@
|
||||
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||
import { sendSyncStep1, computeMessageSyncStep1, computeMessageSyncStep2, computeMessageUpdate } from './MessageHandler.js'
|
||||
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
||||
function canWrite (auth) { return auth === 'write' }
|
||||
|
||||
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;
|
||||
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') {
|
||||
@@ -29,57 +47,57 @@ export default function extendConnector (Y/* :any */) {
|
||||
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.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())
|
||||
}
|
||||
if (opts.maxBufferLength == null) {
|
||||
this.maxBufferLength = -1
|
||||
} else {
|
||||
this.maxBufferLength = opts.maxBufferLength
|
||||
if (opts.generateUserId === true) {
|
||||
this.setUserId(Y.utils.generateGuid())
|
||||
}
|
||||
}
|
||||
resetAuth (auth) {
|
||||
if (this.authInfo !== auth) {
|
||||
this.authInfo = auth
|
||||
this.broadcast({
|
||||
type: 'auth',
|
||||
auth: this.authInfo
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
reconnect () {
|
||||
this.log('reconnecting..')
|
||||
return this.y.db.startGarbageCollector()
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
this.log('discronnecting..')
|
||||
this.connections = new Map()
|
||||
this.connections = {}
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.syncingClients = []
|
||||
this.whenSyncedListeners = []
|
||||
this.y.db.stopGarbageCollector()
|
||||
return this.y.db.whenTransactionsFinished()
|
||||
}
|
||||
|
||||
repair () {
|
||||
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
|
||||
for (var name in this.connections) {
|
||||
this.connections[name].isSynced = false
|
||||
}
|
||||
this.isSynced = false
|
||||
this.connections.forEach((user, userId) => {
|
||||
user.isSynced = false
|
||||
this._syncWithUser(userId)
|
||||
})
|
||||
this.currentSyncTarget = null
|
||||
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)
|
||||
@@ -87,21 +105,23 @@ export default function extendConnector (Y/* :any */) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onUserEvent (f) {
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
|
||||
removeUserEventListener (f) {
|
||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||
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) {
|
||||
this.log('User left: %s', user)
|
||||
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',
|
||||
@@ -110,26 +130,18 @@ 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.log('User joined: %s', 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',
|
||||
@@ -137,9 +149,10 @@ 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
|
||||
whenSynced (f) {
|
||||
@@ -149,45 +162,58 @@ 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!"
|
||||
findNextSyncTarget () {
|
||||
if (this.currentSyncTarget != null) {
|
||||
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: this.garbageCollectAfterSync()
|
||||
// call whensynced listeners
|
||||
for (var f of this.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = []
|
||||
var syncUser = null
|
||||
for (var uid in this.connections) {
|
||||
if (!this.connections[uid].isSynced) {
|
||||
syncUser = uid
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
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')
|
||||
var conn = this
|
||||
if (syncUser != null) {
|
||||
this.currentSyncTarget = syncUser
|
||||
this.y.db.requestTransaction(function *() {
|
||||
var stateSet = yield* this.getStateSet()
|
||||
var deleteSet = yield* this.getDeleteSet()
|
||||
var answer = {
|
||||
type: 'sync step 1',
|
||||
stateSet: stateSet,
|
||||
deleteSet: deleteSet,
|
||||
protocolVersion: conn.protocolVersion,
|
||||
auth: conn.authInfo
|
||||
}
|
||||
conn.send(syncUser, answer)
|
||||
})
|
||||
} else {
|
||||
if (!conn.isSynced) {
|
||||
this.y.db.requestTransaction(function *() {
|
||||
if (!conn.isSynced) {
|
||||
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||
conn.isSynced = true
|
||||
yield* this.garbageCollectAfterSync()
|
||||
// call whensynced listeners
|
||||
for (var f of conn.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
conn.whenSyncedListeners = []
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
|
||||
this.logMessage('Message: %Y', buffer)
|
||||
}
|
||||
|
||||
send (uid, message) {
|
||||
this.log('Send \'%s\' to %s', message.type, uid)
|
||||
this.logMessage('Message: %j', message)
|
||||
}
|
||||
broadcast (message) {
|
||||
this.log('Broadcast \'%s\'', message.type)
|
||||
this.logMessage('Message: %j', message)
|
||||
}
|
||||
/*
|
||||
Buffer operations, and broadcast them when ready.
|
||||
*/
|
||||
@@ -198,105 +224,260 @@ 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
|
||||
let length = ops.length
|
||||
let encoderPosLen = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
for (var i = 0; i < length && (self.maxBufferLength < 0 || encoder.length < self.maxBufferLength); i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
encoder.setUint32(encoderPosLen, i)
|
||||
self.broadcastOpBuffer = ops.slice(i)
|
||||
self.broadcast(encoder.createBuffer())
|
||||
if (i !== length) {
|
||||
setTimeout(broadcastOperations, 100)
|
||||
}
|
||||
self.broadcast({
|
||||
type: 'update',
|
||||
ops: self.broadcastOpBuffer
|
||||
})
|
||||
self.broadcastOpBuffer = []
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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()
|
||||
}
|
||||
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!')
|
||||
this.log('Receive \'%s\' from %s', message.type, sender)
|
||||
this.logMessage('Message: %j', message)
|
||||
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
||||
this.log(
|
||||
`You tried to sync with a yjs instance that has a different protocol version
|
||||
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
||||
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
||||
`)
|
||||
this.send(sender, {
|
||||
type: 'sync stop',
|
||||
protocolVersion: this.protocolVersion
|
||||
})
|
||||
return Promise.reject('Incompatible protocol version')
|
||||
}
|
||||
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 = []
|
||||
if (message.auth != null && this.connections[sender] != null) {
|
||||
// authenticate using auth in message
|
||||
var auth = this.checkAuth(message.auth, this.y, sender)
|
||||
this.connections[sender].auth = auth
|
||||
auth.then(auth => {
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userAuthenticated',
|
||||
user: sender,
|
||||
auth: auth
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
|
||||
// authenticate without otherwise
|
||||
this.connections[sender].auth = this.checkAuth(null, this.y, sender)
|
||||
}
|
||||
if (this.connections[sender] != null && this.connections[sender].auth != null) {
|
||||
return this.connections[sender].auth.then((auth) => {
|
||||
if (message.type === 'sync step 1' && canRead(auth)) {
|
||||
let conn = this
|
||||
let m = message
|
||||
|
||||
return messages.reduce((p, m) =>
|
||||
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
|
||||
, Promise.resolve())
|
||||
})
|
||||
this.y.db.requestTransaction(function *() {
|
||||
var currentStateSet = yield* this.getStateSet()
|
||||
if (canWrite(auth)) {
|
||||
yield* this.applyDeleteSet(m.deleteSet)
|
||||
}
|
||||
|
||||
var ds = yield* this.getDeleteSet()
|
||||
var answer = {
|
||||
type: 'sync step 2',
|
||||
stateSet: currentStateSet,
|
||||
deleteSet: ds,
|
||||
protocolVersion: this.protocolVersion,
|
||||
auth: this.authInfo
|
||||
}
|
||||
answer.os = yield* this.getOperations(m.stateSet)
|
||||
conn.send(sender, answer)
|
||||
if (this.forwardToSyncingClients) {
|
||||
conn.syncingClients.push(sender)
|
||||
setTimeout(function () {
|
||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
||||
return cli !== sender
|
||||
})
|
||||
conn.send(sender, {
|
||||
type: 'sync done'
|
||||
})
|
||||
}, 5000) // TODO: conn.syncingClientDuration)
|
||||
} else {
|
||||
conn.send(sender, {
|
||||
type: 'sync done'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
||||
var db = this.y.db
|
||||
var defer = {}
|
||||
defer.promise = new Promise(function (resolve) {
|
||||
defer.resolve = resolve
|
||||
})
|
||||
this.syncStep2 = defer.promise
|
||||
let m /* :MessageSyncStep2 */ = message
|
||||
db.requestTransaction(function * () {
|
||||
yield* this.applyDeleteSet(m.deleteSet)
|
||||
if (m.osUntransformed != null) {
|
||||
yield* this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
|
||||
} else {
|
||||
this.store.apply(m.os)
|
||||
}
|
||||
/*
|
||||
* This just sends the complete hb after some time
|
||||
* Mostly for debugging..
|
||||
*
|
||||
db.requestTransaction(function * () {
|
||||
var ops = yield* this.getOperations(m.stateSet)
|
||||
if (ops.length > 0) {
|
||||
if (!broadcastHB) { // TODO: consider to broadcast here..
|
||||
conn.send(sender, {
|
||||
type: 'update',
|
||||
ops: ops
|
||||
})
|
||||
} else {
|
||||
// broadcast only once!
|
||||
conn.broadcastOps(ops)
|
||||
}
|
||||
}
|
||||
})
|
||||
*/
|
||||
defer.resolve()
|
||||
})
|
||||
} else if (message.type === 'sync done') {
|
||||
var self = this
|
||||
this.syncStep2.then(function () {
|
||||
self._setSyncedWith(sender)
|
||||
})
|
||||
} else if (message.type === 'update' && canWrite(auth)) {
|
||||
if (this.forwardToSyncingClients) {
|
||||
for (var client of this.syncingClients) {
|
||||
this.send(client, message)
|
||||
}
|
||||
}
|
||||
if (this.y.db.forwardAppliedOperations) {
|
||||
var delops = message.ops.filter(function (o) {
|
||||
return o.struct === 'Delete'
|
||||
})
|
||||
if (delops.length > 0) {
|
||||
this.broadcastOps(delops)
|
||||
}
|
||||
}
|
||||
this.y.db.apply(message.ops)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return Promise.reject('Unable to deliver message')
|
||||
}
|
||||
}
|
||||
_setSyncedWith (user) {
|
||||
var conn = this.connections[user]
|
||||
if (conn != null) {
|
||||
conn.isSynced = true
|
||||
}
|
||||
if (user === this.currentSyncTarget) {
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
}
|
||||
/*
|
||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||
too much overhead. Y is very likely to get changed a lot in the future
|
||||
|
||||
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
|
||||
we encode the JSON as XML.
|
||||
|
||||
When the HB support encoding as XML, the format should look pretty much like this.
|
||||
|
||||
does not support primitive values as array elements
|
||||
expects an ltx (less than xml) object
|
||||
*/
|
||||
parseMessageFromXml (m/* :any */) {
|
||||
function parseArray (node) {
|
||||
for (var n of node.children) {
|
||||
if (n.getAttribute('isArray') === 'true') {
|
||||
return parseArray(n)
|
||||
} else {
|
||||
return parseObject(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (skipAuth || senderConn.auth != null) {
|
||||
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||
} else {
|
||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
|
||||
function parseObject (node/* :any */) {
|
||||
var json = {}
|
||||
for (var attrName in node.attrs) {
|
||||
var value = node.attrs[attrName]
|
||||
var int = parseInt(value, 10)
|
||||
if (isNaN(int) || ('' + int) !== value) {
|
||||
json[attrName] = value
|
||||
} else {
|
||||
json[attrName] = int
|
||||
}
|
||||
}
|
||||
for (var n/* :any */ in node.children) {
|
||||
var name = n.name
|
||||
if (n.getAttribute('isArray') === 'true') {
|
||||
json[name] = parseArray(n)
|
||||
} else {
|
||||
json[name] = parseObject(n)
|
||||
}
|
||||
}
|
||||
return json
|
||||
}
|
||||
parseObject(m)
|
||||
}
|
||||
|
||||
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)
|
||||
/*
|
||||
encode message in xml
|
||||
we use string because Strophe only accepts an "xml-string"..
|
||||
So {a:4,b:{c:5}} will look like
|
||||
<y a="4">
|
||||
<b c="5"></b>
|
||||
</y>
|
||||
m - ltx element
|
||||
json - Object
|
||||
*/
|
||||
encodeMessageToXml (msg, obj) {
|
||||
// attributes is optional
|
||||
function encodeObject (m, json) {
|
||||
for (var name in json) {
|
||||
var value = json[name]
|
||||
if (name == null) {
|
||||
// nop
|
||||
} else if (value.constructor === Object) {
|
||||
encodeObject(m.c(name), value)
|
||||
} else if (value.constructor === Array) {
|
||||
encodeArray(m.c(name), value)
|
||||
} else {
|
||||
m.setAttribute(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
function encodeArray (m, array) {
|
||||
m.setAttribute('isArray', 'true')
|
||||
for (var e of array) {
|
||||
if (e.constructor === Object) {
|
||||
encodeObject(m.c('array-element'), e)
|
||||
} else {
|
||||
encodeArray(m.c('array-element'), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (obj.constructor === Object) {
|
||||
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||
} else if (obj.constructor === Array) {
|
||||
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
||||
} else {
|
||||
return Promise.reject(new Error('Unable to receive message'))
|
||||
}
|
||||
}
|
||||
|
||||
_setSyncedWith (user) {
|
||||
if (user != null) {
|
||||
this.connections.get(user).isSynced = true
|
||||
}
|
||||
let conns = Array.from(this.connections.values())
|
||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||
this._fireIsSyncedListeners()
|
||||
throw new Error("I can't encode this json!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
178
src/Connectors/Test.js
Normal file
178
src/Connectors/Test.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/* 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 self = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
// The connector first has to send the messages to the db.
|
||||
// Wait for the checkAuth-function to resolve
|
||||
// The test lib only has a simple checkAuth function: `() => Promise.resolve()`
|
||||
// Just add a function to the event-queue, in order to wait for the event.
|
||||
// TODO: this may be buggy in test applications (but it isn't be for real-life apps)
|
||||
setTimeout(function () {
|
||||
var ps = []
|
||||
for (var name in self.users) {
|
||||
ps.push(self.users[name].y.db.whenTransactionsFinished())
|
||||
}
|
||||
Promise.all(ps).then(resolve, reject)
|
||||
}, 10)
|
||||
})
|
||||
},
|
||||
flushOne: function flushOne () {
|
||||
var bufs = []
|
||||
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]
|
||||
return user.receiveMessage(m[0], m[1]).then(function () {
|
||||
return user.y.db.whenTransactionsFinished()
|
||||
}, function () {})
|
||||
} 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 {
|
||||
c = globalRoom.flushOne()
|
||||
if (c) {
|
||||
c.then(function () {
|
||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return 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 () {
|
||||
var waitForMe = Promise.resolve()
|
||||
if (!this.isDisconnected()) {
|
||||
globalRoom.removeUser(this.userId)
|
||||
waitForMe = super.disconnect()
|
||||
}
|
||||
var self = this
|
||||
return waitForMe.then(function () {
|
||||
return self.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]
|
||||
}
|
||||
yield this.receiveMessage(m[0], m[1])
|
||||
}
|
||||
yield self.whenTransactionsFinished()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Y.Test = Test
|
||||
}
|
||||
133
src/Database.js
133
src/Database.js
@@ -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,14 @@ 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,7 +71,7 @@ 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
|
||||
@@ -80,16 +79,16 @@ export default function extendDatabase (Y /* :any */) {
|
||||
|
||||
function garbageCollect () {
|
||||
return os.whenTransactionsFinished().then(function () {
|
||||
if (os.gcTimeout > 0 && (os.gc1.length > 0 || os.gc2.length > 0)) {
|
||||
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
||||
if (!os.y.connector.isSynced) {
|
||||
console.warn('gc should be empty when not synced!')
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
os.requestTransaction(function () {
|
||||
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]
|
||||
this.garbageCollectOperation(oid)
|
||||
yield* this.garbageCollectOperation(oid)
|
||||
}
|
||||
os.gc2 = os.gc1
|
||||
os.gc1 = []
|
||||
@@ -118,9 +117,9 @@ export default function extendDatabase (Y /* :any */) {
|
||||
this.startRepairCheck()
|
||||
}
|
||||
startGarbageCollector () {
|
||||
this.gc = this.dbOpts.gc
|
||||
this.gc = this.dbOpts.gc == null || this.dbOpts.gc
|
||||
if (this.gc) {
|
||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
|
||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 50000 : this.dbOpts.gcTimeout
|
||||
} else {
|
||||
this.gcTimeout = -1
|
||||
}
|
||||
@@ -178,7 +177,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
|
||||
@@ -197,15 +196,15 @@ export default function extendDatabase (Y /* :any */) {
|
||||
this.gc = false
|
||||
this.gcTimeout = -1
|
||||
return new Promise(function (resolve) {
|
||||
self.requestTransaction(function () {
|
||||
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 = this.getOperation(ungc[i])
|
||||
var op = yield* this.getOperation(ungc[i])
|
||||
if (op != null) {
|
||||
delete op.gc
|
||||
this.setOperation(op)
|
||||
yield* this.setOperation(op)
|
||||
}
|
||||
}
|
||||
resolve()
|
||||
@@ -224,7 +223,7 @@ export default function extendDatabase (Y /* :any */) {
|
||||
|
||||
returns true iff op was added to GC
|
||||
*/
|
||||
addToGarbageCollector (op, left) {
|
||||
* addToGarbageCollector (op, left) {
|
||||
if (
|
||||
op.gc == null &&
|
||||
op.deleted === true &&
|
||||
@@ -235,12 +234,12 @@ export default function extendDatabase (Y /* :any */) {
|
||||
if (left != null && left.deleted === true) {
|
||||
gc = true
|
||||
} else if (op.content != null && op.content.length > 1) {
|
||||
op = 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
|
||||
this.setOperation(op)
|
||||
yield* this.setOperation(op)
|
||||
this.store.queueGarbageCollector(op.id)
|
||||
return true
|
||||
}
|
||||
@@ -265,8 +264,8 @@ export default function extendDatabase (Y /* :any */) {
|
||||
}
|
||||
}
|
||||
}
|
||||
destroy () {
|
||||
clearTimeout(this.gcInterval)
|
||||
* destroy () {
|
||||
clearInterval(this.gcInterval)
|
||||
this.gcInterval = null
|
||||
this.stopRepairCheck()
|
||||
}
|
||||
@@ -274,9 +273,9 @@ export default function extendDatabase (Y /* :any */) {
|
||||
if (!this.userIdPromise.inProgress) {
|
||||
this.userIdPromise.inProgress = true
|
||||
var self = this
|
||||
self.requestTransaction(function () {
|
||||
self.requestTransaction(function * () {
|
||||
self.userId = userId
|
||||
var state = this.getState(userId)
|
||||
var state = yield* this.getState(userId)
|
||||
self.opClock = state.clock
|
||||
self.userIdPromise.resolve(userId)
|
||||
})
|
||||
@@ -306,12 +305,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) {
|
||||
@@ -355,7 +352,7 @@ export default function extendDatabase (Y /* :any */) {
|
||||
this.listenersByIdRequestPending = true
|
||||
var store = this
|
||||
|
||||
this.requestTransaction(function () {
|
||||
this.requestTransaction(function * () {
|
||||
var exeNow = store.listenersByIdExecuteNow
|
||||
store.listenersByIdExecuteNow = []
|
||||
|
||||
@@ -366,7 +363,7 @@ export default function extendDatabase (Y /* :any */) {
|
||||
|
||||
for (let key = 0; key < exeNow.length; key++) {
|
||||
let o = exeNow[key].op
|
||||
store.tryExecute.call(this, o)
|
||||
yield* store.tryExecute.call(this, o)
|
||||
}
|
||||
|
||||
for (var sid in ls) {
|
||||
@@ -374,9 +371,9 @@ export default function extendDatabase (Y /* :any */) {
|
||||
var id = JSON.parse(sid)
|
||||
var op
|
||||
if (typeof id[1] === 'string') {
|
||||
op = this.getOperation(id)
|
||||
op = yield* this.getOperation(id)
|
||||
} else {
|
||||
op = this.getInsertion(id)
|
||||
op = yield* this.getInsertion(id)
|
||||
}
|
||||
if (op == null) {
|
||||
store.listenersById[sid] = l
|
||||
@@ -385,7 +382,7 @@ export default function extendDatabase (Y /* :any */) {
|
||||
let listener = l[i]
|
||||
let o = listener.op
|
||||
if (--listener.missing === 0) {
|
||||
store.tryExecute.call(this, o)
|
||||
yield* store.tryExecute.call(this, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,15 +399,15 @@ export default function extendDatabase (Y /* :any */) {
|
||||
addOperation: any;
|
||||
whenOperationsExist: any;
|
||||
*/
|
||||
tryExecute (op) {
|
||||
this.store.addToDebug('this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||
* tryExecute (op) {
|
||||
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||
if (op.struct === 'Delete') {
|
||||
Y.Struct.Delete.execute.call(this, op)
|
||||
yield* Y.Struct.Delete.execute.call(this, op)
|
||||
// this is now called in Transaction.deleteOperation!
|
||||
// this.store.operationAdded(this, op)
|
||||
// yield* this.store.operationAdded(this, op)
|
||||
} else {
|
||||
// check if this op was defined
|
||||
var defined = 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 +416,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 = 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 = this.isGarbageCollected(opid)
|
||||
var isGarbageCollected = yield* this.isGarbageCollected(opid)
|
||||
if (!isGarbageCollected) {
|
||||
// TODO: reduce number of get / put calls for op ..
|
||||
Y.Struct[op.struct].execute.call(this, op)
|
||||
this.addOperation(op)
|
||||
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 = this.getOperation(opid)
|
||||
op = yield* this.getOperation(opid)
|
||||
// if insertion, try to combine with left
|
||||
this.tryCombineWithLeft(op)
|
||||
yield* this.tryCombineWithLeft(op)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,15 +449,15 @@ export default function extendDatabase (Y /* :any */) {
|
||||
* Always:
|
||||
* * Call type
|
||||
*/
|
||||
operationAdded (transaction, op) {
|
||||
* operationAdded (transaction, op) {
|
||||
if (op.struct === 'Delete') {
|
||||
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
|
||||
if (type != null) {
|
||||
type._changed(transaction, op)
|
||||
yield* type._changed(transaction, op)
|
||||
}
|
||||
} else {
|
||||
// increase SS
|
||||
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 +477,9 @@ export default function extendDatabase (Y /* :any */) {
|
||||
|
||||
// if parent is deleted, mark as gc'd and return
|
||||
if (op.parent != null) {
|
||||
var parentIsDeleted = transaction.isDeleted(op.parent)
|
||||
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
|
||||
if (parentIsDeleted) {
|
||||
transaction.deleteList(op.id)
|
||||
yield* transaction.deleteList(op.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -490,7 +487,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)
|
||||
t._changed(transaction, o)
|
||||
yield* t._changed(transaction, o)
|
||||
}
|
||||
if (!op.deleted) {
|
||||
// Delete if DS says this is actually deleted
|
||||
@@ -499,28 +496,27 @@ 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 = transaction.isDeleted(id)
|
||||
var opIsDeleted = yield* transaction.isDeleted(id)
|
||||
if (opIsDeleted) {
|
||||
var delop = {
|
||||
struct: 'Delete',
|
||||
target: id
|
||||
}
|
||||
this.tryExecute.call(transaction, delop)
|
||||
yield* this.tryExecute.call(transaction, delop)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -529,7 +525,6 @@ export default function extendDatabase (Y /* :any */) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is another transaction request.
|
||||
// * the last transaction is always a flush :)
|
||||
getNextRequest () {
|
||||
@@ -544,8 +539,8 @@ export default function extendDatabase (Y /* :any */) {
|
||||
return null
|
||||
} else {
|
||||
this.transactionIsFlushed = true
|
||||
return function () {
|
||||
this.flush()
|
||||
return function * () {
|
||||
yield* this.flush()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -572,13 +567,13 @@ export default function extendDatabase (Y /* :any */) {
|
||||
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) {
|
||||
* initType (id, args) {
|
||||
var sid = JSON.stringify(id)
|
||||
var t = this.store.initializedTypes[sid]
|
||||
if (t == null) {
|
||||
var op/* :MapStruct | ListStruct */ = this.getOperation(id)
|
||||
var op/* :MapStruct | ListStruct */ = yield* this.getOperation(id)
|
||||
if (op != null) {
|
||||
t = Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
|
||||
t = yield* Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
|
||||
this.store.initializedTypes[sid] = t
|
||||
}
|
||||
}
|
||||
@@ -590,14 +585,14 @@ export default function extendDatabase (Y /* :any */) {
|
||||
createType (typedefinition, id) {
|
||||
var structname = typedefinition[0].struct
|
||||
id = id || this.getNextOpId(1)
|
||||
var op = Y.Struct[structname].create(id, typedefinition[1])
|
||||
var op = Y.Struct[structname].create(id)
|
||||
op.type = typedefinition[0].name
|
||||
|
||||
this.requestTransaction(function () {
|
||||
if (op.id[0] === 0xFFFFFF) {
|
||||
this.setOperation(op)
|
||||
this.requestTransaction(function * () {
|
||||
if (op.id[0] === '_') {
|
||||
yield* this.setOperation(op)
|
||||
} else {
|
||||
this.applyCreatedOperations([op])
|
||||
yield* this.applyCreatedOperations([op])
|
||||
}
|
||||
})
|
||||
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])
|
||||
|
||||
354
src/Database.spec.js
Normal file
354
src/Database.spec.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/* global async, databases, describe, beforeEach, afterEach */
|
||||
/* eslint-env browser,jasmine,console */
|
||||
'use strict'
|
||||
|
||||
var Y = require('./SpecHelper.js')
|
||||
|
||||
for (let database of databases) {
|
||||
describe(`Database (${database})`, function () {
|
||||
var store
|
||||
describe('DeleteStore', function () {
|
||||
describe('Basic', function () {
|
||||
beforeEach(function () {
|
||||
store = new Y[database](null, {
|
||||
gcTimeout: -1,
|
||||
namespace: 'testing'
|
||||
})
|
||||
})
|
||||
afterEach(function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.store.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('Deleted operation is deleted', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['u1', 10], 1)
|
||||
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
||||
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['u1', 10], 1)
|
||||
yield* this.markDeleted(['u1', 11], 1)
|
||||
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
||||
expect(yield* this.isDeleted(['u1', 11])).toBeTruthy()
|
||||
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['0', 3], 1)
|
||||
yield* this.markDeleted(['0', 4], 1)
|
||||
yield* this.markDeleted(['0', 2], 1)
|
||||
expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #1', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['166', 0], 1)
|
||||
yield* this.markDeleted(['166', 2], 1)
|
||||
yield* this.markDeleted(['166', 0], 1)
|
||||
yield* this.markDeleted(['166', 2], 1)
|
||||
yield* this.markGarbageCollected(['166', 2], 1)
|
||||
yield* this.markDeleted(['166', 1], 1)
|
||||
yield* this.markDeleted(['166', 3], 1)
|
||||
yield* this.markGarbageCollected(['166', 3], 1)
|
||||
yield* this.markDeleted(['166', 0], 1)
|
||||
expect(yield* this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #2', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['293', 0], 1)
|
||||
yield* this.markDeleted(['291', 2], 1)
|
||||
yield* this.markDeleted(['291', 2], 1)
|
||||
yield* this.markGarbageCollected(['293', 0], 1)
|
||||
yield* this.markDeleted(['293', 1], 1)
|
||||
yield* this.markGarbageCollected(['291', 2], 1)
|
||||
expect(yield* this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #3', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['581', 0], 1)
|
||||
yield* this.markDeleted(['581', 1], 1)
|
||||
yield* this.markDeleted(['580', 0], 1)
|
||||
yield* this.markDeleted(['580', 0], 1)
|
||||
yield* this.markGarbageCollected(['581', 0], 1)
|
||||
yield* this.markDeleted(['581', 2], 1)
|
||||
yield* this.markDeleted(['580', 1], 1)
|
||||
yield* this.markDeleted(['580', 2], 1)
|
||||
yield* this.markDeleted(['580', 1], 1)
|
||||
yield* this.markDeleted(['580', 2], 1)
|
||||
yield* this.markGarbageCollected(['581', 2], 1)
|
||||
yield* this.markGarbageCollected(['581', 1], 1)
|
||||
yield* this.markGarbageCollected(['580', 1], 1)
|
||||
expect(yield* this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #4', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['544', 0], 1)
|
||||
yield* this.markDeleted(['543', 2], 1)
|
||||
yield* this.markDeleted(['544', 0], 1)
|
||||
yield* this.markDeleted(['543', 2], 1)
|
||||
yield* this.markGarbageCollected(['544', 0], 1)
|
||||
yield* this.markDeleted(['545', 1], 1)
|
||||
yield* this.markDeleted(['543', 4], 1)
|
||||
yield* this.markDeleted(['543', 3], 1)
|
||||
yield* this.markDeleted(['544', 1], 1)
|
||||
yield* this.markDeleted(['544', 2], 1)
|
||||
yield* this.markDeleted(['544', 1], 1)
|
||||
yield* this.markDeleted(['544', 2], 1)
|
||||
yield* this.markGarbageCollected(['543', 2], 1)
|
||||
yield* this.markGarbageCollected(['543', 4], 1)
|
||||
yield* this.markGarbageCollected(['544', 2], 1)
|
||||
yield* this.markGarbageCollected(['543', 3], 1)
|
||||
expect(yield* this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #5', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
||||
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #6', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.applyDeleteSet({'40': [[0, 3, false]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
|
||||
yield* this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #7', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['9', 2], 1)
|
||||
yield* this.markDeleted(['11', 2], 1)
|
||||
yield* this.markDeleted(['11', 4], 1)
|
||||
yield* this.markDeleted(['11', 1], 1)
|
||||
yield* this.markDeleted(['9', 4], 1)
|
||||
yield* this.markDeleted(['10', 0], 1)
|
||||
yield* this.markGarbageCollected(['11', 2], 1)
|
||||
yield* this.markDeleted(['11', 2], 1)
|
||||
yield* this.markGarbageCollected(['11', 3], 1)
|
||||
yield* this.markDeleted(['11', 3], 1)
|
||||
yield* this.markDeleted(['11', 3], 1)
|
||||
yield* this.markDeleted(['9', 4], 1)
|
||||
yield* this.markDeleted(['10', 0], 1)
|
||||
yield* this.markGarbageCollected(['11', 1], 1)
|
||||
yield* this.markDeleted(['11', 1], 1)
|
||||
expect(yield* this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
describe('OperationStore', function () {
|
||||
describe('Basic Tests', function () {
|
||||
beforeEach(function () {
|
||||
store = new Y[database](null, {
|
||||
gcTimeout: -1,
|
||||
namespace: 'testing'
|
||||
})
|
||||
})
|
||||
afterEach(function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.store.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('debug #1', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.put({id: [2]})
|
||||
yield* this.os.put({id: [0]})
|
||||
yield* this.os.delete([2])
|
||||
yield* this.os.put({id: [1]})
|
||||
expect(yield* this.os.find([0])).toBeTruthy()
|
||||
expect(yield* this.os.find([1])).toBeTruthy()
|
||||
expect(yield* this.os.find([2])).toBeFalsy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('can add&retrieve 5 elements', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.put({val: 'four', id: [4]})
|
||||
yield* this.os.put({val: 'one', id: [1]})
|
||||
yield* this.os.put({val: 'three', id: [3]})
|
||||
yield* this.os.put({val: 'two', id: [2]})
|
||||
yield* this.os.put({val: 'five', id: [5]})
|
||||
expect((yield* this.os.find([1])).val).toEqual('one')
|
||||
expect((yield* this.os.find([2])).val).toEqual('two')
|
||||
expect((yield* this.os.find([3])).val).toEqual('three')
|
||||
expect((yield* this.os.find([4])).val).toEqual('four')
|
||||
expect((yield* this.os.find([5])).val).toEqual('five')
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('5 elements do not exist anymore after deleting them', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.put({val: 'four', id: [4]})
|
||||
yield* this.os.put({val: 'one', id: [1]})
|
||||
yield* this.os.put({val: 'three', id: [3]})
|
||||
yield* this.os.put({val: 'two', id: [2]})
|
||||
yield* this.os.put({val: 'five', id: [5]})
|
||||
yield* this.os.delete([4])
|
||||
expect(yield* this.os.find([4])).not.toBeTruthy()
|
||||
yield* this.os.delete([3])
|
||||
expect(yield* this.os.find([3])).not.toBeTruthy()
|
||||
yield* this.os.delete([2])
|
||||
expect(yield* this.os.find([2])).not.toBeTruthy()
|
||||
yield* this.os.delete([1])
|
||||
expect(yield* this.os.find([1])).not.toBeTruthy()
|
||||
yield* this.os.delete([5])
|
||||
expect(yield* this.os.find([5])).not.toBeTruthy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
var numberOfOSTests = 1000
|
||||
describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () {
|
||||
var elements = []
|
||||
beforeAll(function (done) {
|
||||
store = new Y[database](null, {
|
||||
gcTimeout: -1,
|
||||
namespace: 'testing'
|
||||
})
|
||||
store.requestTransaction(function * () {
|
||||
for (var i = 0; i < numberOfOSTests; i++) {
|
||||
var r = Math.random()
|
||||
if (r < 0.8) {
|
||||
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
|
||||
if (!(yield* this.os.find(obj))) {
|
||||
elements.push(obj)
|
||||
yield* this.os.put({id: obj})
|
||||
}
|
||||
} else if (elements.length > 0) {
|
||||
var elemid = Math.floor(Math.random() * elements.length)
|
||||
var elem = elements[elemid]
|
||||
elements = elements.filter(function (e) {
|
||||
return !Y.utils.compareIds(e, elem)
|
||||
})
|
||||
yield* this.os.delete(elem)
|
||||
}
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
afterAll(function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.store.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('can find every object', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
for (var id of elements) {
|
||||
expect((yield* this.os.find(id)).id).toEqual(id)
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('can find every object with lower bound search', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
for (var id of elements) {
|
||||
var e = yield* this.os.findWithLowerBound(id)
|
||||
expect(e.id).toEqual(id)
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('iterating over a tree with lower bound yields the right amount of results', function (done) {
|
||||
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos
|
||||
}).length
|
||||
|
||||
var actualResults = 0
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.iterate(this, lowerBound, null, function * (val) {
|
||||
expect(val).toBeDefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('iterating over a tree without bounds yield the right amount of results', function (done) {
|
||||
var lowerBound = null
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return elements.indexOf(e) === pos
|
||||
}).length
|
||||
var actualResults = 0
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.iterate(this, lowerBound, null, function * (val) {
|
||||
expect(val).toBeDefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('iterating over a tree with upper bound yields the right amount of results', function (done) {
|
||||
var upperBound = elements[Math.floor(Math.random() * elements.length)]
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
||||
}).length
|
||||
|
||||
var actualResults = 0
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.iterate(this, null, upperBound, function * (val) {
|
||||
expect(val).toBeDefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) {
|
||||
var b1 = elements[Math.floor(Math.random() * elements.length)]
|
||||
var b2 = elements[Math.floor(Math.random() * elements.length)]
|
||||
var upperBound, lowerBound
|
||||
if (Y.utils.smaller(b1, b2)) {
|
||||
lowerBound = b1
|
||||
upperBound = b2
|
||||
} else {
|
||||
lowerBound = b2
|
||||
upperBound = b1
|
||||
}
|
||||
var expectedResults = elements.filter(function (e, pos) {
|
||||
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
|
||||
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
||||
}).length
|
||||
var actualResults = 0
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.iterate(this, lowerBound, upperBound, function * (val) {
|
||||
expect(val).toBeDefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
156
src/Encoding.js
156
src/Encoding.js
@@ -1,156 +0,0 @@
|
||||
import utf8 from 'utf-8'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
export class BinaryEncoder {
|
||||
constructor () {
|
||||
this.data = []
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
get pos () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
createBuffer () {
|
||||
return Uint8Array.from(this.data).buffer
|
||||
}
|
||||
|
||||
writeUint8 (num) {
|
||||
this.data.push(num & bits8)
|
||||
}
|
||||
|
||||
setUint8 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
}
|
||||
|
||||
writeUint16 (num) {
|
||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
setUint16 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
this.data[pos + 1] = (num >>> 8) & bits8
|
||||
}
|
||||
|
||||
writeUint32 (num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data.push(num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
setUint32 (pos, num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data[pos + i] = num & bits8
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
writeVarUint (num) {
|
||||
while (num >= 0b10000000) {
|
||||
this.data.push(0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
this.data.push(bits7 & num)
|
||||
}
|
||||
|
||||
writeVarString (str) {
|
||||
let bytes = utf8.setBytesFromString(str)
|
||||
let len = bytes.length
|
||||
this.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
this.data.push(bytes[i])
|
||||
}
|
||||
}
|
||||
|
||||
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()]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
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)
|
||||
this.writeOperationsUntransformed(encoder)
|
||||
} else {
|
||||
encoder.writeUint8(0)
|
||||
this.writeOperations(encoder, decoder)
|
||||
}
|
||||
|
||||
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) {
|
||||
this.applyOperationsUntransformed(decoder)
|
||||
} else {
|
||||
this.store.applyOperations(decoder)
|
||||
}
|
||||
})
|
||||
// then apply ds
|
||||
db.requestTransaction(function () {
|
||||
this.applyDeleteSet(decoder)
|
||||
})
|
||||
return db.whenTransactionsFinished().then(() => {
|
||||
conn._setSyncedWith(sender)
|
||||
defer.resolve()
|
||||
})
|
||||
}
|
||||
@@ -1,46 +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
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
|
||||
export default function extendRBTree (Y) {
|
||||
class N {
|
||||
// A created node is always red!
|
||||
constructor (val) {
|
||||
this.val = val
|
||||
this.color = true
|
||||
this._left = null
|
||||
this._right = null
|
||||
this._parent = null
|
||||
if (val.id === null) {
|
||||
throw new Error('You must define id!')
|
||||
}
|
||||
}
|
||||
isRed () { return this.color }
|
||||
isBlack () { return !this.color }
|
||||
redden () { this.color = true; return this }
|
||||
blacken () { this.color = false; return this }
|
||||
get grandparent () {
|
||||
return this.parent.parent
|
||||
}
|
||||
get parent () {
|
||||
return this._parent
|
||||
}
|
||||
get sibling () {
|
||||
return (this === this.parent.left)
|
||||
? this.parent.right : this.parent.left
|
||||
}
|
||||
get left () {
|
||||
return this._left
|
||||
}
|
||||
get right () {
|
||||
return this._right
|
||||
}
|
||||
set left (n) {
|
||||
if (n !== null) {
|
||||
n._parent = this
|
||||
}
|
||||
this._left = n
|
||||
}
|
||||
set right (n) {
|
||||
if (n !== null) {
|
||||
n._parent = this
|
||||
}
|
||||
this._right = n
|
||||
}
|
||||
rotateLeft (tree) {
|
||||
var parent = this.parent
|
||||
var newParent = this.right
|
||||
var newRight = this.right.left
|
||||
newParent.left = this
|
||||
this.right = newRight
|
||||
if (parent === null) {
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === this) {
|
||||
parent.left = newParent
|
||||
} else if (parent.right === this) {
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error('The elements are wrongly connected!')
|
||||
}
|
||||
}
|
||||
next () {
|
||||
if (this.right !== null) {
|
||||
// search the most left node in the right tree
|
||||
var o = this.right
|
||||
while (o.left !== null) {
|
||||
o = o.left
|
||||
}
|
||||
return o
|
||||
} else {
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.left) {
|
||||
p = p.parent
|
||||
}
|
||||
return p.parent
|
||||
}
|
||||
}
|
||||
prev () {
|
||||
if (this.left !== null) {
|
||||
// search the most right node in the left tree
|
||||
var o = this.left
|
||||
while (o.right !== null) {
|
||||
o = o.right
|
||||
}
|
||||
return o
|
||||
} else {
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.right) {
|
||||
p = p.parent
|
||||
}
|
||||
return p.parent
|
||||
}
|
||||
}
|
||||
rotateRight (tree) {
|
||||
var parent = this.parent
|
||||
var newParent = this.left
|
||||
var newLeft = this.left.right
|
||||
newParent.right = this
|
||||
this.left = newLeft
|
||||
if (parent === null) {
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === this) {
|
||||
parent.left = newParent
|
||||
} else if (parent.right === this) {
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error('The elements are wrongly connected!')
|
||||
}
|
||||
}
|
||||
getUncle () {
|
||||
// we can assume that grandparent exists when this is called!
|
||||
if (this.parent === this.parent.parent.left) {
|
||||
return this.parent.parent.right
|
||||
} else {
|
||||
return this.parent.parent.left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RBTree {
|
||||
constructor () {
|
||||
this.root = null
|
||||
this.length = 0
|
||||
}
|
||||
findNext (id) {
|
||||
return this.findWithLowerBound([id[0], id[1] + 1])
|
||||
}
|
||||
findPrev (id) {
|
||||
return this.findWithUpperBound([id[0], id[1] - 1])
|
||||
}
|
||||
findNodeWithLowerBound (from) {
|
||||
if (from === void 0) {
|
||||
throw new Error('You must define from!')
|
||||
}
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return null
|
||||
} else {
|
||||
while (true) {
|
||||
if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
|
||||
// o is included in the bound
|
||||
// try to find an element that is closer to the bound
|
||||
o = o.left
|
||||
} else if (from !== null && Y.utils.smaller(o.val.id, from)) {
|
||||
// o is not within the bound, maybe one of the right elements is..
|
||||
if (o.right !== null) {
|
||||
o = o.right
|
||||
} else {
|
||||
// there is no right element. Search for the next bigger element,
|
||||
// this should be within the bounds
|
||||
return o.next()
|
||||
}
|
||||
} else {
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
findNodeWithUpperBound (to) {
|
||||
if (to === void 0) {
|
||||
throw new Error('You must define from!')
|
||||
}
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return null
|
||||
} else {
|
||||
while (true) {
|
||||
if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
|
||||
// o is included in the bound
|
||||
// try to find an element that is closer to the bound
|
||||
o = o.right
|
||||
} else if (to !== null && Y.utils.smaller(to, o.val.id)) {
|
||||
// o is not within the bound, maybe one of the left elements is..
|
||||
if (o.left !== null) {
|
||||
o = o.left
|
||||
} else {
|
||||
// there is no left element. Search for the prev smaller element,
|
||||
// this should be within the bounds
|
||||
return o.prev()
|
||||
}
|
||||
} else {
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
findSmallestNode () {
|
||||
var o = this.root
|
||||
while (o != null && o.left != null) {
|
||||
o = o.left
|
||||
}
|
||||
return o
|
||||
}
|
||||
findWithLowerBound (from) {
|
||||
var n = this.findNodeWithLowerBound(from)
|
||||
return n == null ? null : n.val
|
||||
}
|
||||
findWithUpperBound (to) {
|
||||
var n = this.findNodeWithUpperBound(to)
|
||||
return n == null ? null : n.val
|
||||
}
|
||||
iterate (t, from, to, f) {
|
||||
var o
|
||||
if (from === null) {
|
||||
o = this.findSmallestNode()
|
||||
} else {
|
||||
o = this.findNodeWithLowerBound(from)
|
||||
}
|
||||
while (
|
||||
o !== null &&
|
||||
(
|
||||
to === null || // eslint-disable-line no-unmodified-loop-condition
|
||||
Y.utils.smaller(o.val.id, to) ||
|
||||
Y.utils.compareIds(o.val.id, to)
|
||||
)
|
||||
) {
|
||||
f.call(t, o.val)
|
||||
o = o.next()
|
||||
}
|
||||
return true
|
||||
}
|
||||
logTable (from, to, filter) {
|
||||
if (filter == null) {
|
||||
filter = function () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (from == null) { from = null }
|
||||
if (to == null) { to = null }
|
||||
var os = []
|
||||
this.iterate(this, from, to, function (o) {
|
||||
if (filter(o)) {
|
||||
var o_ = {}
|
||||
for (var key in o) {
|
||||
if (typeof o[key] === 'object') {
|
||||
o_[key] = JSON.stringify(o[key])
|
||||
} else {
|
||||
o_[key] = o[key]
|
||||
}
|
||||
}
|
||||
os.push(o_)
|
||||
}
|
||||
})
|
||||
if (console.table != null) {
|
||||
console.table(os)
|
||||
}
|
||||
}
|
||||
find (id) {
|
||||
var n
|
||||
return (n = this.findNode(id)) ? n.val : null
|
||||
}
|
||||
findNode (id) {
|
||||
if (id == null || id.constructor !== Array) {
|
||||
throw new Error('Expect id to be an array!')
|
||||
}
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return false
|
||||
} else {
|
||||
while (true) {
|
||||
if (o === null) {
|
||||
return false
|
||||
}
|
||||
if (Y.utils.smaller(id, o.val.id)) {
|
||||
o = o.left
|
||||
} else if (Y.utils.smaller(o.val.id, id)) {
|
||||
o = o.right
|
||||
} else {
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete (id) {
|
||||
if (id == null || id.constructor !== Array) {
|
||||
throw new Error('id is expected to be an Array!')
|
||||
}
|
||||
var d = this.findNode(id)
|
||||
if (d == null) {
|
||||
// throw new Error('Element does not exist!')
|
||||
return
|
||||
}
|
||||
this.length--
|
||||
if (d.left !== null && d.right !== null) {
|
||||
// switch d with the greates element in the left subtree.
|
||||
// o should have at most one child.
|
||||
var o = d.left
|
||||
// find
|
||||
while (o.right !== null) {
|
||||
o = o.right
|
||||
}
|
||||
// switch
|
||||
d.val = o.val
|
||||
d = o
|
||||
}
|
||||
// d has at most one child
|
||||
// let n be the node that replaces d
|
||||
var isFakeChild
|
||||
var child = d.left || d.right
|
||||
if (child === null) {
|
||||
isFakeChild = true
|
||||
child = new N({id: 0})
|
||||
child.blacken()
|
||||
d.right = child
|
||||
} else {
|
||||
isFakeChild = false
|
||||
}
|
||||
|
||||
if (d.parent === null) {
|
||||
if (!isFakeChild) {
|
||||
this.root = child
|
||||
child.blacken()
|
||||
child._parent = null
|
||||
} else {
|
||||
this.root = null
|
||||
}
|
||||
return
|
||||
} else if (d.parent.left === d) {
|
||||
d.parent.left = child
|
||||
} else if (d.parent.right === d) {
|
||||
d.parent.right = child
|
||||
} else {
|
||||
throw new Error('Impossible!')
|
||||
}
|
||||
if (d.isBlack()) {
|
||||
if (child.isRed()) {
|
||||
child.blacken()
|
||||
} else {
|
||||
this._fixDelete(child)
|
||||
}
|
||||
}
|
||||
this.root.blacken()
|
||||
if (isFakeChild) {
|
||||
if (child.parent.left === child) {
|
||||
child.parent.left = null
|
||||
} else if (child.parent.right === child) {
|
||||
child.parent.right = null
|
||||
} else {
|
||||
throw new Error('Impossible #3')
|
||||
}
|
||||
}
|
||||
}
|
||||
_fixDelete (n) {
|
||||
function isBlack (node) {
|
||||
return node !== null ? node.isBlack() : true
|
||||
}
|
||||
function isRed (node) {
|
||||
return node !== null ? node.isRed() : false
|
||||
}
|
||||
if (n.parent === null) {
|
||||
// this can only be called after the first iteration of fixDelete.
|
||||
return
|
||||
}
|
||||
// d was already replaced by the child
|
||||
// d is not the root
|
||||
// d and child are black
|
||||
var sibling = n.sibling
|
||||
if (isRed(sibling)) {
|
||||
// make sibling the grandfather
|
||||
n.parent.redden()
|
||||
sibling.blacken()
|
||||
if (n === n.parent.left) {
|
||||
n.parent.rotateLeft(this)
|
||||
} else if (n === n.parent.right) {
|
||||
n.parent.rotateRight(this)
|
||||
} else {
|
||||
throw new Error('Impossible #2')
|
||||
}
|
||||
sibling = n.sibling
|
||||
}
|
||||
// parent, sibling, and children of n are black
|
||||
if (n.parent.isBlack() &&
|
||||
sibling.isBlack() &&
|
||||
isBlack(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden()
|
||||
this._fixDelete(n.parent)
|
||||
} else if (n.parent.isRed() &&
|
||||
sibling.isBlack() &&
|
||||
isBlack(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden()
|
||||
n.parent.blacken()
|
||||
} else {
|
||||
if (n === n.parent.left &&
|
||||
sibling.isBlack() &&
|
||||
isRed(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden()
|
||||
sibling.left.blacken()
|
||||
sibling.rotateRight(this)
|
||||
sibling = n.sibling
|
||||
} else if (n === n.parent.right &&
|
||||
sibling.isBlack() &&
|
||||
isRed(sibling.right) &&
|
||||
isBlack(sibling.left)
|
||||
) {
|
||||
sibling.redden()
|
||||
sibling.right.blacken()
|
||||
sibling.rotateLeft(this)
|
||||
sibling = n.sibling
|
||||
}
|
||||
sibling.color = n.parent.color
|
||||
n.parent.blacken()
|
||||
if (n === n.parent.left) {
|
||||
sibling.right.blacken()
|
||||
n.parent.rotateLeft(this)
|
||||
} else {
|
||||
sibling.left.blacken()
|
||||
n.parent.rotateRight(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
put (v) {
|
||||
if (v == null || v.id == null || v.id.constructor !== Array) {
|
||||
throw new Error('v is expected to have an id property which is an Array!')
|
||||
}
|
||||
var node = new N(v)
|
||||
if (this.root !== null) {
|
||||
var p = this.root // p abbrev. parent
|
||||
while (true) {
|
||||
if (Y.utils.smaller(node.val.id, p.val.id)) {
|
||||
if (p.left === null) {
|
||||
p.left = node
|
||||
break
|
||||
} else {
|
||||
p = p.left
|
||||
}
|
||||
} else if (Y.utils.smaller(p.val.id, node.val.id)) {
|
||||
if (p.right === null) {
|
||||
p.right = node
|
||||
break
|
||||
} else {
|
||||
p = p.right
|
||||
}
|
||||
} else {
|
||||
p.val = node.val
|
||||
return p
|
||||
}
|
||||
}
|
||||
this._fixInsert(node)
|
||||
} else {
|
||||
this.root = node
|
||||
}
|
||||
this.length++
|
||||
this.root.blacken()
|
||||
return node
|
||||
}
|
||||
_fixInsert (n) {
|
||||
if (n.parent === null) {
|
||||
n.blacken()
|
||||
return
|
||||
} else if (n.parent.isBlack()) {
|
||||
return
|
||||
}
|
||||
var uncle = n.getUncle()
|
||||
if (uncle !== null && uncle.isRed()) {
|
||||
// Note: parent: red, uncle: red
|
||||
n.parent.blacken()
|
||||
uncle.blacken()
|
||||
n.grandparent.redden()
|
||||
this._fixInsert(n.grandparent)
|
||||
} else {
|
||||
// Note: parent: red, uncle: black or null
|
||||
// Now we transform the tree in such a way that
|
||||
// either of these holds:
|
||||
// 1) grandparent.left.isRed
|
||||
// and grandparent.left.left.isRed
|
||||
// 2) grandparent.right.isRed
|
||||
// and grandparent.right.right.isRed
|
||||
if (n === n.parent.right && n.parent === n.grandparent.left) {
|
||||
n.parent.rotateLeft(this)
|
||||
// Since we rotated and want to use the previous
|
||||
// cases, we need to set n in such a way that
|
||||
// n.parent.isRed again
|
||||
n = n.left
|
||||
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
|
||||
n.parent.rotateRight(this)
|
||||
// see above
|
||||
n = n.right
|
||||
}
|
||||
// Case 1) or 2) hold from here on.
|
||||
// Now traverse grandparent, make parent a black node
|
||||
// on the highest level which holds two red nodes.
|
||||
n.parent.blacken()
|
||||
n.grandparent.redden()
|
||||
if (n === n.parent.left) {
|
||||
// Case 1
|
||||
n.grandparent.rotateRight(this)
|
||||
} else {
|
||||
// Case 2
|
||||
n.grandparent.rotateLeft(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
flush () {}
|
||||
}
|
||||
|
||||
Y.utils.RBTree = RBTree
|
||||
}
|
||||
404
src/SpecHelper.js
Normal file
404
src/SpecHelper.js
Normal file
@@ -0,0 +1,404 @@
|
||||
/* 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)
|
||||
|
||||
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 instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
|
||||
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
|
||||
type = type.y
|
||||
}
|
||||
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
|
||||
type.eventHandler.awaiting = 1
|
||||
type.eventHandler._debuggingAwaiting = true
|
||||
} 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) {
|
||||
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
|
||||
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
|
||||
type = type.y
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
type.os.whenTransactionsFinished().then(function () {
|
||||
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
|
||||
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
|
||||
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield g.garbageCollectAllUsers(users)
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
|
||||
// disconnect, then reconnect all users
|
||||
// We do this to make sure that the gc is updated by everyone
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
yield users[i].disconnect()
|
||||
yield wait()
|
||||
yield users[i].reconnect()
|
||||
}
|
||||
yield wait()
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
|
||||
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
||||
function * t1 () {
|
||||
s1 = yield* this.getStateSet()
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
gc: true,
|
||||
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
|
||||
955
src/Struct.js
955
src/Struct.js
@@ -1,8 +1,5 @@
|
||||
const CDELETE = 0
|
||||
const CINSERT = 1
|
||||
const CLIST = 2
|
||||
const CMAP = 3
|
||||
const CXML = 4
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
/*
|
||||
An operation also defines the structure of a type. This is why operation and
|
||||
@@ -22,598 +19,396 @@ const CXML = 4
|
||||
* requiredOps
|
||||
- Operations that are required to execute this operation.
|
||||
*/
|
||||
export default function extendStruct (Y) {
|
||||
let Struct = {}
|
||||
Y.Struct = Struct
|
||||
Struct.binaryDecodeOperation = function (decoder) {
|
||||
let code = decoder.peekUint8()
|
||||
if (code === CDELETE) {
|
||||
return Struct.Delete.binaryDecode(decoder)
|
||||
} else if (code === CINSERT) {
|
||||
return Struct.Insert.binaryDecode(decoder)
|
||||
} else if (code === CLIST) {
|
||||
return Struct.List.binaryDecode(decoder)
|
||||
} else if (code === CMAP) {
|
||||
return Struct.Map.binaryDecode(decoder)
|
||||
} else if (code === CXML) {
|
||||
return Struct.Xml.binaryDecode(decoder)
|
||||
} else {
|
||||
throw new Error('Unable to decode operation!')
|
||||
module.exports = function (Y/* :any */) {
|
||||
var Struct = {
|
||||
/* This is the only operation that is actually not a structure, because
|
||||
it is not stored in the OS. This is why it _does not_ have an id
|
||||
|
||||
op = {
|
||||
target: Id
|
||||
}
|
||||
}
|
||||
|
||||
/* This is the only operation that is actually not a structure, because
|
||||
it is not stored in the OS. This is why it _does not_ have an id
|
||||
|
||||
op = {
|
||||
target: Id
|
||||
}
|
||||
*/
|
||||
Struct.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'
|
||||
}
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function (op) {
|
||||
return this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
}
|
||||
|
||||
/* {
|
||||
content: [any],
|
||||
opContent: Id,
|
||||
id: Id,
|
||||
left: Id,
|
||||
origin: Id,
|
||||
right: Id,
|
||||
parent: Id,
|
||||
parentSub: string (optional), // child of Map type
|
||||
}
|
||||
*/
|
||||
Struct.Insert = {
|
||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||
// TODO: you could not send the "left" property, then you also have to
|
||||
// "op.left = null" in $execute or $decode
|
||||
var e/* :any */ = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
origin: op.origin,
|
||||
parent: op.parent,
|
||||
struct: op.struct
|
||||
}
|
||||
if (op.parentSub != null) {
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
if (op.hasOwnProperty('opContent')) {
|
||||
e.opContent = op.opContent
|
||||
} else {
|
||||
e.content = op.content.slice()
|
||||
}
|
||||
|
||||
return e
|
||||
},
|
||||
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) {
|
||||
ids.push(op.left)
|
||||
}
|
||||
if (op.right != null) {
|
||||
ids.push(op.right)
|
||||
}
|
||||
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
||||
ids.push(op.origin)
|
||||
}
|
||||
// if (op.right == null && op.left == null) {
|
||||
ids.push(op.parent)
|
||||
|
||||
if (op.opContent != null) {
|
||||
ids.push(op.opContent)
|
||||
}
|
||||
return ids
|
||||
},
|
||||
getDistanceToOrigin: function (op) {
|
||||
if (op.left == null) {
|
||||
return 0
|
||||
} else {
|
||||
var d = 0
|
||||
var o = this.getInsertion(op.left)
|
||||
while (!Y.utils.matchesId(o, op.origin)) {
|
||||
d++
|
||||
if (o.left == null) {
|
||||
break
|
||||
} else {
|
||||
o = this.getInsertion(o.left)
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
},
|
||||
/*
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change
|
||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
*/
|
||||
execute: function (op) {
|
||||
var i // loop counter
|
||||
|
||||
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
||||
// We try to merge them later, if possible
|
||||
var tryToRemergeLater = []
|
||||
|
||||
if (op.origin != null) { // TODO: !== instead of !=
|
||||
// we save in origin that op originates in it
|
||||
// we need that later when we eventually garbage collect origin (see transaction)
|
||||
var origin = this.getInsertionCleanEnd(op.origin)
|
||||
if (origin.originOf == null) {
|
||||
origin.originOf = []
|
||||
Delete: {
|
||||
encode: function (op) {
|
||||
return {
|
||||
target: op.target,
|
||||
length: op.length || 0,
|
||||
struct: 'Delete'
|
||||
}
|
||||
origin.originOf.push(op.id)
|
||||
this.setOperation(origin)
|
||||
if (origin.right != null) {
|
||||
tryToRemergeLater.push(origin.right)
|
||||
}
|
||||
}
|
||||
var distanceToOrigin = i = Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
||||
|
||||
// now we begin to insert op in the list of insertions..
|
||||
var o
|
||||
var parent
|
||||
var start
|
||||
|
||||
// find o. o is the first conflicting operation
|
||||
if (op.left != null) {
|
||||
o = 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 : this.getOperation(o.right)
|
||||
} else { // left == null
|
||||
parent = this.getOperation(op.parent)
|
||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||
start = startId == null ? null : 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)
|
||||
this.getInsertionCleanStart(op.right)
|
||||
}
|
||||
|
||||
// handle conflicts
|
||||
while (true) {
|
||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||
var oOriginDistance = Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||
if (oOriginDistance === i) {
|
||||
// case 1
|
||||
if (o.id[0] < op.id[0]) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else if (oOriginDistance < i) {
|
||||
// case 2
|
||||
if (i - distanceToOrigin <= oOriginDistance) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
i++
|
||||
if (o.right != null) {
|
||||
o = this.getInsertion(o.right)
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// reconnect..
|
||||
var left = null
|
||||
var right = null
|
||||
if (parent == null) {
|
||||
parent = this.getOperation(op.parent)
|
||||
}
|
||||
|
||||
// reconnect left and set right of op
|
||||
if (op.left != null) {
|
||||
left = this.getInsertion(op.left)
|
||||
// link left
|
||||
op.right = left.right
|
||||
left.right = op.id
|
||||
|
||||
this.setOperation(left)
|
||||
} else {
|
||||
// set op.right from parent, if necessary
|
||||
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
||||
}
|
||||
// reconnect right
|
||||
if (op.right != null) {
|
||||
// TODO: wanna connect right too?
|
||||
right = 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 = this.getInsertionCleanEnd(right.id)
|
||||
}
|
||||
this.store.removeFromGarbageCollector(right)
|
||||
}
|
||||
this.setOperation(right)
|
||||
}
|
||||
|
||||
// update parents .map/start/end properties
|
||||
if (op.parentSub != null) {
|
||||
if (left == null) {
|
||||
parent.map[op.parentSub] = op.id
|
||||
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) {
|
||||
this.deleteOperation(op.right, 1, true)
|
||||
}
|
||||
if (op.left != null) {
|
||||
this.deleteOperation(op.id, 1, true)
|
||||
}
|
||||
} else {
|
||||
if (right == null || left == null) {
|
||||
if (right == null) {
|
||||
parent.end = Y.utils.getLastId(op)
|
||||
}
|
||||
if (left == null) {
|
||||
parent.start = op.id
|
||||
}
|
||||
this.setOperation(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// try to merge original op.left and op.origin
|
||||
for (i = 0; i < tryToRemergeLater.length; i++) {
|
||||
var m = this.getOperation(tryToRemergeLater[i])
|
||||
this.tryCombineWithLeft(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.List = {
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'List',
|
||||
id: id
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function * (op) {
|
||||
return yield* this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
}
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CLIST)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeVarString(op.type)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
let op = {
|
||||
id: decoder.readOpID(),
|
||||
type: decoder.readVarString(),
|
||||
struct: 'List',
|
||||
start: null,
|
||||
end: null
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
Insert: {
|
||||
/* {
|
||||
content: [any],
|
||||
opContent: Id,
|
||||
id: Id,
|
||||
left: Id,
|
||||
origin: Id,
|
||||
right: Id,
|
||||
parent: Id,
|
||||
parentSub: string (optional), // child of Map type
|
||||
}
|
||||
*/
|
||||
return []
|
||||
},
|
||||
execute: function (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null
|
||||
}
|
||||
var res = null
|
||||
var o = this.getOperation(op.start)
|
||||
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o
|
||||
pos--
|
||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||
// TODO: you could not send the "left" property, then you also have to
|
||||
// "op.left = null" in $execute or $decode
|
||||
var e/* :any */ = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
origin: op.origin,
|
||||
parent: op.parent,
|
||||
struct: op.struct
|
||||
}
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = this.getOperation(o.right)
|
||||
if (op.parentSub != null) {
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
if (op.hasOwnProperty('opContent')) {
|
||||
e.opContent = op.opContent
|
||||
} else {
|
||||
break
|
||||
e.content = op.content.slice()
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
map: function (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o != null) { // TODO: change to != (at least some convention)
|
||||
var operation = this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation))
|
||||
}
|
||||
o = operation.right
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
struct: "Map",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.Map = {
|
||||
create: function (id) {
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
struct: 'Map'
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Map',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {} // overwrite map!!
|
||||
}
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CMAP)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeVarString(op.type)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
let op = {
|
||||
id: decoder.readOpID(),
|
||||
type: decoder.readVarString(),
|
||||
struct: 'Map',
|
||||
map: {}
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function () {
|
||||
return []
|
||||
},
|
||||
execute: function (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
/*
|
||||
Get a property by name
|
||||
*/
|
||||
get: function (op, name) {
|
||||
var oid = op.map[name]
|
||||
if (oid != null) {
|
||||
var res = this.getOperation(oid)
|
||||
if (res == null || res.deleted) {
|
||||
return void 0
|
||||
} else if (res.opContent == null) {
|
||||
return res.content[0]
|
||||
return e
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
var ids = []
|
||||
if (op.left != null) {
|
||||
ids.push(op.left)
|
||||
}
|
||||
if (op.right != null) {
|
||||
ids.push(op.right)
|
||||
}
|
||||
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
||||
ids.push(op.origin)
|
||||
}
|
||||
// if (op.right == null && op.left == null) {
|
||||
ids.push(op.parent)
|
||||
|
||||
if (op.opContent != null) {
|
||||
ids.push(op.opContent)
|
||||
}
|
||||
return ids
|
||||
},
|
||||
getDistanceToOrigin: function * (op) {
|
||||
if (op.left == null) {
|
||||
return 0
|
||||
} else {
|
||||
return this.getType(res.opContent)
|
||||
var d = 0
|
||||
var o = yield* this.getInsertion(op.left)
|
||||
while (!Y.utils.matchesId(o, op.origin)) {
|
||||
d++
|
||||
if (o.left == null) {
|
||||
break
|
||||
} else {
|
||||
o = yield* this.getInsertion(o.left)
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
},
|
||||
/*
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change
|
||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
*/
|
||||
execute: function * (op) {
|
||||
var i // loop counter
|
||||
|
||||
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
||||
// We try to merge them later, if possible
|
||||
var tryToRemergeLater = []
|
||||
|
||||
if (op.origin != null) { // TODO: !== instead of !=
|
||||
// we save in origin that op originates in it
|
||||
// we need that later when we eventually garbage collect origin (see transaction)
|
||||
var origin = yield* this.getInsertionCleanEnd(op.origin)
|
||||
if (origin.originOf == null) {
|
||||
origin.originOf = []
|
||||
}
|
||||
origin.originOf.push(op.id)
|
||||
yield* this.setOperation(origin)
|
||||
if (origin.right != null) {
|
||||
tryToRemergeLater.push(origin.right)
|
||||
}
|
||||
}
|
||||
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
||||
|
||||
// now we begin to insert op in the list of insertions..
|
||||
var o
|
||||
var parent
|
||||
var start
|
||||
|
||||
// find o. o is the first conflicting operation
|
||||
if (op.left != null) {
|
||||
o = yield* this.getInsertionCleanEnd(op.left)
|
||||
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
||||
// only if not added previously
|
||||
tryToRemergeLater.push(o.right)
|
||||
}
|
||||
o = (o.right == null) ? null : yield* this.getOperation(o.right)
|
||||
} else { // left == null
|
||||
parent = yield* this.getOperation(op.parent)
|
||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||
start = startId == null ? null : yield* this.getOperation(startId)
|
||||
o = start
|
||||
}
|
||||
|
||||
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
||||
if (op.right != null) {
|
||||
tryToRemergeLater.push(op.right)
|
||||
yield* this.getInsertionCleanStart(op.right)
|
||||
}
|
||||
|
||||
// handle conflicts
|
||||
while (true) {
|
||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||
if (oOriginDistance === i) {
|
||||
// case 1
|
||||
if (o.id[0] < op.id[0]) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else if (oOriginDistance < i) {
|
||||
// case 2
|
||||
if (i - distanceToOrigin <= oOriginDistance) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
i++
|
||||
if (o.right != null) {
|
||||
o = yield* this.getInsertion(o.right)
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// reconnect..
|
||||
var left = null
|
||||
var right = null
|
||||
if (parent == null) {
|
||||
parent = yield* this.getOperation(op.parent)
|
||||
}
|
||||
|
||||
// reconnect left and set right of op
|
||||
if (op.left != null) {
|
||||
left = yield* this.getInsertion(op.left)
|
||||
// link left
|
||||
op.right = left.right
|
||||
left.right = op.id
|
||||
|
||||
yield* this.setOperation(left)
|
||||
} else {
|
||||
// set op.right from parent, if necessary
|
||||
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
||||
}
|
||||
// reconnect right
|
||||
if (op.right != null) {
|
||||
// TODO: wanna connect right too?
|
||||
right = yield* this.getOperation(op.right)
|
||||
right.left = Y.utils.getLastId(op)
|
||||
|
||||
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
||||
if (right.gc != null) {
|
||||
if (right.content != null && right.content.length > 1) {
|
||||
right = yield* this.getInsertionCleanEnd(right.id)
|
||||
}
|
||||
this.store.removeFromGarbageCollector(right)
|
||||
}
|
||||
yield* this.setOperation(right)
|
||||
}
|
||||
|
||||
// update parents .map/start/end properties
|
||||
if (op.parentSub != null) {
|
||||
if (left == null) {
|
||||
parent.map[op.parentSub] = op.id
|
||||
yield* this.setOperation(parent)
|
||||
}
|
||||
// is a child of a map struct.
|
||||
// Then also make sure that only the most left element is not deleted
|
||||
// We do not call the type in this case (this is what the third parameter is for)
|
||||
if (op.right != null) {
|
||||
yield* this.deleteOperation(op.right, 1, true)
|
||||
}
|
||||
if (op.left != null) {
|
||||
yield* this.deleteOperation(op.id, 1, true)
|
||||
}
|
||||
} else {
|
||||
if (right == null || left == null) {
|
||||
if (right == null) {
|
||||
parent.end = Y.utils.getLastId(op)
|
||||
}
|
||||
if (left == null) {
|
||||
parent.start = op.id
|
||||
}
|
||||
yield* this.setOperation(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// try to merge original op.left and op.origin
|
||||
for (i = 0; i < tryToRemergeLater.length; i++) {
|
||||
var m = yield* this.getOperation(tryToRemergeLater[i])
|
||||
yield* this.tryCombineWithLeft(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "Xml",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.Xml = {
|
||||
create: function (id, args) {
|
||||
let nodeName = args != null ? args.nodeName : null
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
},
|
||||
List: {
|
||||
/*
|
||||
{
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'Xml',
|
||||
nodeName
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'List',
|
||||
id: id
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
if (op.info != null) {
|
||||
e.info = op.info
|
||||
}
|
||||
return e
|
||||
},
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
*/
|
||||
return []
|
||||
},
|
||||
execute: function * (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function * (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null
|
||||
}
|
||||
var res = null
|
||||
var o = yield* this.getOperation(op.start)
|
||||
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o
|
||||
pos--
|
||||
}
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = yield* this.getOperation(o.right)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
map: function * (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o != null) { // TODO: change to != (at least some convention)
|
||||
var operation = yield* this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation))
|
||||
}
|
||||
o = operation.right
|
||||
}
|
||||
return res
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Xml',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {},
|
||||
nodeName: op.nodeName
|
||||
Map: {
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
struct: "Map",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
struct: 'Map'
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Map',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {} // overwrite map!!
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
if (op.info != null) {
|
||||
e.info = op.info
|
||||
}
|
||||
return e
|
||||
},
|
||||
requiredOps: function () {
|
||||
return []
|
||||
},
|
||||
execute: function * () {},
|
||||
/*
|
||||
Get a property by name
|
||||
*/
|
||||
get: function * (op, name) {
|
||||
var oid = op.map[name]
|
||||
if (oid != null) {
|
||||
var res = yield* this.getOperation(oid)
|
||||
if (res == null || res.deleted) {
|
||||
return void 0
|
||||
} else if (res.opContent == null) {
|
||||
return res.content[0]
|
||||
} else {
|
||||
return yield* this.getType(res.opContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CXML)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeVarString(op.type)
|
||||
encoder.writeVarString(op.nodeName)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
let op = {
|
||||
id: decoder.readOpID(),
|
||||
type: decoder.readVarString(),
|
||||
struct: 'Xml',
|
||||
map: {},
|
||||
start: null,
|
||||
end: null,
|
||||
nodeName: decoder.readVarString()
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function () {
|
||||
return []
|
||||
},
|
||||
execute: function () {},
|
||||
ref: Struct.List.ref,
|
||||
map: Struct.List.map,
|
||||
/*
|
||||
Get a property by name
|
||||
*/
|
||||
get: Struct.Map.get
|
||||
}
|
||||
}
|
||||
Y.Struct = Struct
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
145
src/Utils.js
145
src/Utils.js
@@ -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,12 +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
|
||||
}
|
||||
module.exports = function (Y /* : any*/) {
|
||||
Y.utils = {}
|
||||
|
||||
Y.utils.bubbleEvent = function (type, event) {
|
||||
type.eventHandler.callEventListeners(event)
|
||||
@@ -49,36 +44,6 @@ export default function Utils (Y) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
let listener = this._eventListener[name] || []
|
||||
if (name === 'error' && listener.length === 0) {
|
||||
console.error(value)
|
||||
}
|
||||
listener.forEach(l => l(value))
|
||||
}
|
||||
destroy () {
|
||||
this._eventListener = null
|
||||
}
|
||||
}
|
||||
Y.utils.NamedEventHandler = NamedEventHandler
|
||||
|
||||
class EventListenerHandler {
|
||||
constructor () {
|
||||
this.eventListeners = []
|
||||
@@ -109,12 +74,7 @@ export default function Utils (Y) {
|
||||
}
|
||||
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('Your observer threw an error. This error was caught so that Yjs still can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions"', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,7 +279,7 @@ export default function Utils (Y) {
|
||||
this.awaiting++
|
||||
ops.map(Y.utils.copyOperation).forEach(this.onevent)
|
||||
}
|
||||
awaitOps (transaction, f, args) {
|
||||
* awaitOps (transaction, f, args) {
|
||||
function notSoSmartSort (array) {
|
||||
// this function sorts insertions in a executable order
|
||||
var result = []
|
||||
@@ -343,7 +303,7 @@ export default function Utils (Y) {
|
||||
}
|
||||
var before = this.waiting.length
|
||||
// somehow create new operations
|
||||
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--
|
||||
@@ -353,7 +313,7 @@ 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 = transaction.getInsertion(o.id)
|
||||
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)
|
||||
@@ -365,10 +325,10 @@ export default function Utils (Y) {
|
||||
o.left = null
|
||||
} else {
|
||||
// find next undeleted op
|
||||
var left = transaction.getInsertion(_o.left)
|
||||
var left = yield* transaction.getInsertion(_o.left)
|
||||
while (left.deleted != null) {
|
||||
if (left.left != null) {
|
||||
left = transaction.getInsertion(left.left)
|
||||
left = yield* transaction.getInsertion(left.left)
|
||||
} else {
|
||||
left = null
|
||||
break
|
||||
@@ -652,7 +612,6 @@ export default function Utils (Y) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Y.utils.matchesId = matchesId
|
||||
|
||||
@@ -694,7 +653,7 @@ export default function Utils (Y) {
|
||||
this.writeBuffer = createEmptyOpsArray(5)
|
||||
this.readBuffer = createEmptyOpsArray(10)
|
||||
}
|
||||
find (id, noSuperCall) {
|
||||
* find (id, noSuperCall) {
|
||||
var i, r
|
||||
for (i = this.readBuffer.length - 1; i >= 0; i--) {
|
||||
r = this.readBuffer[i]
|
||||
@@ -720,7 +679,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 = super.find(id)
|
||||
o = yield* super.find(id)
|
||||
}
|
||||
if (o != null) {
|
||||
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
||||
@@ -730,7 +689,7 @@ export default function Utils (Y) {
|
||||
}
|
||||
return o
|
||||
}
|
||||
put (o) {
|
||||
* put (o) {
|
||||
var id = o.id
|
||||
var i, r // helper variables
|
||||
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
|
||||
@@ -750,7 +709,7 @@ export default function Utils (Y) {
|
||||
// write writeBuffer[0]
|
||||
var write = this.writeBuffer[0]
|
||||
if (write.id[0] !== null) {
|
||||
super.put(write)
|
||||
yield* super.put(write)
|
||||
}
|
||||
// put o to the end of writeBuffer
|
||||
for (i = 0; i < this.writeBuffer.length - 1; i++) {
|
||||
@@ -770,7 +729,7 @@ export default function Utils (Y) {
|
||||
}
|
||||
this.readBuffer[this.readBuffer.length - 1] = o
|
||||
}
|
||||
delete (id) {
|
||||
* delete (id) {
|
||||
var i, r
|
||||
for (i = 0; i < this.readBuffer.length; i++) {
|
||||
r = this.readBuffer[i]
|
||||
@@ -780,44 +739,44 @@ export default function Utils (Y) {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.flush()
|
||||
super.delete(id)
|
||||
yield* this.flush()
|
||||
yield* super.delete(id)
|
||||
}
|
||||
findWithLowerBound (id) {
|
||||
var o = this.find(id, true)
|
||||
* findWithLowerBound (id) {
|
||||
var o = yield* this.find(id, true)
|
||||
if (o != null) {
|
||||
return o
|
||||
} else {
|
||||
this.flush()
|
||||
return super.findWithLowerBound.apply(this, arguments)
|
||||
yield* this.flush()
|
||||
return yield* super.findWithLowerBound.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
findWithUpperBound (id) {
|
||||
var o = this.find(id, true)
|
||||
* findWithUpperBound (id) {
|
||||
var o = yield* this.find(id, true)
|
||||
if (o != null) {
|
||||
return o
|
||||
} else {
|
||||
this.flush()
|
||||
return super.findWithUpperBound.apply(this, arguments)
|
||||
yield* this.flush()
|
||||
return yield* super.findWithUpperBound.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
findNext () {
|
||||
this.flush()
|
||||
return super.findNext.apply(this, arguments)
|
||||
* findNext () {
|
||||
yield* this.flush()
|
||||
return yield* super.findNext.apply(this, arguments)
|
||||
}
|
||||
findPrev () {
|
||||
this.flush()
|
||||
return super.findPrev.apply(this, arguments)
|
||||
* findPrev () {
|
||||
yield* this.flush()
|
||||
return yield* super.findPrev.apply(this, arguments)
|
||||
}
|
||||
iterate () {
|
||||
this.flush()
|
||||
super.iterate.apply(this, arguments)
|
||||
* iterate () {
|
||||
yield* this.flush()
|
||||
yield* super.iterate.apply(this, arguments)
|
||||
}
|
||||
flush () {
|
||||
* flush () {
|
||||
for (var i = 0; i < this.writeBuffer.length; i++) {
|
||||
var write = this.writeBuffer[i]
|
||||
if (write.id[0] !== null) {
|
||||
super.put(write)
|
||||
yield* super.put(write)
|
||||
this.writeBuffer[i] = {
|
||||
id: [null, null]
|
||||
}
|
||||
@@ -829,32 +788,8 @@ export default function Utils (Y) {
|
||||
}
|
||||
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
|
||||
|
||||
Y.utils.parseTypeDefinition = function parseTypeDefinition (type, typeArgs) {
|
||||
var args = []
|
||||
try {
|
||||
args = JSON.parse('[' + typeArgs + ']')
|
||||
} catch (e) {
|
||||
throw new Error('Was not able to parse type definition!')
|
||||
}
|
||||
if (type.typeDefinition.parseArguments != null) {
|
||||
args = type.typeDefinition.parseArguments(args[0])[1]
|
||||
}
|
||||
return args
|
||||
}
|
||||
// Generates a unique id, for use as a user id.
|
||||
// Thx to @jed for this script https://gist.github.com/jed/982883
|
||||
function generateGuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,generateGuid)} // eslint-disable-line
|
||||
Y.utils.generateGuid = generateGuid
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import extendRBTree from './RedBlackTree'
|
||||
|
||||
export default function extend (Y) {
|
||||
extendRBTree(Y)
|
||||
|
||||
class Transaction extends Y.Transaction {
|
||||
constructor (store) {
|
||||
super(store)
|
||||
this.store = store
|
||||
this.ss = store.ss
|
||||
this.os = store.os
|
||||
this.ds = store.ds
|
||||
}
|
||||
}
|
||||
var Store = Y.utils.RBTree
|
||||
var BufferedStore = Y.utils.createSmallLookupBuffer(Store)
|
||||
|
||||
class Database extends Y.AbstractDatabase {
|
||||
constructor (y, opts) {
|
||||
super(y, opts)
|
||||
this.os = new BufferedStore()
|
||||
this.ds = new Store()
|
||||
this.ss = new BufferedStore()
|
||||
}
|
||||
logTable () {
|
||||
var self = this
|
||||
self.requestTransaction(function () {
|
||||
console.log('User: ', this.store.y.connector.userId, "==============================") // eslint-disable-line
|
||||
console.log("State Set (SS):", this.getStateSet()) // eslint-disable-line
|
||||
console.log("Operation Store (OS):") // eslint-disable-line
|
||||
this.os.logTable() // eslint-disable-line
|
||||
console.log("Deletion Store (DS):") //eslint-disable-line
|
||||
this.ds.logTable() // eslint-disable-line
|
||||
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
|
||||
console.warn('GC1|2 not empty!', this.store.gc1, this.store.gc2)
|
||||
}
|
||||
if (JSON.stringify(this.store.listenersById) !== '{}') {
|
||||
console.warn('listenersById not empty!')
|
||||
}
|
||||
if (JSON.stringify(this.store.listenersByIdExecuteNow) !== '[]') {
|
||||
console.warn('listenersByIdExecuteNow not empty!')
|
||||
}
|
||||
if (this.store.transactionInProgress) {
|
||||
console.warn('Transaction still in progress!')
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
transact (makeGen) {
|
||||
const t = new Transaction(this)
|
||||
try {
|
||||
while (makeGen != null) {
|
||||
makeGen.call(t)
|
||||
makeGen = this.getNextRequest()
|
||||
}
|
||||
} catch (e) {
|
||||
this.y.emit('error', e)
|
||||
}
|
||||
}
|
||||
destroy () {
|
||||
super.destroy()
|
||||
delete this.os
|
||||
delete this.ss
|
||||
delete this.ds
|
||||
}
|
||||
}
|
||||
Y.memory = Database
|
||||
}
|
||||
121
src/y.js
121
src/y.js
@@ -1,27 +1,18 @@
|
||||
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 extendMemory from './y-memory.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)
|
||||
extendMemory(Y)
|
||||
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)
|
||||
|
||||
Y.debug = debug
|
||||
debug.formatters.Y = formatYjsMessage
|
||||
debug.formatters.y = formatYjsMessageType
|
||||
Y.debug = require('debug')
|
||||
|
||||
var requiringModules = {}
|
||||
|
||||
module.exports = Y
|
||||
Y.requiringModules = requiringModules
|
||||
|
||||
Y.extend = function (name, value) {
|
||||
@@ -119,7 +110,7 @@ type YOptions = {
|
||||
}
|
||||
*/
|
||||
|
||||
export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||
if (opts.hasOwnProperty('sourceDir')) {
|
||||
Y.sourceDir = opts.sourceDir
|
||||
}
|
||||
@@ -129,42 +120,31 @@ export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||
modules.push(opts.share[name])
|
||||
}
|
||||
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)'))
|
||||
if (opts == null) reject('An options object is expected! ')
|
||||
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
||||
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
||||
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
||||
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
||||
else {
|
||||
opts = Y.utils.copyObject(opts)
|
||||
opts.connector = Y.utils.copyObject(opts.connector)
|
||||
opts.db = Y.utils.copyObject(opts.db)
|
||||
opts.share = Y.utils.copyObject(opts.share)
|
||||
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 (yconfig.persistence != null) {
|
||||
yconfig.persistence.retrieveContent()
|
||||
}
|
||||
yconfig.db.whenUserIdSet(function () {
|
||||
yconfig.init(function () {
|
||||
resolved = true
|
||||
resolve(yconfig)
|
||||
}, reject)
|
||||
})
|
||||
}).catch(reject)
|
||||
setTimeout(function () {
|
||||
Y.requestModules(modules).then(function () {
|
||||
var yconfig = new YConfig(opts)
|
||||
yconfig.db.whenUserIdSet(function () {
|
||||
yconfig.init(function () {
|
||||
resolve(yconfig)
|
||||
})
|
||||
})
|
||||
}).catch(reject)
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class YConfig extends Y.utils.NamedEventHandler {
|
||||
class YConfig {
|
||||
/* ::
|
||||
db: Y.AbstractDatabase;
|
||||
connector: Y.AbstractConnector;
|
||||
@@ -172,39 +152,41 @@ 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
|
||||
var share = {}
|
||||
this.share = share
|
||||
this.db.requestTransaction(function requestTransaction () {
|
||||
this.db.requestTransaction(function * requestTransaction () {
|
||||
// create shared object
|
||||
for (var propertyname in opts.share) {
|
||||
var typeConstructor = opts.share[propertyname].split('(')
|
||||
let typeArgs = ''
|
||||
if (typeConstructor.length === 2) {
|
||||
typeArgs = typeConstructor[1].split(')')[0] || ''
|
||||
}
|
||||
var typeName = typeConstructor.splice(0, 1)
|
||||
var type = Y[typeName]
|
||||
var typedef = type.typeDefinition
|
||||
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
|
||||
let args = Y.utils.parseTypeDefinition(type, typeArgs)
|
||||
share[propertyname] = this.store.initType.call(this, id, args)
|
||||
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
||||
var args = []
|
||||
if (typeConstructor.length === 1) {
|
||||
try {
|
||||
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
|
||||
} catch (e) {
|
||||
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
||||
}
|
||||
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)
|
||||
}
|
||||
this.store.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
})
|
||||
this.db.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
}
|
||||
isConnected () {
|
||||
return this.connector.isSynced
|
||||
@@ -233,9 +215,6 @@ class YConfig extends Y.utils.NamedEventHandler {
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}).then(() => {
|
||||
// remove existing event listener
|
||||
super.destroy()
|
||||
})
|
||||
}
|
||||
close () {
|
||||
@@ -246,13 +225,13 @@ class YConfig extends Y.utils.NamedEventHandler {
|
||||
} else {
|
||||
this.connector.disconnect()
|
||||
}
|
||||
return this.db.whenTransactionsFinished().then(function () {
|
||||
self.db.destroyTypes()
|
||||
return this.db.whenTransactionsFinished(function () {
|
||||
this.db.destroyTypes()
|
||||
// make sure to wait for all transactions before destroying the db
|
||||
self.db.requestTransaction(function () {
|
||||
self.db.destroy()
|
||||
this.db.requestTransaction(function * () {
|
||||
yield* self.db.destroy()
|
||||
})
|
||||
return self.db.whenTransactionsFinished()
|
||||
return this.db.whenTransactionsFinished()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,180 +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',
|
||||
start: null,
|
||||
end: null
|
||||
})
|
||||
})
|
||||
|
||||
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: {}
|
||||
})
|
||||
})
|
||||
@@ -1,217 +0,0 @@
|
||||
import Y from '../src/y.js'
|
||||
import Chance from 'chance'
|
||||
import { test, proxyConsole } from 'cutest'
|
||||
|
||||
proxyConsole()
|
||||
|
||||
var numberOfRBTreeTests = 10000
|
||||
|
||||
function checkRedNodesDoNotHaveBlackChildren (t, tree) {
|
||||
let correct = true
|
||||
function traverse (n) {
|
||||
if (n == null) {
|
||||
return
|
||||
}
|
||||
if (n.isRed()) {
|
||||
if (n.left != null) {
|
||||
correct = correct && !n.left.isRed()
|
||||
}
|
||||
if (n.right != null) {
|
||||
correct = correct && !n.right.isRed()
|
||||
}
|
||||
}
|
||||
traverse(n.left)
|
||||
traverse(n.right)
|
||||
}
|
||||
traverse(tree.root)
|
||||
t.assert(correct, 'Red nodes do not have black children')
|
||||
}
|
||||
|
||||
function checkBlackHeightOfSubTreesAreEqual (t, tree) {
|
||||
let correct = true
|
||||
function traverse (n) {
|
||||
if (n == null) {
|
||||
return 0
|
||||
}
|
||||
var sub1 = traverse(n.left)
|
||||
var sub2 = traverse(n.right)
|
||||
if (sub1 !== sub2) {
|
||||
correct = false
|
||||
}
|
||||
if (n.isRed()) {
|
||||
return sub1
|
||||
} else {
|
||||
return sub1 + 1
|
||||
}
|
||||
}
|
||||
traverse(tree.root)
|
||||
t.assert(correct, 'Black-height of sub-trees are equal')
|
||||
}
|
||||
|
||||
function checkRootNodeIsBlack (t, tree) {
|
||||
t.assert(tree.root == null || tree.root.isBlack(), 'root node is black')
|
||||
}
|
||||
|
||||
test('RedBlack Tree', async function redBlackTree (t) {
|
||||
let memory = new Y.memory(null, { // eslint-disable-line
|
||||
name: 'Memory',
|
||||
gcTimeout: -1
|
||||
})
|
||||
let tree = memory.os
|
||||
memory.requestTransaction(function () {
|
||||
tree.put({id: [8433]})
|
||||
tree.put({id: [12844]})
|
||||
tree.put({id: [1795]})
|
||||
tree.put({id: [30302]})
|
||||
tree.put({id: [64287]})
|
||||
tree.delete([8433])
|
||||
tree.put({id: [28996]})
|
||||
tree.delete([64287])
|
||||
tree.put({id: [22721]})
|
||||
})
|
||||
await memory.whenTransactionsFinished()
|
||||
checkRootNodeIsBlack(t, tree)
|
||||
checkBlackHeightOfSubTreesAreEqual(t, tree)
|
||||
checkRedNodesDoNotHaveBlackChildren(t, tree)
|
||||
})
|
||||
|
||||
test(`random tests (${numberOfRBTreeTests})`, async function random (t) {
|
||||
let chance = new Chance(t.getSeed() * 1000000000)
|
||||
let memory = new Y.memory(null, { // eslint-disable-line
|
||||
name: 'Memory',
|
||||
gcTimeout: -1
|
||||
})
|
||||
let tree = memory.os
|
||||
let elements = []
|
||||
memory.requestTransaction(function () {
|
||||
for (var i = 0; i < numberOfRBTreeTests; i++) {
|
||||
if (chance.bool({likelihood: 80})) {
|
||||
// 80% chance to insert an element
|
||||
let obj = [chance.integer({min: 0, max: numberOfRBTreeTests})]
|
||||
let nodeExists = tree.find(obj)
|
||||
if (!nodeExists) {
|
||||
if (elements.some(e => e[0] === obj[0])) {
|
||||
t.assert(false, 'tree and elements contain different results')
|
||||
}
|
||||
elements.push(obj)
|
||||
tree.put({id: obj})
|
||||
}
|
||||
} else if (elements.length > 0) {
|
||||
// ~20% chance to delete an element
|
||||
var elem = chance.pickone(elements)
|
||||
elements = elements.filter(function (e) {
|
||||
return !Y.utils.compareIds(e, elem)
|
||||
})
|
||||
tree.delete(elem)
|
||||
}
|
||||
}
|
||||
})
|
||||
await memory.whenTransactionsFinished()
|
||||
checkRootNodeIsBlack(t, tree)
|
||||
checkBlackHeightOfSubTreesAreEqual(t, tree)
|
||||
checkRedNodesDoNotHaveBlackChildren(t, tree)
|
||||
memory.requestTransaction(function () {
|
||||
let allNodesExist = true
|
||||
for (let id of elements) {
|
||||
let node = tree.find(id)
|
||||
if (!Y.utils.compareIds(node.id, id)) {
|
||||
allNodesExist = false
|
||||
}
|
||||
}
|
||||
t.assert(allNodesExist, 'All inserted nodes exist')
|
||||
})
|
||||
memory.requestTransaction(function () {
|
||||
let findAllNodesWithLowerBoundSerach = true
|
||||
for (let id of elements) {
|
||||
let node = tree.findWithLowerBound(id)
|
||||
if (!Y.utils.compareIds(node.id, id)) {
|
||||
findAllNodesWithLowerBoundSerach = false
|
||||
}
|
||||
}
|
||||
t.assert(
|
||||
findAllNodesWithLowerBoundSerach,
|
||||
'Find every object with lower bound search'
|
||||
)
|
||||
})
|
||||
|
||||
memory.requestTransaction(function () {
|
||||
let lowerBound = chance.pickone(elements)
|
||||
let expectedResults = elements.filter((e, pos) =>
|
||||
(Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
let actualResults = 0
|
||||
tree.iterate(this, lowerBound, null, function (val) {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'Iterating over a tree with lower bound yields the right amount of results'
|
||||
)
|
||||
})
|
||||
|
||||
memory.requestTransaction(function () {
|
||||
let expectedResults = elements.filter((e, pos) =>
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
let actualResults = 0
|
||||
tree.iterate(this, null, null, function (val) {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'iterating over a tree without bounds yields the right amount of results'
|
||||
)
|
||||
})
|
||||
|
||||
memory.requestTransaction(function () {
|
||||
let upperBound = chance.pickone(elements)
|
||||
let expectedResults = elements.filter((e, pos) =>
|
||||
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
let actualResults = 0
|
||||
tree.iterate(this, null, upperBound, function (val) {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'iterating over a tree with upper bound yields the right amount of results'
|
||||
)
|
||||
})
|
||||
|
||||
memory.requestTransaction(function () {
|
||||
let upperBound = chance.pickone(elements)
|
||||
let lowerBound = chance.pickone(elements)
|
||||
if (Y.utils.smaller(upperBound, lowerBound)) {
|
||||
[lowerBound, upperBound] = [upperBound, lowerBound]
|
||||
}
|
||||
let expectedResults = elements.filter((e, pos) =>
|
||||
(Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
|
||||
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) &&
|
||||
elements.indexOf(e) === pos
|
||||
).length
|
||||
let actualResults = 0
|
||||
tree.iterate(this, lowerBound, upperBound, function (val) {
|
||||
if (val == null) {
|
||||
t.assert(false, 'val is undefined!')
|
||||
}
|
||||
actualResults++
|
||||
})
|
||||
t.assert(
|
||||
expectedResults === actualResults,
|
||||
'iterating over a tree with upper bound yields the right amount of results'
|
||||
)
|
||||
})
|
||||
|
||||
await memory.whenTransactionsFinished()
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
*/
|
||||
@@ -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)
|
||||
})
|
||||
*/
|
||||
@@ -1,352 +0,0 @@
|
||||
import { wait, initArrays, compareUsers, Y, flushAll, applyRandomTests } from '../../yjs/tests-lib/helper.js'
|
||||
import { test } from 'cutest'
|
||||
|
||||
test('set property', async function xml0 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
xml0.setAttribute('height', 10)
|
||||
t.assert(xml0.getAttribute('height') === 10, 'Simple set+get works')
|
||||
await flushAll(t, users)
|
||||
t.assert(xml1.getAttribute('height') === 10, 'Simple set+get works (remote)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('events', async function xml1 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||
var event
|
||||
var remoteEvent
|
||||
let expectedEvent
|
||||
xml0.observe(function (e) {
|
||||
delete e._content
|
||||
delete e.nodes
|
||||
delete e.values
|
||||
event = e
|
||||
})
|
||||
xml1.observe(function (e) {
|
||||
delete e._content
|
||||
delete e.nodes
|
||||
delete e.values
|
||||
remoteEvent = e
|
||||
})
|
||||
xml0.setAttribute('key', 'value')
|
||||
expectedEvent = {
|
||||
type: 'attributeChanged',
|
||||
value: 'value',
|
||||
name: 'key'
|
||||
}
|
||||
t.compare(event, expectedEvent, 'attribute changed event')
|
||||
await flushAll(t, users)
|
||||
t.compare(remoteEvent, expectedEvent, 'attribute changed event (remote)')
|
||||
// check attributeRemoved
|
||||
xml0.removeAttribute('key')
|
||||
expectedEvent = {
|
||||
type: 'attributeRemoved',
|
||||
name: 'key'
|
||||
}
|
||||
t.compare(event, expectedEvent, 'attribute deleted event')
|
||||
await flushAll(t, users)
|
||||
t.compare(remoteEvent, expectedEvent, 'attribute deleted event (remote)')
|
||||
// test childInserted event
|
||||
expectedEvent = {
|
||||
type: 'childInserted',
|
||||
index: 0
|
||||
}
|
||||
xml0.insert(0, [Y.XmlText('some text')])
|
||||
t.compare(event, expectedEvent, 'child inserted event')
|
||||
await flushAll(t, users)
|
||||
t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)')
|
||||
// test childRemoved
|
||||
xml0.delete(0)
|
||||
expectedEvent = {
|
||||
type: 'childRemoved',
|
||||
index: 0
|
||||
}
|
||||
t.compare(event, expectedEvent, 'child deleted event')
|
||||
await flushAll(t, users)
|
||||
t.compare(remoteEvent, expectedEvent, 'child deleted event (remote)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('attribute modifications (y -> dom)', async function xml2 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.setAttribute('height', '100px')
|
||||
await wait()
|
||||
t.assert(dom0.getAttribute('height') === '100px', 'setAttribute')
|
||||
xml0.removeAttribute('height')
|
||||
await wait()
|
||||
t.assert(dom0.getAttribute('height') == null, 'removeAttribute')
|
||||
xml0.setAttribute('class', 'stuffy stuff')
|
||||
await wait()
|
||||
t.assert(dom0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('attribute modifications (dom -> y)', async function xml3 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
dom0.setAttribute('height', '100px')
|
||||
await wait()
|
||||
t.assert(xml0.getAttribute('height') === '100px', 'setAttribute')
|
||||
dom0.removeAttribute('height')
|
||||
await wait()
|
||||
t.assert(xml0.getAttribute('height') == null, 'removeAttribute')
|
||||
dom0.setAttribute('class', 'stuffy stuff')
|
||||
await wait()
|
||||
t.assert(xml0.getAttribute('class') === 'stuffy stuff', 'set class attribute')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('element insert (dom -> y)', async function xml4 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
dom0.insertBefore(document.createTextNode('some text'), null)
|
||||
dom0.insertBefore(document.createElement('p'), null)
|
||||
await wait()
|
||||
t.assert(xml0.get(0).toString() === 'some text', 'Retrieve Text Node')
|
||||
t.assert(xml0.get(1).nodeName === 'P', 'Retrieve Element node')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('element insert (y -> dom)', async function xml5 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.insert(0, [Y.XmlText('some text')])
|
||||
xml0.insert(1, [Y.XmlElement('p')])
|
||||
t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node')
|
||||
t.assert(dom0.childNodes[1].nodeName === 'P', 'Retrieve Element node')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('y on insert, then delete (dom -> y)', async function xml6 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
dom0.insertBefore(document.createElement('p'), null)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'one node present')
|
||||
dom0.childNodes[0].remove()
|
||||
await wait()
|
||||
t.assert(xml0.length === 0, 'no node present after delete')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('y on insert, then delete (y -> dom)', async function xml7 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.insert(0, [Y.XmlElement('p')])
|
||||
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
|
||||
xml0.delete(0, 1)
|
||||
t.assert(dom0.childNodes.length === 0, '#childNodes is empty after delete')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (1) (Text)', async function xml8 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.insert(0, ['1', '2', '3'].map(Y.XmlText))
|
||||
await wait()
|
||||
xml0.delete(1, 2)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].textContent === '1', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (2) (Text)', async function xml9 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.insert(0, ['1', '2', '3'].map(Y.XmlText))
|
||||
await wait()
|
||||
xml0.delete(0, 1)
|
||||
xml0.delete(1, 1)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].textContent === '2', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (1) (Element)', async function xml10 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
|
||||
await wait()
|
||||
xml0.delete(1, 2)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].nodeName === 'A', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('delete consecutive (2) (Element)', async function xml11 (t) {
|
||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
|
||||
await wait()
|
||||
xml0.delete(0, 1)
|
||||
xml0.delete(1, 1)
|
||||
await wait()
|
||||
t.assert(xml0.length === 1, 'check length (y)')
|
||||
t.assert(dom0.childNodes.length === 1, 'check length (dom)')
|
||||
t.assert(dom0.childNodes[0].nodeName === 'B', 'check content')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
let dom1 = xml1.getDom()
|
||||
users[1].disconnect()
|
||||
xml0.insert(0, [Y.XmlElement('A'), Y.XmlElement('B'), Y.XmlElement('C')])
|
||||
xml0.insert(0, [Y.XmlElement('X'), Y.XmlElement('Y'), Y.XmlElement('Z')])
|
||||
await users[1].reconnect()
|
||||
await flushAll(t, users)
|
||||
t.assert(xml0.length === 6, 'check length (y)')
|
||||
t.assert(xml1.length === 6, 'check length (y) (reconnected user)')
|
||||
t.assert(dom0.childNodes.length === 6, 'check length (dom)')
|
||||
t.assert(dom1.childNodes.length === 6, 'check length (dom) (reconnected user)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('move element to a different position', async function xml13 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
let dom1 = xml1.getDom()
|
||||
dom0.append(document.createElement('div'))
|
||||
dom0.append(document.createElement('h1'))
|
||||
await flushAll(t, users)
|
||||
dom1.insertBefore(dom1.childNodes[0], null)
|
||||
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 0)')
|
||||
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 0)')
|
||||
t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 1)')
|
||||
t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 1)')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('filter node', async function xml14 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
let dom1 = xml1.getDom()
|
||||
let domFilter = (node, attrs) => {
|
||||
if (node.nodeName === 'H1') {
|
||||
return null
|
||||
} else {
|
||||
return attrs
|
||||
}
|
||||
}
|
||||
xml0.setDomFilter(domFilter)
|
||||
xml1.setDomFilter(domFilter)
|
||||
dom0.append(document.createElement('div'))
|
||||
dom0.append(document.createElement('h1'))
|
||||
await flushAll(t, users)
|
||||
t.assert(dom1.childNodes.length === 1, 'Only one node was not transmitted')
|
||||
t.assert(dom1.childNodes[0].nodeName === 'DIV', 'div node was transmitted')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
test('filter attribute', async function xml15 (t) {
|
||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
||||
let dom0 = xml0.getDom()
|
||||
let dom1 = xml1.getDom()
|
||||
let domFilter = (node, attrs) => {
|
||||
return attrs.filter(name => name !== 'hidden')
|
||||
}
|
||||
xml0.setDomFilter(domFilter)
|
||||
xml1.setDomFilter(domFilter)
|
||||
dom0.setAttribute('hidden', 'true')
|
||||
dom0.setAttribute('style', 'height: 30px')
|
||||
dom0.setAttribute('data-me', '77')
|
||||
await flushAll(t, users)
|
||||
t.assert(dom0.getAttribute('hidden') === 'true', 'User 0 still has the attribute')
|
||||
t.assert(dom1.getAttribute('hidden') == null, 'User 1 did not receive update')
|
||||
t.assert(dom1.getAttribute('style') === 'height: 30px', 'User 1 received style update')
|
||||
t.assert(dom1.getAttribute('data-me') === '77', 'User 1 received data update')
|
||||
await compareUsers(t, users)
|
||||
})
|
||||
|
||||
// TODO: move elements
|
||||
var xmlTransactions = [
|
||||
function attributeChange (t, user, chance) {
|
||||
user.share.xml.getDom().setAttribute(chance.word(), chance.word())
|
||||
},
|
||||
function attributeChangeHidden (t, user, chance) {
|
||||
user.share.xml.getDom().setAttribute('hidden', chance.word())
|
||||
},
|
||||
function insertText (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||
dom.insertBefore(document.createTextNode(chance.word()), succ)
|
||||
},
|
||||
function insertHiddenDom (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||
dom.insertBefore(document.createElement('hidden'), succ)
|
||||
},
|
||||
function insertDom (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||
dom.insertBefore(document.createElement(chance.word()), succ)
|
||||
},
|
||||
function deleteChild (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
if (dom.childNodes.length > 0) {
|
||||
var d = chance.pickone(dom.childNodes)
|
||||
d.remove()
|
||||
}
|
||||
},
|
||||
function insertTextSecondLayer (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = chance.pickone(dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createTextNode(chance.word()), succ)
|
||||
}
|
||||
},
|
||||
function insertDomSecondLayer (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = chance.pickone(dom.children)
|
||||
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
||||
dom2.insertBefore(document.createElement(chance.word()), succ)
|
||||
}
|
||||
},
|
||||
function deleteChildSecondLayer (t, user, chance) {
|
||||
let dom = user.share.xml.getDom()
|
||||
if (dom.children.length > 0) {
|
||||
let dom2 = chance.pickone(dom.children)
|
||||
if (dom2.childNodes.length > 0) {
|
||||
let d = chance.pickone(dom2.childNodes)
|
||||
d.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
test('y-xml: Random tests (10)', async function xmlRandom10 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 10)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (42)', async function xmlRandom42 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 42)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (43)', async function xmlRandom43 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 43)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (44)', async function xmlRandom44 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 44)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (45)', async function xmlRandom45 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 45)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (46)', async function xmlRandom46 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 46)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (47)', async function xmlRandom47 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 47)
|
||||
})
|
||||
@@ -1,324 +0,0 @@
|
||||
|
||||
import _Y from '../../yjs/src/y.js'
|
||||
|
||||
import yArray from '../../y-array/src/y-array.js'
|
||||
import yText from '../../y-text/src/y-text.js'
|
||||
import yMap from '../../y-map/src/y-map.js'
|
||||
import yXml from '../../y-xml/src/y-xml.js'
|
||||
import yTest from './test-connector.js'
|
||||
|
||||
import Chance from 'chance'
|
||||
|
||||
export let Y = _Y
|
||||
|
||||
Y.extend(yArray, yText, yMap, yTest, yXml)
|
||||
|
||||
export var database = { name: 'memory' }
|
||||
export var connector = { name: 'test', url: 'http://localhost:1234' }
|
||||
|
||||
function getStateSet () {
|
||||
var ss = {}
|
||||
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 = {}
|
||||
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()))
|
||||
}
|
||||
|
||||
export function attrsObject (dom) {
|
||||
let keys = []
|
||||
let yxml = dom.__yxml
|
||||
for (let i = 0; i < dom.attributes.length; i++) {
|
||||
keys.push(dom.attributes[i].name)
|
||||
}
|
||||
keys = yxml._domFilter(dom, keys)
|
||||
let obj = {}
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i]
|
||||
obj[key] = dom.getAttribute(key)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function domToJson (dom) {
|
||||
if (dom.nodeType === document.TEXT_NODE) {
|
||||
return dom.textContent
|
||||
} else if (dom.nodeType === document.ELEMENT_NODE) {
|
||||
let attributes = attrsObject(dom, dom.__yxml)
|
||||
let children = Array.from(dom.childNodes.values())
|
||||
.filter(d => d.__yxml !== false)
|
||||
.map(domToJson)
|
||||
return {
|
||||
name: dom.nodeName,
|
||||
children: children,
|
||||
attributes: attributes
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unsupported node type')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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)
|
||||
var userXmlValues = users.map(u => u.share.xml.getDom()).map(domToJson)
|
||||
|
||||
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 = []
|
||||
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 = this.isDeleted(op.id)
|
||||
if (!opIsDeleted) {
|
||||
data.os[JSON.stringify(op.id)] = op
|
||||
}
|
||||
} else {
|
||||
data.os[JSON.stringify(op.id)] = op
|
||||
}
|
||||
}
|
||||
data.ds = getDeleteSet.apply(this)
|
||||
data.ss = 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(userXmlValues[i], userXmlValues[i + 1], 'xml types')
|
||||
t.compare(data[i].os, data[i + 1].os, 'os')
|
||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||
}, `Compare user${i} with user${i + 1}`)
|
||||
}
|
||||
await Promise.all(users.map(async (u) => {
|
||||
await u.close()
|
||||
}))
|
||||
}
|
||||
|
||||
export async function initArrays (t, opts) {
|
||||
var result = {
|
||||
users: []
|
||||
}
|
||||
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map', xml: 'XmlElement("div")' }, 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]
|
||||
}
|
||||
y.share.xml.setDomFilter(function (d, attrs) {
|
||||
if (d.nodeName === 'HIDDEN') {
|
||||
return null
|
||||
} else {
|
||||
return attrs.filter(a => a !== 'hidden')
|
||||
}
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user