Compare commits

..

178 Commits
v0.6.19 ... v11

Author SHA1 Message Date
Kevin Jahns
9374c363c3 Release v11.2.6 (hot fix for #54) 2016-09-28 13:35:09 +02:00
Kevin Jahns
4078020afd Release 11.2.5 2016-08-22 12:15:32 +02:00
Kevin Jahns
e31d5e0e1d fixed late-join sync issue & use leveldb only in node environment 2016-08-22 12:05:39 +02:00
Kevin Jahns
acbc884eb5 Release 11.2.4 2016-08-04 19:25:36 +02:00
Kevin Jahns
f9315288d0 found another bug that only happens in persistent databases (operationAdded changes an operation, so it needs to be retrieved again after calling it) 2016-08-01 20:48:55 +02:00
Kevin Jahns
3b0d0343f4 Release 11.2.2 2016-08-01 17:04:06 +02:00
Kevin Jahns
74c881bb5b Fixed bug that only occurs in persistent databases (setOperation was not called). 2016-08-01 16:52:02 +02:00
Kevin Jahns
63f8a891be update dist 2016-07-05 16:21:19 +02:00
Kevin Jahns
2083cdb6b0 several bug-fixes (for y-richtext beta) 2016-07-01 17:45:53 +02:00
Kevin Jahns
2091392031 implemented random seed generator, in order to reproduce tests 2016-06-29 17:48:56 +02:00
Kevin Jahns
3dc67e075b debugged some of the special cases of Utils.receivedOp & found some bugs 2016-06-28 16:23:02 +02:00
Kevin Jahns
81e72126ce implemented new extention for awaitOps. It fixes several consistency issues (they were previously hard to detect), and it is also pretty efficient. It still has some debugger statements, so enjoy with care 2016-06-14 21:27:42 +02:00
Kevin Jahns
e77a753708 fixed bug in array & richtext 2016-06-03 19:04:47 +02:00
Kevin Jahns
bc856a09f5 Release 11.2.1 2016-05-24 15:25:14 +02:00
Kevin Jahns
f7ae62a906 Release 11.2.0 2016-05-10 18:13:01 +02:00
Kevin Jahns
6669be104e Release 11.1.0 2016-05-07 13:20:43 +01:00
Kevin Jahns
14d59de2bd improved awaitOps 2016-05-07 13:16:48 +01:00
Kevin Jahns
483d2c78aa Use uglify-js v2.5.0 to fix #52 2016-05-06 12:57:07 +01:00
Kevin Jahns
5b835563c8 implement new method awaitedOps in favor of awaitedDeletes/awaitedInserts. This will fix some bugs when the type gets out of sync with the state of yjs 2016-05-06 12:53:10 +01:00
Kevin Jahns
996566419c some flush updates 2016-05-04 17:14:39 +02:00
Kevin Jahns
5d6a9872e2 Michael found a weird character in my code 2016-05-02 11:04:24 +02:00
Kevin Jahns
8930865a21 Release 11.0.4 2016-05-02 11:00:07 +02:00
Kevin Jahns
2897695680 update dist, more args for SmallLookupBuffer 2016-04-30 23:01:36 +01:00
Kevin Jahns
5118f02b49 Release 11.0.3 2016-04-27 12:04:39 +01:00
Kevin Jahns
a10933beef fixed awaitedInserts (critical for y-richtext) 2016-04-27 12:02:39 +01:00
Kevin Jahns
c2ffe0b697 Release 11.0.2 2016-04-26 21:28:21 +02:00
Kevin Jahns
2d1a7b067b fixed some minor problems related to compareId 2016-04-26 21:27:08 +02:00
Kevin Jahns
2675f0277c Release 11.0.1 2016-04-26 15:34:19 +02:00
Kevin Jahns
918bc334b2 Release 11.0.0 2016-04-26 15:02:44 +02:00
Kevin Jahns
accf0dbafb fixed bump script 2016-04-26 15:01:22 +02:00
Kevin Jahns
6b8ce0ab4f changed the observe functionality 2016-04-25 16:32:45 +02:00
Kevin Jahns
71bf6438e1 found some gc bugs that occur when using deletion lengths 2016-04-25 13:09:52 +02:00
Kevin Jahns
90b7b01e9a fixes #49 2016-04-22 22:09:49 +01:00
Kevin Jahns
895ec86ff6 all tests working. Fixed an older bug: When gc an op I forgot to update the state. This only affected offline editing, and was very hard to catch in the past 2016-04-22 21:27:43 +01:00
Kevin Jahns
bffd130b92 fixed first two random cases, (gc seems still to be an issue ..) 2016-04-21 18:04:46 +02:00
Kevin Jahns
feae0d51bd fixed content.length in tryExecute 2016-04-20 12:37:56 +02:00
Kevin Jahns
f46c8df605 fixed some cases that lead to inconsistencies 2016-04-20 12:03:06 +02:00
Kevin Jahns
82025c5de9 better debugging, pretty print (no undefined) in logtable 2016-04-19 16:34:14 +02:00
Kevin Jahns
153ec811e2 fixed some gc bugs, improved applyDeleteSet 2016-04-18 16:13:29 +02:00
Kevin Jahns
01031d27c3 some gc fixes 2016-04-18 10:48:48 +01:00
Kevin Jahns
c72f62ecb6 implemented support for range of deletions (unfinished) 2016-04-14 18:09:27 +02:00
Kevin Jahns
e1df1a7a12 fixed one more problem with ds & fixed ace example & insertions are combined even when created in different execution tasks 2016-04-13 16:59:54 +02:00
Kevin Jahns
a7f845f553 fixed some old todos, fixed some cases related to "content is an array" 2016-04-12 16:18:05 +02:00
Kevin Jahns
20321c8a7d fixed test connector buffer to really be parallel! 2016-04-11 16:20:27 +02:00
Kevin Jahns
f3fadd3895 test suite works again with "contentS" feature! 2016-04-11 15:07:08 +02:00
Kevin Jahns
08a79d0e7b several bug fixes regarding "content is an array" 2016-04-07 15:54:47 +02:00
Kevin Jahns
5b21104da3 del is applied after ins (type is always called when ins already deleted) 2016-04-06 15:45:30 +02:00
Kevin Jahns
ecc2aef0f8 update 2016-04-05 15:38:22 +02:00
Kevin Jahns
1c32067908 implementing new insertion struct (unfinished) 2016-04-05 10:43:57 +01:00
Kevin Jahns
fe75ed6208 typos in README.md 2016-03-24 12:00:19 +01:00
Kevin Jahns
c2404b1e98 Release 10.0.3 2016-03-23 19:55:56 +01:00
Kevin Jahns
f363e1e9fc Merge branch 'master' of https://github.com/y-js/yjs 2016-03-23 19:54:40 +01:00
Kevin Jahns
749514c074 update dist 2016-03-23 19:54:28 +01:00
Kevin Jahns
24f8616386 Release 10.0.2 2016-03-23 14:42:26 +01:00
Kevin Jahns
d4ee8af772 Release 10.0.1 2016-03-23 14:41:08 +01:00
Kevin Jahns
83a42271ad fix remaining memory leaks 2016-03-23 14:33:51 +01:00
Kevin Jahns
88971b4e69 fixed several issues of the gc. I.e. the gc sometimes did not collect the whole subtree when deleting an operation 2016-03-21 21:00:28 +01:00
Kevin Jahns
f844dcbc1e bugfixes creating structs without loading type (e.g. for y-websockets-server) 2016-03-14 19:46:57 +01:00
Kevin Jahns
c9c00b5a08 found bug: types are not called anymore 2016-03-11 01:08:26 +01:00
Kevin Jahns
d79e3102fc new approach for type definitions 2016-03-10 17:49:36 +01:00
Kevin Jahns
ba4f444f32 Implemented support for composite type, fixed insert type issues for y-array 2016-02-29 13:46:08 +01:00
Kevin Jahns
effc2fe576 Release 9.0.4 2016-02-24 12:00:50 +01:00
Kevin Jahns
f9a54626b1 update dist 2016-02-24 12:00:27 +01:00
Kevin Jahns
808a07d218 added some error messages 2016-02-24 11:40:19 +01:00
Kevin Jahns
afbe81a602 update .gitignore 2016-02-24 10:09:56 +01:00
Kevin Jahns
2883947641 remove jsconfig 2016-02-24 10:08:11 +01:00
Kevin Jahns
1c15edd332 remove another .vscode 2016-02-24 10:07:07 +01:00
Kevin Jahns
214380c3ca updated changelog 2016-02-23 16:24:07 +01:00
Kevin Jahns
ecbf03ab10 Release 9.0.3 2016-02-23 15:50:19 +01:00
Kevin Jahns
5aedddeea3 update dist 2016-02-23 15:48:15 +01:00
Kevin Jahns
babdb765c5 Release 9.0.2 2016-02-23 15:42:13 +01:00
Kevin Jahns
43b4d59f9b updated package.json, major version upgrade, added peerDependencies 2016-02-23 15:41:28 +01:00
Kevin Jahns
64a5fae838 Release 9.0.1 2016-02-23 11:58:14 +01:00
Kevin Jahns
5036053d9c added peer dependencies, upgraded version to major (new versioning scheme) 2016-02-23 11:57:44 +01:00
Kevin Jahns
0ec249d388 Release 0.8.28 2016-02-23 11:27:30 +01:00
Kevin Jahns
be68a25904 Implement vesion header. Different version headers must not sync. implements #48 2016-02-23 11:21:37 +01:00
Kevin Jahns
fc92b12e85 Release 0.8.27 2016-02-22 13:08:05 +01:00
Kevin Jahns
e35f4d19f3 Release 0.8.26 2016-02-22 13:06:15 +01:00
Kevin Jahns
6d3c4b21fb Release 0.8.25 2016-02-22 13:04:36 +01:00
Kevin Jahns
339590f49e Release 0.8.22 2016-02-22 12:38:26 +01:00
Kevin Jahns
429c1f83c1 Merge pull request #45 from istvank/master
Making documentation consistent
2016-02-18 17:33:04 +01:00
Kevin Jahns
03bab63358 Release 0.8.21 2016-02-18 16:57:32 +01:00
Kevin Jahns
06ef22b8ca sorry sorry sorry -.- fixes #47 2016-02-18 16:52:13 +01:00
Kevin Jahns
f579a436c7 Release 0.8.20 2016-02-16 16:08:54 +01:00
Kevin Jahns
da7e67d97d implemented destroy & updated disconnect 2016-02-16 15:51:12 +01:00
Kevin Jahns
bd54a43a33 Release 0.8.18 2016-02-15 15:22:55 +01:00
Kevin Jahns
68c21131d3 Remove that single \for.. in\ loop that appearantly causes troubles for istvan (why am I fixing this again?) fixes #46 2016-02-15 15:21:11 +01:00
István Koren
3826d9b592 Make it consistent with npm doc few lines below 2016-02-14 13:24:12 +01:00
István Koren
fa9ff669e4 Merge pull request #1 from y-js/master
Updating to latest yjs version
2016-02-13 13:14:43 +01:00
Kevin Jahns
bca7477ca5 Release 0.8.17 2016-02-04 23:13:51 +01:00
Kevin Jahns
b40b7e10ab Release 0.8.16 2016-02-04 23:12:53 +01:00
Kevin Jahns
d20141fec1 Release 0.8.15 2016-02-04 23:11:11 +01:00
Kevin Jahns
5f2a81d064 updated api documentation 2016-02-04 23:08:29 +01:00
Kevin Jahns
56ba55cbab Release 0.8.14 2016-02-04 15:26:32 +01:00
Kevin Jahns
7be262e9f3 fixing @Joeao bug 2016-02-04 15:26:09 +01:00
Kevin Jahns
1da76dbc20 update dist 2016-02-04 12:53:39 +01:00
Kevin Jahns
8924c3e163 Release 0.8.13 2016-02-04 12:47:09 +01:00
Kevin Jahns
608b5e3319 Release 0.8.12 2016-02-04 12:12:57 +01:00
Kevin Jahns
d532fc530f update dist 2016-02-04 12:06:06 +01:00
Kevin Jahns
a5760a45bb Release 0.8.11 2016-02-04 10:53:04 +01:00
Kevin Jahns
437955ba84 update 2016-02-03 11:47:07 +01:00
Kevin Jahns
dab72be87f update 2016-02-03 11:37:36 +01:00
Kevin Jahns
89a6ec374e update 2016-01-27 17:05:28 +01:00
Kevin Jahns
4b6352b11a typo 2016-01-27 11:34:11 +01:00
Kevin Jahns
31d2a231e3 Further reduced number of db requests 2016-01-26 15:30:19 +01:00
Kevin Jahns
6b1cf18822 Improvements on DS lookups 2016-01-26 11:29:58 +01:00
Kevin Jahns
39dc2317b7 Implemented more efficient garbage collectior
from worst case of O(n) -> O(1) - where n is the number of insertions in
a list

So this is a huge improvement, I guess :)
2016-01-23 20:09:30 +01:00
Kevin Jahns
38bf398709 Improvements that are required for offline editing 2016-01-23 01:02:01 +01:00
Kevin Jahns
364ed325b0 fixed spec 2016-01-22 14:16:16 +01:00
Kevin Jahns
1b3f5443b3 implemented small lookup buffer. This heavily improves lookups for slow databases 2016-01-22 14:09:51 +01:00
Kevin Jahns
37ac7787d0 Update garbage collect algorithm. Fixed some tests appearantly :) 2016-01-21 21:08:20 +01:00
Kevin Jahns
8e4cf83330 typos 2016-01-18 17:21:47 +01:00
Kevin Jahns
5524ab9c20 Release 0.8.9 2016-01-18 16:45:46 +01:00
Kevin Jahns
65dc716936 Release 0.8.8 2016-01-18 15:40:38 +01:00
Kevin Jahns
5b7a4482cf Release 0.8.7 2016-01-16 01:45:58 +01:00
Kevin Jahns
cfa089f7cf Release 0.8.6 2016-01-16 01:42:00 +01:00
Kevin Jahns
190442a58d update documentation 2016-01-16 01:40:26 +01:00
Kevin Jahns
0398b5260a Release 0.8.5 2016-01-15 18:09:46 +01:00
Kevin Jahns
8544c16771 Release 0.8.4 2016-01-15 17:58:08 +01:00
Kevin Jahns
a5f55359c3 improve data exchange performance 2016-01-15 17:57:06 +01:00
Kevin Jahns
102555a3b0 Release 0.8.3 2016-01-15 03:46:55 +01:00
Kevin Jahns
ece8268e44 Release 0.8.2 2016-01-15 03:10:58 +01:00
Kevin Jahns
dd279bccf7 Release 0.8.1 2016-01-15 00:03:43 +01:00
Kevin Jahns
7e046e0753 Release 0.8.0 2016-01-15 00:02:12 +01:00
Kevin Jahns
51a834d6c9 Implemente a new sync procedure that is optimal with respect to big O notation (there is probably a way to reduce it by a factor of 1/2) 2016-01-15 00:00:41 +01:00
Kevin Jahns
a33d0bf7bc Release 0.7.6 2016-01-11 15:48:10 +01:00
Kevin Jahns
fd6a28eb25 Release 0.7.5 2016-01-11 15:47:24 +01:00
Kevin Jahns
579fd52455 publish v0.7.3 2016-01-09 21:08:02 +01:00
Kevin Jahns
8cfc9d41c3 Made compatible with windows 2016-01-09 04:17:23 +01:00
Kevin Jahns
bdf290adb2 added safety to setUserId (when called twice) 2015-12-30 16:37:35 +01:00
Kevin Jahns
98d87cb26d update 2015-12-18 16:34:21 +01:00
Kevin Jahns
fbbfa9fd47 added example 2015-12-09 18:40:10 +01:00
Kevin Jahns
72bd0d9c3a update map type 2015-12-08 16:26:55 +01:00
Kevin Jahns
3dbeb2c415 Merge pull request #34 from istvank/master
Changed to semver ;)
2015-12-08 14:17:29 +01:00
István Koren
2a9fd96958 Changed to semver ;)
Two lines below it states you switch to semver, still there was 1.0... ;)
2015-12-08 12:08:12 +01:00
Kevin Jahns
9d34ccfdbc update 2015-12-03 18:05:12 +01:00
Kevin Jahns
7753994e36 fixed bugs resolving from new init style 2015-12-03 17:27:13 +01:00
Kevin Jahns
709779425c make module import safer 2015-12-02 20:04:59 +01:00
Kevin Jahns
334db3234b outsourced Y.Map type 2015-12-02 16:57:55 +01:00
Kevin Jahns
0db7fe5d46 added support for static content, added jigsaw puzzle 2015-12-02 15:58:22 +01:00
Kevin Jahns
3a55ca4f21 update 2015-12-01 19:27:14 +01:00
Kevin Jahns
8d14a9cbba starting to implement new sharedObjects idea 2015-11-30 15:56:45 +01:00
Kevin Jahns
f6c5051472 added es6 distribution & gulp task for es6 distribution 2015-11-30 15:25:55 +01:00
Kevin Jahns
eff6fb1cc5 added flow support for everything except tests 2015-11-30 15:02:34 +01:00
Kevin Jahns
0ebfae6997 added flow support for Transaction.js 2015-11-30 14:26:22 +01:00
Kevin Jahns
e9c40f9a83 added flow support for Struct.js 2015-11-30 12:47:33 +01:00
Kevin Jahns
da2762edf5 added flow support for Connector.js 2015-11-30 12:26:02 +01:00
Kevin Jahns
bd9c3813fd * starting flow integration
* found a bug in EventHelper
* reduce wait() calls
2015-11-26 00:46:02 +01:00
Kevin Jahns
940a44bb7c fix transaction wait bug 2015-11-25 16:04:01 +01:00
Kevin Jahns
aa2e7fd917 Added jsconfig.json, fixed tests for large numbers 2015-11-20 21:18:34 +01:00
Kevin Jahns
9fc55f5386 update readme 2015-11-19 18:10:31 +01:00
Kevin Jahns
8ee563f873 finally fixed the timeout hack for tests 2015-11-18 16:17:59 +01:00
Kevin Jahns
5fcfbbfe94 updated build process 2015-11-17 15:28:45 +01:00
Kevin Jahns
8870fdc495 lint 2015-11-15 02:14:06 +01:00
Kevin Jahns
58a612eaa1 added option for servers that want to propagate applied operations (aka the websockets connector) 2015-11-15 02:04:06 +01:00
Kevin Jahns
ae12b087e7 fixed module loading issue 2015-11-14 20:53:38 +01:00
Kevin Jahns
528dbc6e5a announcing new version in readme 2015-11-14 20:44:54 +01:00
Kevin Jahns
1deb453cc5 fixed the dist build process 2015-11-14 20:41:34 +01:00
Kevin Jahns
099297ebdf working on build process 2015-11-13 16:09:40 +01:00
Kevin Jahns
3faeb628fd updated dist build process 2015-11-12 20:42:58 +01:00
Kevin Jahns
d1e30c5040 updated examples and dist build 2015-11-11 17:19:22 +01:00
Kevin Jahns
fa45ce04ef prettyfied README for website 2015-11-11 00:00:15 +01:00
Kevin Jahns
2d20fd59d0 outsourced Textbind, improved automatic module loader 2015-11-09 03:03:37 +01:00
Kevin Jahns
08d07796ee added spec helper 2015-11-07 22:20:47 +01:00
Kevin Jahns
010d0d684e fixed linting 2015-11-07 22:18:28 +01:00
Kevin Jahns
6dc347642b implemented module loader for yjs 2015-11-07 22:12:48 +01:00
Kevin Jahns
138afe39dc improving.. breaking.. the gulpfile 2015-11-06 16:16:38 +01:00
Kevin Jahns
0832be2380 improved error messaging.. thats it for today 2015-11-05 17:20:27 +01:00
Kevin Jahns
8a2a184f30 Release 0.6.32 2015-11-05 17:09:01 +01:00
Kevin Jahns
4882e77fdd improved gulpfile.helper 2015-11-05 16:55:03 +01:00
Kevin Jahns
78f4f6f5b9 implemented gulpfile.helper 2015-11-05 15:53:26 +01:00
Kevin Jahns
317f7f19bb updated gulpfile to wiki 2015-11-05 00:35:11 +01:00
Kevin Jahns
00f58ba68f fixed travis 2015-11-04 17:12:59 +01:00
Kevin Jahns
029a169114 fixed serve:examples 2015-11-04 17:06:20 +01:00
Kevin Jahns
f58889a05d outsourced examples 2015-11-04 16:53:02 +01:00
Kevin Jahns
e9ac59dcf8 fixed tests, finalizing the scripts (sorry for all the commits -.-) 2015-11-04 15:01:12 +01:00
Kevin Jahns
57cf20555f Deploy 0.6.21 2015-11-04 14:39:54 +01:00
Kevin Jahns
805ed3b577 Deploy 0.6.20 2015-11-04 14:37:06 +01:00
36 changed files with 4001 additions and 4565 deletions

View File

@@ -1,3 +0,0 @@
{
"directory": "../"
}

12
.flowconfig Normal file
View File

@@ -0,0 +1,12 @@
[ignore]
.*/node_modules/.*
.*/dist/.*
.*/build/.*
[include]
./src/
[libs]
./declarations/
[options]

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ build_test
.validate.json
/y.js
/y.js.map
/y-*
.vscode
jsconfig.json

View File

@@ -1,8 +0,0 @@
language: node_js
before_install:
- "npm install -g bower"
node_js:
- "0.12"
branches:
only:
- master

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<button id="button">Disconnect</button>
<h1 id="contenteditable" contentEditable></h1>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<script src="../../node_modules/simplewebrtc/simplewebrtc.bundle.js"></script>
<script src="../../y.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,50 +0,0 @@
/* global Y */
// create a shared object. This function call will return a promise!
Y({
db: {
name: 'IndexedDB',
namespace: 'offlineEditingDemo'
},
connector: {
name: 'WebRTC',
room: 'offlineEditingDemo',
debug: true
}
}).then(function (yconfig) {
// yconfig holds all the information about the shared object
window.yconfig = yconfig
// yconfig.root holds the shared element
window.y = yconfig.root
// now we bind the textarea and the contenteditable h1 element
// to a shared element
var textarea = document.getElementById('textfield')
var contenteditable = document.getElementById('contenteditable')
yconfig.root.observePath(['text'], function (text) {
// every time the 'text' property of the yconfig.root changes,
// this function is called. Then we bind it to the html elements
if (text != null) {
// when the text property is deleted, text may be undefined!
// This is why we have to check if text exists..
text.bind(textarea)
text.bind(contenteditable)
}
})
// create a shared TextBind
var textpromise = yconfig.root.get('text')
if (textpromise == null) {
yconfig.root.set('text', Y.TextBind)
}
// We also provide a button for disconnecting/reconnecting the shared element
var button = document.querySelector('#button')
button.onclick = function () {
if (button.innerText === 'Disconnect') {
yconfig.disconnect()
button.innerText = 'Reconnect'
} else {
yconfig.reconnect()
button.innerText = 'Disconnect'
}
}
})

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<button id="button">Disconnect</button>
<h1 id="contenteditable" contentEditable></h1>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<script src="../../node_modules/simplewebrtc/simplewebrtc.bundle.js"></script>
<script src="../../y.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -1,47 +0,0 @@
/* global Y */
// create a shared object. This function call will return a promise!
Y({
db: {
name: 'Memory'
},
connector: {
name: 'WebRTC',
room: 'TextBindDemo',
debug: true
}
}).then(function (yconfig) {
// yconfig holds all the information about the shared object
window.yconfig = yconfig
// yconfig.root holds the shared element
window.y = yconfig.root
// now we bind the textarea and the contenteditable h1 element
// to a shared element
var textarea = document.getElementById('textfield')
var contenteditable = document.getElementById('contenteditable')
yconfig.root.observePath(['text'], function (text) {
// every time the 'text' property of the yconfig.root changes,
// this function is called. Then we bind it to the html elements
if (text != null) {
// when the text property is deleted, text may be undefined!
// This is why we have to check if text exists..
text.bind(textarea)
text.bind(contenteditable)
}
})
// create a shared TextBind
yconfig.root.set('text', Y.TextBind)
// We also provide a button for disconnecting/reconnecting the shared element
var button = document.querySelector('#button')
button.onclick = function () {
if (button.innerText === 'Disconnect') {
yconfig.disconnect()
button.innerText = 'Reconnect'
} else {
yconfig.reconnect()
button.innerText = 'Disconnect'
}
}
})

235
README.md
View File

@@ -1,50 +1,57 @@
# ![Yjs](http://y-js.org/files/layout/yjs.svg)
# ![Yjs](http://y-js.org/images/yjs.png)
[![Build Status](https://travis-ci.org/y-js/yjs.svg)](https://travis-ci.org/y-js/yjs)
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data.
The framework provides similar functionality as [ShareJs] and [OpenCoweb], but supports peer-to-peer
communication protocols by default. Yjs was designed to handle concurrent actions on arbitrary data
like Text, Json, and XML. We also provide support for storing and manipulating your shared data offline.
For more information and demo applications visit our [homepage](http://y-js.org/).
Yjs is a framework for optimistic concurrency control and automatic conflict resolution on shared data types. The framework implements a new OT-like concurrency algorithm and provides similar functionality as [ShareJs] and [OpenCoweb]. Yjs was designed to handle concurrent actions on arbitrary complex data types like Text, Json, and XML. We provide a tutorial and some applications for this framework on our [homepage](http://y-js.org/).
You can create you own shared types easily.
Therefore, you can design the structure of your custom type,
and ensure data validity, while Yjs ensures data consistency (everyone will eventually end up with the same data).
We already provide abstract data types for
You can create you own shared types easily. Therefore, you can take matters into your own hand by defining the meaning of the shared types and ensure that it is valid, while Yjs ensures data consistency (everyone will eventually end up with the same data). We already provide data types for
| Name | Description |
|----------|-------------------|
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to textareas, input elements, or HTML elements (e.g. <*h1*>, or <*p*>). Also supports the [Ace Editor](https://ace.c9.io) |
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
| Name | Description
| ---------------------------------------------------- | ---------------------------------------------
y-object | Add, update, and remove properties of an object. Circular references are supported. Included in Yjs
[y-list](https://github.com/y-js/y-list) | A shared linked list implementation. Circular references are supported
[y-selections](https://github.com/y-js/y-selections) | Manages selections on types that use linear structures (e.g. the y-list type). You can select a range of elements and assign meaning to them.
[y-xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects
[y-text](https://github.com/y-js/y-text) | Collaborate on text. You can create a two way binding to textareas, input elements, or HTML elements (e.g. *h1*, or *p*)
[y-richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. You can create a two way binding to several editors
Yjs supports P2P message propagation, and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
Unlike other frameworks, Yjs supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
We support several communication protocols as so called *Connectors*.
You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors).
Currently, we support the following communication protocols:
We support several communication protocols as so called *Connectors*. You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors). Currently, we support the following communication protocols:
|Name | Description |
|----------------|-----------------------------------|
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|[websockets](https://github.com/y-js/y-websockets-client) | Exchange updates efficiently in the classical client-server model |
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
Name | Description
---------------------------------------- | -------------------------------------------------------
[y-xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))
[y-webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC
[y-test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios
You are not limited to use a specific database to store the shared data. We provide the following database adapters:
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide polymer elements for Yjs!
|Name | Description |
|----------------|-----------------------------------|
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
The advantages over similar frameworks are support for
* .. P2P message propagation and arbitrary communication protocols
* .. arbitrary complex data types
* .. offline editing: Changes are stored persistently and only relevant changes are propagated on rejoin
* .. AnyUndo: Undo *any* action that was executed in constant time (coming..)
* .. share any type of data. The types provide a convenient interface
* .. offline support: Changes are stored persistently and only relevant changes are propagated on rejoin
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline. Every type has a notion on how we define Intention Preservation on it.
## Use it!
You can find a tutorial, and examples on the [website](http://y-js.org). Furthermore, the [github wiki](https://github.com/y-js/yjs/wiki) offers more information about how you can use Yjs in your application.
Either clone this git repository, install it with [bower](http://bower.io/), or install it with [npm](https://www.npmjs.org/package/yjs).
Install yjs and its modules with [bower](http://bower.io/), or with [npm](https://www.npmjs.org/package/yjs).
### Bower
```
bower install y-js/yjs
bower install yjs --save
```
Then you include the libraries directly from the installation folder.
```
@@ -61,69 +68,138 @@ And use it like this with *npm*:
Y = require("yjs");
```
# Y()
In order to create an instance of Y, you need to have a connection object (instance of a Connector). Then, you can create a shared data type like this:
# Text editing example
```
var y = new Y(connector);
Y({
db: {
name: 'memory' // store in memory.
// name: 'indexeddb'
},
connector: {
name: 'websockets-client', // choose the websockets connector
// name: 'webrtc'
// name: 'xmpp'
room: 'Textarea-example-dev'
},
sourceDir: '/bower_components', // location of the y-* modules
share: {
textarea: 'Text' // y.share.textarea is of type Y.Text
}
// types: ['Richtext', 'Array'] // optional list of types you want to import
}).then(function (y) {
// bind the textarea to a shared text element
y.share.textarea.bind(document.getElementById('textfield'))
}
```
# Api
# Y.Map
Yjs includes only one type by default - the Y.Map type. It mimics the behaviour of a javascript Object. You can create, update, and remove properies on the Y.Map type. Furthermore, you can observe changes on this type as you can observe changes on Javascript Objects with [Object.observe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) - an ECMAScript 7 proposal which is likely to become accepted by the committee. Until then, we have our own implementation.
##### Reference
* Create
### Y(options)
* options.db
* Will be forwarded to the database adapter. Specify the database adaper on `options.db.name`.
* Have a look at the used database adapter repository to see all available options.
* options.connector
* Will be forwarded to the connector adapter. Specify the connector adaper on `options.connector.name`.
* All our connectors implement a `room` property. Clients that specify the same room share the same data.
* All of our connectors specify an `url` property that defines the connection endpoint of the used connector.
* All of our connectors also have a default connection endpoint that you can use for development.
* Have a look at the used connector repository to see all available options.
* options.sourceDir
* Path where all y-* modules are stored.
* Defaults to `/bower_components`
* Not required when running on `nodejs` / `iojs`
* When using browserify you can specify all used modules like this:
```
var map = y.set("new_map", Y.Map).then(function(map){
map // is my map type
});
```
* Every instance of Y is an Y.Map
```
var y = new Y(options);
```
* .get(name)
* Retrieve the value of a property. If the value is a type, `.get(name)` returns a promise
* .set(name, value)
* Set/update a property. `value` may be a primitive type, or a custom type definition (e.g. `Y.Map`)
* .delete(name)
* Delete a property
* .observe(observer)
* The `observer` is called whenever something on this object changes. Throws *add*, *update*, and *delete* events
* .observePath(path, observer)
* `path` is an array of property names. `observer` is called when the property under `path` is set, deleted, or updated
* .unobserve(f)
* Delete an observer
var Y = require('yjs')
// you need to require the db, connector, and *all* types you use!
require('y-memory')(Y)
require('y-webrtc')(Y)
require('y-map')(Y)
// ..
```
* options.share
* Specify on `options.share[arbitraryName]` types that are shared among all users.
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and create an Y.Array type on `y.share[arbitraryName]`.
* If userA doesn't specify `options.share[arbitraryName]`, it won't be available for userA.
* If userB specifies `options.share[arbitraryName]`, it still won't be available for userA. But all the updates are send from userB to userA.
* In contrast to Y.Map, types on `y.share.*` cannot be overwritten or deleted. Instead, they are merged among all users. This feature is only available on `y.share.*`
* Weird behavior: It is supported that two users specify different types with the same property name.
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies `options.share.x = 'Text'`. But they'll only share data if they specified the same type with the same property name
* options.type
* Array of modules that Yjs needs to require, before instantiating a shared type.
* By default Yjs requires the specified database adapter, the specified connector, and all modules that are used in `options.share.*`
* Put all types here that you intend to use, but are not used in y.share.*
# A note on intention preservation
When users create/update/delete the same property concurrently, only one change will prevail. Changes on different properties do not conflict with each other.
### Instantiated Y object (y)
`Y(options)` returns a promise that is fulfilled when..
# A note on time complexities
* .get(name)
* O(1)
* .set(name, value)
* O(1)
* .delete(name)
* O(1)
* Apply a delete operation from another user
* O(1)
* Apply an update operation from another user (set/update a property)
* Yjs does not transform against operations that do not conflict with each other.
* An operation conflicts with another operation if it changes the same property.
* Overall worst case complexety: O(|conflicts|!)
* All modules are loaded
* The specified database adapter is loaded
* The specified connector is loaded
* All types are included
* The connector is initialized, and a unique user id is set (received from the server)
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
# Status
Yjs is a work in progress. Different versions of the *y-* repositories may not work together. Just drop me a line if you run into troubles.
The promise returns an instance of Y. We denote it with a lower case `y`.
* y.share.*
* Instances of the types you specified on options.share.*
* y.share.* can only be defined once when you instantiate Y!
* y.connector is an instance of Y.AbstractConnector
* y.connector.onUserEvent(function (event) {..})
* Observe user events (event.action is either 'userLeft' or 'userJoined')
* y.connector.whenSynced(listener)
* `listener` is executed when y synced with at least one user.
* `listener` is not called when no other user is in the same room.
* y-websockets-client aways waits to sync with the server
* y.connector.disconnect()
* Force to disconnect this instance from the other instances
* y.connector.reconnect()
* Try to reconnect to the other instances (needs to be supported by the connector)
* Not supported by y-xmpp
* y.destroy()
* Destroy this object.
* Destroys all types (they will throw weird errors if you still use them)
* Disconnects from the other instances (via connector)
* Removes all data from the database
* y.db.stopGarbageCollector()
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage collection
* y.db.gcTimeout :: Number (defaults to 50000 ms)
* Time interval between two garbage collect cycles
* It is required that all instances exchanged all messages after two garbage collect cycles (after 100000 ms per default)
* y.db.userId :: String
* The used user id for this client. **Never overwrite this**
## Get help
There are some friendly people on [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who may help you with your problem, and answer your questions.
Please report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
If you want to see an issue fixed, please subscribe to the thread (or remind me via gitter).
## Changelog
##### 1.0
This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 1.0 it is possible to work asynchronously on a persistent database, which enables offline support.
### 11.0.0
* **All types now return a single event instead of list of events**
* Insert events contain a list of values
* Improved performance for large insertions & deletions
* Several bugfixes (offline editing related)
* Native support for node 4 (see #49)
### 10.0.0
* Support for more complex types (a type can be a composition of several types)
* Fixes several memory leaks
### 9.0.0
There were several rolling updates from 0.6 to 0.8. We consider Yjs stable since a long time,
and intend to continue stable releases. From this release forward y-* modules will implement peer-dependencies for npm, and dependencies for bower.
Furthermore, incompatible yjs instances will now throw errors when syncing - this feature was influenced by #48. The versioning jump was influenced by react (see [here](https://facebook.github.io/react/blog/2016/02/19/new-versioning-scheme.html))
### 0.6.0
This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 0.6.0 it is possible to work asynchronously on a persistent database, which enables offline support.
* Switched to semver versioning
* Requires a promise implementation in environment (es6 promises suffice, included in all the major browsers). Otherwise you have to include a polyfill
* Y.Object has been renamed to Y.Map
@@ -132,6 +208,7 @@ This is a complete rewrite of the 0.5 version of Yjs. Since Yjs 1.0 it is possib
* The Connector definition slightly changed (I'll update the wiki)
* The Type definitions completely changed, so you have to rewrite them (I'll rewrite the article in the wiki)
* Support for several packaging systems
* Flowtype
## Contribution
@@ -142,4 +219,6 @@ Yjs is licensed under the [MIT License](./LICENSE.txt).
<yjs@dbis.rwth-aachen.de>
[ShareJs]: https://github.com/sh
[ShareJs]: https://github.com/share/ShareJS
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki

72
declarations/Structs.js Normal file
View File

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

0
declarations/Type.js Normal file
View File

34
declarations/Y.js Normal file
View File

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

2
dist

Submodule dist updated: a94553f05c...47bcec8bc7

202
gulpfile.helper.js Normal file
View File

@@ -0,0 +1,202 @@
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]
}
gulp.task('dist:es5', function () {
var babelOptions = {
presets: ['es2015']
}
return (browserify({
entries: files.distEs5,
debug: true
}).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($.sourcemaps.write('.'))
.pipe(gulp.dest('./dist/')))
})
gulp.task('dist:es6', function () {
return (browserify({
entries: files.dist,
debug: true
}).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($.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

@@ -44,164 +44,61 @@
*/
var gulp = require('gulp')
var sourcemaps = require('gulp-sourcemaps')
var babel = require('gulp-babel')
var uglify = require('gulp-uglify')
var minimist = require('minimist')
var jasmine = require('gulp-jasmine')
var jasmineBrowser = require('gulp-jasmine-browser')
var concat = require('gulp-concat')
var watch = require('gulp-watch')
var shell = require('gulp-shell')
var $ = require('gulp-load-plugins')()
var runSequence = require('run-sequence').use(gulp)
var options = minimist(process.argv.slice(2), {
string: ['export', 'name', 'testport', 'testfiles', 'regenerator'],
default: {
export: 'ignore',
name: 'y.js',
testport: '8888',
testfiles: 'src/**/*.js',
regenerator: process.version < 'v0.12'
}
require('./gulpfile.helper.js')(gulp, {
polyfills: [],
entry: './src/y.js',
targetName: 'y.js',
moduleName: 'yjs',
includeRuntime: true,
specs: [
'./src/Database.spec.js',
'../y-array/src/Array.spec.js',
'../y-map/src/Map.spec.js'
]
})
var polyfills = [
'./node_modules/gulp-babel/node_modules/babel-core/node_modules/regenerator/runtime.js'
]
var concatOrder = [
'y.js',
'Connector.js',
'Database.js',
'Transaction.js',
'Struct.js',
'Utils.js',
'Databases/RedBlackTree.js',
'Databases/Memory.js',
'Databases/IndexedDB.js',
'Connectors/Test.js',
'Connectors/WebRTC.js',
'Types/Array.js',
'Types/Map.js',
'Types/TextBind.js'
]
var files = {
src: polyfills.concat(concatOrder.map(function (f) {
return 'src/' + f
})),
test: ['build/Helper.spec.js'].concat(concatOrder.map(function (f) {
return 'build/' + f
}).concat(['build/**/*.spec.js']))
}
if (options.regenerator) {
files.test = polyfills.concat(files.test)
}
gulp.task('deploy:build', function () {
return gulp.src(files.src)
.pipe(sourcemaps.init())
.pipe(concat('y.js'))
.pipe(babel({
loose: 'all',
modules: 'ignore',
experimental: true
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(uglify())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('.'))
.pipe(gulp.dest('./dist/Examples/bower_components/'))
return $.serve('dist/Examples/')()
})
gulp.task('deploy:updateSubmodule', function () {
return $.git.updateSubmodule({ args: '--init' })
})
gulp.task('deploy:copy', function () {
return gulp.src(['./y.js', './y.js.map', './README.md', 'package.json', 'LICENSE'])
.pipe(gulp.dest('./dist/'))
})
gulp.task('deploy:bump', function () {
return gulp.src('./package.json')
.pipe($.bump({type: 'patch'}))
.pipe(gulp.dest('./'))
})
gulp.task('deploy', ['deploy:updateSubmodule', 'deploy:bump', 'deploy:build', 'deploy:copy'], function () {
return gulp.src('./package.json', {read: false})
.pipe(shell([
'echo "Deploying version <%= getVersion(file.path) %>"',
'cd ./dist/',
'git add -A',
'git commit -am "Deploy <%= getVersion(file.path) %>" -n',
'git tag -a v<%= getVersion(file.path) %> -m "Release <%= getVersion(file.path) %>"',
'git push',
'git push origin --tags',
'cd ..',
'git commit -am "Release <%= getVersion(file.path) %>" -n',
'git push'
], {
templateData: {
getVersion: function (s) {
return require(s).version
}
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')
}
}))
})
gulp.task('build:test', function () {
var babelOptions = {
loose: 'all',
modules: 'ignore',
experimental: true
}
if (!options.regenerator) {
babelOptions.blacklist = 'regenerator'
}
gulp.src(files.src)
.pipe(sourcemaps.init())
.pipe(concat('y.js'))
.pipe(babel(babelOptions))
.pipe(sourcemaps.write())
.pipe(gulp.dest('.'))
return gulp.src('src/**/*.js')
.pipe(sourcemaps.init())
.pipe(babel(babelOptions))
.pipe(sourcemaps.write())
.pipe(gulp.dest('build'))
})
gulp.task('dev:node', ['test'], function () {
gulp.watch('src/**/*.js', ['test'])
})
gulp.task('dev:browser', ['build:test'], function () {
gulp.watch('src/**/*.js', ['build:test'])
gulp.src(files.test)
.pipe(watch(['build/**/*.js']))
.pipe(jasmineBrowser.specRunner())
.pipe(jasmineBrowser.server({port: options.testport}))
})
gulp.task('dev', ['build:test'], function () {
gulp.start('dev:browser')
gulp.start('dev:node')
})
gulp.task('test', ['build:test'], function () {
var testfiles = files.test
if (typeof Promise === 'undefined') {
testfiles.concat(['src/polyfills.js'])
}
return gulp.src(testfiles)
.pipe(jasmine({
verbose: true,
includeStuckTrace: true
}))
})
gulp.task('default', ['test'])

View File

@@ -1,12 +1,11 @@
{
"name": "yjs",
"version": "0.6.20",
"version": "11.2.6",
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
"main": "y.js",
"main": "./src/y.js",
"scripts": {
"test": "node --harmony ./node_modules/.bin/gulp test",
"lint": "./node_modules/.bin/standard",
"build": "./node_modules/.bin/standard build"
"lint": "./node_modules/.bin/standard"
},
"pre-commit": [
"lint",
@@ -16,6 +15,8 @@
"parser": "babel-eslint",
"ignore": [
"build/**",
"dist/**",
"declarations/**",
"./y.js",
"./y.js.map"
]
@@ -25,13 +26,13 @@
"url": "https://github.com/y-js/yjs.git"
},
"keywords": [
"Yjs",
"OT",
"Operational Transformation",
"collaboration",
"synchronization",
"ShareJs",
"OpenCoweb",
"concurrency"
"Collaboration",
"Synchronization",
"ShareJS",
"Coweb",
"Concurrency"
],
"author": "Kevin Jahns",
"email": "kevin.jahns@rwth-aachen.de",
@@ -41,25 +42,38 @@
},
"homepage": "http://y-js.org",
"devDependencies": {
"babel-eslint": "^4.1.2",
"babel-eslint": "^5.0.0-beta6",
"babel-plugin-transform-runtime": "^6.1.18",
"babel-preset-es2015": "^6.1.18",
"babelify": "^7.2.0",
"browserify": "^12.0.1",
"eslint": "^1.10.2",
"gulp": "^3.9.0",
"gulp-babel": "^5.2.1",
"gulp-bump": "^1.0.0",
"gulp-concat": "^2.6.0",
"gulp-filter": "^3.0.1",
"gulp-git": "^1.6.0",
"gulp-if": "^2.0.0",
"gulp-jasmine": "^2.0.1",
"gulp-jasmine-browser": "^0.2.3",
"gulp-load-plugins": "^1.0.0",
"gulp-prompt": "^0.1.2",
"gulp-rename": "^1.2.2",
"gulp-serve": "^1.2.0",
"gulp-shell": "^0.5.1",
"gulp-sourcemaps": "^1.5.2",
"gulp-tag-version": "^1.3.0",
"gulp-uglify": "^1.4.1",
"gulp-uglify": "1.4.*",
"gulp-util": "^3.0.6",
"gulp-watch": "^4.3.5",
"minimist": "^1.2.0",
"pre-commit": "^1.1.1",
"promise-polyfill": "^2.1.0",
"standard": "^5.2.2"
}
"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": {}
}

View File

@@ -1,328 +1,424 @@
/* globals Y */
/* @flow */
'use strict'
class AbstractConnector {
/*
opts contains the following information:
role : String Role of this client ("master" or "slave")
userId : String Uniquely defines the user.
debug: Boolean Whether to print debug messages (optional)
*/
constructor (y, opts) {
this.y = y
if (opts == null) {
opts = {}
}
if (opts.role == null || opts.role === 'master') {
this.role = 'master'
} else if (opts.role === 'slave') {
this.role = 'slave'
} else {
throw new Error("Role must be either 'master' or 'slave'!")
}
this.role = opts.role
this.connections = {}
this.isSynced = false
this.userEventListeners = []
this.whenSyncedListeners = []
this.currentSyncTarget = null
this.syncingClients = []
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
this.debug = opts.debug === true
this.broadcastedHB = false
this.syncStep2 = Promise.resolve()
}
reconnect () {
}
disconnect () {
this.connections = {}
this.isSynced = false
this.currentSyncTarget = null
this.broadcastedHB = false
this.syncingClients = []
this.whenSyncedListeners = []
return this.y.db.stopGarbageCollector()
}
setUserId (userId) {
this.userId = userId
return this.y.db.setUserId(userId)
}
onUserEvent (f) {
this.userEventListeners.push(f)
}
userLeft (user) {
delete this.connections[user]
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
this.findNextSyncTarget()
}
this.syncingClients = this.syncingClients.filter(function (cli) {
return cli !== user
})
for (var f of this.userEventListeners) {
f({
action: 'userLeft',
user: user
})
}
}
userJoined (user, role) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
}
if (this.connections[user] != null) {
throw new Error('This user already joined!')
}
this.connections[user] = {
isSynced: false,
role: role
}
for (var f of this.userEventListeners) {
f({
action: 'userJoined',
user: user,
role: role
})
}
if (this.currentSyncTarget == null) {
this.findNextSyncTarget()
}
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
whenSynced (f) {
if (this.isSynced) {
f()
} else {
this.whenSyncedListeners.push(f)
}
}
/*
returns false, if there is no sync target
true otherwise
*/
findNextSyncTarget () {
if (this.currentSyncTarget != null || this.isSynced) {
return // "The current sync has not finished!"
}
var syncUser = null
for (var uid in this.connections) {
if (!this.connections[uid].isSynced) {
syncUser = uid
break
module.exports = function (Y/* :any */) {
class AbstractConnector {
/* ::
y: YConfig;
role: SyncRole;
connections: Object;
isSynced: boolean;
userEventListeners: Array<Function>;
whenSyncedListeners: Array<Function>;
currentSyncTarget: ?UserId;
syncingClients: Array<UserId>;
forwardToSyncingClients: boolean;
debug: boolean;
broadcastedHB: boolean;
syncStep2: Promise;
userId: UserId;
send: Function;
broadcast: Function;
broadcastOpBuffer: Array<Operation>;
protocolVersion: number;
*/
/*
opts contains the following information:
role : String Role of this client ("master" or "slave")
userId : String Uniquely defines the user.
debug: Boolean Whether to print debug messages (optional)
*/
constructor (y, opts) {
this.y = y
if (opts == null) {
opts = {}
}
}
if (syncUser != null) {
var conn = this
this.currentSyncTarget = syncUser
this.y.db.requestTransaction(function *() {
conn.send(syncUser, {
type: 'sync step 1',
stateSet: yield* this.getStateSet(),
deleteSet: yield* this.getDeleteSet()
})
})
} else {
this.isSynced = true
// call when synced listeners
for (var f of this.whenSyncedListeners) {
f()
if (opts.role == null || opts.role === 'master') {
this.role = 'master'
} else if (opts.role === 'slave') {
this.role = 'slave'
} else {
throw new Error("Role must be either 'master' or 'slave'!")
}
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
this.role = opts.role
this.connections = {}
this.isSynced = false
this.userEventListeners = []
this.whenSyncedListeners = []
this.y.db.requestTransaction(function *() {
yield* this.garbageCollectAfterSync()
})
this.currentSyncTarget = null
this.syncingClients = []
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
this.debug = opts.debug === true
this.broadcastedHB = false
this.syncStep2 = Promise.resolve()
this.broadcastOpBuffer = []
this.protocolVersion = 11
}
}
send (uid, message) {
if (this.debug) {
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m) // eslint-disable-line
reconnect () {
}
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender, m) {
if (sender === this.userId) {
return
disconnect () {
this.connections = {}
this.isSynced = false
this.currentSyncTarget = null
this.broadcastedHB = false
this.syncingClients = []
this.whenSyncedListeners = []
return this.y.db.stopGarbageCollector()
}
if (this.debug) {
console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, JSON.parse(JSON.stringify(m))) // eslint-disable-line
repair () {
console.info('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
for (var name in this.connections) {
this.connections[name].isSynced = false
}
this.isSynced = false
this.currentSyncTarget = null
this.broadcastedHB = false
this.findNextSyncTarget()
}
if (m.type === 'sync step 1') {
// TODO: make transaction, stream the ops
let conn = this
this.y.db.requestTransaction(function *() {
var currentStateSet = yield* this.getStateSet()
yield* this.applyDeleteSet(m.deleteSet)
var ds = yield* this.getDeleteSet()
var ops = yield* this.getOperations(m.stateSet)
conn.send(sender, {
type: 'sync step 2',
os: ops,
stateSet: currentStateSet,
deleteSet: ds
setUserId (userId) {
if (this.userId == null) {
this.userId = userId
return this.y.db.setUserId(userId)
} else {
return null
}
}
onUserEvent (f) {
this.userEventListeners.push(f)
}
userLeft (user) {
if (this.connections[user] != null) {
delete this.connections[user]
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
this.findNextSyncTarget()
}
this.syncingClients = this.syncingClients.filter(function (cli) {
return cli !== user
})
if (this.forwardToSyncingClients) {
conn.syncingClients.push(sender)
setTimeout(function () {
conn.syncingClients = conn.syncingClients.filter(function (cli) {
return cli !== sender
})
for (var f of this.userEventListeners) {
f({
action: 'userLeft',
user: user
})
}
}
}
userJoined (user, role) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
}
if (this.connections[user] != null) {
throw new Error('This user already joined!')
}
this.connections[user] = {
isSynced: false,
role: role
}
for (var f of this.userEventListeners) {
f({
action: 'userJoined',
user: user,
role: role
})
}
if (this.currentSyncTarget == null) {
this.findNextSyncTarget()
}
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
whenSynced (f) {
if (this.isSynced) {
f()
} else {
this.whenSyncedListeners.push(f)
}
}
/*
returns false, if there is no sync target
true otherwise
*/
findNextSyncTarget () {
if (this.currentSyncTarget != null || this.isSynced) {
return // "The current sync has not finished!"
}
var syncUser = null
for (var uid in this.connections) {
if (!this.connections[uid].isSynced) {
syncUser = uid
break
}
}
var conn = this
if (syncUser != null) {
this.currentSyncTarget = syncUser
this.y.db.requestTransaction(function *() {
var stateSet = yield* this.getStateSet()
var deleteSet = yield* this.getDeleteSet()
conn.send(syncUser, {
type: 'sync step 1',
stateSet: stateSet,
deleteSet: deleteSet,
protocolVersion: conn.protocolVersion
})
})
} else {
this.y.db.requestTransaction(function *() {
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
conn.isSynced = true
yield* this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of conn.whenSyncedListeners) {
f()
}
conn.whenSyncedListeners = []
})
}
}
send (uid, message) {
if (this.debug) {
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, message) // eslint-disable-line
}
}
/*
Buffer operations, and broadcast them when ready.
*/
broadcastOps (ops) {
ops = ops.map(function (op) {
return Y.Struct[op.struct].encode(op)
})
var self = this
function broadcastOperations () {
if (self.broadcastOpBuffer.length > 0) {
self.broadcast({
type: 'update',
ops: self.broadcastOpBuffer
})
self.broadcastOpBuffer = []
}
}
if (this.broadcastOpBuffer.length === 0) {
this.broadcastOpBuffer = ops
if (this.y.db.transactionInProgress) {
this.y.db.whenTransactionsFinished().then(broadcastOperations)
} else {
setTimeout(broadcastOperations, 0)
}
} else {
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
}
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender/* :UserId */, message/* :Message */) {
if (sender === this.userId) {
return
}
if (this.debug) {
console.log(`receive ${sender} -> ${this.userId}: ${message.type}`, JSON.parse(JSON.stringify(message))) // eslint-disable-line
}
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
console.error(
`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
}
if (message.type === 'sync step 1') {
let conn = this
let m = message
this.y.db.requestTransaction(function *() {
var currentStateSet = yield* this.getStateSet()
yield* this.applyDeleteSet(m.deleteSet)
var ds = yield* this.getDeleteSet()
var ops = yield* this.getOperations(m.stateSet)
conn.send(sender, {
type: 'sync step 2',
os: ops,
stateSet: currentStateSet,
deleteSet: ds,
protocolVersion: this.protocolVersion
})
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'
})
}, conn.syncingClientDuration)
} else {
conn.send(sender, {
type: 'sync done'
})
}
conn._setSyncedWith(sender)
})
} else if (m.type === 'sync step 2') {
let conn = this
var broadcastHB = !this.broadcastedHB
this.broadcastedHB = true
var db = this.y.db
this.syncStep2 = new Promise(function (resolve) {
}
conn._setSyncedWith(sender)
})
} else if (message.type === 'sync step 2') {
let conn = this
var broadcastHB = !this.broadcastedHB
this.broadcastedHB = true
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)
this.store.apply(m.os)
db.requestTransaction(function * () {
var ops = yield* this.getOperations(m.stateSet)
if (ops.length > 0) {
m = {
type: 'update',
ops: ops
}
if (!broadcastHB) { // TODO: consider to broadcast here..
conn.send(sender, m)
conn.send(sender, {
type: 'update',
ops: ops
})
} else {
// broadcast only once!
conn.broadcast(m)
conn.broadcastOps(ops)
}
}
resolve()
defer.resolve()
})
})
})
} else if (m.type === 'sync done') {
var self = this
this.syncStep2.then(function () {
self._setSyncedWith(sender)
})
} else if (m.type === 'update') {
if (this.forwardToSyncingClients) {
for (var client of this.syncingClients) {
this.send(client, m)
} else if (message.type === 'sync done') {
var self = this
this.syncStep2.then(function () {
self._setSyncedWith(sender)
})
} else if (message.type === 'update') {
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)
}
this.y.db.apply(m.ops)
}
}
_setSyncedWith (user) {
var conn = this.connections[user]
if (conn != null) {
conn.isSynced = true
_setSyncedWith (user) {
var conn = this.connections[user]
if (conn != null) {
conn.isSynced = true
}
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
this.findNextSyncTarget()
}
}
if (user === this.currentSyncTarget) {
this.currentSyncTarget = null
this.findNextSyncTarget()
}
}
/*
Currently, the HB encodes operations as JSON. For the moment I want to keep it
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
too much overhead. Y is very likely to get changed a lot in the future
/*
Currently, the HB encodes operations as JSON. For the moment I want to keep it
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
too much overhead. Y is very likely to get changed a lot in the future
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
we encode the JSON as XML.
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
we encode the JSON as XML.
When the HB support encoding as XML, the format should look pretty much like this.
When the HB support encoding as XML, the format should look pretty much like this.
does not support primitive values as array elements
expects an ltx (less than xml) object
*/
parseMessageFromXml (m) {
function parseArray (node) {
for (var n of node.children) {
if (n.getAttribute('isArray') === 'true') {
return parseArray(n)
} else {
return parseObject(n)
does not support primitive values as array elements
expects an ltx (less than xml) object
*/
parseMessageFromXml (m/* :any */) {
function parseArray (node) {
for (var n of node.children) {
if (n.getAttribute('isArray') === 'true') {
return parseArray(n)
} else {
return parseObject(n)
}
}
}
function parseObject (node/* :any */) {
var json = {}
for (var attrName in node.attrs) {
var value = node.attrs[attrName]
var int = parseInt(value, 10)
if (isNaN(int) || ('' + int) !== value) {
json[attrName] = value
} else {
json[attrName] = int
}
}
for (var n/* :any */ in node.children) {
var name = n.name
if (n.getAttribute('isArray') === 'true') {
json[name] = parseArray(n)
} else {
json[name] = parseObject(n)
}
}
return json
}
parseObject(m)
}
function parseObject (node) {
var json = {}
for (var attrName in node.attrs) {
var value = node.attrs[attrName]
var int = parseInt(value, 10)
if (isNaN(int) || ('' + int) !== value) {
json[attrName] = value
} else {
json[attrName] = int
/*
encode message in xml
we use string because Strophe only accepts an "xml-string"..
So {a:4,b:{c:5}} will look like
<y a="4">
<b c="5"></b>
</y>
m - ltx element
json - Object
*/
encodeMessageToXml (msg, obj) {
// attributes is optional
function encodeObject (m, json) {
for (var name in json) {
var value = json[name]
if (name == null) {
// nop
} else if (value.constructor === Object) {
encodeObject(m.c(name), value)
} else if (value.constructor === Array) {
encodeArray(m.c(name), value)
} else {
m.setAttribute(name, value)
}
}
}
for (var n in node.children) {
var name = n.name
if (n.getAttribute('isArray') === 'true') {
json[name] = parseArray(n)
} else {
json[name] = parseObject(n)
function encodeArray (m, array) {
m.setAttribute('isArray', 'true')
for (var e of array) {
if (e.constructor === Object) {
encodeObject(m.c('array-element'), e)
} else {
encodeArray(m.c('array-element'), e)
}
}
}
return json
}
parseObject(m)
}
/*
encode message in xml
we use string because Strophe only accepts an "xml-string"..
So {a:4,b:{c:5}} will look like
<y a="4">
<b c="5"></b>
</y>
m - ltx element
json - Object
*/
encodeMessageToXml (msg, obj) {
// attributes is optional
function encodeObject (m, json) {
for (var name in json) {
var value = json[name]
if (name == null) {
// nop
} else if (value.constructor === Object) {
encodeObject(m.c(name), value)
} else if (value.constructor === Array) {
encodeArray(m.c(name), value)
} else {
m.setAttribute(name, value)
}
if (obj.constructor === Object) {
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else if (obj.constructor === Array) {
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else {
throw new Error("I can't encode this json!")
}
}
function encodeArray (m, array) {
m.setAttribute('isArray', 'true')
for (var e of array) {
if (e.constructor === Object) {
encodeObject(m.c('array-element'), e)
} else {
encodeArray(m.c('array-element'), e)
}
}
}
if (obj.constructor === Object) {
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else if (obj.constructor === Array) {
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
} else {
throw new Error("I can't encode this json!")
}
}
Y.AbstractConnector = AbstractConnector
}
Y.AbstractConnector = AbstractConnector

View File

@@ -1,136 +1,165 @@
/* global getRandom, Y, wait, async */
/* global getRandom, async */
'use strict'
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')
module.exports = function (Y) {
var globalRoom = {
users: {},
buffers: {},
removeUser: function (user) {
for (var i in this.users) {
this.users[i].userLeft(user)
}
}
}
}
Y.utils.globalRoom = globalRoom
function flushOne () {
var bufs = []
for (var i in globalRoom.buffers) {
if (globalRoom.buffers[i].length > 0) {
bufs.push(i)
}
}
if (bufs.length > 0) {
var userId = getRandom(bufs)
var m = globalRoom.buffers[userId].shift()
var user = globalRoom.users[userId]
user.receiveMessage(m[0], m[1])
return true
} else {
return false
}
}
// setInterval(flushOne, 10)
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) {
super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
}
send (userId, message) {
var buffer = globalRoom.buffers[userId]
if (buffer != null) {
buffer.push(JSON.parse(JSON.stringify([this.userId, message])))
}
}
broadcast (message) {
for (var key in globalRoom.buffers) {
globalRoom.buffers[key].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 this.flushAll()
}
disconnect () {
if (!this.isDisconnected()) {
globalRoom.removeUser(this.userId)
super.disconnect()
}
return wait()
}
flush () {
var self = this
return async(function * () {
yield wait()
while (globalRoom.buffers[self.userId].length > 0) {
var m = globalRoom.buffers[self.userId].shift()
this.receiveMessage(m[0], m[1])
yield wait()
}
})
}
flushAll () {
return new Promise(function (resolve) {
// flushes may result in more created operations,
// flush until there is nothing more to flush
function nextFlush () {
var c = flushOne()
if (c) {
while (flushOne()) {
// nop
}
wait().then(nextFlush)
} else {
wait().then(function () {
resolve()
})
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')
}
}
// in the case that there are
// still actions that want to be performed
wait().then(nextFlush)
})
},
whenTransactionsFinished: function () {
var ps = []
for (var name in this.users) {
ps.push(this.users[name].y.db.whenTransactionsFinished())
}
return Promise.all(ps)
},
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]
user.receiveMessage(m[0], m[1])
return user.y.db.whenTransactionsFinished()
} 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 {
setTimeout(function () {
var c = globalRoom.flushOne()
if (c) {
c.then(function () {
globalRoom.whenTransactionsFinished().then(nextFlush)
})
} else {
resolve()
}
}, 0)
}
}
globalRoom.whenTransactionsFinished().then(nextFlush)
})
}
}
/*
Flushes an operation for some user..
*/
flushOne () {
flushOne()
}
}
Y.utils.globalRoom = globalRoom
Y.Test = Test
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) {
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 () {
if (!this.isDisconnected()) {
globalRoom.removeUser(this.userId)
super.disconnect()
}
return this.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]
}
this.receiveMessage(m[0], m[1])
}
yield self.whenTransactionsFinished()
})
}
}
Y.Test = Test
}

View File

@@ -1,94 +0,0 @@
/* global Y, SimpleWebRTC */
'use strict'
class WebRTC 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.role = 'slave'
super(y, options)
this.webrtcOptions = {
url: options.url || 'https://yatta.ninja:8888',
room: options.room
}
var swr = new SimpleWebRTC(this.webrtcOptions)
this.swr = swr
var self = this
swr.once('connectionReady', function (userId) {
// SimpleWebRTC (swr) is initialized
swr.joinRoom(self.webrtcOptions.room)
swr.once('joinedRoom', function () {
self.setUserId(userId)
/*
var i
// notify the connector class about all the users that already
// joined the session
for(i in self.swr.webrtc.peers){
self.userJoined(self.swr.webrtc.peers[i].id, "master")
}*/
swr.on('channelMessage', function (peer, room_, message) {
// The client received a message
// Check if the connector is already initialized,
// only then forward the message to the connector class
if (message.type != null) {
self.receiveMessage(peer.id, message.payload)
}
})
})
swr.on('createdPeer', function (peer) {
// a new peer/client joined the session.
// Notify the connector class, if the connector
// is already initialized
self.userJoined(peer.id, 'master')
})
swr.on('peerStreamRemoved', function (peer) {
// a client left the session.
// Notify the connector class, if the connector
// is already initialized
self.userLeft(peer.id)
})
})
}
disconnect () {
this.swr.leaveRoom()
super.disconnect()
}
reconnect () {
this.swr.joinRoom(this.webrtcOptions.room)
super.reconnect()
}
send (uid, message) {
var self = this
// we have to make sure that the message is sent under all circumstances
var send = function () {
// check if the clients still exists
var peer = self.swr.webrtc.getPeers(uid)[0]
var success
if (peer) {
// success is true, if the message is successfully sent
success = peer.sendDirectly('simplewebrtc', 'yjs', message)
}
if (!success) {
// resend the message if it didn't work
setTimeout(send, 500)
}
}
// try to send the message
send()
}
broadcast (message) {
this.swr.sendDirectlyToAll('simplewebrtc', 'yjs', message)
}
isDisconnected () {
return false
}
}
Y.WebRTC = WebRTC

View File

@@ -1,341 +1,548 @@
/* global Y */
/* @flow */
'use strict'
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
module.exports = function (Y /* :any */) {
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
A database definition must alse define the following methods:
* logTable() (optional)
- show relevant information information in a table
* requestTransaction(makeGen)
- request a transaction
* destroy()
- destroy the database
*/
class AbstractDatabase {
constructor (y, opts) {
this.y = y
// E.g. this.listenersById[id] : Array<Listener>
this.listenersById = {}
// Execute the next time a transaction is requested
this.listenersByIdExecuteNow = []
// A transaction is requested
this.listenersByIdRequestPending = false
/* To make things more clear, the following naming conventions:
* ls : we put this.listenersById on ls
* l : Array<Listener>
* id : Id (can't use as property name)
* sid : String (converted from id via JSON.stringify
so we can use it as a property name)
Always remember to first overwrite
a property before you iterate over it!
A database definition must alse define the following methods:
* logTable() (optional)
- show relevant information information in a table
* requestTransaction(makeGen)
- request a transaction
* destroy()
- destroy the database
*/
class AbstractDatabase {
/* ::
y: YConfig;
forwardAppliedOperations: boolean;
listenersById: Object;
listenersByIdExecuteNow: Array<Object>;
listenersByIdRequestPending: boolean;
initializedTypes: Object;
whenUserIdSetListener: ?Function;
waitingTransactions: Array<Transaction>;
transactionInProgress: boolean;
executeOrder: Array<Object>;
gc1: Array<Struct>;
gc2: Array<Struct>;
gcTimeout: number;
gcInterval: any;
garbageCollect: Function;
executeOrder: Array<any>; // for debugging only
userId: UserId;
opClock: number;
transactionsFinished: ?{promise: Promise, resolve: any};
transact: (x: ?Generator) => any;
*/
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
// wont be kept in memory.
this.initializedTypes = {}
this.whenUserIdSetListener = null
this.waitingTransactions = []
this.transactionInProgress = false
if (typeof YConcurrency_TestingMode !== 'undefined') {
this.executeOrder = []
}
this.gc1 = [] // first stage
this.gc2 = [] // second stage -> after that, remove the op
this.gcTimeout = opts.gcTimeout || 5000
var os = this
function garbageCollect () {
return new Promise((resolve) => {
os.requestTransaction(function * () {
if (os.y.connector != null && os.y.connector.isSynced) {
for (var i in os.gc2) {
var oid = os.gc2[i]
yield* this.garbageCollectOperation(oid)
constructor (y, opts) {
this.y = y
var os = this
this.userId = null
var resolve
this.userIdPromise = new Promise(function (r) {
resolve = r
})
this.userIdPromise.resolve = resolve
// whether to broadcast all applied operations (insert & delete hook)
this.forwardAppliedOperations = false
// E.g. this.listenersById[id] : Array<Listener>
this.listenersById = {}
// Execute the next time a transaction is requested
this.listenersByIdExecuteNow = []
// A transaction is requested
this.listenersByIdRequestPending = false
/* To make things more clear, the following naming conventions:
* ls : we put this.listenersById on ls
* l : Array<Listener>
* id : Id (can't use as property name)
* sid : String (converted from id via JSON.stringify
so we can use it as a property name)
Always remember to first overwrite
a property before you iterate over it!
*/
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
// wont be kept in memory.
this.initializedTypes = {}
this.waitingTransactions = []
this.transactionInProgress = false
this.transactionIsFlushed = false
if (typeof YConcurrency_TestingMode !== 'undefined') {
this.executeOrder = []
}
this.gc1 = [] // first stage
this.gc2 = [] // second stage -> after that, remove the op
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeouts
function garbageCollect () {
return os.whenTransactionsFinished().then(function () {
if (os.gc1.length > 0 || os.gc2.length > 0) {
if (!os.y.isConnected()) {
console.warn('gc should be empty when disconnected!')
}
os.gc2 = os.gc1
os.gc1 = []
return new Promise((resolve) => {
os.requestTransaction(function * () {
if (os.y.connector != null && os.y.connector.isSynced) {
for (var i = 0; i < os.gc2.length; i++) {
var oid = os.gc2[i]
yield* this.garbageCollectOperation(oid)
}
os.gc2 = os.gc1
os.gc1 = []
}
// TODO: Use setInterval here instead (when garbageCollect is called several times there will be several timeouts..)
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
resolve()
})
})
} else {
// TODO: see above
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
}
return Promise.resolve()
}
if (os.gcTimeout > 0) {
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
})
}
this.garbageCollect = garbageCollect
if (this.gcTimeout > 0) {
garbageCollect()
}
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
this.opsReceivedTimestamp = new Date()
this.startRepairCheck()
}
startRepairCheck () {
var os = this
if (this.repairCheckInterval > 0) {
this.repairCheckIntervalHandler = setInterval(function repairOnMissingOperations () {
/*
Case 1. No ops have been received in a while (new Date() - os.opsReceivedTimestamp > os.repairCheckInterval)
- 1.1 os.listenersById is empty. Then the state was correct the whole time. -> Nothing to do (nor to update)
- 1.2 os.listenersById is not empty.
* Then the state was incorrect for at least {os.repairCheckInterval} seconds.
* -> Remove everything in os.listenersById and sync again (connector.repair())
Case 2. An op has been received in the last {os.repairCheckInterval } seconds.
It is not yet necessary to check for faulty behavior. Everything can still resolve itself. Wait for more messages.
If nothing was received for a while and os.listenersById is still not emty, we are in case 1.2
-> Do nothing
Baseline here is: we really only have to catch case 1.2..
*/
if (
new Date() - os.opsReceivedTimestamp > os.repairCheckInterval &&
Object.keys(os.listenersById).length > 0 // os.listenersById is not empty
) {
// haven't received operations for over {os.repairCheckInterval} seconds, resend state vector
os.listenersById = {}
os.opsReceivedTimestamp = new Date() // update so you don't send repair several times in a row
os.y.connector.repair()
}
}, this.repairCheckInterval)
}
}
stopRepairCheck () {
clearInterval(this.repairCheckIntervalHandler)
}
queueGarbageCollector (id) {
if (this.y.isConnected()) {
this.gc1.push(id)
}
}
emptyGarbageCollector () {
return new Promise(resolve => {
var check = () => {
if (this.gc1.length > 0 || this.gc2.length > 0) {
this.garbageCollect().then(check)
} else {
resolve()
}
}
setTimeout(check, 0)
})
}
addToDebug () {
if (typeof YConcurrency_TestingMode !== 'undefined') {
var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
if (typeof s === 'string') {
return s
} else {
return JSON.stringify(s)
}
}).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
this.executeOrder.push(command)
}
}
getDebugData () {
console.log(this.executeOrder.join('\n'))
}
stopGarbageCollector () {
var self = this
return new Promise(function (resolve) {
self.requestTransaction(function * () {
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i = 0; i < ungc.length; i++) {
var op = yield* this.getOperation(ungc[i])
if (op != null) {
delete op.gc
yield* this.setOperation(op)
}
}
resolve()
})
})
}
this.garbageCollect = garbageCollect
if (this.gcTimeout > 0) {
garbageCollect()
}
}
addToDebug () {
if (typeof YConcurrency_TestingMode !== 'undefined') {
var command = Array.prototype.map.call(arguments, function (s) {
if (typeof s === 'string') {
return s
} else {
return JSON.stringify(s)
/*
Try to add to GC.
TODO: rename this function
Rulez:
* Only gc if this user is online
* The most left element in a list must not be gc'd.
=> There is at least one element in the list
returns true iff op was added to GC
*/
* addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true
) {
var gc = false
if (left != null && left.deleted === true) {
gc = true
} else if (op.content != null && op.content.length > 1) {
op = yield* this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
gc = true
}
}).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
this.executeOrder.push(command)
}
}
getDebugData () {
console.log(this.executeOrder.join('\n'))
}
stopGarbageCollector () {
var self = this
return new Promise(function (resolve) {
self.requestTransaction(function * () {
var ungc = self.gc1.concat(self.gc2)
self.gc1 = []
self.gc2 = []
for (var i in ungc) {
var op = yield* this.getOperation(ungc[i])
delete op.gc
if (gc) {
op.gc = true
yield* this.setOperation(op)
this.store.queueGarbageCollector(op.id)
return true
}
resolve()
})
})
}
/*
Try to add to GC.
TODO: rename this function
Rulez:
* Only gc if this user is online
* The most left element in a list must not be gc'd.
=> There is at least one element in the list
returns true iff op was added to GC
*/
addToGarbageCollector (op, left) {
if (
op.gc == null &&
op.deleted === true &&
this.y.connector.isSynced &&
left != null &&
left.deleted === true
) {
op.gc = true
this.gc1.push(op.id)
return true
} else {
}
return false
}
}
removeFromGarbageCollector (op) {
function filter (o) {
return !Y.utils.compareIds(o, op.id)
}
this.gc1 = this.gc1.filter(filter)
this.gc2 = this.gc2.filter(filter)
delete op.gc
}
destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
}
setUserId (userId) {
var self = this
return new Promise(function (resolve) {
self.requestTransaction(function * () {
self.userId = userId
self.opClock = (yield* this.getState(userId)).clock
if (self.whenUserIdSetListener != null) {
self.whenUserIdSetListener()
self.whenUserIdSetListener = null
}
resolve()
})
})
}
whenUserIdSet (f) {
if (this.userId != null) {
f()
} else {
this.whenUserIdSetListener = f
}
}
getNextOpId () {
if (this.userId == null) {
throw new Error('OperationStore not yet initialized!')
}
return [this.userId, this.opClock++]
}
/*
Apply a list of operations.
* get a transaction
* check whether all Struct.*.requiredOps are in the OS
* check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied
*/
apply (ops) {
for (var key in ops) {
var o = ops[key]
var required = Y.Struct[o.struct].requiredOps(o)
this.whenOperationsExist(required, o)
}
}
/*
op is executed as soon as every operation requested is available.
Note that Transaction can (and should) buffer requests.
*/
whenOperationsExist (ids, op) {
if (ids.length > 0) {
let listener = {
op: op,
missing: ids.length
removeFromGarbageCollector (op) {
function filter (o) {
return !Y.utils.compareIds(o, op.id)
}
for (let key in ids) {
let id = ids[key]
let sid = JSON.stringify(id)
let l = this.listenersById[sid]
if (l == null) {
l = []
this.listenersById[sid] = l
}
l.push(listener)
}
} else {
this.listenersByIdExecuteNow.push({
op: op
})
this.gc1 = this.gc1.filter(filter)
this.gc2 = this.gc2.filter(filter)
delete op.gc
}
if (this.listenersByIdRequestPending) {
return
}
this.listenersByIdRequestPending = true
var store = this
this.requestTransaction(function * () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
var ls = store.listenersById
store.listenersById = {}
store.listenersByIdRequestPending = false
for (let key in exeNow) {
let o = exeNow[key].op
yield* store.tryExecute.call(this, o)
}
for (var sid in ls) {
var l = ls[sid]
var id = JSON.parse(sid)
if ((yield* this.getOperation(id)) == null) {
store.listenersById[sid] = l
* destroy () {
clearInterval(this.gcInterval)
this.gcInterval = null
this.stopRepairCheck()
for (var key in this.initializedTypes) {
var type = this.initializedTypes[key]
if (type._destroy != null) {
type._destroy()
} else {
for (let key in l) {
let listener = l[key]
let o = listener.op
if (--listener.missing === 0) {
yield* store.tryExecute.call(this, o)
console.error('The type you included does not provide destroy functionality, it will remain in memory (updating your packages will help).')
}
}
}
setUserId (userId) {
if (!this.userIdPromise.inProgress) {
this.userIdPromise.inProgress = true
var self = this
self.requestTransaction(function * () {
self.userId = userId
var state = yield* this.getState(userId)
self.opClock = state.clock
self.userIdPromise.resolve(userId)
})
}
return this.userIdPromise
}
whenUserIdSet (f) {
this.userIdPromise.then(f)
}
getNextOpId (numberOfIds) {
if (numberOfIds == null) {
throw new Error('getNextOpId expects the number of created ids to create!')
} else if (this.userId == null) {
throw new Error('OperationStore not yet initialized!')
} else {
var id = [this.userId, this.opClock]
this.opClock += numberOfIds
return id
}
}
/*
Apply a list of operations.
* we save a timestamp, because we received new operations that could resolve ops in this.listenersById (see this.startRepairCheck)
* get a transaction
* check whether all Struct.*.requiredOps are in the OS
* check if it is an expected op (otherwise wait for it)
* check if was deleted, apply a delete operation after op was applied
*/
apply (ops) {
this.opsReceivedTimestamp = new Date()
for (var i = 0; i < ops.length; i++) {
var o = ops[i]
if (o.id == null || o.id[0] !== this.y.connector.userId) {
var required = Y.Struct[o.struct].requiredOps(o)
if (o.requires != null) {
required = required.concat(o.requires)
}
this.whenOperationsExist(required, o)
}
}
}
/*
op is executed as soon as every operation requested is available.
Note that Transaction can (and should) buffer requests.
*/
whenOperationsExist (ids, op) {
if (ids.length > 0) {
let listener = {
op: op,
missing: ids.length
}
for (let i = 0; i < ids.length; i++) {
let id = ids[i]
let sid = JSON.stringify(id)
let l = this.listenersById[sid]
if (l == null) {
l = []
this.listenersById[sid] = l
}
l.push(listener)
}
} else {
this.listenersByIdExecuteNow.push({
op: op
})
}
if (this.listenersByIdRequestPending) {
return
}
this.listenersByIdRequestPending = true
var store = this
this.requestTransaction(function * () {
var exeNow = store.listenersByIdExecuteNow
store.listenersByIdExecuteNow = []
var ls = store.listenersById
store.listenersById = {}
store.listenersByIdRequestPending = false
for (let key = 0; key < exeNow.length; key++) {
let o = exeNow[key].op
yield* store.tryExecute.call(this, o)
}
for (var sid in ls) {
var l = ls[sid]
var id = JSON.parse(sid)
var op
if (typeof id[1] === 'string') {
op = yield* this.getOperation(id)
} else {
op = yield* this.getInsertion(id)
}
if (op == null) {
store.listenersById[sid] = l
} else {
for (let i = 0; i < l.length; i++) {
let listener = l[i]
let o = listener.op
if (--listener.missing === 0) {
yield* store.tryExecute.call(this, o)
}
}
}
}
})
}
/*
Actually execute an operation, when all expected operations are available.
*/
/* :: // TODO: this belongs somehow to transaction
store: Object;
getOperation: any;
isGarbageCollected: any;
addOperation: any;
whenOperationsExist: any;
*/
* tryExecute (op) {
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
if (op.struct === 'Delete') {
yield* Y.Struct.Delete.execute.call(this, op)
// this is now called in Transaction.deleteOperation!
// yield* this.store.operationAdded(this, op)
} else {
// check if this op was defined
var defined = yield* this.getInsertion(op.id)
while (defined != null && defined.content != null) {
// check if this op has a longer content in the case it is defined
if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
var overlapSize = defined.content.length - (op.id[1] - defined.id[1])
op.content.splice(0, overlapSize)
op.id = [op.id[0], op.id[1] + overlapSize]
op.left = Y.utils.getLastId(defined)
op.origin = op.left
defined = yield* this.getOperation(op.id) // getOperation suffices here
} else {
break
}
}
if (defined == null) {
var opid = op.id
var isGarbageCollected = yield* this.isGarbageCollected(opid)
if (!isGarbageCollected) {
// TODO: reduce number of get / put calls for op ..
yield* Y.Struct[op.struct].execute.call(this, op)
yield* this.addOperation(op)
yield* this.store.operationAdded(this, op)
// operationAdded can change op..
op = yield* this.getOperation(opid)
// if insertion, try to combine with left
yield* this.tryCombineWithLeft(op)
}
}
}
}
/*
* Called by a transaction when an operation is added.
* This function is especially important for y-indexeddb, where several instances may share a single database.
* Every time an operation is created by one instance, it is send to all other instances and operationAdded is called
*
* If it's not a Delete operation:
* * Checks if another operation is executable (listenersById)
* * Update state, if possible
*
* Always:
* * Call type
*/
* operationAdded (transaction, op) {
if (op.struct === 'Delete') {
var target = yield* transaction.getInsertion(op.target)
var type = this.initializedTypes[JSON.stringify(target.parent)]
if (type != null) {
yield* type._changed(transaction, op)
}
} else {
// increase SS
yield* transaction.updateState(op.id[0])
var opLen = op.content != null ? op.content.length : 1
for (let i = 0; i < opLen; i++) {
// notify whenOperation listeners (by id)
var sid = JSON.stringify([op.id[0], op.id[1] + i])
var l = this.listenersById[sid]
delete this.listenersById[sid]
if (l != null) {
for (var key in l) {
var listener = l[key]
if (--listener.missing === 0) {
this.whenOperationsExist([], listener.op)
}
}
}
}
var t = this.initializedTypes[JSON.stringify(op.parent)]
// if parent is deleted, mark as gc'd and return
if (op.parent != null) {
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
if (parentIsDeleted) {
yield* transaction.deleteList(op.id)
return
}
}
// notify parent, if it was instanciated as a custom type
if (t != null) {
let o = Y.utils.copyOperation(op)
yield* t._changed(transaction, o)
}
if (!op.deleted) {
// Delete if DS says this is actually deleted
var len = op.content != null ? op.content.length : 1
var startId = op.id // You must not use op.id in the following loop, because op will change when deleted
// TODO: !! console.log('TODO: change this before commiting')
for (let i = 0; i < len; i++) {
var id = [startId[0], startId[1] + i]
var opIsDeleted = yield* transaction.isDeleted(id)
if (opIsDeleted) {
var delop = {
struct: 'Delete',
target: id
}
yield* this.tryExecute.call(transaction, delop)
}
}
}
}
})
}
/*
Actually execute an operation, when all expected operations are available.
*/
* tryExecute (op) {
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
if (op.struct === 'Delete') {
yield* Y.Struct.Delete.execute.call(this, op)
yield* this.store.operationAdded(this, op)
} else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
yield* Y.Struct[op.struct].execute.call(this, op)
yield* this.addOperation(op)
yield* this.store.operationAdded(this, op)
}
}
// called by a transaction when an operation is added
* operationAdded (transaction, op) {
if (op.struct === 'Delete') {
var target = yield* transaction.getOperation(op.target)
if (target != null) {
var type = transaction.store.initializedTypes[JSON.stringify(target.parent)]
if (type != null) {
yield* type._changed(transaction, {
struct: 'Delete',
target: op.target
whenTransactionsFinished () {
if (this.transactionInProgress) {
if (this.transactionsFinished == null) {
var resolve
var promise = new Promise(function (r) {
resolve = r
})
this.transactionsFinished = {
resolve: resolve,
promise: promise
}
return promise
} else {
return this.transactionsFinished.promise
}
} else {
return Promise.resolve()
}
} else {
// increase SS
var o = op
var state = yield* transaction.getState(op.id[0])
while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
state.clock++
yield* transaction.checkDeleteStoreForState(state)
o = yield* transaction.os.findNext(o.id)
}
yield* transaction.setState(state)
// notify whenOperation listeners (by id)
var sid = JSON.stringify(op.id)
var l = this.listenersById[sid]
delete this.listenersById[sid]
if (l != null) {
for (var key in l) {
var listener = l[key]
if (--listener.missing === 0) {
this.whenOperationsExist([], listener.op)
}
// Check if there is another transaction request.
// * the last transaction is always a flush :)
getNextRequest () {
if (this.waitingTransactions.length === 0) {
if (this.transactionIsFlushed) {
this.transactionInProgress = false
this.transactionIsFlushed = false
if (this.transactionsFinished != null) {
this.transactionsFinished.resolve()
this.transactionsFinished = null
}
return null
} else {
this.transactionIsFlushed = true
return function * () {
yield* this.flush()
}
}
}
var t = this.initializedTypes[JSON.stringify(op.parent)]
// notify parent, if it has been initialized as a custom type
if (t != null) {
yield* t._changed(transaction, Y.utils.copyObject(op))
}
// Delete if DS says this is actually deleted
if (!op.deleted && (yield* transaction.isDeleted(op.id))) {
var delop = {
struct: 'Delete',
target: op.id
}
yield* Y.Struct['Delete'].execute.call(transaction, delop)
if (t != null) {
yield* t._changed(transaction, delop)
}
} else {
this.transactionIsFlushed = false
return this.waitingTransactions.shift()
}
}
}
getNextRequest () {
if (this.waitingTransactions.length === 0) {
this.transactionInProgress = false
return null
} else {
return this.waitingTransactions.shift()
}
}
requestTransaction (makeGen, callImmediately) {
if (callImmediately) {
this.transact(makeGen)
} else if (!this.transactionInProgress) {
this.transactionInProgress = true
var self = this
setTimeout(function () {
self.transact(makeGen)
}, 0)
} else {
requestTransaction (makeGen/* :any */, callImmediately) {
this.waitingTransactions.push(makeGen)
if (!this.transactionInProgress) {
this.transactionInProgress = true
setTimeout(() => {
this.transact(this.getNextRequest())
}, 0)
}
}
}
Y.AbstractDatabase = AbstractDatabase
}
Y.AbstractDatabase = AbstractDatabase

View File

@@ -1,5 +1,8 @@
/* global Y, async, databases */
/* global async, databases, describe, beforeEach, afterEach */
/* eslint-env browser,jasmine,console */
'use strict'
var Y = require('./SpecHelper.js')
for (let database of databases) {
describe(`Database (${database})`, function () {
@@ -20,7 +23,7 @@ for (let database of databases) {
})
it('Deleted operation is deleted', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['u1', 10])
yield* this.markDeleted(['u1', 10], 1)
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
done()
@@ -28,8 +31,8 @@ for (let database of databases) {
}))
it('Deleted operation extends other deleted operation', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['u1', 10])
yield* this.markDeleted(['u1', 11])
yield* this.markDeleted(['u1', 10], 1)
yield* this.markDeleted(['u1', 11], 1)
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
expect(yield* this.isDeleted(['u1', 11])).toBeTruthy()
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
@@ -38,77 +41,77 @@ for (let database of databases) {
}))
it('Deleted operation extends other deleted operation', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['0', 3])
yield* this.markDeleted(['0', 4])
yield* this.markDeleted(['0', 2])
yield* this.markDeleted(['0', 3], 1)
yield* this.markDeleted(['0', 4], 1)
yield* this.markDeleted(['0', 2], 1)
expect(yield* this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
done()
})
}))
it('Debug #1', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 2])
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 2])
yield* this.markGarbageCollected(['166', 2])
yield* this.markDeleted(['166', 1])
yield* this.markDeleted(['166', 3])
yield* this.markGarbageCollected(['166', 3])
yield* this.markDeleted(['166', 0])
yield* this.markDeleted(['166', 0], 1)
yield* this.markDeleted(['166', 2], 1)
yield* this.markDeleted(['166', 0], 1)
yield* this.markDeleted(['166', 2], 1)
yield* this.markGarbageCollected(['166', 2], 1)
yield* this.markDeleted(['166', 1], 1)
yield* this.markDeleted(['166', 3], 1)
yield* this.markGarbageCollected(['166', 3], 1)
yield* this.markDeleted(['166', 0], 1)
expect(yield* this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
done()
})
}))
it('Debug #2', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['293', 0])
yield* this.markDeleted(['291', 2])
yield* this.markDeleted(['291', 2])
yield* this.markGarbageCollected(['293', 0])
yield* this.markDeleted(['293', 1])
yield* this.markGarbageCollected(['291', 2])
yield* this.markDeleted(['293', 0], 1)
yield* this.markDeleted(['291', 2], 1)
yield* this.markDeleted(['291', 2], 1)
yield* this.markGarbageCollected(['293', 0], 1)
yield* this.markDeleted(['293', 1], 1)
yield* this.markGarbageCollected(['291', 2], 1)
expect(yield* this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
done()
})
}))
it('Debug #3', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['581', 0])
yield* this.markDeleted(['581', 1])
yield* this.markDeleted(['580', 0])
yield* this.markDeleted(['580', 0])
yield* this.markGarbageCollected(['581', 0])
yield* this.markDeleted(['581', 2])
yield* this.markDeleted(['580', 1])
yield* this.markDeleted(['580', 2])
yield* this.markDeleted(['580', 1])
yield* this.markDeleted(['580', 2])
yield* this.markGarbageCollected(['581', 2])
yield* this.markGarbageCollected(['581', 1])
yield* this.markGarbageCollected(['580', 1])
yield* this.markDeleted(['581', 0], 1)
yield* this.markDeleted(['581', 1], 1)
yield* this.markDeleted(['580', 0], 1)
yield* this.markDeleted(['580', 0], 1)
yield* this.markGarbageCollected(['581', 0], 1)
yield* this.markDeleted(['581', 2], 1)
yield* this.markDeleted(['580', 1], 1)
yield* this.markDeleted(['580', 2], 1)
yield* this.markDeleted(['580', 1], 1)
yield* this.markDeleted(['580', 2], 1)
yield* this.markGarbageCollected(['581', 2], 1)
yield* this.markGarbageCollected(['581', 1], 1)
yield* this.markGarbageCollected(['580', 1], 1)
expect(yield* this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
done()
})
}))
it('Debug #4', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['544', 0])
yield* this.markDeleted(['543', 2])
yield* this.markDeleted(['544', 0])
yield* this.markDeleted(['543', 2])
yield* this.markGarbageCollected(['544', 0])
yield* this.markDeleted(['545', 1])
yield* this.markDeleted(['543', 4])
yield* this.markDeleted(['543', 3])
yield* this.markDeleted(['544', 1])
yield* this.markDeleted(['544', 2])
yield* this.markDeleted(['544', 1])
yield* this.markDeleted(['544', 2])
yield* this.markGarbageCollected(['543', 2])
yield* this.markGarbageCollected(['543', 4])
yield* this.markGarbageCollected(['544', 2])
yield* this.markGarbageCollected(['543', 3])
yield* this.markDeleted(['544', 0], 1)
yield* this.markDeleted(['543', 2], 1)
yield* this.markDeleted(['544', 0], 1)
yield* this.markDeleted(['543', 2], 1)
yield* this.markGarbageCollected(['544', 0], 1)
yield* this.markDeleted(['545', 1], 1)
yield* this.markDeleted(['543', 4], 1)
yield* this.markDeleted(['543', 3], 1)
yield* this.markDeleted(['544', 1], 1)
yield* this.markDeleted(['544', 2], 1)
yield* this.markDeleted(['544', 1], 1)
yield* this.markDeleted(['544', 2], 1)
yield* this.markGarbageCollected(['543', 2], 1)
yield* this.markGarbageCollected(['543', 4], 1)
yield* this.markGarbageCollected(['544', 2], 1)
yield* this.markGarbageCollected(['543', 3], 1)
expect(yield* this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
done()
})
@@ -133,21 +136,21 @@ for (let database of databases) {
}))
it('Debug #7', async(function * (done) {
store.requestTransaction(function * () {
yield* this.markDeleted(['9', 2])
yield* this.markDeleted(['11', 2])
yield* this.markDeleted(['11', 4])
yield* this.markDeleted(['11', 1])
yield* this.markDeleted(['9', 4])
yield* this.markDeleted(['10', 0])
yield* this.markGarbageCollected(['11', 2])
yield* this.markDeleted(['11', 2])
yield* this.markGarbageCollected(['11', 3])
yield* this.markDeleted(['11', 3])
yield* this.markDeleted(['11', 3])
yield* this.markDeleted(['9', 4])
yield* this.markDeleted(['10', 0])
yield* this.markGarbageCollected(['11', 1])
yield* this.markDeleted(['11', 1])
yield* this.markDeleted(['9', 2], 1)
yield* this.markDeleted(['11', 2], 1)
yield* this.markDeleted(['11', 4], 1)
yield* this.markDeleted(['11', 1], 1)
yield* this.markDeleted(['9', 4], 1)
yield* this.markDeleted(['10', 0], 1)
yield* this.markGarbageCollected(['11', 2], 1)
yield* this.markDeleted(['11', 2], 1)
yield* this.markGarbageCollected(['11', 3], 1)
yield* this.markDeleted(['11', 3], 1)
yield* this.markDeleted(['11', 3], 1)
yield* this.markDeleted(['9', 4], 1)
yield* this.markDeleted(['10', 0], 1)
yield* this.markGarbageCollected(['11', 1], 1)
yield* this.markDeleted(['11', 1], 1)
expect(yield* this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
done()
})

View File

@@ -1,181 +0,0 @@
/* global Y */
'use strict'
Y.IndexedDB = (function () {
class Store {
constructor (transaction, name) {
this.store = transaction.objectStore(name)
}
* find (id) {
return yield this.store.get(id)
}
* put (v) {
yield this.store.put(v)
}
* delete (id) {
yield this.store.delete(id)
}
* findWithLowerBound (start) {
return yield this.store.openCursor(window.IDBKeyRange.lowerBound(start))
}
* findWithUpperBound (end) {
return yield this.store.openCursor(window.IDBKeyRange.upperBound(end), 'prev')
}
* findNext (id) {
return yield* this.findWithLowerBound([id[0], id[1] + 1])
}
* findPrev (id) {
return yield* this.findWithUpperBound([id[0], id[1] - 1])
}
* iterate (t, start, end, gen) {
var range = null
if (start != null && end != null) {
range = window.IDBKeyRange.bound(start, end)
} else if (start != null) {
range = window.IDBKeyRange.lowerBound(start)
} else if (end != null) {
range = window.IDBKeyRange.upperBound(end)
}
var cursorResult = this.store.openCursor(range)
while ((yield cursorResult) != null) {
yield* gen.call(t, cursorResult.result.value)
cursorResult.result.continue()
}
}
}
class Transaction extends Y.Transaction {
constructor (store) {
super(store)
var transaction = store.db.transaction(['OperationStore', 'StateStore', 'DeleteStore'], 'readwrite')
this.store = store
this.ss = new Store(transaction, 'StateStore')
this.os = new Store(transaction, 'OperationStore')
this.ds = new Store(transaction, 'DeleteStore')
}
}
class OperationStore extends Y.AbstractDatabase {
constructor (y, opts) {
super(y, opts)
if (opts == null) {
opts = {}
}
if (opts.namespace == null || typeof opts.namespace !== 'string') {
throw new Error('IndexedDB: expect a string (opts.namespace)!')
} else {
this.namespace = opts.namespace
}
if (opts.idbVersion != null) {
this.idbVersion = opts.idbVersion
} else {
this.idbVersion = 5
}
var store = this
// initialize database!
this.requestTransaction(function * () {
store.db = yield window.indexedDB.open(opts.namespace, store.idbVersion)
})
if (opts.cleanStart) {
this.requestTransaction(function * () {
yield this.os.store.clear()
yield this.ds.store.clear()
yield this.ss.store.clear()
})
}
var operationsToAdd = []
window.addEventListener('storage', function (event) {
if (event.key === '__YJS__' + store.namespace) {
operationsToAdd.push(event.newValue)
if (operationsToAdd.length === 1) {
store.requestTransaction(function * () {
var add = operationsToAdd
operationsToAdd = []
for (var i in add) {
// don't call the localStorage event twice..
var op = JSON.parse(add[i])
if (op.struct !== 'Delete') {
op = yield* this.getOperation(op.id)
}
yield* this.store.operationAdded(this, op, true)
}
})
}
}
}, false)
}
* operationAdded (transaction, op, noAdd) {
yield* super.operationAdded(transaction, op)
if (!noAdd) {
window.localStorage['__YJS__' + this.namespace] = JSON.stringify(op)
}
}
transact (makeGen) {
var transaction = this.db != null ? new Transaction(this) : null
var store = this
var gen = makeGen.call(transaction)
handleTransactions(gen.next())
function handleTransactions (result) {
var request = result.value
if (result.done) {
makeGen = store.getNextRequest()
if (makeGen != null) {
if (transaction == null && store.db != null) {
transaction = new Transaction(store)
}
gen = makeGen.call(transaction)
handleTransactions(gen.next())
} // else no transaction in progress!
return
}
if (request.constructor === window.IDBRequest) {
request.onsuccess = function () {
var res = request.result
if (res != null && res.constructor === window.IDBCursorWithValue) {
res = res.value
}
handleTransactions(gen.next(res))
}
request.onerror = function (err) {
gen.throw(err)
}
} else if (request.constructor === window.IDBCursor) {
request.onsuccess = function () {
handleTransactions(gen.next(request.result != null ? request.result.value : null))
}
request.onerror = function (err) {
gen.throw(err)
}
} else if (request.constructor === window.IDBOpenDBRequest) {
request.onsuccess = function (event) {
var db = event.target.result
handleTransactions(gen.next(db))
}
request.onerror = function () {
gen.throw("Couldn't open IndexedDB database!")
}
request.onupgradeneeded = function (event) {
var db = event.target.result
try {
db.createObjectStore('OperationStore', {keyPath: 'id'})
db.createObjectStore('DeleteStore', {keyPath: 'id'})
db.createObjectStore('StateStore', {keyPath: 'id'})
} catch (e) {
console.log('Store already exists!')
}
}
} else {
gen.throw('You must not yield this type!')
}
}
}
// TODO: implement "free"..
* destroy () {
this.db.close()
yield window.indexedDB.deleteDatabase(this.namespace)
}
}
return OperationStore
})()

View File

@@ -1,19 +0,0 @@
/* global Y */
/* eslint-env browser,jasmine */
if (typeof window !== 'undefined' && false) {
describe('IndexedDB', function () {
var ob
beforeAll(function () {
ob = new Y.IndexedDB(null, {namespace: 'Test', gcTimeout: -1})
})
afterAll(function (done) {
ob.requestTransaction(function *() {
yield* ob.removeDatabase()
ob = null
done()
})
})
})
}

View File

@@ -1,63 +0,0 @@
/* global Y */
'use strict'
Y.Memory = (function () {
class Transaction extends Y.Transaction {
constructor (store) {
super(store)
this.store = store
this.ss = store.ss
this.os = store.os
this.ds = store.ds
}
}
class Database extends Y.AbstractDatabase {
constructor (y, opts) {
super(y, opts)
this.os = new Y.utils.RBTree()
this.ds = new Y.utils.RBTree()
this.ss = new Y.utils.RBTree()
}
logTable () {
var self = this
self.requestTransaction(function * () {
console.log('User: ', this.store.y.connector.userId, "==============================") // eslint-disable-line
console.log("State Set (SS):", yield* this.getStateSet()) // eslint-disable-line
console.log("Operation Store (OS):") // eslint-disable-line
yield* this.os.logTable() // eslint-disable-line
console.log("Deletion Store (DS):") //eslint-disable-line
yield* this.ds.logTable() // eslint-disable-line
if (this.store.gc1.length > 0 || this.store.gc2.length > 0) {
console.warn('GC1|2 not empty!', this.store.gc1, this.store.gc2)
}
if (JSON.stringify(this.store.listenersById) !== '{}') {
console.warn('listenersById not empty!')
}
if (JSON.stringify(this.store.listenersByIdExecuteNow) !== '[]') {
console.warn('listenersByIdExecuteNow not empty!')
}
if (this.store.transactionInProgress) {
console.warn('Transaction still in progress!')
}
}, true)
}
transact (makeGen) {
var t = new Transaction(this)
while (makeGen !== null) {
var gen = makeGen.call(t)
var res = gen.next()
while (!res.done) {
res = gen.next(res.value)
}
makeGen = this.getNextRequest()
}
}
* destroy () {
super.destroy()
delete this.os
delete this.ss
delete this.ds
}
}
return Database
})()

View File

@@ -1,489 +0,0 @@
/* global Y */
'use strict'
/*
This file contains a not so fancy implemantion of a Red Black Tree.
*/
class N {
// A created node is always red!
constructor (val) {
this.val = val
this.color = true
this._left = null
this._right = null
this._parent = null
if (val.id === null) {
throw new Error('You must define id!')
}
}
isRed () { return this.color }
isBlack () { return !this.color }
redden () { this.color = true; return this }
blacken () { this.color = false; return this }
get grandparent () {
return this.parent.parent
}
get parent () {
return this._parent
}
get sibling () {
return (this === this.parent.left)
? this.parent.right : this.parent.left
}
get left () {
return this._left
}
get right () {
return this._right
}
set left (n) {
if (n !== null) {
n._parent = this
}
this._left = n
}
set right (n) {
if (n !== null) {
n._parent = this
}
this._right = n
}
rotateLeft (tree) {
var parent = this.parent
var newParent = this.right
var newRight = this.right.left
newParent.left = this
this.right = newRight
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === this) {
parent.left = newParent
} else if (parent.right === this) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
next () {
if (this.right !== null) {
// search the most left node in the right tree
var o = this.right
while (o.left !== null) {
o = o.left
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.left) {
p = p.parent
}
return p.parent
}
}
prev () {
if (this.left !== null) {
// search the most right node in the left tree
var o = this.left
while (o.right !== null) {
o = o.right
}
return o
} else {
var p = this
while (p.parent !== null && p !== p.parent.right) {
p = p.parent
}
return p.parent
}
}
rotateRight (tree) {
var parent = this.parent
var newParent = this.left
var newLeft = this.left.right
newParent.right = this
this.left = newLeft
if (parent === null) {
tree.root = newParent
newParent._parent = null
} else if (parent.left === this) {
parent.left = newParent
} else if (parent.right === this) {
parent.right = newParent
} else {
throw new Error('The elements are wrongly connected!')
}
}
getUncle () {
// we can assume that grandparent exists when this is called!
if (this.parent === this.parent.parent.left) {
return this.parent.parent.right
} else {
return this.parent.parent.left
}
}
}
class RBTree {
constructor () {
this.root = null
this.length = 0
}
* findNext (id) {
return yield* this.findWithLowerBound([id[0], id[1] + 1])
}
* findPrev (id) {
return yield* this.findWithUpperBound([id[0], id[1] - 1])
}
findNodeWithLowerBound (from) {
if (from === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((from === null || Y.utils.smaller(from, o.val.id)) && o.left !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.left
} else if (from !== null && Y.utils.smaller(o.val.id, from)) {
// o is not within the bound, maybe one of the right elements is..
if (o.right !== null) {
o = o.right
} else {
// there is no right element. Search for the next bigger element,
// this should be within the bounds
return o.next()
}
} else {
return o
}
}
}
}
findNodeWithUpperBound (to) {
if (to === void 0) {
throw new Error('You must define from!')
}
var o = this.root
if (o === null) {
return null
} else {
while (true) {
if ((to === null || Y.utils.smaller(o.val.id, to)) && o.right !== null) {
// o is included in the bound
// try to find an element that is closer to the bound
o = o.right
} else if (to !== null && Y.utils.smaller(to, o.val.id)) {
// o is not within the bound, maybe one of the left elements is..
if (o.left !== null) {
o = o.left
} else {
// there is no left element. Search for the prev smaller element,
// this should be within the bounds
return o.prev()
}
} else {
return o
}
}
}
}
* findWithLowerBound (from) {
var n = this.findNodeWithLowerBound(from)
return n == null ? null : n.val
}
* findWithUpperBound (to) {
var n = this.findNodeWithUpperBound(to)
return n == null ? null : n.val
}
* iterate (t, from, to, f) {
var o = this.findNodeWithLowerBound(from)
while (o !== null && (to === null || Y.utils.smaller(o.val.id, to) || Y.utils.compareIds(o.val.id, to))) {
yield* f.call(t, o.val)
o = o.next()
}
return true
}
* logTable (from, to, filter) {
if (filter == null) {
filter = function () {
return true
}
}
if (from == null) { from = null }
if (to == null) { to = null }
var os = []
yield* this.iterate(this, from, to, function * (o) {
if (filter(o)) {
var o_ = {}
for (var key in o) {
if (typeof o[key] === 'object') {
o_[key] = JSON.stringify(o[key])
} else {
o_[key] = o[key]
}
}
os.push(o_)
}
})
if (console.table != null) {
console.table(os)
}
}
* find (id) {
var n
return (n = this.findNode(id)) ? n.val : null
}
findNode (id) {
if (id == null || id.constructor !== Array) {
throw new Error('Expect id to be an array!')
}
var o = this.root
if (o === null) {
return false
} else {
while (true) {
if (o === null) {
return false
}
if (Y.utils.smaller(id, o.val.id)) {
o = o.left
} else if (Y.utils.smaller(o.val.id, id)) {
o = o.right
} else {
return o
}
}
}
}
* delete (id) {
if (id == null || id.constructor !== Array) {
throw new Error('id is expected to be an Array!')
}
var d = this.findNode(id)
if (d == null) {
throw new Error('Element does not exist!')
}
this.length--
if (d.left !== null && d.right !== null) {
// switch d with the greates element in the left subtree.
// o should have at most one child.
var o = d.left
// find
while (o.right !== null) {
o = o.right
}
// switch
d.val = o.val
d = o
}
// d has at most one child
// let n be the node that replaces d
var isFakeChild
var child = d.left || d.right
if (child === null) {
isFakeChild = true
child = new N({id: 0})
child.blacken()
d.right = child
} else {
isFakeChild = false
}
if (d.parent === null) {
if (!isFakeChild) {
this.root = child
child.blacken()
child._parent = null
} else {
this.root = null
}
return
} else if (d.parent.left === d) {
d.parent.left = child
} else if (d.parent.right === d) {
d.parent.right = child
} else {
throw new Error('Impossible!')
}
if (d.isBlack()) {
if (child.isRed()) {
child.blacken()
} else {
this._fixDelete(child)
}
}
this.root.blacken()
if (isFakeChild) {
if (child.parent.left === child) {
child.parent.left = null
} else if (child.parent.right === child) {
child.parent.right = null
} else {
throw new Error('Impossible #3')
}
}
}
_fixDelete (n) {
function isBlack (node) {
return node !== null ? node.isBlack() : true
}
function isRed (node) {
return node !== null ? node.isRed() : false
}
if (n.parent === null) {
// this can only be called after the first iteration of fixDelete.
return
}
// d was already replaced by the child
// d is not the root
// d and child are black
var sibling = n.sibling
if (isRed(sibling)) {
// make sibling the grandfather
n.parent.redden()
sibling.blacken()
if (n === n.parent.left) {
n.parent.rotateLeft(this)
} else if (n === n.parent.right) {
n.parent.rotateRight(this)
} else {
throw new Error('Impossible #2')
}
sibling = n.sibling
}
// parent, sibling, and children of n are black
if (n.parent.isBlack() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
this._fixDelete(n.parent)
} else if (n.parent.isRed() &&
sibling.isBlack() &&
isBlack(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
n.parent.blacken()
} else {
if (n === n.parent.left &&
sibling.isBlack() &&
isRed(sibling.left) &&
isBlack(sibling.right)
) {
sibling.redden()
sibling.left.blacken()
sibling.rotateRight(this)
sibling = n.sibling
} else if (n === n.parent.right &&
sibling.isBlack() &&
isRed(sibling.right) &&
isBlack(sibling.left)
) {
sibling.redden()
sibling.right.blacken()
sibling.rotateLeft(this)
sibling = n.sibling
}
sibling.color = n.parent.color
n.parent.blacken()
if (n === n.parent.left) {
sibling.right.blacken()
n.parent.rotateLeft(this)
} else {
sibling.left.blacken()
n.parent.rotateRight(this)
}
}
}
* put (v) {
if (v == null || v.id == null || v.id.constructor !== Array) {
throw new Error('v is expected to have an id property which is an Array!')
}
var node = new N(v)
if (this.root !== null) {
var p = this.root // p abbrev. parent
while (true) {
if (Y.utils.smaller(node.val.id, p.val.id)) {
if (p.left === null) {
p.left = node
break
} else {
p = p.left
}
} else if (Y.utils.smaller(p.val.id, node.val.id)) {
if (p.right === null) {
p.right = node
break
} else {
p = p.right
}
} else {
p.val = node.val
return p
}
}
this._fixInsert(node)
} else {
this.root = node
}
this.length++
this.root.blacken()
return node
}
_fixInsert (n) {
if (n.parent === null) {
n.blacken()
return
} else if (n.parent.isBlack()) {
return
}
var uncle = n.getUncle()
if (uncle !== null && uncle.isRed()) {
// Note: parent: red, uncle: red
n.parent.blacken()
uncle.blacken()
n.grandparent.redden()
this._fixInsert(n.grandparent)
} else {
// Note: parent: red, uncle: black or null
// Now we transform the tree in such a way that
// either of these holds:
// 1) grandparent.left.isRed
// and grandparent.left.left.isRed
// 2) grandparent.right.isRed
// and grandparent.right.right.isRed
if (n === n.parent.right && n.parent === n.grandparent.left) {
n.parent.rotateLeft(this)
// Since we rotated and want to use the previous
// cases, we need to set n in such a way that
// n.parent.isRed again
n = n.left
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
n.parent.rotateRight(this)
// see above
n = n.right
}
// Case 1) or 2) hold from here on.
// Now traverse grandparent, make parent a black node
// on the highest level which holds two red nodes.
n.parent.blacken()
n.grandparent.redden()
if (n === n.parent.left) {
// Case 1
n.grandparent.rotateRight(this)
} else {
// Case 2
n.grandparent.rotateLeft(this)
}
}
}
}
Y.utils.RBTree = RBTree

View File

@@ -1,212 +0,0 @@
/* global Y */
/* eslint-env browser,jasmine,console */
var numberOfRBTreeTests = 1000
function itRedNodesDoNotHaveBlackChildren () {
it('Red nodes do not have black children', function () {
function traverse (n) {
if (n == null) {
return
}
if (n.isRed()) {
if (n.left != null) {
expect(n.left.isRed()).not.toBeTruthy()
}
if (n.right != null) {
expect(n.right.isRed()).not.toBeTruthy()
}
}
traverse(n.left)
traverse(n.right)
}
traverse(this.tree.root)
})
}
function itBlackHeightOfSubTreesAreEqual () {
it('Black-height of sub-trees are equal', function () {
function traverse (n) {
if (n == null) {
return 0
}
var sub1 = traverse(n.left)
var sub2 = traverse(n.right)
expect(sub1).toEqual(sub2)
if (n.isRed()) {
return sub1
} else {
return sub1 + 1
}
}
traverse(this.tree.root)
})
}
function itRootNodeIsBlack () {
it('root node is black', function () {
expect(this.tree.root == null || this.tree.root.isBlack()).toBeTruthy()
})
}
describe('RedBlack Tree', function () {
var tree, memory
describe('debug #2', function () {
beforeAll(function (done) {
this.memory = new Y.Memory(null, {
name: 'Memory',
gcTimeout: -1
})
this.tree = this.memory.os
tree = this.tree
memory = this.memory
memory.requestTransaction(function * () {
yield* tree.put({id: [8433]})
yield* tree.put({id: [12844]})
yield* tree.put({id: [1795]})
yield* tree.put({id: [30302]})
yield* tree.put({id: [64287]})
yield* tree.delete([8433])
yield* tree.put({id: [28996]})
yield* tree.delete([64287])
yield* tree.put({id: [22721]})
done()
})
})
itRootNodeIsBlack()
itBlackHeightOfSubTreesAreEqual([])
})
describe(`After adding&deleting (0.8/0.2) ${numberOfRBTreeTests} times`, function () {
var elements = []
beforeAll(function (done) {
this.memory = new Y.Memory(null, {
name: 'Memory',
gcTimeout: -1
})
this.tree = this.memory.os
tree = this.tree
memory = this.memory
memory.requestTransaction(function * () {
for (var i = 0; i < numberOfRBTreeTests; i++) {
var r = Math.random()
if (r < 0.8) {
var obj = [Math.floor(Math.random() * numberOfRBTreeTests * 10000)]
if (!tree.findNode(obj)) {
elements.push(obj)
yield* tree.put({id: obj})
}
} else if (elements.length > 0) {
var elemid = Math.floor(Math.random() * elements.length)
var elem = elements[elemid]
elements = elements.filter(function (e) {
return !Y.utils.compareIds(e, elem)
})
yield* tree.delete(elem)
}
}
done()
})
})
itRootNodeIsBlack()
it('can find every object', function (done) {
memory.requestTransaction(function * () {
for (var id of elements) {
expect((yield* tree.find(id)).id).toEqual(id)
}
done()
})
})
it('can find every object with lower bound search', function (done) {
this.memory.requestTransaction(function * () {
for (var id of elements) {
expect((yield* tree.findWithLowerBound(id)).id).toEqual(id)
}
done()
})
})
itRedNodesDoNotHaveBlackChildren()
itBlackHeightOfSubTreesAreEqual()
it('iterating over a tree with lower bound yields the right amount of results', function (done) {
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree without bounds yield the right amount of results', function (done) {
var lowerBound = null
var expectedResults = elements.filter(function (e, pos) {
return elements.indexOf(e) === pos
}).length
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, lowerBound, null, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree with upper bound yields the right amount of results', function (done) {
var upperBound = elements[Math.floor(Math.random() * elements.length)]
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, null, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) {
var b1 = elements[Math.floor(Math.random() * elements.length)]
var b2 = elements[Math.floor(Math.random() * elements.length)]
var upperBound, lowerBound
if (Y.utils.smaller(b1, b2)) {
lowerBound = b1
upperBound = b2
} else {
lowerBound = b2
upperBound = b1
}
var expectedResults = elements.filter(function (e, pos) {
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
}).length
var actualResults = 0
this.memory.requestTransaction(function * () {
yield* tree.iterate(this, lowerBound, upperBound, function * (val) {
expect(val).toBeDefined()
actualResults++
})
expect(expectedResults).toEqual(actualResults)
done()
})
})
})
})

View File

@@ -1,4 +1,3 @@
/* global Y */
/* eslint-env browser, jasmine */
/*
@@ -6,6 +5,17 @@
*/
// 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)
if (typeof window === 'undefined') {
require('../../y-leveldb/src/LevelDB.js')(Y)
}
module.exports = Y
var g
if (typeof global !== 'undefined') {
g = global
@@ -16,9 +26,31 @@ if (typeof global !== 'undefined') {
}
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 = 20000
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
g.describeManyTimes = function describeManyTimes (times, name, f) {
for (var i = 0; i < times; i++) {
@@ -31,21 +63,22 @@ g.describeManyTimes = function describeManyTimes (times, name, f) {
*/
function wait (t) {
if (t == null) {
t = 80
t = 0
}
return new Promise(function (resolve) {
setTimeout(function () {
resolve()
}, t * 2)
}, t)
})
}
g.wait = wait
g.databases = ['Memory']
g.databases = ['memory']
if (typeof window !== 'undefined') {
g.databases.push('IndexedDB')
g.databases.push('indexeddb')
} else {
g.databases.push('leveldb')
}
/*
returns a random element of o.
works on Object, and Array
@@ -54,11 +87,7 @@ function getRandom (o) {
if (o instanceof Array) {
return o[Math.floor(Math.random() * o.length)]
} else if (o.constructor === Object) {
var ks = []
for (var key in o) {
ks.push(key)
}
return o[getRandom(ks)]
return o[getRandom(Object.keys(o))]
}
}
g.getRandom = getRandom
@@ -71,68 +100,118 @@ function getRandomNumber (n) {
}
g.getRandomNumber = getRandomNumber
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions) {
function randomTransaction (root) {
var f = getRandom(transactions)
f(root)
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.5) {
// 50% chance to flush
users[0].connector.flushOne() // flushes for some user.. (not necessarily 0)
} else if (r >= 0.05) {
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.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
randomTransaction(getRandom(objects))
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))
}
yield wait()
}
}
function fixAwaitingInType (type) {
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 users[0].connector.flushAll()
yield wait()
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 wait(100)
yield users[0].connector.flushAll()
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 users[0].connector.flushAll()
yield g.garbageCollectAllUsers(users)
yield wait(100)
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 wait(100)
yield users[0].connector.flushAll()
yield wait(100)
yield Y.utils.globalRoom.flushAll()
yield Promise.all(objects.map(fixAwaitingInType))
yield g.garbageCollectAllUsers(users)
})
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
// gc two times because of the two gc phases (really collect everything)
yield wait(100)
yield Y.utils.globalRoom.flushAll()
for (var i in users) {
yield users[i].db.garbageCollect()
yield users[i].db.garbageCollect()
yield users[i].db.emptyGarbageCollector()
}
yield wait(100)
})
g.compareAllUsers = async(function * compareAllUsers (users) {
@@ -158,13 +237,24 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
allDels2.push(d)
})
}
yield users[0].connector.flushAll()
yield wait()
yield Y.utils.globalRoom.flushAll()
yield g.garbageCollectAllUsers(users)
yield Y.utils.globalRoom.flushAll()
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) {
@@ -179,7 +269,7 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
for (var j in ds) {
var d = ds[j]
for (var i = 0; i < d.len; i++) {
var o = yield* this.getOperation([d.id[0], d.id[1] + i])
var o = yield* this.getInsertion([d.id[0], d.id[1] + i])
// gc'd or deleted
if (d.gc) {
expect(o).toBeFalsy()
@@ -190,39 +280,41 @@ g.compareAllUsers = async(function * compareAllUsers (users) {
}
})
// compare allDels tree
yield wait()
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)
})
})
yield wait()
} else {
// TODO: make requestTransaction return a promise..
u.db.requestTransaction(function * () {
yield* t2.call(this)
expect(s1).toEqual(s2)
expect(allDels1).toEqual(allDels2) // inner structure
expect(ds1).toEqual(ds2) // exported structure
var count = 0
var db2 = []
yield* this.os.iterate(this, null, null, function * (o) {
o = Y.utils.copyObject(o)
delete o.origin
expect(db1[count++]).toEqual(o)
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 wait()
}
yield u.db.whenTransactionsFinished()
}
})
g.createUsers = async(function * createUsers (self, numberOfUsers, database) {
g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
if (Y.utils.globalRoom.users[0] != null) {
yield Y.utils.globalRoom.users[0].flushAll()
yield Y.utils.globalRoom.flushAll()
}
// destroy old users
for (var u in Y.utils.globalRoom.users) {
@@ -237,15 +329,20 @@ g.createUsers = async(function * createUsers (self, numberOfUsers, database) {
name: database,
namespace: 'User ' + i,
cleanStart: true,
gcTimeout: -1
gcTimeout: -1,
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
})

View File

@@ -1,4 +1,4 @@
/* global Y */
/* @flow */
'use strict'
/*
@@ -19,318 +19,392 @@
* requiredOps
- Operations that are required to execute this operation.
*/
module.exports = function (Y/* :any */) {
var Struct = {
/* This is the only operation that is actually not a structure, because
it is not stored in the OS. This is why it _does not_ have an id
var Struct = {
/* This is the only operation that is actually not a structure, because
it is not stored in the OS. This is why it _does not_ have an id
op = {
target: Id
}
*/
Delete: {
encode: function (op) {
return op
},
requiredOps: function (op) {
return [] // [op.target]
},
execute: function * (op) {
return yield* this.deleteOperation(op.target)
}
},
Insert: {
/* {
content: any,
id: Id,
left: Id,
origin: Id,
right: Id,
parent: Id,
parentSub: string (optional), // child of Map type
}
*/
encode: function (op) {
// TODO: you could not send the "left" property, then you also have to
// "op.left = null" in $execute or $decode
var e = {
id: op.id,
left: op.left,
right: op.right,
origin: op.origin,
parent: op.parent,
struct: op.struct
}
if (op.parentSub != null) {
e.parentSub = op.parentSub
}
if (op.opContent != null) {
e.opContent = op.opContent
} else {
e.content = op.content
}
return e
},
requiredOps: function (op) {
var ids = []
if (op.left != null) {
ids.push(op.left)
}
if (op.right != null) {
ids.push(op.right)
}
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
ids.push(op.origin)
}
// if (op.right == null && op.left == null) {
ids.push(op.parent)
if (op.opContent != null) {
ids.push(op.opContent)
}
return ids
},
getDistanceToOrigin: function * (op) {
if (op.left == null) {
return 0
} else {
var d = 0
var o = yield* this.getOperation(op.left)
while (!Y.utils.compareIds(op.origin, (o ? o.id : null))) {
d++
if (o.left == null) {
break
} else {
o = yield* this.getOperation(o.left)
}
}
return d
}
},
/*
# $this has to find a unique position between origin and the next known character
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
# o2,o3 and o4 origin is 1 (the position of o2)
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
# therefore $this would be always to the right of o3
# case 2: $origin < $o.origin
# if current $this insert_position > $o origin: $this ins
# else $insert_position will not change
# (maybe we encounter case 1 later, then this will be to the right of $o)
# case 3: $origin > $o.origin
# $this insert_position is to the left of $o (forever!)
*/
execute: function *(op) {
var i // loop counter
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
var o
var parent
var start
// find o. o is the first conflicting operation
if (op.left != null) {
o = yield* this.getOperation(op.left)
o = (o.right == null) ? null : yield* this.getOperation(o.right)
} else { // left == null
parent = yield* this.getOperation(op.parent)
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
start = startId == null ? null : yield* this.getOperation(startId)
o = start
}
// handle conflicts
while (true) {
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
if (oOriginDistance === i) {
// case 1
if (o.id[0] < op.id[0]) {
op.left = o.id
distanceToOrigin = i + 1
}
} else if (oOriginDistance < i) {
// case 2
if (i - distanceToOrigin <= oOriginDistance) {
op.left = o.id
distanceToOrigin = i + 1
}
} else {
break
}
i++
o = o.right ? yield* this.getOperation(o.right) : null
} else {
break
}
}
// reconnect..
var left = null
var right = null
parent = parent || (yield* this.getOperation(op.parent))
// reconnect left and set right of op
if (op.left != null) {
left = yield* this.getOperation(op.left)
op.right = left.right
left.right = op.id
yield* this.setOperation(left)
} else {
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
}
// reconnect right
if (op.right != null) {
right = yield* this.getOperation(op.right)
right.left = op.id
// if right exists, and it is supposed to be gc'd. Remove it from the gc
if (right.gc != null) {
this.store.removeFromGarbageCollector(right)
}
yield* this.setOperation(right)
}
// update parents .map/start/end properties
if (op.parentSub != null) {
if (left == null) {
parent.map[op.parentSub] = op.id
yield* this.setOperation(parent)
}
// is a child of a map struct.
// Then also make sure that only the most left element is not deleted
if (op.right != null) {
yield* this.deleteOperation(op.right, true)
}
if (op.left != null) {
yield* this.deleteOperation(op.id, true)
}
} else {
if (right == null || left == null) {
if (right == null) {
parent.end = op.id
}
if (left == null) {
parent.start = op.id
}
yield* this.setOperation(parent)
}
}
}
},
List: {
/*
{
start: null,
end: null,
struct: "List",
type: "",
id: this.os.getNextOpId()
op = {
target: Id
}
*/
encode: function (op) {
return {
struct: 'List',
id: op.id,
type: op.type
Delete: {
encode: function (op) {
return op
},
requiredOps: function (op) {
return [] // [op.target]
},
execute: function * (op) {
return yield* this.deleteOperation(op.target, op.length || 1)
}
},
requiredOps: function () {
/*
var ids = []
if (op.start != null) {
ids.push(op.start)
}
if (op.end != null){
ids.push(op.end)
}
return ids
Insert: {
/* {
content: [any],
opContent: Id,
id: Id,
left: Id,
origin: Id,
right: Id,
parent: Id,
parentSub: string (optional), // child of Map type
}
*/
return []
},
execute: function * (op) {
op.start = null
op.end = null
},
ref: function * (op, pos) {
if (op.start == null) {
return null
}
var res = null
var o = yield* this.getOperation(op.start)
while (true) {
if (!o.deleted) {
res = o
pos--
encode: function (op/* :Insertion */) /* :Insertion */ {
// TODO: you could not send the "left" property, then you also have to
// "op.left = null" in $execute or $decode
var e/* :any */ = {
id: op.id,
left: op.left,
right: op.right,
origin: op.origin,
parent: op.parent,
struct: op.struct
}
if (pos >= 0 && o.right != null) {
o = (yield* this.getOperation(o.right))
if (op.parentSub != null) {
e.parentSub = op.parentSub
}
if (op.hasOwnProperty('opContent')) {
e.opContent = op.opContent
} else {
break
e.content = op.content.slice()
}
return e
},
requiredOps: function (op) {
var ids = []
if (op.left != null) {
ids.push(op.left)
}
if (op.right != null) {
ids.push(op.right)
}
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
ids.push(op.origin)
}
// if (op.right == null && op.left == null) {
ids.push(op.parent)
if (op.opContent != null) {
ids.push(op.opContent)
}
return ids
},
getDistanceToOrigin: function * (op) {
if (op.left == null) {
return 0
} else {
var d = 0
var o = yield* this.getInsertion(op.left)
while (!Y.utils.matchesId(o, op.origin)) {
d++
if (o.left == null) {
break
} else {
o = yield* this.getInsertion(o.left)
}
}
return d
}
},
/*
# $this has to find a unique position between origin and the next known character
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
# o2,o3 and o4 origin is 1 (the position of o2)
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
# therefore $this would be always to the right of o3
# case 2: $origin < $o.origin
# if current $this insert_position > $o origin: $this ins
# else $insert_position will not change
# (maybe we encounter case 1 later, then this will be to the right of $o)
# case 3: $origin > $o.origin
# $this insert_position is to the left of $o (forever!)
*/
execute: function * (op) {
var i // loop counter
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
// We try to merge them later, if possible
var tryToRemergeLater = []
if (op.origin != null) { // TODO: !== instead of !=
// we save in origin that op originates in it
// we need that later when we eventually garbage collect origin (see transaction)
var origin = yield* this.getInsertionCleanEnd(op.origin)
if (origin.originOf == null) {
origin.originOf = []
}
origin.originOf.push(op.id)
yield* this.setOperation(origin)
if (origin.right != null) {
tryToRemergeLater.push(origin.right)
}
}
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
// now we begin to insert op in the list of insertions..
var o
var parent
var start
// find o. o is the first conflicting operation
if (op.left != null) {
o = yield* this.getInsertionCleanEnd(op.left)
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
// only if not added previously
tryToRemergeLater.push(o.right)
}
o = (o.right == null) ? null : yield* this.getOperation(o.right)
} else { // left == null
parent = yield* this.getOperation(op.parent)
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
start = startId == null ? null : yield* this.getOperation(startId)
o = start
}
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
if (op.right != null) {
tryToRemergeLater.push(op.right)
yield* this.getInsertionCleanStart(op.right)
}
// handle conflicts
while (true) {
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o)
if (oOriginDistance === i) {
// case 1
if (o.id[0] < op.id[0]) {
op.left = Y.utils.getLastId(o)
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
}
} else if (oOriginDistance < i) {
// case 2
if (i - distanceToOrigin <= oOriginDistance) {
op.left = Y.utils.getLastId(o)
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
}
} else {
break
}
i++
if (o.right != null) {
o = yield* this.getInsertion(o.right)
} else {
o = null
}
} else {
break
}
}
// reconnect..
var left = null
var right = null
if (parent == null) {
parent = yield* this.getOperation(op.parent)
}
// reconnect left and set right of op
if (op.left != null) {
left = yield* this.getInsertion(op.left)
// link left
op.right = left.right
left.right = op.id
yield* this.setOperation(left)
} else {
// set op.right from parent, if necessary
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
}
// reconnect right
if (op.right != null) {
// TODO: wanna connect right too?
right = yield* this.getOperation(op.right)
right.left = Y.utils.getLastId(op)
// if right exists, and it is supposed to be gc'd. Remove it from the gc
if (right.gc != null) {
if (right.content != null && right.content.length > 1) {
right = yield* this.getInsertionCleanEnd(right.id)
}
this.store.removeFromGarbageCollector(right)
}
yield* this.setOperation(right)
}
// update parents .map/start/end properties
if (op.parentSub != null) {
if (left == null) {
parent.map[op.parentSub] = op.id
yield* this.setOperation(parent)
}
// is a child of a map struct.
// Then also make sure that only the most left element is not deleted
// We do not call the type in this case (this is what the third parameter is for)
if (op.right != null) {
yield* this.deleteOperation(op.right, 1, true)
}
if (op.left != null) {
yield* this.deleteOperation(op.id, 1, true)
}
} else {
if (right == null || left == null) {
if (right == null) {
parent.end = Y.utils.getLastId(op)
}
if (left == null) {
parent.start = op.id
}
yield* this.setOperation(parent)
}
}
// try to merge original op.left and op.origin
for (let i = 0; i < tryToRemergeLater.length; i++) {
var m = yield* this.getOperation(tryToRemergeLater[i])
yield* this.tryCombineWithLeft(m)
}
}
return res
},
map: function * (o, f) {
o = o.start
var res = []
while (o != null) { // TODO: change to != (at least some convention)
var operation = yield* this.getOperation(o)
if (!operation.deleted) {
res.push(f(operation))
}
o = operation.right
}
return res
}
},
Map: {
/*
List: {
/*
{
map: {},
struct: "Map",
start: null,
end: null,
struct: "List",
type: "",
id: this.os.getNextOpId()
id: this.os.getNextOpId(1)
}
*/
encode: function (op) {
return {
struct: 'Map',
type: op.type,
id: op.id,
map: {} // overwrite map!!
*/
create: function (id) {
return {
start: null,
end: null,
struct: 'List',
id: id
}
},
encode: function (op) {
var e = {
struct: 'List',
id: op.id,
type: op.type
}
if (op.requires != null) {
e.requires = op.requires
}
if (op.info != null) {
e.info = op.info
}
return e
},
requiredOps: function () {
/*
var ids = []
if (op.start != null) {
ids.push(op.start)
}
if (op.end != null){
ids.push(op.end)
}
return ids
*/
return []
},
execute: function * (op) {
op.start = null
op.end = null
},
ref: function * (op, pos) {
if (op.start == null) {
return null
}
var res = null
var o = yield* this.getOperation(op.start)
while (true) {
if (!o.deleted) {
res = o
pos--
}
if (pos >= 0 && o.right != null) {
o = yield* this.getOperation(o.right)
} else {
break
}
}
return res
},
map: function * (o, f) {
o = o.start
var res = []
while (o != null) { // TODO: change to != (at least some convention)
var operation = yield* this.getOperation(o)
if (!operation.deleted) {
res.push(f(operation))
}
o = operation.right
}
return res
}
},
requiredOps: function () {
return []
},
execute: function * () {},
/*
Get a property by name
*/
get: function * (op, name) {
var oid = op.map[name]
if (oid != null) {
var res = yield* this.getOperation(oid)
return (res == null || res.deleted) ? void 0 : (res.opContent == null
? res.content : yield* this.getType(res.opContent))
}
},
/*
Delete a property by name
*/
delete: function * (op, name) {
var v = op.map[name] || null
if (v != null) {
yield* Struct.Delete.create.call(this, {
target: v
})
Map: {
/*
{
map: {},
struct: "Map",
type: "",
id: this.os.getNextOpId(1)
}
*/
create: function (id) {
return {
id: id,
map: {},
struct: 'Map'
}
},
encode: function (op) {
var e = {
struct: 'Map',
type: op.type,
id: op.id,
map: {} // overwrite map!!
}
if (op.requires != null) {
e.requires = op.requires
}
if (op.info != null) {
e.info = op.info
}
return e
},
requiredOps: function () {
return []
},
execute: function * () {},
/*
Get a property by name
*/
get: function * (op, name) {
var oid = op.map[name]
if (oid != null) {
var res = yield* this.getOperation(oid)
if (res == null || res.deleted) {
return void 0
} else if (res.opContent == null) {
return res.content[0]
} else {
return yield* this.getType(res.opContent)
}
}
}
}
}
Y.Struct = Struct
}
Y.Struct = Struct

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +0,0 @@
/* global Y */
'use strict'
;(function () {
class YArray {
constructor (os, _model, idArray, valArray) {
this.os = os
this._model = _model
// Array of all the operation id's
this.idArray = idArray
// Array of all the values
this.valArray = valArray
this.eventHandler = new Y.utils.EventHandler(ops => {
var userEvents = []
for (var i in ops) {
var op = ops[i]
if (op.struct === 'Insert') {
let pos
// we check op.left only!,
// because op.right might not be defined when this is called
if (op.left === null) {
pos = 0
} else {
var sid = JSON.stringify(op.left)
pos = this.idArray.indexOf(sid) + 1
if (pos <= 0) {
throw new Error('Unexpected operation!')
}
}
this.idArray.splice(pos, 0, JSON.stringify(op.id))
this.valArray.splice(pos, 0, op.content)
userEvents.push({
type: 'insert',
object: this,
index: pos,
length: 1
})
} else if (op.struct === 'Delete') {
let pos = this.idArray.indexOf(JSON.stringify(op.target))
if (pos >= 0) {
this.idArray.splice(pos, 1)
this.valArray.splice(pos, 1)
userEvents.push({
type: 'delete',
object: this,
index: pos,
length: 1
})
}
} else {
throw new Error('Unexpected struct!')
}
}
this.eventHandler.callEventListeners(userEvents)
})
}
get length () {
return this.idArray.length
}
get (pos) {
if (pos == null || typeof pos !== 'number') {
throw new Error('pos must be a number!')
}
return this.valArray[pos]
}
toArray () {
return this.valArray.slice()
}
insert (pos, contents) {
if (typeof pos !== 'number') {
throw new Error('pos must be a number!')
}
if (!(contents instanceof Array)) {
throw new Error('contents must be an Array of objects!')
}
if (contents.length === 0) {
return
}
if (pos > this.idArray.length || pos < 0) {
throw new Error('This position exceeds the range of the array!')
}
var mostLeft = pos === 0 ? null : JSON.parse(this.idArray[pos - 1])
var ops = []
var prevId = mostLeft
for (var i = 0; i < contents.length; i++) {
var op = {
left: prevId,
origin: prevId,
// right: mostRight,
// NOTE: I intentionally do not define right here, because it could be deleted
// at the time of creating this operation, and is therefore not defined in idArray
parent: this._model,
content: contents[i],
struct: 'Insert',
id: this.os.getNextOpId()
}
ops.push(op)
prevId = op.id
}
var eventHandler = this.eventHandler
eventHandler.awaitAndPrematurelyCall(ops)
this.os.requestTransaction(function *() {
// now we can set the right reference.
var mostRight
if (mostLeft != null) {
mostRight = (yield* this.getOperation(mostLeft)).right
} else {
mostRight = (yield* this.getOperation(ops[0].parent)).start
}
for (var j in ops) {
ops[j].right = mostRight
}
yield* this.applyCreatedOperations(ops)
eventHandler.awaitedInserts(ops.length)
})
}
delete (pos, length) {
if (length == null) { length = 1 }
if (typeof length !== 'number') {
throw new Error('pos must be a number!')
}
if (typeof pos !== 'number') {
throw new Error('pos must be a number!')
}
if (pos + length > this.idArray.length || pos < 0 || length < 0) {
throw new Error('The deletion range exceeds the range of the array!')
}
if (length === 0) {
return
}
var eventHandler = this.eventHandler
var newLeft = pos > 0 ? JSON.parse(this.idArray[pos - 1]) : null
var dels = []
for (var i = 0; i < length; i++) {
dels.push({
target: JSON.parse(this.idArray[pos + i]),
struct: 'Delete'
})
}
eventHandler.awaitAndPrematurelyCall(dels)
this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations(dels)
eventHandler.awaitedDeletes(dels.length, newLeft)
})
}
observe (f) {
this.eventHandler.addEventListener(f)
}
* _changed (transaction, op) {
if (!op.deleted) {
if (op.struct === 'Insert') {
var l = op.left
var left
while (l != null) {
left = yield* transaction.getOperation(l)
if (!left.deleted) {
break
}
l = left.left
}
op.left = l
}
this.eventHandler.receivedOp(op)
}
}
}
Y.Array = new Y.utils.CustomType({
class: YArray,
createType: function * YArrayCreator () {
var modelid = this.store.getNextOpId()
var model = {
struct: 'List',
type: 'Array',
start: null,
end: null,
id: modelid
}
yield* this.applyCreatedOperations([model])
return modelid
},
initType: function * YArrayInitializer (os, model) {
var valArray = []
var idArray = yield* Y.Struct.List.map.call(this, model, function (c) {
valArray.push(c.content)
return JSON.stringify(c.id)
})
return new YArray(os, model.id, idArray, valArray)
}
})
})()

View File

@@ -1,310 +0,0 @@
/* global createUsers, databases, wait, Y, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, garbageCollectAllUsers, describeManyTimes */
/* eslint-env browser,jasmine */
var numberOfYArrayTests = 50
var repeatArrayTests = 2
for (let database of databases) {
describe(`Array Type (DB: ${database})`, function () {
var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll
beforeEach(async(function * (done) {
yield createUsers(this, 3, database)
y1 = (yconfig1 = this.users[0]).root
y2 = (yconfig2 = this.users[1]).root
y3 = (yconfig3 = this.users[2]).root
flushAll = this.users[0].connector.flushAll
yield wait(10)
done()
}))
afterEach(async(function * (done) {
yield compareAllUsers(this.users)
done()
}))
describe('Basic tests', function () {
it('insert three elements, try re-get property', async(function * (done) {
var array = yield y1.set('Array', Y.Array)
array.insert(0, [1, 2, 3])
array = yield y1.get('Array') // re-get property
expect(array.toArray()).toEqual([1, 2, 3])
done()
}))
it('Basic insert in array (handle three conflicts)', async(function * (done) {
yield y1.set('Array', Y.Array)
yield flushAll()
var l1 = yield y1.get('Array')
l1.insert(0, [0])
var l2 = yield y2.get('Array')
l2.insert(0, [1])
var l3 = yield y3.get('Array')
l3.insert(0, [2])
yield flushAll()
expect(l1.toArray()).toEqual(l2.toArray())
expect(l2.toArray()).toEqual(l3.toArray())
done()
}))
it('Basic insert&delete in array (handle three conflicts)', async(function * (done) {
var l1, l2, l3
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y', 'z'])
yield flushAll()
l1.insert(1, [0])
l2 = yield y2.get('Array')
l2.delete(0)
l2.delete(1)
l3 = yield y3.get('Array')
l3.insert(1, [2])
yield flushAll()
expect(l1.toArray()).toEqual(l2.toArray())
expect(l2.toArray()).toEqual(l3.toArray())
expect(l2.toArray()).toEqual([0, 2, 'y'])
done()
}))
it('Handles getOperations ascending ids bug in late sync', async(function * (done) {
var l1, l2
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y'])
yield flushAll()
yconfig3.disconnect()
yconfig2.disconnect()
yield wait()
l2 = yield y2.get('Array')
l2.insert(1, [2])
l2.insert(1, [3])
yield yconfig2.reconnect()
yield yconfig3.reconnect()
expect(l1.toArray()).toEqual(l2.toArray())
done()
}))
it('Handles deletions in late sync', async(function * (done) {
var l1, l2
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y'])
yield flushAll()
yield yconfig2.disconnect()
yield wait()
l2 = yield y2.get('Array')
l2.delete(1, 1)
l1.delete(0, 2)
yield yconfig2.reconnect()
expect(l1.toArray()).toEqual(l2.toArray())
done()
}))
it('Handles deletions in late sync (2)', async(function * (done) {
var l1, l2
l1 = yield y1.set('Array', Y.Array)
yield flushAll()
l2 = yield y2.get('Array')
l1.insert(0, ['x', 'y'])
l1.delete(0, 2)
yield flushAll()
expect(l1.toArray()).toEqual(l2.toArray())
done()
}))
it('Basic insert. Then delete the whole array', async(function * (done) {
var l1, l2, l3
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y', 'z'])
yield flushAll()
l1.delete(0, 3)
l2 = yield y2.get('Array')
l3 = yield y3.get('Array')
yield flushAll()
expect(l1.toArray()).toEqual(l2.toArray())
expect(l2.toArray()).toEqual(l3.toArray())
expect(l2.toArray()).toEqual([])
done()
}))
it('Basic insert. Then delete the whole array (merge listeners on late sync)', async(function * (done) {
var l1, l2, l3
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y', 'z'])
yield flushAll()
yconfig2.disconnect()
l1.delete(0, 3)
l2 = yield y2.get('Array')
yield wait()
yield yconfig2.reconnect()
yield wait()
l3 = yield y3.get('Array')
yield flushAll()
expect(l1.toArray()).toEqual(l2.toArray())
expect(l2.toArray()).toEqual(l3.toArray())
expect(l2.toArray()).toEqual([])
done()
}))
// TODO?
/* it('Basic insert. Then delete the whole array (merge deleter on late sync)', async(function * (done) {
var l1, l2, l3
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y', 'z'])
yield flushAll()
yconfig1.disconnect()
l1.delete(0, 3)
l2 = yield y2.get('Array')
yield yconfig1.reconnect()
l3 = yield y3.get('Array')
yield flushAll()
expect(l1.toArray()).toEqual(l2.toArray())
expect(l2.toArray()).toEqual(l3.toArray())
expect(l2.toArray()).toEqual([])
done()
})) */
it('throw insert & delete events', async(function * (done) {
var array = yield this.users[0].root.set('array', Y.Array)
var event
array.observe(function (e) {
event = e
})
array.insert(0, [0])
expect(event).toEqual([{
type: 'insert',
object: array,
index: 0,
length: 1
}])
array.delete(0)
expect(event).toEqual([{
type: 'delete',
object: array,
index: 0,
length: 1
}])
yield wait(50)
done()
}))
it('garbage collects', async(function * (done) {
var l1, l2, l3
l1 = yield y1.set('Array', Y.Array)
l1.insert(0, ['x', 'y', 'z'])
yield flushAll()
yconfig1.disconnect()
l1.delete(0, 3)
l2 = yield y2.get('Array')
yield wait()
yield yconfig1.reconnect()
yield wait()
l3 = yield y3.get('Array')
yield flushAll()
yield garbageCollectAllUsers(this.users)
expect(l1.toArray()).toEqual(l2.toArray())
expect(l2.toArray()).toEqual(l3.toArray())
expect(l2.toArray()).toEqual([])
done()
}))
it('debug right not existend in Insert.execute', async(function * (done) {
yconfig1.db.requestTransaction(function * () {
var ops = [{'struct':'Map','type':'Map','id':['130',0],'map':{}},{'id':['130',1],'left':null,'right':null,'origin':null,'parent':['_',0],'struct':'Insert','parentSub':'Map','opContent':['130',0]},{'struct':'Map','type':'Map','id':['130',0],'map':{}},{'id':['130',1],'left':null,'right':null,'origin':null,'parent':['_',0],'struct':'Insert','parentSub':'Map','opContent':['130',0]},{'struct':'Map','type':'Map','id':['130',0],'map':{}},{'id':['130',1],'left':null,'right':null,'origin':null,'parent':['_',0],'struct':'Insert','parentSub':'Map','opContent':['130',0]},{'left':null,'right':null,'origin':null,'parent':['130',0],'parentSub':'somekey','struct':'Insert','content':512,'id':['133',0]},{'id':['130',2],'left':null,'right':null,'origin':null,'parent':['130',0],'struct':'Insert','parentSub':'somekey','content':1131},{'id':['130',3],'left':null,'right':['130',2],'origin':null,'parent':['130',0],'struct':'Insert','parentSub':'somekey','content':4196},{'id':['131',3],'left':null,'right':null,'origin':null,'parent':['130',0],'struct':'Insert','parentSub':'somekey','content':5022}]//eslint-disable-line
for (var o of ops) {
yield* this.store.tryExecute.call(this, o)
}
})
yield wait()
yield yconfig3.disconnect()
yield yconfig2.disconnect()
yield flushAll()
wait()
yield yconfig3.reconnect()
yield yconfig2.reconnect()
yield wait()
yield flushAll()
done()
}))
it('debug right not existend in Insert.execute (2)', async(function * (done) {
yconfig1.db.requestTransaction(function * () {
yield* this.store.tryExecute.call(this, {'struct': 'Map', 'type': 'Map', 'id': ['153', 0], 'map': {}})
yield* this.store.tryExecute.call(this, {'id': ['153', 1], 'left': null, 'right': null, 'origin': null, 'parent': ['_', 0], 'struct': 'Insert', 'parentSub': 'Map', 'opContent': ['153', 0]})
yield* this.store.tryExecute.call(this, {'struct': 'Map', 'type': 'Map', 'id': ['153', 0], 'map': {}})
yield* this.store.tryExecute.call(this, {'id': ['153', 1], 'left': null, 'right': null, 'origin': null, 'parent': ['_', 0], 'struct': 'Insert', 'parentSub': 'Map', 'opContent': ['153', 0]})
yield* this.store.tryExecute.call(this, {'struct': 'Map', 'type': 'Map', 'id': ['153', 0], 'map': {}})
yield* this.store.tryExecute.call(this, {'id': ['153', 1], 'left': null, 'right': null, 'origin': null, 'parent': ['_', 0], 'struct': 'Insert', 'parentSub': 'Map', 'opContent': ['153', 0]})
yield* this.store.tryExecute.call(this, {'left': null, 'right': null, 'origin': null, 'parent': ['153', 0], 'parentSub': 'somekey', 'struct': 'Insert', 'content': 3784, 'id': ['154', 0]})
yield* this.store.tryExecute.call(this, {'left': null, 'right': ['154', 0], 'origin': null, 'parent': ['153', 0], 'parentSub': 'somekey', 'struct': 'Insert', 'content': 8217, 'id': ['154', 1]})
yield* this.store.tryExecute.call(this, {'left': null, 'right': ['154', 1], 'origin': null, 'parent': ['153', 0], 'parentSub': 'somekey', 'struct': 'Insert', 'content': 5036, 'id': ['154', 2]})
yield* this.store.tryExecute.call(this, {'id': ['153', 2], 'left': null, 'right': null, 'origin': null, 'parent': ['153', 0], 'struct': 'Insert', 'parentSub': 'somekey', 'content': 417})
yield* this.store.tryExecute.call(this, {'id': ['155', 0], 'left': null, 'right': null, 'origin': null, 'parent': ['153', 0], 'struct': 'Insert', 'parentSub': 'somekey', 'content': 2202})
yield* this.garbageCollectOperation(['153', 2])
yield* this.garbageCollectOperation(['154', 0])
yield* this.garbageCollectOperation(['154', 1])
yield* this.garbageCollectOperation(['154', 2])
yield* this.garbageCollectOperation(['155', 0])
yield* this.garbageCollectOperation(['156', 0])
yield* this.garbageCollectOperation(['157', 0])
yield* this.garbageCollectOperation(['157', 1])
yield* this.store.tryExecute.call(this, {'id': ['153', 3], 'left': null, 'right': null, 'origin': null, 'parent': ['153', 0], 'struct': 'Insert', 'parentSub': 'somekey', 'content': 4372})
})
yield wait()
yield yconfig3.disconnect()
yield yconfig2.disconnect()
yield flushAll()
wait()
yield yconfig3.reconnect()
yield yconfig2.reconnect()
yield wait()
yield flushAll()
done()
}))
})
describeManyTimes(repeatArrayTests, `Random tests`, function () {
var randomArrayTransactions = [
function insert (array) {
array.insert(getRandomNumber(array.toArray().length), [getRandomNumber()])
},
function _delete (array) {
var length = array.toArray().length
if (length > 0) {
array.delete(getRandomNumber(length - 1))
}
}
]
function compareArrayValues (arrays) {
var firstArray
for (var l of arrays) {
var val = l.toArray()
if (firstArray == null) {
firstArray = val
} else {
expect(val).toEqual(firstArray)
}
}
}
beforeEach(async(function * (done) {
yield this.users[0].root.set('Array', Y.Array)
yield flushAll()
var promises = []
for (var u = 0; u < this.users.length; u++) {
promises.push(this.users[u].root.get('Array'))
}
this.arrays = yield Promise.all(promises)
done()
}))
it('arrays.length equals users.length', async(function * (done) {
expect(this.arrays.length).toEqual(this.users.length)
done()
}))
it(`succeed after ${numberOfYArrayTests} actions, no GC, all users disconnecting/reconnecting`, async(function * (done) {
for (var u of this.users) {
u.connector.debug = true
}
yield applyRandomTransactionsAllRejoinNoGC(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests)
yield flushAll()
yield compareArrayValues(this.arrays)
yield compareAllUsers(this.users)
done()
}))
it(`succeed after ${numberOfYArrayTests} actions, GC, user[0] is not disconnecting`, async(function * (done) {
for (var u of this.users) {
u.connector.debug = true
}
yield applyRandomTransactionsWithGC(this.users, this.arrays, randomArrayTransactions, numberOfYArrayTests)
yield flushAll()
yield compareArrayValues(this.arrays)
yield compareAllUsers(this.users)
done()
}))
})
})
}

View File

@@ -1,295 +0,0 @@
/* global Y */
'use strict'
;(function () {
class YMap {
constructor (os, model, contents, opContents) {
this._model = model.id
this.os = os
this.map = Y.utils.copyObject(model.map)
this.contents = contents
this.opContents = opContents
this.eventHandler = new Y.utils.EventHandler(ops => {
var userEvents = []
for (var i in ops) {
var op = ops[i]
var oldValue
// key is the name to use to access (op)content
var key = op.struct === 'Delete' ? op.key : op.parentSub
// compute oldValue
if (this.opContents[key] != null) {
let prevType = this.opContents[key]
oldValue = () => {// eslint-disable-line
return new Promise((resolve) => {
this.os.requestTransaction(function *() {// eslint-disable-line
resolve(yield* this.getType(prevType))
})
})
}
} else {
oldValue = this.contents[key]
}
// compute op event
if (op.struct === 'Insert') {
if (op.left === null) {
if (op.opContent != null) {
delete this.contents[key]
if (op.deleted) {
delete this.opContents[key]
} else {
this.opContents[key] = op.opContent
}
} else {
delete this.opContents[key]
if (op.deleted) {
delete this.contents[key]
} else {
this.contents[key] = op.content
}
}
this.map[key] = op.id
var insertEvent = {
name: key,
object: this
}
if (oldValue === undefined) {
insertEvent.type = 'add'
} else {
insertEvent.type = 'update'
insertEvent.oldValue = oldValue
}
userEvents.push(insertEvent)
}
} else if (op.struct === 'Delete') {
if (Y.utils.compareIds(this.map[key], op.target)) {
delete this.opContents[key]
delete this.contents[key]
var deleteEvent = {
name: key,
object: this,
oldValue: oldValue,
type: 'delete'
}
userEvents.push(deleteEvent)
}
} else {
throw new Error('Unexpected Operation!')
}
}
this.eventHandler.callEventListeners(userEvents)
})
}
get (key) {
// return property.
// if property does not exist, return null
// if property is a type, return a promise
if (key == null) {
throw new Error('You must specify key!')
}
if (this.opContents[key] == null) {
return this.contents[key]
} else {
return new Promise((resolve) => {
var oid = this.opContents[key]
this.os.requestTransaction(function *() {
resolve(yield* this.getType(oid))
})
})
}
}
/*
If there is a primitive (not a custom type), then return it.
Returns all primitive values, if propertyName is specified!
Note: modifying the return value could result in inconsistencies!
-- so make sure to copy it first!
*/
getPrimitive (key) {
if (key == null) {
return Y.utils.copyObject(this.contents)
} else {
return this.contents[key]
}
}
delete (key) {
var right = this.map[key]
if (right != null) {
var del = {
target: right,
struct: 'Delete'
}
var eventHandler = this.eventHandler
var modDel = Y.utils.copyObject(del)
modDel.key = key
eventHandler.awaitAndPrematurelyCall([modDel])
this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations([del])
eventHandler.awaitedDeletes(1)
})
}
}
set (key, value) {
// set property.
// if property is a type, return a promise
// if not, apply immediately on this type an call event
var right = this.map[key] || null
var insert = {
left: null,
right: right,
origin: null,
parent: this._model,
parentSub: key,
struct: 'Insert'
}
return new Promise((resolve) => {
if (value instanceof Y.utils.CustomType) {
// construct a new type
this.os.requestTransaction(function *() {
var typeid = yield* value.createType.call(this)
var type = yield* this.getType(typeid)
insert.opContent = typeid
insert.id = this.store.getNextOpId()
yield* this.applyCreatedOperations([insert])
resolve(type)
})
} else {
insert.content = value
insert.id = this.os.getNextOpId()
var eventHandler = this.eventHandler
eventHandler.awaitAndPrematurelyCall([insert])
this.os.requestTransaction(function *() {
yield* this.applyCreatedOperations([insert])
eventHandler.awaitedInserts(1)
})
resolve(value)
}
})
}
observe (f) {
this.eventHandler.addEventListener(f)
}
unobserve (f) {
this.eventHandler.removeEventListener(f)
}
/*
Observe a path.
E.g.
```
o.set('textarea', Y.TextBind)
o.observePath(['textarea'], function(t){
// is called whenever textarea is replaced
t.bind(textarea)
})
returns a Promise that contains a function that removes the observer from the path.
*/
observePath (path, f) {
var self = this
function observeProperty (events) {
// call f whenever path changes
for (var i = 0; i < events.length; i++) {
var event = events[i]
if (event.name === propertyName) {
// call this also for delete events!
var property = self.get(propertyName)
if (property instanceof Promise) {
property.then(f)
} else {
f(property)
}
}
}
}
if (path.length < 1) {
throw new Error('Path must contain at least one element!')
} else if (path.length === 1) {
var propertyName = path[0]
var property = self.get(propertyName)
if (property instanceof Promise) {
property.then(f)
} else {
f(property)
}
this.observe(observeProperty)
return Promise.resolve(function () {
self.unobserve(f)
})
} else {
var deleteChildObservers
var resetObserverPath = function () {
var promise = self.get(path[0])
if (!promise instanceof Promise) {
// its either not defined or a primitive value
promise = self.set(path[0], Y.Map)
}
return promise.then(function (map) {
return map.observePath(path.slice(1), f)
}).then(function (_deleteChildObservers) {
// update deleteChildObservers
deleteChildObservers = _deleteChildObservers
return Promise.resolve() // Promise does not return anything
})
}
var observer = function (events) {
for (var e in events) {
var event = events[e]
if (event.name === path[0]) {
deleteChildObservers()
if (event.type === 'add' || event.type === 'update') {
resetObserverPath()
}
// TODO: what about the delete events?
}
}
}
self.observe(observer)
return resetObserverPath().then(
// this promise contains a function that deletes all the child observers
// and how to unobserve the observe from this object
Promise.resolve(function () {
deleteChildObservers()
self.unobserve(observer)
})
)
}
}
* _changed (transaction, op) {
if (op.struct === 'Delete') {
op.key = (yield* transaction.getOperation(op.target)).parentSub
}
this.eventHandler.receivedOp(op)
}
}
Y.Map = new Y.utils.CustomType({
class: YMap,
createType: function * YMapCreator () {
var modelid = this.store.getNextOpId()
var model = {
map: {},
struct: 'Map',
type: 'Map',
id: modelid
}
yield* this.applyCreatedOperations([model])
return modelid
},
initType: function * YMapInitializer (os, model) {
var contents = {}
var opContents = {}
var map = model.map
for (var name in map) {
var op = yield* this.getOperation(map[name])
if (op.opContent != null) {
opContents[name] = op.opContent
} else {
contents[name] = op.content
}
}
return new YMap(os, model, contents, opContents)
}
})
})()

View File

@@ -1,219 +0,0 @@
/* global createUsers, Y, databases, compareAllUsers, getRandomNumber, applyRandomTransactionsAllRejoinNoGC, applyRandomTransactionsWithGC, async, describeManyTimes */
/* eslint-env browser,jasmine */
var numberOfYMapTests = 40
var repeatMapTeasts = 2
for (let database of databases) {
describe(`Map Type (DB: ${database})`, function () {
var y1, y2, y3, y4, flushAll
beforeEach(async(function * (done) {
yield createUsers(this, 5, database)
y1 = this.users[0].root
y2 = this.users[1].root
y3 = this.users[2].root
y4 = this.users[3].root
flushAll = this.users[0].connector.flushAll
done()
}))
afterEach(async(function * (done) {
yield compareAllUsers(this.users)
done()
}), 5000)
describe('Basic tests', function () {
it('Basic get&set of Map property (converge via sync)', async(function * (done) {
y1.set('stuff', 'stuffy')
expect(y1.get('stuff')).toEqual('stuffy')
yield flushAll()
for (var key in this.users) {
var u = this.users[key].root
expect(u.get('stuff')).toEqual('stuffy')
}
done()
}))
it('Map can set custom types (Map)', async(function * (done) {
var map = yield y1.set('Map', Y.Map)
map.set('one', 1)
map = yield y1.get('Map')
expect(map.get('one')).toEqual(1)
done()
}))
it('Map can set custom types (Array)', async(function * (done) {
var array = yield y1.set('Array', Y.Array)
array.insert(0, [1, 2, 3])
array = yield y1.get('Array')
expect(array.toArray()).toEqual([1, 2, 3])
done()
}))
it('Basic get&set of Map property (converge via update)', async(function * (done) {
yield flushAll()
y1.set('stuff', 'stuffy')
expect(y1.get('stuff')).toEqual('stuffy')
yield flushAll()
for (var key in this.users) {
var r = this.users[key].root
expect(r.get('stuff')).toEqual('stuffy')
}
done()
}))
it('Basic get&set of Map property (handle conflict)', async(function * (done) {
yield flushAll()
y1.set('stuff', 'c0')
y2.set('stuff', 'c1')
yield flushAll()
for (var key in this.users) {
var u = this.users[key]
expect(u.root.get('stuff')).toEqual('c0')
}
done()
}))
it('Basic get&set&delete of Map property (handle conflict)', async(function * (done) {
yield flushAll()
y1.set('stuff', 'c0')
y1.delete('stuff')
y2.set('stuff', 'c1')
yield flushAll()
for (var key in this.users) {
var u = this.users[key]
expect(u.root.get('stuff')).toBeUndefined()
}
done()
}))
it('Basic get&set of Map property (handle three conflicts)', async(function * (done) {
yield flushAll()
y1.set('stuff', 'c0')
y2.set('stuff', 'c1')
y2.set('stuff', 'c2')
y3.set('stuff', 'c3')
yield flushAll()
for (var key in this.users) {
var u = this.users[key]
expect(u.root.get('stuff')).toEqual('c0')
}
done()
}))
it('Basic get&set&delete of Map property (handle three conflicts)', async(function * (done) {
yield flushAll()
y1.set('stuff', 'c0')
y2.set('stuff', 'c1')
y2.set('stuff', 'c2')
y3.set('stuff', 'c3')
yield flushAll()
y1.set('stuff', 'deleteme')
y1.delete('stuff')
y2.set('stuff', 'c1')
y3.set('stuff', 'c2')
y4.set('stuff', 'c3')
yield flushAll()
for (var key in this.users) {
var u = this.users[key]
expect(u.root.get('stuff')).toBeUndefined()
}
done()
}))
it('observePath properties', async(function * (done) {
y1.observePath(['map'], function (map) {
if (map != null) {
map.set('yay', 4)
}
})
yield y2.set('map', Y.Map)
yield flushAll()
var map = yield y3.get('map')
expect(map.get('yay')).toEqual(4)
done()
}))
it('throws add & update & delete events (with type and primitive content)', async(function * (done) {
var event
yield flushAll()
y1.observe(function (e) {
event = e // just put it on event, should be thrown synchronously anyway
})
y1.set('stuff', 4)
expect(event).toEqual([{
type: 'add',
object: y1,
name: 'stuff'
}])
// update, oldValue is in contents
yield y1.set('stuff', Y.Array)
expect(event).toEqual([{
type: 'update',
object: y1,
name: 'stuff',
oldValue: 4
}])
y1.get('stuff').then(function (replacedArray) {
// update, oldValue is in opContents
y1.set('stuff', 5)
var getYArray = event[0].oldValue
expect(typeof getYArray.constructor === 'function').toBeTruthy()
getYArray().then(function (array) {
expect(array).toEqual(replacedArray)
// delete
y1.delete('stuff')
expect(event).toEqual([{
type: 'delete',
name: 'stuff',
object: y1,
oldValue: 5
}])
done()
})
})
}))
})
describeManyTimes(repeatMapTeasts, `${numberOfYMapTests} Random tests`, function () {
var randomMapTransactions = [
function set (map) {
map.set('somekey', getRandomNumber())
},
function delete_ (map) {
map.delete('somekey')
}
]
function compareMapValues (maps) {
var firstMap
for (var map of maps) {
var val = map.getPrimitive()
if (firstMap == null) {
firstMap = val
} else {
expect(val).toEqual(firstMap)
}
}
}
beforeEach(async(function * (done) {
yield y1.set('Map', Y.Map)
yield flushAll()
var promises = []
for (var u = 0; u < this.users.length; u++) {
promises.push(this.users[u].root.get('Map'))
}
this.maps = yield Promise.all(promises)
done()
}))
it(`succeed after ${numberOfYMapTests} actions, no GC, all users disconnecting/reconnecting`, async(function * (done) {
yield applyRandomTransactionsAllRejoinNoGC(this.users, this.maps, randomMapTransactions, numberOfYMapTests)
yield flushAll()
yield compareMapValues(this.maps)
done()
}))
it(`succeed after ${numberOfYMapTests} actions, GC, user[0] is not disconnecting`, async(function * (done) {
yield applyRandomTransactionsWithGC(this.users, this.maps, randomMapTransactions, numberOfYMapTests)
yield flushAll()
yield compareMapValues(this.maps)
done()
}))
})
})
}

View File

@@ -1,290 +0,0 @@
/* global Y */
'use strict'
;(function () {
class YTextBind extends Y.Array['class'] {
constructor (os, _model, idArray, valArray) {
super(os, _model, idArray, valArray)
this.textfields = []
}
toString () {
return this.valArray.join('')
}
insert (pos, content) {
super.insert(pos, content.split(''))
}
bind (textfield, domRoot) {
domRoot = domRoot || window; // eslint-disable-line
if (domRoot.getSelection == null) {
domRoot = window;// eslint-disable-line
}
// don't duplicate!
for (var t in this.textfields) {
if (this.textfields[t] === textfield) {
return
}
}
var creatorToken = false
var word = this
textfield.value = this.toString()
this.textfields.push(textfield)
var createRange, writeRange, writeContent
if (textfield.selectionStart != null && textfield.setSelectionRange != null) {
createRange = function (fix) {
var left = textfield.selectionStart
var right = textfield.selectionEnd
if (fix != null) {
left = fix(left)
right = fix(right)
}
return {
left: left,
right: right
}
}
writeRange = function (range) {
writeContent(word.toString())
textfield.setSelectionRange(range.left, range.right)
}
writeContent = function (content) {
textfield.value = content
}
} else {
createRange = function (fix) {
var range = {}
var s = domRoot.getSelection()
var clength = textfield.textContent.length
range.left = Math.min(s.anchorOffset, clength)
range.right = Math.min(s.focusOffset, clength)
if (fix != null) {
range.left = fix(range.left)
range.right = fix(range.right)
}
var editedElement = s.focusNode
if (editedElement === textfield || editedElement === textfield.childNodes[0]) {
range.isReal = true
} else {
range.isReal = false
}
return range
}
writeRange = function (range) {
writeContent(word.toString())
var textnode = textfield.childNodes[0]
if (range.isReal && textnode != null) {
if (range.left < 0) {
range.left = 0
}
range.right = Math.max(range.left, range.right)
if (range.right > textnode.length) {
range.right = textnode.length
}
range.left = Math.min(range.left, range.right)
var r = document.createRange(); // eslint-disable-line
r.setStart(textnode, range.left)
r.setEnd(textnode, range.right)
var s = window.getSelection(); // eslint-disable-line
s.removeAllRanges()
s.addRange(r)
}
}
writeContent = function (content) {
var contentArray = content.replace(new RegExp('\n', 'g'), ' ').split(' ');// eslint-disable-line
textfield.innerText = ''
for (var i in contentArray) {
var c = contentArray[i]
textfield.innerText += c
if (i !== contentArray.length - 1) {
textfield.innerHTML += '&nbsp;'
}
}
}
}
writeContent(this.toString())
this.observe(function (events) {
for (var e in events) {
var event = events[e]
if (!creatorToken) {
var oPos, fix
if (event.type === 'insert') {
oPos = event.index
fix = function (cursor) {// eslint-disable-line
if (cursor <= oPos) {
return cursor
} else {
cursor += 1
return cursor
}
}
var r = createRange(fix)
writeRange(r)
} else if (event.type === 'delete') {
oPos = event.index
fix = function (cursor) {// eslint-disable-line
if (cursor < oPos) {
return cursor
} else {
cursor -= 1
return cursor
}
}
r = createRange(fix)
writeRange(r)
}
}
}
})
// consume all text-insert changes.
textfield.onkeypress = function (event) {
if (word.is_deleted) {
// if word is deleted, do not do anything ever again
textfield.onkeypress = null
return true
}
creatorToken = true
var char
if (event.keyCode === 13) {
char = '\n'
} else if (event.key != null) {
if (event.charCode === 32) {
char = ' '
} else {
char = event.key
}
} else {
char = window.String.fromCharCode(event.keyCode); // eslint-disable-line
}
if (char.length > 1) {
return true
} else if (char.length > 0) {
var r = createRange()
var pos = Math.min(r.left, r.right, word.length)
var diff = Math.abs(r.right - r.left)
word.delete(pos, diff)
word.insert(pos, char)
r.left = pos + char.length
r.right = r.left
writeRange(r)
}
event.preventDefault()
creatorToken = false
return false
}
textfield.onpaste = function (event) {
if (word.is_deleted) {
// if word is deleted, do not do anything ever again
textfield.onpaste = null
return true
}
event.preventDefault()
}
textfield.oncut = function (event) {
if (word.is_deleted) {
// if word is deleted, do not do anything ever again
textfield.oncut = null
return true
}
event.preventDefault()
}
//
// consume deletes. Note that
// chrome: won't consume deletions on keypress event.
// keyCode is deprecated. BUT: I don't see another way.
// since event.key is not implemented in the current version of chrome.
// Every browser supports keyCode. Let's stick with it for now..
//
textfield.onkeydown = function (event) {
creatorToken = true
if (word.is_deleted) {
// if word is deleted, do not do anything ever again
textfield.onkeydown = null
return true
}
var r = createRange()
var pos = Math.min(r.left, r.right, word.toString().length)
var diff = Math.abs(r.left - r.right)
if (event.keyCode != null && event.keyCode === 8) { // Backspace
if (diff > 0) {
word.delete(pos, diff)
r.left = pos
r.right = pos
writeRange(r)
} else {
if (event.ctrlKey != null && event.ctrlKey) {
var val = word.toString()
var newPos = pos
var delLength = 0
if (pos > 0) {
newPos--
delLength++
}
while (newPos > 0 && val[newPos] !== ' ' && val[newPos] !== '\n') {
newPos--
delLength++
}
word.delete(newPos, pos - newPos)
r.left = newPos
r.right = newPos
writeRange(r)
} else {
if (pos > 0) {
word.delete(pos - 1, 1)
r.left = pos - 1
r.right = pos - 1
writeRange(r)
}
}
}
event.preventDefault()
creatorToken = false
return false
} else if (event.keyCode != null && event.keyCode === 46) { // Delete
if (diff > 0) {
word.delete(pos, diff)
r.left = pos
r.right = pos
writeRange(r)
} else {
word.delete(pos, 1)
r.left = pos
r.right = pos
writeRange(r)
}
event.preventDefault()
creatorToken = false
return false
} else {
creatorToken = false
return true
}
}
}
}
Y.TextBind = new Y.utils.CustomType({
class: YTextBind,
createType: function * YTextBindCreator () {
var modelid = this.store.getNextOpId()
var model = {
start: null,
end: null,
struct: 'List',
type: 'TextBind',
id: modelid
}
yield* this.applyCreatedOperations([model])
return modelid
},
initType: function * YTextBindInitializer (os, model) {
var valArray = []
var idArray = yield* Y.Struct.List.map.call(this, model, function (c) {
valArray.push(c.content)
return JSON.stringify(c.id)
})
return new YTextBind(os, model.id, idArray, valArray)
}
})
})()

View File

@@ -1,4 +1,4 @@
/* global Y */
/* @flow */
'use strict'
/*
@@ -7,192 +7,730 @@
Why: When constructing custom types, you sometimes want your types to work
synchronous: E.g.
``` Synchronous
mytype.setSomething("yay")
mytype.getSomething() === "yay"
```
``` Asynchronous
mytype.setSomething("yay")
mytype.getSomething() === undefined
mytype.waitForSomething().then(function(){
mytype.setSomething("yay")
mytype.getSomething() === "yay"
})
```
versus
``` Asynchronous
mytype.setSomething("yay")
mytype.getSomething() === undefined
mytype.waitForSomething().then(function(){
mytype.getSomething() === "yay"
})
```
The structures usually work asynchronously (you have to wait for the
database request to finish). EventHandler will help you to make your type
synchronously.
database request to finish). EventHandler helps you to make your type
synchronous.
*/
class EventHandler {
/*
onevent: is called when the structure changes.
module.exports = function (Y /* : any*/) {
Y.utils = {}
Note: "awaiting opertations" is used to denote operations that were
prematurely called. Events for received operations can not be executed until
all prematurely called operations were executed ("waiting operations")
*/
constructor (onevent) {
this.waiting = []
this.awaiting = 0
this.onevent = onevent
this.eventListeners = []
}
/*
Call this when a new operation arrives. It will be executed right away if
there are no waiting operations, that you prematurely executed
*/
receivedOp (op) {
if (this.awaiting <= 0) {
this.onevent([op])
} else {
this.waiting.push(Y.utils.copyObject(op))
class EventListenerHandler {
constructor () {
this.eventListeners = []
}
}
/*
You created some operations, and you want the `onevent` function to be
called right away. Received operations will not be executed untill all
prematurely called operations are executed
*/
awaitAndPrematurelyCall (ops) {
this.awaiting++
this.onevent(ops)
}
/*
Basic event listener boilerplate...
TODO: maybe put this in a different type..
*/
addEventListener (f) {
this.eventListeners.push(f)
}
removeEventListener (f) {
this.eventListeners = this.eventListeners.filter(function (g) {
return f !== g
})
}
removeAllEventListeners () {
this.eventListeners = []
}
callEventListeners (event) {
for (var i in this.eventListeners) {
try {
this.eventListeners[i](event)
} catch (e) {
console.log('User events must not throw Errors!') // eslint-disable-line
}
destroy () {
this.eventListeners = null
}
}
/*
Call this when you successfully awaited the execution of n Insert operations
*/
awaitedInserts (n) {
var ops = this.waiting.splice(this.waiting.length - n)
for (var oid = 0; oid < ops.length; oid++) {
var op = ops[oid]
for (var i = this.waiting.length - 1; i >= 0; i--) {
let w = this.waiting[i]
if (Y.utils.compareIds(op.left, w.id)) {
// include the effect of op in w
w.right = op.id
// exclude the effect of w in op
op.left = w.left
} else if (Y.utils.compareIds(op.right, w.id)) {
// similar..
w.left = op.id
op.right = w.right
/*
Basic event listener boilerplate...
*/
addEventListener (f) {
this.eventListeners.push(f)
}
removeEventListener (f) {
this.eventListeners = this.eventListeners.filter(function (g) {
return f !== g
})
}
removeAllEventListeners () {
this.eventListeners = []
}
callEventListeners (event) {
for (var i = 0; i < this.eventListeners.length; i++) {
try {
this.eventListeners[i](event)
} catch (e) {
console.error('User events must not throw Errors!')
}
}
}
this._tryCallEvents()
}
/*
Call this when you successfully awaited the execution of n Delete operations
*/
awaitedDeletes (n, newLeft) {
var ops = this.waiting.splice(this.waiting.length - n)
for (var j in ops) {
var del = ops[j]
if (newLeft != null) {
for (var i in this.waiting) {
let w = this.waiting[i]
// We will just care about w.left
if (Y.utils.compareIds(del.target, w.left)) {
del.left = newLeft
Y.utils.EventListenerHandler = EventListenerHandler
class EventHandler extends EventListenerHandler {
/* ::
waiting: Array<Insertion | Deletion>;
awaiting: number;
onevent: Function;
eventListeners: Array<Function>;
*/
/*
onevent: is called when the structure changes.
Note: "awaiting opertations" is used to denote operations that were
prematurely called. Events for received operations can not be executed until
all prematurely called operations were executed ("waiting operations")
*/
constructor (onevent /* : Function */) {
super()
this.waiting = []
this.awaiting = 0
this.onevent = onevent
}
destroy () {
super.destroy()
this.waiting = null
this.awaiting = null
this.onevent = null
}
/*
Call this when a new operation arrives. It will be executed right away if
there are no waiting operations, that you prematurely executed
*/
receivedOp (op) {
if (this.awaiting <= 0) {
this.onevent(op)
} else if (op.struct === 'Delete') {
var self = this
var checkDelete = function checkDelete (d) {
if (d.length == null) {
throw new Error('This shouldn\'t happen! d.length must be defined!')
}
// we check if o deletes something in self.waiting
// if so, we remove the deleted operation
for (var w = 0; w < self.waiting.length; w++) {
var i = self.waiting[w]
if (i.struct === 'Insert' && i.id[0] === d.target[0]) {
var iLength = i.hasOwnProperty('content') ? i.content.length : 1
var dStart = d.target[1]
var dEnd = d.target[1] + (d.length || 1)
var iStart = i.id[1]
var iEnd = i.id[1] + iLength
// Check if they don't overlap
if (iEnd <= dStart || dEnd <= iStart) {
// no overlapping
continue
}
// we check all overlapping cases. All cases:
/*
1) iiiii
ddddd
--> modify i and d
2) iiiiiii
ddddd
--> modify i, remove d
3) iiiiiii
ddd
--> remove d, modify i, and create another i (for the right hand side)
4) iiiii
ddddddd
--> remove i, modify d
5) iiiiiii
ddddddd
--> remove both i and d (**)
6) iiiiiii
ddddd
--> modify i, remove d
7) iii
ddddddd
--> remove i, create and apply two d with checkDelete(d) (**)
8) iiiii
ddddddd
--> remove i, modify d (**)
9) iiiii
ddddd
--> modify i and d
(**) (also check if i contains content or type)
*/
// TODO: I left some debugger statements, because I want to debug all cases once in production. REMEMBER END TODO
if (iStart < dStart) {
if (dStart < iEnd) {
if (iEnd < dEnd) {
// Case 1
// remove the right part of i's content
i.content.splice(dStart - iStart)
// remove the start of d's deletion
d.length = dEnd - iEnd
d.target = [d.target[0], iEnd]
continue
} else if (iEnd === dEnd) {
// Case 2
i.content.splice(dStart - iStart)
// remove d, we do that by simply ending this function
return
} else { // (dEnd < iEnd)
// Case 3
var newI = {
id: [i.id[0], dEnd],
content: i.content.slice(dEnd - iStart),
struct: 'Insert'
}
self.waiting.push(newI)
i.content.splice(dStart - iStart)
return
}
}
} else if (dStart === iStart) {
if (iEnd < dEnd) {
// Case 4
d.length = dEnd - iEnd
d.target = [d.target[0], iEnd]
i.content = []
continue
} else if (iEnd === dEnd) {
// Case 5
self.waiting.splice(w, 1)
return
} else { // (dEnd < iEnd)
// Case 6
i.content = i.content.slice(dEnd - iStart)
i.id = [i.id[0], dEnd]
return
}
} else { // (dStart < iStart)
if (iStart < dEnd) {
// they overlap
/*
7) iii
ddddddd
--> remove i, create and apply two d with checkDelete(d) (**)
8) iiiii
ddddddd
--> remove i, modify d (**)
9) iiiii
ddddd
--> modify i and d
*/
if (iEnd < dEnd) {
// Case 7
// debugger // TODO: You did not test this case yet!!!! (add the debugger here)
self.waiting.splice(w, 1)
checkDelete({
target: [d.target[0], dStart],
length: iStart - dStart,
struct: 'Delete'
})
checkDelete({
target: [d.target[0], iEnd],
length: iEnd - dEnd,
struct: 'Delete'
})
return
} else if (iEnd === dEnd) {
// Case 8
self.waiting.splice(w, 1)
w--
d.length -= iLength
continue
} else { // dEnd < iEnd
// Case 9
d.length = iStart - dStart
i.content.splice(0, dEnd - iStart)
i.id = [i.id[0], dEnd]
continue
}
}
}
}
}
// finished with remaining operations
self.waiting.push(d)
}
checkDelete(op)
} else {
this.waiting.push(op)
}
}
/*
You created some operations, and you want the `onevent` function to be
called right away. Received operations will not be executed untill all
prematurely called operations are executed
*/
awaitAndPrematurelyCall (ops) {
this.awaiting++
ops.map(Y.utils.copyOperation).forEach(this.onevent)
}
* awaitOps (transaction, f, args) {
function notSoSmartSort (array) {
// this function sorts insertions in a executable order
var result = []
while (array.length > 0) {
for (var i = 0; i < array.length; i++) {
var independent = true
for (var j = 0; j < array.length; j++) {
if (Y.utils.matchesId(array[j], array[i].left)) {
// array[i] depends on array[j]
independent = false
break
}
}
if (independent) {
result.push(array.splice(i, 1)[0])
i--
}
}
}
return result
}
var before = this.waiting.length
// somehow create new operations
yield* f.apply(transaction, args)
// remove all appended ops / awaited ops
this.waiting.splice(before)
if (this.awaiting > 0) this.awaiting--
// if there are no awaited ops anymore, we can update all waiting ops, and send execute them (if there are still no awaited ops)
if (this.awaiting === 0 && this.waiting.length > 0) {
// update all waiting ops
for (let i = 0; i < this.waiting.length; i++) {
var o = this.waiting[i]
if (o.struct === 'Insert') {
var _o = yield* transaction.getInsertion(o.id)
if (!Y.utils.compareIds(_o.id, o.id)) {
// o got extended
o.left = [o.id[0], o.id[1] - 1]
} else if (_o.left == null) {
o.left = null
} else {
// find next undeleted op
var left = yield* transaction.getInsertion(_o.left)
while (left.deleted != null) {
if (left.left != null) {
left = yield* transaction.getInsertion(left.left)
} else {
left = null
break
}
}
o.left = left != null ? Y.utils.getLastId(left) : null
}
}
}
// the previous stuff was async, so we have to check again!
// We also pull changes from the bindings, if there exists such a method, this could increase awaiting too
if (this._pullChanges != null) {
this._pullChanges()
}
if (this.awaiting === 0) {
// sort by type, execute inserts first
var ins = []
var dels = []
this.waiting.forEach(function (o) {
if (o.struct === 'Delete') {
dels.push(o)
} else {
ins.push(o)
}
})
this.waiting = []
// put in executable order
ins = notSoSmartSort(ins)
// this.onevent can trigger the creation of another operation
// -> check if this.awaiting increased & stop computation if it does
for (var i = 0; i < ins.length; i++) {
if (this.awaiting === 0) {
this.onevent(ins[i])
} else {
this.waiting = this.waiting.concat(ins.slice(i))
break
}
}
for (i = 0; i < dels.length; i++) {
if (this.awaiting === 0) {
this.onevent(dels[i])
} else {
this.waiting = this.waiting.concat(dels.slice(i))
break
}
}
}
}
}
this._tryCallEvents()
// TODO: Remove awaitedInserts and awaitedDeletes in favor of awaitedOps, as they are deprecated and do not always work
// Do this in one of the coming releases that are breaking anyway
/*
Call this when you successfully awaited the execution of n Insert operations
*/
awaitedInserts (n) {
var ops = this.waiting.splice(this.waiting.length - n)
for (var oid = 0; oid < ops.length; oid++) {
var op = ops[oid]
if (op.struct === 'Insert') {
for (var i = this.waiting.length - 1; i >= 0; i--) {
let w = this.waiting[i]
// TODO: do I handle split operations correctly here? Super unlikely, but yeah..
// Also: can this case happen? Can op be inserted in the middle of a larger op that is in $waiting?
if (w.struct === 'Insert') {
if (Y.utils.matchesId(w, op.left)) {
// include the effect of op in w
w.right = op.id
// exclude the effect of w in op
op.left = w.left
} else if (Y.utils.compareIds(w.id, op.right)) {
// similar..
w.left = Y.utils.getLastId(op)
op.right = w.right
}
}
}
} else {
throw new Error('Expected Insert Operation!')
}
}
this._tryCallEvents(n)
}
/*
Call this when you successfully awaited the execution of n Delete operations
*/
awaitedDeletes (n, newLeft) {
var ops = this.waiting.splice(this.waiting.length - n)
for (var j = 0; j < ops.length; j++) {
var del = ops[j]
if (del.struct === 'Delete') {
if (newLeft != null) {
for (var i = 0; i < this.waiting.length; i++) {
let w = this.waiting[i]
// We will just care about w.left
if (w.struct === 'Insert' && Y.utils.compareIds(del.target, w.left)) {
w.left = newLeft
}
}
}
} else {
throw new Error('Expected Delete Operation!')
}
}
this._tryCallEvents(n)
}
/* (private)
Try to execute the events for the waiting operations
*/
_tryCallEvents () {
function notSoSmartSort (array) {
var result = []
while (array.length > 0) {
for (var i = 0; i < array.length; i++) {
var independent = true
for (var j = 0; j < array.length; j++) {
if (Y.utils.matchesId(array[j], array[i].left)) {
// array[i] depends on array[j]
independent = false
break
}
}
if (independent) {
result.push(array.splice(i, 1)[0])
i--
}
}
}
return result
}
if (this.awaiting > 0) this.awaiting--
if (this.awaiting === 0 && this.waiting.length > 0) {
var ins = []
var dels = []
this.waiting.forEach(function (o) {
if (o.struct === 'Delete') {
dels.push(o)
} else {
ins.push(o)
}
})
ins = notSoSmartSort(ins)
ins.forEach(this.onevent)
dels.forEach(this.onevent)
this.waiting = []
}
}
}
/* (private)
Try to execute the events for the waiting operations
Y.utils.EventHandler = EventHandler
/*
A wrapper for the definition of a custom type.
Every custom type must have three properties:
* struct
- Structname of this type
* initType
- Given a model, creates a custom type
* class
- the constructor of the custom type (e.g. in order to inherit from a type)
*/
_tryCallEvents () {
this.awaiting--
if (this.awaiting <= 0 && this.waiting.length > 0) {
var events = this.waiting
this.waiting = []
this.onevent(events)
class CustomType { // eslint-disable-line
/* ::
struct: any;
initType: any;
class: Function;
name: String;
*/
constructor (def) {
if (def.struct == null ||
def.initType == null ||
def.class == null ||
def.name == null
) {
throw new Error('Custom type was not initialized correctly!')
}
this.struct = def.struct
this.initType = def.initType
this.class = def.class
this.name = def.name
if (def.appendAdditionalInfo != null) {
this.appendAdditionalInfo = def.appendAdditionalInfo
}
this.parseArguments = (def.parseArguments || function () {
return [this]
}).bind(this)
this.parseArguments.typeDefinition = this
}
}
}
Y.utils.EventHandler = EventHandler
Y.utils.CustomType = CustomType
/*
A wrapper for the definition of a custom type.
Every custom type must have three properties:
* createType
- Defines the model of a newly created custom type and returns the type
* initType
- Given a model, creates a custom type
* class
- the constructor of the custom type (e.g. in order to inherit from a type)
*/
class CustomType { // eslint-disable-line
constructor (def) {
if (def.createType == null ||
def.initType == null ||
def.class == null
) {
throw new Error('Custom type was not initialized correctly!')
}
this.createType = def.createType
this.initType = def.initType
this.class = def.class
}
}
Y.utils.CustomType = CustomType
/*
Make a flat copy of an object
(just copy properties)
*/
function copyObject (o) {
var c = {}
for (var key in o) {
c[key] = o[key]
}
return c
}
Y.utils.copyObject = copyObject
/*
Defines a smaller relation on Id's
*/
function smaller (a, b) {
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1])
}
Y.utils.smaller = smaller
function compareIds (id1, id2) {
if (id1 == null || id2 == null) {
if (id1 == null && id2 == null) {
return true
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
if (v != null) {
if (v instanceof Y.utils.CustomType) return [v]
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomType) return v
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomType) return [v.typeDefinition]
}
return false
}
if (id1[0] === id2[0] && id1[1] === id2[1]) {
return true
} else {
return false
/*
Make a flat copy of an object
(just copy properties)
*/
function copyObject (o) {
var c = {}
for (var key in o) {
c[key] = o[key]
}
return c
}
Y.utils.copyObject = copyObject
/*
Copy an operation, so that it can be manipulated.
Note: You must not change subproperties (except o.content)!
*/
function copyOperation (o) {
o = copyObject(o)
if (o.content != null) {
o.content = o.content.map(function (c) { return c })
}
return o
}
Y.utils.copyOperation = copyOperation
/*
Defines a smaller relation on Id's
*/
function smaller (a, b) {
return a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || typeof a[1] < typeof b[1]))
}
Y.utils.smaller = smaller
function inDeletionRange (del, ins) {
return del.target[0] === ins[0] && del.target[1] <= ins[1] && ins[1] < del.target[1] + (del.length || 1)
}
Y.utils.inDeletionRange = inDeletionRange
function compareIds (id1, id2) {
if (id1 == null || id2 == null) {
return id1 === id2
} else {
return id1[0] === id2[0] && id1[1] === id2[1]
}
}
Y.utils.compareIds = compareIds
function matchesId (op, id) {
if (id == null || op == null) {
return id === op
} else {
if (id[0] === op.id[0]) {
if (op.content == null) {
return id[1] === op.id[1]
} else {
return id[1] >= op.id[1] && id[1] < op.id[1] + op.content.length
}
}
}
}
Y.utils.matchesId = matchesId
function getLastId (op) {
if (op.content == null || op.content.length === 1) {
return op.id
} else {
return [op.id[0], op.id[1] + op.content.length - 1]
}
}
Y.utils.getLastId = getLastId
function createEmptyOpsArray (n) {
var a = new Array(n)
for (var i = 0; i < a.length; i++) {
a[i] = {
id: [null, null]
}
}
return a
}
function createSmallLookupBuffer (Store) {
/*
This buffer implements a very small buffer that temporarily stores operations
after they are read / before they are written.
The buffer basically implements FIFO. Often requested lookups will be re-queued every time they are looked up / written.
It can speed up lookups on Operation Stores and State Stores. But it does not require notable use of memory or processing power.
Good for os and ss, bot not for ds (because it often uses methods that require a flush)
I tried to optimize this for performance, therefore no highlevel operations.
*/
class SmallLookupBuffer extends Store {
constructor (arg1, arg2) {
// super(...arguments) -- do this when this is supported by stable nodejs
super(arg1, arg2)
this.writeBuffer = createEmptyOpsArray(5)
this.readBuffer = createEmptyOpsArray(10)
}
* find (id, noSuperCall) {
var i, r
for (i = this.readBuffer.length - 1; i >= 0; i--) {
r = this.readBuffer[i]
// we don't have to use compareids, because id is always defined!
if (r.id[1] === id[1] && r.id[0] === id[0]) {
// found r
// move r to the end of readBuffer
for (; i < this.readBuffer.length - 1; i++) {
this.readBuffer[i] = this.readBuffer[i + 1]
}
this.readBuffer[this.readBuffer.length - 1] = r
return r
}
}
var o
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
r = this.writeBuffer[i]
if (r.id[1] === id[1] && r.id[0] === id[0]) {
o = r
break
}
}
if (i < 0 && noSuperCall === undefined) {
// did not reach break in last loop
// read id and put it to the end of readBuffer
o = yield* super.find(id)
}
if (o != null) {
for (i = 0; i < this.readBuffer.length - 1; i++) {
this.readBuffer[i] = this.readBuffer[i + 1]
}
this.readBuffer[this.readBuffer.length - 1] = o
}
return o
}
* put (o) {
var id = o.id
var i, r // helper variables
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
r = this.writeBuffer[i]
if (r.id[1] === id[1] && r.id[0] === id[0]) {
// is already in buffer
// forget r, and move o to the end of writeBuffer
for (; i < this.writeBuffer.length - 1; i++) {
this.writeBuffer[i] = this.writeBuffer[i + 1]
}
this.writeBuffer[this.writeBuffer.length - 1] = o
break
}
}
if (i < 0) {
// did not reach break in last loop
// write writeBuffer[0]
var write = this.writeBuffer[0]
if (write.id[0] !== null) {
yield* super.put(write)
}
// put o to the end of writeBuffer
for (i = 0; i < this.writeBuffer.length - 1; i++) {
this.writeBuffer[i] = this.writeBuffer[i + 1]
}
this.writeBuffer[this.writeBuffer.length - 1] = o
}
// check readBuffer for every occurence of o.id, overwrite if found
// whether found or not, we'll append o to the readbuffer
for (i = 0; i < this.readBuffer.length - 1; i++) {
r = this.readBuffer[i + 1]
if (r.id[1] === id[1] && r.id[0] === id[0]) {
this.readBuffer[i] = o
} else {
this.readBuffer[i] = r
}
}
this.readBuffer[this.readBuffer.length - 1] = o
}
* delete (id) {
var i, r
for (i = 0; i < this.readBuffer.length; i++) {
r = this.readBuffer[i]
if (r.id[1] === id[1] && r.id[0] === id[0]) {
this.readBuffer[i] = {
id: [null, null]
}
}
}
yield* this.flush()
yield* super.delete(id)
}
* findWithLowerBound (id) {
var o = yield* this.find(id, true)
if (o != null) {
return o
} else {
yield* this.flush()
return yield* super.findWithLowerBound.apply(this, arguments)
}
}
* findWithUpperBound (id) {
var o = yield* this.find(id, true)
if (o != null) {
return o
} else {
yield* this.flush()
return yield* super.findWithUpperBound.apply(this, arguments)
}
}
* findNext () {
yield* this.flush()
return yield* super.findNext.apply(this, arguments)
}
* findPrev () {
yield* this.flush()
return yield* super.findPrev.apply(this, arguments)
}
* iterate () {
yield* this.flush()
yield* super.iterate.apply(this, arguments)
}
* flush () {
for (var i = 0; i < this.writeBuffer.length; i++) {
var write = this.writeBuffer[i]
if (write.id[0] !== null) {
yield* super.put(write)
this.writeBuffer[i] = {
id: [null, null]
}
}
}
}
}
return SmallLookupBuffer
}
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
}
Y.utils.compareIds = compareIds

183
src/y.js
View File

@@ -1,32 +1,160 @@
/* @flow */
'use strict'
function Y (opts) {
return new Promise(function (resolve) {
var yconfig = new YConfig(opts, function () {
yconfig.db.whenUserIdSet(function () {
resolve(yconfig)
})
})
require('./Connector.js')(Y)
require('./Database.js')(Y)
require('./Transaction.js')(Y)
require('./Struct.js')(Y)
require('./Utils.js')(Y)
require('./Connectors/Test.js')(Y)
var requiringModules = {}
module.exports = Y
Y.requiringModules = requiringModules
Y.extend = function (name, value) {
if (value instanceof Y.utils.CustomType) {
Y[name] = value.parseArguments
} else {
Y[name] = value
}
if (requiringModules[name] != null) {
requiringModules[name].resolve()
delete requiringModules[name]
}
}
Y.requestModules = requestModules
function requestModules (modules) {
// determine if this module was compiled for es5 or es6 (y.js vs. y.es6)
// if Insert.execute is a Function, then it isnt a generator..
// then load the es5(.js) files..
var extention = typeof regeneratorRuntime !== 'undefined' ? '.js' : '.es6'
var promises = []
for (var i = 0; i < modules.length; i++) {
var module = modules[i].split('(')[0]
var modulename = 'y-' + module.toLowerCase()
if (Y[module] == null) {
if (requiringModules[module] == null) {
// module does not exist
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
var imported = document.createElement('script')
imported.src = Y.sourceDir + '/' + modulename + '/' + modulename + extention
document.head.appendChild(imported)
let requireModule = {}
requiringModules[module] = requireModule
requireModule.promise = new Promise(function (resolve) {
requireModule.resolve = resolve
})
promises.push(requireModule.promise)
} else {
console.info('YJS: Please do not depend on automatic requiring of modules anymore! Extend modules as follows `require(\'y-modulename\')(Y)`')
require(modulename)(Y)
}
} else {
promises.push(requiringModules[modules[i]].promise)
}
}
}
return Promise.all(promises)
}
/* ::
type MemoryOptions = {
name: 'memory'
}
type IndexedDBOptions = {
name: 'indexeddb',
namespace: string
}
type DbOptions = MemoryOptions | IndexedDBOptions
type WebRTCOptions = {
name: 'webrtc',
room: string
}
type WebsocketsClientOptions = {
name: 'websockets-client',
room: string
}
type ConnectionOptions = WebRTCOptions | WebsocketsClientOptions
type YOptions = {
connector: ConnectionOptions,
db: DbOptions,
types: Array<TypeName>,
sourceDir: string,
share: {[key: string]: TypeName}
}
*/
function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
opts.types = opts.types != null ? opts.types : []
var modules = [opts.db.name, opts.connector.name].concat(opts.types)
for (var name in opts.share) {
modules.push(opts.share[name])
}
Y.sourceDir = opts.sourceDir
return new Promise(function (resolve, reject) {
setTimeout(function () {
Y.requestModules(modules).then(function () {
if (opts == null) reject('An options object is expected! ')
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
else if (opts.db == null) reject('You must specify a database! (missing db property)')
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
else if (opts.share == null) reject('You must specify a set of shared types!')
else {
var yconfig = new YConfig(opts)
yconfig.db.whenUserIdSet(function () {
yconfig.init(function () {
resolve(yconfig)
})
})
}
}).catch(reject)
}, 0)
})
}
class YConfig {
/* ::
db: Y.AbstractDatabase;
connector: Y.AbstractConnector;
share: {[key: string]: any};
options: Object;
*/
constructor (opts, callback) {
this.options = opts
this.db = new Y[opts.db.name](this, opts.db)
this.connector = new Y[opts.connector.name](this, opts.connector)
}
init (callback) {
var opts = this.options
var share = {}
this.share = share
this.db.requestTransaction(function * requestTransaction () {
// create initial Map type
var model = {
id: ['_', 0],
struct: 'Map',
type: 'Map',
map: {}
// create shared object
for (var propertyname in opts.share) {
var typeConstructor = opts.share[propertyname].split('(')
var typeName = typeConstructor.splice(0, 1)
var args = []
if (typeConstructor.length === 1) {
try {
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
} catch (e) {
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
}
}
var type = Y[typeName]
var typedef = type.typeDefinition
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
share[propertyname] = yield* this.createType(type.apply(typedef, args), id)
}
yield* this.store.tryExecute.call(this, model)
var root = yield* this.getType(model.id)
this.store.y.root = root
callback()
this.store.whenTransactionsFinished()
.then(callback)
})
}
isConnected () {
@@ -39,15 +167,20 @@ class YConfig {
return this.connector.reconnect()
}
destroy () {
this.disconnect()
this.db.destroy()
this.connector = null
this.db = null
if (this.connector.destroy != null) {
this.connector.destroy()
} else {
this.connector.disconnect()
}
var self = this
this.db.requestTransaction(function * () {
yield* self.db.destroy()
self.connector = null
self.db = null
})
}
}
if (typeof YConcurrency_TestingMode !== 'undefined') {
g.Y = Y //eslint-disable-line
// debugger //eslint-disable-line
if (typeof window !== 'undefined') {
window.Y = Y
}
Y.utils = {}