Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b5c02f1ce | ||
|
|
2be6e935a4 | ||
|
|
0ddf3bf742 | ||
|
|
5f29724578 | ||
|
|
ab6cde07e6 | ||
|
|
0455eaa8ad | ||
|
|
9ed7e15d0f | ||
|
|
6e633d0bd9 | ||
|
|
e16195cb54 | ||
|
|
86c46cf0ec | ||
|
|
8770c8e934 | ||
|
|
7e12ea2db5 | ||
|
|
3ca260e0da | ||
|
|
edb5e4f719 | ||
|
|
be3b8b65ce | ||
|
|
d093ef56c8 | ||
|
|
90b2a895b8 | ||
|
|
4f57c91b82 | ||
|
|
3e1d89253f | ||
|
|
03e1a3fc12 | ||
|
|
5c33f41c30 | ||
|
|
65e8c29b33 | ||
|
|
fed77d532f | ||
|
|
d129184f7b | ||
|
|
a05bb1d4f9 | ||
|
|
65af4963e6 | ||
|
|
4dce0816a6 | ||
|
|
5384bf4faf | ||
|
|
454ac9ba16 | ||
|
|
e2ec53be65 | ||
|
|
aa6edcfd9b | ||
|
|
f31ec9a8b8 | ||
|
|
003fa735a0 | ||
|
|
574f0c3269 | ||
|
|
eb4fb3a225 | ||
|
|
c97130abc4 | ||
|
|
a19cfa1465 | ||
|
|
bb45abbb70 | ||
|
|
67b47fd868 | ||
|
|
2c18b9ffad | ||
|
|
a6b7d76544 | ||
|
|
442ea7ec70 | ||
|
|
747da52c0b | ||
|
|
6c37bd4463 | ||
|
|
dd6c196135 | ||
|
|
252bec0ad2 | ||
|
|
6c8876d282 | ||
|
|
3c317828d1 | ||
|
|
cd3f4a72d6 | ||
|
|
2c852c85c6 | ||
|
|
434ec84837 | ||
|
|
2b618cd83c | ||
|
|
f4327529b9 | ||
|
|
67189f4d44 | ||
|
|
6225fb4dfd | ||
|
|
a7550fe5d3 | ||
|
|
9d9c84f40e | ||
|
|
ae91902de3 | ||
|
|
033d24eee7 | ||
|
|
8abef69aa7 | ||
|
|
7e4dedab38 | ||
|
|
85e488bbe6 | ||
|
|
a6a321da10 | ||
|
|
008764ccdc | ||
|
|
de5f4abe32 | ||
|
|
382d06f6d4 | ||
|
|
66de422749 | ||
|
|
bbf5e39408 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,15 +1,4 @@
|
||||
node_modules
|
||||
bower_components
|
||||
build
|
||||
build_test
|
||||
.directory
|
||||
.codio
|
||||
.settings
|
||||
.jshintignore
|
||||
.jshintrc
|
||||
.validate.json
|
||||
/y.js
|
||||
/y.js.map
|
||||
/y-*
|
||||
.vscode
|
||||
jsconfig.json
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "dist"]
|
||||
path = dist
|
||||
url = https://github.com/y-js/yjs.git
|
||||
branch = dist
|
||||
@@ -22,6 +22,7 @@ is a list of the modules we know of:
|
||||
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
||||
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
||||
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
||||
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|
||||
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
||||
|
||||
##### Database adapters
|
||||
|
||||
1
dist
1
dist
Submodule dist deleted from 42aa7ec5c9
@@ -9,16 +9,6 @@
|
||||
"license": "MIT",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"yjs": "latest",
|
||||
"y-array": "latest",
|
||||
"y-map": "latest",
|
||||
"y-memory": "latest",
|
||||
"y-richtext": "latest",
|
||||
"y-webrtc": "latest",
|
||||
"y-websockets-client": "latest",
|
||||
"y-text": "latest",
|
||||
"y-indexeddb": "latest",
|
||||
"y-xml": "latest",
|
||||
"quill": "^1.0.0-rc.2",
|
||||
"ace": "~1.2.3",
|
||||
"ace-builds": "~1.2.3",
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<input name="message" type="text" style="width:60%;">
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<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,5 +1,4 @@
|
||||
/* @flow */
|
||||
/* global Y */
|
||||
/* global Y, chat */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
@@ -17,10 +16,10 @@ Y({
|
||||
}).then(function (y) {
|
||||
window.yChat = y
|
||||
// This functions inserts a message at the specified position in the DOM
|
||||
function appendMessage(message, position) {
|
||||
function appendMessage (message, position) {
|
||||
var p = document.createElement('p')
|
||||
var uname = document.createElement('span')
|
||||
uname.appendChild(document.createTextNode(message.username + ": "))
|
||||
uname.appendChild(document.createTextNode(message.username + ': '))
|
||||
p.appendChild(uname)
|
||||
p.appendChild(document.createTextNode(message.message))
|
||||
document.querySelector('#chat').insertBefore(p, chat.children[position] || null)
|
||||
@@ -28,23 +27,22 @@ Y({
|
||||
// This function makes sure that only 7 messages exist in the chat history.
|
||||
// The rest is deleted
|
||||
function cleanupChat () {
|
||||
var len
|
||||
while ((len = y.share.chat.length) > 7) {
|
||||
y.share.chat.delete(0)
|
||||
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 (var i = 0; i < event.length; i++) {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
appendMessage(event.values[i], event.index + i)
|
||||
}
|
||||
} else if (event.type === 'delete') {
|
||||
for (var i = 0; i < event.length; i++) {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
chat.children[event.index].remove()
|
||||
}
|
||||
}
|
||||
@@ -54,8 +52,8 @@ Y({
|
||||
document.querySelector('#chatform').onsubmit = function (event) {
|
||||
// the form is submitted
|
||||
var message = {
|
||||
username: this.querySelector("[name=username]").value,
|
||||
message: this.querySelector("[name=message]").value
|
||||
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) {
|
||||
@@ -66,10 +64,10 @@ Y({
|
||||
// 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 = ""
|
||||
this.querySelector('[name=message]').value = ''
|
||||
}
|
||||
// Do not send this form!
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
</style>
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../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>
|
||||
|
||||
@@ -7,8 +7,8 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example'
|
||||
// url: 'localhost:1234'
|
||||
room: 'drawing-example',
|
||||
url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
|
||||
11
examples/html-editor/index.html
Normal file
11
examples/html-editor/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!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>
|
||||
21
examples/html-editor/index.js
Normal file
21
examples/html-editor/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// 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)
|
||||
})
|
||||
58
examples/infiniteyjs/index.html
Normal file
58
examples/infiniteyjs/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!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>
|
||||
64
examples/infiniteyjs/index.js
Normal file
64
examples/infiniteyjs/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/* 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)
|
||||
})
|
||||
})
|
||||
@@ -16,7 +16,10 @@
|
||||
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../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>
|
||||
|
||||
@@ -8,9 +8,9 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Puzzle-example'
|
||||
room: 'Puzzle-example',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
piece1: 'Map',
|
||||
piece2: 'Map',
|
||||
@@ -19,11 +19,16 @@ Y({
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yJigsaw = y
|
||||
var origin // mouse start position - translation of piece
|
||||
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)
|
||||
var translation = d3
|
||||
.select(this)
|
||||
.attr('transform')
|
||||
.slice(10, -1)
|
||||
.split(',')
|
||||
.map(Number)
|
||||
// mouse coordinates
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
origin = {
|
||||
@@ -31,11 +36,11 @@ Y({
|
||||
y: mouse[1] - translation[1]
|
||||
}
|
||||
})
|
||||
.on("drag", function(){
|
||||
.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 + ")")
|
||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||
})
|
||||
.on('dragend', function (piece, i) {
|
||||
// save the current translation of the puzzle piece
|
||||
@@ -46,24 +51,24 @@ 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)
|
||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||
|
||||
pieces
|
||||
.classed('draggable', true)
|
||||
.attr("transform", function (piece) {
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return "translate(" + translation.x + "," + translation.y + ")"
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
}).call(drag)
|
||||
|
||||
data.forEach(function(piece){
|
||||
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) {
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return "translate(" + translation.x + "," + translation.y + ")"
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* global Y */
|
||||
/* global Y, monaco */
|
||||
|
||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' }})
|
||||
require(['vs/editor/editor.main'], function() {
|
||||
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: {
|
||||
@@ -28,4 +28,3 @@ require(['vs/editor/editor.main'], function() {
|
||||
y.share.monaco.bindMonaco(editor)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
1173
examples/package-lock.json
generated
Normal file
1173
examples/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,19 @@
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<!-- 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">
|
||||
<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;
|
||||
@@ -19,13 +19,17 @@
|
||||
</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://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="../bower_components/yjs/y.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>
|
||||
|
||||
@@ -8,7 +8,8 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example-quill-1.0-test'
|
||||
room: 'richtext-example-quill-1.0-test',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
@@ -29,11 +30,11 @@ Y({
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{list: 'ordered' }]
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
});
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
})
|
||||
|
||||
27
examples/rollup.config.js
Normal file
27
examples/rollup.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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,13 +1,13 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// register yjs service worker
|
||||
if('serviceWorker' in navigator){
|
||||
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);
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ Y({
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{list: 'ordered' }]
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../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,5 +1,8 @@
|
||||
/* global Y */
|
||||
|
||||
// eslint-disable-next-line
|
||||
let search = new URLSearchParams(location.search)
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
@@ -7,17 +10,21 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example'
|
||||
// url: '127.0.0.1:1234'
|
||||
room: 'Textarea-example',
|
||||
// url: '//localhost:1234',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
// options: { transports: ['websocket'], upgrade: false }
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
textarea: 'Text' // y.share.textarea is of type Y.Text
|
||||
}
|
||||
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'))
|
||||
// thats it..
|
||||
}).catch(() => {
|
||||
console.log('Something went wrong while creating the instance..')
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<!-- 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>
|
||||
|
||||
@@ -7,6 +7,8 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'Xml-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
|
||||
12
examples/yjs-dist.esm
Normal file
12
examples/yjs-dist.esm
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import Y from '../src/y.js'
|
||||
import yArray from '../../y-array/src/y-array.js'
|
||||
import yMap from '../../y-map/src/Map.js'
|
||||
import yText from '../../y-text/src/Text.js'
|
||||
import yXml from '../../y-xml/src/y-xml.js'
|
||||
import yMemory from '../../y-memory/src/y-memory.js'
|
||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||
|
||||
Y.extend(yArray, yMap, yText, yXml, yMemory, yWebsocketsClient)
|
||||
|
||||
export default Y
|
||||
758
package-lock.json
generated
758
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,21 +1,26 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-0",
|
||||
"version": "13.0.0-16",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./src/y.js",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./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.dist.js",
|
||||
"serve": "concurrently 'serve examples' 'rollup -wc rollup.dist.js -o examples/bower_components/yjs/y.js'"
|
||||
"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"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint",
|
||||
"test"
|
||||
"files": [
|
||||
"y.*"
|
||||
],
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"./y.js",
|
||||
"./y.js.map"
|
||||
"/y.js",
|
||||
"/y.js.map"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
@@ -40,12 +45,13 @@
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-preset-latest": "^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",
|
||||
@@ -54,9 +60,11 @@
|
||||
"rollup-plugin-uglify": "^1.0.2",
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"standard": "^10.0.2"
|
||||
"standard": "^10.0.2",
|
||||
"tag-dist-files": "^0.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^2.6.8"
|
||||
"debug": "^2.6.8",
|
||||
"utf-8": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
26
rollup.node.js
Normal file
26
rollup.node.js
Normal file
@@ -0,0 +1,26 @@
|
||||
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}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
import multiEntry from 'rollup-plugin-multi-entry'
|
||||
|
||||
export default {
|
||||
entry: 'tests/*.js',
|
||||
moduleName: 'y-array-tests',
|
||||
entry: 'test/y-xml.tests.js',
|
||||
moduleName: 'y-tests',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
@@ -15,6 +15,6 @@ export default {
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
],
|
||||
dest: 'y-array.test.js',
|
||||
dest: 'y.test.js',
|
||||
sourceMap: true
|
||||
}
|
||||
|
||||
399
src/Connector.js
399
src/Connector.js
@@ -1,40 +1,18 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
||||
function canWrite (auth) { return auth === 'write' }
|
||||
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||
import { sendSyncStep1, computeMessageSyncStep1, computeMessageSyncStep2, computeMessageUpdate } from './MessageHandler.js'
|
||||
|
||||
export default function extendConnector (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.
|
||||
@@ -51,56 +29,52 @@ 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 = {}
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.userEventListeners = []
|
||||
this.whenSyncedListeners = []
|
||||
this.currentSyncTarget = null
|
||||
this.syncingClients = []
|
||||
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
||||
this.debug = opts.debug === true
|
||||
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 === true) {
|
||||
this.setUserId(Y.utils.generateGuid())
|
||||
}
|
||||
}
|
||||
resetAuth (auth) {
|
||||
if (this.authInfo !== auth) {
|
||||
this.authInfo = auth
|
||||
this.broadcast({
|
||||
type: 'auth',
|
||||
auth: this.authInfo
|
||||
})
|
||||
if (opts.generateUserId !== false) {
|
||||
this.setUserId(Y.utils.generateUserId())
|
||||
}
|
||||
}
|
||||
|
||||
reconnect () {
|
||||
this.log('reconnecting..')
|
||||
return this.y.db.startGarbageCollector()
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
this.log('discronnecting..')
|
||||
this.connections = {}
|
||||
this.connections = new Map()
|
||||
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.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
this.connections.forEach((user, userId) => {
|
||||
user.isSynced = false
|
||||
this._syncWithUser(userId)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -108,23 +82,21 @@ export default function extendConnector (Y/* :any */) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onUserEvent (f) {
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
|
||||
removeUserEventListener (f) {
|
||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||
}
|
||||
|
||||
userLeft (user) {
|
||||
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
|
||||
})
|
||||
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)
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userLeft',
|
||||
@@ -133,21 +105,25 @@ export default function extendConnector (Y/* :any */) {
|
||||
}
|
||||
}
|
||||
}
|
||||
userJoined (user, role) {
|
||||
userJoined (user, role, auth) {
|
||||
if (role == null) {
|
||||
throw new Error('You must specify the role of the joined user!')
|
||||
}
|
||||
if (this.connections[user] != null) {
|
||||
if (this.connections.has(user)) {
|
||||
throw new Error('This user already joined!')
|
||||
}
|
||||
this.log('User joined: %s', user)
|
||||
this.connections[user] = {
|
||||
this.log('%s: User joined %s', this.userId, user)
|
||||
this.connections.set(user, {
|
||||
uid: user,
|
||||
isSynced: false,
|
||||
role: role
|
||||
}
|
||||
role: role,
|
||||
processAfterAuth: [],
|
||||
auth: auth || null,
|
||||
receivedSyncStep2: false
|
||||
})
|
||||
let defer = {}
|
||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
||||
this.connections[user].syncStep2 = defer
|
||||
this.connections.get(user).syncStep2 = defer
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userJoined',
|
||||
@@ -155,9 +131,7 @@ export default function extendConnector (Y/* :any */) {
|
||||
role: role
|
||||
})
|
||||
}
|
||||
if (this.currentSyncTarget == null) {
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
this._syncWithUser(user)
|
||||
}
|
||||
// Execute a function _when_ we are connected.
|
||||
// If not connected, wait until connected
|
||||
@@ -168,61 +142,39 @@ export default function extendConnector (Y/* :any */) {
|
||||
this.whenSyncedListeners.push(f)
|
||||
}
|
||||
}
|
||||
findNextSyncTarget () {
|
||||
if (this.currentSyncTarget != null) {
|
||||
return // "The current sync has not finished!"
|
||||
}
|
||||
|
||||
var syncUser = null
|
||||
for (var uid in this.connections) {
|
||||
if (!this.connections[uid].isSynced) {
|
||||
syncUser = uid
|
||||
break
|
||||
}
|
||||
}
|
||||
var conn = this
|
||||
if (syncUser != null) {
|
||||
this.currentSyncTarget = syncUser
|
||||
this.y.db.requestTransaction(function * () {
|
||||
var stateSet = yield * this.getStateSet()
|
||||
var deleteSet = yield * this.getDeleteSet()
|
||||
var answer = {
|
||||
type: 'sync step 1',
|
||||
stateSet: stateSet,
|
||||
// deleteSet: deleteSet,
|
||||
protocolVersion: conn.protocolVersion,
|
||||
auth: conn.authInfo
|
||||
}
|
||||
if (conn.preferUntransformed && Object.keys(stateSet).length === 0) {
|
||||
answer.preferUntransformed = true
|
||||
}
|
||||
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
|
||||
// It is safer to remove this!
|
||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||
// call whensynced listeners
|
||||
for (var f of conn.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
conn.whenSyncedListeners = []
|
||||
}
|
||||
})
|
||||
}
|
||||
_syncWithUser (userid) {
|
||||
if (this.role === 'slave') {
|
||||
return // "The current sync has not finished or this is controlled by a master!"
|
||||
}
|
||||
sendSyncStep1(this, userid)
|
||||
}
|
||||
send (uid, message) {
|
||||
this.log('Send \'%s\' to %s', message.type, uid)
|
||||
this.logMessage('Message: %j', message)
|
||||
_fireIsSyncedListeners () {
|
||||
this.y.db.whenTransactionsFinished().then(() => {
|
||||
if (!this.isSynced) {
|
||||
this.isSynced = true
|
||||
// It is safer to remove this!
|
||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||
// call whensynced listeners
|
||||
for (var f of this.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = []
|
||||
}
|
||||
})
|
||||
}
|
||||
broadcast (message) {
|
||||
this.log('Broadcast \'%s\'', message.type)
|
||||
this.logMessage('Message: %j', message)
|
||||
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')
|
||||
}
|
||||
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
|
||||
this.logMessage('Message: %Y', buffer)
|
||||
}
|
||||
/*
|
||||
Buffer operations, and broadcast them when ready.
|
||||
@@ -234,11 +186,18 @@ export default function extendConnector (Y/* :any */) {
|
||||
var self = this
|
||||
function broadcastOperations () {
|
||||
if (self.broadcastOpBuffer.length > 0) {
|
||||
self.broadcast({
|
||||
type: 'update',
|
||||
ops: self.broadcastOpBuffer
|
||||
})
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(self.opts.room)
|
||||
encoder.writeVarString('update')
|
||||
let ops = self.broadcastOpBuffer
|
||||
self.broadcastOpBuffer = []
|
||||
let length = ops.length
|
||||
encoder.writeUint32(length)
|
||||
for (var i = 0; i < length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
self.broadcast(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
if (this.broadcastOpBuffer.length === 0) {
|
||||
@@ -251,153 +210,81 @@ export default function extendConnector (Y/* :any */) {
|
||||
/*
|
||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||
*/
|
||||
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
||||
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!'))
|
||||
}
|
||||
if (sender === this.userId) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
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(new Error('Incompatible protocol version'))
|
||||
}
|
||||
if (message.auth != null && this.connections[sender] != null) {
|
||||
// authenticate using auth in message
|
||||
var auth = this.checkAuth(message.auth, this.y)
|
||||
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)
|
||||
}
|
||||
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
|
||||
let wait // wait for sync step 2 to complete
|
||||
if (this.role === 'slave') {
|
||||
wait = Promise.all(Object.keys(this.connections)
|
||||
.map(uid => this.connections[uid])
|
||||
.filter(conn => conn.role === 'master')
|
||||
.map(conn => conn.syncStep2.promise)
|
||||
)
|
||||
} else {
|
||||
wait = Promise.resolve()
|
||||
}
|
||||
wait.then(() => {
|
||||
this.y.db.requestTransaction(function * () {
|
||||
var currentStateSet = yield * this.getStateSet()
|
||||
// TODO: remove
|
||||
// if (canWrite(auth)) {
|
||||
// yield * this.applyDeleteSet(m.deleteSet)
|
||||
// }
|
||||
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)
|
||||
|
||||
var ds = yield * this.getDeleteSet()
|
||||
var answer = {
|
||||
type: 'sync step 2',
|
||||
stateSet: currentStateSet,
|
||||
deleteSet: ds,
|
||||
protocolVersion: this.protocolVersion,
|
||||
auth: this.authInfo
|
||||
}
|
||||
if (message.preferUntransformed === true && Object.keys(m.stateSet).length === 0) {
|
||||
answer.osUntransformed = yield * this.getOperationsUntransformed()
|
||||
} else {
|
||||
answer.os = yield * this.getOperations(m.stateSet)
|
||||
}
|
||||
conn.send(sender, answer)
|
||||
if (this.forwardToSyncingClients) {
|
||||
conn.syncingClients.push(sender)
|
||||
setTimeout(function () {
|
||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
||||
return cli !== sender
|
||||
})
|
||||
conn.send(sender, {
|
||||
type: 'sync done'
|
||||
})
|
||||
}, 5000) // TODO: conn.syncingClientDuration)
|
||||
} else {
|
||||
conn.send(sender, {
|
||||
type: 'sync done'
|
||||
})
|
||||
}
|
||||
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
|
||||
this.logMessage('Message: %Y', buffer)
|
||||
|
||||
if (senderConn == null && !skipAuth) {
|
||||
throw new Error('Received message from unknown peer!')
|
||||
}
|
||||
|
||||
if (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
|
||||
})
|
||||
})
|
||||
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
||||
var db = this.y.db
|
||||
let defer = this.connections[sender].syncStep2
|
||||
let m = message
|
||||
// apply operations first
|
||||
db.requestTransaction(function * () {
|
||||
yield * this.applyDeleteSet(m.deleteSet)
|
||||
if (m.osUntransformed != null) {
|
||||
yield * this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
|
||||
} else {
|
||||
this.store.apply(m.os)
|
||||
}
|
||||
defer.resolve()
|
||||
})
|
||||
/*/ then apply ds
|
||||
db.whenTransactionsFinished().then(() => {
|
||||
db.requestTransaction(function * () {
|
||||
yield * this.applyDeleteSet(m.deleteSet)
|
||||
})
|
||||
defer.resolve()
|
||||
})*/
|
||||
return defer.promise
|
||||
} else if (message.type === 'sync done') {
|
||||
var self = this
|
||||
this.connections[sender].syncStep2.promise.then(function () {
|
||||
self._setSyncedWith(sender)
|
||||
})
|
||||
} else if (message.type === 'update' && canWrite(auth)) {
|
||||
if (this.forwardToSyncingClients) {
|
||||
for (var client of this.syncingClients) {
|
||||
this.send(client, message)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
let messages = senderConn.processAfterAuth
|
||||
senderConn.processAfterAuth = []
|
||||
|
||||
return messages.reduce((p, m) =>
|
||||
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
|
||||
, Promise.resolve())
|
||||
})
|
||||
}
|
||||
}
|
||||
if (skipAuth || senderConn.auth != null) {
|
||||
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||
} else {
|
||||
return Promise.reject(new Error('Unable to deliver message'))
|
||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
|
||||
}
|
||||
}
|
||||
|
||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
|
||||
return this.y.db.whenTransactionsFinished()
|
||||
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
|
||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
||||
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
|
||||
} else {
|
||||
return Promise.reject(new Error('Unable to receive message'))
|
||||
}
|
||||
}
|
||||
|
||||
_setSyncedWith (user) {
|
||||
var conn = this.connections[user]
|
||||
if (conn != null) {
|
||||
conn.isSynced = true
|
||||
if (user != null) {
|
||||
this.connections.get(user).isSynced = true
|
||||
}
|
||||
if (user === this.currentSyncTarget) {
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
let conns = Array.from(this.connections.values())
|
||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||
this._fireIsSyncedListeners()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
/* 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
|
||||
}
|
||||
@@ -81,10 +81,6 @@ 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)) {
|
||||
// debug
|
||||
if (os.y.connector.isSynced === false) {
|
||||
debugger
|
||||
}
|
||||
if (!os.y.connector.isSynced) {
|
||||
console.warn('gc should be empty when not synced!')
|
||||
}
|
||||
@@ -124,7 +120,7 @@ export default function extendDatabase (Y /* :any */) {
|
||||
startGarbageCollector () {
|
||||
this.gc = this.dbOpts.gc
|
||||
if (this.gc) {
|
||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 50000 : this.dbOpts.gcTimeout
|
||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
|
||||
} else {
|
||||
this.gcTimeout = -1
|
||||
}
|
||||
@@ -310,10 +306,12 @@ 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
|
||||
*/
|
||||
apply (ops) {
|
||||
applyOperations (decoder) {
|
||||
this.opsReceivedTimestamp = new Date()
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
var o = ops[i]
|
||||
let length = decoder.readUint32()
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
let o = Y.Struct.binaryDecodeOperation(decoder)
|
||||
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
||||
var required = Y.Struct[o.struct].requiredOps(o)
|
||||
if (o.requires != null) {
|
||||
@@ -405,11 +403,11 @@ export default function extendDatabase (Y /* :any */) {
|
||||
whenOperationsExist: any;
|
||||
*/
|
||||
* tryExecute (op) {
|
||||
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||
this.store.addToDebug('yield * this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
||||
if (op.struct === 'Delete') {
|
||||
yield * Y.Struct.Delete.execute.call(this, op)
|
||||
// this is now called in Transaction.deleteOperation!
|
||||
// yield* this.store.operationAdded(this, op)
|
||||
// yield * this.store.operationAdded(this, op)
|
||||
} else {
|
||||
// check if this op was defined
|
||||
var defined = yield * this.getInsertion(op.id)
|
||||
@@ -590,11 +588,11 @@ 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)
|
||||
var op = Y.Struct[structname].create(id, typedefinition[1])
|
||||
op.type = typedefinition[0].name
|
||||
|
||||
this.requestTransaction(function * () {
|
||||
if (op.id[0] === '_') {
|
||||
if (op.id[0] === 0xFFFFFF) {
|
||||
yield * this.setOperation(op)
|
||||
} else {
|
||||
yield * this.applyCreatedOperations([op])
|
||||
|
||||
152
src/Encoding.js
Normal file
152
src/Encoding.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import utf8 from 'utf-8'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
export class BinaryEncoder {
|
||||
constructor () {
|
||||
this.data = []
|
||||
}
|
||||
|
||||
get pos () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
createBuffer () {
|
||||
return Uint8Array.from(this.data).buffer
|
||||
}
|
||||
|
||||
writeUint8 (num) {
|
||||
this.data.push(num & bits8)
|
||||
}
|
||||
|
||||
setUint8 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
}
|
||||
|
||||
writeUint16 (num) {
|
||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
setUint16 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
this.data[pos + 1] = (num >>> 8) & bits8
|
||||
}
|
||||
|
||||
writeUint32 (num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data.push(num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
setUint32 (pos, num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data[pos + i] = num & bits8
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
writeVarUint (num) {
|
||||
while (num >= 0b10000000) {
|
||||
this.data.push(0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
this.data.push(bits7 & num)
|
||||
}
|
||||
|
||||
writeVarString (str) {
|
||||
let bytes = utf8.setBytesFromString(str)
|
||||
let len = bytes.length
|
||||
this.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
this.data.push(bytes[i])
|
||||
}
|
||||
}
|
||||
|
||||
writeOpID (id) {
|
||||
let user = id[0]
|
||||
this.writeVarUint(user)
|
||||
if (user !== 0xFFFFFF) {
|
||||
this.writeVarUint(id[1])
|
||||
} else {
|
||||
this.writeVarString(id[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BinaryDecoder {
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.uint8arr = new Uint8Array(buffer)
|
||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
||||
this.uint8arr = buffer
|
||||
} else {
|
||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||
}
|
||||
this.pos = 0
|
||||
}
|
||||
|
||||
skip8 () {
|
||||
this.pos++
|
||||
}
|
||||
|
||||
readUint8 () {
|
||||
return this.uint8arr[this.pos++]
|
||||
}
|
||||
|
||||
readUint32 () {
|
||||
let uint =
|
||||
this.uint8arr[this.pos] +
|
||||
(this.uint8arr[this.pos + 1] << 8) +
|
||||
(this.uint8arr[this.pos + 2] << 16) +
|
||||
(this.uint8arr[this.pos + 3] << 24)
|
||||
this.pos += 4
|
||||
return uint
|
||||
}
|
||||
|
||||
peekUint8 () {
|
||||
return this.uint8arr[this.pos]
|
||||
}
|
||||
|
||||
readVarUint () {
|
||||
let num = 0
|
||||
let len = 0
|
||||
while (true) {
|
||||
let r = this.uint8arr[this.pos++]
|
||||
num = num | ((r & bits7) << len)
|
||||
len += 7
|
||||
if (r < 1 << 7) {
|
||||
return num >>> 0 // return unsigned number!
|
||||
}
|
||||
if (len > 35) {
|
||||
throw new Error('Integer out of range!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readVarString () {
|
||||
let len = this.readVarUint()
|
||||
let bytes = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = this.uint8arr[this.pos++]
|
||||
}
|
||||
return utf8.getStringFromBytes(bytes)
|
||||
}
|
||||
|
||||
peekVarString () {
|
||||
let pos = this.pos
|
||||
let s = this.readVarString()
|
||||
this.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
readOpID () {
|
||||
let user = this.readVarUint()
|
||||
if (user !== 0xFFFFFF) {
|
||||
return [user, this.readVarUint()]
|
||||
} else {
|
||||
return [user, this.readVarString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
193
src/MessageHandler.js
Normal file
193
src/MessageHandler.js
Normal file
@@ -0,0 +1,193 @@
|
||||
|
||||
import Y from './y.js'
|
||||
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
|
||||
|
||||
export function formatYjsMessage (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // read roomname
|
||||
let type = decoder.readVarString()
|
||||
let strBuilder = []
|
||||
strBuilder.push('\n === ' + type + ' ===\n')
|
||||
if (type === 'update') {
|
||||
logMessageUpdate(decoder, strBuilder)
|
||||
} else if (type === 'sync step 1') {
|
||||
logMessageSyncStep1(decoder, strBuilder)
|
||||
} else if (type === 'sync step 2') {
|
||||
logMessageSyncStep2(decoder, strBuilder)
|
||||
} else {
|
||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
||||
}
|
||||
return strBuilder.join('')
|
||||
}
|
||||
|
||||
export function formatYjsMessageType (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // roomname
|
||||
return decoder.readVarString()
|
||||
}
|
||||
|
||||
export function logMessageUpdate (decoder, strBuilder) {
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
export function computeMessageUpdate (decoder, encoder, conn) {
|
||||
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
|
||||
let messagePosition = decoder.pos
|
||||
let len = decoder.readUint32()
|
||||
let delops = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||
if (op.struct === 'Delete') {
|
||||
delops.push(op)
|
||||
}
|
||||
}
|
||||
if (delops.length > 0) {
|
||||
if (conn.y.db.forwardAppliedOperations) {
|
||||
conn.broadcastOps(delops)
|
||||
}
|
||||
if (conn.y.persistence) {
|
||||
conn.y.persistence.saveOperations(delops)
|
||||
}
|
||||
}
|
||||
decoder.pos = messagePosition
|
||||
}
|
||||
conn.y.db.applyOperations(decoder)
|
||||
}
|
||||
|
||||
export function sendSyncStep1 (conn, syncUser) {
|
||||
conn.y.db.requestTransaction(function * () {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(conn.opts.room || '')
|
||||
encoder.writeVarString('sync step 1')
|
||||
encoder.writeVarString(conn.authInfo || '')
|
||||
encoder.writeVarUint(conn.protocolVersion)
|
||||
let preferUntransformed = conn.preferUntransformed && this.os.length === 0 // TODO: length may not be defined
|
||||
encoder.writeUint8(preferUntransformed ? 1 : 0)
|
||||
yield * this.writeStateSet(encoder)
|
||||
conn.send(syncUser, encoder.createBuffer())
|
||||
})
|
||||
}
|
||||
|
||||
export function logMessageSyncStep1 (decoder, strBuilder) {
|
||||
let auth = decoder.readVarString()
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
let preferUntransformed = decoder.readUint8() === 1
|
||||
strBuilder.push(`
|
||||
- auth: "${auth}"
|
||||
- protocolVersion: ${protocolVersion}
|
||||
- preferUntransformed: ${preferUntransformed}
|
||||
`)
|
||||
logSS(decoder, strBuilder)
|
||||
}
|
||||
|
||||
export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
let preferUntransformed = decoder.readUint8() === 1
|
||||
|
||||
// check protocol version
|
||||
if (protocolVersion !== conn.protocolVersion) {
|
||||
console.warn(
|
||||
`You tried to sync with a yjs instance that has a different protocol version
|
||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
||||
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
||||
`)
|
||||
conn.y.destroy()
|
||||
}
|
||||
|
||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
||||
// send sync step 2
|
||||
conn.y.db.requestTransaction(function * () {
|
||||
encoder.writeVarString('sync step 2')
|
||||
encoder.writeVarString(conn.authInfo || '')
|
||||
|
||||
if (preferUntransformed) {
|
||||
encoder.writeUint8(1)
|
||||
yield * this.writeOperationsUntransformed(encoder)
|
||||
} else {
|
||||
encoder.writeUint8(0)
|
||||
yield * this.writeOperations(encoder, decoder)
|
||||
}
|
||||
|
||||
yield * this.writeDeleteSet(encoder)
|
||||
conn.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
})
|
||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
||||
if (conn.role === 'slave') {
|
||||
sendSyncStep1(conn, sender)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function logSS (decoder, strBuilder) {
|
||||
strBuilder.push(' == SS: \n')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
strBuilder.push(` ${user}: ${clock}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
export function logOS (decoder, strBuilder) {
|
||||
strBuilder.push(' == OS: \n')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||
strBuilder.push(JSON.stringify(op) + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
export function logDS (decoder, strBuilder) {
|
||||
strBuilder.push(' == DS: \n')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(` User: ${user}: `)
|
||||
let len2 = decoder.readVarUint()
|
||||
for (let j = 0; j < len2; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let to = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logMessageSyncStep2 (decoder, strBuilder) {
|
||||
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n')
|
||||
let osTransformed = decoder.readUint8() === 1
|
||||
strBuilder.push(' - osUntransformed: ' + osTransformed + '\n')
|
||||
logOS(decoder, strBuilder)
|
||||
if (osTransformed) {
|
||||
logSS(decoder, strBuilder)
|
||||
}
|
||||
logDS(decoder, strBuilder)
|
||||
}
|
||||
|
||||
export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
||||
var db = conn.y.db
|
||||
let defer = senderConn.syncStep2
|
||||
|
||||
// apply operations first
|
||||
db.requestTransaction(function * () {
|
||||
let osUntransformed = decoder.readUint8()
|
||||
if (osUntransformed === 1) {
|
||||
yield * this.applyOperationsUntransformed(decoder)
|
||||
} else {
|
||||
this.store.applyOperations(decoder)
|
||||
}
|
||||
})
|
||||
// then apply ds
|
||||
db.requestTransaction(function * () {
|
||||
yield * this.applyDeleteSet(decoder)
|
||||
})
|
||||
return db.whenTransactionsFinished().then(() => {
|
||||
conn._setSyncedWith(sender)
|
||||
defer.resolve()
|
||||
})
|
||||
}
|
||||
43
src/Persistence.js
Normal file
43
src/Persistence.js
Normal file
@@ -0,0 +1,43 @@
|
||||
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,404 +0,0 @@
|
||||
/* 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.YConcurrencyTestingMode = 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
|
||||
935
src/Struct.js
935
src/Struct.js
@@ -1,5 +1,8 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
const CDELETE = 0
|
||||
const CINSERT = 1
|
||||
const CLIST = 2
|
||||
const CMAP = 3
|
||||
const CXML = 4
|
||||
|
||||
/*
|
||||
An operation also defines the structure of a type. This is why operation and
|
||||
@@ -20,395 +23,597 @@
|
||||
- Operations that are required to execute this operation.
|
||||
*/
|
||||
export default function extendStruct (Y) {
|
||||
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
|
||||
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!')
|
||||
}
|
||||
*/
|
||||
Delete: {
|
||||
encode: function (op) {
|
||||
return {
|
||||
target: op.target,
|
||||
length: op.length || 0,
|
||||
struct: 'Delete'
|
||||
}
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function * (op) {
|
||||
return yield * this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
|
||||
/* 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'
|
||||
}
|
||||
},
|
||||
Insert: {
|
||||
/* {
|
||||
content: [any],
|
||||
opContent: Id,
|
||||
id: Id,
|
||||
left: Id,
|
||||
origin: Id,
|
||||
right: Id,
|
||||
parent: Id,
|
||||
parentSub: string (optional), // child of Map type
|
||||
}
|
||||
*/
|
||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||
// TODO: you could not send the "left" property, then you also have to
|
||||
// "op.left = null" in $execute or $decode
|
||||
var e/* :any */ = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
origin: op.origin,
|
||||
parent: op.parent,
|
||||
struct: op.struct
|
||||
}
|
||||
if (op.parentSub != null) {
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
if (op.hasOwnProperty('opContent')) {
|
||||
e.opContent = op.opContent
|
||||
} else {
|
||||
e.content = op.content.slice()
|
||||
}
|
||||
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 yield * this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
/* {
|
||||
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()
|
||||
}
|
||||
|
||||
if (op.opContent != null) {
|
||||
ids.push(op.opContent)
|
||||
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 = 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 ids
|
||||
},
|
||||
getDistanceToOrigin: function * (op) {
|
||||
if (op.left == null) {
|
||||
return 0
|
||||
} else {
|
||||
var d = 0
|
||||
var o = yield * this.getInsertion(op.left)
|
||||
while (!Y.utils.matchesId(o, op.origin)) {
|
||||
d++
|
||||
if (o.left == null) {
|
||||
break
|
||||
} else {
|
||||
o = yield * this.getInsertion(o.left)
|
||||
return d
|
||||
}
|
||||
},
|
||||
/*
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change
|
||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
*/
|
||||
execute: function * (op) {
|
||||
var i // loop counter
|
||||
|
||||
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
||||
// We try to merge them later, if possible
|
||||
var tryToRemergeLater = []
|
||||
|
||||
if (op.origin != null) { // TODO: !== instead of !=
|
||||
// we save in origin that op originates in it
|
||||
// we need that later when we eventually garbage collect origin (see transaction)
|
||||
var origin = yield * this.getInsertionCleanEnd(op.origin)
|
||||
if (origin.originOf == null) {
|
||||
origin.originOf = []
|
||||
}
|
||||
origin.originOf.push(op.id)
|
||||
yield * this.setOperation(origin)
|
||||
if (origin.right != null) {
|
||||
tryToRemergeLater.push(origin.right)
|
||||
}
|
||||
}
|
||||
var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
||||
|
||||
// now we begin to insert op in the list of insertions..
|
||||
var o
|
||||
var parent
|
||||
var start
|
||||
|
||||
// find o. o is the first conflicting operation
|
||||
if (op.left != null) {
|
||||
o = yield * this.getInsertionCleanEnd(op.left)
|
||||
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
||||
// only if not added previously
|
||||
tryToRemergeLater.push(o.right)
|
||||
}
|
||||
o = (o.right == null) ? null : yield * this.getOperation(o.right)
|
||||
} else { // left == null
|
||||
parent = yield * this.getOperation(op.parent)
|
||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||
start = startId == null ? null : yield * this.getOperation(startId)
|
||||
o = start
|
||||
}
|
||||
|
||||
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
||||
if (op.right != null) {
|
||||
tryToRemergeLater.push(op.right)
|
||||
yield * this.getInsertionCleanStart(op.right)
|
||||
}
|
||||
|
||||
// handle conflicts
|
||||
while (true) {
|
||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||
var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||
if (oOriginDistance === i) {
|
||||
// case 1
|
||||
if (o.id[0] < op.id[0]) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
}
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
i++
|
||||
if (o.right != null) {
|
||||
o = yield * this.getInsertion(o.right)
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
} 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)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
List: {
|
||||
/*
|
||||
{
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.List = {
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'List',
|
||||
id: id
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
if (op.info != null) {
|
||||
e.info = op.info
|
||||
}
|
||||
return e
|
||||
},
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
*/
|
||||
return []
|
||||
},
|
||||
execute: function * (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function * (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null
|
||||
}
|
||||
var res = null
|
||||
var o = yield * this.getOperation(op.start)
|
||||
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o
|
||||
pos--
|
||||
}
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = yield * this.getOperation(o.right)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
map: function * (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o != null) { // TODO: change to != (at least some convention)
|
||||
var operation = yield * this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation))
|
||||
}
|
||||
o = operation.right
|
||||
}
|
||||
return res
|
||||
struct: 'List',
|
||||
id: id
|
||||
}
|
||||
},
|
||||
Map: {
|
||||
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 () {
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
struct: "Map",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
struct: 'Map'
|
||||
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--
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Map',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {} // overwrite map!!
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = yield * this.getOperation(o.right)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
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))
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 = yield * this.getOperation(oid)
|
||||
if (res == null || res.deleted) {
|
||||
return void 0
|
||||
} else if (res.opContent == null) {
|
||||
return res.content[0]
|
||||
} else {
|
||||
return yield * this.getType(res.opContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Y.Struct = Struct
|
||||
|
||||
/*
|
||||
{
|
||||
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: {},
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'Xml',
|
||||
nodeName
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Xml',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {},
|
||||
nodeName: op.nodeName
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||
|
||||
/*
|
||||
Partial definition of a transaction
|
||||
@@ -96,9 +95,12 @@ export default function extendTransaction (Y) {
|
||||
send.push(Y.Struct[op.struct].encode(op))
|
||||
}
|
||||
}
|
||||
if (this.store.y.connector.isSynced && send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
||||
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
||||
// is connected, and this is not going to be send in addOperation
|
||||
this.store.y.connector.broadcastOps(send)
|
||||
if (this.store.y.persistence != null) {
|
||||
this.store.y.persistence.saveOperations(send)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,14 +164,14 @@ export default function extendTransaction (Y) {
|
||||
if (target.start != null) {
|
||||
// TODO: don't do it like this .. -.-
|
||||
yield * this.deleteList(target.start)
|
||||
// yield* this.deleteList(target.id) -- do not gc itself because this may still get referenced
|
||||
// yield * this.deleteList(target.id) -- do not gc itself because this may still get referenced
|
||||
}
|
||||
if (target.map != null) {
|
||||
for (var name in target.map) {
|
||||
yield * this.deleteList(target.map[name])
|
||||
}
|
||||
// TODO: here to.. (see above)
|
||||
// yield* this.deleteList(target.id) -- see above
|
||||
// yield * this.deleteList(target.id) -- see above
|
||||
}
|
||||
if (target.opContent != null) {
|
||||
yield * this.deleteOperation(target.opContent)
|
||||
@@ -223,7 +225,7 @@ export default function extendTransaction (Y) {
|
||||
*/
|
||||
* markGarbageCollected (id, len) {
|
||||
// this.mem.push(["gc", id]);
|
||||
this.store.addToDebug('yield* this.markGarbageCollected(', id, ', ', len, ')')
|
||||
this.store.addToDebug('yield * this.markGarbageCollected(', id, ', ', len, ')')
|
||||
var n = yield * this.markDeleted(id, len)
|
||||
if (n.id[1] < id[1] && !n.gc) {
|
||||
// un-extend left
|
||||
@@ -294,8 +296,9 @@ export default function extendTransaction (Y) {
|
||||
yield * this.ds.put(n)
|
||||
} else {
|
||||
// already gc'd
|
||||
throw new Error('Cannot happen! (it dit though.. :()')
|
||||
// return n
|
||||
throw new Error(
|
||||
'DS reached an inconsistent state. Please report this issue!'
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -366,6 +369,7 @@ export default function extendTransaction (Y) {
|
||||
operations that can be gc'd and add them to the garbage collector.
|
||||
*/
|
||||
* garbageCollectAfterSync () {
|
||||
// debugger
|
||||
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
|
||||
console.warn('gc should be empty after sync')
|
||||
}
|
||||
@@ -417,7 +421,7 @@ export default function extendTransaction (Y) {
|
||||
* reset origins of all right ops
|
||||
*/
|
||||
* garbageCollectOperation (id) {
|
||||
this.store.addToDebug('yield* this.garbageCollectOperation(', id, ')')
|
||||
this.store.addToDebug('yield * this.garbageCollectOperation(', id, ')')
|
||||
var o = yield * this.getOperation(id)
|
||||
yield * this.markGarbageCollected(id, (o != null && o.content != null) ? o.content.length : 1) // always mark gc'd
|
||||
// if op exists, then clean that mess up..
|
||||
@@ -459,16 +463,8 @@ export default function extendTransaction (Y) {
|
||||
|
||||
if (o.originOf != null && o.originOf.length > 0) {
|
||||
// find new origin of right ops
|
||||
// origin is the first left deleted operation
|
||||
// origin is the first left operation
|
||||
var neworigin = o.left
|
||||
var neworigin_ = null
|
||||
while (neworigin != null) {
|
||||
neworigin_ = yield * this.getInsertion(neworigin)
|
||||
if (neworigin_.deleted) {
|
||||
break
|
||||
}
|
||||
neworigin = neworigin_.left
|
||||
}
|
||||
|
||||
// reset origin of all right ops (except first right - duh!),
|
||||
|
||||
@@ -482,7 +478,7 @@ export default function extendTransaction (Y) {
|
||||
right.origin = neworigin
|
||||
// search until you find origin pointer to the left of o
|
||||
if (right.right != null) {
|
||||
var i = yield* this.getOperation(right.right)
|
||||
var i = yield * this.getOperation(right.right)
|
||||
var ids = [o.id, o.right]
|
||||
while (ids.some(function (id) {
|
||||
return Y.utils.compareIds(id, i.origin)
|
||||
@@ -490,14 +486,14 @@ export default function extendTransaction (Y) {
|
||||
if (Y.utils.compareIds(i.origin, o.id)) {
|
||||
// reset origin of i
|
||||
i.origin = neworigin
|
||||
yield* this.setOperation(i)
|
||||
yield * this.setOperation(i)
|
||||
}
|
||||
// get next i
|
||||
if (i.right == null) {
|
||||
break
|
||||
} else {
|
||||
ids.push(i.id)
|
||||
i = yield* this.getOperation(i.right)
|
||||
i = yield * this.getOperation(i.right)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,6 +509,7 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
}
|
||||
if (neworigin != null) {
|
||||
var neworigin_ = yield * this.getInsertion(neworigin)
|
||||
if (neworigin_.originOf == null) {
|
||||
neworigin_.originOf = o.originOf
|
||||
} else {
|
||||
@@ -593,11 +590,20 @@ export default function extendTransaction (Y) {
|
||||
apply a delete set in order to get
|
||||
the state of the supplied ds
|
||||
*/
|
||||
* applyDeleteSet (ds) {
|
||||
* applyDeleteSet (decoder) {
|
||||
var deletions = []
|
||||
|
||||
for (var user in ds) {
|
||||
var dv = ds[user]
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let dv = []
|
||||
let dvLength = decoder.readVarUint()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
dv.push([from, len, gc])
|
||||
}
|
||||
var pos = 0
|
||||
var d = dv[pos]
|
||||
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
|
||||
@@ -677,10 +683,15 @@ export default function extendTransaction (Y) {
|
||||
yield * this.garbageCollectOperation(o.id)
|
||||
}
|
||||
}
|
||||
if (this.store.forwardAppliedOperations) {
|
||||
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
|
||||
var ops = []
|
||||
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
||||
this.store.y.connector.broadcastOps(ops)
|
||||
if (this.store.forwardAppliedOperations) {
|
||||
this.store.y.connector.broadcastOps(ops)
|
||||
}
|
||||
if (this.store.y.persistence != null) {
|
||||
this.store.y.persistence.saveOperations(ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,21 +702,34 @@ export default function extendTransaction (Y) {
|
||||
/*
|
||||
A DeleteSet (ds) describes all the deleted ops in the OS
|
||||
*/
|
||||
* getDeleteSet () {
|
||||
var ds = {}
|
||||
* writeDeleteSet (encoder) {
|
||||
var ds = new Map()
|
||||
yield * this.ds.iterate(this, null, null, function * (n) {
|
||||
var user = n.id[0]
|
||||
var counter = n.id[1]
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
var dv = ds[user]
|
||||
var dv = ds.get(user)
|
||||
if (dv === void 0) {
|
||||
dv = []
|
||||
ds[user] = dv
|
||||
ds.set(user, dv)
|
||||
}
|
||||
dv.push([counter, len, gc])
|
||||
})
|
||||
return ds
|
||||
let keys = Array.from(ds.keys())
|
||||
encoder.writeUint32(keys.length)
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
let user = keys[i]
|
||||
let deletions = ds.get(user)
|
||||
encoder.writeVarUint(user)
|
||||
encoder.writeVarUint(deletions.length)
|
||||
for (var j = 0; j < deletions.length; j++) {
|
||||
let del = deletions[j]
|
||||
encoder.writeVarUint(del[0])
|
||||
encoder.writeVarUint(del[1])
|
||||
encoder.writeUint8(del[2] ? 1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
* isDeleted (id) {
|
||||
var n = yield * this.ds.findWithUpperBound(id)
|
||||
@@ -717,9 +741,15 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
* addOperation (op) {
|
||||
yield * this.os.put(op)
|
||||
if (this.store.y.connector.isSynced && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
||||
// is connected, and this is not going to be send in addOperation
|
||||
this.store.y.connector.broadcastOps([op])
|
||||
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
||||
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
|
||||
if (this.store.forwardAppliedOperations) {
|
||||
// is connected, and this is not going to be send in addOperation
|
||||
this.store.y.connector.broadcastOps([op])
|
||||
}
|
||||
if (this.store.y.persistence != null) {
|
||||
this.store.y.persistence.saveOperations([op])
|
||||
}
|
||||
}
|
||||
}
|
||||
// if insertion, try to combine with left insertion (if both have content property)
|
||||
@@ -826,22 +856,27 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
* getOperation (id/* :any */)/* :Transaction<any> */ {
|
||||
var o = yield * this.os.find(id)
|
||||
if (id[0] !== '_' || o != null) {
|
||||
if (id[0] !== 0xFFFFFF || o != null) {
|
||||
return o
|
||||
} else { // type is string
|
||||
// generate this operation?
|
||||
var comp = id[1].split('_')
|
||||
if (comp.length > 1) {
|
||||
var struct = comp[0]
|
||||
var op = Y.Struct[struct].create(id)
|
||||
let type = Y[comp[1]]
|
||||
let args = null
|
||||
if (type != null) {
|
||||
args = Y.utils.parseTypeDefinition(type, comp[3])
|
||||
}
|
||||
var op = Y.Struct[struct].create(id, args)
|
||||
op.type = comp[1]
|
||||
yield * this.setOperation(op)
|
||||
return op
|
||||
} else {
|
||||
// won't be called. but just in case..
|
||||
console.error('Unexpected case. How can this happen?')
|
||||
debugger // eslint-disable-line
|
||||
return null
|
||||
throw new Error(
|
||||
'Unexpected case. Operation cannot be generated correctly!' +
|
||||
'Incompatible Yjs version?'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -883,6 +918,18 @@ export default function extendTransaction (Y) {
|
||||
})
|
||||
return ss
|
||||
}
|
||||
* writeStateSet (encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0)
|
||||
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||
encoder.writeVarUint(n.id[0])
|
||||
encoder.writeVarUint(n.clock)
|
||||
len++
|
||||
})
|
||||
encoder.setUint32(lenPosition, len)
|
||||
return len === 0
|
||||
}
|
||||
/*
|
||||
Here, we make all missing operations executable for the receiving user.
|
||||
|
||||
@@ -932,34 +979,43 @@ export default function extendTransaction (Y) {
|
||||
* getOperations (startSS) {
|
||||
// TODO: use bounds here!
|
||||
if (startSS == null) {
|
||||
startSS = {}
|
||||
startSS = new Map()
|
||||
}
|
||||
var send = []
|
||||
|
||||
var endSV = yield * this.getStateVector()
|
||||
for (var endState of endSV) {
|
||||
var user = endState.user
|
||||
if (user === '_') {
|
||||
for (let endState of endSV) {
|
||||
let user = endState.user
|
||||
if (user === 0xFFFFFF) {
|
||||
continue
|
||||
}
|
||||
var startPos = startSS[user] || 0
|
||||
let startPos = startSS.get(user) || 0
|
||||
if (startPos > 0) {
|
||||
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
|
||||
// find out if that is the case
|
||||
var firstMissing = yield * this.getInsertion([user, startPos])
|
||||
let firstMissing = yield * this.getInsertion([user, startPos])
|
||||
if (firstMissing != null) {
|
||||
// update startPos
|
||||
startPos = firstMissing.id[1]
|
||||
startSS[user] = startPos
|
||||
}
|
||||
}
|
||||
startSS.set(user, startPos)
|
||||
}
|
||||
for (let endState of endSV) {
|
||||
let user = endState.user
|
||||
let startPos = startSS.get(user)
|
||||
if (user === 0xFFFFFF) {
|
||||
continue
|
||||
}
|
||||
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
||||
op = Y.Struct[op.struct].encode(op)
|
||||
if (op.struct !== 'Insert') {
|
||||
send.push(op)
|
||||
} else if (op.right == null || op.right[1] < (startSS[op.right[0]] || 0)) {
|
||||
} else if (op.right == null || op.right[1] < (startSS.get(op.right[0]) || 0)) {
|
||||
// case 1. op.right is known
|
||||
var o = op
|
||||
// this case is only reached if op.right is known.
|
||||
// => this is not called for op.left, as op.right is unknown
|
||||
let o = op
|
||||
// Remember: ?
|
||||
// -> set op.right
|
||||
// 1. to the first operation that is known (according to startSS)
|
||||
@@ -972,11 +1028,14 @@ export default function extendTransaction (Y) {
|
||||
if (o.left == null) {
|
||||
op.left = null
|
||||
send.push(op)
|
||||
if (!Y.utils.compareIds(o.id, op.id)) {
|
||||
/* not necessary, as o is already sent..
|
||||
if (!Y.utils.compareIds(o.id, op.id) && o.id[1] >= (startSS.get(o.id[0]) || 0)) {
|
||||
// o is not op && o is unknown
|
||||
o = Y.Struct[op.struct].encode(o)
|
||||
o.right = missingOrigins[missingOrigins.length - 1].id
|
||||
send.push(o)
|
||||
}
|
||||
*/
|
||||
break
|
||||
}
|
||||
o = yield * this.getInsertion(o.left)
|
||||
@@ -984,7 +1043,7 @@ export default function extendTransaction (Y) {
|
||||
while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
|
||||
missingOrigins.pop()
|
||||
}
|
||||
if (o.id[1] < (startSS[o.id[0]] || 0)) {
|
||||
if (o.id[1] < (startSS.get(o.id[0]) || 0)) {
|
||||
// case 2. o is known
|
||||
op.left = Y.utils.getLastId(o)
|
||||
send.push(op)
|
||||
@@ -996,8 +1055,10 @@ export default function extendTransaction (Y) {
|
||||
op = Y.Struct[op.struct].encode(o)
|
||||
op.right = newright
|
||||
if (missingOrigins.length > 0) {
|
||||
debugger
|
||||
console.log('This should not happen .. :( please report this')
|
||||
throw new Error(
|
||||
'Reached inconsistent OS state.' +
|
||||
'Operations are not correctly connected.'
|
||||
)
|
||||
}
|
||||
missingOrigins = [op]
|
||||
} else {
|
||||
@@ -1014,28 +1075,62 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
return send.reverse()
|
||||
}
|
||||
|
||||
* writeOperations (encoder, decoder) {
|
||||
let ss = new Map()
|
||||
let ssLength = decoder.readUint32()
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ss.set(user, clock)
|
||||
}
|
||||
let ops = yield * this.getOperations(ss)
|
||||
encoder.writeUint32(ops.length)
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
||||
}
|
||||
}
|
||||
|
||||
* toBinary () {
|
||||
let encoder = new BinaryEncoder()
|
||||
yield * this.writeOperationsUntransformed(encoder)
|
||||
yield * this.writeDeleteSet(encoder)
|
||||
return encoder.createBuffer()
|
||||
}
|
||||
|
||||
* fromBinary (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
yield * this.applyOperationsUntransformed(decoder)
|
||||
yield * this.applyDeleteSet(decoder)
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the plain untransformed operations from the database.
|
||||
* You can apply these operations using .applyOperationsUntransformed(ops)
|
||||
*
|
||||
*/
|
||||
* getOperationsUntransformed () {
|
||||
var ops = []
|
||||
* writeOperationsUntransformed (encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0) // placeholder
|
||||
yield * this.os.iterate(this, null, null, function * (op) {
|
||||
if (op.id[0] !== '_') {
|
||||
ops.push(op)
|
||||
if (op.id[0] !== 0xFFFFFF) {
|
||||
len++
|
||||
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
||||
}
|
||||
})
|
||||
return {
|
||||
untransformed: ops
|
||||
}
|
||||
encoder.setUint32(lenPosition, len)
|
||||
yield * this.writeStateSet(encoder)
|
||||
}
|
||||
* applyOperationsUntransformed (m, stateSet) {
|
||||
var ops = m.untransformed
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
var op = ops[i]
|
||||
// create, and modify parent, if it is created implicitly
|
||||
if (op.parent != null && op.parent[0] === '_') {
|
||||
* applyOperationsUntransformed (decoder) {
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||
yield * this.os.put(op)
|
||||
}
|
||||
yield * this.os.iterate(this, null, null, function * (op) {
|
||||
if (op.parent != null) {
|
||||
if (op.struct === 'Insert') {
|
||||
// update parents .map/start/end properties
|
||||
if (op.parentSub != null && op.left == null) {
|
||||
@@ -1055,12 +1150,14 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
}
|
||||
}
|
||||
yield * this.os.put(op)
|
||||
}
|
||||
for (var user in stateSet) {
|
||||
})
|
||||
let stateSetLength = decoder.readUint32()
|
||||
for (let i = 0; i < stateSetLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
yield * this.ss.put({
|
||||
id: [user],
|
||||
clock: stateSet[user]
|
||||
clock: clock
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1075,7 +1172,7 @@ export default function extendTransaction (Y) {
|
||||
// or the o that has no origin to the right of op
|
||||
// (this is why we use the ids array)
|
||||
while (o.right != null) {
|
||||
var right = yield* this.getOperation(o.right)
|
||||
var right = yield * this.getOperation(o.right)
|
||||
if (o.right[1] < (startSS[o.right[0]] || 0) || !ids.some(function (id) {
|
||||
return Y.utils.compareIds(id, right.origin)
|
||||
})) {
|
||||
|
||||
75
src/Utils.js
75
src/Utils.js
@@ -1,3 +1,7 @@
|
||||
/* globals crypto */
|
||||
|
||||
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
|
||||
|
||||
/*
|
||||
EventHandler is an helper class for constructing custom types.
|
||||
|
||||
@@ -22,7 +26,10 @@
|
||||
*/
|
||||
|
||||
export default function Utils (Y) {
|
||||
Y.utils = {}
|
||||
Y.utils = {
|
||||
BinaryDecoder: BinaryDecoder,
|
||||
BinaryEncoder: BinaryEncoder
|
||||
}
|
||||
|
||||
Y.utils.bubbleEvent = function (type, event) {
|
||||
type.eventHandler.callEventListeners(event)
|
||||
@@ -42,6 +49,32 @@ 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) {
|
||||
(this._eventListener[name] || []).forEach(l => l(value))
|
||||
}
|
||||
destroy () {
|
||||
this._eventListener = null
|
||||
}
|
||||
}
|
||||
Y.utils.NamedEventHandler = NamedEventHandler
|
||||
|
||||
class EventListenerHandler {
|
||||
constructor () {
|
||||
this.eventListeners = []
|
||||
@@ -72,7 +105,12 @@ export default function Utils (Y) {
|
||||
}
|
||||
this.eventListeners[i](_event)
|
||||
} catch (e) {
|
||||
console.error('Your observer threw an error. This error was caught so that Yjs still can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions"', e)
|
||||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,6 +648,7 @@ export default function Utils (Y) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Y.utils.matchesId = matchesId
|
||||
|
||||
@@ -786,8 +825,32 @@ export default function Utils (Y) {
|
||||
}
|
||||
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
||||
|
||||
// Generates a unique id, for use as a user id.
|
||||
// Thx to @jed for this script https://gist.github.com/jed/982883
|
||||
function generateGuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,generateGuid)} // eslint-disable-line
|
||||
Y.utils.generateGuid = generateGuid
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
60
src/y.js
60
src/y.js
@@ -1,17 +1,22 @@
|
||||
import debug from 'debug'
|
||||
import extendConnector from './Connector.js'
|
||||
import extendPersistence from './Persistence.js'
|
||||
import extendDatabase from './Database.js'
|
||||
import extendTransaction from './Transaction.js'
|
||||
import extendStruct from './Struct.js'
|
||||
import extendUtils from './Utils.js'
|
||||
import debug from 'debug'
|
||||
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
|
||||
|
||||
extendConnector(Y)
|
||||
extendPersistence(Y)
|
||||
extendDatabase(Y)
|
||||
extendTransaction(Y)
|
||||
extendStruct(Y)
|
||||
extendUtils(Y)
|
||||
|
||||
Y.debug = debug
|
||||
debug.formatters.Y = formatYjsMessage
|
||||
debug.formatters.y = formatYjsMessageType
|
||||
|
||||
var requiringModules = {}
|
||||
|
||||
@@ -134,17 +139,27 @@ export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||
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)
|
||||
}
|
||||
yconfig.db.whenUserIdSet(function () {
|
||||
yconfig.init(function () {
|
||||
resolved = true
|
||||
resolve(yconfig)
|
||||
})
|
||||
}, reject)
|
||||
})
|
||||
}).catch(reject)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
class YConfig {
|
||||
class YConfig extends Y.utils.NamedEventHandler {
|
||||
/* ::
|
||||
db: Y.AbstractDatabase;
|
||||
connector: Y.AbstractConnector;
|
||||
@@ -152,9 +167,15 @@ class YConfig {
|
||||
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) {
|
||||
@@ -165,28 +186,26 @@ class YConfig {
|
||||
// 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 = ['_', 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]
|
||||
}
|
||||
}
|
||||
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
|
||||
let args = Y.utils.parseTypeDefinition(type, typeArgs)
|
||||
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
||||
}
|
||||
this.store.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
})
|
||||
if (this.persistence != null) {
|
||||
this.persistence.retrieveContent()
|
||||
.then(() => this.db.whenTransactionsFinished())
|
||||
.then(callback)
|
||||
} else {
|
||||
this.db.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
}
|
||||
}
|
||||
isConnected () {
|
||||
return this.connector.isSynced
|
||||
@@ -215,6 +234,9 @@ class YConfig {
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}).then(() => {
|
||||
// remove existing event listener
|
||||
super.destroy()
|
||||
})
|
||||
}
|
||||
close () {
|
||||
|
||||
178
test/encode-decode.js
Normal file
178
test/encode-decode.js
Normal file
@@ -0,0 +1,178 @@
|
||||
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'
|
||||
})
|
||||
})
|
||||
|
||||
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: {}
|
||||
})
|
||||
})
|
||||
374
test/y-array.tests.js
Normal file
374
test/y-array.tests.js
Normal file
@@ -0,0 +1,374 @@
|
||||
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)
|
||||
})
|
||||
*/
|
||||
371
test/y-map.tests.js
Normal file
371
test/y-map.tests.js
Normal file
@@ -0,0 +1,371 @@
|
||||
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)
|
||||
})
|
||||
*/
|
||||
288
test/y-xml.tests.js
Normal file
288
test/y-xml.tests.js
Normal file
@@ -0,0 +1,288 @@
|
||||
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)
|
||||
})
|
||||
|
||||
// TODO: move elements
|
||||
var xmlTransactions = [
|
||||
function attributeChange (t, user, chance) {
|
||||
user.share.xml.getDom().setAttribute(chance.word(), 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 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 randomXml10 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 10)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (42)', async function randomXml42 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 42)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (43)', async function randomXml43 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 43)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (44)', async function randomXml44 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 44)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (45)', async function randomXml45 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 45)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (46)', async function randomXml46 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 46)
|
||||
})
|
||||
|
||||
test('y-xml: Random tests (47)', async function randomXml47 (t) {
|
||||
await applyRandomTests(t, xmlTransactions, 47)
|
||||
})
|
||||
@@ -1,22 +1,79 @@
|
||||
|
||||
import _Y from '../../yjs/src/y.js'
|
||||
|
||||
import yMemory from '../../y-memory/src/Memory.js'
|
||||
import yMemory from '../../y-memory/src/y-memory.js'
|
||||
import yArray from '../../y-array/src/y-array.js'
|
||||
import yText from '../../y-text/src/Text.js'
|
||||
import yMap from '../../y-map/src/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(yMemory, yArray, yMap, yTest)
|
||||
Y.extend(yMemory, yArray, yText, yMap, yTest, yXml)
|
||||
|
||||
export var database = { name: 'memory' }
|
||||
export var connector = { name: 'test', url: 'http://localhost:1234' }
|
||||
|
||||
function * getStateSet () {
|
||||
var ss = {}
|
||||
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||
var user = n.id[0]
|
||||
var clock = n.clock
|
||||
ss[user] = clock
|
||||
})
|
||||
return ss
|
||||
}
|
||||
|
||||
function * getDeleteSet () {
|
||||
var ds = {}
|
||||
yield * this.ds.iterate(this, null, null, function * (n) {
|
||||
var user = n.id[0]
|
||||
var counter = n.id[1]
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
var dv = ds[user]
|
||||
if (dv === void 0) {
|
||||
dv = []
|
||||
ds[user] = dv
|
||||
}
|
||||
dv.push([counter, len, gc])
|
||||
})
|
||||
return ds
|
||||
}
|
||||
|
||||
export async function garbageCollectUsers (t, users) {
|
||||
await flushAll(t, users)
|
||||
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
|
||||
}
|
||||
|
||||
export function attrsToObject (attrs) {
|
||||
let obj = {}
|
||||
for (var i = 0; i < attrs.length; i++) {
|
||||
let attr = attrs[i]
|
||||
obj[attr.name] = attr.value
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function domToJson (dom) {
|
||||
if (dom.nodeType === document.TEXT_NODE) {
|
||||
return dom.textContent
|
||||
} else if (dom.nodeType === document.ELEMENT_NODE) {
|
||||
let attributes = attrsToObject(dom.attributes)
|
||||
let children = Array.from(dom.childNodes.values()).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
|
||||
@@ -30,12 +87,24 @@ export async function compareUsers (t, users) {
|
||||
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()
|
||||
|
||||
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
|
||||
|
||||
// disconnect all except user 0
|
||||
await Promise.all(users.slice(1).map(async u =>
|
||||
u.disconnect()
|
||||
@@ -54,27 +123,45 @@ export async function compareUsers (t, users) {
|
||||
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 * () {
|
||||
var os = yield * this.getOperationsUntransformed()
|
||||
let ops = []
|
||||
yield * this.os.iterate(this, null, null, function * (op) {
|
||||
ops.push(Y.Struct[op.struct].encode(op))
|
||||
})
|
||||
|
||||
data.os = {}
|
||||
os.untransformed.forEach((op) => {
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
let op = ops[i]
|
||||
op = Y.Struct[op.struct].encode(op)
|
||||
delete op.origin
|
||||
data.os[JSON.stringify(op.id)] = op
|
||||
return op
|
||||
})
|
||||
data.ds = yield * this.getDeleteSet()
|
||||
data.ss = yield * this.getStateSet()
|
||||
/*
|
||||
If gc = false, it is necessary to filter deleted ops
|
||||
as they might have been split up differently..
|
||||
*/
|
||||
if (filterDeletedOps) {
|
||||
let opIsDeleted = yield * this.isDeleted(op.id)
|
||||
if (!opIsDeleted) {
|
||||
data.os[JSON.stringify(op.id)] = op
|
||||
}
|
||||
} else {
|
||||
data.os[JSON.stringify(op.id)] = op
|
||||
}
|
||||
}
|
||||
data.ds = yield * getDeleteSet.apply(this)
|
||||
data.ss = yield * getStateSet.apply(this)
|
||||
})
|
||||
await u.db.whenTransactionsFinished()
|
||||
return data
|
||||
}))
|
||||
for (var i = 0; i < data.length - 1; i++) {
|
||||
await t.asyncGroup(async () => {
|
||||
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
|
||||
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')
|
||||
@@ -89,19 +176,19 @@ export async function initArrays (t, opts) {
|
||||
var result = {
|
||||
users: []
|
||||
}
|
||||
var share = Object.assign({ flushHelper: 'Map', array: 'Array' }, opts.share)
|
||||
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 connector = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, opts.connector)
|
||||
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: true }, opts.db)
|
||||
connOpts = Object.assign({ role: 'master' }, connector)
|
||||
dbOpts = Object.assign({ gc: false }, database)
|
||||
connOpts = Object.assign({ role: 'master' }, conn)
|
||||
} else {
|
||||
dbOpts = Object.assign({ gc: false }, opts.db)
|
||||
connOpts = Object.assign({ role: 'slave' }, connector)
|
||||
dbOpts = Object.assign({ gc: false }, database)
|
||||
connOpts = Object.assign({ role: 'slave' }, conn)
|
||||
}
|
||||
let y = await Y({
|
||||
connector: connOpts,
|
||||
@@ -176,3 +263,48 @@ export function wait (t) {
|
||||
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,51 +1,53 @@
|
||||
/* 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 = {}
|
||||
this.users = new Map()
|
||||
this.nextUserId = 0
|
||||
}
|
||||
join (connector) {
|
||||
if (connector.userId == null) {
|
||||
connector.setUserId('' + (this.nextUserId++))
|
||||
connector.setUserId(this.nextUserId++)
|
||||
}
|
||||
Object.keys(this.users).forEach(uid => {
|
||||
this.users[uid].userJoined(connector.userId, connector.role)
|
||||
connector.userJoined(uid, this.users[uid].role)
|
||||
this.users.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[connector.userId] = connector
|
||||
this.users.set(connector.userId, connector)
|
||||
}
|
||||
leave (connector) {
|
||||
delete this.users[connector.userId]
|
||||
Object.keys(this.users).forEach(uid => {
|
||||
this.users[uid].userLeft(connector.userId)
|
||||
this.users.delete(connector.userId)
|
||||
this.users.forEach(user => {
|
||||
user.userLeft(connector.userId)
|
||||
})
|
||||
}
|
||||
send (sender, receiver, m) {
|
||||
m = JSON.parse(JSON.stringify(m))
|
||||
var user = this.users[receiver]
|
||||
var user = this.users.get(receiver)
|
||||
if (user != null) {
|
||||
user.receiveMessage(sender, m)
|
||||
}
|
||||
}
|
||||
broadcast (sender, m) {
|
||||
Object.keys(this.users).forEach(receiver => {
|
||||
this.users.forEach((user, receiver) => {
|
||||
this.send(sender, receiver, m)
|
||||
})
|
||||
}
|
||||
async flushAll (users) {
|
||||
let flushing = true
|
||||
let allUserIds = Object.keys(this.users)
|
||||
let allUserIds = Array.from(this.users.keys())
|
||||
if (users == null) {
|
||||
users = allUserIds.map(id => this.users[id].y)
|
||||
users = allUserIds.map(id => this.users.get(id).y)
|
||||
}
|
||||
while (flushing) {
|
||||
await wait(10)
|
||||
let res = await Promise.all(allUserIds.map(id => this.users[id]._flushAll(users)))
|
||||
let res = await Promise.all(allUserIds.map(id => this.users.get(id)._flushAll(users)))
|
||||
flushing = res.some(status => status === 'flushing')
|
||||
}
|
||||
}
|
||||
@@ -67,6 +69,7 @@ export default function extendTestConnector (Y) {
|
||||
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
|
||||
@@ -78,14 +81,25 @@ export default function extendTestConnector (Y) {
|
||||
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) {
|
||||
@@ -105,10 +119,10 @@ export default function extendTestConnector (Y) {
|
||||
})
|
||||
}
|
||||
receiveMessage (sender, m) {
|
||||
if (this.userId !== sender && this.connections[sender] != null) {
|
||||
var buffer = this.connections[sender].buffer
|
||||
if (this.userId !== sender && this.connections.has(sender)) {
|
||||
var buffer = this.connections.get(sender).buffer
|
||||
if (buffer == null) {
|
||||
buffer = this.connections[sender].buffer = []
|
||||
buffer = this.connections.get(sender).buffer = []
|
||||
}
|
||||
buffer.push(m)
|
||||
if (this.chance.bool({likelihood: 30})) {
|
||||
@@ -123,18 +137,19 @@ export default function extendTestConnector (Y) {
|
||||
async _flushAll (flushUsers) {
|
||||
if (flushUsers.some(u => u.connector.userId === this.userId)) {
|
||||
// this one needs to sync with every other user
|
||||
flushUsers = Object.keys(this.connections).map(id => this.testRoom.users[id].y)
|
||||
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) continue
|
||||
let buffer = this.connections[userId].buffer
|
||||
if (buffer != null) {
|
||||
var messages = buffer.splice(0)
|
||||
for (let j = 0; j < messages.length; j++) {
|
||||
let p = super.receiveMessage(userId, messages[j])
|
||||
finished.push(p)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user