Compare commits

...

62 Commits

Author SHA1 Message Date
Kevin Jahns
c9634da316 v13.0.0-9 -- distribution files 2017-08-01 16:22:27 +02:00
Kevin Jahns
03e1a3fc12 13.0.0-9 2017-08-01 16:21:38 +02:00
Kevin Jahns
5c33f41c30 fix linting 2017-08-01 16:19:25 +02:00
Kevin Jahns
65e8c29b33 remove all async-functions - making it compatible with node 6 2017-08-01 16:15:36 +02:00
Kevin Jahns
fed77d532f 13.0.0-8 2017-07-31 16:05:30 +02:00
Kevin Jahns
d129184f7b fix linting 2017-07-31 15:43:04 +02:00
Kevin Jahns
a05bb1d4f9 merge bugfix-unable-to-deliver-message branch 2017-07-31 15:40:25 +02:00
Kevin Jahns
65af4963e6 merge bugfix-multiple-clients-sync branch 2017-07-31 15:35:27 +02:00
Kevin Jahns
4dce0816a6 fix preferUntransformed sync 2017-07-31 14:41:40 +02:00
Kevin Jahns
5384bf4faf remove unneccesarry whenTransactionsFinished command 2017-07-31 14:01:34 +02:00
Kevin Jahns
454ac9ba16 remove ds.length == 0 condition for preferUntransformed 2017-07-31 03:19:47 +02:00
Kevin Jahns
e2ec53be65 implemented three-way sync for master-slave apps 2017-07-31 02:06:07 +02:00
Kevin Jahns
aa6edcfd9b add warning for message type when ArrayBuffer is expected 2017-07-31 01:13:52 +02:00
Kevin Jahns
f31ec9a8b8 fixed varUint encoding issue 2017-07-30 22:16:59 +02:00
Kevin Jahns
003fa735a0 enable y-map tests 2017-07-27 15:15:20 +02:00
Kevin Jahns
574f0c3269 fix logging message type 2017-07-27 14:49:36 +02:00
Kevin Jahns
eb4fb3a225 binary encoding bugfixes & export BinaryEncoder + BinaryDecoder 2017-07-24 15:37:04 +02:00
Kevin Jahns
c97130abc4 implement generateUserId for node & clients that dont support crypto 2017-07-22 18:37:48 +02:00
Kevin Jahns
a19cfa1465 redesigned connector protocol - enabled binary compression 2017-07-22 18:07:56 +02:00
Kevin Jahns
bb45abbb70 13.0.0-7 2017-07-22 01:16:50 +02:00
Kevin Jahns
67b47fd868 bugfix - sync step 2 also authenticates) 2017-07-22 01:15:13 +02:00
Kevin Jahns
2c18b9ffad 13.0.0-6 2017-07-21 23:56:13 +02:00
Kevin Jahns
a6b7d76544 bugfix: unable to deliver message. fixes receiving message before authentication 2017-07-21 23:55:11 +02:00
Kevin Jahns
442ea7ec70 13.0.0-5 2017-07-19 21:22:37 +02:00
Kevin Jahns
747da52c0b fix two clients syncing at the time 2017-07-19 21:19:41 +02:00
Kevin Jahns
6c37bd4463 Merge remote-tracking branch 'origin/master' into v13 2017-07-13 20:03:29 +02:00
Kevin Jahns
252bec0ad2 implemented binary encoding for all basic structs 2017-07-13 17:42:21 +02:00
Kevin Jahns
6c8876d282 remove option forwardToSyncing clients as it is no longer necessary - it was previously only used by y-webrtc 2017-07-13 00:48:14 +02:00
Kevin Jahns
3c317828d1 Use integer as userId instead of String 2017-07-13 00:37:35 +02:00
Kevin Jahns
cd3f4a72d6 13.0.0-4 2017-07-06 15:17:23 +02:00
Kevin Jahns
2c852c85c6 add node build 2017-07-06 15:16:13 +02:00
Kevin Jahns
434ec84837 13.0.0-3 2017-07-06 03:29:09 +02:00
Kevin Jahns
2b618cd83c change to correct main file 2017-07-06 03:28:06 +02:00
Kevin Jahns
f4327529b9 13.0.0-2 2017-07-05 18:41:26 +02:00
Kevin Jahns
67189f4d44 dont lint in postversion 2017-07-05 18:40:41 +02:00
Kevin Jahns
6225fb4dfd fix linting of examples 2017-07-05 18:33:16 +02:00
Kevin Jahns
a7550fe5d3 13.0.0-1 2017-07-05 18:12:35 +02:00
Kevin Jahns
9d9c84f40e fit y-memory in helper.js 2017-07-05 18:10:24 +02:00
Kevin Jahns
ae91902de3 Yjs throws "error" event in unexpected cases. fixes #72 2017-07-05 17:58:19 +02:00
Kevin Jahns
033d24eee7 use y-memory@v8 directory structure 2017-07-05 17:44:17 +02:00
Kevin Jahns
8abef69aa7 implemented named event handler 2017-07-05 17:01:21 +02:00
Kevin Jahns
7e4dedab38 always use generateUniqueUserId. fixes #74 2017-07-05 11:40:19 +02:00
Kevin Jahns
85e488bbe6 Throw proper error stack when observer function thrown an error - implements #75 2017-07-05 11:37:22 +02:00
Kevin Jahns
a6a321da10 fix textarea example to fit new directory structure 2017-07-05 11:34:28 +02:00
Kevin Jahns
008764ccdc remove dist submodule 2017-07-05 11:26:20 +02:00
Kevin Jahns
de5f4abe32 filter deleted ops only if gc is disabled 2017-07-04 04:59:07 -07:00
Kevin Jahns
382d06f6d4 reworked getOperations (decrease size of sent operations, fixe some gc issues). garbageCollectOperation now sets origin to the direct left operation, if possible 2017-07-03 23:19:11 -07:00
Kevin Jahns
66de422749 fix issues with new master-slave tests 2017-06-30 15:18:07 -07:00
Kevin Jahns
bbf5e39408 implemented client-server model (untested) 2017-06-30 14:07:14 -07:00
Kevin Jahns
c8bca15d72 13.0.0-0 2017-06-30 09:16:58 -07:00
Kevin Jahns
a64730e651 fix several sync issues. improve performance a bit by removing ds from first sync step 2017-06-29 15:04:36 -07:00
Kevin Jahns
409a9414f1 fix the "gc state" warning 2017-06-27 02:07:03 +02:00
Kevin Jahns
24facaab09 fix os comparison in compareUsers 2017-06-21 16:29:51 +02:00
Kevin Jahns
060549f2cb enable gc in random tests 2017-06-19 21:16:42 +02:00
Kevin Jahns
dfe3b0b1d1 Merge branch 'master' into v13 2017-06-19 10:48:16 +02:00
Kevin Jahns
e23154bec2 update dependencies 2017-06-16 01:04:58 +02:00
Kevin Jahns
1682d43c26 added chancejs to dependencies 2017-06-07 21:43:52 +02:00
Kevin Jahns
68c417fe6f fix gc timeout 2017-05-24 16:34:57 +02:00
Kevin Jahns
2ea163a5cf outsourced helper and test-connector 2017-05-22 13:57:16 +02:00
Kevin Jahns
020dacdad4 removed some unnecessary setTimeouts 2017-05-21 00:31:16 +02:00
Kevin Jahns
42abcc897c added examples 2017-05-19 02:22:00 +02:00
Kevin Jahns
0a321610aa use rollup for yjs 2017-05-16 18:35:30 +02:00
58 changed files with 30464 additions and 1769 deletions

12
.babelrc Normal file
View File

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

14
.gitignore vendored
View File

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

4
.gitmodules vendored
View File

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

View File

@@ -1,72 +0,0 @@
/* @flow */
type UserId = string
type Id = [UserId, number|string]
/*
type Struct = {
id: Id,
left?: Id,
right?: Id,
target?: Id,
struct: 'Insert' | 'Delete'
}*/
type Struct = Insertion | Deletion
type Operation = Struct
type Insertion = {
id: Id,
left: ?Id,
origin: ?Id,
right: ?Id,
parent: Id,
parentSub: ?Id,
opContent: ?Id,
content: ?any,
struct: 'Insert'
}
type Deletion = {
target: Id,
struct: 'Delete'
}
type MapStruct = {
id: Id,
type: TypeNames,
map: any
}
type ListStruct = {
id: Id,
type: TypeNames,
start: Id,
end: Id
}
type MessageSyncStep1 = {
type: 'sync step 1',
deleteSet: any,
stateSet: any
}
type MessageSyncStep2 = {
type: 'sync step 2',
os: Array<Operation>,
deleteSet: any,
stateSet: any
}
type MessageUpdate = {
type: 'update',
ops: Array<Operation>
}
type MessageSyncDone = {
type: 'sync done'
}
type Message = MessageSyncStep1 | MessageSyncStep2 | MessageUpdate | MessageSyncDone

View File

View File

@@ -1,34 +0,0 @@
/* @flow */
type YGlobal = {
utils: Object,
Struct: any,
AbstractDatabase: any,
AbstractConnector: any,
Transaction: any
}
type YConfig = {
db: Object,
connector: Object,
root: Object
}
type TypeName = 'array' | 'map' | 'text'
declare var YConcurrency_TestingMode : boolean
type Transaction<A> = Generator<any, A, any>
type SyncRole = 'master' | 'slave'
declare class Store {
find: (id:Id) => Transaction<any>;
put: (n:any) => Transaction<void>;
delete: (id:Id) => Transaction<void>;
findWithLowerBound: (start:Id) => Transaction<any>;
findWithUpperBound: (end:Id) => Transaction<any>;
findNext: (id:Id) => Transaction<any>;
findPrev: (id:Id) => Transaction<any>;
iterate: (t:any,start:?Id,end:?Id,gen:any) => Transaction<any>;
}

1
dist

Submodule dist deleted from 42aa7ec5c9

32
examples/ace/index.html Normal file
View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css" media="screen">
#aceContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.inserted {
position:absolute;
z-index:20;
background-color: #FFC107;
}
.deleted {
position:absolute;
z-index:20;
background-color: #FFC107;
}
</style>
</head>
<body>
<div id="aceContainer"></div>
<script src="../bower_components/yjs/y.js"></script>
<script src="../bower_components/ace-builds/src/ace.js"></script>
<script src="./index.js"></script>
</body>
</html>

24
examples/ace/index.js Normal file
View File

@@ -0,0 +1,24 @@
/* global Y, ace */
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'ace-example'
},
sourceDir: '/bower_components',
share: {
ace: 'Text' // y.share.textarea is of type Y.Text
}
}).then(function (y) {
window.yAce = y
// bind the textarea to a shared text element
var editor = ace.edit('aceContainer')
editor.setTheme('ace/theme/chrome')
editor.getSession().setMode('ace/mode/javascript')
y.share.ace.bindAce(editor)
})

19
examples/bower.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "yjs-examples",
"version": "0.0.0",
"homepage": "y-js.org",
"authors": [
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
],
"description": "Examples for Yjs",
"license": "MIT",
"ignore": [],
"dependencies": {
"quill": "^1.0.0-rc.2",
"ace": "~1.2.3",
"ace-builds": "~1.2.3",
"jquery": "~2.2.2",
"d3": "^3.5.16",
"codemirror": "^5.25.0"
}
}

23
examples/chat/index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<body>
<style>
#chat p span {
color: blue;
}
</style>
<div id="chat"></div>
<form id="chatform">
<input name="username" type="text" style="width:15%;">
<input name="message" type="text" style="width:60%;">
<input type="submit" value="Send">
</form>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-map/dist/y-map.js"></script>
<script src="../../../y-text/dist/y-text.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
<script src="./index.js"></script>
</body>
</html>

73
examples/chat/index.js Normal file
View File

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

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="codeMirrorContainer"></div>
<script src="../bower_components/yjs/y.js"></script>
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
<style>
.CodeMirror {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
/* global Y, CodeMirror */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'codemirror-example'
},
sourceDir: '/bower_components',
share: {
codemirror: 'Text' // y.share.codemirror is of type Y.Text
}
}).then(function (y) {
window.yCodeMirror = y
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
mode: 'javascript',
lineNumbers: true
})
y.share.codemirror.bindCodeMirror(editor)
})

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<body>
<style>
path {
fill: none;
stroke: blue;
stroke-width: 1px;
stroke-linejoin: round;
stroke-linecap: round;
}
</style>
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-map/dist/y-map.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="../bower_components/d3/d3.js"></script>
<script src="./index.js"></script>
</body>
</html>

84
examples/drawing/index.js Normal file
View File

@@ -0,0 +1,84 @@
/* globals Y, d3 */
'strict mode'
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'drawing-example',
url: 'localhost:1234'
},
sourceDir: '/bower_components',
share: {
drawing: 'Array'
}
}).then(function (y) {
window.yDrawing = y
var drawing = y.share.drawing
var renderPath = d3.svg.line()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] })
.interpolate('basis')
var svg = d3.select('#drawingCanvas')
.call(d3.behavior.drag()
.on('dragstart', dragstart)
.on('drag', drag)
.on('dragend', dragend))
// create line from a shared array object and update the line when the array changes
function drawLine (yarray) {
var line = svg.append('path').datum(yarray.toArray())
line.attr('d', renderPath)
yarray.observe(function (event) {
// we only implement insert events that are appended to the end of the array
event.values.forEach(function (value) {
line.datum().push(value)
})
line.attr('d', renderPath)
})
}
// call drawLine every time an array is appended
y.share.drawing.observe(function (event) {
if (event.type === 'insert') {
event.values.forEach(drawLine)
} else {
// just remove all elements (thats what we do anyway)
svg.selectAll('path').remove()
}
})
// draw all existing content
for (var i = 0; i < drawing.length; i++) {
drawLine(drawing.get(i))
}
// clear canvas on request
document.querySelector('#clearDrawingCanvas').onclick = function () {
drawing.delete(0, drawing.length)
}
var sharedLine = null
function dragstart () {
drawing.insert(drawing.length, [Y.Array])
sharedLine = drawing.get(drawing.length - 1)
}
// After one dragged event is recognized, we ignore them for 33ms.
var ignoreDrag = null
function drag () {
if (sharedLine != null && ignoreDrag == null) {
ignoreDrag = window.setTimeout(function () {
ignoreDrag = null
}, 33)
sharedLine.push([d3.mouse(this)])
}
}
function dragend () {
sharedLine = null
window.clearTimeout(ignoreDrag)
ignoreDrag = null
}
})

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.draggable {
cursor: move;
}
</style>
</head>
<body>
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
<g>
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
</g>
</svg>
<script src="../../y.js"></script>
<script src="../../../y-map/dist/y-map.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="../bower_components/d3/d3.js"></script>
<script src="./index.js"></script>
</body>
</html>

74
examples/jigsaw/index.js Normal file
View File

@@ -0,0 +1,74 @@
/* @flow */
/* global Y, d3 */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Puzzle-example',
url: 'http://localhost:1234'
},
share: {
piece1: 'Map',
piece2: 'Map',
piece3: 'Map',
piece4: 'Map'
}
}).then(function (y) {
window.yJigsaw = y
var origin // mouse start position - translation of piece
var drag = d3.behavior.drag()
.on('dragstart', function (params) {
// get the translation of the element
var translation = d3
.select(this)
.attr('transform')
.slice(10, -1)
.split(',')
.map(Number)
// mouse coordinates
var mouse = d3.mouse(this.parentNode)
origin = {
x: mouse[0] - translation[0],
y: mouse[1] - translation[1]
}
})
.on('drag', function () {
var mouse = d3.mouse(this.parentNode)
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
var y = mouse[1] - origin.y
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
})
.on('dragend', function (piece, i) {
// save the current translation of the puzzle piece
var mouse = d3.mouse(this.parentNode)
var x = mouse[0] - origin.x
var y = mouse[1] - origin.y
piece.set('translation', {x: x, y: y})
})
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
pieces
.classed('draggable', true)
.attr('transform', function (piece) {
var translation = piece.get('translation') || {x: 0, y: 0}
return 'translate(' + translation.x + ',' + translation.y + ')'
}).call(drag)
data.forEach(function (piece) {
piece.observe(function () {
// whenever a property of a piece changes, update the translation of the pieces
pieces
.transition()
.attr('transform', function (piece) {
var translation = piece.get('translation') || {x: 0, y: 0}
return 'translate(' + translation.x + ',' + translation.y + ')'
})
})
})
})

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div id="monacoContainer"></div>
<style>
#monacoContainer {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
</style>
<script src="../bower_components/yjs/y.js"></script>
<script src="../bower_components/y-array/y-array.js"></script>
<script src="../bower_components/y-text/y-text.js"></script>
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
<script src="../bower_components/y-memory/y-memory.js"></script>
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./index.js"></script>
</body>
</html>

30
examples/monaco/index.js Normal file
View File

@@ -0,0 +1,30 @@
/* global Y, monaco */
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
require(['vs/editor/editor.main'], function () {
// Initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'monaco-example'
},
sourceDir: '/bower_components',
share: {
monaco: 'Text' // y.share.monaco is of type Y.Text
}
}).then(function (y) {
window.yMonaco = y
// Create Monaco editor
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
language: 'javascript'
})
// Bind to y.share.monaco
y.share.monaco.bindMonaco(editor)
})
})

1173
examples/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
examples/package.json Normal file
View File

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

35
examples/quill/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<!-- quill does not include dist files! We are using the hosted version instead -->
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
<style>
#quill-container {
border: 1px solid gray;
box-shadow: 0px 0px 10px gray;
}
</style>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
<!-- quill does not include dist files! We are using the hosted version instead (see above)
<script src="../bower_components/quill/dist/quill.js"></script>
-->
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-richtext/dist/y-richtext.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="./index.js"></script>
</body>
</html>

40
examples/quill/index.js Normal file
View File

@@ -0,0 +1,40 @@
/* global Y, Quill */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'richtext-example-quill-1.0-test',
url: 'http://localhost:1234'
},
sourceDir: '/bower_components',
share: {
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
}
}).then(function (y) {
window.yQuill = y
// create quill element
window.quill = new Quill('#quill', {
modules: {
formula: true,
syntax: true,
toolbar: [
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }]
]
},
theme: 'snow'
})
// bind quill to richtext type
y.share.richtext.bind(window.quill)
})

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<!-- quill does not include dist files! We are using the hosted version instead -->
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
<style>
#quill-container {
border: 1px solid gray;
box-shadow: 0px 0px 10px gray;
}
</style>
</head>
<body>
<div id="quill-container">
<div id="quill">
</div>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
<!-- quill does not include dist files! We are using the hosted version instead (see above)
<script src="../bower_components/quill/dist/quill.js"></script>
-->
<script src="../bower_components/yjs/y.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
/* global Y, Quill */
// register yjs service worker
if ('serviceWorker' in navigator) {
// Register service worker
// it is important to copy yjs-sw-template to the root directory!
navigator.serviceWorker.register('./yjs-sw-template.js').then(function (reg) {
console.log('Yjs service worker registration succeeded. Scope is ' + reg.scope)
}).catch(function (err) {
console.error('Yjs service worker registration failed with error ' + err)
})
}
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'serviceworker',
room: 'ServiceWorkerExample2'
},
sourceDir: '/bower_components',
share: {
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
}
}).then(function (y) {
window.yServiceWorker = y
// create quill element
window.quill = new Quill('#quill', {
modules: {
formula: true,
syntax: true,
toolbar: [
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }]
]
},
theme: 'snow'
})
// bind quill to richtext type
y.share.richtext.bind(window.quill)
})

View File

@@ -0,0 +1,22 @@
/* eslint-env worker */
// copy and modify this file
self.DBConfig = {
name: 'indexeddb'
}
self.ConnectorConfig = {
name: 'websockets-client',
// url: '..',
options: {
jsonp: false
}
}
importScripts(
'/bower_components/yjs/y.js',
'/bower_components/y-memory/y-memory.js',
'/bower_components/y-indexeddb/y-indexeddb.js',
'/bower_components/y-websockets-client/y-websockets-client.js',
'/bower_components/y-serviceworker/yjs-sw-include.js'
)

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<body>
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
<script src="../../y.js"></script>
<script src="../../../y-array/y-array.js"></script>
<script src="../../../y-text/dist/y-text.js"></script>
<script src="../../../y-memory/y-memory.js"></script>
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
/* global Y */
// initialize a shared object. This function call returns a promise!
Y({
db: {
name: 'memory'
},
connector: {
name: 'websockets-client',
room: 'Textarea-example',
url: 'http://127.0.0.1:1234'
},
sourceDir: '/bower_components',
share: {
textarea: 'Text', // y.share.textarea is of type Y.Text
test: 'Array'
}
}).then(function (y) {
window.yTextarea = y
// bind the textarea to a shared text element
y.share.textarea.bind(document.getElementById('textfield'))
// thats it..
})

39
examples/xml/index.html Normal file
View File

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

21
examples/xml/index.js Normal file
View 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',
room: 'Xml-example'
},
sourceDir: '/bower_components',
share: {
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
}
}).then(function (y) {
window.yXml = y
// bind xml type to a dom, and put it in body
window.sharedDom = y.share.xml.getDom()
document.body.appendChild(window.sharedDom)
})

View File

@@ -1,214 +0,0 @@
var $ = require('gulp-load-plugins')()
var minimist = require('minimist')
var browserify = require('browserify')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
module.exports = function (gulp, helperOptions) {
var runSequence = require('run-sequence').use(gulp)
var options = minimist(process.argv.slice(2), {
string: ['modulename', 'export', 'name', 'port', 'testfiles', 'es6'],
default: {
moduleName: helperOptions.moduleName,
targetName: helperOptions.targetName,
export: 'ignore',
port: '8888',
testfiles: '**/*.spec.js',
es6: false,
browserify: helperOptions.browserify != null ? helperOptions.browserify : false,
includeRuntime: helperOptions.includeRuntime || false,
debug: false
}
})
if (options.es6 !== false) {
options.es6 = true
}
var files = {
dist: helperOptions.entry,
specs: helperOptions.specs,
src: './src/**/*.js'
}
if (options.includeRuntime) {
files.distEs5 = ['node_modules/regenerator/runtime.js', files.dist]
} else {
files.distEs5 = [files.dist]
}
var header = require('gulp-header')
var banner = ['/**',
' * <%= pkg.name %> - <%= pkg.description %>',
' * @version v<%= pkg.version %>',
' * @link <%= pkg.homepage %>',
' * @license <%= pkg.license %>',
' */',
''].join('\n')
gulp.task('dist:es5', function () {
var babelOptions = {
presets: ['es2015']
}
return (browserify({
entries: files.distEs5,
debug: true,
standalone: options.moduleName
}).transform('babelify', babelOptions)
.bundle()
.pipe(source(options.targetName))
.pipe(buffer())
.pipe($.sourcemaps.init({loadMaps: true}))
.pipe($.if(!options.debug, $.uglify().on('error', function (e) {
console.log('\x07', e.message, JSON.stringify(e)); return this.end()
})))
.pipe(header(banner, { pkg: require('./package.json') }))
.pipe($.sourcemaps.write('.'))
.pipe(gulp.dest('./dist/')))
})
gulp.task('dist:es6', function () {
return (browserify({
entries: files.dist,
debug: true,
standalone: options.moduleName
}).bundle()
.pipe(source(options.targetName))
.pipe(buffer())
.pipe($.sourcemaps.init({loadMaps: true}))
// .pipe($.uglify()) -- generators not yet supported see #448
.pipe($.rename({
extname: '.es6'
}))
.pipe(header(banner, { pkg: require('./package.json') }))
.pipe($.sourcemaps.write('.'))
.pipe(gulp.dest('./dist/')))
})
gulp.task('dist', ['dist:es6', 'dist:es5'])
gulp.task('watch:dist', function (cb) {
options.debug = true
gulp.src(['./README.md'])
.pipe($.watch('./README.md'))
.pipe(gulp.dest('./dist/'))
runSequence('dist', function () {
gulp.watch(files.src.concat('./README.md'), ['dist'])
cb()
})
})
gulp.task('dev:node', ['test'], function () {
gulp.watch(files.src, ['test'])
})
gulp.task('spec-build', function () {
var browserify = require('browserify')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
return browserify({
entries: files.specs, // .concat(files.distEs5),
debug: true
})// .transform('babelify', { presets: ['es2015'] })
.bundle()
.pipe(source('specs.js'))
.pipe(buffer())
// .pipe($.sourcemaps.init({loadMaps: true}))
// .pipe($.sourcemaps.write('.'))
.pipe(gulp.dest('./build/'))
})
gulp.task('dev:browser', ['spec-build'], function () {
gulp.watch(files.src, ['spec-build'])
return gulp.src('./build/specs.js')
.pipe($.jasmineBrowser.specRunner())
.pipe($.jasmineBrowser.server({port: options.port}))
})
gulp.task('test', function () {
return gulp.src(files.specs)
.pipe($.jasmine({
verbose: true,
includeStuckTrace: true
}))
})
gulp.task('updateSubmodule', function () {
return gulp.src('./package.json', {read: false})
.pipe($.shell([
'git submodule update --init',
'cd dist && git pull origin dist'
]))
})
gulp.task('bump', function (cb) {
gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
.pipe($.prompt.prompt({
type: 'checkbox',
name: 'bump',
message: 'What type of bump would you like to do?',
choices: ['patch', 'minor', 'major']
}, function (res) {
if (res.bump.length === 0) {
console.info('You have to select a bump type. Now I\'m going to use "patch" as bump type..')
}
var bumptype = res.bump[0]
if (bumptype === 'major') {
runSequence('bump_major', cb)
} else if (bumptype === 'minor') {
runSequence('bump_minor', cb)
} else {
runSequence('bump_patch', cb)
}
}))
})
gulp.task('bump_patch', function () {
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
.pipe($.bump({type: 'patch'}))
.pipe(gulp.dest('./'))
})
gulp.task('bump_minor', function () {
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
.pipe($.bump({type: 'minor'}))
.pipe(gulp.dest('./'))
})
gulp.task('bump_major', function () {
return gulp.src(['./package.json', './bower.json', './dist/bower.json'], {base: '.'})
.pipe($.bump({type: 'major'}))
.pipe(gulp.dest('./'))
})
gulp.task('publish_commits', function () {
return gulp.src('./package.json')
.pipe($.prompt.confirm({
message: 'Are you sure you want to publish this release?',
default: false
}))
.pipe($.shell([
'cp README.md dist',
'standard',
'echo "Deploying version <%= getVersion(file.path) %>"',
'git pull',
'cd ./dist/ && git add -A',
'cd ./dist/ && git commit -am "Deploy <%= getVersion(file.path) %>" -n',
'cd ./dist/ && git push origin HEAD:dist',
'cd ./dist/ && git tag -a v<%= getVersion(file.path) %> -m "Release <%= getVersion(file.path) %>"',
'cd ./dist/ && git push origin --tags',
'git commit -am "Release <%= getVersion(file.path) %>" -n',
'git push',
'npm publish',
'echo Finished'
], {
templateData: {
getVersion: function () {
return JSON.parse(String.fromCharCode.apply(null, this.file._contents)).version
}
}
}))
})
gulp.task('publish', function (cb) {
/* TODO: include 'test',*/
runSequence('updateSubmodule', 'bump', 'dist', 'publish_commits', cb)
})
}

View File

@@ -1,104 +0,0 @@
/* eslint-env node */
/** Gulp Commands
gulp command*
[--export ModuleType]
[--name ModuleName]
[--testport TestPort]
[--testfiles TestFiles]
Module name (ModuleName):
Compile this to "y.js" (default)
Supported module types (ModuleType):
- amd
- amdStrict
- common
- commonStrict
- ignore (default)
- system
- umd
- umdStrict
Test port (TestPort):
Serve the specs on port 8888 (default)
Test files (TestFiles):
Specify which specs to use!
Commands:
- build:deploy
Build this library for deployment (es6->es5, minified)
- dev:browser
Watch the ./src directory.
Builds the library on changes.
Starts an http-server and serves the test suite on http://127.0.0.1:8888.
- dev:node
Watch the ./src directory.
Builds and specs the library on changes.
Usefull to run with node-inspector.
`node-debug $(which gulp) dev:node
- test:
Test this library
*/
var gulp = require('gulp')
var $ = require('gulp-load-plugins')()
var runSequence = require('run-sequence').use(gulp)
require('./gulpfile.helper.js')(gulp, {
polyfills: [],
entry: './src/y.js',
targetName: 'y.js',
moduleName: 'Y',
includeRuntime: true,
specs: [
'./src/Database.spec.js',
'../y-array/src/Array.spec.js',
'../y-map/src/Map.spec.js'
]
})
gulp.task('dev:examples', ['watch:dist'], function () {
// watch all distfiles and copy them to bower_components
var distfiles = ['./dist/*.{js,es6}', './dist/*.{js,es6}.map', '../y-*/dist/*.{js,es6}', '../y-*/dist/*.{js,es6}.map']
gulp.src(distfiles)
.pipe($.watch(distfiles))
.pipe($.rename(function (path) {
var dir = path.dirname.split(/[\\\/]/)[0]
console.log(JSON.stringify(path))
path.dirname = dir === '.' ? 'yjs' : dir
}))
.pipe(gulp.dest('./dist/Examples/bower_components/'))
return $.serve('dist/Examples/')()
})
gulp.task('default', ['updateSubmodule'], function (cb) {
gulp.src('package.json')
.pipe($.prompt.prompt({
type: 'checkbox',
name: 'tasks',
message: 'Which tasks would you like to run?',
choices: [
'test Test this project',
'dev:examples Serve the examples directory in ./dist/',
'dev:browser Watch files & serve the testsuite for the browser',
'dev:nodejs Watch filse & test this project with nodejs',
'bump Bump the current state of the project',
'publish Publish this project. Creates a github tag',
'dist Build the distribution files'
]
}, function (res) {
var tasks = res.tasks.map(function (task) {
return task.split(' ')[0]
})
if (tasks.length > 0) {
console.info('gulp ' + tasks.join(' '))
runSequence(tasks, cb)
} else {
console.info('Ok, .. goodbye')
}
}))
})

3649
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

47
rollup.browser.js Normal file
View File

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

26
rollup.node.js Normal file
View File

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

20
rollup.test.js Normal file
View File

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

View File

@@ -1,40 +1,18 @@
/* @flow */ import { BinaryEncoder, BinaryDecoder } from './Encoding.js'
'use strict' import { sendSyncStep1, computeMessageSyncStep1, computeMessageSyncStep2, computeMessageUpdate } from './MessageHandler.js'
function canRead (auth) { return auth === 'read' || auth === 'write' } export default function extendConnector (Y/* :any */) {
function canWrite (auth) { return auth === 'write' }
module.exports = function (Y/* :any */) {
class AbstractConnector { class AbstractConnector {
/* ::
y: YConfig;
role: SyncRole;
connections: Object;
isSynced: boolean;
userEventListeners: Array<Function>;
whenSyncedListeners: Array<Function>;
currentSyncTarget: ?UserId;
syncingClients: Array<UserId>;
forwardToSyncingClients: boolean;
debug: boolean;
syncStep2: Promise;
userId: UserId;
send: Function;
broadcast: Function;
broadcastOpBuffer: Array<Operation>;
protocolVersion: number;
*/
/* /*
opts contains the following information: opts contains the following information:
role : String Role of this client ("master" or "slave") role : String Role of this client ("master" or "slave")
userId : String Uniquely defines the user.
debug: Boolean Whether to print debug messages (optional)
*/ */
constructor (y, opts) { constructor (y, opts) {
this.y = y this.y = y
if (opts == null) { if (opts == null) {
opts = {} opts = {}
} }
this.opts = opts
// Prefer to receive untransformed operations. This does only work if // Prefer to receive untransformed operations. This does only work if
// this client receives operations from only one other client. // this client receives operations from only one other client.
// In particular, this does not work with y-webrtc. // In particular, this does not work with y-webrtc.
@@ -51,30 +29,18 @@ module.exports = function (Y/* :any */) {
this.logMessage = Y.debug('y:connector-message') this.logMessage = Y.debug('y:connector-message')
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
this.role = opts.role this.role = opts.role
this.connections = {} this.connections = new Map()
this.isSynced = false this.isSynced = false
this.userEventListeners = [] this.userEventListeners = []
this.whenSyncedListeners = [] this.whenSyncedListeners = []
this.currentSyncTarget = null this.currentSyncTarget = null
this.syncingClients = []
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
this.debug = opts.debug === true this.debug = opts.debug === true
this.syncStep2 = Promise.resolve()
this.broadcastOpBuffer = [] this.broadcastOpBuffer = []
this.protocolVersion = 11 this.protocolVersion = 11
this.authInfo = opts.auth || null this.authInfo = opts.auth || null
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
if (opts.generateUserId === true) { if (opts.generateUserId !== false) {
this.setUserId(Y.utils.generateGuid()) this.setUserId(Y.utils.generateUserId())
}
}
resetAuth (auth) {
if (this.authInfo !== auth) {
this.authInfo = auth
this.broadcast({
type: 'auth',
auth: this.authInfo
})
} }
} }
reconnect () { reconnect () {
@@ -83,25 +49,27 @@ module.exports = function (Y/* :any */) {
} }
disconnect () { disconnect () {
this.log('discronnecting..') this.log('discronnecting..')
this.connections = {} this.connections = new Map()
this.isSynced = false this.isSynced = false
this.currentSyncTarget = null this.currentSyncTarget = null
this.syncingClients = []
this.whenSyncedListeners = [] this.whenSyncedListeners = []
this.y.db.stopGarbageCollector() this.y.db.stopGarbageCollector()
return this.y.db.whenTransactionsFinished() return this.y.db.whenTransactionsFinished()
} }
repair () { repair () {
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues') this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
for (var name in this.connections) { this.connections.forEach(user => { user.isSynced = false })
this.connections[name].isSynced = false
}
this.isSynced = false this.isSynced = false
this.currentSyncTarget = null this.currentSyncTarget = null
this.findNextSyncTarget() this.findNextSyncTarget()
} }
setUserId (userId) { setUserId (userId) {
if (this.userId == null) { if (this.userId == null) {
if (!Number.isInteger(userId)) {
let err = new Error('UserId must be an integer!')
this.y.emit('error', err)
throw err
}
this.log('Set userId to "%s"', userId) this.log('Set userId to "%s"', userId)
this.userId = userId this.userId = userId
return this.y.db.setUserId(userId) return this.y.db.setUserId(userId)
@@ -113,19 +81,16 @@ module.exports = function (Y/* :any */) {
this.userEventListeners.push(f) this.userEventListeners.push(f)
} }
removeUserEventListener (f) { removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => { f !== g }) this.userEventListeners = this.userEventListeners.filter(g => f !== g)
} }
userLeft (user) { userLeft (user) {
if (this.connections[user] != null) { if (this.connections.has(user)) {
this.log('User left: %s', user) this.log('%s: User left %s', this.userId, user)
delete this.connections[user] this.connections.delete(user)
if (user === this.currentSyncTarget) { if (user === this.currentSyncTarget) {
this.currentSyncTarget = null this.currentSyncTarget = null
this.findNextSyncTarget() this.findNextSyncTarget()
} }
this.syncingClients = this.syncingClients.filter(function (cli) {
return cli !== user
})
for (var f of this.userEventListeners) { for (var f of this.userEventListeners) {
f({ f({
action: 'userLeft', action: 'userLeft',
@@ -138,14 +103,21 @@ module.exports = function (Y/* :any */) {
if (role == null) { if (role == null) {
throw new Error('You must specify the role of the joined user!') throw new Error('You must specify the role of the joined user!')
} }
if (this.connections[user] != null) { if (this.connections.has(user)) {
throw new Error('This user already joined!') throw new Error('This user already joined!')
} }
this.log('User joined: %s', user) this.log('%s: User joined %s', this.userId, user)
this.connections[user] = { this.connections.set(user, {
uid: user,
isSynced: false, isSynced: false,
role: role role: role,
} processAfterAuth: [],
auth: null,
receivedSyncStep2: false
})
let defer = {}
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
this.connections.get(user).syncStep2 = defer
for (var f of this.userEventListeners) { for (var f of this.userEventListeners) {
f({ f({
action: 'userJoined', action: 'userJoined',
@@ -167,13 +139,13 @@ module.exports = function (Y/* :any */) {
} }
} }
findNextSyncTarget () { findNextSyncTarget () {
if (this.currentSyncTarget != null) { if (this.currentSyncTarget != null || this.role === 'slave') {
return // "The current sync has not finished!" return // "The current sync has not finished or this is controlled by a master!"
} }
var syncUser = null var syncUser = null
for (var uid in this.connections) { for (var [uid, user] of this.connections) {
if (!this.connections[uid].isSynced) { if (!user.isSynced) {
syncUser = uid syncUser = uid
break break
} }
@@ -181,28 +153,15 @@ module.exports = function (Y/* :any */) {
var conn = this var conn = this
if (syncUser != null) { if (syncUser != null) {
this.currentSyncTarget = syncUser this.currentSyncTarget = syncUser
this.y.db.requestTransaction(function *() { sendSyncStep1(this, syncUser)
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 { } else {
if (!conn.isSynced) { if (!conn.isSynced) {
this.y.db.requestTransaction(function * () { this.y.db.requestTransaction(function * () {
if (!conn.isSynced) { if (!conn.isSynced) {
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called // it is crucial that isSynced is set at the time garbageCollectAfterSync is called
conn.isSynced = true conn.isSynced = true
yield* this.garbageCollectAfterSync() // It is safer to remove this!
// TODO: remove: yield * this.garbageCollectAfterSync()
// call whensynced listeners // call whensynced listeners
for (var f of conn.whenSyncedListeners) { for (var f of conn.whenSyncedListeners) {
f() f()
@@ -213,13 +172,19 @@ module.exports = function (Y/* :any */) {
} }
} }
} }
send (uid, message) { send (uid, buffer) {
this.log('Send \'%s\' to %s', message.type, uid) if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
this.logMessage('Message: %j', message) throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
} }
broadcast (message) { this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid)
this.log('Broadcast \'%s\'', message.type) this.logMessage('Message: %Y', buffer)
this.logMessage('Message: %j', message) }
broadcast (buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
}
this.log('%s: Broadcast \'%y\'', this.userId, buffer)
this.logMessage('Message: %Y', buffer)
} }
/* /*
Buffer operations, and broadcast them when ready. Buffer operations, and broadcast them when ready.
@@ -231,20 +196,23 @@ module.exports = function (Y/* :any */) {
var self = this var self = this
function broadcastOperations () { function broadcastOperations () {
if (self.broadcastOpBuffer.length > 0) { if (self.broadcastOpBuffer.length > 0) {
self.broadcast({ let encoder = new BinaryEncoder()
type: 'update', encoder.writeVarString(self.opts.room)
ops: self.broadcastOpBuffer encoder.writeVarString('update')
}) let ops = self.broadcastOpBuffer
self.broadcastOpBuffer = [] self.broadcastOpBuffer = []
let length = ops.length
encoder.writeUint32(length)
for (var i = 0; i < length; i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, op)
}
self.broadcast(encoder.createBuffer())
} }
} }
if (this.broadcastOpBuffer.length === 0) { if (this.broadcastOpBuffer.length === 0) {
this.broadcastOpBuffer = ops this.broadcastOpBuffer = ops
if (this.y.db.transactionInProgress) {
this.y.db.whenTransactionsFinished().then(broadcastOperations) this.y.db.whenTransactionsFinished().then(broadcastOperations)
} else {
setTimeout(broadcastOperations, 0)
}
} else { } else {
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops) this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
} }
@@ -252,147 +220,67 @@ module.exports = function (Y/* :any */) {
/* /*
You received a raw message, and you know that it is intended for Yjs. Then call this function. You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/ */
receiveMessage (sender/* :UserId */, message/* :Message */) { receiveMessage (sender, buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array!')
}
if (sender === this.userId) { if (sender === this.userId) {
return
}
let decoder = new BinaryDecoder(buffer)
let encoder = new BinaryEncoder()
let roomname = decoder.readVarString() // read room name
encoder.writeVarString(roomname)
let messageType = decoder.readVarString()
let senderConn = this.connections.get(sender)
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender)
this.logMessage('Message: %Y', buffer)
if (senderConn == null) {
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([sender, buffer])
// check auth
return this.checkAuth(auth, this.y, sender).then(authPermissions => {
senderConn.auth = authPermissions
this.y.emit('userAuthenticated', {
user: senderConn.uid,
auth: authPermissions
})
return senderConn.syncStep2.promise
}).then(() => {
if (senderConn.processAfterAuth == null) {
return Promise.resolve() return Promise.resolve()
} }
this.log('Receive \'%s\' from %s', message.type, sender) let messages = senderConn.processAfterAuth
this.logMessage('Message: %j', message) senderConn.processAfterAuth = null
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) { return Promise.all(messages.map(m =>
this.log( this.receiveMessage(m[0], m[1])
`You tried to sync with a yjs instance that has a different protocol version ))
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
`)
this.send(sender, {
type: 'sync stop',
protocolVersion: this.protocolVersion
})
return Promise.reject('Incompatible protocol version')
}
if (message.auth != null && this.connections[sender] != null) {
// authenticate using auth in message
var auth = this.checkAuth(message.auth, this.y)
this.connections[sender].auth = auth
auth.then(auth => {
for (var f of this.userEventListeners) {
f({
action: 'userAuthenticated',
user: sender,
auth: auth
}) })
} }
})
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
// authenticate without otherwise
this.connections[sender].auth = this.checkAuth(null, this.y)
}
if (this.connections[sender] != null && this.connections[sender].auth != null) {
return this.connections[sender].auth.then((auth) => {
if (message.type === 'sync step 1' && canRead(auth)) {
let conn = this
let m = message
this.y.db.requestTransaction(function *() {
var currentStateSet = yield* this.getStateSet()
if (canWrite(auth)) {
yield* this.applyDeleteSet(m.deleteSet)
} }
var ds = yield* this.getDeleteSet() if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
var answer = { // cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
type: 'sync step 2', computeMessageSyncStep1(decoder, encoder, this, senderConn, sender)
stateSet: currentStateSet, return this.y.db.whenTransactionsFinished()
deleteSet: ds, } else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
protocolVersion: this.protocolVersion, return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
auth: this.authInfo } else if (messageType === 'update' && senderConn.auth === 'write') {
} return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
if (message.preferUntransformed === true && Object.keys(m.stateSet).length === 0) {
answer.osUntransformed = yield* this.getOperationsUntransformed()
} else { } else {
answer.os = yield* this.getOperations(m.stateSet) Promise.reject(new Error('Unable to receive message'))
}
conn.send(sender, answer)
if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender)
setTimeout(function () {
conn.syncingClients = conn.syncingClients.filter(function (cli) {
return cli !== sender
})
conn.send(sender, {
type: 'sync done'
})
}, 5000) // TODO: conn.syncingClientDuration)
} else {
conn.send(sender, {
type: 'sync done'
})
}
})
} else if (message.type === 'sync step 2' && canWrite(auth)) {
var db = this.y.db
var defer = {}
defer.promise = new Promise(function (resolve) {
defer.resolve = resolve
})
this.syncStep2 = defer.promise
let m /* :MessageSyncStep2 */ = message
db.requestTransaction(function * () {
yield* this.applyDeleteSet(m.deleteSet)
if (m.osUntransformed != null) {
yield* this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
} else {
this.store.apply(m.os)
}
/*
* This just sends the complete hb after some time
* Mostly for debugging..
*
db.requestTransaction(function * () {
var ops = yield* this.getOperations(m.stateSet)
if (ops.length > 0) {
if (!broadcastHB) { // TODO: consider to broadcast here..
conn.send(sender, {
type: 'update',
ops: ops
})
} else {
// broadcast only once!
conn.broadcastOps(ops)
}
}
})
*/
defer.resolve()
})
} else if (message.type === 'sync done') {
var self = this
this.syncStep2.then(function () {
self._setSyncedWith(sender)
})
} else if (message.type === 'update' && canWrite(auth)) {
if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) {
this.send(client, message)
}
}
if (this.y.db.forwardAppliedOperations) {
var delops = message.ops.filter(function (o) {
return o.struct === 'Delete'
})
if (delops.length > 0) {
this.broadcastOps(delops)
}
}
this.y.db.apply(message.ops)
}
})
} else {
return Promise.reject('Unable to deliver message')
} }
} }
_setSyncedWith (user) { _setSyncedWith (user) {
var conn = this.connections[user] var conn = this.connections.get(user)
if (conn != null) { if (conn != null) {
conn.isSynced = true conn.isSynced = true
} }

View File

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

View File

@@ -1,7 +1,7 @@
/* @flow */ /* @flow */
'use strict' 'use strict'
module.exports = function (Y /* :any */) { export default function extendDatabase (Y /* :any */) {
/* /*
Partial definition of an OperationStore. Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations. TODO: name it Database, operation store only holds operations.
@@ -39,14 +39,15 @@ module.exports = function (Y /* :any */) {
*/ */
constructor (y, opts) { constructor (y, opts) {
this.y = y this.y = y
opts.gc = opts.gc === true
this.dbOpts = opts this.dbOpts = opts
var os = this var os = this
this.userId = null this.userId = null
var resolve var resolve_
this.userIdPromise = new Promise(function (r) { this.userIdPromise = new Promise(function (resolve) {
resolve = r resolve_ = resolve
}) })
this.userIdPromise.resolve = resolve this.userIdPromise.resolve = resolve_
// whether to broadcast all applied operations (insert & delete hook) // whether to broadcast all applied operations (insert & delete hook)
this.forwardAppliedOperations = false this.forwardAppliedOperations = false
// E.g. this.listenersById[id] : Array<Listener> // E.g. this.listenersById[id] : Array<Listener>
@@ -71,7 +72,7 @@ module.exports = function (Y /* :any */) {
this.waitingTransactions = [] this.waitingTransactions = []
this.transactionInProgress = false this.transactionInProgress = false
this.transactionIsFlushed = false this.transactionIsFlushed = false
if (typeof YConcurrency_TestingMode !== 'undefined') { if (typeof YConcurrencyTestingMode !== 'undefined') {
this.executeOrder = [] this.executeOrder = []
} }
this.gc1 = [] // first stage this.gc1 = [] // first stage
@@ -79,7 +80,7 @@ module.exports = function (Y /* :any */) {
function garbageCollect () { function garbageCollect () {
return os.whenTransactionsFinished().then(function () { return os.whenTransactionsFinished().then(function () {
if (os.gc1.length > 0 || os.gc2.length > 0) { if (os.gcTimeout > 0 && (os.gc1.length > 0 || os.gc2.length > 0)) {
if (!os.y.connector.isSynced) { if (!os.y.connector.isSynced) {
console.warn('gc should be empty when not synced!') console.warn('gc should be empty when not synced!')
} }
@@ -117,9 +118,9 @@ module.exports = function (Y /* :any */) {
this.startRepairCheck() this.startRepairCheck()
} }
startGarbageCollector () { startGarbageCollector () {
this.gc = this.dbOpts.gc == null || this.dbOpts.gc this.gc = this.dbOpts.gc
if (this.gc) { if (this.gc) {
this.gcTimeout = !this.dbOpts.gcTimeout ? 50000 : this.dbOpts.gcTimeout this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
} else { } else {
this.gcTimeout = -1 this.gcTimeout = -1
} }
@@ -177,7 +178,7 @@ module.exports = function (Y /* :any */) {
}) })
} }
addToDebug () { addToDebug () {
if (typeof YConcurrency_TestingMode !== 'undefined') { if (typeof YConcurrencyTestingMode !== 'undefined') {
var command /* :string */ = Array.prototype.map.call(arguments, function (s) { var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
if (typeof s === 'string') { if (typeof s === 'string') {
return s return s
@@ -265,7 +266,7 @@ module.exports = function (Y /* :any */) {
} }
} }
* destroy () { * destroy () {
clearInterval(this.gcInterval) clearTimeout(this.gcInterval)
this.gcInterval = null this.gcInterval = null
this.stopRepairCheck() this.stopRepairCheck()
} }
@@ -305,10 +306,12 @@ module.exports = function (Y /* :any */) {
* check if it is an expected op (otherwise wait for it) * check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied * check if was deleted, apply a delete operation after op was applied
*/ */
apply (ops) { applyOperations (decoder) {
this.opsReceivedTimestamp = new Date() this.opsReceivedTimestamp = new Date()
for (var i = 0; i < ops.length; i++) { let length = decoder.readUint32()
var o = ops[i]
for (var i = 0; i < length; i++) {
let o = Y.Struct.binaryDecodeOperation(decoder)
if (o.id == null || o.id[0] !== this.y.connector.userId) { if (o.id == null || o.id[0] !== this.y.connector.userId) {
var required = Y.Struct[o.struct].requiredOps(o) var required = Y.Struct[o.struct].requiredOps(o)
if (o.requires != null) { if (o.requires != null) {
@@ -511,12 +514,12 @@ module.exports = function (Y /* :any */) {
whenTransactionsFinished () { whenTransactionsFinished () {
if (this.transactionInProgress) { if (this.transactionInProgress) {
if (this.transactionsFinished == null) { if (this.transactionsFinished == null) {
var resolve var resolve_
var promise = new Promise(function (r) { var promise = new Promise(function (resolve) {
resolve = r resolve_ = resolve
}) })
this.transactionsFinished = { this.transactionsFinished = {
resolve: resolve, resolve: resolve_,
promise: promise promise: promise
} }
} }
@@ -589,7 +592,7 @@ module.exports = function (Y /* :any */) {
op.type = typedefinition[0].name op.type = typedefinition[0].name
this.requestTransaction(function * () { this.requestTransaction(function * () {
if (op.id[0] === '_') { if (op.id[0] === 0xFFFFFF) {
yield * this.setOperation(op) yield * this.setOperation(op)
} else { } else {
yield * this.applyCreatedOperations([op]) yield * this.applyCreatedOperations([op])

133
src/Encoding.js Normal file
View File

@@ -0,0 +1,133 @@
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) {
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()]
}
}
}

185
src/MessageHandler.js Normal file
View File

@@ -0,0 +1,185 @@
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) {
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) {
conn.broadcastOps(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()
}
// 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
})
if (conn.role === 'slave') {
sendSyncStep1(conn, sender)
}
return conn.y.db.whenTransactionsFinished()
}
export function logSS (decoder, strBuilder) {
strBuilder.push(' == SS: \n')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
strBuilder.push(` ${user}: ${clock}\n`)
}
}
export function logOS (decoder, strBuilder) {
strBuilder.push(' == OS: \n')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder)
strBuilder.push(JSON.stringify(op) + '\n')
}
}
export function logDS (decoder, strBuilder) {
strBuilder.push(' == DS: \n')
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint()
strBuilder.push(` User: ${user}: `)
let len2 = decoder.readVarUint()
for (let j = 0; j < len2; j++) {
let from = decoder.readVarUint()
let to = decoder.readVarUint()
let gc = decoder.readUint8() === 1
strBuilder.push(`[${from}, ${to}, ${gc}]`)
}
}
}
export function logMessageSyncStep2 (decoder, strBuilder) {
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n')
let osTransformed = decoder.readUint8() === 1
strBuilder.push(' - osUntransformed: ' + osTransformed + '\n')
logOS(decoder, strBuilder)
if (osTransformed) {
logSS(decoder, strBuilder)
}
logDS(decoder, strBuilder)
}
export function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
var db = conn.y.db
let defer = senderConn.syncStep2
// apply operations first
db.requestTransaction(function * () {
let osUntransformed = decoder.readUint8()
if (osUntransformed === 1) {
yield * this.applyOperationsUntransformed(decoder)
} else {
this.store.applyOperations(decoder)
}
})
// then apply ds
db.requestTransaction(function * () {
yield * this.applyDeleteSet(decoder)
})
return db.whenTransactionsFinished().then(() => {
conn._setSyncedWith(sender)
defer.resolve()
})
}

View File

@@ -1,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.YConcurrency_TestingMode = true
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
g.describeManyTimes = function describeManyTimes (times, name, f) {
for (var i = 0; i < times; i++) {
describe(name, f)
}
}
/*
Wait for a specified amount of time (in ms). defaults to 5ms
*/
function wait (t) {
if (t == null) {
t = 0
}
return new Promise(function (resolve) {
setTimeout(function () {
resolve()
}, t)
})
}
g.wait = wait
g.databases = ['memory']
if (typeof window !== 'undefined') {
g.databases.push('indexeddb')
} else {
g.databases.push('leveldb')
}
/*
returns a random element of o.
works on Object, and Array
*/
function getRandom (o) {
if (o instanceof Array) {
return o[Math.floor(Math.random() * o.length)]
} else if (o.constructor === Object) {
return o[getRandom(Object.keys(o))]
}
}
g.getRandom = getRandom
function getRandomNumber (n) {
if (n == null) {
n = 9999
}
return Math.floor(Math.random() * n)
}
g.getRandomNumber = getRandomNumber
function getRandomString () {
var chars = 'abcdefghijklmnopqrstuvwxyzäüöABCDEFGHIJKLMNOPQRSTUVWXYZÄÜÖ'
var char = chars[getRandomNumber(chars.length)] // ü\n\n\n\n\n\n\n'
var length = getRandomNumber(7)
var string = ''
for (var i = 0; i < length; i++) {
string += char
}
return string
}
g.getRandomString = getRandomString
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) {
g.generateRandomSeed() // create a new seed, so we can re-create the behavior
for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
var r = Math.random()
if (r > 0.95) {
// 10% chance of toggling concurrent user interactions.
// There will be an artificial delay until ops can be executed by the type,
// therefore, operations of the database will be (pre)transformed until user operations arrive
yield (function simulateConcurrentUserInteractions (type) {
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
type = type.y
}
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
type.eventHandler.awaiting = 1
type.eventHandler._debuggingAwaiting = true
} else {
// fixAwaitingInType will handle _debuggingAwaiting
return fixAwaitingInType(type)
}
})(getRandom(objects))
} else if (r >= 0.5) {
// 40% chance to flush
yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0)
} else if (noReconnect || r >= 0.05) {
// 45% chance to create operation
var done = getRandom(transactions)(getRandom(objects))
if (done != null) {
yield done
} else {
yield wait()
}
yield Y.utils.globalRoom.whenTransactionsFinished()
} else {
// 5% chance to disconnect/reconnect
var u = getRandom(users)
yield Promise.all(objects.map(fixAwaitingInType))
if (u.connector.isDisconnected()) {
yield u.reconnect()
} else {
yield u.disconnect()
}
yield Promise.all(objects.map(fixAwaitingInType))
}
}
}
function fixAwaitingInType (type) {
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
type = type.y
}
return new Promise(function (resolve) {
type.os.whenTransactionsFinished().then(function () {
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
type.os.requestTransaction(function * () {
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
type.eventHandler._debuggingAwaiting = false
yield* type.eventHandler.awaitOps(this, function * () { /* mock function */ })
}
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
})
})
})
}
g.fixAwaitingInType = fixAwaitingInType
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
yield Y.utils.globalRoom.flushAll()
yield Promise.all(objects.map(fixAwaitingInType))
})
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
yield Promise.all(objects.map(fixAwaitingInType))
yield Y.utils.globalRoom.flushAll()
yield Promise.all(objects.map(fixAwaitingInType))
for (var u in users) {
yield Promise.all(objects.map(fixAwaitingInType))
yield users[u].reconnect()
yield Promise.all(objects.map(fixAwaitingInType))
}
yield Promise.all(objects.map(fixAwaitingInType))
yield Y.utils.globalRoom.flushAll()
yield Promise.all(objects.map(fixAwaitingInType))
yield g.garbageCollectAllUsers(users)
})
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
yield Y.utils.globalRoom.flushAll()
yield Promise.all(objects.map(fixAwaitingInType))
for (var u in users) {
// TODO: here, we enforce that two users never sync at the same time with u[0]
// enforce that in the connector itself!
yield users[u].reconnect()
}
yield Y.utils.globalRoom.flushAll()
yield Promise.all(objects.map(fixAwaitingInType))
yield g.garbageCollectAllUsers(users)
})
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
yield Y.utils.globalRoom.flushAll()
for (var i in users) {
yield users[i].db.emptyGarbageCollector()
}
})
g.compareAllUsers = async(function * compareAllUsers (users) {
var s1, s2 // state sets
var ds1, ds2 // delete sets
var allDels1, allDels2 // all deletions
var db1 = [] // operation store of user1
yield Y.utils.globalRoom.flushAll()
yield g.garbageCollectAllUsers(users)
yield Y.utils.globalRoom.flushAll()
// disconnect, then reconnect all users
// We do this to make sure that the gc is updated by everyone
for (var i = 0; i < users.length; i++) {
yield users[i].disconnect()
yield wait()
yield users[i].reconnect()
}
yield wait()
yield Y.utils.globalRoom.flushAll()
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
function * t1 () {
s1 = yield* this.getStateSet()
ds1 = yield* this.getDeleteSet()
allDels1 = []
yield* this.ds.iterate(this, null, null, function * (d) {
allDels1.push(d)
})
}
function * t2 () {
s2 = yield* this.getStateSet()
ds2 = yield* this.getDeleteSet()
allDels2 = []
yield* this.ds.iterate(this, null, null, function * (d) {
allDels2.push(d)
})
}
var buffer = Y.utils.globalRoom.buffers
for (var name in buffer) {
if (buffer[name].length > 0) {
// not all ops were transmitted..
debugger // eslint-disable-line
}
}
for (var uid = 0; uid < users.length; uid++) {
var u = users[uid]
u.db.requestTransaction(function * () {
var sv = yield* this.getStateVector()
for (var s of sv) {
yield* this.updateState(s.user)
}
// compare deleted ops against deleteStore
yield* this.os.iterate(this, null, null, function * (o) {
if (o.deleted === true) {
expect(yield* this.isDeleted(o.id)).toBeTruthy()
}
})
// compare deleteStore against deleted ops
var ds = []
yield* this.ds.iterate(this, null, null, function * (d) {
ds.push(d)
})
for (var j in ds) {
var d = ds[j]
for (var i = 0; i < d.len; i++) {
var o = yield* this.getInsertion([d.id[0], d.id[1] + i])
// gc'd or deleted
if (d.gc) {
expect(o).toBeFalsy()
} else {
expect(o.deleted).toBeTruthy()
}
}
}
})
// compare allDels tree
if (s1 == null) {
u.db.requestTransaction(function * () {
yield* t1.call(this)
yield* this.os.iterate(this, null, null, function * (o) {
o = Y.utils.copyObject(o)
delete o.origin
delete o.originOf
db1.push(o)
})
})
} else {
u.db.requestTransaction(function * () {
yield* t2.call(this)
var db2 = []
yield* this.os.iterate(this, null, null, function * (o) {
o = Y.utils.copyObject(o)
delete o.origin
delete o.originOf
db2.push(o)
})
expect(s1).toEqual(s2)
expect(allDels1).toEqual(allDels2) // inner structure
expect(ds1).toEqual(ds2) // exported structure
db2.forEach((o, i) => {
expect(db1[i]).toEqual(o)
})
})
}
yield u.db.whenTransactionsFinished()
}
})
g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
if (Y.utils.globalRoom.users[0] != null) {
yield Y.utils.globalRoom.flushAll()
}
// destroy old users
for (var u in Y.utils.globalRoom.users) {
Y.utils.globalRoom.users[u].y.destroy()
}
self.users = null
var promises = []
for (var i = 0; i < numberOfUsers; i++) {
promises.push(Y({
db: {
name: database,
namespace: 'User ' + i,
cleanStart: true,
gcTimeout: -1,
gc: true,
repairCheckInterval: -1
},
connector: {
name: 'Test',
debug: false
},
share: {
root: initType || 'Map'
}
}))
}
self.users = yield Promise.all(promises)
self.types = self.users.map(function (u) { return u.share.root })
return self.users
})
/*
Until async/await arrives in js, we use this function to wait for promises
by yielding them.
*/
function async (makeGenerator) {
return function (arg) {
var generator = makeGenerator.apply(this, arguments)
function handle (result) {
if (result.done) return Promise.resolve(result.value)
return Promise.resolve(result.value).then(function (res) {
return handle(generator.next(res))
}, function (err) {
return handle(generator.throw(err))
})
}
try {
return handle(generator.next())
} catch (ex) {
generator.throw(ex)
// return Promise.reject(ex)
}
}
}
g.async = async
function logUsers (self) {
if (self.constructor === Array) {
self = {users: self}
}
self.users[0].db.logTable()
self.users[1].db.logTable()
self.users[2].db.logTable()
}
g.logUsers = logUsers

View File

@@ -1,5 +1,7 @@
/* @flow */ const CDELETE = 0
'use strict' const CINSERT = 1
const CLIST = 2
const CMAP = 3
/* /*
An operation also defines the structure of a type. This is why operation and An operation also defines the structure of a type. This is why operation and
@@ -19,8 +21,22 @@
* requiredOps * requiredOps
- Operations that are required to execute this operation. - Operations that are required to execute this operation.
*/ */
module.exports = function (Y/* :any */) { export default function extendStruct (Y) {
var Struct = { var Struct = {
binaryDecodeOperation: function (decoder) {
let code = decoder.peekUint8()
if (code === CDELETE) {
return Y.Struct.Delete.binaryDecode(decoder)
} else if (code === CINSERT) {
return Y.Struct.Insert.binaryDecode(decoder)
} else if (code === CLIST) {
return Y.Struct.List.binaryDecode(decoder)
} else if (code === CMAP) {
return Y.Struct.Map.binaryDecode(decoder)
} else {
throw new Error('Unable to decode operation!')
}
},
/* This is the only operation that is actually not a structure, because /* 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 it is not stored in the OS. This is why it _does not_ have an id
@@ -36,6 +52,19 @@ module.exports = function (Y/* :any */) {
struct: 'Delete' struct: 'Delete'
} }
}, },
binaryEncode: function (encoder, op) {
encoder.writeUint8(CDELETE)
encoder.writeOpID(op.target)
encoder.writeVarUint(op.length || 0)
},
binaryDecode: function (decoder) {
decoder.skip8()
return {
target: decoder.readOpID(),
length: decoder.readVarUint(),
struct: 'Delete'
}
},
requiredOps: function (op) { requiredOps: function (op) {
return [] // [op.target] return [] // [op.target]
}, },
@@ -77,6 +106,91 @@ module.exports = function (Y/* :any */) {
return e 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) { requiredOps: function (op) {
var ids = [] var ids = []
if (op.left != null) { if (op.left != null) {
@@ -299,14 +413,31 @@ module.exports = function (Y/* :any */) {
id: op.id, id: op.id,
type: op.type type: op.type
} }
if (op.requires != null) {
e.requires = op.requires
}
if (op.info != null) { if (op.info != null) {
e.info = op.info e.info = op.info
} }
return e return e
}, },
binaryEncode: function (encoder, op) {
encoder.writeUint8(CLIST)
encoder.writeOpID(op.id)
encoder.writeVarString(op.type)
let info = op.info != null ? JSON.stringify(op.info) : ''
encoder.writeVarString(info)
},
binaryDecode: function (decoder) {
decoder.skip8()
let op = {
id: decoder.readOpID(),
type: decoder.readVarString(),
struct: 'List'
}
let info = decoder.readVarString()
if (info.length > 0) {
op.info = JSON.parse(info)
}
return op
},
requiredOps: function () { requiredOps: function () {
/* /*
var ids = [] var ids = []
@@ -381,13 +512,36 @@ module.exports = function (Y/* :any */) {
map: {} // overwrite map!! map: {} // overwrite map!!
} }
if (op.requires != null) { if (op.requires != null) {
e.requires = op.requires e.requires = op.require
// TODO: !!
console.warn('requires is used! see same note above for List')
} }
if (op.info != null) { if (op.info != null) {
e.info = op.info e.info = op.info
} }
return e return e
}, },
binaryEncode: function (encoder, op) {
encoder.writeUint8(CMAP)
encoder.writeOpID(op.id)
encoder.writeVarString(op.type)
let info = op.info != null ? JSON.stringify(op.info) : ''
encoder.writeVarString(info)
},
binaryDecode: function (decoder) {
decoder.skip8()
let op = {
id: decoder.readOpID(),
type: decoder.readVarString(),
struct: 'Map',
map: {}
}
let info = decoder.readVarString()
if (info.length > 0) {
op.info = JSON.parse(info)
}
return op
},
requiredOps: function () { requiredOps: function () {
return [] return []
}, },

View File

@@ -1,5 +1,3 @@
/* @flow */
'use strict'
/* /*
Partial definition of a transaction Partial definition of a transaction
@@ -74,7 +72,7 @@
- this is called only by `getOperations(startSS)`. It makes an operation - this is called only by `getOperations(startSS)`. It makes an operation
applyable on a given SS. applyable on a given SS.
*/ */
module.exports = function (Y/* :any */) { export default function extendTransaction (Y) {
class TransactionInterface { class TransactionInterface {
/* :: /* ::
store: Y.AbstractDatabase; store: Y.AbstractDatabase;
@@ -96,7 +94,7 @@ module.exports = function (Y/* :any */) {
send.push(Y.Struct[op.struct].encode(op)) send.push(Y.Struct[op.struct].encode(op))
} }
} }
if (this.store.y.connector.isSynced && send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops) if (send.length > 0) { // TODO: && !this.store.forwardAppliedOperations (but then i don't send delete ops)
// is connected, and this is not going to be send in addOperation // is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps(send) this.store.y.connector.broadcastOps(send)
} }
@@ -294,8 +292,9 @@ module.exports = function (Y/* :any */) {
yield * this.ds.put(n) yield * this.ds.put(n)
} else { } else {
// already gc'd // already gc'd
throw new Error('Cannot happen! (it dit though.. :()') throw new Error(
// return n 'DS reached an inconsistent state. Please report this issue!'
)
} }
} }
} else { } else {
@@ -366,6 +365,7 @@ module.exports = function (Y/* :any */) {
operations that can be gc'd and add them to the garbage collector. operations that can be gc'd and add them to the garbage collector.
*/ */
* garbageCollectAfterSync () { * garbageCollectAfterSync () {
// debugger
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) { if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('gc should be empty after sync') console.warn('gc should be empty after sync')
} }
@@ -459,16 +459,8 @@ module.exports = function (Y/* :any */) {
if (o.originOf != null && o.originOf.length > 0) { if (o.originOf != null && o.originOf.length > 0) {
// find new origin of right ops // find new origin of right ops
// origin is the first left deleted operation // origin is the first left operation
var neworigin = o.left var neworigin = o.left
var neworigin_ = null
while (neworigin != null) {
neworigin_ = yield* this.getInsertion(neworigin)
if (neworigin_.deleted) {
break
}
neworigin = neworigin_.left
}
// reset origin of all right ops (except first right - duh!), // reset origin of all right ops (except first right - duh!),
@@ -513,6 +505,7 @@ module.exports = function (Y/* :any */) {
} }
} }
if (neworigin != null) { if (neworigin != null) {
var neworigin_ = yield * this.getInsertion(neworigin)
if (neworigin_.originOf == null) { if (neworigin_.originOf == null) {
neworigin_.originOf = o.originOf neworigin_.originOf = o.originOf
} else { } else {
@@ -593,11 +586,20 @@ module.exports = function (Y/* :any */) {
apply a delete set in order to get apply a delete set in order to get
the state of the supplied ds the state of the supplied ds
*/ */
* applyDeleteSet (ds) { * applyDeleteSet (decoder) {
var deletions = [] var deletions = []
for (var user in ds) { let dsLength = decoder.readUint32()
var dv = ds[user] for (let i = 0; i < dsLength; i++) {
let user = decoder.readVarUint()
let dv = []
let dvLength = decoder.readVarUint()
for (let j = 0; j < dvLength; j++) {
let from = decoder.readVarUint()
let len = decoder.readVarUint()
let gc = decoder.readUint8() === 1
dv.push([from, len, gc])
}
var pos = 0 var pos = 0
var d = dv[pos] var d = dv[pos]
yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) { yield * this.ds.iterate(this, [user, 0], [user, Number.MAX_VALUE], function * (n) {
@@ -691,21 +693,34 @@ module.exports = function (Y/* :any */) {
/* /*
A DeleteSet (ds) describes all the deleted ops in the OS A DeleteSet (ds) describes all the deleted ops in the OS
*/ */
* getDeleteSet () { * writeDeleteSet (encoder) {
var ds = {} var ds = new Map()
yield * this.ds.iterate(this, null, null, function * (n) { yield * this.ds.iterate(this, null, null, function * (n) {
var user = n.id[0] var user = n.id[0]
var counter = n.id[1] var counter = n.id[1]
var len = n.len var len = n.len
var gc = n.gc var gc = n.gc
var dv = ds[user] var dv = ds.get(user)
if (dv === void 0) { if (dv === void 0) {
dv = [] dv = []
ds[user] = dv ds.set(user, dv)
} }
dv.push([counter, len, gc]) dv.push([counter, len, gc])
}) })
return ds let keys = Array.from(ds.keys())
encoder.writeUint32(keys.length)
for (var i = 0; i < keys.length; i++) {
let user = keys[i]
let deletions = ds.get(user)
encoder.writeVarUint(user)
encoder.writeVarUint(deletions.length)
for (var j = 0; j < deletions.length; j++) {
let del = deletions[j]
encoder.writeVarUint(del[0])
encoder.writeVarUint(del[1])
encoder.writeUint8(del[2] ? 1 : 0)
}
}
} }
* isDeleted (id) { * isDeleted (id) {
var n = yield * this.ds.findWithUpperBound(id) var n = yield * this.ds.findWithUpperBound(id)
@@ -717,7 +732,8 @@ module.exports = function (Y/* :any */) {
} }
* addOperation (op) { * addOperation (op) {
yield * this.os.put(op) yield * this.os.put(op)
if (this.store.y.connector.isSynced && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') { // case op is created by this user, op is already broadcasted in applyCreatedOperations
if (op.id[0] !== this.store.userId && this.store.forwardAppliedOperations && typeof op.id[1] !== 'string') {
// is connected, and this is not going to be send in addOperation // is connected, and this is not going to be send in addOperation
this.store.y.connector.broadcastOps([op]) this.store.y.connector.broadcastOps([op])
} }
@@ -826,7 +842,7 @@ module.exports = function (Y/* :any */) {
} }
* getOperation (id/* :any */)/* :Transaction<any> */ { * getOperation (id/* :any */)/* :Transaction<any> */ {
var o = yield * this.os.find(id) var o = yield * this.os.find(id)
if (id[0] !== '_' || o != null) { if (id[0] !== 0xFFFFFF || o != null) {
return o return o
} else { // type is string } else { // type is string
// generate this operation? // generate this operation?
@@ -838,10 +854,10 @@ module.exports = function (Y/* :any */) {
yield * this.setOperation(op) yield * this.setOperation(op)
return op return op
} else { } else {
// won't be called. but just in case.. throw new Error(
console.error('Unexpected case. How can this happen?') 'Unexpected case. Operation cannot be generated correctly!' +
debugger // eslint-disable-line 'Incompatible Yjs version?'
return null )
} }
} }
} }
@@ -883,6 +899,18 @@ module.exports = function (Y/* :any */) {
}) })
return ss return ss
} }
* writeStateSet (encoder) {
let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0)
yield * this.ss.iterate(this, null, null, function * (n) {
encoder.writeVarUint(n.id[0])
encoder.writeVarUint(n.clock)
len++
})
encoder.setUint32(lenPosition, len)
return len === 0
}
/* /*
Here, we make all missing operations executable for the receiving user. Here, we make all missing operations executable for the receiving user.
@@ -932,59 +960,71 @@ module.exports = function (Y/* :any */) {
* getOperations (startSS) { * getOperations (startSS) {
// TODO: use bounds here! // TODO: use bounds here!
if (startSS == null) { if (startSS == null) {
startSS = {} startSS = new Map()
} }
var send = [] var send = []
var endSV = yield * this.getStateVector() var endSV = yield * this.getStateVector()
for (var endState of endSV) { for (let endState of endSV) {
var user = endState.user let user = endState.user
if (user === '_') { if (user === 0xFFFFFF) {
continue continue
} }
var startPos = startSS[user] || 0 let startPos = startSS.get(user) || 0
if (startPos > 0) { if (startPos > 0) {
// There is a change that [user, startPos] is in a composed Insertion (with a smaller counter) // There is a change that [user, startPos] is in a composed Insertion (with a smaller counter)
// find out if that is the case // find out if that is the case
var firstMissing = yield* this.getInsertion([user, startPos]) let firstMissing = yield * this.getInsertion([user, startPos])
if (firstMissing != null) { if (firstMissing != null) {
// update startPos // update startPos
startPos = firstMissing.id[1] startPos = firstMissing.id[1]
startSS[user] = startPos
} }
} }
startSS.set(user, startPos)
}
for (let endState of endSV) {
let user = endState.user
let startPos = startSS.get(user)
if (user === 0xFFFFFF) {
continue
}
yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) { yield * this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
op = Y.Struct[op.struct].encode(op) op = Y.Struct[op.struct].encode(op)
if (op.struct !== 'Insert') { if (op.struct !== 'Insert') {
send.push(op) send.push(op)
} else if (op.right == null || op.right[1] < (startSS[op.right[0]] || 0)) { } else if (op.right == null || op.right[1] < (startSS.get(op.right[0]) || 0)) {
// case 1. op.right is known // case 1. op.right is known
var o = op // this case is only reached if op.right is known.
// => this is not called for op.left, as op.right is unknown
let o = op
// Remember: ? // Remember: ?
// -> set op.right // -> set op.right
// 1. to the first operation that is known (according to startSS) // 1. to the first operation that is known (according to startSS)
// 2. or to the first operation that has an origin that is not to the // 2. or to the first operation that has an origin that is not to the
// right of op. // right of op.
// For this we maintain a list of ops which origins are not found yet. // For this we maintain a list of ops which origins are not found yet.
var missing_origins = [op] var missingOrigins = [op]
var newright = op.right var newright = op.right
while (true) { while (true) {
if (o.left == null) { if (o.left == null) {
op.left = null op.left = null
send.push(op) send.push(op)
if (!Y.utils.compareIds(o.id, op.id)) { /* not necessary, as o is already sent..
if (!Y.utils.compareIds(o.id, op.id) && o.id[1] >= (startSS.get(o.id[0]) || 0)) {
// o is not op && o is unknown
o = Y.Struct[op.struct].encode(o) o = Y.Struct[op.struct].encode(o)
o.right = missing_origins[missing_origins.length - 1].id o.right = missingOrigins[missingOrigins.length - 1].id
send.push(o) send.push(o)
} }
*/
break break
} }
o = yield * this.getInsertion(o.left) o = yield * this.getInsertion(o.left)
// we set another o, check if we can reduce $missing_origins // we set another o, check if we can reduce $missingOrigins
while (missing_origins.length > 0 && Y.utils.matchesId(o, missing_origins[missing_origins.length - 1].origin)) { while (missingOrigins.length > 0 && Y.utils.matchesId(o, missingOrigins[missingOrigins.length - 1].origin)) {
missing_origins.pop() missingOrigins.pop()
} }
if (o.id[1] < (startSS[o.id[0]] || 0)) { if (o.id[1] < (startSS.get(o.id[0]) || 0)) {
// case 2. o is known // case 2. o is known
op.left = Y.utils.getLastId(o) op.left = Y.utils.getLastId(o)
send.push(op) send.push(op)
@@ -995,17 +1035,20 @@ module.exports = function (Y/* :any */) {
send.push(op) send.push(op)
op = Y.Struct[op.struct].encode(o) op = Y.Struct[op.struct].encode(o)
op.right = newright op.right = newright
if (missing_origins.length > 0) { if (missingOrigins.length > 0) {
console.log('This should not happen .. :( please report this') throw new Error(
'Reached inconsistent OS state.' +
'Operations are not correctly connected.'
)
} }
missing_origins = [op] missingOrigins = [op]
} else { } else {
// case 4. send o, continue to find op.origin // case 4. send o, continue to find op.origin
var s = Y.Struct[op.struct].encode(o) var s = Y.Struct[op.struct].encode(o)
s.right = missing_origins[missing_origins.length - 1].id s.right = missingOrigins[missingOrigins.length - 1].id
s.left = s.origin s.left = s.origin
send.push(s) send.push(s)
missing_origins.push(o) missingOrigins.push(o)
} }
} }
} }
@@ -1013,28 +1056,48 @@ module.exports = function (Y/* :any */) {
} }
return send.reverse() return send.reverse()
} }
* writeOperations (encoder, decoder) {
let ss = new Map()
let ssLength = decoder.readUint32()
for (let i = 0; i < ssLength; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
ss.set(user, clock)
}
let ops = yield * this.getOperations(ss)
encoder.writeUint32(ops.length)
for (let i = 0; i < ops.length; i++) {
let op = ops[i]
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
}
}
/* /*
* Get the plain untransformed operations from the database. * Get the plain untransformed operations from the database.
* You can apply these operations using .applyOperationsUntransformed(ops) * You can apply these operations using .applyOperationsUntransformed(ops)
* *
*/ */
* getOperationsUntransformed () { * writeOperationsUntransformed (encoder) {
var ops = [] let lenPosition = encoder.pos
let len = 0
encoder.writeUint32(0) // placeholder
yield * this.os.iterate(this, null, null, function * (op) { yield * this.os.iterate(this, null, null, function * (op) {
if (op.id[0] !== '_') { if (op.id[0] !== 0xFFFFFF) {
ops.push(op) len++
Y.Struct[op.struct].binaryEncode(encoder, Y.Struct[op.struct].encode(op))
} }
}) })
return { encoder.setUint32(lenPosition, len)
untransformed: ops yield * this.writeStateSet(encoder)
} }
* applyOperationsUntransformed (decoder) {
let len = decoder.readUint32()
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder)
yield * this.os.put(op)
} }
* applyOperationsUntransformed (m, stateSet) { yield * this.os.iterate(this, null, null, function * (op) {
var ops = m.untransformed if (op.parent != null) {
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] === '_') {
if (op.struct === 'Insert') { if (op.struct === 'Insert') {
// update parents .map/start/end properties // update parents .map/start/end properties
if (op.parentSub != null && op.left == null) { if (op.parentSub != null && op.left == null) {
@@ -1054,12 +1117,14 @@ module.exports = function (Y/* :any */) {
} }
} }
} }
yield* this.os.put(op) })
} let stateSetLength = decoder.readUint32()
for (var user in stateSet) { for (let i = 0; i < stateSetLength; i++) {
let user = decoder.readVarUint()
let clock = decoder.readVarUint()
yield * this.ss.put({ yield * this.ss.put({
id: [user], id: [user],
clock: stateSet[user] clock: clock
}) })
} }
} }

View File

@@ -1,5 +1,6 @@
/* @flow */ /* globals crypto */
'use strict'
import { BinaryDecoder, BinaryEncoder } from './Encoding.js'
/* /*
EventHandler is an helper class for constructing custom types. EventHandler is an helper class for constructing custom types.
@@ -23,8 +24,12 @@
database request to finish). EventHandler helps you to make your type database request to finish). EventHandler helps you to make your type
synchronous. synchronous.
*/ */
module.exports = function (Y /* : any*/) {
Y.utils = {} export default function Utils (Y) {
Y.utils = {
BinaryDecoder: BinaryDecoder,
BinaryEncoder: BinaryEncoder
}
Y.utils.bubbleEvent = function (type, event) { Y.utils.bubbleEvent = function (type, event) {
type.eventHandler.callEventListeners(event) type.eventHandler.callEventListeners(event)
@@ -44,6 +49,32 @@ module.exports = function (Y /* : any*/) {
} }
} }
class NamedEventHandler {
constructor () {
this._eventListener = {}
}
on (name, f) {
if (this._eventListener[name] == null) {
this._eventListener[name] = []
}
this._eventListener[name].push(f)
}
off (name, f) {
if (name == null || f == null) {
throw new Error('You must specify event name and function!')
}
let listener = this._eventListener[name] || []
this._eventListener[name] = listener.filter(e => e !== f)
}
emit (name, value) {
(this._eventListener[name] || []).forEach(l => l(value))
}
destroy () {
this._eventListener = null
}
}
Y.utils.NamedEventHandler = NamedEventHandler
class EventListenerHandler { class EventListenerHandler {
constructor () { constructor () {
this.eventListeners = [] this.eventListeners = []
@@ -74,7 +105,12 @@ module.exports = function (Y /* : any*/) {
} }
this.eventListeners[i](_event) this.eventListeners[i](_event)
} catch (e) { } catch (e) {
console.error('Your observer threw an error. This error was caught so that Yjs still can ensure data consistency! In order to debug this error you have to check "Pause On Caught Exceptions"', e) /*
Your observer threw an error. This error was caught so that Yjs
can ensure data consistency! In order to debug this error you
have to check "Pause On Caught Exceptions" in developer tools.
*/
console.error(e)
} }
} }
} }
@@ -612,6 +648,7 @@ module.exports = function (Y /* : any*/) {
} }
} }
} }
return false
} }
Y.utils.matchesId = matchesId Y.utils.matchesId = matchesId
@@ -788,8 +825,19 @@ module.exports = function (Y /* : any*/) {
} }
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
// Generates a unique id, for use as a user id. function generateUserId () {
// Thx to @jed for this script https://gist.github.com/jed/982883 if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
function generateGuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,generateGuid)} // eslint-disable-line // browser
Y.utils.generateGuid = generateGuid let arr = new Uint32Array(1)
crypto.getRandomValues(arr)
return arr[0]
} else if (typeof crypto !== 'undefined' && crypto.randomBytes != null) {
// node
let buf = crypto.randomBytes(4)
return new Uint32Array(buf.buffer)[0]
} else {
return Math.ceil(Math.random() * 0xFFFFFFFF)
}
}
Y.utils.generateUserId = generateUserId
} }

View File

@@ -1,18 +1,23 @@
/* @flow */ import extendConnector from './Connector.js'
'use strict' 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'
require('./Connector.js')(Y) extendConnector(Y)
require('./Database.js')(Y) extendDatabase(Y)
require('./Transaction.js')(Y) extendTransaction(Y)
require('./Struct.js')(Y) extendStruct(Y)
require('./Utils.js')(Y) extendUtils(Y)
require('./Connectors/Test.js')(Y)
Y.debug = require('debug') Y.debug = debug
debug.formatters.Y = formatYjsMessage
debug.formatters.y = formatYjsMessageType
var requiringModules = {} var requiringModules = {}
module.exports = Y
Y.requiringModules = requiringModules Y.requiringModules = requiringModules
Y.extend = function (name, value) { Y.extend = function (name, value) {
@@ -110,7 +115,7 @@ type YOptions = {
} }
*/ */
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ { export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
if (opts.hasOwnProperty('sourceDir')) { if (opts.hasOwnProperty('sourceDir')) {
Y.sourceDir = opts.sourceDir Y.sourceDir = opts.sourceDir
} }
@@ -120,17 +125,16 @@ function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
modules.push(opts.share[name]) modules.push(opts.share[name])
} }
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
if (opts == null) reject('An options object is expected! ') if (opts == null) reject(new Error('An options object is expected!'))
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)') else if (opts.connector == null) reject(new Error('You must specify a connector! (missing connector property)'))
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)') else if (opts.connector.name == null) reject(new Error('You must specify connector name! (missing connector.name property)'))
else if (opts.db == null) reject('You must specify a database! (missing db property)') else if (opts.db == null) reject(new Error('You must specify a database! (missing db property)'))
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)') else if (opts.connector.name == null) reject(new Error('You must specify db name! (missing db.name property)'))
else { else {
opts = Y.utils.copyObject(opts) opts = Y.utils.copyObject(opts)
opts.connector = Y.utils.copyObject(opts.connector) opts.connector = Y.utils.copyObject(opts.connector)
opts.db = Y.utils.copyObject(opts.db) opts.db = Y.utils.copyObject(opts.db)
opts.share = Y.utils.copyObject(opts.share) opts.share = Y.utils.copyObject(opts.share)
setTimeout(function () {
Y.requestModules(modules).then(function () { Y.requestModules(modules).then(function () {
var yconfig = new YConfig(opts) var yconfig = new YConfig(opts)
yconfig.db.whenUserIdSet(function () { yconfig.db.whenUserIdSet(function () {
@@ -139,12 +143,11 @@ function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
}) })
}) })
}).catch(reject) }).catch(reject)
}, 0)
} }
}) })
} }
class YConfig { class YConfig extends Y.utils.NamedEventHandler {
/* :: /* ::
db: Y.AbstractDatabase; db: Y.AbstractDatabase;
connector: Y.AbstractConnector; connector: Y.AbstractConnector;
@@ -152,6 +155,7 @@ class YConfig {
options: Object; options: Object;
*/ */
constructor (opts, callback) { constructor (opts, callback) {
super()
this.options = opts this.options = opts
this.db = new Y[opts.db.name](this, opts.db) this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector) this.connector = new Y[opts.connector.name](this, opts.connector)
@@ -168,7 +172,7 @@ class YConfig {
var typeName = typeConstructor.splice(0, 1) var typeName = typeConstructor.splice(0, 1)
var type = Y[typeName] var type = Y[typeName]
var typedef = type.typeDefinition var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor] var id = [0xFFFFFF, typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
var args = [] var args = []
if (typeConstructor.length === 1) { if (typeConstructor.length === 1) {
try { try {
@@ -215,6 +219,9 @@ class YConfig {
} else { } else {
return Promise.resolve() return Promise.resolve()
} }
}).then(() => {
// remove existing event listener
super.destroy()
}) })
} }
close () { close () {
@@ -225,13 +232,13 @@ class YConfig {
} else { } else {
this.connector.disconnect() this.connector.disconnect()
} }
return this.db.whenTransactionsFinished(function () { return this.db.whenTransactionsFinished().then(function () {
this.db.destroyTypes() self.db.destroyTypes()
// make sure to wait for all transactions before destroying the db // make sure to wait for all transactions before destroying the db
this.db.requestTransaction(function * () { self.db.requestTransaction(function * () {
yield * self.db.destroy() yield * self.db.destroy()
}) })
return this.db.whenTransactionsFinished() return self.db.whenTransactionsFinished()
}) })
} }
} }

229
test/encode-decode.js Normal file
View File

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

222
tests-lib/helper.js Normal file
View File

@@ -0,0 +1,222 @@
import _Y from '../../yjs/src/y.js'
import yMemory from '../../y-memory/src/y-memory.js'
import yArray from '../../y-array/src/y-array.js'
import yMap from '../../y-map/src/Map.js'
import yTest from './test-connector.js'
import Chance from 'chance'
export let Y = _Y
Y.extend(yMemory, yArray, yMap, yTest)
function * getStateSet () {
var ss = {}
yield * this.ss.iterate(this, null, null, function * (n) {
var user = n.id[0]
var clock = n.clock
ss[user] = clock
})
return ss
}
function * getDeleteSet () {
var ds = {}
yield * this.ds.iterate(this, null, null, function * (n) {
var user = n.id[0]
var counter = n.id[1]
var len = n.len
var gc = n.gc
var dv = ds[user]
if (dv === void 0) {
dv = []
ds[user] = dv
}
dv.push([counter, len, gc])
})
return ds
}
export async function garbageCollectUsers (t, users) {
await flushAll(t, users)
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
}
/*
* 1. reconnect and flush all
* 2. user 0 gc
* 3. get type content
* 4. disconnect & reconnect all (so gc is propagated)
* 5. compare os, ds, ss
*/
export async function compareUsers (t, users) {
await Promise.all(users.map(u => u.reconnect()))
if (users[0].connector.testRoom == null) {
await wait(100)
}
await flushAll(t, users)
await wait()
await flushAll(t, users)
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
await users[0].db.garbageCollect()
await users[0].db.garbageCollect()
// disconnect all except user 0
await Promise.all(users.slice(1).map(async u =>
u.disconnect()
))
if (users[0].connector.testRoom == null) {
await wait(100)
}
// reconnect all
await Promise.all(users.map(u => u.reconnect()))
if (users[0].connector.testRoom == null) {
await wait(100)
}
await users[0].connector.testRoom.flushAll(users)
await Promise.all(users.map(u =>
new Promise(function (resolve) {
u.connector.whenSynced(resolve)
})
))
let filterDeletedOps = users.every(u => u.db.gc === false)
var data = await Promise.all(users.map(async (u) => {
var data = {}
u.db.requestTransaction(function * () {
let ops = []
yield * this.os.iterate(this, null, null, function * (op) {
ops.push(Y.Struct[op.struct].encode(op))
})
data.os = {}
for (let i = 0; i < ops.length; i++) {
let op = ops[i]
op = Y.Struct[op.struct].encode(op)
delete op.origin
/*
If gc = false, it is necessary to filter deleted ops
as they might have been split up differently..
*/
if (filterDeletedOps) {
let opIsDeleted = yield * this.isDeleted(op.id)
if (!opIsDeleted) {
data.os[JSON.stringify(op.id)] = op
}
} else {
data.os[JSON.stringify(op.id)] = op
}
}
data.ds = yield * getDeleteSet.apply(this)
data.ss = yield * getStateSet.apply(this)
})
await u.db.whenTransactionsFinished()
return data
}))
for (var i = 0; i < data.length - 1; i++) {
await t.asyncGroup(async () => {
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
t.compare(data[i].os, data[i + 1].os, 'os')
t.compare(data[i].ds, data[i + 1].ds, 'ds')
t.compare(data[i].ss, data[i + 1].ss, 'ss')
}, `Compare user${i} with user${i + 1}`)
}
await Promise.all(users.map(async (u) => {
await u.close()
}))
}
export async function initArrays (t, opts) {
var result = {
users: []
}
var share = Object.assign({ flushHelper: 'Map', array: 'Array', map: 'Map' }, opts.share)
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
var connector = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, opts.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)
} else {
dbOpts = Object.assign({ gc: false }, opts.db)
connOpts = Object.assign({ role: 'slave' }, connector)
}
let y = await Y({
connector: connOpts,
db: dbOpts,
share: share
})
result.users.push(y)
for (let name in share) {
result[name + i] = y.share[name]
}
}
result.array0.delete(0, result.array0.length)
if (result.users[0].connector.testRoom != null) {
// flush for sync if test-connector
await result.users[0].connector.testRoom.flushAll(result.users)
}
await Promise.all(result.users.map(u => {
return new Promise(function (resolve) {
u.connector.whenSynced(resolve)
})
}))
await flushAll(t, result.users)
return result
}
export async function flushAll (t, users) {
// users = users.filter(u => u.connector.isSynced)
if (users.length === 0) {
return
}
await wait(0)
if (users[0].connector.testRoom != null) {
// use flushAll method specified in Test Connector
await users[0].connector.testRoom.flushAll(users)
} else {
// flush for any connector
await Promise.all(users.map(u => { return u.db.whenTransactionsFinished() }))
var flushCounter = users[0].share.flushHelper.get('0') || 0
flushCounter++
await Promise.all(users.map(async (u, i) => {
// wait for all users to set the flush counter to the same value
await new Promise(resolve => {
function observer () {
var allUsersReceivedUpdate = true
for (var i = 0; i < users.length; i++) {
if (u.share.flushHelper.get(i + '') !== flushCounter) {
allUsersReceivedUpdate = false
break
}
}
if (allUsersReceivedUpdate) {
resolve()
}
}
u.share.flushHelper.observe(observer)
u.share.flushHelper.set(i + '', flushCounter)
})
}))
}
}
export async function flushSome (t, users) {
if (users[0].connector.testRoom == null) {
// if not test-connector, wait for some time for operations to arrive
await wait(100)
}
}
export function wait (t) {
return new Promise(function (resolve) {
setTimeout(resolve, t != null ? t : 100)
})
}

166
tests-lib/test-connector.js Normal file
View File

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

10
y.js Normal file

File diff suppressed because one or more lines are too long

1
y.js.map Normal file

File diff suppressed because one or more lines are too long

4948
y.node.js Normal file

File diff suppressed because it is too large Load Diff

1
y.node.js.map Normal file

File diff suppressed because one or more lines are too long

17975
y.test.js Normal file

File diff suppressed because one or more lines are too long

1
y.test.js.map Normal file

File diff suppressed because one or more lines are too long