Compare commits
52 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 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,15 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
bower_components
|
bower_components
|
||||||
build
|
/y.*
|
||||||
build_test
|
/examples/yjs-dist.js*
|
||||||
.directory
|
|
||||||
.codio
|
|
||||||
.settings
|
|
||||||
.jshintignore
|
|
||||||
.jshintrc
|
|
||||||
.validate.json
|
|
||||||
/y.js
|
|
||||||
/y.js.map
|
|
||||||
/y-*
|
|
||||||
.vscode
|
|
||||||
jsconfig.json
|
|
||||||
|
|||||||
@@ -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|
|
|[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 |
|
|[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))|
|
|[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|
|
|[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
|
##### Database adapters
|
||||||
|
|||||||
@@ -9,16 +9,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"ignore": [],
|
"ignore": [],
|
||||||
"dependencies": {
|
"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",
|
"quill": "^1.0.0-rc.2",
|
||||||
"ace": "~1.2.3",
|
"ace": "~1.2.3",
|
||||||
"ace-builds": "~1.2.3",
|
"ace-builds": "~1.2.3",
|
||||||
|
|||||||
@@ -12,7 +12,12 @@
|
|||||||
<input name="message" type="text" style="width:60%;">
|
<input name="message" type="text" style="width:60%;">
|
||||||
<input type="submit" value="Send">
|
<input type="submit" value="Send">
|
||||||
</form>
|
</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>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,7 +12,11 @@
|
|||||||
</style>
|
</style>
|
||||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
<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="../bower_components/d3/d3.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ Y({
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'drawing-example'
|
room: 'drawing-example',
|
||||||
// url: 'localhost:1234'
|
url: 'localhost:1234'
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
sourceDir: '/bower_components',
|
||||||
share: {
|
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"/>
|
<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>
|
</g>
|
||||||
</svg>
|
</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="../bower_components/d3/d3.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Y({
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'Puzzle-example'
|
room: 'Puzzle-example',
|
||||||
|
url: 'http://localhost:1234'
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
share: {
|
||||||
piece1: 'Map',
|
piece1: 'Map',
|
||||||
piece2: 'Map',
|
piece2: 'Map',
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"name": "examples",
|
"name": "examples",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"dist": "rollup -c",
|
||||||
|
"watch": "rollup -cw"
|
||||||
|
},
|
||||||
"author": "Kevin Jahns",
|
"author": "Kevin Jahns",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
<!-- 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 rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
<link href="https://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="https://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/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
#quill-container {
|
#quill-container {
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
@@ -19,13 +19,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.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="//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/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></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)
|
<!-- 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/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>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ Y({
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'richtext-example-quill-1.0-test'
|
room: 'richtext-example-quill-1.0-test',
|
||||||
|
url: 'http://localhost:1234'
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
sourceDir: '/bower_components',
|
||||||
share: {
|
share: {
|
||||||
|
|||||||
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}
|
||||||
|
*/
|
||||||
|
`
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<script src="../../../y-array/y-array.js"></script>
|
<script src="../../../y-array/y-array.js"></script>
|
||||||
<script src="../../../y-text/dist/y-text.js"></script>
|
<script src="../../../y-text/dist/y-text.js"></script>
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
<script src="../../../y-memory/y-memory.js"></script>
|
||||||
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
let search = new URLSearchParams(location.search)
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
// initialize a shared object. This function call returns a promise!
|
||||||
Y({
|
Y({
|
||||||
db: {
|
db: {
|
||||||
@@ -7,17 +10,21 @@ Y({
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
room: 'Textarea-example'
|
room: 'Textarea-example',
|
||||||
// url: '127.0.0.1:1234'
|
// url: '//localhost:1234',
|
||||||
|
url: 'https://yjs-v13.herokuapp.com/'
|
||||||
|
// options: { transports: ['websocket'], upgrade: false }
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
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) {
|
}).then(function (y) {
|
||||||
window.yTextarea = y
|
window.yTextarea = y
|
||||||
|
|
||||||
// bind the textarea to a shared text element
|
// bind the textarea to a shared text element
|
||||||
y.share.textarea.bind(document.getElementById('textfield'))
|
y.share.textarea.bind(document.getElementById('textfield'))
|
||||||
// thats it..
|
// thats it..
|
||||||
|
}).catch(() => {
|
||||||
|
console.log('Something went wrong while creating the instance..')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
</head>
|
</head>
|
||||||
<script src="../bower_components/yjs/y.js"></script>
|
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||||
<script src="../bower_components/jquery/dist/jquery.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||||
|
<script src="../yjs-dist.js"></script>
|
||||||
<script src="./index.js"></script>
|
<script src="./index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Y({
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: 'websockets-client',
|
name: 'websockets-client',
|
||||||
|
// url: 'http://127.0.0.1:1234',
|
||||||
|
url: 'http://192.168.178.81:1234',
|
||||||
room: 'Xml-example'
|
room: 'Xml-example'
|
||||||
},
|
},
|
||||||
sourceDir: '/bower_components',
|
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
|
||||||
594
package-lock.json
generated
594
package-lock.json
generated
@@ -1,8 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-2",
|
"version": "13.0.0-16",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"accepts": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
|
||||||
|
"integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "4.0.13",
|
"version": "4.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
|
||||||
@@ -63,8 +69,19 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz",
|
||||||
"integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=",
|
"integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
},
|
||||||
|
"apache-crypt": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.1.tgz",
|
||||||
|
"integrity": "sha1-1vxyqm0n2ZyVqU/RiNcx7v/6Zjw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"apache-md5": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-7klza2ObTxCLbp5ibG2pkwa0FpI=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"argparse": {
|
"argparse": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
@@ -90,6 +107,12 @@
|
|||||||
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
|
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"array-find-index": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"array-union": {
|
"array-union": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||||
@@ -130,8 +153,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
|
||||||
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
|
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"babel-cli": {
|
"babel-cli": {
|
||||||
"version": "6.24.1",
|
"version": "6.24.1",
|
||||||
@@ -499,12 +521,29 @@
|
|||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"basic-auth": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"batch": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||||
|
"integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"bcryptjs": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
|
"integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"binary-extensions": {
|
"binary-extensions": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz",
|
||||||
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
|
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
@@ -556,6 +595,20 @@
|
|||||||
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
|
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"camelcase-keys": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
|
||||||
|
"integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"center-align": {
|
"center-align": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
|
||||||
@@ -586,8 +639,7 @@
|
|||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
||||||
"integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
|
"integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"circular-json": {
|
"circular-json": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
@@ -625,6 +677,12 @@
|
|||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"colors": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.10.0.tgz",
|
||||||
@@ -701,6 +759,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"connect": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect/-/connect-3.5.1.tgz",
|
||||||
|
"integrity": "sha1-bTDXpjx/FwhXprOqazY9lz3KWI4=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
|
||||||
|
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"contains-path": {
|
"contains-path": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
|
||||||
@@ -725,6 +803,24 @@
|
|||||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"cors": {
|
||||||
|
"version": "2.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.3.tgz",
|
||||||
|
"integrity": "sha1-TPeOHSMymnSWsvwiJbd8pbteuAI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"currently-unhandled": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
|
||||||
|
"integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"cutest": {
|
||||||
|
"version": "0.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/cutest/-/cutest-0.1.9.tgz",
|
||||||
|
"integrity": "sha512-bRyVi9vWknRWw+wIx0hhsCJKnsvRsB3Jmssl0zlFrKyqrYeBPpMKoZItpl7nziZi9ZqrgYoGo21fWKvnJIo8Dw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"d": {
|
"d": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
|
||||||
@@ -778,6 +874,18 @@
|
|||||||
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"depd": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"destroy": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||||
|
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"detect-indent": {
|
"detect-indent": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
|
||||||
@@ -790,12 +898,36 @@
|
|||||||
"integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=",
|
"integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"duplexer": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"encodeurl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"error-ex": {
|
"error-ex": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
|
||||||
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
|
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"error-stack-parser": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-oyArj7AxFKqbQKDjZp5IsrZaAQo=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"es-abstract": {
|
"es-abstract": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz",
|
||||||
@@ -844,6 +976,12 @@
|
|||||||
"integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
|
"integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"escape-string-regexp": {
|
"escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
@@ -990,12 +1128,24 @@
|
|||||||
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
|
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"etag": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz",
|
||||||
|
"integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"event-emitter": {
|
"event-emitter": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
|
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"event-stream": {
|
||||||
|
"version": "3.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
|
||||||
|
"integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"exit-hook": {
|
"exit-hook": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
|
||||||
@@ -1044,6 +1194,12 @@
|
|||||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"faye-websocket": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz",
|
||||||
|
"integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"figures": {
|
"figures": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
||||||
@@ -1068,6 +1224,26 @@
|
|||||||
"integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
|
"integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"finalhandler": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.5.1.tgz",
|
||||||
|
"integrity": "sha1-LEANjUUwk1vCMlScX6OF7Afeb80=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
|
||||||
|
"integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"find-root": {
|
"find-root": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||||
@@ -1104,6 +1280,18 @@
|
|||||||
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
|
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fresh": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz",
|
||||||
|
"integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
|
||||||
|
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"fs-exists-sync": {
|
"fs-exists-sync": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
|
||||||
@@ -1893,6 +2081,24 @@
|
|||||||
"integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
|
"integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"hosted-git-info": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"http-auth": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-auth/-/http-auth-3.1.3.tgz",
|
||||||
|
"integrity": "sha1-lFz63WZSHq+PfISRPTd9exXyTjE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"http-errors": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz",
|
||||||
|
"integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ignore": {
|
"ignore": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz",
|
||||||
@@ -1905,6 +2111,12 @@
|
|||||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
|
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"indent-string": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"inflight": {
|
"inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@@ -1951,8 +2163,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
|
||||||
"integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
|
"integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"is-buffer": {
|
"is-buffer": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
@@ -1960,6 +2171,12 @@
|
|||||||
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=",
|
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-builtin-module": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"is-callable": {
|
"is-callable": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
|
||||||
@@ -2086,6 +2303,12 @@
|
|||||||
"integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
|
"integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-utf8": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"is-valid-glob": {
|
"is-valid-glob": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz",
|
||||||
@@ -2098,6 +2321,12 @@
|
|||||||
"integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=",
|
"integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-wsl": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"isarray": {
|
"isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
@@ -2182,6 +2411,20 @@
|
|||||||
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
|
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"live-server": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/live-server/-/live-server-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-RJhkS7+Bpm8Y3Y3/3vYcTBw3TKM=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"load-json-file": {
|
"load-json-file": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||||
@@ -2226,24 +2469,74 @@
|
|||||||
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
|
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"loud-rejection": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
|
||||||
|
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"magic-string": {
|
"magic-string": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.19.1.tgz",
|
||||||
"integrity": "sha1-FNdoATyvLsj96hakmvgvw3fnUgE=",
|
"integrity": "sha1-FNdoATyvLsj96hakmvgvw3fnUgE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"map-obj": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"map-stream": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
|
||||||
|
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"matched": {
|
"matched": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/matched/-/matched-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/matched/-/matched-0.4.4.tgz",
|
||||||
"integrity": "sha1-Vte36xgDPwz5vFLrIJD6x9weifo=",
|
"integrity": "sha1-Vte36xgDPwz5vFLrIJD6x9weifo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"meow": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
|
||||||
|
"integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"micromatch": {
|
"micromatch": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
|
||||||
"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
|
"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz",
|
||||||
|
"integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz",
|
||||||
|
"integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
|
||||||
|
"integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
@@ -2262,6 +2555,12 @@
|
|||||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"morgan": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.8.2.tgz",
|
||||||
|
"integrity": "sha1-eErHc05KRTqcbm6GgKkyknXItoc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
@@ -2286,6 +2585,18 @@
|
|||||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"negotiator": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||||
|
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"normalize-package-data": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"normalize-path": {
|
"normalize-path": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
||||||
@@ -2322,6 +2633,18 @@
|
|||||||
"integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
|
"integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"on-headers": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"once": {
|
"once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -2334,6 +2657,12 @@
|
|||||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"opn": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/opn/-/opn-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-iPNl7SyM8L30Rm1sjGdLLheyHVw5YXVfi3SKWJzBI7efxRwHojfRFjwE/OLM6qp9xJYMgab8WicTU1cPoY+Hpg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"optionator": {
|
"optionator": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
|
||||||
@@ -2396,6 +2725,12 @@
|
|||||||
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
|
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"parseurl": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
|
||||||
|
"integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"path-exists": {
|
"path-exists": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
|
||||||
@@ -2420,6 +2755,18 @@
|
|||||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"path-type": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"pause-stream": {
|
||||||
|
"version": "0.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
|
||||||
|
"integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"pify": {
|
"pify": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
@@ -2506,6 +2853,12 @@
|
|||||||
"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
|
"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"proxy-middleware": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz",
|
||||||
|
"integrity": "sha1-o/3xvvtzD5UZZYcqwvYHTGFHelY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"randomatic": {
|
"randomatic": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",
|
||||||
@@ -2534,6 +2887,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"range-parser": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||||
|
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"read-pkg": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"load-json-file": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"strip-bom": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"read-pkg-up": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||||
@@ -2544,8 +2929,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||||
"integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
|
"integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"readline2": {
|
"readline2": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -2559,6 +2943,12 @@
|
|||||||
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
|
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"redent": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz",
|
||||||
@@ -2793,6 +3183,26 @@
|
|||||||
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
|
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"send": {
|
||||||
|
"version": "0.15.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-0.15.3.tgz",
|
||||||
|
"integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
|
||||||
|
"integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-index": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.0.tgz",
|
||||||
|
"integrity": "sha1-0rKA/FYNYW7oG0i/D6gqvtJIXOc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"set-getter": {
|
"set-getter": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz",
|
||||||
@@ -2803,8 +3213,13 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||||
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
|
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
},
|
||||||
|
"setprototypeof": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"shelljs": {
|
"shelljs": {
|
||||||
"version": "0.7.8",
|
"version": "0.7.8",
|
||||||
@@ -2812,6 +3227,12 @@
|
|||||||
"integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
|
"integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"signal-exit": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||||
|
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"slash": {
|
"slash": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||||
@@ -2842,12 +3263,60 @@
|
|||||||
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
|
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"spdx-correct": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"spdx-expression-parse": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz",
|
||||||
|
"integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"spdx-license-ids": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
|
||||||
|
"integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"split": {
|
||||||
|
"version": "0.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
|
||||||
|
"integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"sprintf-js": {
|
"sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"stack-generator": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-s32LDZoqblLAbMjhhfmPGZ+2OAQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"stackframe": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-/mSrILFw5M5JBEsSbBGd+g5dx8w=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"stacktrace-gps": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-Hm9Jl4QdK1vaurnmEX6WXrvmQoY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"stacktrace-js": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-d2ymRqlbxsayuQd2U2p/xyxt21g=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"version": "10.0.2",
|
"version": "10.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/standard/-/standard-10.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/standard/-/standard-10.0.2.tgz",
|
||||||
@@ -2868,6 +3337,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"statuses": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
|
||||||
|
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"stream-combiner": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
|
||||||
|
"integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||||
@@ -2880,6 +3361,16 @@
|
|||||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"string.fromcodepoint": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-jZeDM8C8klOPUPOD5IiPPlYZ1lM="
|
||||||
|
},
|
||||||
|
"string.prototype.codepointat": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz",
|
||||||
|
"integrity": "sha1-aybpvTr8qnvjtCabUm3huCAArHg="
|
||||||
|
},
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||||
@@ -2892,6 +3383,20 @@
|
|||||||
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
|
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"strip-indent": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"get-stdin": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
|
||||||
|
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"strip-json-comments": {
|
"strip-json-comments": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
@@ -2972,6 +3477,12 @@
|
|||||||
"integrity": "sha1-yWPc8DciiS7FnLpWnpQLcZVNFyk=",
|
"integrity": "sha1-yWPc8DciiS7FnLpWnpQLcZVNFyk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"trim-newlines": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"trim-right": {
|
"trim-right": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
|
||||||
@@ -3015,30 +3526,83 @@
|
|||||||
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
|
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"unix-crypt-td-js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-HAgkFQSBvHoB1J6Y8exmjYJBLzs=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"user-home": {
|
"user-home": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
|
||||||
"integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
|
"integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"utf-8": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf-8/-/utf-8-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-QpwJ+xrDLOuvVllh7aSMs/RSIZc="
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"utils-merge": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"v8flags": {
|
"v8flags": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
|
||||||
"integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
|
"integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"validate-npm-package-license": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"vary": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"vlq": {
|
"vlq": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.2.tgz",
|
||||||
"integrity": "sha1-4xbVJXtAuGu0PLjV/qXX9U1rDKE=",
|
"integrity": "sha1-4xbVJXtAuGu0PLjV/qXX9U1rDKE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"websocket-driver": {
|
||||||
|
"version": "0.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz",
|
||||||
|
"integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"websocket-extensions": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz",
|
||||||
|
"integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"which": {
|
"which": {
|
||||||
"version": "1.2.14",
|
"version": "1.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-2",
|
"version": "13.0.0-16",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"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": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run lint",
|
||||||
|
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||||
"lint": "standard",
|
"lint": "standard",
|
||||||
"dist": "rollup -c rollup.dist.js",
|
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||||
"serve": "concurrently 'serve ..' 'rollup -wc rollup.dist.js -o examples/bower_components/yjs/y.js'",
|
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||||
"postversion": "npm run dist",
|
"postversion": "npm run dist",
|
||||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
||||||
},
|
},
|
||||||
@@ -48,6 +51,7 @@
|
|||||||
"babel-preset-latest": "^6.24.1",
|
"babel-preset-latest": "^6.24.1",
|
||||||
"chance": "^1.0.9",
|
"chance": "^1.0.9",
|
||||||
"concurrently": "^3.4.0",
|
"concurrently": "^3.4.0",
|
||||||
|
"cutest": "^0.1.9",
|
||||||
"rollup-plugin-babel": "^2.7.1",
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
"rollup-plugin-commonjs": "^8.0.2",
|
"rollup-plugin-commonjs": "^8.0.2",
|
||||||
"rollup-plugin-inject": "^2.0.0",
|
"rollup-plugin-inject": "^2.0.0",
|
||||||
@@ -60,6 +64,7 @@
|
|||||||
"tag-dist-files": "^0.1.6"
|
"tag-dist-files": "^0.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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'
|
import multiEntry from 'rollup-plugin-multi-entry'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
entry: 'tests/*.js',
|
entry: 'test/y-xml.tests.js',
|
||||||
moduleName: 'y-array-tests',
|
moduleName: 'y-tests',
|
||||||
format: 'umd',
|
format: 'umd',
|
||||||
plugins: [
|
plugins: [
|
||||||
nodeResolve({
|
nodeResolve({
|
||||||
@@ -15,6 +15,6 @@ export default {
|
|||||||
commonjs(),
|
commonjs(),
|
||||||
multiEntry()
|
multiEntry()
|
||||||
],
|
],
|
||||||
dest: 'y-array.test.js',
|
dest: 'y.test.js',
|
||||||
sourceMap: true
|
sourceMap: true
|
||||||
}
|
}
|
||||||
|
|||||||
397
src/Connector.js
397
src/Connector.js
@@ -1,40 +1,18 @@
|
|||||||
/* @flow */
|
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||||
'use strict'
|
import { sendSyncStep1, computeMessageSyncStep1, computeMessageSyncStep2, computeMessageUpdate } from './MessageHandler.js'
|
||||||
|
|
||||||
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
|
||||||
function canWrite (auth) { return auth === 'write' }
|
|
||||||
|
|
||||||
export default function extendConnector (Y/* :any */) {
|
export default function extendConnector (Y/* :any */) {
|
||||||
class AbstractConnector {
|
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:
|
opts contains the following information:
|
||||||
role : String Role of this client ("master" or "slave")
|
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) {
|
constructor (y, opts) {
|
||||||
this.y = y
|
this.y = y
|
||||||
if (opts == null) {
|
if (opts == null) {
|
||||||
opts = {}
|
opts = {}
|
||||||
}
|
}
|
||||||
|
this.opts = opts
|
||||||
// Prefer to receive untransformed operations. This does only work if
|
// Prefer to receive untransformed operations. This does only work if
|
||||||
// this client receives operations from only one other client.
|
// this client receives operations from only one other client.
|
||||||
// In particular, this does not work with y-webrtc.
|
// 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.logMessage = Y.debug('y:connector-message')
|
||||||
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
|
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
|
||||||
this.role = opts.role
|
this.role = opts.role
|
||||||
this.connections = {}
|
this.connections = new Map()
|
||||||
this.isSynced = false
|
this.isSynced = false
|
||||||
this.userEventListeners = []
|
this.userEventListeners = []
|
||||||
this.whenSyncedListeners = []
|
this.whenSyncedListeners = []
|
||||||
this.currentSyncTarget = null
|
this.currentSyncTarget = null
|
||||||
this.syncingClients = []
|
|
||||||
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
|
||||||
this.debug = opts.debug === true
|
this.debug = opts.debug === true
|
||||||
this.broadcastOpBuffer = []
|
this.broadcastOpBuffer = []
|
||||||
this.protocolVersion = 11
|
this.protocolVersion = 11
|
||||||
this.authInfo = opts.auth || null
|
this.authInfo = opts.auth || null
|
||||||
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
||||||
if (opts.generateUserId !== false) {
|
if (opts.generateUserId !== false) {
|
||||||
this.setUserId(Y.utils.generateGuid())
|
this.setUserId(Y.utils.generateUserId())
|
||||||
}
|
|
||||||
}
|
|
||||||
resetAuth (auth) {
|
|
||||||
if (this.authInfo !== auth) {
|
|
||||||
this.authInfo = auth
|
|
||||||
this.broadcast({
|
|
||||||
type: 'auth',
|
|
||||||
auth: this.authInfo
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect () {
|
reconnect () {
|
||||||
this.log('reconnecting..')
|
this.log('reconnecting..')
|
||||||
return this.y.db.startGarbageCollector()
|
return this.y.db.startGarbageCollector()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect () {
|
disconnect () {
|
||||||
this.log('discronnecting..')
|
this.log('discronnecting..')
|
||||||
this.connections = {}
|
this.connections = new Map()
|
||||||
this.isSynced = false
|
this.isSynced = false
|
||||||
this.currentSyncTarget = null
|
this.currentSyncTarget = null
|
||||||
this.syncingClients = []
|
|
||||||
this.whenSyncedListeners = []
|
this.whenSyncedListeners = []
|
||||||
this.y.db.stopGarbageCollector()
|
this.y.db.stopGarbageCollector()
|
||||||
return this.y.db.whenTransactionsFinished()
|
return this.y.db.whenTransactionsFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
repair () {
|
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')
|
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.isSynced = false
|
||||||
this.currentSyncTarget = null
|
this.connections.forEach((user, userId) => {
|
||||||
this.findNextSyncTarget()
|
user.isSynced = false
|
||||||
|
this._syncWithUser(userId)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
if (this.userId == null) {
|
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.log('Set userId to "%s"', userId)
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
return this.y.db.setUserId(userId)
|
return this.y.db.setUserId(userId)
|
||||||
@@ -108,23 +82,21 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserEvent (f) {
|
onUserEvent (f) {
|
||||||
this.userEventListeners.push(f)
|
this.userEventListeners.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUserEventListener (f) {
|
removeUserEventListener (f) {
|
||||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||||
}
|
}
|
||||||
|
|
||||||
userLeft (user) {
|
userLeft (user) {
|
||||||
if (this.connections[user] != null) {
|
if (this.connections.has(user)) {
|
||||||
this.log('User left: %s', user)
|
this.log('%s: User left %s', this.userId, user)
|
||||||
delete this.connections[user]
|
this.connections.delete(user)
|
||||||
if (user === this.currentSyncTarget) {
|
// check if isSynced event can be sent now
|
||||||
this.currentSyncTarget = null
|
this._setSyncedWith(null)
|
||||||
this.findNextSyncTarget()
|
|
||||||
}
|
|
||||||
this.syncingClients = this.syncingClients.filter(function (cli) {
|
|
||||||
return cli !== user
|
|
||||||
})
|
|
||||||
for (var f of this.userEventListeners) {
|
for (var f of this.userEventListeners) {
|
||||||
f({
|
f({
|
||||||
action: 'userLeft',
|
action: 'userLeft',
|
||||||
@@ -133,21 +105,25 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userJoined (user, role) {
|
userJoined (user, role, auth) {
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
throw new Error('You must specify the role of the joined user!')
|
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!')
|
throw new Error('This user already joined!')
|
||||||
}
|
}
|
||||||
this.log('User joined: %s', user)
|
this.log('%s: User joined %s', this.userId, user)
|
||||||
this.connections[user] = {
|
this.connections.set(user, {
|
||||||
|
uid: user,
|
||||||
isSynced: false,
|
isSynced: false,
|
||||||
role: role
|
role: role,
|
||||||
}
|
processAfterAuth: [],
|
||||||
|
auth: auth || null,
|
||||||
|
receivedSyncStep2: false
|
||||||
|
})
|
||||||
let defer = {}
|
let defer = {}
|
||||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
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) {
|
for (var f of this.userEventListeners) {
|
||||||
f({
|
f({
|
||||||
action: 'userJoined',
|
action: 'userJoined',
|
||||||
@@ -155,9 +131,7 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
role: role
|
role: role
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.currentSyncTarget == null) {
|
this._syncWithUser(user)
|
||||||
this.findNextSyncTarget()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Execute a function _when_ we are connected.
|
// Execute a function _when_ we are connected.
|
||||||
// If not connected, wait until connected
|
// If not connected, wait until connected
|
||||||
@@ -168,61 +142,39 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
this.whenSyncedListeners.push(f)
|
this.whenSyncedListeners.push(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
findNextSyncTarget () {
|
_syncWithUser (userid) {
|
||||||
if (this.currentSyncTarget != null) {
|
if (this.role === 'slave') {
|
||||||
return // "The current sync has not finished!"
|
return // "The current sync has not finished or this is controlled by a master!"
|
||||||
}
|
|
||||||
|
|
||||||
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 = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
sendSyncStep1(this, userid)
|
||||||
}
|
}
|
||||||
send (uid, message) {
|
_fireIsSyncedListeners () {
|
||||||
this.log('Send \'%s\' to %s', message.type, uid)
|
this.y.db.whenTransactionsFinished().then(() => {
|
||||||
this.logMessage('Message: %j', message)
|
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) {
|
send (uid, buffer) {
|
||||||
this.log('Broadcast \'%s\'', message.type)
|
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||||
this.logMessage('Message: %j', message)
|
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.
|
Buffer operations, and broadcast them when ready.
|
||||||
@@ -234,11 +186,18 @@ export default function extendConnector (Y/* :any */) {
|
|||||||
var self = this
|
var self = this
|
||||||
function broadcastOperations () {
|
function broadcastOperations () {
|
||||||
if (self.broadcastOpBuffer.length > 0) {
|
if (self.broadcastOpBuffer.length > 0) {
|
||||||
self.broadcast({
|
let encoder = new BinaryEncoder()
|
||||||
type: 'update',
|
encoder.writeVarString(self.opts.room)
|
||||||
ops: self.broadcastOpBuffer
|
encoder.writeVarString('update')
|
||||||
})
|
let ops = self.broadcastOpBuffer
|
||||||
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) {
|
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.
|
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) {
|
if (sender === this.userId) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
this.log('Receive \'%s\' from %s', message.type, sender)
|
let decoder = new BinaryDecoder(buffer)
|
||||||
this.logMessage('Message: %j', message)
|
let encoder = new BinaryEncoder()
|
||||||
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
let roomname = decoder.readVarString() // read room name
|
||||||
this.log(
|
encoder.writeVarString(roomname)
|
||||||
`You tried to sync with a yjs instance that has a different protocol version
|
let messageType = decoder.readVarString()
|
||||||
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
let senderConn = this.connections.get(sender)
|
||||||
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)
|
|
||||||
// }
|
|
||||||
|
|
||||||
var ds = yield * this.getDeleteSet()
|
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
|
||||||
var answer = {
|
this.logMessage('Message: %Y', buffer)
|
||||||
type: 'sync step 2',
|
|
||||||
stateSet: currentStateSet,
|
if (senderConn == null && !skipAuth) {
|
||||||
deleteSet: ds,
|
throw new Error('Received message from unknown peer!')
|
||||||
protocolVersion: this.protocolVersion,
|
}
|
||||||
auth: this.authInfo
|
|
||||||
}
|
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||||
if (message.preferUntransformed === true && Object.keys(m.stateSet).length === 0) {
|
let auth = decoder.readVarUint()
|
||||||
answer.osUntransformed = yield * this.getOperationsUntransformed()
|
if (senderConn.auth == null) {
|
||||||
} else {
|
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
||||||
answer.os = yield * this.getOperations(m.stateSet)
|
// check auth
|
||||||
}
|
return this.checkAuth(auth, this.y, sender).then(authPermissions => {
|
||||||
conn.send(sender, answer)
|
if (senderConn.auth == null) {
|
||||||
if (this.forwardToSyncingClients) {
|
senderConn.auth = authPermissions
|
||||||
conn.syncingClients.push(sender)
|
this.y.emit('userAuthenticated', {
|
||||||
setTimeout(function () {
|
user: senderConn.uid,
|
||||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
auth: authPermissions
|
||||||
return cli !== sender
|
|
||||||
})
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
|
||||||
}, 5000) // TODO: conn.syncingClientDuration)
|
|
||||||
} else {
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
|
||||||
var db = this.y.db
|
|
||||||
let defer = this.connections[sender].syncStep2
|
|
||||||
let m = message
|
|
||||||
// apply operations first
|
|
||||||
db.requestTransaction(function * () {
|
|
||||||
// yield * this.applyDeleteSet(m.deleteSet)
|
|
||||||
if (m.osUntransformed != null) {
|
|
||||||
yield * this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
|
|
||||||
} else {
|
|
||||||
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) {
|
let messages = senderConn.processAfterAuth
|
||||||
var delops = message.ops.filter(function (o) {
|
senderConn.processAfterAuth = []
|
||||||
return o.struct === 'Delete'
|
|
||||||
})
|
return messages.reduce((p, m) =>
|
||||||
if (delops.length > 0) {
|
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
|
||||||
this.broadcastOps(delops)
|
, Promise.resolve())
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
this.y.db.apply(message.ops)
|
}
|
||||||
}
|
if (skipAuth || senderConn.auth != null) {
|
||||||
})
|
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||||
} else {
|
} 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) {
|
_setSyncedWith (user) {
|
||||||
var conn = this.connections[user]
|
if (user != null) {
|
||||||
if (conn != null) {
|
this.connections.get(user).isSynced = true
|
||||||
conn.isSynced = true
|
|
||||||
}
|
}
|
||||||
if (user === this.currentSyncTarget) {
|
let conns = Array.from(this.connections.values())
|
||||||
this.currentSyncTarget = null
|
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||||
this.findNextSyncTarget()
|
this._fireIsSyncedListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
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
|
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
|
|
||||||
}
|
|
||||||
@@ -120,7 +120,7 @@ export default function extendDatabase (Y /* :any */) {
|
|||||||
startGarbageCollector () {
|
startGarbageCollector () {
|
||||||
this.gc = this.dbOpts.gc
|
this.gc = this.dbOpts.gc
|
||||||
if (this.gc) {
|
if (this.gc) {
|
||||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
|
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
|
||||||
} else {
|
} else {
|
||||||
this.gcTimeout = -1
|
this.gcTimeout = -1
|
||||||
}
|
}
|
||||||
@@ -306,10 +306,12 @@ export default function extendDatabase (Y /* :any */) {
|
|||||||
* check if it is an expected op (otherwise wait for it)
|
* check if it is an expected op (otherwise wait for it)
|
||||||
* check if was deleted, apply a delete operation after op was applied
|
* check if was deleted, apply a delete operation after op was applied
|
||||||
*/
|
*/
|
||||||
apply (ops) {
|
applyOperations (decoder) {
|
||||||
this.opsReceivedTimestamp = new Date()
|
this.opsReceivedTimestamp = new Date()
|
||||||
for (var i = 0; i < ops.length; i++) {
|
let length = decoder.readUint32()
|
||||||
var o = ops[i]
|
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
let o = Y.Struct.binaryDecodeOperation(decoder)
|
||||||
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
||||||
var required = Y.Struct[o.struct].requiredOps(o)
|
var required = Y.Struct[o.struct].requiredOps(o)
|
||||||
if (o.requires != null) {
|
if (o.requires != null) {
|
||||||
@@ -586,11 +588,11 @@ export default function extendDatabase (Y /* :any */) {
|
|||||||
createType (typedefinition, id) {
|
createType (typedefinition, id) {
|
||||||
var structname = typedefinition[0].struct
|
var structname = typedefinition[0].struct
|
||||||
id = id || this.getNextOpId(1)
|
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
|
op.type = typedefinition[0].name
|
||||||
|
|
||||||
this.requestTransaction(function * () {
|
this.requestTransaction(function * () {
|
||||||
if (op.id[0] === '_') {
|
if (op.id[0] === 0xFFFFFF) {
|
||||||
yield * this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
} else {
|
} else {
|
||||||
yield * this.applyCreatedOperations([op])
|
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 */
|
const CDELETE = 0
|
||||||
'use strict'
|
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
|
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.
|
- Operations that are required to execute this operation.
|
||||||
*/
|
*/
|
||||||
export default function extendStruct (Y) {
|
export default function extendStruct (Y) {
|
||||||
var Struct = {
|
let Struct = {}
|
||||||
/* This is the only operation that is actually not a structure, because
|
Y.Struct = Struct
|
||||||
it is not stored in the OS. This is why it _does not_ have an id
|
Struct.binaryDecodeOperation = function (decoder) {
|
||||||
|
let code = decoder.peekUint8()
|
||||||
op = {
|
if (code === CDELETE) {
|
||||||
target: Id
|
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) {
|
/* This is the only operation that is actually not a structure, because
|
||||||
return {
|
it is not stored in the OS. This is why it _does not_ have an id
|
||||||
target: op.target,
|
|
||||||
length: op.length || 0,
|
op = {
|
||||||
struct: 'Delete'
|
target: Id
|
||||||
}
|
}
|
||||||
},
|
*/
|
||||||
requiredOps: function (op) {
|
Struct.Delete = {
|
||||||
return [] // [op.target]
|
encode: function (op) {
|
||||||
},
|
return {
|
||||||
execute: function * (op) {
|
target: op.target,
|
||||||
return yield * this.deleteOperation(op.target, op.length || 1)
|
length: op.length || 0,
|
||||||
|
struct: 'Delete'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Insert: {
|
binaryEncode: function (encoder, op) {
|
||||||
/* {
|
encoder.writeUint8(CDELETE)
|
||||||
content: [any],
|
encoder.writeOpID(op.target)
|
||||||
opContent: Id,
|
encoder.writeVarUint(op.length || 0)
|
||||||
id: Id,
|
},
|
||||||
left: Id,
|
binaryDecode: function (decoder) {
|
||||||
origin: Id,
|
decoder.skip8()
|
||||||
right: Id,
|
return {
|
||||||
parent: Id,
|
target: decoder.readOpID(),
|
||||||
parentSub: string (optional), // child of Map type
|
length: decoder.readVarUint(),
|
||||||
}
|
struct: 'Delete'
|
||||||
*/
|
}
|
||||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
},
|
||||||
// TODO: you could not send the "left" property, then you also have to
|
requiredOps: function (op) {
|
||||||
// "op.left = null" in $execute or $decode
|
return [] // [op.target]
|
||||||
var e/* :any */ = {
|
},
|
||||||
id: op.id,
|
execute: function * (op) {
|
||||||
left: op.left,
|
return yield * this.deleteOperation(op.target, op.length || 1)
|
||||||
right: op.right,
|
}
|
||||||
origin: op.origin,
|
}
|
||||||
parent: op.parent,
|
|
||||||
struct: op.struct
|
|
||||||
}
|
|
||||||
if (op.parentSub != null) {
|
|
||||||
e.parentSub = op.parentSub
|
|
||||||
}
|
|
||||||
if (op.hasOwnProperty('opContent')) {
|
|
||||||
e.opContent = op.opContent
|
|
||||||
} else {
|
|
||||||
e.content = op.content.slice()
|
|
||||||
}
|
|
||||||
|
|
||||||
return e
|
/* {
|
||||||
},
|
content: [any],
|
||||||
requiredOps: function (op) {
|
opContent: Id,
|
||||||
var ids = []
|
id: Id,
|
||||||
if (op.left != null) {
|
left: Id,
|
||||||
ids.push(op.left)
|
origin: Id,
|
||||||
}
|
right: Id,
|
||||||
if (op.right != null) {
|
parent: Id,
|
||||||
ids.push(op.right)
|
parentSub: string (optional), // child of Map type
|
||||||
}
|
}
|
||||||
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
*/
|
||||||
ids.push(op.origin)
|
Struct.Insert = {
|
||||||
}
|
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||||
// if (op.right == null && op.left == null) {
|
// TODO: you could not send the "left" property, then you also have to
|
||||||
ids.push(op.parent)
|
// "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) {
|
return e
|
||||||
ids.push(op.opContent)
|
},
|
||||||
|
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
|
return d
|
||||||
},
|
}
|
||||||
getDistanceToOrigin: function * (op) {
|
},
|
||||||
if (op.left == null) {
|
/*
|
||||||
return 0
|
# $this has to find a unique position between origin and the next known character
|
||||||
} else {
|
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||||
var d = 0
|
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||||
var o = yield * this.getInsertion(op.left)
|
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||||
while (!Y.utils.matchesId(o, op.origin)) {
|
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||||
d++
|
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||||
if (o.left == null) {
|
# therefore $this would be always to the right of o3
|
||||||
break
|
# case 2: $origin < $o.origin
|
||||||
} else {
|
# if current $this insert_position > $o origin: $this ins
|
||||||
o = yield * this.getInsertion(o.left)
|
# 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) {
|
||||||
return d
|
// 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
|
||||||
# $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 {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
i++
|
||||||
|
if (o.right != null) {
|
||||||
// reconnect..
|
o = yield * this.getInsertion(o.right)
|
||||||
var left = null
|
} else {
|
||||||
var right = null
|
o = 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 {
|
} else {
|
||||||
if (right == null || left == null) {
|
break
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
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,
|
start: null,
|
||||||
end: null,
|
end: null,
|
||||||
struct: "List",
|
struct: 'List',
|
||||||
type: "",
|
id: id
|
||||||
id: this.os.getNextOpId(1)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
create: function (id) {
|
|
||||||
return {
|
|
||||||
start: null,
|
|
||||||
end: null,
|
|
||||||
struct: 'List',
|
|
||||||
id: id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
encode: function (op) {
|
|
||||||
var e = {
|
|
||||||
struct: 'List',
|
|
||||||
id: op.id,
|
|
||||||
type: op.type
|
|
||||||
}
|
|
||||||
if (op.requires != null) {
|
|
||||||
e.requires = op.requires
|
|
||||||
}
|
|
||||||
if (op.info != null) {
|
|
||||||
e.info = op.info
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
},
|
|
||||||
requiredOps: function () {
|
|
||||||
/*
|
|
||||||
var ids = []
|
|
||||||
if (op.start != null) {
|
|
||||||
ids.push(op.start)
|
|
||||||
}
|
|
||||||
if (op.end != null){
|
|
||||||
ids.push(op.end)
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
*/
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
execute: function * (op) {
|
|
||||||
op.start = null
|
|
||||||
op.end = null
|
|
||||||
},
|
|
||||||
ref: function * (op, pos) {
|
|
||||||
if (op.start == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var res = null
|
|
||||||
var o = yield * this.getOperation(op.start)
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (!o.deleted) {
|
|
||||||
res = o
|
|
||||||
pos--
|
|
||||||
}
|
|
||||||
if (pos >= 0 && o.right != null) {
|
|
||||||
o = yield * this.getOperation(o.right)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
},
|
|
||||||
map: function * (o, f) {
|
|
||||||
o = o.start
|
|
||||||
var res = []
|
|
||||||
while (o != null) { // TODO: change to != (at least some convention)
|
|
||||||
var operation = yield * this.getOperation(o)
|
|
||||||
if (!operation.deleted) {
|
|
||||||
res.push(f(operation))
|
|
||||||
}
|
|
||||||
o = operation.right
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Map: {
|
encode: function (op) {
|
||||||
|
var e = {
|
||||||
|
struct: 'List',
|
||||||
|
id: op.id,
|
||||||
|
type: op.type
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
},
|
||||||
|
binaryEncode: function (encoder, op) {
|
||||||
|
encoder.writeUint8(CLIST)
|
||||||
|
encoder.writeOpID(op.id)
|
||||||
|
encoder.writeVarString(op.type)
|
||||||
|
},
|
||||||
|
binaryDecode: function (decoder) {
|
||||||
|
decoder.skip8()
|
||||||
|
let op = {
|
||||||
|
id: decoder.readOpID(),
|
||||||
|
type: decoder.readVarString(),
|
||||||
|
struct: 'List',
|
||||||
|
start: null,
|
||||||
|
end: null
|
||||||
|
}
|
||||||
|
return op
|
||||||
|
},
|
||||||
|
requiredOps: function () {
|
||||||
/*
|
/*
|
||||||
{
|
var ids = []
|
||||||
map: {},
|
if (op.start != null) {
|
||||||
struct: "Map",
|
ids.push(op.start)
|
||||||
type: "",
|
}
|
||||||
id: this.os.getNextOpId(1)
|
if (op.end != null){
|
||||||
}
|
ids.push(op.end)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
*/
|
*/
|
||||||
create: function (id) {
|
return []
|
||||||
return {
|
},
|
||||||
id: id,
|
execute: function * (op) {
|
||||||
map: {},
|
op.start = null
|
||||||
struct: 'Map'
|
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) {
|
||||||
encode: function (op) {
|
o = yield * this.getOperation(o.right)
|
||||||
var e = {
|
} else {
|
||||||
struct: 'Map',
|
break
|
||||||
type: op.type,
|
|
||||||
id: op.id,
|
|
||||||
map: {} // overwrite map!!
|
|
||||||
}
|
}
|
||||||
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) {
|
o = operation.right
|
||||||
e.info = op.info
|
}
|
||||||
}
|
return res
|
||||||
return e
|
}
|
||||||
},
|
}
|
||||||
requiredOps: function () {
|
|
||||||
return []
|
/*
|
||||||
},
|
{
|
||||||
execute: function * () {},
|
map: {},
|
||||||
/*
|
struct: "Map",
|
||||||
Get a property by name
|
type: "",
|
||||||
*/
|
id: this.os.getNextOpId(1)
|
||||||
get: function * (op, name) {
|
}
|
||||||
var oid = op.map[name]
|
*/
|
||||||
if (oid != null) {
|
Struct.Map = {
|
||||||
var res = yield * this.getOperation(oid)
|
create: function (id) {
|
||||||
if (res == null || res.deleted) {
|
return {
|
||||||
return void 0
|
id: id,
|
||||||
} else if (res.opContent == null) {
|
map: {},
|
||||||
return res.content[0]
|
struct: 'Map'
|
||||||
} else {
|
}
|
||||||
return yield * this.getType(res.opContent)
|
},
|
||||||
}
|
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 */
|
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Partial definition of a transaction
|
Partial definition of a transaction
|
||||||
@@ -96,9 +95,12 @@ export default function extendTransaction (Y) {
|
|||||||
send.push(Y.Struct[op.struct].encode(op))
|
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
|
// is connected, and this is not going to be send in addOperation
|
||||||
this.store.y.connector.broadcastOps(send)
|
this.store.y.connector.broadcastOps(send)
|
||||||
|
if (this.store.y.persistence != null) {
|
||||||
|
this.store.y.persistence.saveOperations(send)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,11 +590,20 @@ export default function extendTransaction (Y) {
|
|||||||
apply a delete set in order to get
|
apply a delete set in order to get
|
||||||
the state of the supplied ds
|
the state of the supplied ds
|
||||||
*/
|
*/
|
||||||
* applyDeleteSet (ds) {
|
* applyDeleteSet (decoder) {
|
||||||
var deletions = []
|
var deletions = []
|
||||||
|
|
||||||
for (var user in ds) {
|
let dsLength = decoder.readUint32()
|
||||||
var dv = ds[user]
|
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 pos = 0
|
||||||
var d = dv[pos]
|
var d = dv[pos]
|
||||||
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
|
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
|
||||||
@@ -672,10 +683,15 @@ export default function extendTransaction (Y) {
|
|||||||
yield * this.garbageCollectOperation(o.id)
|
yield * this.garbageCollectOperation(o.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.store.forwardAppliedOperations) {
|
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
|
||||||
var ops = []
|
var ops = []
|
||||||
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -686,21 +702,34 @@ export default function extendTransaction (Y) {
|
|||||||
/*
|
/*
|
||||||
A DeleteSet (ds) describes all the deleted ops in the OS
|
A DeleteSet (ds) describes all the deleted ops in the OS
|
||||||
*/
|
*/
|
||||||
* getDeleteSet () {
|
* writeDeleteSet (encoder) {
|
||||||
var ds = {}
|
var ds = new Map()
|
||||||
yield * this.ds.iterate(this, null, null, function * (n) {
|
yield * this.ds.iterate(this, null, null, function * (n) {
|
||||||
var user = n.id[0]
|
var user = n.id[0]
|
||||||
var counter = n.id[1]
|
var counter = n.id[1]
|
||||||
var len = n.len
|
var len = n.len
|
||||||
var gc = n.gc
|
var gc = n.gc
|
||||||
var dv = ds[user]
|
var dv = ds.get(user)
|
||||||
if (dv === void 0) {
|
if (dv === void 0) {
|
||||||
dv = []
|
dv = []
|
||||||
ds[user] = dv
|
ds.set(user, dv)
|
||||||
}
|
}
|
||||||
dv.push([counter, len, gc])
|
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) {
|
* isDeleted (id) {
|
||||||
var n = yield * this.ds.findWithUpperBound(id)
|
var n = yield * this.ds.findWithUpperBound(id)
|
||||||
@@ -712,9 +741,15 @@ export default function extendTransaction (Y) {
|
|||||||
}
|
}
|
||||||
* addOperation (op) {
|
* addOperation (op) {
|
||||||
yield * this.os.put(op)
|
yield * this.os.put(op)
|
||||||
if (this.store.y.connector.isSynced && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
||||||
// is connected, and this is not going to be send in addOperation
|
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
|
||||||
this.store.y.connector.broadcastOps([op])
|
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)
|
// if insertion, try to combine with left insertion (if both have content property)
|
||||||
@@ -821,14 +856,19 @@ export default function extendTransaction (Y) {
|
|||||||
}
|
}
|
||||||
* getOperation (id/* :any */)/* :Transaction<any> */ {
|
* getOperation (id/* :any */)/* :Transaction<any> */ {
|
||||||
var o = yield * this.os.find(id)
|
var o = yield * this.os.find(id)
|
||||||
if (id[0] !== '_' || o != null) {
|
if (id[0] !== 0xFFFFFF || o != null) {
|
||||||
return o
|
return o
|
||||||
} else { // type is string
|
} else { // type is string
|
||||||
// generate this operation?
|
// generate this operation?
|
||||||
var comp = id[1].split('_')
|
var comp = id[1].split('_')
|
||||||
if (comp.length > 1) {
|
if (comp.length > 1) {
|
||||||
var struct = comp[0]
|
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]
|
op.type = comp[1]
|
||||||
yield * this.setOperation(op)
|
yield * this.setOperation(op)
|
||||||
return op
|
return op
|
||||||
@@ -878,6 +918,18 @@ export default function extendTransaction (Y) {
|
|||||||
})
|
})
|
||||||
return ss
|
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.
|
Here, we make all missing operations executable for the receiving user.
|
||||||
|
|
||||||
@@ -927,17 +979,17 @@ export default function extendTransaction (Y) {
|
|||||||
* getOperations (startSS) {
|
* getOperations (startSS) {
|
||||||
// TODO: use bounds here!
|
// TODO: use bounds here!
|
||||||
if (startSS == null) {
|
if (startSS == null) {
|
||||||
startSS = {}
|
startSS = new Map()
|
||||||
}
|
}
|
||||||
var send = []
|
var send = []
|
||||||
|
|
||||||
var endSV = yield * this.getStateVector()
|
var endSV = yield * this.getStateVector()
|
||||||
for (let endState of endSV) {
|
for (let endState of endSV) {
|
||||||
let user = endState.user
|
let user = endState.user
|
||||||
if (user === '_') {
|
if (user === 0xFFFFFF) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let startPos = startSS[user] || 0
|
let startPos = startSS.get(user) || 0
|
||||||
if (startPos > 0) {
|
if (startPos > 0) {
|
||||||
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
|
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
|
||||||
// find out if that is the case
|
// find out if that is the case
|
||||||
@@ -947,19 +999,19 @@ export default function extendTransaction (Y) {
|
|||||||
startPos = firstMissing.id[1]
|
startPos = firstMissing.id[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startSS[user] = startPos
|
startSS.set(user, startPos)
|
||||||
}
|
}
|
||||||
for (let endState of endSV) {
|
for (let endState of endSV) {
|
||||||
let user = endState.user
|
let user = endState.user
|
||||||
let startPos = startSS[user]
|
let startPos = startSS.get(user)
|
||||||
if (user === '_') {
|
if (user === 0xFFFFFF) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
||||||
op = Y.Struct[op.struct].encode(op)
|
op = Y.Struct[op.struct].encode(op)
|
||||||
if (op.struct !== 'Insert') {
|
if (op.struct !== 'Insert') {
|
||||||
send.push(op)
|
send.push(op)
|
||||||
} 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
|
// case 1. op.right is known
|
||||||
// this case is only reached if op.right is known.
|
// this case is only reached if op.right is known.
|
||||||
// => this is not called for op.left, as op.right is unknown
|
// => this is not called for op.left, as op.right is unknown
|
||||||
@@ -977,7 +1029,7 @@ export default function extendTransaction (Y) {
|
|||||||
op.left = null
|
op.left = null
|
||||||
send.push(op)
|
send.push(op)
|
||||||
/* not necessary, as o is already sent..
|
/* not necessary, as o is already sent..
|
||||||
if (!Y.utils.compareIds(o.id, op.id) && o.id[1] >= (startSS[o.id[0]] || 0)) {
|
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 is not op && o is unknown
|
||||||
o = Y.Struct[op.struct].encode(o)
|
o = Y.Struct[op.struct].encode(o)
|
||||||
o.right = missingOrigins[missingOrigins.length - 1].id
|
o.right = missingOrigins[missingOrigins.length - 1].id
|
||||||
@@ -991,7 +1043,7 @@ export default function extendTransaction (Y) {
|
|||||||
while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
|
while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
|
||||||
missingOrigins.pop()
|
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
|
// case 2. o is known
|
||||||
op.left = Y.utils.getLastId(o)
|
op.left = Y.utils.getLastId(o)
|
||||||
send.push(op)
|
send.push(op)
|
||||||
@@ -1023,28 +1075,62 @@ export default function extendTransaction (Y) {
|
|||||||
}
|
}
|
||||||
return send.reverse()
|
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.
|
* Get the plain untransformed operations from the database.
|
||||||
* You can apply these operations using .applyOperationsUntransformed(ops)
|
* You can apply these operations using .applyOperationsUntransformed(ops)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
* getOperationsUntransformed () {
|
* writeOperationsUntransformed (encoder) {
|
||||||
var ops = []
|
let lenPosition = encoder.pos
|
||||||
|
let len = 0
|
||||||
|
encoder.writeUint32(0) // placeholder
|
||||||
yield * this.os.iterate(this, null, null, function * (op) {
|
yield * this.os.iterate(this, null, null, function * (op) {
|
||||||
if (op.id[0] !== '_') {
|
if (op.id[0] !== 0xFFFFFF) {
|
||||||
ops.push(op)
|
len++
|
||||||
|
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return {
|
encoder.setUint32(lenPosition, len)
|
||||||
untransformed: ops
|
yield * this.writeStateSet(encoder)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
* applyOperationsUntransformed (m, stateSet) {
|
* applyOperationsUntransformed (decoder) {
|
||||||
var ops = m.untransformed
|
let len = decoder.readUint32()
|
||||||
for (var i = 0; i < ops.length; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
var op = ops[i]
|
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||||
// create, and modify parent, if it is created implicitly
|
yield * this.os.put(op)
|
||||||
if (op.parent != null && op.parent[0] === '_') {
|
}
|
||||||
|
yield * this.os.iterate(this, null, null, function * (op) {
|
||||||
|
if (op.parent != null) {
|
||||||
if (op.struct === 'Insert') {
|
if (op.struct === 'Insert') {
|
||||||
// update parents .map/start/end properties
|
// update parents .map/start/end properties
|
||||||
if (op.parentSub != null && op.left == null) {
|
if (op.parentSub != null && op.left == null) {
|
||||||
@@ -1064,12 +1150,14 @@ export default function extendTransaction (Y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield * this.os.put(op)
|
})
|
||||||
}
|
let stateSetLength = decoder.readUint32()
|
||||||
for (var user in stateSet) {
|
for (let i = 0; i < stateSetLength; i++) {
|
||||||
|
let user = decoder.readVarUint()
|
||||||
|
let clock = decoder.readVarUint()
|
||||||
yield * this.ss.put({
|
yield * this.ss.put({
|
||||||
id: [user],
|
id: [user],
|
||||||
clock: stateSet[user]
|
clock: clock
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/Utils.js
41
src/Utils.js
@@ -1,3 +1,7 @@
|
|||||||
|
/* globals crypto */
|
||||||
|
|
||||||
|
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
EventHandler is an helper class for constructing custom types.
|
EventHandler is an helper class for constructing custom types.
|
||||||
|
|
||||||
@@ -22,7 +26,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default function Utils (Y) {
|
export default function Utils (Y) {
|
||||||
Y.utils = {}
|
Y.utils = {
|
||||||
|
BinaryDecoder: BinaryDecoder,
|
||||||
|
BinaryEncoder: BinaryEncoder
|
||||||
|
}
|
||||||
|
|
||||||
Y.utils.bubbleEvent = function (type, event) {
|
Y.utils.bubbleEvent = function (type, event) {
|
||||||
type.eventHandler.callEventListeners(event)
|
type.eventHandler.callEventListeners(event)
|
||||||
@@ -818,8 +825,32 @@ export default function Utils (Y) {
|
|||||||
}
|
}
|
||||||
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
||||||
|
|
||||||
// Generates a unique id, for use as a user id.
|
function generateUserId () {
|
||||||
// Thx to @jed for this script https://gist.github.com/jed/982883
|
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
|
||||||
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
|
// browser
|
||||||
Y.utils.generateGuid = generateGuid
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/y.js
54
src/y.js
@@ -1,17 +1,22 @@
|
|||||||
import debug from 'debug'
|
|
||||||
import extendConnector from './Connector.js'
|
import extendConnector from './Connector.js'
|
||||||
|
import extendPersistence from './Persistence.js'
|
||||||
import extendDatabase from './Database.js'
|
import extendDatabase from './Database.js'
|
||||||
import extendTransaction from './Transaction.js'
|
import extendTransaction from './Transaction.js'
|
||||||
import extendStruct from './Struct.js'
|
import extendStruct from './Struct.js'
|
||||||
import extendUtils from './Utils.js'
|
import extendUtils from './Utils.js'
|
||||||
|
import debug from 'debug'
|
||||||
|
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
|
||||||
|
|
||||||
extendConnector(Y)
|
extendConnector(Y)
|
||||||
|
extendPersistence(Y)
|
||||||
extendDatabase(Y)
|
extendDatabase(Y)
|
||||||
extendTransaction(Y)
|
extendTransaction(Y)
|
||||||
extendStruct(Y)
|
extendStruct(Y)
|
||||||
extendUtils(Y)
|
extendUtils(Y)
|
||||||
|
|
||||||
Y.debug = debug
|
Y.debug = debug
|
||||||
|
debug.formatters.Y = formatYjsMessage
|
||||||
|
debug.formatters.y = formatYjsMessageType
|
||||||
|
|
||||||
var requiringModules = {}
|
var requiringModules = {}
|
||||||
|
|
||||||
@@ -134,10 +139,20 @@ export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
|||||||
opts.share = Y.utils.copyObject(opts.share)
|
opts.share = Y.utils.copyObject(opts.share)
|
||||||
Y.requestModules(modules).then(function () {
|
Y.requestModules(modules).then(function () {
|
||||||
var yconfig = new YConfig(opts)
|
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.db.whenUserIdSet(function () {
|
||||||
yconfig.init(function () {
|
yconfig.init(function () {
|
||||||
|
resolved = true
|
||||||
resolve(yconfig)
|
resolve(yconfig)
|
||||||
})
|
}, reject)
|
||||||
})
|
})
|
||||||
}).catch(reject)
|
}).catch(reject)
|
||||||
}
|
}
|
||||||
@@ -156,6 +171,11 @@ class YConfig extends Y.utils.NamedEventHandler {
|
|||||||
this.options = opts
|
this.options = opts
|
||||||
this.db = new Y[opts.db.name](this, opts.db)
|
this.db = new Y[opts.db.name](this, opts.db)
|
||||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
this.connector = new Y[opts.connector.name](this, opts.connector)
|
||||||
|
if (opts.persistence != null) {
|
||||||
|
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
|
||||||
|
} else {
|
||||||
|
this.persistence = null
|
||||||
|
}
|
||||||
this.connected = true
|
this.connected = true
|
||||||
}
|
}
|
||||||
init (callback) {
|
init (callback) {
|
||||||
@@ -166,28 +186,26 @@ class YConfig extends Y.utils.NamedEventHandler {
|
|||||||
// create shared object
|
// create shared object
|
||||||
for (var propertyname in opts.share) {
|
for (var propertyname in opts.share) {
|
||||||
var typeConstructor = opts.share[propertyname].split('(')
|
var typeConstructor = opts.share[propertyname].split('(')
|
||||||
|
let typeArgs = ''
|
||||||
|
if (typeConstructor.length === 2) {
|
||||||
|
typeArgs = typeConstructor[1].split(')')[0] || ''
|
||||||
|
}
|
||||||
var typeName = typeConstructor.splice(0, 1)
|
var typeName = typeConstructor.splice(0, 1)
|
||||||
var type = Y[typeName]
|
var type = Y[typeName]
|
||||||
var typedef = type.typeDefinition
|
var typedef = type.typeDefinition
|
||||||
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
|
||||||
var args = []
|
let args = Y.utils.parseTypeDefinition(type, typeArgs)
|
||||||
if (typeConstructor.length === 1) {
|
|
||||||
try {
|
|
||||||
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
|
||||||
}
|
|
||||||
if (type.typeDefinition.parseArguments == null) {
|
|
||||||
throw new Error(typeName + ' does not expect arguments!')
|
|
||||||
} else {
|
|
||||||
args = typedef.parseArguments(args[0])[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
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 () {
|
isConnected () {
|
||||||
return this.connector.isSynced
|
return this.connector.isSynced
|
||||||
|
|||||||
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)
|
||||||
|
})
|
||||||
@@ -3,20 +3,77 @@ import _Y from '../../yjs/src/y.js'
|
|||||||
|
|
||||||
import yMemory from '../../y-memory/src/y-memory.js'
|
import yMemory from '../../y-memory/src/y-memory.js'
|
||||||
import yArray from '../../y-array/src/y-array.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 yMap from '../../y-map/src/Map.js'
|
||||||
|
import yXml from '../../y-xml/src/y-xml.js'
|
||||||
import yTest from './test-connector.js'
|
import yTest from './test-connector.js'
|
||||||
|
|
||||||
import Chance from 'chance'
|
import Chance from 'chance'
|
||||||
|
|
||||||
export let Y = _Y
|
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) {
|
export async function garbageCollectUsers (t, users) {
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
|
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
|
* 1. reconnect and flush all
|
||||||
* 2. user 0 gc
|
* 2. user 0 gc
|
||||||
@@ -33,7 +90,17 @@ export async function compareUsers (t, users) {
|
|||||||
await wait()
|
await wait()
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
|
|
||||||
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
|
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()
|
||||||
await users[0].db.garbageCollect()
|
await users[0].db.garbageCollect()
|
||||||
@@ -60,10 +127,14 @@ export async function compareUsers (t, users) {
|
|||||||
var data = await Promise.all(users.map(async (u) => {
|
var data = await Promise.all(users.map(async (u) => {
|
||||||
var data = {}
|
var data = {}
|
||||||
u.db.requestTransaction(function * () {
|
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 = {}
|
data.os = {}
|
||||||
for (let i = 0; i < os.untransformed.length; i++) {
|
for (let i = 0; i < ops.length; i++) {
|
||||||
let op = os.untransformed[i]
|
let op = ops[i]
|
||||||
op = Y.Struct[op.struct].encode(op)
|
op = Y.Struct[op.struct].encode(op)
|
||||||
delete op.origin
|
delete op.origin
|
||||||
/*
|
/*
|
||||||
@@ -79,15 +150,18 @@ export async function compareUsers (t, users) {
|
|||||||
data.os[JSON.stringify(op.id)] = op
|
data.os[JSON.stringify(op.id)] = op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.ds = yield * this.getDeleteSet()
|
data.ds = yield * getDeleteSet.apply(this)
|
||||||
data.ss = yield * this.getStateSet()
|
data.ss = yield * getStateSet.apply(this)
|
||||||
})
|
})
|
||||||
await u.db.whenTransactionsFinished()
|
await u.db.whenTransactionsFinished()
|
||||||
return data
|
return data
|
||||||
}))
|
}))
|
||||||
for (var i = 0; i < data.length - 1; i++) {
|
for (var i = 0; i < data.length - 1; i++) {
|
||||||
await t.asyncGroup(async () => {
|
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].os, data[i + 1].os, 'os')
|
||||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||||
@@ -102,19 +176,19 @@ export async function initArrays (t, opts) {
|
|||||||
var result = {
|
var result = {
|
||||||
users: []
|
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 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++) {
|
for (let i = 0; i < opts.users; i++) {
|
||||||
let dbOpts
|
let dbOpts
|
||||||
let connOpts
|
let connOpts
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
// Only one instance can gc!
|
// Only one instance can gc!
|
||||||
dbOpts = Object.assign({ gc: true }, opts.db)
|
dbOpts = Object.assign({ gc: false }, database)
|
||||||
connOpts = Object.assign({ role: 'master' }, connector)
|
connOpts = Object.assign({ role: 'master' }, conn)
|
||||||
} else {
|
} else {
|
||||||
dbOpts = Object.assign({ gc: false }, opts.db)
|
dbOpts = Object.assign({ gc: false }, database)
|
||||||
connOpts = Object.assign({ role: 'slave' }, connector)
|
connOpts = Object.assign({ role: 'slave' }, conn)
|
||||||
}
|
}
|
||||||
let y = await Y({
|
let y = await Y({
|
||||||
connector: connOpts,
|
connector: connOpts,
|
||||||
@@ -189,3 +263,48 @@ export function wait (t) {
|
|||||||
setTimeout(resolve, t != null ? t : 100)
|
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,54 +1,53 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
import { wait } from './helper.js'
|
import { wait } from './helper.js'
|
||||||
|
import { formatYjsMessage } from '../src/MessageHandler.js'
|
||||||
|
|
||||||
var rooms = {}
|
var rooms = {}
|
||||||
|
|
||||||
export class TestRoom {
|
export class TestRoom {
|
||||||
constructor (roomname) {
|
constructor (roomname) {
|
||||||
this.room = roomname
|
this.room = roomname
|
||||||
this.users = {}
|
this.users = new Map()
|
||||||
this.nextUserId = 0
|
this.nextUserId = 0
|
||||||
}
|
}
|
||||||
join (connector) {
|
join (connector) {
|
||||||
if (connector.userId == null) {
|
if (connector.userId == null) {
|
||||||
connector.setUserId('' + (this.nextUserId++))
|
connector.setUserId(this.nextUserId++)
|
||||||
}
|
}
|
||||||
Object.keys(this.users).forEach(uid => {
|
this.users.forEach((user, uid) => {
|
||||||
let user = this.users[uid]
|
|
||||||
if (user.role === 'master' || connector.role === 'master') {
|
if (user.role === 'master' || connector.role === 'master') {
|
||||||
this.users[uid].userJoined(connector.userId, connector.role)
|
this.users.get(uid).userJoined(connector.userId, connector.role)
|
||||||
connector.userJoined(uid, this.users[uid].role)
|
connector.userJoined(uid, this.users.get(uid).role)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.users[connector.userId] = connector
|
this.users.set(connector.userId, connector)
|
||||||
}
|
}
|
||||||
leave (connector) {
|
leave (connector) {
|
||||||
delete this.users[connector.userId]
|
this.users.delete(connector.userId)
|
||||||
Object.keys(this.users).forEach(uid => {
|
this.users.forEach(user => {
|
||||||
this.users[uid].userLeft(connector.userId)
|
user.userLeft(connector.userId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
send (sender, receiver, m) {
|
send (sender, receiver, m) {
|
||||||
m = JSON.parse(JSON.stringify(m))
|
var user = this.users.get(receiver)
|
||||||
var user = this.users[receiver]
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
user.receiveMessage(sender, m)
|
user.receiveMessage(sender, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
broadcast (sender, m) {
|
broadcast (sender, m) {
|
||||||
Object.keys(this.users).forEach(receiver => {
|
this.users.forEach((user, receiver) => {
|
||||||
this.send(sender, receiver, m)
|
this.send(sender, receiver, m)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
async flushAll (users) {
|
async flushAll (users) {
|
||||||
let flushing = true
|
let flushing = true
|
||||||
let allUserIds = Object.keys(this.users)
|
let allUserIds = Array.from(this.users.keys())
|
||||||
if (users == null) {
|
if (users == null) {
|
||||||
users = allUserIds.map(id => this.users[id].y)
|
users = allUserIds.map(id => this.users.get(id).y)
|
||||||
}
|
}
|
||||||
while (flushing) {
|
while (flushing) {
|
||||||
await wait(10)
|
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')
|
flushing = res.some(status => status === 'flushing')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,14 +81,25 @@ export default function extendTestConnector (Y) {
|
|||||||
this.testRoom.leave(this)
|
this.testRoom.leave(this)
|
||||||
return super.disconnect()
|
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 () {
|
reconnect () {
|
||||||
this.testRoom.join(this)
|
this.testRoom.join(this)
|
||||||
return super.reconnect()
|
return super.reconnect()
|
||||||
}
|
}
|
||||||
send (uid, message) {
|
send (uid, message) {
|
||||||
|
super.send(uid, message)
|
||||||
this.testRoom.send(this.userId, uid, message)
|
this.testRoom.send(this.userId, uid, message)
|
||||||
}
|
}
|
||||||
broadcast (message) {
|
broadcast (message) {
|
||||||
|
super.broadcast(message)
|
||||||
this.testRoom.broadcast(this.userId, message)
|
this.testRoom.broadcast(this.userId, message)
|
||||||
}
|
}
|
||||||
async whenSynced (f) {
|
async whenSynced (f) {
|
||||||
@@ -109,10 +119,10 @@ export default function extendTestConnector (Y) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
receiveMessage (sender, m) {
|
receiveMessage (sender, m) {
|
||||||
if (this.userId !== sender && this.connections[sender] != null) {
|
if (this.userId !== sender && this.connections.has(sender)) {
|
||||||
var buffer = this.connections[sender].buffer
|
var buffer = this.connections.get(sender).buffer
|
||||||
if (buffer == null) {
|
if (buffer == null) {
|
||||||
buffer = this.connections[sender].buffer = []
|
buffer = this.connections.get(sender).buffer = []
|
||||||
}
|
}
|
||||||
buffer.push(m)
|
buffer.push(m)
|
||||||
if (this.chance.bool({likelihood: 30})) {
|
if (this.chance.bool({likelihood: 30})) {
|
||||||
@@ -127,13 +137,13 @@ export default function extendTestConnector (Y) {
|
|||||||
async _flushAll (flushUsers) {
|
async _flushAll (flushUsers) {
|
||||||
if (flushUsers.some(u => u.connector.userId === this.userId)) {
|
if (flushUsers.some(u => u.connector.userId === this.userId)) {
|
||||||
// this one needs to sync with every other user
|
// 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 = []
|
var finished = []
|
||||||
for (let i = 0; i < flushUsers.length; i++) {
|
for (let i = 0; i < flushUsers.length; i++) {
|
||||||
let userId = flushUsers[i].connector.userId
|
let userId = flushUsers[i].connector.userId
|
||||||
if (userId !== this.userId && this.connections[userId] != null) {
|
if (userId !== this.userId && this.connections.has(userId)) {
|
||||||
let buffer = this.connections[userId].buffer
|
let buffer = this.connections.get(userId).buffer
|
||||||
if (buffer != null) {
|
if (buffer != null) {
|
||||||
var messages = buffer.splice(0)
|
var messages = buffer.splice(0)
|
||||||
for (let j = 0; j < messages.length; j++) {
|
for (let j = 0; j < messages.length; j++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user