Compare commits
42 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 | ||
|
|
6c37bd4463 | ||
|
|
dd6c196135 | ||
|
|
252bec0ad2 | ||
|
|
6c8876d282 | ||
|
|
3c317828d1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
bower_components
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
|
||||
@@ -22,6 +22,7 @@ is a list of the modules we know of:
|
||||
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
||||
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
||||
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
||||
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|
||||
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
||||
|
||||
##### Database adapters
|
||||
|
||||
@@ -9,16 +9,6 @@
|
||||
"license": "MIT",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"yjs": "latest",
|
||||
"y-array": "latest",
|
||||
"y-map": "latest",
|
||||
"y-memory": "latest",
|
||||
"y-richtext": "latest",
|
||||
"y-webrtc": "latest",
|
||||
"y-websockets-client": "latest",
|
||||
"y-text": "latest",
|
||||
"y-indexeddb": "latest",
|
||||
"y-xml": "latest",
|
||||
"quill": "^1.0.0-rc.2",
|
||||
"ace": "~1.2.3",
|
||||
"ace-builds": "~1.2.3",
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<input name="message" type="text" style="width:60%;">
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-text/dist/y-text.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
</style>
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -7,8 +7,8 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example'
|
||||
// url: 'localhost:1234'
|
||||
room: 'drawing-example',
|
||||
url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
|
||||
11
examples/html-editor/index.html
Normal file
11
examples/html-editor/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
21
examples/html-editor/index.js
Normal file
21
examples/html-editor/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'html-editor-example6'
|
||||
},
|
||||
share: {
|
||||
xml: 'XmlFragment()' // y.share.xml is of type Y.Xml with tagname "p"
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yXml = y
|
||||
// Bind children of XmlFragment to the document.body
|
||||
window.yXml.share.xml.bindToDom(document.body)
|
||||
})
|
||||
58
examples/infiniteyjs/index.html
Normal file
58
examples/infiniteyjs/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: 7px;
|
||||
}
|
||||
.one {
|
||||
grid-column: 1 ;
|
||||
}
|
||||
.two {
|
||||
grid-column: 2;
|
||||
}
|
||||
.three {
|
||||
grid-column: 3;
|
||||
}
|
||||
textarea {
|
||||
width: calc(100% - 10px)
|
||||
}
|
||||
.editor-container {
|
||||
background-color: #4caf50;
|
||||
padding: 4px 5px 10px 5px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
.editor-container[disconnected] {
|
||||
background-color: red;
|
||||
}
|
||||
.disconnected-info {
|
||||
display: none;
|
||||
}
|
||||
.editor-container[disconnected] .disconnected-info {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
<div class="wrapper">
|
||||
<div id="container1" class="one editor-container">
|
||||
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container2" class="two editor-container">
|
||||
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container3" class="three editor-container">
|
||||
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/dist/y-text.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
64
examples/infiniteyjs/index.js
Normal file
64
examples/infiniteyjs/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/* global Y */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y1 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea1'))
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-second.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y2 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea2'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container2').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container2').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-third.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y3 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea3'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container3').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container3').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
@@ -16,7 +16,10 @@
|
||||
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -8,9 +8,9 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Puzzle-example'
|
||||
room: 'Puzzle-example',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
piece1: 'Map',
|
||||
piece2: 'Map',
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"name": "examples",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "rollup -c",
|
||||
"watch": "rollup -cw"
|
||||
},
|
||||
"author": "Kevin Jahns",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
@@ -19,13 +19,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
-->
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,8 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example-quill-1.0-test'
|
||||
room: 'richtext-example-quill-1.0-test',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
|
||||
27
examples/rollup.config.js
Normal file
27
examples/rollup.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'yjs-dist.esm',
|
||||
dest: 'yjs-dist.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
/* global Y */
|
||||
|
||||
// eslint-disable-next-line
|
||||
let search = new URLSearchParams(location.search)
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
@@ -7,17 +10,21 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example-dev'
|
||||
// url: '127.0.0.1:1234'
|
||||
room: 'Textarea-example',
|
||||
// url: '//localhost:1234',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
// options: { transports: ['websocket'], upgrade: false }
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
textarea: 'Text' // y.share.textarea is of type Y.Text
|
||||
}
|
||||
textarea: 'Text'
|
||||
},
|
||||
timeout: 5000 // reject if no connection was established within 5 seconds
|
||||
}).then(function (y) {
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
y.share.textarea.bind(document.getElementById('textfield'))
|
||||
// thats it..
|
||||
}).catch(() => {
|
||||
console.log('Something went wrong while creating the instance..')
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -7,6 +7,8 @@ Y({
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'Xml-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
|
||||
12
examples/yjs-dist.esm
Normal file
12
examples/yjs-dist.esm
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
import Y from '../src/y.js'
|
||||
import yArray from '../../y-array/src/y-array.js'
|
||||
import yMap from '../../y-map/src/Map.js'
|
||||
import yText from '../../y-text/src/Text.js'
|
||||
import yXml from '../../y-xml/src/y-xml.js'
|
||||
import yMemory from '../../y-memory/src/y-memory.js'
|
||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||
|
||||
Y.extend(yArray, yMap, yText, yXml, yMemory, yWebsocketsClient)
|
||||
|
||||
export default Y
|
||||
594
package-lock.json
generated
594
package-lock.json
generated
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-7",
|
||||
"version": "13.0.0-16",
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
|
||||
"integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "4.0.13",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz",
|
||||
@@ -63,8 +69,19 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz",
|
||||
"integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": 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": {
|
||||
"version": "1.0.9",
|
||||
@@ -90,6 +107,12 @@
|
||||
"integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
|
||||
@@ -130,8 +153,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
|
||||
"integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"babel-cli": {
|
||||
"version": "6.24.1",
|
||||
@@ -499,12 +521,29 @@
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"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": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz",
|
||||
"integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.8",
|
||||
@@ -556,6 +595,20 @@
|
||||
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=",
|
||||
"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": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
|
||||
@@ -586,8 +639,7 @@
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
|
||||
"integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"circular-json": {
|
||||
"version": "0.3.1",
|
||||
@@ -625,6 +677,12 @@
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
|
||||
"integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
|
||||
"dev": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.10.0",
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
|
||||
@@ -725,6 +803,24 @@
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
|
||||
@@ -778,6 +874,18 @@
|
||||
"integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
|
||||
@@ -790,12 +898,36 @@
|
||||
"integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=",
|
||||
"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": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
|
||||
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.7.0.tgz",
|
||||
@@ -844,6 +976,12 @@
|
||||
"integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
@@ -990,12 +1128,24 @@
|
||||
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
|
||||
"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": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||
"integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
|
||||
@@ -1044,6 +1194,12 @@
|
||||
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
|
||||
"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": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
|
||||
@@ -1068,6 +1224,26 @@
|
||||
"integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
@@ -1104,6 +1280,18 @@
|
||||
"integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=",
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz",
|
||||
@@ -1893,6 +2081,24 @@
|
||||
"integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
|
||||
"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": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz",
|
||||
@@ -1905,6 +2111,12 @@
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@@ -1951,8 +2163,7 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
|
||||
"integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "1.1.5",
|
||||
@@ -1960,6 +2171,12 @@
|
||||
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=",
|
||||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz",
|
||||
@@ -2086,6 +2303,12 @@
|
||||
"integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=",
|
||||
"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": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz",
|
||||
@@ -2098,6 +2321,12 @@
|
||||
"integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -2182,6 +2411,20 @@
|
||||
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
@@ -2226,24 +2469,74 @@
|
||||
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
|
||||
"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": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.19.1.tgz",
|
||||
"integrity": "sha1-FNdoATyvLsj96hakmvgvw3fnUgE=",
|
||||
"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": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/matched/-/matched-0.4.4.tgz",
|
||||
"integrity": "sha1-Vte36xgDPwz5vFLrIJD6x9weifo=",
|
||||
"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": {
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
|
||||
"integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
@@ -2262,6 +2555,12 @@
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.8.2.tgz",
|
||||
"integrity": "sha1-eErHc05KRTqcbm6GgKkyknXItoc=",
|
||||
"dev": true
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -2286,6 +2585,18 @@
|
||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
||||
@@ -2322,6 +2633,18 @@
|
||||
"integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -2334,6 +2657,12 @@
|
||||
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
|
||||
"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": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
|
||||
@@ -2396,6 +2725,12 @@
|
||||
"integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=",
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
|
||||
@@ -2420,6 +2755,18 @@
|
||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||
@@ -2506,6 +2853,12 @@
|
||||
"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
|
||||
"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": {
|
||||
"version": "1.1.7",
|
||||
"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": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
@@ -2544,8 +2929,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"readline2": {
|
||||
"version": "1.0.1",
|
||||
@@ -2559,6 +2943,12 @@
|
||||
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
|
||||
"dev": true
|
||||
},
|
||||
"redent": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
|
||||
"integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
|
||||
"dev": true
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz",
|
||||
@@ -2793,6 +3183,26 @@
|
||||
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz",
|
||||
@@ -2803,8 +3213,13 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
|
||||
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": 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": {
|
||||
"version": "0.7.8",
|
||||
@@ -2812,6 +3227,12 @@
|
||||
"integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||
@@ -2842,12 +3263,60 @@
|
||||
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
|
||||
"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": {
|
||||
"version": "10.0.2",
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||
@@ -2880,6 +3361,16 @@
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
@@ -2892,6 +3383,20 @@
|
||||
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
@@ -2972,6 +3477,12 @@
|
||||
"integrity": "sha1-yWPc8DciiS7FnLpWnpQLcZVNFyk=",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
|
||||
@@ -3015,30 +3526,83 @@
|
||||
"integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
|
||||
"integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
|
||||
"integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=",
|
||||
"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": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.2.tgz",
|
||||
"integrity": "sha1-4xbVJXtAuGu0PLjV/qXX9U1rDKE=",
|
||||
"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": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-7",
|
||||
"version": "13.0.0-16",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"scripts": {
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
||||
},
|
||||
@@ -49,6 +51,7 @@
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"chance": "^1.0.9",
|
||||
"concurrently": "^3.4.0",
|
||||
"cutest": "^0.1.9",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-inject": "^2.0.0",
|
||||
@@ -61,6 +64,7 @@
|
||||
"tag-dist-files": "^0.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^2.6.8"
|
||||
"debug": "^2.6.8",
|
||||
"utf-8": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
import multiEntry from 'rollup-plugin-multi-entry'
|
||||
|
||||
export default {
|
||||
entry: 'tests/*.js',
|
||||
moduleName: 'y-array-tests',
|
||||
entry: 'test/y-xml.tests.js',
|
||||
moduleName: 'y-tests',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
@@ -15,6 +15,6 @@ export default {
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
],
|
||||
dest: 'y-array.test.js',
|
||||
dest: 'y.test.js',
|
||||
sourceMap: true
|
||||
}
|
||||
|
||||
406
src/Connector.js
406
src/Connector.js
@@ -1,38 +1,18 @@
|
||||
|
||||
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
||||
function canWrite (auth) { return auth === 'write' }
|
||||
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||
import { sendSyncStep1, computeMessageSyncStep1, computeMessageSyncStep2, computeMessageUpdate } from './MessageHandler.js'
|
||||
|
||||
export default function extendConnector (Y/* :any */) {
|
||||
class AbstractConnector {
|
||||
/* ::
|
||||
y: YConfig;
|
||||
role: SyncRole;
|
||||
connections: Object;
|
||||
isSynced: boolean;
|
||||
userEventListeners: Array<Function>;
|
||||
whenSyncedListeners: Array<Function>;
|
||||
currentSyncTarget: ?UserId;
|
||||
syncingClients: Array<UserId>;
|
||||
forwardToSyncingClients: boolean;
|
||||
debug: boolean;
|
||||
syncStep2: Promise;
|
||||
userId: UserId;
|
||||
send: Function;
|
||||
broadcast: Function;
|
||||
broadcastOpBuffer: Array<Operation>;
|
||||
protocolVersion: number;
|
||||
*/
|
||||
/*
|
||||
opts contains the following information:
|
||||
role : String Role of this client ("master" or "slave")
|
||||
userId : String Uniquely defines the user.
|
||||
debug: Boolean Whether to print debug messages (optional)
|
||||
*/
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
if (opts == null) {
|
||||
opts = {}
|
||||
}
|
||||
this.opts = opts
|
||||
// Prefer to receive untransformed operations. This does only work if
|
||||
// this client receives operations from only one other client.
|
||||
// In particular, this does not work with y-webrtc.
|
||||
@@ -49,56 +29,52 @@ export default function extendConnector (Y/* :any */) {
|
||||
this.logMessage = Y.debug('y:connector-message')
|
||||
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
|
||||
this.role = opts.role
|
||||
this.connections = {}
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.userEventListeners = []
|
||||
this.whenSyncedListeners = []
|
||||
this.currentSyncTarget = null
|
||||
this.syncingClients = []
|
||||
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
||||
this.debug = opts.debug === true
|
||||
this.broadcastOpBuffer = []
|
||||
this.protocolVersion = 11
|
||||
this.authInfo = opts.auth || null
|
||||
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
||||
if (opts.generateUserId !== false) {
|
||||
this.setUserId(Y.utils.generateGuid())
|
||||
}
|
||||
}
|
||||
resetAuth (auth) {
|
||||
if (this.authInfo !== auth) {
|
||||
this.authInfo = auth
|
||||
this.broadcast({
|
||||
type: 'auth',
|
||||
auth: this.authInfo
|
||||
})
|
||||
this.setUserId(Y.utils.generateUserId())
|
||||
}
|
||||
}
|
||||
|
||||
reconnect () {
|
||||
this.log('reconnecting..')
|
||||
return this.y.db.startGarbageCollector()
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
this.log('discronnecting..')
|
||||
this.connections = {}
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.syncingClients = []
|
||||
this.whenSyncedListeners = []
|
||||
this.y.db.stopGarbageCollector()
|
||||
return this.y.db.whenTransactionsFinished()
|
||||
}
|
||||
|
||||
repair () {
|
||||
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
|
||||
for (var name in this.connections) {
|
||||
this.connections[name].isSynced = false
|
||||
}
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
this.connections.forEach((user, userId) => {
|
||||
user.isSynced = false
|
||||
this._syncWithUser(userId)
|
||||
})
|
||||
}
|
||||
|
||||
setUserId (userId) {
|
||||
if (this.userId == null) {
|
||||
if (!Number.isInteger(userId)) {
|
||||
let err = new Error('UserId must be an integer!')
|
||||
this.y.emit('error', err)
|
||||
throw err
|
||||
}
|
||||
this.log('Set userId to "%s"', userId)
|
||||
this.userId = userId
|
||||
return this.y.db.setUserId(userId)
|
||||
@@ -106,23 +82,21 @@ export default function extendConnector (Y/* :any */) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
onUserEvent (f) {
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
|
||||
removeUserEventListener (f) {
|
||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||
}
|
||||
|
||||
userLeft (user) {
|
||||
if (this.connections[user] != null) {
|
||||
this.log('User left: %s', user)
|
||||
delete this.connections[user]
|
||||
if (user === this.currentSyncTarget) {
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
this.syncingClients = this.syncingClients.filter(function (cli) {
|
||||
return cli !== user
|
||||
})
|
||||
if (this.connections.has(user)) {
|
||||
this.log('%s: User left %s', this.userId, user)
|
||||
this.connections.delete(user)
|
||||
// check if isSynced event can be sent now
|
||||
this._setSyncedWith(null)
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userLeft',
|
||||
@@ -131,23 +105,25 @@ export default function extendConnector (Y/* :any */) {
|
||||
}
|
||||
}
|
||||
}
|
||||
userJoined (user, role) {
|
||||
userJoined (user, role, auth) {
|
||||
if (role == null) {
|
||||
throw new Error('You must specify the role of the joined user!')
|
||||
}
|
||||
if (this.connections[user] != null) {
|
||||
if (this.connections.has(user)) {
|
||||
throw new Error('This user already joined!')
|
||||
}
|
||||
this.log('User joined: %s', user)
|
||||
this.connections[user] = {
|
||||
this.log('%s: User joined %s', this.userId, user)
|
||||
this.connections.set(user, {
|
||||
uid: user,
|
||||
isSynced: false,
|
||||
role: role,
|
||||
waitingMessages: [],
|
||||
auth: null
|
||||
}
|
||||
processAfterAuth: [],
|
||||
auth: auth || null,
|
||||
receivedSyncStep2: false
|
||||
})
|
||||
let defer = {}
|
||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
||||
this.connections[user].syncStep2 = defer
|
||||
this.connections.get(user).syncStep2 = defer
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userJoined',
|
||||
@@ -155,9 +131,7 @@ export default function extendConnector (Y/* :any */) {
|
||||
role: role
|
||||
})
|
||||
}
|
||||
if (this.currentSyncTarget == null) {
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
this._syncWithUser(user)
|
||||
}
|
||||
// Execute a function _when_ we are connected.
|
||||
// If not connected, wait until connected
|
||||
@@ -168,61 +142,39 @@ export default function extendConnector (Y/* :any */) {
|
||||
this.whenSyncedListeners.push(f)
|
||||
}
|
||||
}
|
||||
findNextSyncTarget () {
|
||||
if (this.currentSyncTarget != null) {
|
||||
return // "The current sync has not finished!"
|
||||
}
|
||||
|
||||
var syncUser = null
|
||||
for (var uid in this.connections) {
|
||||
if (!this.connections[uid].isSynced) {
|
||||
syncUser = uid
|
||||
break
|
||||
}
|
||||
}
|
||||
var conn = this
|
||||
if (syncUser != null) {
|
||||
this.currentSyncTarget = syncUser
|
||||
this.y.db.requestTransaction(function * () {
|
||||
var stateSet = yield * this.getStateSet()
|
||||
// var deleteSet = yield * this.getDeleteSet()
|
||||
var answer = {
|
||||
type: 'sync step 1',
|
||||
stateSet: stateSet,
|
||||
// deleteSet: deleteSet,
|
||||
protocolVersion: conn.protocolVersion,
|
||||
auth: conn.authInfo
|
||||
}
|
||||
if (conn.preferUntransformed && Object.keys(stateSet).length === 0) {
|
||||
answer.preferUntransformed = true
|
||||
}
|
||||
conn.send(syncUser, answer)
|
||||
})
|
||||
} else {
|
||||
if (!conn.isSynced) {
|
||||
this.y.db.requestTransaction(function * () {
|
||||
if (!conn.isSynced) {
|
||||
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
||||
conn.isSynced = true
|
||||
// It is safer to remove this!
|
||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||
// call whensynced listeners
|
||||
for (var f of conn.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
conn.whenSyncedListeners = []
|
||||
}
|
||||
})
|
||||
}
|
||||
_syncWithUser (userid) {
|
||||
if (this.role === 'slave') {
|
||||
return // "The current sync has not finished or this is controlled by a master!"
|
||||
}
|
||||
sendSyncStep1(this, userid)
|
||||
}
|
||||
send (uid, message) {
|
||||
this.log('Send \'%s\' to %s', message.type, uid)
|
||||
this.logMessage('Message: %j', message)
|
||||
_fireIsSyncedListeners () {
|
||||
this.y.db.whenTransactionsFinished().then(() => {
|
||||
if (!this.isSynced) {
|
||||
this.isSynced = true
|
||||
// It is safer to remove this!
|
||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
||||
// call whensynced listeners
|
||||
for (var f of this.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = []
|
||||
}
|
||||
})
|
||||
}
|
||||
broadcast (message) {
|
||||
this.log('Broadcast \'%s\'', message.type)
|
||||
this.logMessage('Message: %j', message)
|
||||
send (uid, buffer) {
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
|
||||
}
|
||||
this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid)
|
||||
this.logMessage('Message: %Y', buffer)
|
||||
}
|
||||
broadcast (buffer) {
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
|
||||
}
|
||||
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
|
||||
this.logMessage('Message: %Y', buffer)
|
||||
}
|
||||
/*
|
||||
Buffer operations, and broadcast them when ready.
|
||||
@@ -234,11 +186,18 @@ export default function extendConnector (Y/* :any */) {
|
||||
var self = this
|
||||
function broadcastOperations () {
|
||||
if (self.broadcastOpBuffer.length > 0) {
|
||||
self.broadcast({
|
||||
type: 'update',
|
||||
ops: self.broadcastOpBuffer
|
||||
})
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(self.opts.room)
|
||||
encoder.writeVarString('update')
|
||||
let ops = self.broadcastOpBuffer
|
||||
self.broadcastOpBuffer = []
|
||||
let length = ops.length
|
||||
encoder.writeUint32(length)
|
||||
for (var i = 0; i < length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
self.broadcast(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
if (this.broadcastOpBuffer.length === 0) {
|
||||
@@ -251,164 +210,81 @@ export default function extendConnector (Y/* :any */) {
|
||||
/*
|
||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||
*/
|
||||
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
||||
receiveMessage (sender, buffer, skipAuth) {
|
||||
skipAuth = skipAuth || false
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
||||
}
|
||||
if (sender === this.userId) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
this.log('Receive \'%s\' from %s', message.type, sender)
|
||||
this.logMessage('Message: %j', message)
|
||||
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
||||
this.log(
|
||||
`You tried to sync with a yjs instance that has a different protocol version
|
||||
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
||||
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
||||
`)
|
||||
this.send(sender, {
|
||||
type: 'sync stop',
|
||||
protocolVersion: this.protocolVersion
|
||||
})
|
||||
return Promise.reject(new Error('Incompatible protocol version'))
|
||||
}
|
||||
if ((message.type === 'sync step 1' || message.type === 'sync step 2') && this.connections[sender] != null && this.connections[sender].auth == null) {
|
||||
// authenticate using auth in message
|
||||
var auth = this.checkAuth(message.auth, this.y)
|
||||
this.connections[sender].auth = auth
|
||||
auth.then(auth => {
|
||||
// in case operations were received before sender was received
|
||||
// we apply the messages after authentication
|
||||
this.connections[sender].syncStep2.promise.then(() => {
|
||||
// we do it after sync step 1
|
||||
this.connections[sender].waitingMessages.forEach(msg => {
|
||||
this.receiveMessage(sender, msg)
|
||||
})
|
||||
this.connections[sender].waitingMessages = null
|
||||
})
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userAuthenticated',
|
||||
user: sender,
|
||||
auth: auth
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.connections[sender] != null && this.connections[sender].auth != null) {
|
||||
return this.connections[sender].auth.then((auth) => {
|
||||
if (message.type === 'sync step 1' && canRead(auth)) {
|
||||
let conn = this
|
||||
let m = message
|
||||
let wait // wait for sync step 2 to complete
|
||||
if (this.role === 'slave') {
|
||||
wait = Promise.all(Object.keys(this.connections)
|
||||
.map(uid => this.connections[uid])
|
||||
.filter(conn => conn.role === 'master')
|
||||
.map(conn => conn.syncStep2.promise)
|
||||
)
|
||||
} else {
|
||||
wait = Promise.resolve()
|
||||
}
|
||||
wait.then(() => {
|
||||
this.y.db.requestTransaction(function * () {
|
||||
var currentStateSet = yield * this.getStateSet()
|
||||
// TODO: remove
|
||||
// if (canWrite(auth)) {
|
||||
// yield * this.applyDeleteSet(m.deleteSet)
|
||||
// }
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
let encoder = new BinaryEncoder()
|
||||
let roomname = decoder.readVarString() // read room name
|
||||
encoder.writeVarString(roomname)
|
||||
let messageType = decoder.readVarString()
|
||||
let senderConn = this.connections.get(sender)
|
||||
|
||||
var ds = yield * this.getDeleteSet()
|
||||
var answer = {
|
||||
type: 'sync step 2',
|
||||
stateSet: currentStateSet,
|
||||
deleteSet: ds,
|
||||
protocolVersion: this.protocolVersion,
|
||||
auth: this.authInfo
|
||||
}
|
||||
if (message.preferUntransformed === true && Object.keys(m.stateSet).length === 0) {
|
||||
answer.osUntransformed = yield * this.getOperationsUntransformed()
|
||||
} else {
|
||||
answer.os = yield * this.getOperations(m.stateSet)
|
||||
}
|
||||
conn.send(sender, answer)
|
||||
if (this.forwardToSyncingClients) {
|
||||
conn.syncingClients.push(sender)
|
||||
setTimeout(function () {
|
||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
||||
return cli !== sender
|
||||
})
|
||||
conn.send(sender, {
|
||||
type: 'sync done'
|
||||
})
|
||||
}, 5000) // TODO: conn.syncingClientDuration)
|
||||
} else {
|
||||
conn.send(sender, {
|
||||
type: 'sync done'
|
||||
})
|
||||
}
|
||||
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
|
||||
this.logMessage('Message: %Y', buffer)
|
||||
|
||||
if (senderConn == null && !skipAuth) {
|
||||
throw new Error('Received message from unknown peer!')
|
||||
}
|
||||
|
||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||
let auth = decoder.readVarUint()
|
||||
if (senderConn.auth == null) {
|
||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
||||
// check auth
|
||||
return this.checkAuth(auth, this.y, sender).then(authPermissions => {
|
||||
if (senderConn.auth == null) {
|
||||
senderConn.auth = authPermissions
|
||||
this.y.emit('userAuthenticated', {
|
||||
user: senderConn.uid,
|
||||
auth: authPermissions
|
||||
})
|
||||
})
|
||||
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
||||
var db = this.y.db
|
||||
let defer = this.connections[sender].syncStep2
|
||||
let m = message
|
||||
// apply operations first
|
||||
db.requestTransaction(function * () {
|
||||
// yield * this.applyDeleteSet(m.deleteSet)
|
||||
if (m.osUntransformed != null) {
|
||||
yield * this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
|
||||
} else {
|
||||
this.store.apply(m.os)
|
||||
}
|
||||
// defer.resolve()
|
||||
})
|
||||
// then apply ds
|
||||
db.whenTransactionsFinished().then(() => {
|
||||
db.requestTransaction(function * () {
|
||||
yield * this.applyDeleteSet(m.deleteSet)
|
||||
})
|
||||
defer.resolve()
|
||||
})
|
||||
return defer.promise
|
||||
} else if (message.type === 'sync done') {
|
||||
var self = this
|
||||
this.connections[sender].syncStep2.promise.then(function () {
|
||||
self._setSyncedWith(sender)
|
||||
})
|
||||
} else if (message.type === 'update' && canWrite(auth)) {
|
||||
if (this.forwardToSyncingClients) {
|
||||
for (var client of this.syncingClients) {
|
||||
this.send(client, message)
|
||||
}
|
||||
}
|
||||
if (this.y.db.forwardAppliedOperations) {
|
||||
var delops = message.ops.filter(function (o) {
|
||||
return o.struct === 'Delete'
|
||||
})
|
||||
if (delops.length > 0) {
|
||||
this.broadcastOps(delops)
|
||||
}
|
||||
}
|
||||
this.y.db.apply(message.ops)
|
||||
}
|
||||
})
|
||||
} else if (this.connections[sender] != null) {
|
||||
// wait for authentication
|
||||
let senderConn = this.connections[sender]
|
||||
senderConn.waitingMessages = senderConn.waitingMessages || []
|
||||
senderConn.waitingMessages.push(message)
|
||||
let messages = senderConn.processAfterAuth
|
||||
senderConn.processAfterAuth = []
|
||||
|
||||
return messages.reduce((p, m) =>
|
||||
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
|
||||
, Promise.resolve())
|
||||
})
|
||||
}
|
||||
}
|
||||
if (skipAuth || senderConn.auth != null) {
|
||||
return this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||
} else {
|
||||
return Promise.reject(new Error('Unknown user - Unable to deliver message'))
|
||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender, false])
|
||||
}
|
||||
}
|
||||
|
||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
|
||||
return this.y.db.whenTransactionsFinished()
|
||||
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
|
||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
||||
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
|
||||
} else {
|
||||
return Promise.reject(new Error('Unable to receive message'))
|
||||
}
|
||||
}
|
||||
|
||||
_setSyncedWith (user) {
|
||||
var conn = this.connections[user]
|
||||
if (conn != null) {
|
||||
conn.isSynced = true
|
||||
if (user != null) {
|
||||
this.connections.get(user).isSynced = true
|
||||
}
|
||||
if (user === this.currentSyncTarget) {
|
||||
this.currentSyncTarget = null
|
||||
this.findNextSyncTarget()
|
||||
let conns = Array.from(this.connections.values())
|
||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||
this._fireIsSyncedListeners()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
/* global getRandom, async */
|
||||
'use strict'
|
||||
|
||||
module.exports = function (Y) {
|
||||
var globalRoom = {
|
||||
users: {},
|
||||
buffers: {},
|
||||
removeUser: function (user) {
|
||||
for (var i in this.users) {
|
||||
this.users[i].userLeft(user)
|
||||
}
|
||||
delete this.users[user]
|
||||
delete this.buffers[user]
|
||||
},
|
||||
addUser: function (connector) {
|
||||
this.users[connector.userId] = connector
|
||||
this.buffers[connector.userId] = {}
|
||||
for (var uname in this.users) {
|
||||
if (uname !== connector.userId) {
|
||||
var u = this.users[uname]
|
||||
u.userJoined(connector.userId, 'master')
|
||||
connector.userJoined(u.userId, 'master')
|
||||
}
|
||||
}
|
||||
},
|
||||
whenTransactionsFinished: function () {
|
||||
var self = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
// The connector first has to send the messages to the db.
|
||||
// Wait for the checkAuth-function to resolve
|
||||
// The test lib only has a simple checkAuth function: `() => Promise.resolve()`
|
||||
// Just add a function to the event-queue, in order to wait for the event.
|
||||
// TODO: this may be buggy in test applications (but it isn't be for real-life apps)
|
||||
setTimeout(function () {
|
||||
var ps = []
|
||||
for (var name in self.users) {
|
||||
ps.push(self.users[name].y.db.whenTransactionsFinished())
|
||||
}
|
||||
Promise.all(ps).then(resolve, reject)
|
||||
}, 10)
|
||||
})
|
||||
},
|
||||
flushOne: function flushOne () {
|
||||
var bufs = []
|
||||
for (var receiver in globalRoom.buffers) {
|
||||
let buff = globalRoom.buffers[receiver]
|
||||
var push = false
|
||||
for (let sender in buff) {
|
||||
if (buff[sender].length > 0) {
|
||||
push = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (push) {
|
||||
bufs.push(receiver)
|
||||
}
|
||||
}
|
||||
if (bufs.length > 0) {
|
||||
var userId = getRandom(bufs)
|
||||
let buff = globalRoom.buffers[userId]
|
||||
let sender = getRandom(Object.keys(buff))
|
||||
var m = buff[sender].shift()
|
||||
if (buff[sender].length === 0) {
|
||||
delete buff[sender]
|
||||
}
|
||||
var user = globalRoom.users[userId]
|
||||
return user.receiveMessage(m[0], m[1]).then(function () {
|
||||
return user.y.db.whenTransactionsFinished()
|
||||
}, function () {})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
flushAll: function () {
|
||||
return new Promise(function (resolve) {
|
||||
// flushes may result in more created operations,
|
||||
// flush until there is nothing more to flush
|
||||
function nextFlush () {
|
||||
var c = globalRoom.flushOne()
|
||||
if (c) {
|
||||
while (c) {
|
||||
c = globalRoom.flushOne()
|
||||
}
|
||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||
} else {
|
||||
c = globalRoom.flushOne()
|
||||
if (c) {
|
||||
c.then(function () {
|
||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||
})
|
||||
}
|
||||
}
|
||||
Y.utils.globalRoom = globalRoom
|
||||
|
||||
var userIdCounter = 0
|
||||
|
||||
class Test extends Y.AbstractConnector {
|
||||
constructor (y, options) {
|
||||
if (options === undefined) {
|
||||
throw new Error('Options must not be undefined!')
|
||||
}
|
||||
options.role = 'master'
|
||||
options.forwardToSyncingClients = false
|
||||
super(y, options)
|
||||
this.setUserId((userIdCounter++) + '').then(() => {
|
||||
globalRoom.addUser(this)
|
||||
})
|
||||
this.globalRoom = globalRoom
|
||||
this.syncingClientDuration = 0
|
||||
}
|
||||
receiveMessage (sender, m) {
|
||||
return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
||||
}
|
||||
send (userId, message) {
|
||||
var buffer = globalRoom.buffers[userId]
|
||||
if (buffer != null) {
|
||||
if (buffer[this.userId] == null) {
|
||||
buffer[this.userId] = []
|
||||
}
|
||||
buffer[this.userId].push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||
}
|
||||
}
|
||||
broadcast (message) {
|
||||
for (var key in globalRoom.buffers) {
|
||||
var buff = globalRoom.buffers[key]
|
||||
if (buff[this.userId] == null) {
|
||||
buff[this.userId] = []
|
||||
}
|
||||
buff[this.userId].push(JSON.parse(JSON.stringify([this.userId, message])))
|
||||
}
|
||||
}
|
||||
isDisconnected () {
|
||||
return globalRoom.users[this.userId] == null
|
||||
}
|
||||
reconnect () {
|
||||
if (this.isDisconnected()) {
|
||||
globalRoom.addUser(this)
|
||||
super.reconnect()
|
||||
}
|
||||
return Y.utils.globalRoom.flushAll()
|
||||
}
|
||||
disconnect () {
|
||||
var waitForMe = Promise.resolve()
|
||||
if (!this.isDisconnected()) {
|
||||
globalRoom.removeUser(this.userId)
|
||||
waitForMe = super.disconnect()
|
||||
}
|
||||
var self = this
|
||||
return waitForMe.then(function () {
|
||||
return self.y.db.whenTransactionsFinished()
|
||||
})
|
||||
}
|
||||
flush () {
|
||||
var self = this
|
||||
return async(function * () {
|
||||
var buff = globalRoom.buffers[self.userId]
|
||||
while (Object.keys(buff).length > 0) {
|
||||
var sender = getRandom(Object.keys(buff))
|
||||
var m = buff[sender].shift()
|
||||
if (buff[sender].length === 0) {
|
||||
delete buff[sender]
|
||||
}
|
||||
yield this.receiveMessage(m[0], m[1])
|
||||
}
|
||||
yield self.whenTransactionsFinished()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Y.Test = Test
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export default function extendDatabase (Y /* :any */) {
|
||||
startGarbageCollector () {
|
||||
this.gc = this.dbOpts.gc
|
||||
if (this.gc) {
|
||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
|
||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 30000 : this.dbOpts.gcTimeout
|
||||
} else {
|
||||
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 was deleted, apply a delete operation after op was applied
|
||||
*/
|
||||
apply (ops) {
|
||||
applyOperations (decoder) {
|
||||
this.opsReceivedTimestamp = new Date()
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
var o = ops[i]
|
||||
let length = decoder.readUint32()
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
let o = Y.Struct.binaryDecodeOperation(decoder)
|
||||
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
||||
var required = Y.Struct[o.struct].requiredOps(o)
|
||||
if (o.requires != null) {
|
||||
@@ -586,11 +588,11 @@ export default function extendDatabase (Y /* :any */) {
|
||||
createType (typedefinition, id) {
|
||||
var structname = typedefinition[0].struct
|
||||
id = id || this.getNextOpId(1)
|
||||
var op = Y.Struct[structname].create(id)
|
||||
var op = Y.Struct[structname].create(id, typedefinition[1])
|
||||
op.type = typedefinition[0].name
|
||||
|
||||
this.requestTransaction(function * () {
|
||||
if (op.id[0] === '_') {
|
||||
if (op.id[0] === 0xFFFFFF) {
|
||||
yield * this.setOperation(op)
|
||||
} else {
|
||||
yield * this.applyCreatedOperations([op])
|
||||
|
||||
152
src/Encoding.js
Normal file
152
src/Encoding.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import utf8 from 'utf-8'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
export class BinaryEncoder {
|
||||
constructor () {
|
||||
this.data = []
|
||||
}
|
||||
|
||||
get pos () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
createBuffer () {
|
||||
return Uint8Array.from(this.data).buffer
|
||||
}
|
||||
|
||||
writeUint8 (num) {
|
||||
this.data.push(num & bits8)
|
||||
}
|
||||
|
||||
setUint8 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
}
|
||||
|
||||
writeUint16 (num) {
|
||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
setUint16 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
this.data[pos + 1] = (num >>> 8) & bits8
|
||||
}
|
||||
|
||||
writeUint32 (num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data.push(num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
setUint32 (pos, num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data[pos + i] = num & bits8
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
writeVarUint (num) {
|
||||
while (num >= 0b10000000) {
|
||||
this.data.push(0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
this.data.push(bits7 & num)
|
||||
}
|
||||
|
||||
writeVarString (str) {
|
||||
let bytes = utf8.setBytesFromString(str)
|
||||
let len = bytes.length
|
||||
this.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
this.data.push(bytes[i])
|
||||
}
|
||||
}
|
||||
|
||||
writeOpID (id) {
|
||||
let user = id[0]
|
||||
this.writeVarUint(user)
|
||||
if (user !== 0xFFFFFF) {
|
||||
this.writeVarUint(id[1])
|
||||
} else {
|
||||
this.writeVarString(id[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BinaryDecoder {
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.uint8arr = new Uint8Array(buffer)
|
||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
||||
this.uint8arr = buffer
|
||||
} else {
|
||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||
}
|
||||
this.pos = 0
|
||||
}
|
||||
|
||||
skip8 () {
|
||||
this.pos++
|
||||
}
|
||||
|
||||
readUint8 () {
|
||||
return this.uint8arr[this.pos++]
|
||||
}
|
||||
|
||||
readUint32 () {
|
||||
let uint =
|
||||
this.uint8arr[this.pos] +
|
||||
(this.uint8arr[this.pos + 1] << 8) +
|
||||
(this.uint8arr[this.pos + 2] << 16) +
|
||||
(this.uint8arr[this.pos + 3] << 24)
|
||||
this.pos += 4
|
||||
return uint
|
||||
}
|
||||
|
||||
peekUint8 () {
|
||||
return this.uint8arr[this.pos]
|
||||
}
|
||||
|
||||
readVarUint () {
|
||||
let num = 0
|
||||
let len = 0
|
||||
while (true) {
|
||||
let r = this.uint8arr[this.pos++]
|
||||
num = num | ((r & bits7) << len)
|
||||
len += 7
|
||||
if (r < 1 << 7) {
|
||||
return num >>> 0 // return unsigned number!
|
||||
}
|
||||
if (len > 35) {
|
||||
throw new Error('Integer out of range!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readVarString () {
|
||||
let len = this.readVarUint()
|
||||
let bytes = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = this.uint8arr[this.pos++]
|
||||
}
|
||||
return utf8.getStringFromBytes(bytes)
|
||||
}
|
||||
|
||||
peekVarString () {
|
||||
let pos = this.pos
|
||||
let s = this.readVarString()
|
||||
this.pos = pos
|
||||
return s
|
||||
}
|
||||
|
||||
readOpID () {
|
||||
let user = this.readVarUint()
|
||||
if (user !== 0xFFFFFF) {
|
||||
return [user, this.readVarUint()]
|
||||
} else {
|
||||
return [user, this.readVarString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
193
src/MessageHandler.js
Normal file
193
src/MessageHandler.js
Normal file
@@ -0,0 +1,193 @@
|
||||
|
||||
import Y from './y.js'
|
||||
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
|
||||
|
||||
export function formatYjsMessage (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // read roomname
|
||||
let type = decoder.readVarString()
|
||||
let strBuilder = []
|
||||
strBuilder.push('\n === ' + type + ' ===\n')
|
||||
if (type === 'update') {
|
||||
logMessageUpdate(decoder, strBuilder)
|
||||
} else if (type === 'sync step 1') {
|
||||
logMessageSyncStep1(decoder, strBuilder)
|
||||
} else if (type === 'sync step 2') {
|
||||
logMessageSyncStep2(decoder, strBuilder)
|
||||
} else {
|
||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
||||
}
|
||||
return strBuilder.join('')
|
||||
}
|
||||
|
||||
export function formatYjsMessageType (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // roomname
|
||||
return decoder.readVarString()
|
||||
}
|
||||
|
||||
export function logMessageUpdate (decoder, strBuilder) {
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
export function computeMessageUpdate (decoder, encoder, conn) {
|
||||
if (conn.y.db.forwardAppliedOperations || conn.y.persistence != null) {
|
||||
let messagePosition = decoder.pos
|
||||
let len = decoder.readUint32()
|
||||
let delops = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||
if (op.struct === 'Delete') {
|
||||
delops.push(op)
|
||||
}
|
||||
}
|
||||
if (delops.length > 0) {
|
||||
if (conn.y.db.forwardAppliedOperations) {
|
||||
conn.broadcastOps(delops)
|
||||
}
|
||||
if (conn.y.persistence) {
|
||||
conn.y.persistence.saveOperations(delops)
|
||||
}
|
||||
}
|
||||
decoder.pos = messagePosition
|
||||
}
|
||||
conn.y.db.applyOperations(decoder)
|
||||
}
|
||||
|
||||
export function sendSyncStep1 (conn, syncUser) {
|
||||
conn.y.db.requestTransaction(function * () {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(conn.opts.room || '')
|
||||
encoder.writeVarString('sync step 1')
|
||||
encoder.writeVarString(conn.authInfo || '')
|
||||
encoder.writeVarUint(conn.protocolVersion)
|
||||
let preferUntransformed = conn.preferUntransformed && this.os.length === 0 // TODO: length may not be defined
|
||||
encoder.writeUint8(preferUntransformed ? 1 : 0)
|
||||
yield * this.writeStateSet(encoder)
|
||||
conn.send(syncUser, encoder.createBuffer())
|
||||
})
|
||||
}
|
||||
|
||||
export function logMessageSyncStep1 (decoder, strBuilder) {
|
||||
let auth = decoder.readVarString()
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
let preferUntransformed = decoder.readUint8() === 1
|
||||
strBuilder.push(`
|
||||
- auth: "${auth}"
|
||||
- protocolVersion: ${protocolVersion}
|
||||
- preferUntransformed: ${preferUntransformed}
|
||||
`)
|
||||
logSS(decoder, strBuilder)
|
||||
}
|
||||
|
||||
export function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
let preferUntransformed = decoder.readUint8() === 1
|
||||
|
||||
// check protocol version
|
||||
if (protocolVersion !== conn.protocolVersion) {
|
||||
console.warn(
|
||||
`You tried to sync with a yjs instance that has a different protocol version
|
||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
||||
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
||||
`)
|
||||
conn.y.destroy()
|
||||
}
|
||||
|
||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
||||
// send sync step 2
|
||||
conn.y.db.requestTransaction(function * () {
|
||||
encoder.writeVarString('sync step 2')
|
||||
encoder.writeVarString(conn.authInfo || '')
|
||||
|
||||
if (preferUntransformed) {
|
||||
encoder.writeUint8(1)
|
||||
yield * this.writeOperationsUntransformed(encoder)
|
||||
} else {
|
||||
encoder.writeUint8(0)
|
||||
yield * this.writeOperations(encoder, decoder)
|
||||
}
|
||||
|
||||
yield * this.writeDeleteSet(encoder)
|
||||
conn.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
})
|
||||
return conn.y.db.whenTransactionsFinished().then(() => {
|
||||
if (conn.role === 'slave') {
|
||||
sendSyncStep1(conn, sender)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function logSS (decoder, strBuilder) {
|
||||
strBuilder.push(' == SS: \n')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
strBuilder.push(` ${user}: ${clock}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
export function logOS (decoder, strBuilder) {
|
||||
strBuilder.push(' == OS: \n')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||
strBuilder.push(JSON.stringify(op) + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
export function logDS (decoder, strBuilder) {
|
||||
strBuilder.push(' == DS: \n')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(` User: ${user}: `)
|
||||
let len2 = decoder.readVarUint()
|
||||
for (let j = 0; j < len2; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let to = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function logMessageSyncStep2 (decoder, strBuilder) {
|
||||
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n')
|
||||
let osTransformed = decoder.readUint8() === 1
|
||||
strBuilder.push(' - osUntransformed: ' + osTransformed + '\n')
|
||||
logOS(decoder, strBuilder)
|
||||
if (osTransformed) {
|
||||
logSS(decoder, strBuilder)
|
||||
}
|
||||
logDS(decoder, strBuilder)
|
||||
}
|
||||
|
||||
export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
|
||||
var db = conn.y.db
|
||||
let defer = senderConn.syncStep2
|
||||
|
||||
// apply operations first
|
||||
db.requestTransaction(function * () {
|
||||
let osUntransformed = decoder.readUint8()
|
||||
if (osUntransformed === 1) {
|
||||
yield * this.applyOperationsUntransformed(decoder)
|
||||
} else {
|
||||
this.store.applyOperations(decoder)
|
||||
}
|
||||
})
|
||||
// then apply ds
|
||||
db.requestTransaction(function * () {
|
||||
yield * this.applyDeleteSet(decoder)
|
||||
})
|
||||
return db.whenTransactionsFinished().then(() => {
|
||||
conn._setSyncedWith(sender)
|
||||
defer.resolve()
|
||||
})
|
||||
}
|
||||
43
src/Persistence.js
Normal file
43
src/Persistence.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { BinaryEncoder } from './Encoding.js'
|
||||
|
||||
export default function extendPersistence (Y) {
|
||||
class AbstractPersistence {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
this.opts = opts
|
||||
this.saveOperationsBuffer = []
|
||||
this.log = Y.debug('y:persistence')
|
||||
}
|
||||
saveToMessageQueue (binary) {
|
||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
||||
}
|
||||
saveOperations (ops) {
|
||||
ops = ops.map(function (op) {
|
||||
return Y.Struct[op.struct].encode(op)
|
||||
})
|
||||
const saveOperations = () => {
|
||||
if (this.saveOperationsBuffer.length > 0) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(this.opts.room)
|
||||
encoder.writeVarString('update')
|
||||
let ops = this.saveOperationsBuffer
|
||||
this.saveOperationsBuffer = []
|
||||
let length = ops.length
|
||||
encoder.writeUint32(length)
|
||||
for (var i = 0; i < length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
this.saveToMessageQueue(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
if (this.saveOperationsBuffer.length === 0) {
|
||||
this.saveOperationsBuffer = ops
|
||||
this.y.db.whenTransactionsFinished().then(saveOperations)
|
||||
} else {
|
||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
Y.AbstractPersistence = AbstractPersistence
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
/* eslint-env browser, jasmine */
|
||||
|
||||
/*
|
||||
This is just a compilation of functions that help to test this library!
|
||||
*/
|
||||
|
||||
// When testing, you store everything on the global object. We call it g
|
||||
|
||||
var Y = require('./y.js')
|
||||
require('../../y-memory/src/Memory.js')(Y)
|
||||
require('../../y-array/src/Array.js')(Y)
|
||||
require('../../y-map/src/Map.js')(Y)
|
||||
require('../../y-indexeddb/src/IndexedDB.js')(Y)
|
||||
|
||||
module.exports = Y
|
||||
|
||||
var g
|
||||
if (typeof global !== 'undefined') {
|
||||
g = global
|
||||
} else if (typeof window !== 'undefined') {
|
||||
g = window
|
||||
} else {
|
||||
throw new Error('No global object?')
|
||||
}
|
||||
g.g = g
|
||||
|
||||
// Helper methods for the random number generator
|
||||
Math.seedrandom = require('seedrandom')
|
||||
|
||||
g.generateRandomSeed = function generateRandomSeed () {
|
||||
var seed
|
||||
if (typeof window !== 'undefined' && window.location.hash.length > 1) {
|
||||
seed = window.location.hash.slice(1) // first character is the hash!
|
||||
console.warn('Using random seed that was specified in the url!')
|
||||
} else {
|
||||
seed = JSON.stringify(Math.random())
|
||||
}
|
||||
console.info('Using random seed: ' + seed)
|
||||
g.setRandomSeed(seed)
|
||||
}
|
||||
|
||||
g.setRandomSeed = function setRandomSeed (seed) {
|
||||
Math.seedrandom.currentSeed = seed
|
||||
Math.seedrandom(Math.seedrandom.currentSeed, { global: true })
|
||||
}
|
||||
|
||||
g.generateRandomSeed()
|
||||
|
||||
g.YConcurrencyTestingMode = true
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
|
||||
|
||||
g.describeManyTimes = function describeManyTimes (times, name, f) {
|
||||
for (var i = 0; i < times; i++) {
|
||||
describe(name, f)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Wait for a specified amount of time (in ms). defaults to 5ms
|
||||
*/
|
||||
function wait (t) {
|
||||
if (t == null) {
|
||||
t = 0
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(function () {
|
||||
resolve()
|
||||
}, t)
|
||||
})
|
||||
}
|
||||
g.wait = wait
|
||||
|
||||
g.databases = ['memory']
|
||||
if (typeof window !== 'undefined') {
|
||||
g.databases.push('indexeddb')
|
||||
} else {
|
||||
g.databases.push('leveldb')
|
||||
}
|
||||
/*
|
||||
returns a random element of o.
|
||||
works on Object, and Array
|
||||
*/
|
||||
function getRandom (o) {
|
||||
if (o instanceof Array) {
|
||||
return o[Math.floor(Math.random() * o.length)]
|
||||
} else if (o.constructor === Object) {
|
||||
return o[getRandom(Object.keys(o))]
|
||||
}
|
||||
}
|
||||
g.getRandom = getRandom
|
||||
|
||||
function getRandomNumber (n) {
|
||||
if (n == null) {
|
||||
n = 9999
|
||||
}
|
||||
return Math.floor(Math.random() * n)
|
||||
}
|
||||
g.getRandomNumber = getRandomNumber
|
||||
|
||||
function getRandomString () {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyzäüöABCDEFGHIJKLMNOPQRSTUVWXYZÄÜÖ'
|
||||
var char = chars[getRandomNumber(chars.length)] // ü\n\n\n\n\n\n\n'
|
||||
var length = getRandomNumber(7)
|
||||
var string = ''
|
||||
for (var i = 0; i < length; i++) {
|
||||
string += char
|
||||
}
|
||||
return string
|
||||
}
|
||||
g.getRandomString = getRandomString
|
||||
|
||||
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) {
|
||||
g.generateRandomSeed() // create a new seed, so we can re-create the behavior
|
||||
for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
|
||||
var r = Math.random()
|
||||
if (r > 0.95) {
|
||||
// 10% chance of toggling concurrent user interactions.
|
||||
// There will be an artificial delay until ops can be executed by the type,
|
||||
// therefore, operations of the database will be (pre)transformed until user operations arrive
|
||||
yield (function simulateConcurrentUserInteractions (type) {
|
||||
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
|
||||
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
|
||||
type = type.y
|
||||
}
|
||||
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
|
||||
type.eventHandler.awaiting = 1
|
||||
type.eventHandler._debuggingAwaiting = true
|
||||
} else {
|
||||
// fixAwaitingInType will handle _debuggingAwaiting
|
||||
return fixAwaitingInType(type)
|
||||
}
|
||||
})(getRandom(objects))
|
||||
} else if (r >= 0.5) {
|
||||
// 40% chance to flush
|
||||
yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0)
|
||||
} else if (noReconnect || r >= 0.05) {
|
||||
// 45% chance to create operation
|
||||
var done = getRandom(transactions)(getRandom(objects))
|
||||
if (done != null) {
|
||||
yield done
|
||||
} else {
|
||||
yield wait()
|
||||
}
|
||||
yield Y.utils.globalRoom.whenTransactionsFinished()
|
||||
} else {
|
||||
// 5% chance to disconnect/reconnect
|
||||
var u = getRandom(users)
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
if (u.connector.isDisconnected()) {
|
||||
yield u.reconnect()
|
||||
} else {
|
||||
yield u.disconnect()
|
||||
}
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fixAwaitingInType (type) {
|
||||
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
|
||||
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
|
||||
type = type.y
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
type.os.whenTransactionsFinished().then(function () {
|
||||
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
|
||||
type.os.requestTransaction(function * () {
|
||||
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
|
||||
type.eventHandler._debuggingAwaiting = false
|
||||
yield * type.eventHandler.awaitOps(this, function * () { /* mock function */ })
|
||||
}
|
||||
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
g.fixAwaitingInType = fixAwaitingInType
|
||||
|
||||
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||
yield * applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
})
|
||||
|
||||
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||
yield * applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
for (var u in users) {
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield users[u].reconnect()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
}
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield g.garbageCollectAllUsers(users)
|
||||
})
|
||||
|
||||
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||
yield * applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
for (var u in users) {
|
||||
// TODO: here, we enforce that two users never sync at the same time with u[0]
|
||||
// enforce that in the connector itself!
|
||||
yield users[u].reconnect()
|
||||
}
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield g.garbageCollectAllUsers(users)
|
||||
})
|
||||
|
||||
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
for (var i in users) {
|
||||
yield users[i].db.emptyGarbageCollector()
|
||||
}
|
||||
})
|
||||
|
||||
g.compareAllUsers = async(function * compareAllUsers (users) {
|
||||
var s1, s2 // state sets
|
||||
var ds1, ds2 // delete sets
|
||||
var allDels1, allDels2 // all deletions
|
||||
var db1 = [] // operation store of user1
|
||||
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield g.garbageCollectAllUsers(users)
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
|
||||
// disconnect, then reconnect all users
|
||||
// We do this to make sure that the gc is updated by everyone
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
yield users[i].disconnect()
|
||||
yield wait()
|
||||
yield users[i].reconnect()
|
||||
}
|
||||
yield wait()
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
|
||||
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
||||
function * t1 () {
|
||||
s1 = yield * this.getStateSet()
|
||||
ds1 = yield * this.getDeleteSet()
|
||||
allDels1 = []
|
||||
yield * this.ds.iterate(this, null, null, function * (d) {
|
||||
allDels1.push(d)
|
||||
})
|
||||
}
|
||||
function * t2 () {
|
||||
s2 = yield * this.getStateSet()
|
||||
ds2 = yield * this.getDeleteSet()
|
||||
allDels2 = []
|
||||
yield * this.ds.iterate(this, null, null, function * (d) {
|
||||
allDels2.push(d)
|
||||
})
|
||||
}
|
||||
|
||||
var buffer = Y.utils.globalRoom.buffers
|
||||
for (var name in buffer) {
|
||||
if (buffer[name].length > 0) {
|
||||
// not all ops were transmitted..
|
||||
debugger // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
for (var uid = 0; uid < users.length; uid++) {
|
||||
var u = users[uid]
|
||||
u.db.requestTransaction(function * () {
|
||||
var sv = yield * this.getStateVector()
|
||||
for (var s of sv) {
|
||||
yield * this.updateState(s.user)
|
||||
}
|
||||
// compare deleted ops against deleteStore
|
||||
yield * this.os.iterate(this, null, null, function * (o) {
|
||||
if (o.deleted === true) {
|
||||
expect(yield * this.isDeleted(o.id)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
// compare deleteStore against deleted ops
|
||||
var ds = []
|
||||
yield * this.ds.iterate(this, null, null, function * (d) {
|
||||
ds.push(d)
|
||||
})
|
||||
for (var j in ds) {
|
||||
var d = ds[j]
|
||||
for (var i = 0; i < d.len; i++) {
|
||||
var o = yield * this.getInsertion([d.id[0], d.id[1] + i])
|
||||
// gc'd or deleted
|
||||
if (d.gc) {
|
||||
expect(o).toBeFalsy()
|
||||
} else {
|
||||
expect(o.deleted).toBeTruthy()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// compare allDels tree
|
||||
if (s1 == null) {
|
||||
u.db.requestTransaction(function * () {
|
||||
yield * t1.call(this)
|
||||
yield * this.os.iterate(this, null, null, function * (o) {
|
||||
o = Y.utils.copyObject(o)
|
||||
delete o.origin
|
||||
delete o.originOf
|
||||
db1.push(o)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
u.db.requestTransaction(function * () {
|
||||
yield * t2.call(this)
|
||||
var db2 = []
|
||||
yield * this.os.iterate(this, null, null, function * (o) {
|
||||
o = Y.utils.copyObject(o)
|
||||
delete o.origin
|
||||
delete o.originOf
|
||||
db2.push(o)
|
||||
})
|
||||
expect(s1).toEqual(s2)
|
||||
expect(allDels1).toEqual(allDels2) // inner structure
|
||||
expect(ds1).toEqual(ds2) // exported structure
|
||||
db2.forEach((o, i) => {
|
||||
expect(db1[i]).toEqual(o)
|
||||
})
|
||||
})
|
||||
}
|
||||
yield u.db.whenTransactionsFinished()
|
||||
}
|
||||
})
|
||||
|
||||
g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
|
||||
if (Y.utils.globalRoom.users[0] != null) {
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
}
|
||||
// destroy old users
|
||||
for (var u in Y.utils.globalRoom.users) {
|
||||
Y.utils.globalRoom.users[u].y.destroy()
|
||||
}
|
||||
self.users = null
|
||||
|
||||
var promises = []
|
||||
for (var i = 0; i < numberOfUsers; i++) {
|
||||
promises.push(Y({
|
||||
db: {
|
||||
name: database,
|
||||
namespace: 'User ' + i,
|
||||
cleanStart: true,
|
||||
gcTimeout: -1,
|
||||
gc: true,
|
||||
repairCheckInterval: -1
|
||||
},
|
||||
connector: {
|
||||
name: 'Test',
|
||||
debug: false
|
||||
},
|
||||
share: {
|
||||
root: initType || 'Map'
|
||||
}
|
||||
}))
|
||||
}
|
||||
self.users = yield Promise.all(promises)
|
||||
self.types = self.users.map(function (u) { return u.share.root })
|
||||
return self.users
|
||||
})
|
||||
|
||||
/*
|
||||
Until async/await arrives in js, we use this function to wait for promises
|
||||
by yielding them.
|
||||
*/
|
||||
function async (makeGenerator) {
|
||||
return function (arg) {
|
||||
var generator = makeGenerator.apply(this, arguments)
|
||||
|
||||
function handle (result) {
|
||||
if (result.done) return Promise.resolve(result.value)
|
||||
|
||||
return Promise.resolve(result.value).then(function (res) {
|
||||
return handle(generator.next(res))
|
||||
}, function (err) {
|
||||
return handle(generator.throw(err))
|
||||
})
|
||||
}
|
||||
try {
|
||||
return handle(generator.next())
|
||||
} catch (ex) {
|
||||
generator.throw(ex)
|
||||
// return Promise.reject(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
g.async = async
|
||||
|
||||
function logUsers (self) {
|
||||
if (self.constructor === Array) {
|
||||
self = {users: self}
|
||||
}
|
||||
self.users[0].db.logTable()
|
||||
self.users[1].db.logTable()
|
||||
self.users[2].db.logTable()
|
||||
}
|
||||
|
||||
g.logUsers = logUsers
|
||||
935
src/Struct.js
935
src/Struct.js
@@ -1,5 +1,8 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
const CDELETE = 0
|
||||
const CINSERT = 1
|
||||
const CLIST = 2
|
||||
const CMAP = 3
|
||||
const CXML = 4
|
||||
|
||||
/*
|
||||
An operation also defines the structure of a type. This is why operation and
|
||||
@@ -20,395 +23,597 @@
|
||||
- Operations that are required to execute this operation.
|
||||
*/
|
||||
export default function extendStruct (Y) {
|
||||
var Struct = {
|
||||
/* This is the only operation that is actually not a structure, because
|
||||
it is not stored in the OS. This is why it _does not_ have an id
|
||||
|
||||
op = {
|
||||
target: Id
|
||||
let Struct = {}
|
||||
Y.Struct = Struct
|
||||
Struct.binaryDecodeOperation = function (decoder) {
|
||||
let code = decoder.peekUint8()
|
||||
if (code === CDELETE) {
|
||||
return Struct.Delete.binaryDecode(decoder)
|
||||
} else if (code === CINSERT) {
|
||||
return Struct.Insert.binaryDecode(decoder)
|
||||
} else if (code === CLIST) {
|
||||
return Struct.List.binaryDecode(decoder)
|
||||
} else if (code === CMAP) {
|
||||
return Struct.Map.binaryDecode(decoder)
|
||||
} else if (code === CXML) {
|
||||
return Struct.Xml.binaryDecode(decoder)
|
||||
} else {
|
||||
throw new Error('Unable to decode operation!')
|
||||
}
|
||||
*/
|
||||
Delete: {
|
||||
encode: function (op) {
|
||||
return {
|
||||
target: op.target,
|
||||
length: op.length || 0,
|
||||
struct: 'Delete'
|
||||
}
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function * (op) {
|
||||
return yield * this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
|
||||
/* This is the only operation that is actually not a structure, because
|
||||
it is not stored in the OS. This is why it _does not_ have an id
|
||||
|
||||
op = {
|
||||
target: Id
|
||||
}
|
||||
*/
|
||||
Struct.Delete = {
|
||||
encode: function (op) {
|
||||
return {
|
||||
target: op.target,
|
||||
length: op.length || 0,
|
||||
struct: 'Delete'
|
||||
}
|
||||
},
|
||||
Insert: {
|
||||
/* {
|
||||
content: [any],
|
||||
opContent: Id,
|
||||
id: Id,
|
||||
left: Id,
|
||||
origin: Id,
|
||||
right: Id,
|
||||
parent: Id,
|
||||
parentSub: string (optional), // child of Map type
|
||||
}
|
||||
*/
|
||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||
// TODO: you could not send the "left" property, then you also have to
|
||||
// "op.left = null" in $execute or $decode
|
||||
var e/* :any */ = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
origin: op.origin,
|
||||
parent: op.parent,
|
||||
struct: op.struct
|
||||
}
|
||||
if (op.parentSub != null) {
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
if (op.hasOwnProperty('opContent')) {
|
||||
e.opContent = op.opContent
|
||||
} else {
|
||||
e.content = op.content.slice()
|
||||
}
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CDELETE)
|
||||
encoder.writeOpID(op.target)
|
||||
encoder.writeVarUint(op.length || 0)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
return {
|
||||
target: decoder.readOpID(),
|
||||
length: decoder.readVarUint(),
|
||||
struct: 'Delete'
|
||||
}
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function * (op) {
|
||||
return yield * this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
var ids = []
|
||||
if (op.left != null) {
|
||||
ids.push(op.left)
|
||||
}
|
||||
if (op.right != null) {
|
||||
ids.push(op.right)
|
||||
}
|
||||
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
||||
ids.push(op.origin)
|
||||
}
|
||||
// if (op.right == null && op.left == null) {
|
||||
ids.push(op.parent)
|
||||
/* {
|
||||
content: [any],
|
||||
opContent: Id,
|
||||
id: Id,
|
||||
left: Id,
|
||||
origin: Id,
|
||||
right: Id,
|
||||
parent: Id,
|
||||
parentSub: string (optional), // child of Map type
|
||||
}
|
||||
*/
|
||||
Struct.Insert = {
|
||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||
// TODO: you could not send the "left" property, then you also have to
|
||||
// "op.left = null" in $execute or $decode
|
||||
var e/* :any */ = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
origin: op.origin,
|
||||
parent: op.parent,
|
||||
struct: op.struct
|
||||
}
|
||||
if (op.parentSub != null) {
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
if (op.hasOwnProperty('opContent')) {
|
||||
e.opContent = op.opContent
|
||||
} else {
|
||||
e.content = op.content.slice()
|
||||
}
|
||||
|
||||
if (op.opContent != null) {
|
||||
ids.push(op.opContent)
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CINSERT)
|
||||
// compute info property
|
||||
let contentIsText = op.content != null && op.content.every(c => typeof c === 'string' && c.length === 1)
|
||||
let originIsLeft = Y.utils.compareIds(op.left, op.origin)
|
||||
let info =
|
||||
(op.parentSub != null ? 1 : 0) |
|
||||
(op.opContent != null ? 2 : 0) |
|
||||
(contentIsText ? 4 : 0) |
|
||||
(originIsLeft ? 8 : 0) |
|
||||
(op.left != null ? 16 : 0) |
|
||||
(op.right != null ? 32 : 0) |
|
||||
(op.origin != null ? 64 : 0)
|
||||
encoder.writeUint8(info)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeOpID(op.parent)
|
||||
if (info & 16) {
|
||||
encoder.writeOpID(op.left)
|
||||
}
|
||||
if (info & 32) {
|
||||
encoder.writeOpID(op.right)
|
||||
}
|
||||
if (!originIsLeft && info & 64) {
|
||||
encoder.writeOpID(op.origin)
|
||||
}
|
||||
if (info & 1) {
|
||||
// write parentSub
|
||||
encoder.writeVarString(op.parentSub)
|
||||
}
|
||||
if (info & 2) {
|
||||
// write opContent
|
||||
encoder.writeOpID(op.opContent)
|
||||
} else if (info & 4) {
|
||||
// write text
|
||||
encoder.writeVarString(op.content.join(''))
|
||||
} else {
|
||||
// convert to JSON and write
|
||||
encoder.writeVarString(JSON.stringify(op.content))
|
||||
}
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
let op = {
|
||||
struct: 'Insert'
|
||||
}
|
||||
decoder.skip8()
|
||||
// get info property
|
||||
let info = decoder.readUint8()
|
||||
|
||||
op.id = decoder.readOpID()
|
||||
op.parent = decoder.readOpID()
|
||||
if (info & 16) {
|
||||
op.left = decoder.readOpID()
|
||||
} else {
|
||||
op.left = null
|
||||
}
|
||||
if (info & 32) {
|
||||
op.right = decoder.readOpID()
|
||||
} else {
|
||||
op.right = null
|
||||
}
|
||||
if (info & 8) {
|
||||
// origin is left
|
||||
op.origin = op.left
|
||||
} else if (info & 64) {
|
||||
op.origin = decoder.readOpID()
|
||||
} else {
|
||||
op.origin = null
|
||||
}
|
||||
if (info & 1) {
|
||||
// has parentSub
|
||||
op.parentSub = decoder.readVarString()
|
||||
}
|
||||
if (info & 2) {
|
||||
// has opContent
|
||||
op.opContent = decoder.readOpID()
|
||||
} else if (info & 4) {
|
||||
// has pure text content
|
||||
op.content = decoder.readVarString().split('')
|
||||
} else {
|
||||
// has mixed content
|
||||
let s = decoder.readVarString()
|
||||
op.content = JSON.parse(s)
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
var ids = []
|
||||
if (op.left != null) {
|
||||
ids.push(op.left)
|
||||
}
|
||||
if (op.right != null) {
|
||||
ids.push(op.right)
|
||||
}
|
||||
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
||||
ids.push(op.origin)
|
||||
}
|
||||
// if (op.right == null && op.left == null) {
|
||||
ids.push(op.parent)
|
||||
|
||||
if (op.opContent != null) {
|
||||
ids.push(op.opContent)
|
||||
}
|
||||
return ids
|
||||
},
|
||||
getDistanceToOrigin: function * (op) {
|
||||
if (op.left == null) {
|
||||
return 0
|
||||
} else {
|
||||
var d = 0
|
||||
var o = yield * this.getInsertion(op.left)
|
||||
while (!Y.utils.matchesId(o, op.origin)) {
|
||||
d++
|
||||
if (o.left == null) {
|
||||
break
|
||||
} else {
|
||||
o = yield * this.getInsertion(o.left)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
},
|
||||
getDistanceToOrigin: function * (op) {
|
||||
if (op.left == null) {
|
||||
return 0
|
||||
} else {
|
||||
var d = 0
|
||||
var o = yield * this.getInsertion(op.left)
|
||||
while (!Y.utils.matchesId(o, op.origin)) {
|
||||
d++
|
||||
if (o.left == null) {
|
||||
break
|
||||
} else {
|
||||
o = yield * this.getInsertion(o.left)
|
||||
return d
|
||||
}
|
||||
},
|
||||
/*
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change
|
||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
*/
|
||||
execute: function * (op) {
|
||||
var i // loop counter
|
||||
|
||||
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
||||
// We try to merge them later, if possible
|
||||
var tryToRemergeLater = []
|
||||
|
||||
if (op.origin != null) { // TODO: !== instead of !=
|
||||
// we save in origin that op originates in it
|
||||
// we need that later when we eventually garbage collect origin (see transaction)
|
||||
var origin = yield * this.getInsertionCleanEnd(op.origin)
|
||||
if (origin.originOf == null) {
|
||||
origin.originOf = []
|
||||
}
|
||||
origin.originOf.push(op.id)
|
||||
yield * this.setOperation(origin)
|
||||
if (origin.right != null) {
|
||||
tryToRemergeLater.push(origin.right)
|
||||
}
|
||||
}
|
||||
var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
||||
|
||||
// now we begin to insert op in the list of insertions..
|
||||
var o
|
||||
var parent
|
||||
var start
|
||||
|
||||
// find o. o is the first conflicting operation
|
||||
if (op.left != null) {
|
||||
o = yield * this.getInsertionCleanEnd(op.left)
|
||||
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
||||
// only if not added previously
|
||||
tryToRemergeLater.push(o.right)
|
||||
}
|
||||
o = (o.right == null) ? null : yield * this.getOperation(o.right)
|
||||
} else { // left == null
|
||||
parent = yield * this.getOperation(op.parent)
|
||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||
start = startId == null ? null : yield * this.getOperation(startId)
|
||||
o = start
|
||||
}
|
||||
|
||||
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
||||
if (op.right != null) {
|
||||
tryToRemergeLater.push(op.right)
|
||||
yield * this.getInsertionCleanStart(op.right)
|
||||
}
|
||||
|
||||
// handle conflicts
|
||||
while (true) {
|
||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||
var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||
if (oOriginDistance === i) {
|
||||
// case 1
|
||||
if (o.id[0] < op.id[0]) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
},
|
||||
/*
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change
|
||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
*/
|
||||
execute: function * (op) {
|
||||
var i // loop counter
|
||||
|
||||
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
||||
// We try to merge them later, if possible
|
||||
var tryToRemergeLater = []
|
||||
|
||||
if (op.origin != null) { // TODO: !== instead of !=
|
||||
// we save in origin that op originates in it
|
||||
// we need that later when we eventually garbage collect origin (see transaction)
|
||||
var origin = yield * this.getInsertionCleanEnd(op.origin)
|
||||
if (origin.originOf == null) {
|
||||
origin.originOf = []
|
||||
}
|
||||
origin.originOf.push(op.id)
|
||||
yield * this.setOperation(origin)
|
||||
if (origin.right != null) {
|
||||
tryToRemergeLater.push(origin.right)
|
||||
}
|
||||
}
|
||||
var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
||||
|
||||
// now we begin to insert op in the list of insertions..
|
||||
var o
|
||||
var parent
|
||||
var start
|
||||
|
||||
// find o. o is the first conflicting operation
|
||||
if (op.left != null) {
|
||||
o = yield * this.getInsertionCleanEnd(op.left)
|
||||
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
||||
// only if not added previously
|
||||
tryToRemergeLater.push(o.right)
|
||||
}
|
||||
o = (o.right == null) ? null : yield * this.getOperation(o.right)
|
||||
} else { // left == null
|
||||
parent = yield * this.getOperation(op.parent)
|
||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
||||
start = startId == null ? null : yield * this.getOperation(startId)
|
||||
o = start
|
||||
}
|
||||
|
||||
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
||||
if (op.right != null) {
|
||||
tryToRemergeLater.push(op.right)
|
||||
yield * this.getInsertionCleanStart(op.right)
|
||||
}
|
||||
|
||||
// handle conflicts
|
||||
while (true) {
|
||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
||||
var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o)
|
||||
if (oOriginDistance === i) {
|
||||
// case 1
|
||||
if (o.id[0] < op.id[0]) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else if (oOriginDistance < i) {
|
||||
// case 2
|
||||
if (i - distanceToOrigin <= oOriginDistance) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
i++
|
||||
if (o.right != null) {
|
||||
o = yield * this.getInsertion(o.right)
|
||||
} else {
|
||||
o = null
|
||||
} else if (oOriginDistance < i) {
|
||||
// case 2
|
||||
if (i - distanceToOrigin <= oOriginDistance) {
|
||||
op.left = Y.utils.getLastId(o)
|
||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// reconnect..
|
||||
var left = null
|
||||
var right = null
|
||||
if (parent == null) {
|
||||
parent = yield * this.getOperation(op.parent)
|
||||
}
|
||||
|
||||
// reconnect left and set right of op
|
||||
if (op.left != null) {
|
||||
left = yield * this.getInsertion(op.left)
|
||||
// link left
|
||||
op.right = left.right
|
||||
left.right = op.id
|
||||
|
||||
yield * this.setOperation(left)
|
||||
} else {
|
||||
// set op.right from parent, if necessary
|
||||
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
||||
}
|
||||
// reconnect right
|
||||
if (op.right != null) {
|
||||
// TODO: wanna connect right too?
|
||||
right = yield * this.getOperation(op.right)
|
||||
right.left = Y.utils.getLastId(op)
|
||||
|
||||
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
||||
if (right.gc != null) {
|
||||
if (right.content != null && right.content.length > 1) {
|
||||
right = yield * this.getInsertionCleanEnd(right.id)
|
||||
}
|
||||
this.store.removeFromGarbageCollector(right)
|
||||
}
|
||||
yield * this.setOperation(right)
|
||||
}
|
||||
|
||||
// update parents .map/start/end properties
|
||||
if (op.parentSub != null) {
|
||||
if (left == null) {
|
||||
parent.map[op.parentSub] = op.id
|
||||
yield * this.setOperation(parent)
|
||||
}
|
||||
// is a child of a map struct.
|
||||
// Then also make sure that only the most left element is not deleted
|
||||
// We do not call the type in this case (this is what the third parameter is for)
|
||||
if (op.right != null) {
|
||||
yield * this.deleteOperation(op.right, 1, true)
|
||||
}
|
||||
if (op.left != null) {
|
||||
yield * this.deleteOperation(op.id, 1, true)
|
||||
i++
|
||||
if (o.right != null) {
|
||||
o = yield * this.getInsertion(o.right)
|
||||
} else {
|
||||
o = null
|
||||
}
|
||||
} else {
|
||||
if (right == null || left == null) {
|
||||
if (right == null) {
|
||||
parent.end = Y.utils.getLastId(op)
|
||||
}
|
||||
if (left == null) {
|
||||
parent.start = op.id
|
||||
}
|
||||
yield * this.setOperation(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// try to merge original op.left and op.origin
|
||||
for (i = 0; i < tryToRemergeLater.length; i++) {
|
||||
var m = yield * this.getOperation(tryToRemergeLater[i])
|
||||
yield * this.tryCombineWithLeft(m)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
List: {
|
||||
/*
|
||||
{
|
||||
|
||||
// reconnect..
|
||||
var left = null
|
||||
var right = null
|
||||
if (parent == null) {
|
||||
parent = yield * this.getOperation(op.parent)
|
||||
}
|
||||
|
||||
// reconnect left and set right of op
|
||||
if (op.left != null) {
|
||||
left = yield * this.getInsertion(op.left)
|
||||
// link left
|
||||
op.right = left.right
|
||||
left.right = op.id
|
||||
|
||||
yield * this.setOperation(left)
|
||||
} else {
|
||||
// set op.right from parent, if necessary
|
||||
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
||||
}
|
||||
// reconnect right
|
||||
if (op.right != null) {
|
||||
// TODO: wanna connect right too?
|
||||
right = yield * this.getOperation(op.right)
|
||||
right.left = Y.utils.getLastId(op)
|
||||
|
||||
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
||||
if (right.gc != null) {
|
||||
if (right.content != null && right.content.length > 1) {
|
||||
right = yield * this.getInsertionCleanEnd(right.id)
|
||||
}
|
||||
this.store.removeFromGarbageCollector(right)
|
||||
}
|
||||
yield * this.setOperation(right)
|
||||
}
|
||||
|
||||
// update parents .map/start/end properties
|
||||
if (op.parentSub != null) {
|
||||
if (left == null) {
|
||||
parent.map[op.parentSub] = op.id
|
||||
yield * this.setOperation(parent)
|
||||
}
|
||||
// is a child of a map struct.
|
||||
// Then also make sure that only the most left element is not deleted
|
||||
// We do not call the type in this case (this is what the third parameter is for)
|
||||
if (op.right != null) {
|
||||
yield * this.deleteOperation(op.right, 1, true)
|
||||
}
|
||||
if (op.left != null) {
|
||||
yield * this.deleteOperation(op.id, 1, true)
|
||||
}
|
||||
} else {
|
||||
if (right == null || left == null) {
|
||||
if (right == null) {
|
||||
parent.end = Y.utils.getLastId(op)
|
||||
}
|
||||
if (left == null) {
|
||||
parent.start = op.id
|
||||
}
|
||||
yield * this.setOperation(parent)
|
||||
}
|
||||
}
|
||||
|
||||
// try to merge original op.left and op.origin
|
||||
for (i = 0; i < tryToRemergeLater.length; i++) {
|
||||
var m = yield * this.getOperation(tryToRemergeLater[i])
|
||||
yield * this.tryCombineWithLeft(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.List = {
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'List',
|
||||
id: id
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
if (op.info != null) {
|
||||
e.info = op.info
|
||||
}
|
||||
return e
|
||||
},
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
*/
|
||||
return []
|
||||
},
|
||||
execute: function * (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function * (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null
|
||||
}
|
||||
var res = null
|
||||
var o = yield * this.getOperation(op.start)
|
||||
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o
|
||||
pos--
|
||||
}
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = yield * this.getOperation(o.right)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
map: function * (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o != null) { // TODO: change to != (at least some convention)
|
||||
var operation = yield * this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation))
|
||||
}
|
||||
o = operation.right
|
||||
}
|
||||
return res
|
||||
struct: 'List',
|
||||
id: id
|
||||
}
|
||||
},
|
||||
Map: {
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
}
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CLIST)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeVarString(op.type)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
let op = {
|
||||
id: decoder.readOpID(),
|
||||
type: decoder.readVarString(),
|
||||
struct: 'List',
|
||||
start: null,
|
||||
end: null
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function () {
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
struct: "Map",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
struct: 'Map'
|
||||
return []
|
||||
},
|
||||
execute: function * (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function * (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null
|
||||
}
|
||||
var res = null
|
||||
var o = yield * this.getOperation(op.start)
|
||||
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o
|
||||
pos--
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Map',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {} // overwrite map!!
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = yield * this.getOperation(o.right)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
return res
|
||||
},
|
||||
map: function * (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o != null) { // TODO: change to != (at least some convention)
|
||||
var operation = yield * this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation))
|
||||
}
|
||||
if (op.info != null) {
|
||||
e.info = op.info
|
||||
}
|
||||
return e
|
||||
},
|
||||
requiredOps: function () {
|
||||
return []
|
||||
},
|
||||
execute: function * () {},
|
||||
/*
|
||||
Get a property by name
|
||||
*/
|
||||
get: function * (op, name) {
|
||||
var oid = op.map[name]
|
||||
if (oid != null) {
|
||||
var res = yield * this.getOperation(oid)
|
||||
if (res == null || res.deleted) {
|
||||
return void 0
|
||||
} else if (res.opContent == null) {
|
||||
return res.content[0]
|
||||
} else {
|
||||
return yield * this.getType(res.opContent)
|
||||
}
|
||||
o = operation.right
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
struct: "Map",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.Map = {
|
||||
create: function (id) {
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
struct: 'Map'
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Map',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {} // overwrite map!!
|
||||
}
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CMAP)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeVarString(op.type)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
let op = {
|
||||
id: decoder.readOpID(),
|
||||
type: decoder.readVarString(),
|
||||
struct: 'Map',
|
||||
map: {}
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function () {
|
||||
return []
|
||||
},
|
||||
execute: function * (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
/*
|
||||
Get a property by name
|
||||
*/
|
||||
get: function * (op, name) {
|
||||
var oid = op.map[name]
|
||||
if (oid != null) {
|
||||
var res = yield * this.getOperation(oid)
|
||||
if (res == null || res.deleted) {
|
||||
return void 0
|
||||
} else if (res.opContent == null) {
|
||||
return res.content[0]
|
||||
} else {
|
||||
return yield * this.getType(res.opContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Y.Struct = Struct
|
||||
|
||||
/*
|
||||
{
|
||||
map: {},
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "Xml",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
Struct.Xml = {
|
||||
create: function (id, args) {
|
||||
let nodeName = args != null ? args.nodeName : null
|
||||
return {
|
||||
id: id,
|
||||
map: {},
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'Xml',
|
||||
nodeName
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'Xml',
|
||||
type: op.type,
|
||||
id: op.id,
|
||||
map: {},
|
||||
nodeName: op.nodeName
|
||||
}
|
||||
return e
|
||||
},
|
||||
binaryEncode: function (encoder, op) {
|
||||
encoder.writeUint8(CXML)
|
||||
encoder.writeOpID(op.id)
|
||||
encoder.writeVarString(op.type)
|
||||
encoder.writeVarString(op.nodeName)
|
||||
},
|
||||
binaryDecode: function (decoder) {
|
||||
decoder.skip8()
|
||||
let op = {
|
||||
id: decoder.readOpID(),
|
||||
type: decoder.readVarString(),
|
||||
struct: 'Xml',
|
||||
map: {},
|
||||
start: null,
|
||||
end: null,
|
||||
nodeName: decoder.readVarString()
|
||||
}
|
||||
return op
|
||||
},
|
||||
requiredOps: function () {
|
||||
return []
|
||||
},
|
||||
execute: function * () {},
|
||||
ref: Struct.List.ref,
|
||||
map: Struct.List.map,
|
||||
/*
|
||||
Get a property by name
|
||||
*/
|
||||
get: Struct.Map.get
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
|
||||
|
||||
/*
|
||||
Partial definition of a transaction
|
||||
@@ -99,6 +98,9 @@ export default function extendTransaction (Y) {
|
||||
if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
|
||||
// is connected, and this is not going to be send in addOperation
|
||||
this.store.y.connector.broadcastOps(send)
|
||||
if (this.store.y.persistence != null) {
|
||||
this.store.y.persistence.saveOperations(send)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,11 +590,20 @@ export default function extendTransaction (Y) {
|
||||
apply a delete set in order to get
|
||||
the state of the supplied ds
|
||||
*/
|
||||
* applyDeleteSet (ds) {
|
||||
* applyDeleteSet (decoder) {
|
||||
var deletions = []
|
||||
|
||||
for (var user in ds) {
|
||||
var dv = ds[user]
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let dv = []
|
||||
let dvLength = decoder.readVarUint()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
dv.push([from, len, gc])
|
||||
}
|
||||
var pos = 0
|
||||
var d = dv[pos]
|
||||
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
|
||||
@@ -672,10 +683,15 @@ export default function extendTransaction (Y) {
|
||||
yield * this.garbageCollectOperation(o.id)
|
||||
}
|
||||
}
|
||||
if (this.store.forwardAppliedOperations) {
|
||||
if (this.store.forwardAppliedOperations || this.store.y.persistence != null) {
|
||||
var ops = []
|
||||
ops.push({struct: 'Delete', target: [del[0], del[1]], length: del[2]})
|
||||
this.store.y.connector.broadcastOps(ops)
|
||||
if (this.store.forwardAppliedOperations) {
|
||||
this.store.y.connector.broadcastOps(ops)
|
||||
}
|
||||
if (this.store.y.persistence != null) {
|
||||
this.store.y.persistence.saveOperations(ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,21 +702,34 @@ export default function extendTransaction (Y) {
|
||||
/*
|
||||
A DeleteSet (ds) describes all the deleted ops in the OS
|
||||
*/
|
||||
* getDeleteSet () {
|
||||
var ds = {}
|
||||
* writeDeleteSet (encoder) {
|
||||
var ds = new Map()
|
||||
yield * this.ds.iterate(this, null, null, function * (n) {
|
||||
var user = n.id[0]
|
||||
var counter = n.id[1]
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
var dv = ds[user]
|
||||
var dv = ds.get(user)
|
||||
if (dv === void 0) {
|
||||
dv = []
|
||||
ds[user] = dv
|
||||
ds.set(user, dv)
|
||||
}
|
||||
dv.push([counter, len, gc])
|
||||
})
|
||||
return ds
|
||||
let keys = Array.from(ds.keys())
|
||||
encoder.writeUint32(keys.length)
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
let user = keys[i]
|
||||
let deletions = ds.get(user)
|
||||
encoder.writeVarUint(user)
|
||||
encoder.writeVarUint(deletions.length)
|
||||
for (var j = 0; j < deletions.length; j++) {
|
||||
let del = deletions[j]
|
||||
encoder.writeVarUint(del[0])
|
||||
encoder.writeVarUint(del[1])
|
||||
encoder.writeUint8(del[2] ? 1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
* isDeleted (id) {
|
||||
var n = yield * this.ds.findWithUpperBound(id)
|
||||
@@ -712,9 +741,15 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
* addOperation (op) {
|
||||
yield * this.os.put(op)
|
||||
if (this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
|
||||
// is connected, and this is not going to be send in addOperation
|
||||
this.store.y.connector.broadcastOps([op])
|
||||
// case op is created by this user, op is already broadcasted in applyCreatedOperations
|
||||
if (op.id[0] !== this.store.userId && typeof op.id[1] !== 'string') {
|
||||
if (this.store.forwardAppliedOperations) {
|
||||
// is connected, and this is not going to be send in addOperation
|
||||
this.store.y.connector.broadcastOps([op])
|
||||
}
|
||||
if (this.store.y.persistence != null) {
|
||||
this.store.y.persistence.saveOperations([op])
|
||||
}
|
||||
}
|
||||
}
|
||||
// if insertion, try to combine with left insertion (if both have content property)
|
||||
@@ -821,14 +856,19 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
* getOperation (id/* :any */)/* :Transaction<any> */ {
|
||||
var o = yield * this.os.find(id)
|
||||
if (id[0] !== '_' || o != null) {
|
||||
if (id[0] !== 0xFFFFFF || o != null) {
|
||||
return o
|
||||
} else { // type is string
|
||||
// generate this operation?
|
||||
var comp = id[1].split('_')
|
||||
if (comp.length > 1) {
|
||||
var struct = comp[0]
|
||||
var op = Y.Struct[struct].create(id)
|
||||
let type = Y[comp[1]]
|
||||
let args = null
|
||||
if (type != null) {
|
||||
args = Y.utils.parseTypeDefinition(type, comp[3])
|
||||
}
|
||||
var op = Y.Struct[struct].create(id, args)
|
||||
op.type = comp[1]
|
||||
yield * this.setOperation(op)
|
||||
return op
|
||||
@@ -878,6 +918,18 @@ export default function extendTransaction (Y) {
|
||||
})
|
||||
return ss
|
||||
}
|
||||
* writeStateSet (encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0)
|
||||
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||
encoder.writeVarUint(n.id[0])
|
||||
encoder.writeVarUint(n.clock)
|
||||
len++
|
||||
})
|
||||
encoder.setUint32(lenPosition, len)
|
||||
return len === 0
|
||||
}
|
||||
/*
|
||||
Here, we make all missing operations executable for the receiving user.
|
||||
|
||||
@@ -927,17 +979,17 @@ export default function extendTransaction (Y) {
|
||||
* getOperations (startSS) {
|
||||
// TODO: use bounds here!
|
||||
if (startSS == null) {
|
||||
startSS = {}
|
||||
startSS = new Map()
|
||||
}
|
||||
var send = []
|
||||
|
||||
var endSV = yield * this.getStateVector()
|
||||
for (let endState of endSV) {
|
||||
let user = endState.user
|
||||
if (user === '_') {
|
||||
if (user === 0xFFFFFF) {
|
||||
continue
|
||||
}
|
||||
let startPos = startSS[user] || 0
|
||||
let startPos = startSS.get(user) || 0
|
||||
if (startPos > 0) {
|
||||
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
|
||||
// find out if that is the case
|
||||
@@ -947,19 +999,19 @@ export default function extendTransaction (Y) {
|
||||
startPos = firstMissing.id[1]
|
||||
}
|
||||
}
|
||||
startSS[user] = startPos
|
||||
startSS.set(user, startPos)
|
||||
}
|
||||
for (let endState of endSV) {
|
||||
let user = endState.user
|
||||
let startPos = startSS[user]
|
||||
if (user === '_') {
|
||||
let startPos = startSS.get(user)
|
||||
if (user === 0xFFFFFF) {
|
||||
continue
|
||||
}
|
||||
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
||||
op = Y.Struct[op.struct].encode(op)
|
||||
if (op.struct !== 'Insert') {
|
||||
send.push(op)
|
||||
} else if (op.right == null || op.right[1] < (startSS[op.right[0]] || 0)) {
|
||||
} else if (op.right == null || op.right[1] < (startSS.get(op.right[0]) || 0)) {
|
||||
// case 1. op.right is known
|
||||
// this case is only reached if op.right is known.
|
||||
// => this is not called for op.left, as op.right is unknown
|
||||
@@ -977,7 +1029,7 @@ export default function extendTransaction (Y) {
|
||||
op.left = null
|
||||
send.push(op)
|
||||
/* 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 = Y.Struct[op.struct].encode(o)
|
||||
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)) {
|
||||
missingOrigins.pop()
|
||||
}
|
||||
if (o.id[1] < (startSS[o.id[0]] || 0)) {
|
||||
if (o.id[1] < (startSS.get(o.id[0]) || 0)) {
|
||||
// case 2. o is known
|
||||
op.left = Y.utils.getLastId(o)
|
||||
send.push(op)
|
||||
@@ -1023,28 +1075,62 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
return send.reverse()
|
||||
}
|
||||
|
||||
* writeOperations (encoder, decoder) {
|
||||
let ss = new Map()
|
||||
let ssLength = decoder.readUint32()
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ss.set(user, clock)
|
||||
}
|
||||
let ops = yield * this.getOperations(ss)
|
||||
encoder.writeUint32(ops.length)
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
||||
}
|
||||
}
|
||||
|
||||
* toBinary () {
|
||||
let encoder = new BinaryEncoder()
|
||||
yield * this.writeOperationsUntransformed(encoder)
|
||||
yield * this.writeDeleteSet(encoder)
|
||||
return encoder.createBuffer()
|
||||
}
|
||||
|
||||
* fromBinary (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
yield * this.applyOperationsUntransformed(decoder)
|
||||
yield * this.applyDeleteSet(decoder)
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the plain untransformed operations from the database.
|
||||
* You can apply these operations using .applyOperationsUntransformed(ops)
|
||||
*
|
||||
*/
|
||||
* getOperationsUntransformed () {
|
||||
var ops = []
|
||||
* writeOperationsUntransformed (encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0) // placeholder
|
||||
yield * this.os.iterate(this, null, null, function * (op) {
|
||||
if (op.id[0] !== '_') {
|
||||
ops.push(op)
|
||||
if (op.id[0] !== 0xFFFFFF) {
|
||||
len++
|
||||
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
|
||||
}
|
||||
})
|
||||
return {
|
||||
untransformed: ops
|
||||
}
|
||||
encoder.setUint32(lenPosition, len)
|
||||
yield * this.writeStateSet(encoder)
|
||||
}
|
||||
* applyOperationsUntransformed (m, stateSet) {
|
||||
var ops = m.untransformed
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
var op = ops[i]
|
||||
// create, and modify parent, if it is created implicitly
|
||||
if (op.parent != null && op.parent[0] === '_') {
|
||||
* applyOperationsUntransformed (decoder) {
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let op = Y.Struct.binaryDecodeOperation(decoder)
|
||||
yield * this.os.put(op)
|
||||
}
|
||||
yield * this.os.iterate(this, null, null, function * (op) {
|
||||
if (op.parent != null) {
|
||||
if (op.struct === 'Insert') {
|
||||
// update parents .map/start/end properties
|
||||
if (op.parentSub != null && op.left == null) {
|
||||
@@ -1064,12 +1150,14 @@ export default function extendTransaction (Y) {
|
||||
}
|
||||
}
|
||||
}
|
||||
yield * this.os.put(op)
|
||||
}
|
||||
for (var user in stateSet) {
|
||||
})
|
||||
let stateSetLength = decoder.readUint32()
|
||||
for (let i = 0; i < stateSetLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
yield * this.ss.put({
|
||||
id: [user],
|
||||
clock: stateSet[user]
|
||||
clock: clock
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -22,7 +26,10 @@
|
||||
*/
|
||||
|
||||
export default function Utils (Y) {
|
||||
Y.utils = {}
|
||||
Y.utils = {
|
||||
BinaryDecoder: BinaryDecoder,
|
||||
BinaryEncoder: BinaryEncoder
|
||||
}
|
||||
|
||||
Y.utils.bubbleEvent = function (type, event) {
|
||||
type.eventHandler.callEventListeners(event)
|
||||
@@ -818,8 +825,32 @@ export default function Utils (Y) {
|
||||
}
|
||||
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
||||
|
||||
// Generates a unique id, for use as a user id.
|
||||
// Thx to @jed for this script https://gist.github.com/jed/982883
|
||||
function generateGuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,generateGuid)} // eslint-disable-line
|
||||
Y.utils.generateGuid = generateGuid
|
||||
function generateUserId () {
|
||||
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
|
||||
// browser
|
||||
let arr = new Uint32Array(1)
|
||||
crypto.getRandomValues(arr)
|
||||
return arr[0]
|
||||
} else if (typeof crypto !== 'undefined' && crypto.randomBytes != null) {
|
||||
// node
|
||||
let buf = crypto.randomBytes(4)
|
||||
return new Uint32Array(buf.buffer)[0]
|
||||
} else {
|
||||
return Math.ceil(Math.random() * 0xFFFFFFFF)
|
||||
}
|
||||
}
|
||||
Y.utils.generateUserId = generateUserId
|
||||
|
||||
Y.utils.parseTypeDefinition = function parseTypeDefinition (type, typeArgs) {
|
||||
var args = []
|
||||
try {
|
||||
args = JSON.parse('[' + typeArgs + ']')
|
||||
} catch (e) {
|
||||
throw new Error('Was not able to parse type definition!')
|
||||
}
|
||||
if (type.typeDefinition.parseArguments != null) {
|
||||
args = type.typeDefinition.parseArguments(args[0])[1]
|
||||
}
|
||||
return args
|
||||
}
|
||||
}
|
||||
|
||||
54
src/y.js
54
src/y.js
@@ -1,17 +1,22 @@
|
||||
import debug from 'debug'
|
||||
import extendConnector from './Connector.js'
|
||||
import extendPersistence from './Persistence.js'
|
||||
import extendDatabase from './Database.js'
|
||||
import extendTransaction from './Transaction.js'
|
||||
import extendStruct from './Struct.js'
|
||||
import extendUtils from './Utils.js'
|
||||
import debug from 'debug'
|
||||
import { formatYjsMessage, formatYjsMessageType } from './MessageHandler.js'
|
||||
|
||||
extendConnector(Y)
|
||||
extendPersistence(Y)
|
||||
extendDatabase(Y)
|
||||
extendTransaction(Y)
|
||||
extendStruct(Y)
|
||||
extendUtils(Y)
|
||||
|
||||
Y.debug = debug
|
||||
debug.formatters.Y = formatYjsMessage
|
||||
debug.formatters.y = formatYjsMessageType
|
||||
|
||||
var requiringModules = {}
|
||||
|
||||
@@ -134,10 +139,20 @@ export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||
opts.share = Y.utils.copyObject(opts.share)
|
||||
Y.requestModules(modules).then(function () {
|
||||
var yconfig = new YConfig(opts)
|
||||
let resolved = false
|
||||
if (opts.timeout != null && opts.timeout >= 0) {
|
||||
setTimeout(function () {
|
||||
if (!resolved) {
|
||||
reject(new Error('Yjs init timeout'))
|
||||
yconfig.destroy()
|
||||
}
|
||||
}, opts.timeout)
|
||||
}
|
||||
yconfig.db.whenUserIdSet(function () {
|
||||
yconfig.init(function () {
|
||||
resolved = true
|
||||
resolve(yconfig)
|
||||
})
|
||||
}, reject)
|
||||
})
|
||||
}).catch(reject)
|
||||
}
|
||||
@@ -156,6 +171,11 @@ class YConfig extends Y.utils.NamedEventHandler {
|
||||
this.options = opts
|
||||
this.db = new Y[opts.db.name](this, opts.db)
|
||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
||||
if (opts.persistence != null) {
|
||||
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
|
||||
} else {
|
||||
this.persistence = null
|
||||
}
|
||||
this.connected = true
|
||||
}
|
||||
init (callback) {
|
||||
@@ -166,28 +186,26 @@ class YConfig extends Y.utils.NamedEventHandler {
|
||||
// create shared object
|
||||
for (var propertyname in opts.share) {
|
||||
var typeConstructor = opts.share[propertyname].split('(')
|
||||
let typeArgs = ''
|
||||
if (typeConstructor.length === 2) {
|
||||
typeArgs = typeConstructor[1].split(')')[0] || ''
|
||||
}
|
||||
var typeName = typeConstructor.splice(0, 1)
|
||||
var type = Y[typeName]
|
||||
var typedef = type.typeDefinition
|
||||
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
||||
var args = []
|
||||
if (typeConstructor.length === 1) {
|
||||
try {
|
||||
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
|
||||
} catch (e) {
|
||||
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
||||
}
|
||||
if (type.typeDefinition.parseArguments == null) {
|
||||
throw new Error(typeName + ' does not expect arguments!')
|
||||
} else {
|
||||
args = typedef.parseArguments(args[0])[1]
|
||||
}
|
||||
}
|
||||
var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeArgs]
|
||||
let args = Y.utils.parseTypeDefinition(type, typeArgs)
|
||||
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
||||
}
|
||||
this.store.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
})
|
||||
if (this.persistence != null) {
|
||||
this.persistence.retrieveContent()
|
||||
.then(() => this.db.whenTransactionsFinished())
|
||||
.then(callback)
|
||||
} else {
|
||||
this.db.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
}
|
||||
}
|
||||
isConnected () {
|
||||
return this.connector.isSynced
|
||||
|
||||
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 yArray from '../../y-array/src/y-array.js'
|
||||
import yText from '../../y-text/src/Text.js'
|
||||
import yMap from '../../y-map/src/Map.js'
|
||||
import yXml from '../../y-xml/src/y-xml.js'
|
||||
import yTest from './test-connector.js'
|
||||
|
||||
import Chance from 'chance'
|
||||
|
||||
export let Y = _Y
|
||||
|
||||
Y.extend(yMemory, yArray, yMap, yTest)
|
||||
Y.extend(yMemory, yArray, yText, yMap, yTest, yXml)
|
||||
|
||||
export var database = { name: 'memory' }
|
||||
export var connector = { name: 'test', url: 'http://localhost:1234' }
|
||||
|
||||
function * getStateSet () {
|
||||
var ss = {}
|
||||
yield * this.ss.iterate(this, null, null, function * (n) {
|
||||
var user = n.id[0]
|
||||
var clock = n.clock
|
||||
ss[user] = clock
|
||||
})
|
||||
return ss
|
||||
}
|
||||
|
||||
function * getDeleteSet () {
|
||||
var ds = {}
|
||||
yield * this.ds.iterate(this, null, null, function * (n) {
|
||||
var user = n.id[0]
|
||||
var counter = n.id[1]
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
var dv = ds[user]
|
||||
if (dv === void 0) {
|
||||
dv = []
|
||||
ds[user] = dv
|
||||
}
|
||||
dv.push([counter, len, gc])
|
||||
})
|
||||
return ds
|
||||
}
|
||||
|
||||
export async function garbageCollectUsers (t, users) {
|
||||
await flushAll(t, users)
|
||||
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
|
||||
}
|
||||
|
||||
export function attrsToObject (attrs) {
|
||||
let obj = {}
|
||||
for (var i = 0; i < attrs.length; i++) {
|
||||
let attr = attrs[i]
|
||||
obj[attr.name] = attr.value
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function domToJson (dom) {
|
||||
if (dom.nodeType === document.TEXT_NODE) {
|
||||
return dom.textContent
|
||||
} else if (dom.nodeType === document.ELEMENT_NODE) {
|
||||
let attributes = attrsToObject(dom.attributes)
|
||||
let children = Array.from(dom.childNodes.values()).map(domToJson)
|
||||
return {
|
||||
name: dom.nodeName,
|
||||
children: children,
|
||||
attributes: attributes
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unsupported node type')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. reconnect and flush all
|
||||
* 2. user 0 gc
|
||||
@@ -33,7 +90,17 @@ export async function compareUsers (t, users) {
|
||||
await wait()
|
||||
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()
|
||||
@@ -60,10 +127,14 @@ export async function compareUsers (t, users) {
|
||||
var data = await Promise.all(users.map(async (u) => {
|
||||
var data = {}
|
||||
u.db.requestTransaction(function * () {
|
||||
var os = yield * this.getOperationsUntransformed()
|
||||
let ops = []
|
||||
yield * this.os.iterate(this, null, null, function * (op) {
|
||||
ops.push(Y.Struct[op.struct].encode(op))
|
||||
})
|
||||
|
||||
data.os = {}
|
||||
for (let i = 0; i < os.untransformed.length; i++) {
|
||||
let op = os.untransformed[i]
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
let op = ops[i]
|
||||
op = Y.Struct[op.struct].encode(op)
|
||||
delete op.origin
|
||||
/*
|
||||
@@ -79,15 +150,18 @@ export async function compareUsers (t, users) {
|
||||
data.os[JSON.stringify(op.id)] = op
|
||||
}
|
||||
}
|
||||
data.ds = yield * this.getDeleteSet()
|
||||
data.ss = yield * this.getStateSet()
|
||||
data.ds = yield * getDeleteSet.apply(this)
|
||||
data.ss = yield * getStateSet.apply(this)
|
||||
})
|
||||
await u.db.whenTransactionsFinished()
|
||||
return data
|
||||
}))
|
||||
for (var i = 0; i < data.length - 1; i++) {
|
||||
await t.asyncGroup(async () => {
|
||||
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types')
|
||||
t.compare(userMapOneValues[i], userMapOneValues[i + 1], 'map types (propery "one")')
|
||||
t.compare(userMapTwoValues[i], userMapTwoValues[i + 1], 'map types (propery "two")')
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types')
|
||||
t.compare(data[i].os, data[i + 1].os, 'os')
|
||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
||||
@@ -102,19 +176,19 @@ export async function initArrays (t, opts) {
|
||||
var result = {
|
||||
users: []
|
||||
}
|
||||
var share = Object.assign({ flushHelper: 'Map', array: 'Array' }, opts.share)
|
||||
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map', xml: 'XmlElement("div")' }, opts.share)
|
||||
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
||||
var connector = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, opts.connector)
|
||||
var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector)
|
||||
for (let i = 0; i < opts.users; i++) {
|
||||
let dbOpts
|
||||
let connOpts
|
||||
if (i === 0) {
|
||||
// Only one instance can gc!
|
||||
dbOpts = Object.assign({ gc: true }, opts.db)
|
||||
connOpts = Object.assign({ role: 'master' }, connector)
|
||||
dbOpts = Object.assign({ gc: false }, database)
|
||||
connOpts = Object.assign({ role: 'master' }, conn)
|
||||
} else {
|
||||
dbOpts = Object.assign({ gc: false }, opts.db)
|
||||
connOpts = Object.assign({ role: 'slave' }, connector)
|
||||
dbOpts = Object.assign({ gc: false }, database)
|
||||
connOpts = Object.assign({ role: 'slave' }, conn)
|
||||
}
|
||||
let y = await Y({
|
||||
connector: connOpts,
|
||||
@@ -189,3 +263,48 @@ export function wait (t) {
|
||||
setTimeout(resolve, t != null ? t : 100)
|
||||
})
|
||||
}
|
||||
|
||||
export async function applyRandomTests (t, mods, iterations) {
|
||||
const chance = new Chance(t.getSeed() * 1000000000)
|
||||
var initInformation = await initArrays(t, { users: 5, chance: chance })
|
||||
let { users } = initInformation
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
if (chance.bool({likelihood: 10})) {
|
||||
// 10% chance to disconnect/reconnect a user
|
||||
// we make sure that the first users always is connected
|
||||
let user = chance.pickone(users.slice(1))
|
||||
if (user.connector.isSynced) {
|
||||
if (users.filter(u => u.connector.isSynced).length > 1) {
|
||||
// make sure that at least one user remains in the room
|
||||
await user.disconnect()
|
||||
if (users[0].connector.testRoom == null) {
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await user.reconnect()
|
||||
if (users[0].connector.testRoom == null) {
|
||||
await wait(100)
|
||||
}
|
||||
await new Promise(function (resolve) {
|
||||
user.connector.whenSynced(resolve)
|
||||
})
|
||||
}
|
||||
} else if (chance.bool({likelihood: 5})) {
|
||||
// 20%*!prev chance to flush all & garbagecollect
|
||||
// TODO: We do not gc all users as this does not work yet
|
||||
// await garbageCollectUsers(t, users)
|
||||
await flushAll(t, users)
|
||||
await users[0].db.emptyGarbageCollector()
|
||||
await flushAll(t, users)
|
||||
} else if (chance.bool({likelihood: 10})) {
|
||||
// 20%*!prev chance to flush some operations
|
||||
await flushSome(t, users)
|
||||
}
|
||||
let user = chance.pickone(users)
|
||||
var test = chance.pickone(mods)
|
||||
test(t, user, chance)
|
||||
}
|
||||
await compareUsers(t, users)
|
||||
return initInformation
|
||||
}
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
/* global Y */
|
||||
import { wait } from './helper.js'
|
||||
import { formatYjsMessage } from '../src/MessageHandler.js'
|
||||
|
||||
var rooms = {}
|
||||
|
||||
export class TestRoom {
|
||||
constructor (roomname) {
|
||||
this.room = roomname
|
||||
this.users = {}
|
||||
this.users = new Map()
|
||||
this.nextUserId = 0
|
||||
}
|
||||
join (connector) {
|
||||
if (connector.userId == null) {
|
||||
connector.setUserId('' + (this.nextUserId++))
|
||||
connector.setUserId(this.nextUserId++)
|
||||
}
|
||||
Object.keys(this.users).forEach(uid => {
|
||||
let user = this.users[uid]
|
||||
this.users.forEach((user, uid) => {
|
||||
if (user.role === 'master' || connector.role === 'master') {
|
||||
this.users[uid].userJoined(connector.userId, connector.role)
|
||||
connector.userJoined(uid, this.users[uid].role)
|
||||
this.users.get(uid).userJoined(connector.userId, connector.role)
|
||||
connector.userJoined(uid, this.users.get(uid).role)
|
||||
}
|
||||
})
|
||||
this.users[connector.userId] = connector
|
||||
this.users.set(connector.userId, connector)
|
||||
}
|
||||
leave (connector) {
|
||||
delete this.users[connector.userId]
|
||||
Object.keys(this.users).forEach(uid => {
|
||||
this.users[uid].userLeft(connector.userId)
|
||||
this.users.delete(connector.userId)
|
||||
this.users.forEach(user => {
|
||||
user.userLeft(connector.userId)
|
||||
})
|
||||
}
|
||||
send (sender, receiver, m) {
|
||||
m = JSON.parse(JSON.stringify(m))
|
||||
var user = this.users[receiver]
|
||||
var user = this.users.get(receiver)
|
||||
if (user != null) {
|
||||
user.receiveMessage(sender, m)
|
||||
}
|
||||
}
|
||||
broadcast (sender, m) {
|
||||
Object.keys(this.users).forEach(receiver => {
|
||||
this.users.forEach((user, receiver) => {
|
||||
this.send(sender, receiver, m)
|
||||
})
|
||||
}
|
||||
async flushAll (users) {
|
||||
let flushing = true
|
||||
let allUserIds = Object.keys(this.users)
|
||||
let allUserIds = Array.from(this.users.keys())
|
||||
if (users == null) {
|
||||
users = allUserIds.map(id => this.users[id].y)
|
||||
users = allUserIds.map(id => this.users.get(id).y)
|
||||
}
|
||||
while (flushing) {
|
||||
await wait(10)
|
||||
let res = await Promise.all(allUserIds.map(id => this.users[id]._flushAll(users)))
|
||||
let res = await Promise.all(allUserIds.map(id => this.users.get(id)._flushAll(users)))
|
||||
flushing = res.some(status => status === 'flushing')
|
||||
}
|
||||
}
|
||||
@@ -82,14 +81,25 @@ export default function extendTestConnector (Y) {
|
||||
this.testRoom.leave(this)
|
||||
return super.disconnect()
|
||||
}
|
||||
logBufferParsed () {
|
||||
console.log(' === Logging buffer of user ' + this.userId + ' === ')
|
||||
for (let [user, conn] of this.connections) {
|
||||
console.log(` ${user}:`)
|
||||
for (let i = 0; i < conn.buffer.length; i++) {
|
||||
console.log(formatYjsMessage(conn.buffer[i]))
|
||||
}
|
||||
}
|
||||
}
|
||||
reconnect () {
|
||||
this.testRoom.join(this)
|
||||
return super.reconnect()
|
||||
}
|
||||
send (uid, message) {
|
||||
super.send(uid, message)
|
||||
this.testRoom.send(this.userId, uid, message)
|
||||
}
|
||||
broadcast (message) {
|
||||
super.broadcast(message)
|
||||
this.testRoom.broadcast(this.userId, message)
|
||||
}
|
||||
async whenSynced (f) {
|
||||
@@ -109,10 +119,10 @@ export default function extendTestConnector (Y) {
|
||||
})
|
||||
}
|
||||
receiveMessage (sender, m) {
|
||||
if (this.userId !== sender && this.connections[sender] != null) {
|
||||
var buffer = this.connections[sender].buffer
|
||||
if (this.userId !== sender && this.connections.has(sender)) {
|
||||
var buffer = this.connections.get(sender).buffer
|
||||
if (buffer == null) {
|
||||
buffer = this.connections[sender].buffer = []
|
||||
buffer = this.connections.get(sender).buffer = []
|
||||
}
|
||||
buffer.push(m)
|
||||
if (this.chance.bool({likelihood: 30})) {
|
||||
@@ -127,13 +137,13 @@ export default function extendTestConnector (Y) {
|
||||
async _flushAll (flushUsers) {
|
||||
if (flushUsers.some(u => u.connector.userId === this.userId)) {
|
||||
// this one needs to sync with every other user
|
||||
flushUsers = Object.keys(this.connections).map(id => this.testRoom.users[id].y)
|
||||
flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y)
|
||||
}
|
||||
var finished = []
|
||||
for (let i = 0; i < flushUsers.length; i++) {
|
||||
let userId = flushUsers[i].connector.userId
|
||||
if (userId !== this.userId && this.connections[userId] != null) {
|
||||
let buffer = this.connections[userId].buffer
|
||||
if (userId !== this.userId && this.connections.has(userId)) {
|
||||
let buffer = this.connections.get(userId).buffer
|
||||
if (buffer != null) {
|
||||
var messages = buffer.splice(0)
|
||||
for (let j = 0; j < messages.length; j++) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user