Compare commits
384 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9374c363c3 | ||
|
|
4078020afd | ||
|
|
e31d5e0e1d | ||
|
|
acbc884eb5 | ||
|
|
f9315288d0 | ||
|
|
3b0d0343f4 | ||
|
|
74c881bb5b | ||
|
|
63f8a891be | ||
|
|
2083cdb6b0 | ||
|
|
2091392031 | ||
|
|
3dc67e075b | ||
|
|
81e72126ce | ||
|
|
e77a753708 | ||
|
|
bc856a09f5 | ||
|
|
f7ae62a906 | ||
|
|
6669be104e | ||
|
|
14d59de2bd | ||
|
|
483d2c78aa | ||
|
|
5b835563c8 | ||
|
|
996566419c | ||
|
|
5d6a9872e2 | ||
|
|
8930865a21 | ||
|
|
2897695680 | ||
|
|
5118f02b49 | ||
|
|
a10933beef | ||
|
|
c2ffe0b697 | ||
|
|
2d1a7b067b | ||
|
|
2675f0277c | ||
|
|
918bc334b2 | ||
|
|
accf0dbafb | ||
|
|
6b8ce0ab4f | ||
|
|
71bf6438e1 | ||
|
|
90b7b01e9a | ||
|
|
895ec86ff6 | ||
|
|
bffd130b92 | ||
|
|
feae0d51bd | ||
|
|
f46c8df605 | ||
|
|
82025c5de9 | ||
|
|
153ec811e2 | ||
|
|
01031d27c3 | ||
|
|
c72f62ecb6 | ||
|
|
e1df1a7a12 | ||
|
|
a7f845f553 | ||
|
|
20321c8a7d | ||
|
|
f3fadd3895 | ||
|
|
08a79d0e7b | ||
|
|
5b21104da3 | ||
|
|
ecc2aef0f8 | ||
|
|
1c32067908 | ||
|
|
fe75ed6208 | ||
|
|
c2404b1e98 | ||
|
|
f363e1e9fc | ||
|
|
749514c074 | ||
|
|
24f8616386 | ||
|
|
d4ee8af772 | ||
|
|
83a42271ad | ||
|
|
88971b4e69 | ||
|
|
f844dcbc1e | ||
|
|
c9c00b5a08 | ||
|
|
d79e3102fc | ||
|
|
ba4f444f32 | ||
|
|
effc2fe576 | ||
|
|
f9a54626b1 | ||
|
|
808a07d218 | ||
|
|
afbe81a602 | ||
|
|
2883947641 | ||
|
|
1c15edd332 | ||
|
|
214380c3ca | ||
|
|
ecbf03ab10 | ||
|
|
5aedddeea3 | ||
|
|
babdb765c5 | ||
|
|
43b4d59f9b | ||
|
|
64a5fae838 | ||
|
|
5036053d9c | ||
|
|
0ec249d388 | ||
|
|
be68a25904 | ||
|
|
fc92b12e85 | ||
|
|
e35f4d19f3 | ||
|
|
6d3c4b21fb | ||
|
|
339590f49e | ||
|
|
429c1f83c1 | ||
|
|
03bab63358 | ||
|
|
06ef22b8ca | ||
|
|
f579a436c7 | ||
|
|
da7e67d97d | ||
|
|
bd54a43a33 | ||
|
|
68c21131d3 | ||
|
|
3826d9b592 | ||
|
|
fa9ff669e4 | ||
|
|
bca7477ca5 | ||
|
|
b40b7e10ab | ||
|
|
d20141fec1 | ||
|
|
5f2a81d064 | ||
|
|
56ba55cbab | ||
|
|
7be262e9f3 | ||
|
|
1da76dbc20 | ||
|
|
8924c3e163 | ||
|
|
608b5e3319 | ||
|
|
d532fc530f | ||
|
|
a5760a45bb | ||
|
|
437955ba84 | ||
|
|
dab72be87f | ||
|
|
89a6ec374e | ||
|
|
4b6352b11a | ||
|
|
31d2a231e3 | ||
|
|
6b1cf18822 | ||
|
|
39dc2317b7 | ||
|
|
38bf398709 | ||
|
|
364ed325b0 | ||
|
|
1b3f5443b3 | ||
|
|
37ac7787d0 | ||
|
|
8e4cf83330 | ||
|
|
5524ab9c20 | ||
|
|
65dc716936 | ||
|
|
5b7a4482cf | ||
|
|
cfa089f7cf | ||
|
|
190442a58d | ||
|
|
0398b5260a | ||
|
|
8544c16771 | ||
|
|
a5f55359c3 | ||
|
|
102555a3b0 | ||
|
|
ece8268e44 | ||
|
|
dd279bccf7 | ||
|
|
7e046e0753 | ||
|
|
51a834d6c9 | ||
|
|
a33d0bf7bc | ||
|
|
fd6a28eb25 | ||
|
|
579fd52455 | ||
|
|
8cfc9d41c3 | ||
|
|
bdf290adb2 | ||
|
|
98d87cb26d | ||
|
|
fbbfa9fd47 | ||
|
|
72bd0d9c3a | ||
|
|
3dbeb2c415 | ||
|
|
2a9fd96958 | ||
|
|
9d34ccfdbc | ||
|
|
7753994e36 | ||
|
|
709779425c | ||
|
|
334db3234b | ||
|
|
0db7fe5d46 | ||
|
|
3a55ca4f21 | ||
|
|
8d14a9cbba | ||
|
|
f6c5051472 | ||
|
|
eff6fb1cc5 | ||
|
|
0ebfae6997 | ||
|
|
e9c40f9a83 | ||
|
|
da2762edf5 | ||
|
|
bd9c3813fd | ||
|
|
940a44bb7c | ||
|
|
aa2e7fd917 | ||
|
|
9fc55f5386 | ||
|
|
8ee563f873 | ||
|
|
5fcfbbfe94 | ||
|
|
8870fdc495 | ||
|
|
58a612eaa1 | ||
|
|
ae12b087e7 | ||
|
|
528dbc6e5a | ||
|
|
1deb453cc5 | ||
|
|
099297ebdf | ||
|
|
3faeb628fd | ||
|
|
d1e30c5040 | ||
|
|
fa45ce04ef | ||
|
|
2d20fd59d0 | ||
|
|
08d07796ee | ||
|
|
010d0d684e | ||
|
|
6dc347642b | ||
|
|
138afe39dc | ||
|
|
0832be2380 | ||
|
|
8a2a184f30 | ||
|
|
4882e77fdd | ||
|
|
78f4f6f5b9 | ||
|
|
317f7f19bb | ||
|
|
00f58ba68f | ||
|
|
029a169114 | ||
|
|
f58889a05d | ||
|
|
e9ac59dcf8 | ||
|
|
57cf20555f | ||
|
|
805ed3b577 | ||
|
|
2a0d5c0cd7 | ||
|
|
13ed66c326 | ||
|
|
1c35198839 | ||
|
|
a7021b9212 | ||
|
|
1fa1f1a668 | ||
|
|
243e62e320 | ||
|
|
15e933ee5b | ||
|
|
605e1052ac | ||
|
|
16c00525d1 | ||
|
|
e9da461625 | ||
|
|
a071c07ee2 | ||
|
|
8dad4f6ed4 | ||
|
|
0980609cc9 | ||
|
|
29f3f3f722 | ||
|
|
04139d3b7e | ||
|
|
45814c4e00 | ||
|
|
cf365b8902 | ||
|
|
aff10fa4db | ||
|
|
181595293f | ||
|
|
ee133ef334 | ||
|
|
661232f23c | ||
|
|
541a93d152 | ||
|
|
d6e1cd42a2 | ||
|
|
51e20fb9c7 | ||
|
|
e32aef4c9f | ||
|
|
9c4074e3e3 | ||
|
|
aadef59934 | ||
|
|
6a13419c62 | ||
|
|
1ace3e3120 | ||
|
|
c95dae3c33 | ||
|
|
82e2254302 | ||
|
|
6e9f990d5c | ||
|
|
7d4adf314d | ||
|
|
8745fd64ca | ||
|
|
638c575dfc | ||
|
|
acf8d37616 | ||
|
|
ae8be1ec6b | ||
|
|
a5f76cee84 | ||
|
|
2013266d56 | ||
|
|
b08aeee4fc | ||
|
|
183f30878e | ||
|
|
5e4c56af29 | ||
|
|
13bef69be4 | ||
|
|
b1d70ef25e | ||
|
|
6f3a291ef5 | ||
|
|
2a601ac6f6 | ||
|
|
82b3e50d49 | ||
|
|
4bfe484fc2 | ||
|
|
b9e21665e2 | ||
|
|
06e7caab2d | ||
|
|
c8ded24842 | ||
|
|
dae0f71cbc | ||
|
|
81c601c65f | ||
|
|
56165a3c10 | ||
|
|
5e0d602e12 | ||
|
|
420821be31 | ||
|
|
d1fda080d9 | ||
|
|
dd5e2adc87 | ||
|
|
ee983ceff6 | ||
|
|
ee116b8ca4 | ||
|
|
d4ef54358b | ||
|
|
ebc628adfc | ||
|
|
4563ccc98e | ||
|
|
a4f7f5c987 | ||
|
|
4a7f09c32d | ||
|
|
f78dc52d7b | ||
|
|
f9f8228db6 | ||
|
|
60b75d1862 | ||
|
|
9b3fe2f197 | ||
|
|
6b153896dd | ||
|
|
66a7d2720d | ||
|
|
d50d34dc12 | ||
|
|
8cc374cabb | ||
|
|
8e9e62b3d0 | ||
|
|
9b45a78e58 | ||
|
|
f862fae473 | ||
|
|
0493d99d57 | ||
|
|
a1026bc365 | ||
|
|
fe4564542b | ||
|
|
7b52111c31 | ||
|
|
c184cb961b | ||
|
|
02f2f6b0fe | ||
|
|
e47dee53a3 | ||
|
|
9b6183ea70 | ||
|
|
79ec71d559 | ||
|
|
bf4d5f24a8 | ||
|
|
9d0373b85b | ||
|
|
f8ad9abcc0 | ||
|
|
b25977be06 | ||
|
|
bffbb6ca27 | ||
|
|
8f63147dbc | ||
|
|
7a274565e5 | ||
|
|
75793d0ced | ||
|
|
7ec409e09f | ||
|
|
fec03dc6e1 | ||
|
|
3142b0f161 | ||
|
|
042bcee482 | ||
|
|
b3e09d001f | ||
|
|
dcec0fe967 | ||
|
|
ae790b6947 | ||
|
|
4b08cbe875 | ||
|
|
01173879a0 | ||
|
|
6f99ee5c34 | ||
|
|
8d1bccbea0 | ||
|
|
b6c278f8e4 | ||
|
|
5a9f59913e | ||
|
|
bf493216a2 | ||
|
|
d37d0ef9af | ||
|
|
c7a6e74dd9 | ||
|
|
24570b791a | ||
|
|
f99853529e | ||
|
|
159f37474d | ||
|
|
1b63f5efde | ||
|
|
c3ba8173d7 | ||
|
|
7a89c1cc6d | ||
|
|
c5b47e88ac | ||
|
|
dc3c6a5d42 | ||
|
|
a9c2ec6ba0 | ||
|
|
f166b9efc5 | ||
|
|
0441b83f74 | ||
|
|
90c82a6a02 | ||
|
|
da25905b73 | ||
|
|
3c07a938cd | ||
|
|
55ccacc442 | ||
|
|
946a11f03d | ||
|
|
93f3a49396 | ||
|
|
3eed100b8d | ||
|
|
eb136ae1bf | ||
|
|
006d0a2643 | ||
|
|
7959bdf5ac | ||
|
|
6f9ee0d9ba | ||
|
|
d901d5f5e4 | ||
|
|
b2c7706a2e | ||
|
|
4d926cf841 | ||
|
|
0314a1b709 | ||
|
|
8a5b69e86c | ||
|
|
ce5250b9d8 | ||
|
|
f51c791490 | ||
|
|
b75305a082 | ||
|
|
8d80fd5614 | ||
|
|
bad6c913fc | ||
|
|
85d85540e7 | ||
|
|
80f1cfd21b | ||
|
|
729d7ed3aa | ||
|
|
0a89150fab | ||
|
|
6fc33e40bb | ||
|
|
7f6592a6b7 | ||
|
|
b9cdbcc6fa | ||
|
|
2a78cdba48 | ||
|
|
b02662c36e | ||
|
|
f44f463e9d | ||
|
|
757bb118ce | ||
|
|
5417ffb999 | ||
|
|
4de979bc33 | ||
|
|
875f56586e | ||
|
|
249b712648 | ||
|
|
58cefae839 | ||
|
|
d9c5ab5fa8 | ||
|
|
6d99ed07f0 | ||
|
|
e55ed9f2b4 | ||
|
|
bb0bfcc5c8 | ||
|
|
b24de43fe2 | ||
|
|
446560d9e8 | ||
|
|
148e46f043 | ||
|
|
e8f20dabd3 | ||
|
|
96ed8b0f98 | ||
|
|
c663230c1b | ||
|
|
0a8118367d | ||
|
|
f932f560bd | ||
|
|
f9542b90db | ||
|
|
014495febd | ||
|
|
82f11c421f | ||
|
|
9059618d1f | ||
|
|
9a8f8fba05 | ||
|
|
3ba89edf7d | ||
|
|
fea6de3bf9 | ||
|
|
2a644f2f0c | ||
|
|
f189ae11b0 | ||
|
|
2e9f8f6d03 | ||
|
|
860934de06 | ||
|
|
792440a71d | ||
|
|
1aacc0e967 | ||
|
|
d4b0c8cbbd | ||
|
|
d6526f12fb | ||
|
|
d3af98cd17 | ||
|
|
e33eb6a928 | ||
|
|
d1be152983 | ||
|
|
548a77833a | ||
|
|
5ba0a7492a | ||
|
|
c65f11b308 | ||
|
|
77b83cae2a | ||
|
|
f609c22be8 | ||
|
|
670854e9d8 | ||
|
|
2bb7ba03cd | ||
|
|
686be484fc | ||
|
|
60de3ce5b0 | ||
|
|
b6fe47efe1 | ||
|
|
e5f16812b3 | ||
|
|
3eb933400a | ||
|
|
58a479be9b | ||
|
|
f835a72151 | ||
|
|
50fa81d191 | ||
|
|
93e75e0111 | ||
|
|
cbcdebf33e | ||
|
|
7c842efd52 | ||
|
|
60daa082f7 |
12
.flowconfig
Normal file
12
.flowconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
[ignore]
|
||||
.*/node_modules/.*
|
||||
.*/dist/.*
|
||||
.*/build/.*
|
||||
|
||||
[include]
|
||||
./src/
|
||||
|
||||
[libs]
|
||||
./declarations/
|
||||
|
||||
[options]
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,6 +1,15 @@
|
||||
/node_modules/
|
||||
node_modules
|
||||
bower_components
|
||||
build
|
||||
build_test
|
||||
.directory
|
||||
.c9
|
||||
.codio
|
||||
.settings
|
||||
.settings
|
||||
.jshintignore
|
||||
.jshintrc
|
||||
.validate.json
|
||||
/y.js
|
||||
/y.js.map
|
||||
/y-*
|
||||
.vscode
|
||||
jsconfig.json
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "dist"]
|
||||
path = dist
|
||||
url = https://github.com/y-js/yjs.git
|
||||
branch = dist
|
||||
@@ -1,6 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Kevin Jahns <kevin.jahns@rwth-aachen.de>.
|
||||
Copyright (c) 2014
|
||||
- Kevin Jahns <kevin.jahns@rwth-aachen.de>.
|
||||
- Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
227
README.md
227
README.md
@@ -1,39 +1,57 @@
|
||||
|
||||
# 
|
||||
[](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/)
|
||||
# 
|
||||
|
||||
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/).
|
||||
|
||||
Y is a framework for optimistic concurrency control and automatic conflict resolution on arbitrary data types. The framework implements a new OT-like concurrency algorithm and provides similar functionality as [ShareJs] and [OpenCoweb]. Y was designed to take away the pain from concurrently editing complex data types like Text, Json, and XML. For more information you should check out the [website](https://dadamonad.github.io/yjs/)!
|
||||
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
|
||||
|
||||
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
|
||||
* Text
|
||||
* Json
|
||||
* XML
|
||||
| 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/)|
|
||||
|
||||
Unlike other frameworks, Y supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Y is extremely scalable and can be used in a wide range of application scenarios.
|
||||
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 find a bunch of Connectors in the [Y-Connectors](https://github.com/rwth-acis/y-connectors) repository. Currently supported communication protocols:
|
||||
* [XMPP-Connector](http://xmpp.org) - Propagates updates in a XMPP multi-user-chat room
|
||||
* [WebRTC-Connector](http://peerjs.com/) - Propagate updates directly with WebRTC
|
||||
* [IWC-Connector](http://dbis.rwth-aachen.de/cms/projects/the-xmpp-experience#interwidget-communication) - Inter-widget Communication
|
||||
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:
|
||||
|
||||
You can use Y client-, and server- side. You can get it as via npm, and bower. We even provide a polymer element for Y!
|
||||
|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|
|
||||
|
||||
The theoretical advantages over similar frameworks are support for
|
||||
You are not limited to use a specific database to store the shared data. We provide the following database adapters:
|
||||
|
||||
|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: Only relevant changes are propagated on rejoin (unimplemented)
|
||||
* .. AnyUndo: Undo *any* action that was executed in constant time (unimplemented)
|
||||
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline.
|
||||
* .. 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 find a tutorial, examples, and documentation on the [website](https://dadamonad.github.io/yjs/).
|
||||
|
||||
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 yjs
|
||||
bower install yjs --save
|
||||
```
|
||||
Then you include the libraries directly from the installation folder.
|
||||
```
|
||||
@@ -50,28 +68,157 @@ And use it like this with *npm*:
|
||||
Y = require("yjs");
|
||||
```
|
||||
|
||||
## Status
|
||||
yjs is still in an early development phase. Don't expect that everything is working fine.
|
||||
But I would become really motivated if you gave me some feedback :) ([github](https://github.com/rwth-acis/yjs/issues)).
|
||||
# Text editing example
|
||||
```
|
||||
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'))
|
||||
}
|
||||
```
|
||||
|
||||
### Current Issues
|
||||
* The History Buffer should be able to store operations in a database
|
||||
* Documentation
|
||||
* Reimplement support for XML as a data type
|
||||
* Custom data types
|
||||
# Api
|
||||
|
||||
## Support
|
||||
Please report _any_ issues to the [Github issue page](https://github.com/rwth-acis/yjs/issues)!
|
||||
I would appreciate if developers give me feedback on how _convenient_ the framework is, and if it is easy to use. Particularly the XML-support may not support every DOM-methods - if you encounter a method that does not cause any change on other peers, please state function name, and sample parameters. However, there are browser-specific features, that Y won't support.
|
||||
### 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 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.*
|
||||
|
||||
### Instantiated Y object (y)
|
||||
`Y(options)` returns a promise that is fulfilled when..
|
||||
|
||||
* 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`
|
||||
|
||||
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 [](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
|
||||
|
||||
### 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
|
||||
* Y.Map exchanges `.val(name [, value])` in favor of `.set(name, value)` and `.get(name)`
|
||||
* Y.Map `.get(name)` returns a promise, if the value is a custom type
|
||||
* 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
|
||||
I created this framework during my bachelor thesis at the chair of computer science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since December 2014 I'm working on Yjs as a part of my student worker job at the i5.
|
||||
|
||||
## License
|
||||
yjs is licensed under the [MIT License](./LICENSE.txt).
|
||||
Yjs is licensed under the [MIT License](./LICENSE.txt).
|
||||
|
||||
<yjs@dbis.rwth-aachen.de>
|
||||
|
||||
[ShareJs]: https://github.com/share/ShareJS
|
||||
[OpenCoweb]: https://github.com/opencoweb/coweb
|
||||
|
||||
<kevin.jahns@rwth-aachen.de>
|
||||
|
||||
|
||||
|
||||
[OpenCoweb]: https://github.com/opencoweb/coweb/wiki
|
||||
|
||||
|
||||
33
bower.json
33
bower.json
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "0.3.0",
|
||||
"homepage": "https://github.com/DadaMonad/yjs",
|
||||
"authors": [
|
||||
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
||||
],
|
||||
"description": "A Framework that enables Real-Time collaboration on arbitrary data structures.",
|
||||
"main": [
|
||||
"./y.js",
|
||||
"./y-object.html",
|
||||
"./build/node/y.js"
|
||||
],
|
||||
"keywords": [
|
||||
"OT",
|
||||
"collaboration",
|
||||
"synchronization",
|
||||
"ShareJS",
|
||||
"Coweb",
|
||||
"concurrency"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"extras",
|
||||
"test"
|
||||
],
|
||||
"dependencies": {
|
||||
"polymer": "Polymer/polymer#~0.5.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
2068
build/browser/y.js
2068
build/browser/y.js
File diff suppressed because one or more lines are too long
@@ -1,68 +0,0 @@
|
||||
var adaptConnector;
|
||||
|
||||
adaptConnector = function(connector, engine, HB, execution_listener) {
|
||||
var applyHB, encode_state_vector, getHB, getStateVector, parse_state_vector, send_;
|
||||
send_ = function(o) {
|
||||
if (o.uid.creator === HB.getUserId() && (typeof o.uid.op_number !== "string")) {
|
||||
return connector.broadcast(o);
|
||||
}
|
||||
};
|
||||
if (connector.invokeSync != null) {
|
||||
HB.setInvokeSyncHandler(connector.invokeSync);
|
||||
}
|
||||
execution_listener.push(send_);
|
||||
encode_state_vector = function(v) {
|
||||
var name, value, _results;
|
||||
_results = [];
|
||||
for (name in v) {
|
||||
value = v[name];
|
||||
_results.push({
|
||||
user: name,
|
||||
state: value
|
||||
});
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
parse_state_vector = function(v) {
|
||||
var s, state_vector, _i, _len;
|
||||
state_vector = {};
|
||||
for (_i = 0, _len = v.length; _i < _len; _i++) {
|
||||
s = v[_i];
|
||||
state_vector[s.user] = s.state;
|
||||
}
|
||||
return state_vector;
|
||||
};
|
||||
getStateVector = function() {
|
||||
return encode_state_vector(HB.getOperationCounter());
|
||||
};
|
||||
getHB = function(v) {
|
||||
var hb, json, o, state_vector, _i, _len;
|
||||
state_vector = parse_state_vector(v);
|
||||
hb = HB._encode(state_vector);
|
||||
for (_i = 0, _len = hb.length; _i < _len; _i++) {
|
||||
o = hb[_i];
|
||||
o.fromHB = "true";
|
||||
}
|
||||
json = {
|
||||
hb: hb,
|
||||
state_vector: encode_state_vector(HB.getOperationCounter())
|
||||
};
|
||||
return json;
|
||||
};
|
||||
applyHB = function(hb) {
|
||||
return engine.applyOp(hb);
|
||||
};
|
||||
connector.getStateVector = getStateVector;
|
||||
connector.getHB = getHB;
|
||||
connector.applyHB = applyHB;
|
||||
connector.whenReceiving(function(sender, op) {
|
||||
if (op.uid.creator !== HB.getUserId()) {
|
||||
return engine.applyOp(op);
|
||||
}
|
||||
});
|
||||
if (connector._whenBoundToY != null) {
|
||||
return connector._whenBoundToY();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = adaptConnector;
|
||||
@@ -1,113 +0,0 @@
|
||||
var Engine;
|
||||
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
window.unprocessed_counter = 0;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
window.unprocessed_exec_counter = 0;
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
window.unprocessed_types = [];
|
||||
}
|
||||
|
||||
Engine = (function() {
|
||||
function Engine(HB, types) {
|
||||
this.HB = HB;
|
||||
this.types = types;
|
||||
this.unprocessed_ops = [];
|
||||
}
|
||||
|
||||
Engine.prototype.parseOperation = function(json) {
|
||||
var type;
|
||||
type = this.types[json.type];
|
||||
if ((type != null ? type.parse : void 0) != null) {
|
||||
return type.parse(json);
|
||||
} else {
|
||||
throw new Error("You forgot to specify a parser for type " + json.type + ". The message is " + (JSON.stringify(json)) + ".");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
applyOpsBundle: (ops_json)->
|
||||
ops = []
|
||||
for o in ops_json
|
||||
ops.push @parseOperation o
|
||||
for o in ops
|
||||
if not o.execute()
|
||||
@unprocessed_ops.push o
|
||||
@tryUnprocessed()
|
||||
*/
|
||||
|
||||
Engine.prototype.applyOpsCheckDouble = function(ops_json) {
|
||||
var o, _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = ops_json.length; _i < _len; _i++) {
|
||||
o = ops_json[_i];
|
||||
if (this.HB.getOperation(o.uid) == null) {
|
||||
_results.push(this.applyOp(o));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Engine.prototype.applyOps = function(ops_json) {
|
||||
return this.applyOp(ops_json);
|
||||
};
|
||||
|
||||
Engine.prototype.applyOp = function(op_json_array) {
|
||||
var o, op_json, _i, _len;
|
||||
if (op_json_array.constructor !== Array) {
|
||||
op_json_array = [op_json_array];
|
||||
}
|
||||
for (_i = 0, _len = op_json_array.length; _i < _len; _i++) {
|
||||
op_json = op_json_array[_i];
|
||||
o = this.parseOperation(op_json);
|
||||
if (op_json.fromHB != null) {
|
||||
o.fromHB = op_json.fromHB;
|
||||
}
|
||||
if (this.HB.getOperation(o) != null) {
|
||||
|
||||
} else if (((!this.HB.isExpectedOperation(o)) && (o.fromHB == null)) || (!o.execute())) {
|
||||
this.unprocessed_ops.push(o);
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
window.unprocessed_types.push(o.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.tryUnprocessed();
|
||||
};
|
||||
|
||||
Engine.prototype.tryUnprocessed = function() {
|
||||
var old_length, op, unprocessed, _i, _len, _ref;
|
||||
while (true) {
|
||||
old_length = this.unprocessed_ops.length;
|
||||
unprocessed = [];
|
||||
_ref = this.unprocessed_ops;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
op = _ref[_i];
|
||||
if (this.HB.getOperation(op) != null) {
|
||||
|
||||
} else if ((!this.HB.isExpectedOperation(op) && (op.fromHB == null)) || (!op.execute())) {
|
||||
unprocessed.push(op);
|
||||
}
|
||||
}
|
||||
this.unprocessed_ops = unprocessed;
|
||||
if (this.unprocessed_ops.length === old_length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this.unprocessed_ops.length !== 0) {
|
||||
return this.HB.invokeSync();
|
||||
}
|
||||
};
|
||||
|
||||
return Engine;
|
||||
|
||||
})();
|
||||
|
||||
module.exports = Engine;
|
||||
@@ -1,250 +0,0 @@
|
||||
var HistoryBuffer,
|
||||
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
HistoryBuffer = (function() {
|
||||
function HistoryBuffer(user_id) {
|
||||
this.user_id = user_id;
|
||||
this.emptyGarbage = __bind(this.emptyGarbage, this);
|
||||
this.operation_counter = {};
|
||||
this.buffer = {};
|
||||
this.change_listeners = [];
|
||||
this.garbage = [];
|
||||
this.trash = [];
|
||||
this.performGarbageCollection = true;
|
||||
this.garbageCollectTimeout = 30000;
|
||||
this.reserved_identifier_counter = 0;
|
||||
setTimeout(this.emptyGarbage, this.garbageCollectTimeout);
|
||||
}
|
||||
|
||||
HistoryBuffer.prototype.resetUserId = function(id) {
|
||||
var o, o_name, own;
|
||||
own = this.buffer[this.user_id];
|
||||
if (own != null) {
|
||||
for (o_name in own) {
|
||||
o = own[o_name];
|
||||
o.uid.creator = id;
|
||||
}
|
||||
if (this.buffer[id] != null) {
|
||||
throw new Error("You are re-assigning an old user id - this is not (yet) possible!");
|
||||
}
|
||||
this.buffer[id] = own;
|
||||
delete this.buffer[this.user_id];
|
||||
}
|
||||
this.operation_counter[id] = this.operation_counter[this.user_id];
|
||||
delete this.operation_counter[this.user_id];
|
||||
return this.user_id = id;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.emptyGarbage = function() {
|
||||
var o, _i, _len, _ref;
|
||||
_ref = this.garbage;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
o = _ref[_i];
|
||||
if (typeof o.cleanup === "function") {
|
||||
o.cleanup();
|
||||
}
|
||||
}
|
||||
this.garbage = this.trash;
|
||||
this.trash = [];
|
||||
if (this.garbageCollectTimeout !== -1) {
|
||||
this.garbageCollectTimeoutId = setTimeout(this.emptyGarbage, this.garbageCollectTimeout);
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.getUserId = function() {
|
||||
return this.user_id;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.addToGarbageCollector = function() {
|
||||
var o, _i, _len, _results;
|
||||
if (this.performGarbageCollection) {
|
||||
_results = [];
|
||||
for (_i = 0, _len = arguments.length; _i < _len; _i++) {
|
||||
o = arguments[_i];
|
||||
if (o != null) {
|
||||
_results.push(this.garbage.push(o));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
}
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.stopGarbageCollection = function() {
|
||||
this.performGarbageCollection = false;
|
||||
this.setManualGarbageCollect();
|
||||
this.garbage = [];
|
||||
return this.trash = [];
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.setManualGarbageCollect = function() {
|
||||
this.garbageCollectTimeout = -1;
|
||||
clearTimeout(this.garbageCollectTimeoutId);
|
||||
return this.garbageCollectTimeoutId = void 0;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.setGarbageCollectTimeout = function(garbageCollectTimeout) {
|
||||
this.garbageCollectTimeout = garbageCollectTimeout;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.getReservedUniqueIdentifier = function() {
|
||||
return {
|
||||
creator: '_',
|
||||
op_number: "_" + (this.reserved_identifier_counter++),
|
||||
doSync: false
|
||||
};
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.getOperationCounter = function(user_id) {
|
||||
var ctn, res, user, _ref;
|
||||
if (user_id == null) {
|
||||
res = {};
|
||||
_ref = this.operation_counter;
|
||||
for (user in _ref) {
|
||||
ctn = _ref[user];
|
||||
res[user] = ctn;
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
return this.operation_counter[user_id];
|
||||
}
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.isExpectedOperation = function(o) {
|
||||
var _base, _name;
|
||||
if ((_base = this.operation_counter)[_name = o.uid.creator] == null) {
|
||||
_base[_name] = 0;
|
||||
}
|
||||
o.uid.op_number <= this.operation_counter[o.uid.creator];
|
||||
return true;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype._encode = function(state_vector) {
|
||||
var json, o, o_json, o_next, o_number, o_prev, u_name, unknown, user, _ref;
|
||||
if (state_vector == null) {
|
||||
state_vector = {};
|
||||
}
|
||||
json = [];
|
||||
unknown = function(user, o_number) {
|
||||
if ((user == null) || (o_number == null)) {
|
||||
throw new Error("dah!");
|
||||
}
|
||||
return (state_vector[user] == null) || state_vector[user] <= o_number;
|
||||
};
|
||||
_ref = this.buffer;
|
||||
for (u_name in _ref) {
|
||||
user = _ref[u_name];
|
||||
for (o_number in user) {
|
||||
o = user[o_number];
|
||||
if (o.uid.doSync && unknown(u_name, o_number)) {
|
||||
o_json = o._encode();
|
||||
if (o.next_cl != null) {
|
||||
o_next = o.next_cl;
|
||||
while ((o_next.next_cl != null) && unknown(o_next.uid.creator, o_next.uid.op_number)) {
|
||||
o_next = o_next.next_cl;
|
||||
}
|
||||
o_json.next = o_next.getUid();
|
||||
} else if (o.prev_cl != null) {
|
||||
o_prev = o.prev_cl;
|
||||
while ((o_prev.prev_cl != null) && unknown(o_prev.uid.creator, o_prev.uid.op_number)) {
|
||||
o_prev = o_prev.prev_cl;
|
||||
}
|
||||
o_json.prev = o_prev.getUid();
|
||||
}
|
||||
json.push(o_json);
|
||||
}
|
||||
}
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.getNextOperationIdentifier = function(user_id) {
|
||||
var uid;
|
||||
if (user_id == null) {
|
||||
user_id = this.user_id;
|
||||
}
|
||||
if (this.operation_counter[user_id] == null) {
|
||||
this.operation_counter[user_id] = 0;
|
||||
}
|
||||
uid = {
|
||||
'creator': user_id,
|
||||
'op_number': this.operation_counter[user_id],
|
||||
'doSync': true
|
||||
};
|
||||
this.operation_counter[user_id]++;
|
||||
return uid;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.getOperation = function(uid) {
|
||||
var o, _ref;
|
||||
if (uid.uid != null) {
|
||||
uid = uid.uid;
|
||||
}
|
||||
o = (_ref = this.buffer[uid.creator]) != null ? _ref[uid.op_number] : void 0;
|
||||
if ((uid.sub != null) && (o != null)) {
|
||||
return o.retrieveSub(uid.sub);
|
||||
} else {
|
||||
return o;
|
||||
}
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.addOperation = function(o) {
|
||||
if (this.buffer[o.uid.creator] == null) {
|
||||
this.buffer[o.uid.creator] = {};
|
||||
}
|
||||
if (this.buffer[o.uid.creator][o.uid.op_number] != null) {
|
||||
throw new Error("You must not overwrite operations!");
|
||||
}
|
||||
if ((o.uid.op_number.constructor !== String) && (!this.isExpectedOperation(o)) && (o.fromHB == null)) {
|
||||
throw new Error("this operation was not expected!");
|
||||
}
|
||||
this.addToCounter(o);
|
||||
this.buffer[o.uid.creator][o.uid.op_number] = o;
|
||||
return o;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.removeOperation = function(o) {
|
||||
var _ref;
|
||||
return (_ref = this.buffer[o.uid.creator]) != null ? delete _ref[o.uid.op_number] : void 0;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.setInvokeSyncHandler = function(f) {
|
||||
return this.invokeSync = f;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.invokeSync = function() {};
|
||||
|
||||
HistoryBuffer.prototype.renewStateVector = function(state_vector) {
|
||||
var state, user, _results;
|
||||
_results = [];
|
||||
for (user in state_vector) {
|
||||
state = state_vector[user];
|
||||
if ((this.operation_counter[user] == null) || (this.operation_counter[user] < state_vector[user])) {
|
||||
_results.push(this.operation_counter[user] = state_vector[user]);
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.addToCounter = function(o) {
|
||||
if (this.operation_counter[o.uid.creator] == null) {
|
||||
this.operation_counter[o.uid.creator] = 0;
|
||||
}
|
||||
if (typeof o.uid.op_number === 'number' && o.uid.creator !== this.getUserId()) {
|
||||
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {
|
||||
return this.operation_counter[o.uid.creator]++;
|
||||
} else {
|
||||
return this.invokeSync(o.uid.creator);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return HistoryBuffer;
|
||||
|
||||
})();
|
||||
|
||||
module.exports = HistoryBuffer;
|
||||
@@ -1,487 +0,0 @@
|
||||
var __slice = [].slice,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
|
||||
module.exports = function(HB) {
|
||||
var execution_listener, types;
|
||||
types = {};
|
||||
execution_listener = [];
|
||||
types.Operation = (function() {
|
||||
function Operation(uid) {
|
||||
this.is_deleted = false;
|
||||
this.garbage_collected = false;
|
||||
this.event_listeners = [];
|
||||
if (uid != null) {
|
||||
this.uid = uid;
|
||||
}
|
||||
}
|
||||
|
||||
Operation.prototype.type = "Operation";
|
||||
|
||||
Operation.prototype.retrieveSub = function() {
|
||||
throw new Error("sub properties are not enable on this operation type!");
|
||||
};
|
||||
|
||||
Operation.prototype.observe = function(f) {
|
||||
return this.event_listeners.push(f);
|
||||
};
|
||||
|
||||
Operation.prototype.unobserve = function(f) {
|
||||
return this.event_listeners = this.event_listeners.filter(function(g) {
|
||||
return f !== g;
|
||||
});
|
||||
};
|
||||
|
||||
Operation.prototype.deleteAllObservers = function() {
|
||||
return this.event_listeners = [];
|
||||
};
|
||||
|
||||
Operation.prototype["delete"] = function() {
|
||||
(new types.Delete(void 0, this)).execute();
|
||||
return null;
|
||||
};
|
||||
|
||||
Operation.prototype.callEvent = function() {
|
||||
return this.forwardEvent.apply(this, [this].concat(__slice.call(arguments)));
|
||||
};
|
||||
|
||||
Operation.prototype.forwardEvent = function() {
|
||||
var args, f, op, _i, _len, _ref, _results;
|
||||
op = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
|
||||
_ref = this.event_listeners;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
f = _ref[_i];
|
||||
_results.push(f.call.apply(f, [op].concat(__slice.call(args))));
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Operation.prototype.isDeleted = function() {
|
||||
return this.is_deleted;
|
||||
};
|
||||
|
||||
Operation.prototype.applyDelete = function(garbagecollect) {
|
||||
if (garbagecollect == null) {
|
||||
garbagecollect = true;
|
||||
}
|
||||
if (!this.garbage_collected) {
|
||||
this.is_deleted = true;
|
||||
if (garbagecollect) {
|
||||
this.garbage_collected = true;
|
||||
return HB.addToGarbageCollector(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.cleanup = function() {
|
||||
HB.removeOperation(this);
|
||||
return this.deleteAllObservers();
|
||||
};
|
||||
|
||||
Operation.prototype.setParent = function(parent) {
|
||||
this.parent = parent;
|
||||
};
|
||||
|
||||
Operation.prototype.getParent = function() {
|
||||
return this.parent;
|
||||
};
|
||||
|
||||
Operation.prototype.getUid = function() {
|
||||
if (this.uid.noOperation == null) {
|
||||
return this.uid;
|
||||
} else {
|
||||
return this.uid.alt;
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.cloneUid = function() {
|
||||
var n, uid, v, _ref;
|
||||
uid = {};
|
||||
_ref = this.getUid();
|
||||
for (n in _ref) {
|
||||
v = _ref[n];
|
||||
uid[n] = v;
|
||||
}
|
||||
return uid;
|
||||
};
|
||||
|
||||
Operation.prototype.dontSync = function() {
|
||||
return this.uid.doSync = false;
|
||||
};
|
||||
|
||||
Operation.prototype.execute = function() {
|
||||
var l, _i, _len;
|
||||
this.is_executed = true;
|
||||
if (this.uid == null) {
|
||||
this.uid = HB.getNextOperationIdentifier();
|
||||
}
|
||||
if (this.uid.noOperation == null) {
|
||||
HB.addOperation(this);
|
||||
for (_i = 0, _len = execution_listener.length; _i < _len; _i++) {
|
||||
l = execution_listener[_i];
|
||||
l(this._encode());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Operation.prototype.saveOperation = function(name, op) {
|
||||
if ((op != null ? op.execute : void 0) != null) {
|
||||
return this[name] = op;
|
||||
} else if (op != null) {
|
||||
if (this.unchecked == null) {
|
||||
this.unchecked = {};
|
||||
}
|
||||
return this.unchecked[name] = op;
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.validateSavedOperations = function() {
|
||||
var name, op, op_uid, success, uninstantiated, _ref;
|
||||
uninstantiated = {};
|
||||
success = this;
|
||||
_ref = this.unchecked;
|
||||
for (name in _ref) {
|
||||
op_uid = _ref[name];
|
||||
op = HB.getOperation(op_uid);
|
||||
if (op) {
|
||||
this[name] = op;
|
||||
} else {
|
||||
uninstantiated[name] = op_uid;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
delete this.unchecked;
|
||||
if (!success) {
|
||||
this.unchecked = uninstantiated;
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
return Operation;
|
||||
|
||||
})();
|
||||
types.Delete = (function(_super) {
|
||||
__extends(Delete, _super);
|
||||
|
||||
function Delete(uid, deletes) {
|
||||
this.saveOperation('deletes', deletes);
|
||||
Delete.__super__.constructor.call(this, uid);
|
||||
}
|
||||
|
||||
Delete.prototype.type = "Delete";
|
||||
|
||||
Delete.prototype._encode = function() {
|
||||
return {
|
||||
'type': "Delete",
|
||||
'uid': this.getUid(),
|
||||
'deletes': this.deletes.getUid()
|
||||
};
|
||||
};
|
||||
|
||||
Delete.prototype.execute = function() {
|
||||
var res;
|
||||
if (this.validateSavedOperations()) {
|
||||
res = Delete.__super__.execute.apply(this, arguments);
|
||||
if (res) {
|
||||
this.deletes.applyDelete(this);
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return Delete;
|
||||
|
||||
})(types.Operation);
|
||||
types.Delete.parse = function(o) {
|
||||
var deletes_uid, uid;
|
||||
uid = o['uid'], deletes_uid = o['deletes'];
|
||||
return new this(uid, deletes_uid);
|
||||
};
|
||||
types.Insert = (function(_super) {
|
||||
__extends(Insert, _super);
|
||||
|
||||
function Insert(uid, prev_cl, next_cl, origin, parent) {
|
||||
this.saveOperation('parent', parent);
|
||||
this.saveOperation('prev_cl', prev_cl);
|
||||
this.saveOperation('next_cl', next_cl);
|
||||
if (origin != null) {
|
||||
this.saveOperation('origin', origin);
|
||||
} else {
|
||||
this.saveOperation('origin', prev_cl);
|
||||
}
|
||||
Insert.__super__.constructor.call(this, uid);
|
||||
}
|
||||
|
||||
Insert.prototype.type = "Insert";
|
||||
|
||||
Insert.prototype.applyDelete = function(o) {
|
||||
var callLater, garbagecollect, _ref;
|
||||
if (this.deleted_by == null) {
|
||||
this.deleted_by = [];
|
||||
}
|
||||
callLater = false;
|
||||
if ((this.parent != null) && !this.isDeleted() && (o != null)) {
|
||||
callLater = true;
|
||||
}
|
||||
if (o != null) {
|
||||
this.deleted_by.push(o);
|
||||
}
|
||||
garbagecollect = false;
|
||||
if (this.next_cl.isDeleted()) {
|
||||
garbagecollect = true;
|
||||
}
|
||||
Insert.__super__.applyDelete.call(this, garbagecollect);
|
||||
if (callLater) {
|
||||
this.callOperationSpecificDeleteEvents(o);
|
||||
}
|
||||
if ((_ref = this.prev_cl) != null ? _ref.isDeleted() : void 0) {
|
||||
return this.prev_cl.applyDelete();
|
||||
}
|
||||
};
|
||||
|
||||
Insert.prototype.cleanup = function() {
|
||||
var d, o, _i, _len, _ref;
|
||||
if (this.next_cl.isDeleted()) {
|
||||
_ref = this.deleted_by;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
d = _ref[_i];
|
||||
d.cleanup();
|
||||
}
|
||||
o = this.next_cl;
|
||||
while (o.type !== "Delimiter") {
|
||||
if (o.origin === this) {
|
||||
o.origin = this.prev_cl;
|
||||
}
|
||||
o = o.next_cl;
|
||||
}
|
||||
this.prev_cl.next_cl = this.next_cl;
|
||||
this.next_cl.prev_cl = this.prev_cl;
|
||||
return Insert.__super__.cleanup.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Insert.prototype.getDistanceToOrigin = function() {
|
||||
var d, o;
|
||||
d = 0;
|
||||
o = this.prev_cl;
|
||||
while (true) {
|
||||
if (this.origin === o) {
|
||||
break;
|
||||
}
|
||||
d++;
|
||||
o = o.prev_cl;
|
||||
}
|
||||
return d;
|
||||
};
|
||||
|
||||
Insert.prototype.execute = function() {
|
||||
var distance_to_origin, i, o;
|
||||
if (!this.validateSavedOperations()) {
|
||||
return false;
|
||||
} else {
|
||||
if (this.parent != null) {
|
||||
if (this.prev_cl == null) {
|
||||
this.prev_cl = this.parent.beginning;
|
||||
}
|
||||
if (this.origin == null) {
|
||||
this.origin = this.parent.beginning;
|
||||
}
|
||||
if (this.next_cl == null) {
|
||||
this.next_cl = this.parent.end;
|
||||
}
|
||||
}
|
||||
if (this.prev_cl != null) {
|
||||
distance_to_origin = this.getDistanceToOrigin();
|
||||
o = this.prev_cl.next_cl;
|
||||
i = distance_to_origin;
|
||||
while (true) {
|
||||
if (o !== this.next_cl) {
|
||||
if (o.getDistanceToOrigin() === i) {
|
||||
if (o.uid.creator < this.uid.creator) {
|
||||
this.prev_cl = o;
|
||||
distance_to_origin = i + 1;
|
||||
} else {
|
||||
|
||||
}
|
||||
} else if (o.getDistanceToOrigin() < i) {
|
||||
if (i - distance_to_origin <= o.getDistanceToOrigin()) {
|
||||
this.prev_cl = o;
|
||||
distance_to_origin = i + 1;
|
||||
} else {
|
||||
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
o = o.next_cl;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.next_cl = this.prev_cl.next_cl;
|
||||
this.prev_cl.next_cl = this;
|
||||
this.next_cl.prev_cl = this;
|
||||
}
|
||||
this.setParent(this.prev_cl.getParent());
|
||||
Insert.__super__.execute.apply(this, arguments);
|
||||
this.callOperationSpecificInsertEvents();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
Insert.prototype.callOperationSpecificInsertEvents = function() {
|
||||
var _ref;
|
||||
return (_ref = this.parent) != null ? _ref.callEvent([
|
||||
{
|
||||
type: "insert",
|
||||
position: this.getPosition(),
|
||||
object: this.parent,
|
||||
changedBy: this.uid.creator,
|
||||
value: this.content
|
||||
}
|
||||
]) : void 0;
|
||||
};
|
||||
|
||||
Insert.prototype.callOperationSpecificDeleteEvents = function(o) {
|
||||
return this.parent.callEvent([
|
||||
{
|
||||
type: "delete",
|
||||
position: this.getPosition(),
|
||||
object: this.parent,
|
||||
length: 1,
|
||||
changedBy: o.uid.creator
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
Insert.prototype.getPosition = function() {
|
||||
var position, prev;
|
||||
position = 0;
|
||||
prev = this.prev_cl;
|
||||
while (true) {
|
||||
if (prev instanceof types.Delimiter) {
|
||||
break;
|
||||
}
|
||||
if (!prev.isDeleted()) {
|
||||
position++;
|
||||
}
|
||||
prev = prev.prev_cl;
|
||||
}
|
||||
return position;
|
||||
};
|
||||
|
||||
return Insert;
|
||||
|
||||
})(types.Operation);
|
||||
types.ImmutableObject = (function(_super) {
|
||||
__extends(ImmutableObject, _super);
|
||||
|
||||
function ImmutableObject(uid, content) {
|
||||
this.content = content;
|
||||
ImmutableObject.__super__.constructor.call(this, uid);
|
||||
}
|
||||
|
||||
ImmutableObject.prototype.type = "ImmutableObject";
|
||||
|
||||
ImmutableObject.prototype.val = function() {
|
||||
return this.content;
|
||||
};
|
||||
|
||||
ImmutableObject.prototype._encode = function() {
|
||||
var json;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid(),
|
||||
'content': this.content
|
||||
};
|
||||
return json;
|
||||
};
|
||||
|
||||
return ImmutableObject;
|
||||
|
||||
})(types.Operation);
|
||||
types.ImmutableObject.parse = function(json) {
|
||||
var content, uid;
|
||||
uid = json['uid'], content = json['content'];
|
||||
return new this(uid, content);
|
||||
};
|
||||
types.Delimiter = (function(_super) {
|
||||
__extends(Delimiter, _super);
|
||||
|
||||
function Delimiter(prev_cl, next_cl, origin) {
|
||||
this.saveOperation('prev_cl', prev_cl);
|
||||
this.saveOperation('next_cl', next_cl);
|
||||
this.saveOperation('origin', prev_cl);
|
||||
Delimiter.__super__.constructor.call(this, {
|
||||
noOperation: true
|
||||
});
|
||||
}
|
||||
|
||||
Delimiter.prototype.type = "Delimiter";
|
||||
|
||||
Delimiter.prototype.applyDelete = function() {
|
||||
var o;
|
||||
Delimiter.__super__.applyDelete.call(this);
|
||||
o = this.prev_cl;
|
||||
while (o != null) {
|
||||
o.applyDelete();
|
||||
o = o.prev_cl;
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
Delimiter.prototype.cleanup = function() {
|
||||
return Delimiter.__super__.cleanup.call(this);
|
||||
};
|
||||
|
||||
Delimiter.prototype.execute = function() {
|
||||
var _ref, _ref1;
|
||||
if (((_ref = this.unchecked) != null ? _ref['next_cl'] : void 0) != null) {
|
||||
return Delimiter.__super__.execute.apply(this, arguments);
|
||||
} else if ((_ref1 = this.unchecked) != null ? _ref1['prev_cl'] : void 0) {
|
||||
if (this.validateSavedOperations()) {
|
||||
if (this.prev_cl.next_cl != null) {
|
||||
throw new Error("Probably duplicated operations");
|
||||
}
|
||||
this.prev_cl.next_cl = this;
|
||||
return Delimiter.__super__.execute.apply(this, arguments);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if ((this.prev_cl != null) && (this.prev_cl.next_cl == null)) {
|
||||
delete this.prev_cl.unchecked.next_cl;
|
||||
this.prev_cl.next_cl = this;
|
||||
return Delimiter.__super__.execute.apply(this, arguments);
|
||||
} else if ((this.prev_cl != null) || (this.next_cl != null) || true) {
|
||||
return Delimiter.__super__.execute.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Delimiter.prototype._encode = function() {
|
||||
var _ref, _ref1;
|
||||
return {
|
||||
'type': this.type,
|
||||
'uid': this.getUid(),
|
||||
'prev': (_ref = this.prev_cl) != null ? _ref.getUid() : void 0,
|
||||
'next': (_ref1 = this.next_cl) != null ? _ref1.getUid() : void 0
|
||||
};
|
||||
};
|
||||
|
||||
return Delimiter;
|
||||
|
||||
})(types.Operation);
|
||||
types.Delimiter.parse = function(json) {
|
||||
var next, prev, uid;
|
||||
uid = json['uid'], prev = json['prev'], next = json['next'];
|
||||
return new this(uid, prev, next);
|
||||
};
|
||||
return {
|
||||
'types': types,
|
||||
'execution_listener': execution_listener
|
||||
};
|
||||
};
|
||||
@@ -1,158 +0,0 @@
|
||||
var text_types_uninitialized,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
|
||||
text_types_uninitialized = require("./TextTypes");
|
||||
|
||||
module.exports = function(HB) {
|
||||
var text_types, types;
|
||||
text_types = text_types_uninitialized(HB);
|
||||
types = text_types.types;
|
||||
types.Object = (function(_super) {
|
||||
__extends(Object, _super);
|
||||
|
||||
function Object() {
|
||||
return Object.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Object.prototype.type = "Object";
|
||||
|
||||
Object.prototype.applyDelete = function() {
|
||||
return Object.__super__.applyDelete.call(this);
|
||||
};
|
||||
|
||||
Object.prototype.cleanup = function() {
|
||||
return Object.__super__.cleanup.call(this);
|
||||
};
|
||||
|
||||
Object.prototype.toJson = function(transform_to_value) {
|
||||
var json, name, o, that, val;
|
||||
if (transform_to_value == null) {
|
||||
transform_to_value = false;
|
||||
}
|
||||
if ((this.bound_json == null) || (Object.observe == null) || true) {
|
||||
val = this.val();
|
||||
json = {};
|
||||
for (name in val) {
|
||||
o = val[name];
|
||||
if (o instanceof types.Object) {
|
||||
json[name] = o.toJson(transform_to_value);
|
||||
} else if (o instanceof types.Array) {
|
||||
json[name] = o.toJson(transform_to_value);
|
||||
} else if (transform_to_value && o instanceof types.Operation) {
|
||||
json[name] = o.val();
|
||||
} else {
|
||||
json[name] = o;
|
||||
}
|
||||
}
|
||||
this.bound_json = json;
|
||||
if (Object.observe != null) {
|
||||
that = this;
|
||||
Object.observe(this.bound_json, function(events) {
|
||||
var event, _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = events.length; _i < _len; _i++) {
|
||||
event = events[_i];
|
||||
if ((event.changedBy == null) && (event.type === "add" || (event.type = "update"))) {
|
||||
_results.push(that.val(event.name, event.object[event.name]));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
});
|
||||
this.observe(function(events) {
|
||||
var event, notifier, oldVal, _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = events.length; _i < _len; _i++) {
|
||||
event = events[_i];
|
||||
if (event.created_ !== HB.getUserId()) {
|
||||
notifier = Object.getNotifier(that.bound_json);
|
||||
oldVal = that.bound_json[event.name];
|
||||
if (oldVal != null) {
|
||||
notifier.performChange('update', function() {
|
||||
return that.bound_json[event.name] = that.val(event.name);
|
||||
}, that.bound_json);
|
||||
_results.push(notifier.notify({
|
||||
object: that.bound_json,
|
||||
type: 'update',
|
||||
name: event.name,
|
||||
oldValue: oldVal,
|
||||
changedBy: event.changedBy
|
||||
}));
|
||||
} else {
|
||||
notifier.performChange('add', function() {
|
||||
return that.bound_json[event.name] = that.val(event.name);
|
||||
}, that.bound_json);
|
||||
_results.push(notifier.notify({
|
||||
object: that.bound_json,
|
||||
type: 'add',
|
||||
name: event.name,
|
||||
oldValue: oldVal,
|
||||
changedBy: event.changedBy
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.bound_json;
|
||||
};
|
||||
|
||||
Object.prototype.val = function(name, content) {
|
||||
var args, i, o, type, _i, _ref;
|
||||
if ((name != null) && arguments.length > 1) {
|
||||
if ((content != null) && (content.constructor != null)) {
|
||||
type = types[content.constructor.name];
|
||||
if ((type != null) && (type.create != null)) {
|
||||
args = [];
|
||||
for (i = _i = 1, _ref = arguments.length; 1 <= _ref ? _i < _ref : _i > _ref; i = 1 <= _ref ? ++_i : --_i) {
|
||||
args.push(arguments[i]);
|
||||
}
|
||||
o = type.create.apply(null, args);
|
||||
return Object.__super__.val.call(this, name, o);
|
||||
} else {
|
||||
throw new Error("The " + content.constructor.name + "-type is not (yet) supported in Y.");
|
||||
}
|
||||
} else {
|
||||
return Object.__super__.val.call(this, name, content);
|
||||
}
|
||||
} else {
|
||||
return Object.__super__.val.call(this, name);
|
||||
}
|
||||
};
|
||||
|
||||
Object.prototype._encode = function() {
|
||||
return {
|
||||
'type': this.type,
|
||||
'uid': this.getUid()
|
||||
};
|
||||
};
|
||||
|
||||
return Object;
|
||||
|
||||
})(types.MapManager);
|
||||
types.Object.parse = function(json) {
|
||||
var uid;
|
||||
uid = json['uid'];
|
||||
return new this(uid);
|
||||
};
|
||||
types.Object.create = function(content, mutable) {
|
||||
var json, n, o;
|
||||
json = new types.Object().execute();
|
||||
for (n in content) {
|
||||
o = content[n];
|
||||
json.val(n, o, mutable);
|
||||
}
|
||||
return json;
|
||||
};
|
||||
types.Number = {};
|
||||
types.Number.create = function(content) {
|
||||
return content;
|
||||
};
|
||||
return text_types;
|
||||
};
|
||||
@@ -1,354 +0,0 @@
|
||||
var basic_types_uninitialized,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
|
||||
basic_types_uninitialized = require("./BasicTypes");
|
||||
|
||||
module.exports = function(HB) {
|
||||
var basic_types, types;
|
||||
basic_types = basic_types_uninitialized(HB);
|
||||
types = basic_types.types;
|
||||
types.MapManager = (function(_super) {
|
||||
__extends(MapManager, _super);
|
||||
|
||||
function MapManager(uid) {
|
||||
this.map = {};
|
||||
MapManager.__super__.constructor.call(this, uid);
|
||||
}
|
||||
|
||||
MapManager.prototype.type = "MapManager";
|
||||
|
||||
MapManager.prototype.applyDelete = function() {
|
||||
var name, p, _ref;
|
||||
_ref = this.map;
|
||||
for (name in _ref) {
|
||||
p = _ref[name];
|
||||
p.applyDelete();
|
||||
}
|
||||
return MapManager.__super__.applyDelete.call(this);
|
||||
};
|
||||
|
||||
MapManager.prototype.cleanup = function() {
|
||||
return MapManager.__super__.cleanup.call(this);
|
||||
};
|
||||
|
||||
MapManager.prototype.val = function(name, content) {
|
||||
var o, prop, result, _ref;
|
||||
if (arguments.length > 1) {
|
||||
this.retrieveSub(name).replace(content);
|
||||
return this;
|
||||
} else if (name != null) {
|
||||
prop = this.map[name];
|
||||
if ((prop != null) && !prop.isContentDeleted()) {
|
||||
return prop.val();
|
||||
} else {
|
||||
return void 0;
|
||||
}
|
||||
} else {
|
||||
result = {};
|
||||
_ref = this.map;
|
||||
for (name in _ref) {
|
||||
o = _ref[name];
|
||||
if (!o.isContentDeleted()) {
|
||||
result[name] = o.val();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
MapManager.prototype["delete"] = function(name) {
|
||||
var _ref;
|
||||
if ((_ref = this.map[name]) != null) {
|
||||
_ref.deleteContent();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
MapManager.prototype.retrieveSub = function(property_name) {
|
||||
var event_properties, event_this, map_uid, rm, rm_uid;
|
||||
if (this.map[property_name] == null) {
|
||||
event_properties = {
|
||||
name: property_name
|
||||
};
|
||||
event_this = this;
|
||||
map_uid = this.cloneUid();
|
||||
map_uid.sub = property_name;
|
||||
rm_uid = {
|
||||
noOperation: true,
|
||||
alt: map_uid
|
||||
};
|
||||
rm = new types.ReplaceManager(event_properties, event_this, rm_uid);
|
||||
this.map[property_name] = rm;
|
||||
rm.setParent(this, property_name);
|
||||
rm.execute();
|
||||
}
|
||||
return this.map[property_name];
|
||||
};
|
||||
|
||||
return MapManager;
|
||||
|
||||
})(types.Operation);
|
||||
types.ListManager = (function(_super) {
|
||||
__extends(ListManager, _super);
|
||||
|
||||
function ListManager(uid) {
|
||||
this.beginning = new types.Delimiter(void 0, void 0);
|
||||
this.end = new types.Delimiter(this.beginning, void 0);
|
||||
this.beginning.next_cl = this.end;
|
||||
this.beginning.execute();
|
||||
this.end.execute();
|
||||
ListManager.__super__.constructor.call(this, uid);
|
||||
}
|
||||
|
||||
ListManager.prototype.type = "ListManager";
|
||||
|
||||
ListManager.prototype.execute = function() {
|
||||
if (this.validateSavedOperations()) {
|
||||
this.beginning.setParent(this);
|
||||
this.end.setParent(this);
|
||||
return ListManager.__super__.execute.apply(this, arguments);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
ListManager.prototype.getLastOperation = function() {
|
||||
return this.end.prev_cl;
|
||||
};
|
||||
|
||||
ListManager.prototype.getFirstOperation = function() {
|
||||
return this.beginning.next_cl;
|
||||
};
|
||||
|
||||
ListManager.prototype.toArray = function() {
|
||||
var o, result;
|
||||
o = this.beginning.next_cl;
|
||||
result = [];
|
||||
while (o !== this.end) {
|
||||
result.push(o);
|
||||
o = o.next_cl;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
ListManager.prototype.getOperationByPosition = function(position) {
|
||||
var o;
|
||||
o = this.beginning;
|
||||
while (true) {
|
||||
if (o instanceof types.Delimiter && (o.prev_cl != null)) {
|
||||
o = o.prev_cl;
|
||||
while (o.isDeleted() || !(o instanceof types.Delimiter)) {
|
||||
o = o.prev_cl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (position <= 0 && !o.isDeleted()) {
|
||||
break;
|
||||
}
|
||||
o = o.next_cl;
|
||||
if (!o.isDeleted()) {
|
||||
position -= 1;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
};
|
||||
|
||||
return ListManager;
|
||||
|
||||
})(types.Operation);
|
||||
types.ReplaceManager = (function(_super) {
|
||||
__extends(ReplaceManager, _super);
|
||||
|
||||
function ReplaceManager(event_properties, event_this, uid, beginning, end) {
|
||||
this.event_properties = event_properties;
|
||||
this.event_this = event_this;
|
||||
if (this.event_properties['object'] == null) {
|
||||
this.event_properties['object'] = this.event_this;
|
||||
}
|
||||
ReplaceManager.__super__.constructor.call(this, uid, beginning, end);
|
||||
}
|
||||
|
||||
ReplaceManager.prototype.type = "ReplaceManager";
|
||||
|
||||
ReplaceManager.prototype.applyDelete = function() {
|
||||
var o;
|
||||
o = this.beginning;
|
||||
while (o != null) {
|
||||
o.applyDelete();
|
||||
o = o.next_cl;
|
||||
}
|
||||
return ReplaceManager.__super__.applyDelete.call(this);
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.cleanup = function() {
|
||||
return ReplaceManager.__super__.cleanup.call(this);
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.callEventDecorator = function(events) {
|
||||
var event, name, prop, _i, _len, _ref;
|
||||
if (!this.isDeleted()) {
|
||||
for (_i = 0, _len = events.length; _i < _len; _i++) {
|
||||
event = events[_i];
|
||||
_ref = this.event_properties;
|
||||
for (name in _ref) {
|
||||
prop = _ref[name];
|
||||
event[name] = prop;
|
||||
}
|
||||
}
|
||||
this.event_this.callEvent(events);
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.replace = function(content, replaceable_uid) {
|
||||
var o, relp;
|
||||
o = this.getLastOperation();
|
||||
relp = (new types.Replaceable(content, this, replaceable_uid, o, o.next_cl)).execute();
|
||||
return void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.isContentDeleted = function() {
|
||||
return this.getLastOperation().isDeleted();
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.deleteContent = function() {
|
||||
(new types.Delete(void 0, this.getLastOperation().uid)).execute();
|
||||
return void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.val = function() {
|
||||
var o;
|
||||
o = this.getLastOperation();
|
||||
return typeof o.val === "function" ? o.val() : void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype._encode = function() {
|
||||
var json;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid(),
|
||||
'beginning': this.beginning.getUid(),
|
||||
'end': this.end.getUid()
|
||||
};
|
||||
return json;
|
||||
};
|
||||
|
||||
return ReplaceManager;
|
||||
|
||||
})(types.ListManager);
|
||||
types.Replaceable = (function(_super) {
|
||||
__extends(Replaceable, _super);
|
||||
|
||||
function Replaceable(content, parent, uid, prev, next, origin, is_deleted) {
|
||||
if ((content != null) && (content.creator != null)) {
|
||||
this.saveOperation('content', content);
|
||||
} else {
|
||||
this.content = content;
|
||||
}
|
||||
this.saveOperation('parent', parent);
|
||||
Replaceable.__super__.constructor.call(this, uid, prev, next, origin);
|
||||
this.is_deleted = is_deleted;
|
||||
}
|
||||
|
||||
Replaceable.prototype.type = "Replaceable";
|
||||
|
||||
Replaceable.prototype.val = function() {
|
||||
return this.content;
|
||||
};
|
||||
|
||||
Replaceable.prototype.applyDelete = function() {
|
||||
var res, _base, _base1, _base2;
|
||||
res = Replaceable.__super__.applyDelete.apply(this, arguments);
|
||||
if (this.content != null) {
|
||||
if (this.next_cl.type !== "Delimiter") {
|
||||
if (typeof (_base = this.content).deleteAllObservers === "function") {
|
||||
_base.deleteAllObservers();
|
||||
}
|
||||
}
|
||||
if (typeof (_base1 = this.content).applyDelete === "function") {
|
||||
_base1.applyDelete();
|
||||
}
|
||||
if (typeof (_base2 = this.content).dontSync === "function") {
|
||||
_base2.dontSync();
|
||||
}
|
||||
}
|
||||
this.content = null;
|
||||
return res;
|
||||
};
|
||||
|
||||
Replaceable.prototype.cleanup = function() {
|
||||
return Replaceable.__super__.cleanup.apply(this, arguments);
|
||||
};
|
||||
|
||||
Replaceable.prototype.callOperationSpecificInsertEvents = function() {
|
||||
var old_value;
|
||||
if (this.next_cl.type === "Delimiter" && this.prev_cl.type !== "Delimiter") {
|
||||
if (!this.is_deleted) {
|
||||
old_value = this.prev_cl.content;
|
||||
this.parent.callEventDecorator([
|
||||
{
|
||||
type: "update",
|
||||
changedBy: this.uid.creator,
|
||||
oldValue: old_value
|
||||
}
|
||||
]);
|
||||
}
|
||||
this.prev_cl.applyDelete();
|
||||
} else if (this.next_cl.type !== "Delimiter") {
|
||||
this.applyDelete();
|
||||
} else {
|
||||
this.parent.callEventDecorator([
|
||||
{
|
||||
type: "add",
|
||||
changedBy: this.uid.creator
|
||||
}
|
||||
]);
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
Replaceable.prototype.callOperationSpecificDeleteEvents = function(o) {
|
||||
if (this.next_cl.type === "Delimiter") {
|
||||
return this.parent.callEventDecorator([
|
||||
{
|
||||
type: "delete",
|
||||
changedBy: o.uid.creator,
|
||||
oldValue: this.content
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
Replaceable.prototype._encode = function() {
|
||||
var json;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'parent': this.parent.getUid(),
|
||||
'prev': this.prev_cl.getUid(),
|
||||
'next': this.next_cl.getUid(),
|
||||
'origin': this.origin.getUid(),
|
||||
'uid': this.getUid(),
|
||||
'is_deleted': this.is_deleted
|
||||
};
|
||||
if (this.content instanceof types.Operation) {
|
||||
json['content'] = this.content.getUid();
|
||||
} else {
|
||||
if ((this.content != null) && (this.content.creator != null)) {
|
||||
throw new Error("You must not set creator here!");
|
||||
}
|
||||
json['content'] = this.content;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
return Replaceable;
|
||||
|
||||
})(types.Insert);
|
||||
types.Replaceable.parse = function(json) {
|
||||
var content, is_deleted, next, origin, parent, prev, uid;
|
||||
content = json['content'], parent = json['parent'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], is_deleted = json['is_deleted'];
|
||||
return new this(content, parent, uid, prev, next, origin, is_deleted);
|
||||
};
|
||||
return basic_types;
|
||||
};
|
||||
@@ -1,558 +0,0 @@
|
||||
var structured_types_uninitialized,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
|
||||
structured_types_uninitialized = require("./StructuredTypes");
|
||||
|
||||
module.exports = function(HB) {
|
||||
var parser, structured_types, types;
|
||||
structured_types = structured_types_uninitialized(HB);
|
||||
types = structured_types.types;
|
||||
parser = structured_types.parser;
|
||||
types.TextInsert = (function(_super) {
|
||||
__extends(TextInsert, _super);
|
||||
|
||||
function TextInsert(content, uid, prev, next, origin, parent) {
|
||||
if (content != null ? content.creator : void 0) {
|
||||
this.saveOperation('content', content);
|
||||
} else {
|
||||
this.content = content;
|
||||
}
|
||||
TextInsert.__super__.constructor.call(this, uid, prev, next, origin, parent);
|
||||
}
|
||||
|
||||
TextInsert.prototype.type = "TextInsert";
|
||||
|
||||
TextInsert.prototype.getLength = function() {
|
||||
if (this.isDeleted()) {
|
||||
return 0;
|
||||
} else {
|
||||
return this.content.length;
|
||||
}
|
||||
};
|
||||
|
||||
TextInsert.prototype.applyDelete = function() {
|
||||
TextInsert.__super__.applyDelete.apply(this, arguments);
|
||||
if (this.content instanceof types.Operation) {
|
||||
this.content.applyDelete();
|
||||
}
|
||||
return this.content = null;
|
||||
};
|
||||
|
||||
TextInsert.prototype.execute = function() {
|
||||
if (!this.validateSavedOperations()) {
|
||||
return false;
|
||||
} else {
|
||||
if (this.content instanceof types.Operation) {
|
||||
this.content.insert_parent = this;
|
||||
}
|
||||
return TextInsert.__super__.execute.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
TextInsert.prototype.val = function(current_position) {
|
||||
if (this.isDeleted() || (this.content == null)) {
|
||||
return "";
|
||||
} else {
|
||||
return this.content;
|
||||
}
|
||||
};
|
||||
|
||||
TextInsert.prototype._encode = function() {
|
||||
var json, _ref;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid(),
|
||||
'prev': this.prev_cl.getUid(),
|
||||
'next': this.next_cl.getUid(),
|
||||
'origin': this.origin.getUid(),
|
||||
'parent': this.parent.getUid()
|
||||
};
|
||||
if (((_ref = this.content) != null ? _ref.getUid : void 0) != null) {
|
||||
json['content'] = this.content.getUid();
|
||||
} else {
|
||||
json['content'] = this.content;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
return TextInsert;
|
||||
|
||||
})(types.Insert);
|
||||
types.TextInsert.parse = function(json) {
|
||||
var content, next, origin, parent, prev, uid;
|
||||
content = json['content'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], parent = json['parent'];
|
||||
return new types.TextInsert(content, uid, prev, next, origin, parent);
|
||||
};
|
||||
types.Array = (function(_super) {
|
||||
__extends(Array, _super);
|
||||
|
||||
function Array() {
|
||||
return Array.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Array.prototype.type = "Array";
|
||||
|
||||
Array.prototype.applyDelete = function() {
|
||||
var o;
|
||||
o = this.end;
|
||||
while (o != null) {
|
||||
o.applyDelete();
|
||||
o = o.prev_cl;
|
||||
}
|
||||
return Array.__super__.applyDelete.call(this);
|
||||
};
|
||||
|
||||
Array.prototype.cleanup = function() {
|
||||
return Array.__super__.cleanup.call(this);
|
||||
};
|
||||
|
||||
Array.prototype.toJson = function(transform_to_value) {
|
||||
var i, o, val, _i, _len, _results;
|
||||
if (transform_to_value == null) {
|
||||
transform_to_value = false;
|
||||
}
|
||||
val = this.val();
|
||||
_results = [];
|
||||
for (o = _i = 0, _len = val.length; _i < _len; o = ++_i) {
|
||||
i = val[o];
|
||||
if (o instanceof types.Object) {
|
||||
_results.push(o.toJson(transform_to_value));
|
||||
} else if (o instanceof types.Array) {
|
||||
_results.push(o.toJson(transform_to_value));
|
||||
} else if (transform_to_value && o instanceof types.Operation) {
|
||||
_results.push(o.val());
|
||||
} else {
|
||||
_results.push(o);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
|
||||
Array.prototype.val = function(pos) {
|
||||
var o, result;
|
||||
if (pos != null) {
|
||||
o = this.getOperationByPosition(pos + 1);
|
||||
if (!(o instanceof types.Delimiter)) {
|
||||
return o.val();
|
||||
} else {
|
||||
throw new Error("this position does not exist");
|
||||
}
|
||||
} else {
|
||||
o = this.beginning.next_cl;
|
||||
result = [];
|
||||
while (o !== this.end) {
|
||||
if (!o.isDeleted()) {
|
||||
result.push(o.val());
|
||||
}
|
||||
o = o.next_cl;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
Array.prototype.push = function(content) {
|
||||
return this.insertAfter(this.end.prev_cl, content);
|
||||
};
|
||||
|
||||
Array.prototype.insertAfter = function(left, content, options) {
|
||||
var c, createContent, right, tmp, _i, _len;
|
||||
createContent = function(content, options) {
|
||||
var type;
|
||||
if ((content != null) && (content.constructor != null)) {
|
||||
type = types[content.constructor.name];
|
||||
if ((type != null) && (type.create != null)) {
|
||||
return type.create(content, options);
|
||||
} else {
|
||||
throw new Error("The " + content.constructor.name + "-type is not (yet) supported in Y.");
|
||||
}
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
};
|
||||
right = left.next_cl;
|
||||
while (right.isDeleted()) {
|
||||
right = right.next_cl;
|
||||
}
|
||||
left = right.prev_cl;
|
||||
if (content instanceof types.Operation) {
|
||||
(new types.TextInsert(content, void 0, left, right)).execute();
|
||||
} else {
|
||||
for (_i = 0, _len = content.length; _i < _len; _i++) {
|
||||
c = content[_i];
|
||||
tmp = (new types.TextInsert(createContent(c, options), void 0, left, right)).execute();
|
||||
left = tmp;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Array.prototype.insert = function(position, content, options) {
|
||||
var ith;
|
||||
ith = this.getOperationByPosition(position);
|
||||
return this.insertAfter(ith, [content], options);
|
||||
};
|
||||
|
||||
Array.prototype["delete"] = function(position, length) {
|
||||
var d, delete_ops, i, o, _i;
|
||||
o = this.getOperationByPosition(position + 1);
|
||||
delete_ops = [];
|
||||
for (i = _i = 0; 0 <= length ? _i < length : _i > length; i = 0 <= length ? ++_i : --_i) {
|
||||
if (o instanceof types.Delimiter) {
|
||||
break;
|
||||
}
|
||||
d = (new types.Delete(void 0, o)).execute();
|
||||
o = o.next_cl;
|
||||
while ((!(o instanceof types.Delimiter)) && o.isDeleted()) {
|
||||
o = o.next_cl;
|
||||
}
|
||||
delete_ops.push(d._encode());
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
Array.prototype._encode = function() {
|
||||
var json;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid()
|
||||
};
|
||||
return json;
|
||||
};
|
||||
|
||||
return Array;
|
||||
|
||||
})(types.ListManager);
|
||||
types.Array.parse = function(json) {
|
||||
var uid;
|
||||
uid = json['uid'];
|
||||
return new this(uid);
|
||||
};
|
||||
types.Array.create = function(content, mutable) {
|
||||
var ith, list;
|
||||
if (mutable === "mutable") {
|
||||
list = new types.Array().execute();
|
||||
ith = list.getOperationByPosition(0);
|
||||
list.insertAfter(ith, content);
|
||||
return list;
|
||||
} else if ((mutable == null) || (mutable === "immutable")) {
|
||||
return content;
|
||||
} else {
|
||||
throw new Error("Specify either \"mutable\" or \"immutable\"!!");
|
||||
}
|
||||
};
|
||||
types.String = (function(_super) {
|
||||
__extends(String, _super);
|
||||
|
||||
function String(uid) {
|
||||
this.textfields = [];
|
||||
String.__super__.constructor.call(this, uid);
|
||||
}
|
||||
|
||||
String.prototype.type = "String";
|
||||
|
||||
String.prototype.val = function() {
|
||||
var c, o;
|
||||
c = (function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = this.toArray();
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
o = _ref[_i];
|
||||
if (o.val != null) {
|
||||
_results.push(o.val());
|
||||
} else {
|
||||
_results.push("");
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
}).call(this);
|
||||
return c.join('');
|
||||
};
|
||||
|
||||
String.prototype.toString = function() {
|
||||
return this.val();
|
||||
};
|
||||
|
||||
String.prototype.insert = function(position, content, options) {
|
||||
var ith;
|
||||
ith = this.getOperationByPosition(position);
|
||||
return this.insertAfter(ith, content, options);
|
||||
};
|
||||
|
||||
String.prototype.bind = function(textfield, dom_root) {
|
||||
var createRange, creator_token, t, word, writeContent, writeRange, _i, _len, _ref;
|
||||
if (dom_root == null) {
|
||||
dom_root = window;
|
||||
}
|
||||
if (dom_root.getSelection == null) {
|
||||
dom_root = window;
|
||||
}
|
||||
_ref = this.textfields;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
t = _ref[_i];
|
||||
if (t === textfield) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
creator_token = false;
|
||||
word = this;
|
||||
textfield.value = this.val();
|
||||
this.textfields.push(textfield);
|
||||
if ((textfield.selectionStart != null) && (textfield.setSelectionRange != null)) {
|
||||
createRange = function(fix) {
|
||||
var left, right;
|
||||
left = textfield.selectionStart;
|
||||
right = textfield.selectionEnd;
|
||||
if (fix != null) {
|
||||
left = fix(left);
|
||||
right = fix(right);
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
right: right
|
||||
};
|
||||
};
|
||||
writeRange = function(range) {
|
||||
writeContent(word.val());
|
||||
return textfield.setSelectionRange(range.left, range.right);
|
||||
};
|
||||
writeContent = function(content) {
|
||||
return textfield.value = content;
|
||||
};
|
||||
} else {
|
||||
createRange = function(fix) {
|
||||
var clength, left, right, s;
|
||||
s = dom_root.getSelection();
|
||||
clength = textfield.textContent.length;
|
||||
left = Math.min(s.anchorOffset, clength);
|
||||
right = Math.min(s.focusOffset, clength);
|
||||
if (fix != null) {
|
||||
left = fix(left);
|
||||
right = fix(right);
|
||||
}
|
||||
return {
|
||||
left: left,
|
||||
right: right,
|
||||
isReal: true
|
||||
};
|
||||
};
|
||||
writeRange = function(range) {
|
||||
var r, s, textnode;
|
||||
writeContent(word.val());
|
||||
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);
|
||||
r = document.createRange();
|
||||
r.setStart(textnode, range.left);
|
||||
r.setEnd(textnode, range.right);
|
||||
s = window.getSelection();
|
||||
s.removeAllRanges();
|
||||
return s.addRange(r);
|
||||
}
|
||||
};
|
||||
writeContent = function(content) {
|
||||
var append;
|
||||
append = "";
|
||||
if (content[content.length - 1] === " ") {
|
||||
content = content.slice(0, content.length - 1);
|
||||
append = ' ';
|
||||
}
|
||||
textfield.textContent = content;
|
||||
return textfield.innerHTML += append;
|
||||
};
|
||||
}
|
||||
writeContent(this.val());
|
||||
this.observe(function(events) {
|
||||
var event, fix, o_pos, r, _j, _len1, _results;
|
||||
_results = [];
|
||||
for (_j = 0, _len1 = events.length; _j < _len1; _j++) {
|
||||
event = events[_j];
|
||||
if (!creator_token) {
|
||||
if (event.type === "insert") {
|
||||
o_pos = event.position;
|
||||
fix = function(cursor) {
|
||||
if (cursor <= o_pos) {
|
||||
return cursor;
|
||||
} else {
|
||||
cursor += 1;
|
||||
return cursor;
|
||||
}
|
||||
};
|
||||
r = createRange(fix);
|
||||
_results.push(writeRange(r));
|
||||
} else if (event.type === "delete") {
|
||||
o_pos = event.position;
|
||||
fix = function(cursor) {
|
||||
if (cursor < o_pos) {
|
||||
return cursor;
|
||||
} else {
|
||||
cursor -= 1;
|
||||
return cursor;
|
||||
}
|
||||
};
|
||||
r = createRange(fix);
|
||||
_results.push(writeRange(r));
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
});
|
||||
textfield.onkeypress = function(event) {
|
||||
var char, diff, pos, r;
|
||||
if (word.is_deleted) {
|
||||
textfield.onkeypress = null;
|
||||
return true;
|
||||
}
|
||||
creator_token = true;
|
||||
char = null;
|
||||
if (event.key != null) {
|
||||
if (event.charCode === 32) {
|
||||
char = " ";
|
||||
} else if (event.keyCode === 13) {
|
||||
char = '\n';
|
||||
} else {
|
||||
char = event.key;
|
||||
}
|
||||
} else {
|
||||
char = window.String.fromCharCode(event.keyCode);
|
||||
}
|
||||
if (char.length > 1) {
|
||||
return true;
|
||||
} else if (char.length > 0) {
|
||||
r = createRange();
|
||||
pos = Math.min(r.left, r.right);
|
||||
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();
|
||||
creator_token = false;
|
||||
return false;
|
||||
};
|
||||
textfield.onpaste = function(event) {
|
||||
if (word.is_deleted) {
|
||||
textfield.onpaste = null;
|
||||
return true;
|
||||
}
|
||||
return event.preventDefault();
|
||||
};
|
||||
textfield.oncut = function(event) {
|
||||
if (word.is_deleted) {
|
||||
textfield.oncut = null;
|
||||
return true;
|
||||
}
|
||||
return event.preventDefault();
|
||||
};
|
||||
return textfield.onkeydown = function(event) {
|
||||
var del_length, diff, new_pos, pos, r, val;
|
||||
creator_token = true;
|
||||
if (word.is_deleted) {
|
||||
textfield.onkeydown = null;
|
||||
return true;
|
||||
}
|
||||
r = createRange();
|
||||
pos = Math.min(r.left, r.right, word.val().length);
|
||||
diff = Math.abs(r.left - r.right);
|
||||
if ((event.keyCode != null) && event.keyCode === 8) {
|
||||
if (diff > 0) {
|
||||
word["delete"](pos, diff);
|
||||
r.left = pos;
|
||||
r.right = pos;
|
||||
writeRange(r);
|
||||
} else {
|
||||
if ((event.ctrlKey != null) && event.ctrlKey) {
|
||||
val = word.val();
|
||||
new_pos = pos;
|
||||
del_length = 0;
|
||||
if (pos > 0) {
|
||||
new_pos--;
|
||||
del_length++;
|
||||
}
|
||||
while (new_pos > 0 && val[new_pos] !== " " && val[new_pos] !== '\n') {
|
||||
new_pos--;
|
||||
del_length++;
|
||||
}
|
||||
word["delete"](new_pos, pos - new_pos);
|
||||
r.left = new_pos;
|
||||
r.right = new_pos;
|
||||
writeRange(r);
|
||||
} else {
|
||||
if (pos > 0) {
|
||||
word["delete"](pos - 1, 1);
|
||||
r.left = pos - 1;
|
||||
r.right = pos - 1;
|
||||
writeRange(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
creator_token = false;
|
||||
return false;
|
||||
} else if ((event.keyCode != null) && event.keyCode === 46) {
|
||||
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();
|
||||
creator_token = false;
|
||||
return false;
|
||||
} else {
|
||||
creator_token = false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
String.prototype._encode = function() {
|
||||
var json;
|
||||
json = {
|
||||
'type': this.type,
|
||||
'uid': this.getUid()
|
||||
};
|
||||
return json;
|
||||
};
|
||||
|
||||
return String;
|
||||
|
||||
})(types.Array);
|
||||
types.String.parse = function(json) {
|
||||
var uid;
|
||||
uid = json['uid'];
|
||||
return new this(uid);
|
||||
};
|
||||
types.String.create = function(content, mutable) {
|
||||
var word;
|
||||
if (mutable === "mutable") {
|
||||
word = new types.String().execute();
|
||||
word.insert(0, content);
|
||||
return word;
|
||||
} else if ((mutable == null) || (mutable === "immutable")) {
|
||||
return content;
|
||||
} else {
|
||||
throw new Error("Specify either \"mutable\" or \"immutable\"!!");
|
||||
}
|
||||
};
|
||||
return structured_types;
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
var Y, bindToChildren;
|
||||
|
||||
Y = require('./y');
|
||||
|
||||
bindToChildren = function(that) {
|
||||
var attr, i, _i, _ref;
|
||||
for (i = _i = 0, _ref = that.children.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
|
||||
attr = that.children.item(i);
|
||||
if (attr.name != null) {
|
||||
attr.val = that.val.val(attr.name);
|
||||
}
|
||||
}
|
||||
return that.val.observe(function(events) {
|
||||
var event, newVal, _j, _len, _results;
|
||||
_results = [];
|
||||
for (_j = 0, _len = events.length; _j < _len; _j++) {
|
||||
event = events[_j];
|
||||
if (event.name != null) {
|
||||
_results.push((function() {
|
||||
var _k, _ref1, _results1;
|
||||
_results1 = [];
|
||||
for (i = _k = 0, _ref1 = that.children.length; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
|
||||
attr = that.children.item(i);
|
||||
if ((attr.name != null) && attr.name === event.name) {
|
||||
newVal = that.val.val(attr.name);
|
||||
if (attr.val !== newVal) {
|
||||
_results1.push(attr.val = newVal);
|
||||
} else {
|
||||
_results1.push(void 0);
|
||||
}
|
||||
} else {
|
||||
_results1.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results1;
|
||||
})());
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
});
|
||||
};
|
||||
|
||||
Polymer("y-object", {
|
||||
ready: function() {
|
||||
if (this.connector != null) {
|
||||
this.val = new Y(this.connector);
|
||||
return bindToChildren(this);
|
||||
} else if (this.val != null) {
|
||||
return bindToChildren(this);
|
||||
}
|
||||
},
|
||||
valChanged: function() {
|
||||
if ((this.val != null) && this.val.type === "Object") {
|
||||
return bindToChildren(this);
|
||||
}
|
||||
},
|
||||
connectorChanged: function() {
|
||||
if (this.val == null) {
|
||||
this.val = new Y(this.connector);
|
||||
return bindToChildren(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Polymer("y-property", {
|
||||
ready: function() {
|
||||
if ((this.val != null) && (this.name != null)) {
|
||||
if (this.val.constructor === Object) {
|
||||
this.val = this.parentElement.val(this.name, this.val).val(this.name);
|
||||
} else if (typeof this.val === "string") {
|
||||
this.parentElement.val(this.name, this.val);
|
||||
}
|
||||
if (this.val.type === "Object") {
|
||||
return bindToChildren(this);
|
||||
}
|
||||
}
|
||||
},
|
||||
valChanged: function() {
|
||||
var _ref;
|
||||
if ((this.val != null) && (this.name != null)) {
|
||||
if (this.val.constructor === Object) {
|
||||
return this.val = this.parentElement.val.val(this.name, this.val).val(this.name);
|
||||
} else if (this.val.type === "Object") {
|
||||
return bindToChildren(this);
|
||||
} else if ((((_ref = this.parentElement.val) != null ? _ref.val : void 0) != null) && this.val !== this.parentElement.val.val(this.name)) {
|
||||
return this.parentElement.val.val(this.name, this.val);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
var Engine, HistoryBuffer, adaptConnector, createY, json_types_uninitialized,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
|
||||
|
||||
json_types_uninitialized = require("./Types/JsonTypes");
|
||||
|
||||
HistoryBuffer = require("./HistoryBuffer");
|
||||
|
||||
Engine = require("./Engine");
|
||||
|
||||
adaptConnector = require("./ConnectorAdapter");
|
||||
|
||||
createY = function(connector) {
|
||||
var HB, Y, type_manager, types, user_id;
|
||||
user_id = null;
|
||||
if (connector.id != null) {
|
||||
user_id = connector.id;
|
||||
} else {
|
||||
user_id = "_temp";
|
||||
connector.whenUserIdSet(function(id) {
|
||||
user_id = id;
|
||||
return HB.resetUserId(id);
|
||||
});
|
||||
}
|
||||
HB = new HistoryBuffer(user_id);
|
||||
type_manager = json_types_uninitialized(HB);
|
||||
types = type_manager.types;
|
||||
Y = (function(_super) {
|
||||
__extends(Y, _super);
|
||||
|
||||
function Y() {
|
||||
this.connector = connector;
|
||||
this.HB = HB;
|
||||
this.types = types;
|
||||
this.engine = new Engine(this.HB, type_manager.types);
|
||||
adaptConnector(this.connector, this.engine, this.HB, type_manager.execution_listener);
|
||||
Y.__super__.constructor.apply(this, arguments);
|
||||
}
|
||||
|
||||
Y.prototype.getConnector = function() {
|
||||
return this.connector;
|
||||
};
|
||||
|
||||
return Y;
|
||||
|
||||
})(types.Object);
|
||||
return new Y(HB.getReservedUniqueIdentifier()).execute();
|
||||
};
|
||||
|
||||
module.exports = createY;
|
||||
|
||||
if ((typeof window !== "undefined" && window !== null) && (window.Y == null)) {
|
||||
window.Y = createY;
|
||||
}
|
||||
16532
build/test/Json_test.js
16532
build/test/Json_test.js
File diff suppressed because one or more lines are too long
16304
build/test/Text_test.js
16304
build/test/Text_test.js
File diff suppressed because one or more lines are too long
@@ -1,27 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test Yatta!</title>
|
||||
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>
|
||||
<script src="../../node_modules/mocha/mocha.js" class="awesome"></script>
|
||||
<script>
|
||||
mocha.setup('bdd');
|
||||
mocha.ui('bdd');
|
||||
mocha.reporter('html');
|
||||
</script>
|
||||
<script src="TextYatta_test.js"></script>
|
||||
<script src="JsonYatta_test.js"></script>
|
||||
<!--script src="XmlYatta_test_browser.js"></script-->
|
||||
<script>
|
||||
//mocha.checkLeaks();
|
||||
//mocha.run();
|
||||
window.onerror = null;
|
||||
if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
|
||||
else { mocha.run(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
declarations/Structs.js
Normal file
72
declarations/Structs.js
Normal 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
0
declarations/Type.js
Normal file
34
declarations/Y.js
Normal file
34
declarations/Y.js
Normal 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>;
|
||||
}
|
||||
1
dist
Submodule
1
dist
Submodule
Submodule dist added at 47bcec8bc7
@@ -1,5 +0,0 @@
|
||||
# Examples
|
||||
|
||||
Here you find some (hopefully) usefull examples on how to use Yatta!
|
||||
|
||||
Please note, that the XMPP Connector is the best supported Connector at the moment.
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8 />
|
||||
<title>Y Example</title>
|
||||
<script src="../../../webcomponentsjs/webcomponents.min.js"></script>
|
||||
<link rel="import" href="../../../polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="y-test.html">
|
||||
</head>
|
||||
<body>
|
||||
<y-test></y-test>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
|
||||
setTimeout(function(){
|
||||
window.y_test = document.querySelector("y-test");
|
||||
|
||||
window.y_test.y.val("stuff",{otherstuff:{nostuff:"this is no stuff"}})
|
||||
setTimeout(function(){
|
||||
var res = y_test.y.val("stuff");
|
||||
if(!(y_test.nostuff === "this is no stuff")){
|
||||
console.log("Deep inherit doesn't work!")
|
||||
}
|
||||
window.y_stuff_property.val = {nostuff: "this is also no stuff"};
|
||||
setTimeout(function(){
|
||||
if(!(y_test.nostuff === "this is also no stuff")){
|
||||
console.log("Element val overwrite doesn't work")
|
||||
}
|
||||
console.log("Everything is fine :)");
|
||||
},500)
|
||||
},500);
|
||||
},3000)
|
||||
@@ -1,38 +0,0 @@
|
||||
<link rel="import" href="../../y-object.html">
|
||||
<link rel="import" href="../../../y-connectors/y-xmpp/y-xmpp.html">
|
||||
<link rel="import" href="../../../paper-slider/paper-slider.html">
|
||||
|
||||
<polymer-element name="y-test" attributes="y connector stuff">
|
||||
<template>
|
||||
<h1 id="text" contentEditable> Check this out !</h1>
|
||||
<y-xmpp id="connector" connector={{connector}} room="testy-xmpp-polymer"></y-xmpp>
|
||||
<y-object connector={{connector}} val={{y}}>
|
||||
<y-property name="slider" val={{slider}}>
|
||||
</y-property>
|
||||
<y-property name="stuff" val={{stuff}}>
|
||||
<y-property id="otherstuff" name="otherstuff" val={{otherstuff}}>
|
||||
</y-property>
|
||||
</y-property>
|
||||
</y-object>
|
||||
<y-object val={{otherstuff}}>
|
||||
<y-property name="nostuff" val={{nostuff}}>
|
||||
</y-property>
|
||||
</y-object>
|
||||
<paper-slider min="0" max="200" immediateValue={{slider}}></paper-slider>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function(){
|
||||
window.y_stuff_property = this.$.otherstuff;
|
||||
this.y.val("slider",50)
|
||||
var that = this;
|
||||
this.connector.whenSynced(function(){
|
||||
if(that.y.val("text") == null){
|
||||
that.y.val("text","stuff","mutable");
|
||||
}
|
||||
that.y.val("text").bind(that.$.text,that.shadowRoot)
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</polymer-element>
|
||||
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8 />
|
||||
<title>Y Example</title>
|
||||
<script src="../../build/browser/y.js"></script>
|
||||
<script src="../../../y-connectors/y-xmpp/y-xmpp.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 contentEditable> yjs Tutorial</h1>
|
||||
<p> Collaborative Json editing with <a href="https://github.com/rwth-acis/yjs/">yjs</a>
|
||||
and XMPP Connector. </p>
|
||||
|
||||
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
|
||||
|
||||
<p> <a href="https://github.com/rwth-acis/yjs/">yjs</a> is a Framework for Real-Time collaboration on arbitrary data types.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
|
||||
connector = new Y.XMPP("testy-xmpp-json2");
|
||||
connector.debug = true
|
||||
|
||||
y = new Y(connector);
|
||||
|
||||
window.onload = function(){
|
||||
var textbox = document.getElementById("textfield");
|
||||
y.observe(function(events){
|
||||
for(var i=0; i<events.length; i++){
|
||||
var event = events[i];
|
||||
if(event.name === "textfield" && event.type !== "delete"){
|
||||
y.val("textfield").bind(textbox);
|
||||
y.val("headline").bind(document.querySelector("h1"))
|
||||
}
|
||||
}
|
||||
});
|
||||
connector.whenSynced(function(){
|
||||
if(y.val("textfield") == null){
|
||||
y.val("headline","headline", "mutable");
|
||||
y.val("textfield","stuff", "mutable")
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
122
gulpfile.coffee
122
gulpfile.coffee
@@ -1,122 +0,0 @@
|
||||
gulp = require('gulp')
|
||||
coffee = require('gulp-coffee')
|
||||
concat = require('gulp-concat')
|
||||
uglify = require 'gulp-uglify'
|
||||
sourcemaps = require('gulp-sourcemaps')
|
||||
browserify = require('gulp-browserify')
|
||||
rename = require 'gulp-rename'
|
||||
rimraf = require 'gulp-rimraf'
|
||||
gulpif = require 'gulp-if'
|
||||
ignore = require 'gulp-ignore'
|
||||
git = require 'gulp-git'
|
||||
debug = require 'gulp-debug'
|
||||
coffeelint = require 'gulp-coffeelint'
|
||||
mocha = require 'gulp-mocha'
|
||||
run = require 'gulp-run'
|
||||
ljs = require 'gulp-ljs'
|
||||
plumber = require 'gulp-plumber'
|
||||
mochaPhantomJS = require 'gulp-mocha-phantomjs'
|
||||
cache = require 'gulp-cached'
|
||||
coffeeify = require 'gulp-coffeeify'
|
||||
|
||||
gulp.task 'default', ['build_browser']
|
||||
|
||||
files =
|
||||
lib : ['./lib/**/*.coffee']
|
||||
browser : ['./lib/y.coffee','./lib/y-object.coffee']
|
||||
#test : ['./test/**/*_test.coffee']
|
||||
test : ['./test/Json_test.coffee', './test/Text_test.coffee']
|
||||
gulp : ['./gulpfile.coffee']
|
||||
examples : ['./examples/**/*.js']
|
||||
other: ['./lib/**/*']
|
||||
|
||||
files.all = []
|
||||
for name,file_list of files
|
||||
if name isnt 'build'
|
||||
files.all = files.all.concat file_list
|
||||
|
||||
gulp.task 'deploy_nodejs', ->
|
||||
gulp.src files.lib
|
||||
.pipe sourcemaps.init()
|
||||
.pipe coffee()
|
||||
.pipe sourcemaps.write './'
|
||||
.pipe gulp.dest 'build/node/'
|
||||
.pipe gulpif '!**/', git.add({args : "-A"})
|
||||
|
||||
gulp.task 'deploy', ['mocha', 'build_browser', 'deploy_nodejs', 'lint', 'phantom_test', 'codo']
|
||||
|
||||
gulp.task 'build_browser', ->
|
||||
gulp.src files.browser, { read: false }
|
||||
.pipe plumber()
|
||||
.pipe browserify
|
||||
transform: ['coffeeify']
|
||||
extensions: ['.coffee']
|
||||
debug : true
|
||||
.pipe rename
|
||||
extname: ".js"
|
||||
.pipe gulp.dest './build/browser/'
|
||||
.pipe uglify()
|
||||
.pipe gulp.dest '.'
|
||||
|
||||
gulp.src files.test, {read: false}
|
||||
.pipe plumber()
|
||||
.pipe browserify
|
||||
transform: ['coffeeify']
|
||||
extensions: ['.coffee']
|
||||
debug: true
|
||||
.pipe rename
|
||||
extname: ".js"
|
||||
.pipe gulp.dest './build/test/'
|
||||
|
||||
gulp.task 'build_node', ->
|
||||
gulp.src files.lib
|
||||
.pipe plumber()
|
||||
.pipe coffee({bare:true})
|
||||
.pipe gulp.dest './build/node'
|
||||
|
||||
gulp.task 'build', ['build_node', 'build_browser'], ->
|
||||
|
||||
gulp.task 'watch', ['build_browser'], ->
|
||||
gulp.watch files.all, ['build_browser']
|
||||
|
||||
gulp.task 'mocha', ->
|
||||
gulp.src files.test, { read: false }
|
||||
.pipe plumber()
|
||||
.pipe mocha {reporter : 'list'}
|
||||
|
||||
|
||||
gulp.task 'lint', ->
|
||||
gulp.src files.all
|
||||
.pipe ignore.include '**/*.coffee'
|
||||
.pipe coffeelint {
|
||||
"max_line_length":
|
||||
"level": "ignore"
|
||||
}
|
||||
.pipe coffeelint.reporter()
|
||||
|
||||
gulp.task 'phantom_watch', ['phantom_test'], ->
|
||||
gulp.watch files.all, ['phantom_test']
|
||||
|
||||
gulp.task 'literate', ->
|
||||
gulp.src files.examples
|
||||
.pipe ljs { code : true }
|
||||
.pipe rename
|
||||
basename : "README"
|
||||
extname : ".md"
|
||||
.pipe gulp.dest 'examples/'
|
||||
.pipe gulpif '!**/', git.add({args : "-A"})
|
||||
|
||||
gulp.task 'codo', [], ()->
|
||||
command = './node_modules/codo/bin/codo -o "./doc" --name "yjs" --readme "README.md" --undocumented false --private true --title "yjs API" ./lib - LICENSE.txt '
|
||||
run(command).exec()
|
||||
|
||||
gulp.task 'phantom_test', ['build_browser'], ()->
|
||||
gulp.src 'build/test/index.html'
|
||||
.pipe mochaPhantomJS()
|
||||
|
||||
gulp.task 'clean', ->
|
||||
gulp.src ['./build/{browser,test,node}/**/*.{js,map}','./doc/'], { read: false }
|
||||
.pipe rimraf()
|
||||
|
||||
gulp.task 'default', ['clean','build'], ->
|
||||
|
||||
202
gulpfile.helper.js
Normal file
202
gulpfile.helper.js
Normal 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)
|
||||
})
|
||||
}
|
||||
104
gulpfile.js
Normal file
104
gulpfile.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/* eslint-env node */
|
||||
|
||||
/** Gulp Commands
|
||||
|
||||
gulp command*
|
||||
[--export ModuleType]
|
||||
[--name ModuleName]
|
||||
[--testport TestPort]
|
||||
[--testfiles TestFiles]
|
||||
|
||||
Module name (ModuleName):
|
||||
Compile this to "y.js" (default)
|
||||
|
||||
Supported module types (ModuleType):
|
||||
- amd
|
||||
- amdStrict
|
||||
- common
|
||||
- commonStrict
|
||||
- ignore (default)
|
||||
- system
|
||||
- umd
|
||||
- umdStrict
|
||||
|
||||
Test port (TestPort):
|
||||
Serve the specs on port 8888 (default)
|
||||
|
||||
Test files (TestFiles):
|
||||
Specify which specs to use!
|
||||
|
||||
Commands:
|
||||
- build:deploy
|
||||
Build this library for deployment (es6->es5, minified)
|
||||
- dev:browser
|
||||
Watch the ./src directory.
|
||||
Builds the library on changes.
|
||||
Starts an http-server and serves the test suite on http://127.0.0.1:8888.
|
||||
- dev:node
|
||||
Watch the ./src directory.
|
||||
Builds and specs the library on changes.
|
||||
Usefull to run with node-inspector.
|
||||
`node-debug $(which gulp) dev:node
|
||||
- test:
|
||||
Test this library
|
||||
*/
|
||||
|
||||
var gulp = require('gulp')
|
||||
var $ = require('gulp-load-plugins')()
|
||||
var runSequence = require('run-sequence').use(gulp)
|
||||
|
||||
require('./gulpfile.helper.js')(gulp, {
|
||||
polyfills: [],
|
||||
entry: './src/y.js',
|
||||
targetName: 'y.js',
|
||||
moduleName: 'yjs',
|
||||
includeRuntime: true,
|
||||
specs: [
|
||||
'./src/Database.spec.js',
|
||||
'../y-array/src/Array.spec.js',
|
||||
'../y-map/src/Map.spec.js'
|
||||
]
|
||||
})
|
||||
|
||||
gulp.task('dev:examples', ['watch:dist'], function () {
|
||||
// watch all distfiles and copy them to bower_components
|
||||
var distfiles = ['./dist/*.{js,es6}', './dist/*.{js,es6}.map', '../y-*/dist/*.{js,es6}', '../y-*/dist/*.{js,es6}.map']
|
||||
gulp.src(distfiles)
|
||||
.pipe($.watch(distfiles))
|
||||
.pipe($.rename(function (path) {
|
||||
var dir = path.dirname.split(/[\\\/]/)[0]
|
||||
console.log(JSON.stringify(path))
|
||||
path.dirname = dir === '.' ? 'yjs' : dir
|
||||
}))
|
||||
.pipe(gulp.dest('./dist/Examples/bower_components/'))
|
||||
|
||||
return $.serve('dist/Examples/')()
|
||||
})
|
||||
|
||||
gulp.task('default', ['updateSubmodule'], function (cb) {
|
||||
gulp.src('package.json')
|
||||
.pipe($.prompt.prompt({
|
||||
type: 'checkbox',
|
||||
name: 'tasks',
|
||||
message: 'Which tasks would you like to run?',
|
||||
choices: [
|
||||
'test Test this project',
|
||||
'dev:examples Serve the examples directory in ./dist/',
|
||||
'dev:browser Watch files & serve the testsuite for the browser',
|
||||
'dev:nodejs Watch filse & test this project with nodejs',
|
||||
'bump Bump the current state of the project',
|
||||
'publish Publish this project. Creates a github tag',
|
||||
'dist Build the distribution files'
|
||||
]
|
||||
}, function (res) {
|
||||
var tasks = res.tasks.map(function (task) {
|
||||
return task.split(' ')[0]
|
||||
})
|
||||
if (tasks.length > 0) {
|
||||
console.info('gulp ' + tasks.join(' '))
|
||||
runSequence(tasks, cb)
|
||||
} else {
|
||||
console.info('Ok, .. goodbye')
|
||||
}
|
||||
}))
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
|
||||
|
||||
#
|
||||
# @param {Engine} engine The transformation engine
|
||||
# @param {HistoryBuffer} HB
|
||||
# @param {Array<Function>} execution_listener You must ensure that whenever an operation is executed, every function in this Array is called.
|
||||
#
|
||||
adaptConnector = (connector, engine, HB, execution_listener)->
|
||||
send_ = (o)->
|
||||
if o.uid.creator is HB.getUserId() and (typeof o.uid.op_number isnt "string")
|
||||
connector.broadcast o
|
||||
|
||||
if connector.invokeSync?
|
||||
HB.setInvokeSyncHandler connector.invokeSync
|
||||
|
||||
execution_listener.push send_
|
||||
# For the XMPPConnector: lets send it as an array
|
||||
# therefore, we have to restructure it later
|
||||
encode_state_vector = (v)->
|
||||
for name,value of v
|
||||
user: name
|
||||
state: value
|
||||
parse_state_vector = (v)->
|
||||
state_vector = {}
|
||||
for s in v
|
||||
state_vector[s.user] = s.state
|
||||
state_vector
|
||||
|
||||
getStateVector = ()->
|
||||
encode_state_vector HB.getOperationCounter()
|
||||
|
||||
getHB = (v)->
|
||||
state_vector = parse_state_vector v
|
||||
hb = HB._encode state_vector
|
||||
for o in hb
|
||||
o.fromHB = "true" # execute immediately
|
||||
json =
|
||||
hb: hb
|
||||
state_vector: encode_state_vector HB.getOperationCounter()
|
||||
json
|
||||
|
||||
applyHB = (hb)->
|
||||
engine.applyOp hb
|
||||
|
||||
connector.getStateVector = getStateVector
|
||||
connector.getHB = getHB
|
||||
connector.applyHB = applyHB
|
||||
|
||||
connector.whenReceiving (sender, op)->
|
||||
if op.uid.creator isnt HB.getUserId()
|
||||
engine.applyOp op
|
||||
|
||||
if connector._whenBoundToY?
|
||||
connector._whenBoundToY()
|
||||
|
||||
module.exports = adaptConnector
|
||||
@@ -1,112 +0,0 @@
|
||||
|
||||
window?.unprocessed_counter = 0 # del this
|
||||
window?.unprocessed_exec_counter = 0 # TODO
|
||||
window?.unprocessed_types = []
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# The Engine handles how and in which order to execute operations and add operations to the HistoryBuffer.
|
||||
#
|
||||
class Engine
|
||||
|
||||
#
|
||||
# @param {HistoryBuffer} HB
|
||||
# @param {Object} types list of available types
|
||||
#
|
||||
constructor: (@HB, @types)->
|
||||
@unprocessed_ops = []
|
||||
|
||||
#
|
||||
# Parses an operatio from the json format. It uses the specified parser in your OperationType module.
|
||||
#
|
||||
parseOperation: (json)->
|
||||
type = @types[json.type]
|
||||
if type?.parse?
|
||||
type.parse json
|
||||
else
|
||||
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
|
||||
|
||||
|
||||
#
|
||||
# Apply a set of operations. E.g. the operations you received from another users HB._encode().
|
||||
# @note You must not use this method when you already have ops in your HB!
|
||||
###
|
||||
applyOpsBundle: (ops_json)->
|
||||
ops = []
|
||||
for o in ops_json
|
||||
ops.push @parseOperation o
|
||||
for o in ops
|
||||
if not o.execute()
|
||||
@unprocessed_ops.push o
|
||||
@tryUnprocessed()
|
||||
###
|
||||
|
||||
#
|
||||
# Same as applyOps but operations that are already in the HB are not applied.
|
||||
# @see Engine.applyOps
|
||||
#
|
||||
applyOpsCheckDouble: (ops_json)->
|
||||
for o in ops_json
|
||||
if not @HB.getOperation(o.uid)?
|
||||
@applyOp o
|
||||
|
||||
#
|
||||
# Apply a set of operations. (Helper for using applyOp on Arrays)
|
||||
# @see Engine.applyOp
|
||||
applyOps: (ops_json)->
|
||||
@applyOp ops_json
|
||||
|
||||
#
|
||||
# Apply an operation that you received from another peer.
|
||||
# TODO: make this more efficient!!
|
||||
# - operations may only executed in order by creator, order them in object of arrays (key by creator)
|
||||
# - you can probably make something like dependencies (creator1 waits for creator2)
|
||||
applyOp: (op_json_array)->
|
||||
if op_json_array.constructor isnt Array
|
||||
op_json_array = [op_json_array]
|
||||
for op_json in op_json_array
|
||||
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
|
||||
o = @parseOperation op_json
|
||||
if op_json.fromHB?
|
||||
o.fromHB = op_json.fromHB
|
||||
# @HB.addOperation o
|
||||
if @HB.getOperation(o)?
|
||||
# nop
|
||||
else if ((not @HB.isExpectedOperation(o)) and (not o.fromHB?)) or (not o.execute())
|
||||
@unprocessed_ops.push o
|
||||
window?.unprocessed_types.push o.type # TODO: delete this
|
||||
@tryUnprocessed()
|
||||
|
||||
#
|
||||
# Call this method when you applied a new operation.
|
||||
# It checks if operations that were previously not executable are now executable.
|
||||
#
|
||||
tryUnprocessed: ()->
|
||||
while true
|
||||
old_length = @unprocessed_ops.length
|
||||
unprocessed = []
|
||||
for op in @unprocessed_ops
|
||||
if @HB.getOperation(op)?
|
||||
# nop
|
||||
else if (not @HB.isExpectedOperation(op) and (not op.fromHB?)) or (not op.execute())
|
||||
unprocessed.push op
|
||||
@unprocessed_ops = unprocessed
|
||||
if @unprocessed_ops.length is old_length
|
||||
break
|
||||
if @unprocessed_ops.length isnt 0
|
||||
@HB.invokeSync()
|
||||
|
||||
|
||||
module.exports = Engine
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# An object that holds all applied operations.
|
||||
#
|
||||
# @note The HistoryBuffer is commonly abbreviated to HB.
|
||||
#
|
||||
class HistoryBuffer
|
||||
|
||||
#
|
||||
# Creates an empty HB.
|
||||
# @param {Object} user_id Creator of the HB.
|
||||
#
|
||||
constructor: (@user_id)->
|
||||
@operation_counter = {}
|
||||
@buffer = {}
|
||||
@change_listeners = []
|
||||
@garbage = [] # Will be cleaned on next call of garbageCollector
|
||||
@trash = [] # Is deleted. Wait until it is not used anymore.
|
||||
@performGarbageCollection = true
|
||||
@garbageCollectTimeout = 30000
|
||||
@reserved_identifier_counter = 0
|
||||
setTimeout @emptyGarbage, @garbageCollectTimeout
|
||||
|
||||
resetUserId: (id)->
|
||||
own = @buffer[@user_id]
|
||||
if own?
|
||||
for o_name,o of own
|
||||
o.uid.creator = id
|
||||
if @buffer[id]?
|
||||
throw new Error "You are re-assigning an old user id - this is not (yet) possible!"
|
||||
@buffer[id] = own
|
||||
delete @buffer[@user_id]
|
||||
|
||||
@operation_counter[id] = @operation_counter[@user_id]
|
||||
delete @operation_counter[@user_id]
|
||||
@user_id = id
|
||||
|
||||
emptyGarbage: ()=>
|
||||
for o in @garbage
|
||||
#if @getOperationCounter(o.uid.creator) > o.uid.op_number
|
||||
o.cleanup?()
|
||||
|
||||
@garbage = @trash
|
||||
@trash = []
|
||||
if @garbageCollectTimeout isnt -1
|
||||
@garbageCollectTimeoutId = setTimeout @emptyGarbage, @garbageCollectTimeout
|
||||
undefined
|
||||
|
||||
#
|
||||
# Get the user id with wich the History Buffer was initialized.
|
||||
#
|
||||
getUserId: ()->
|
||||
@user_id
|
||||
|
||||
addToGarbageCollector: ()->
|
||||
if @performGarbageCollection
|
||||
for o in arguments
|
||||
if o?
|
||||
@garbage.push o
|
||||
|
||||
stopGarbageCollection: ()->
|
||||
@performGarbageCollection = false
|
||||
@setManualGarbageCollect()
|
||||
@garbage = []
|
||||
@trash = []
|
||||
|
||||
setManualGarbageCollect: ()->
|
||||
@garbageCollectTimeout = -1
|
||||
clearTimeout @garbageCollectTimeoutId
|
||||
@garbageCollectTimeoutId = undefined
|
||||
|
||||
setGarbageCollectTimeout: (@garbageCollectTimeout)->
|
||||
|
||||
#
|
||||
# I propose to use it in your Framework, to create something like a root element.
|
||||
# An operation with this identifier is not propagated to other clients.
|
||||
# This is why everybode must create the same operation with this uid.
|
||||
#
|
||||
getReservedUniqueIdentifier: ()->
|
||||
{
|
||||
creator : '_'
|
||||
op_number : "_#{@reserved_identifier_counter++}"
|
||||
doSync: false
|
||||
}
|
||||
|
||||
#
|
||||
# Get the operation counter that describes the current state of the document.
|
||||
#
|
||||
getOperationCounter: (user_id)->
|
||||
if not user_id?
|
||||
res = {}
|
||||
for user,ctn of @operation_counter
|
||||
res[user] = ctn
|
||||
res
|
||||
else
|
||||
@operation_counter[user_id]
|
||||
|
||||
isExpectedOperation: (o)->
|
||||
@operation_counter[o.uid.creator] ?= 0
|
||||
o.uid.op_number <= @operation_counter[o.uid.creator]
|
||||
true #TODO: !! this could break stuff. But I dunno why
|
||||
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
# TODO: Make this more efficient!
|
||||
_encode: (state_vector={})->
|
||||
json = []
|
||||
unknown = (user, o_number)->
|
||||
if (not user?) or (not o_number?)
|
||||
throw new Error "dah!"
|
||||
not state_vector[user]? or state_vector[user] <= o_number
|
||||
|
||||
for u_name,user of @buffer
|
||||
# TODO next, if @state_vector[user] <= state_vector[user]
|
||||
for o_number,o of user
|
||||
if o.uid.doSync and unknown(u_name, o_number)
|
||||
# its necessary to send it, and not known in state_vector
|
||||
o_json = o._encode()
|
||||
if o.next_cl? # applies for all ops but the most right delimiter!
|
||||
# search for the next _known_ operation. (When state_vector is {} then this is the Delimiter)
|
||||
o_next = o.next_cl
|
||||
while o_next.next_cl? and unknown(o_next.uid.creator, o_next.uid.op_number)
|
||||
o_next = o_next.next_cl
|
||||
o_json.next = o_next.getUid()
|
||||
else if o.prev_cl? # most right delimiter only!
|
||||
# same as the above with prev.
|
||||
o_prev = o.prev_cl
|
||||
while o_prev.prev_cl? and unknown(o_prev.uid.creator, o_prev.uid.op_number)
|
||||
o_prev = o_prev.prev_cl
|
||||
o_json.prev = o_prev.getUid()
|
||||
json.push o_json
|
||||
|
||||
json
|
||||
|
||||
#
|
||||
# Get the number of operations that were created by a user.
|
||||
# Accordingly you will get the next operation number that is expected from that user.
|
||||
# This will increment the operation counter.
|
||||
#
|
||||
getNextOperationIdentifier: (user_id)->
|
||||
if not user_id?
|
||||
user_id = @user_id
|
||||
if not @operation_counter[user_id]?
|
||||
@operation_counter[user_id] = 0
|
||||
uid =
|
||||
'creator' : user_id
|
||||
'op_number' : @operation_counter[user_id]
|
||||
'doSync' : true
|
||||
@operation_counter[user_id]++
|
||||
uid
|
||||
|
||||
#
|
||||
# Retrieve an operation from a unique id.
|
||||
#
|
||||
# when uid has a "sub" property, the value of it will be applied
|
||||
# on the operations retrieveSub method (which must! be defined)
|
||||
#
|
||||
getOperation: (uid)->
|
||||
if uid.uid?
|
||||
uid = uid.uid
|
||||
o = @buffer[uid.creator]?[uid.op_number]
|
||||
if uid.sub? and o?
|
||||
o.retrieveSub uid.sub
|
||||
else
|
||||
o
|
||||
|
||||
#
|
||||
# Add an operation to the HB. Note that this will not link it against
|
||||
# other operations (it wont executed)
|
||||
#
|
||||
addOperation: (o)->
|
||||
if not @buffer[o.uid.creator]?
|
||||
@buffer[o.uid.creator] = {}
|
||||
if @buffer[o.uid.creator][o.uid.op_number]?
|
||||
throw new Error "You must not overwrite operations!"
|
||||
if (o.uid.op_number.constructor isnt String) and (not @isExpectedOperation(o)) and (not o.fromHB?) # you already do this in the engine, so delete it here!
|
||||
throw new Error "this operation was not expected!"
|
||||
@addToCounter(o)
|
||||
@buffer[o.uid.creator][o.uid.op_number] = o
|
||||
o
|
||||
|
||||
removeOperation: (o)->
|
||||
delete @buffer[o.uid.creator]?[o.uid.op_number]
|
||||
|
||||
# When the HB determines inconsistencies, then the invokeSync
|
||||
# handler wil be called, which should somehow invoke the sync with another collaborator.
|
||||
# The parameter of the sync handler is the user_id with wich an inconsistency was determined
|
||||
setInvokeSyncHandler: (f)->
|
||||
@invokeSync = f
|
||||
|
||||
# empty per default # TODO: do i need this?
|
||||
invokeSync: ()->
|
||||
|
||||
# after you received the HB of another user (in the sync process),
|
||||
# you renew your own state_vector to the state_vector of the other user
|
||||
renewStateVector: (state_vector)->
|
||||
for user,state of state_vector
|
||||
if (not @operation_counter[user]?) or (@operation_counter[user] < state_vector[user])
|
||||
@operation_counter[user] = state_vector[user]
|
||||
|
||||
#
|
||||
# Increment the operation_counter that defines the current state of the Engine.
|
||||
#
|
||||
addToCounter: (o)->
|
||||
if not @operation_counter[o.uid.creator]?
|
||||
@operation_counter[o.uid.creator] = 0
|
||||
if typeof o.uid.op_number is 'number' and o.uid.creator isnt @getUserId()
|
||||
# TODO: check if operations are send in order
|
||||
if o.uid.op_number is @operation_counter[o.uid.creator]
|
||||
@operation_counter[o.uid.creator]++
|
||||
else
|
||||
@invokeSync o.uid.creator
|
||||
|
||||
#if @operation_counter[o.uid.creator] isnt (o.uid.op_number + 1)
|
||||
#console.log (@operation_counter[o.uid.creator] - (o.uid.op_number + 1))
|
||||
#console.log o
|
||||
#throw new Error "You don't receive operations in the proper order. Try counting like this 0,1,2,3,4,.. ;)"
|
||||
|
||||
module.exports = HistoryBuffer
|
||||
@@ -1,555 +0,0 @@
|
||||
module.exports = (HB)->
|
||||
# @see Engine.parse
|
||||
types = {}
|
||||
execution_listener = []
|
||||
|
||||
#
|
||||
# @private
|
||||
# @abstract
|
||||
# @nodoc
|
||||
# A generic interface to operations.
|
||||
#
|
||||
# An operation has the following methods:
|
||||
# * _encode: encodes an operation (needed only if instance of this operation is sent).
|
||||
# * execute: execute the effects of this operations. Good examples are Insert-type and AddName-type
|
||||
# * val: in the case that the operation holds a value
|
||||
#
|
||||
# Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations.
|
||||
#
|
||||
class types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier.
|
||||
# If uid is undefined, a new uid will be created before at the end of the execution sequence
|
||||
#
|
||||
constructor: (uid)->
|
||||
@is_deleted = false
|
||||
@garbage_collected = false
|
||||
@event_listeners = [] # TODO: rename to observers or sth like that
|
||||
if uid?
|
||||
@uid = uid
|
||||
|
||||
type: "Operation"
|
||||
|
||||
retrieveSub: ()->
|
||||
throw new Error "sub properties are not enable on this operation type!"
|
||||
|
||||
#
|
||||
# Add an event listener. It depends on the operation which events are supported.
|
||||
# @param {Function} f f is executed in case the event fires.
|
||||
#
|
||||
observe: (f)->
|
||||
@event_listeners.push f
|
||||
|
||||
#
|
||||
# Deletes function from the observer list
|
||||
# @see Operation.observe
|
||||
#
|
||||
# @overload unobserve(event, f)
|
||||
# @param f {Function} The function that you want to delete
|
||||
unobserve: (f)->
|
||||
@event_listeners = @event_listeners.filter (g)->
|
||||
f isnt g
|
||||
|
||||
#
|
||||
# Deletes all subscribed event listeners.
|
||||
# This should be called, e.g. after this has been replaced.
|
||||
# (Then only one replace event should fire. )
|
||||
# This is also called in the cleanup method.
|
||||
deleteAllObservers: ()->
|
||||
@event_listeners = []
|
||||
|
||||
delete: ()->
|
||||
(new types.Delete undefined, @).execute()
|
||||
null
|
||||
|
||||
#
|
||||
# Fire an event.
|
||||
# TODO: Do something with timeouts. You don't want this to fire for every operation (e.g. insert).
|
||||
# TODO: do you need callEvent+forwardEvent? Only one suffices probably
|
||||
callEvent: ()->
|
||||
@forwardEvent @, arguments...
|
||||
|
||||
#
|
||||
# Fire an event and specify in which context the listener is called (set 'this').
|
||||
# TODO: do you need this ?
|
||||
forwardEvent: (op, args...)->
|
||||
for f in @event_listeners
|
||||
f.call op, args...
|
||||
|
||||
isDeleted: ()->
|
||||
@is_deleted
|
||||
|
||||
applyDelete: (garbagecollect = true)->
|
||||
if not @garbage_collected
|
||||
#console.log "applyDelete: #{@type}"
|
||||
@is_deleted = true
|
||||
if garbagecollect
|
||||
@garbage_collected = true
|
||||
HB.addToGarbageCollector @
|
||||
|
||||
cleanup: ()->
|
||||
#console.log "cleanup: #{@type}"
|
||||
HB.removeOperation @
|
||||
@deleteAllObservers()
|
||||
|
||||
#
|
||||
# Set the parent of this operation.
|
||||
#
|
||||
setParent: (@parent)->
|
||||
|
||||
#
|
||||
# Get the parent of this operation.
|
||||
#
|
||||
getParent: ()->
|
||||
@parent
|
||||
|
||||
#
|
||||
# Computes a unique identifier (uid) that identifies this operation.
|
||||
#
|
||||
getUid: ()->
|
||||
if not @uid.noOperation?
|
||||
@uid
|
||||
else
|
||||
@uid.alt # could be (safely) undefined
|
||||
|
||||
cloneUid: ()->
|
||||
uid = {}
|
||||
for n,v of @getUid()
|
||||
uid[n] = v
|
||||
uid
|
||||
|
||||
dontSync: ()->
|
||||
@uid.doSync = false
|
||||
|
||||
#
|
||||
# @private
|
||||
# If not already done, set the uid
|
||||
# Add this to the HB
|
||||
# Notify the all the listeners.
|
||||
#
|
||||
execute: ()->
|
||||
@is_executed = true
|
||||
if not @uid?
|
||||
# When this operation was created without a uid, then set it here.
|
||||
# There is only one other place, where this can be done - before an Insertion
|
||||
# is executed (because we need the creator_id)
|
||||
@uid = HB.getNextOperationIdentifier()
|
||||
if not @uid.noOperation?
|
||||
HB.addOperation @
|
||||
for l in execution_listener
|
||||
l @_encode()
|
||||
@
|
||||
|
||||
#
|
||||
# @private
|
||||
# Operations may depend on other operations (linked lists, etc.).
|
||||
# The saveOperation and validateSavedOperations methods provide
|
||||
# an easy way to refer to these operations via an uid or object reference.
|
||||
#
|
||||
# For example: We can create a new Delete operation that deletes the operation $o like this
|
||||
# - var d = new Delete(uid, $o); or
|
||||
# - var d = new Delete(uid, $o.getUid());
|
||||
# Either way we want to access $o via d.deletes. In the second case validateSavedOperations must be called first.
|
||||
#
|
||||
# @overload saveOperation(name, op_uid)
|
||||
# @param {String} name The name of the operation. After validating (with validateSavedOperations) the instantiated operation will be accessible via this[name].
|
||||
# @param {Object} op_uid A uid that refers to an operation
|
||||
# @overload saveOperation(name, op)
|
||||
# @param {String} name The name of the operation. After calling this function op is accessible via this[name].
|
||||
# @param {Operation} op An Operation object
|
||||
#
|
||||
saveOperation: (name, op)->
|
||||
|
||||
#
|
||||
# Every instance of $Operation must have an $execute function.
|
||||
# We use duck-typing to check if op is instantiated since there
|
||||
# could exist multiple classes of $Operation
|
||||
#
|
||||
if op?.execute?
|
||||
# is instantiated
|
||||
@[name] = op
|
||||
else if op?
|
||||
# not initialized. Do it when calling $validateSavedOperations()
|
||||
@unchecked ?= {}
|
||||
@unchecked[name] = op
|
||||
|
||||
#
|
||||
# @private
|
||||
# After calling this function all not instantiated operations will be accessible.
|
||||
# @see Operation.saveOperation
|
||||
#
|
||||
# @return [Boolean] Whether it was possible to instantiate all operations.
|
||||
#
|
||||
validateSavedOperations: ()->
|
||||
uninstantiated = {}
|
||||
success = @
|
||||
for name, op_uid of @unchecked
|
||||
op = HB.getOperation op_uid
|
||||
if op
|
||||
@[name] = op
|
||||
else
|
||||
uninstantiated[name] = op_uid
|
||||
success = false
|
||||
delete @unchecked
|
||||
if not success
|
||||
@unchecked = uninstantiated
|
||||
success
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# A simple Delete-type operation that deletes an operation.
|
||||
#
|
||||
class types.Delete extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Object} deletes UID or reference of the operation that this to be deleted.
|
||||
#
|
||||
constructor: (uid, deletes)->
|
||||
@saveOperation 'deletes', deletes
|
||||
super uid
|
||||
|
||||
type: "Delete"
|
||||
|
||||
#
|
||||
# @private
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be sent to other clients.
|
||||
#
|
||||
_encode: ()->
|
||||
{
|
||||
'type': "Delete"
|
||||
'uid': @getUid()
|
||||
'deletes': @deletes.getUid()
|
||||
}
|
||||
|
||||
#
|
||||
# @private
|
||||
# Apply the deletion.
|
||||
#
|
||||
execute: ()->
|
||||
if @validateSavedOperations()
|
||||
res = super
|
||||
if res
|
||||
@deletes.applyDelete @
|
||||
res
|
||||
else
|
||||
false
|
||||
|
||||
#
|
||||
# Define how to parse Delete operations.
|
||||
#
|
||||
types.Delete.parse = (o)->
|
||||
{
|
||||
'uid' : uid
|
||||
'deletes': deletes_uid
|
||||
} = o
|
||||
new this(uid, deletes_uid)
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# A simple insert-type operation.
|
||||
#
|
||||
# An insert operation is always positioned between two other insert operations.
|
||||
# Internally this is realized as associative lists, whereby each insert operation has a predecessor and a successor.
|
||||
# For the sake of efficiency we maintain two lists:
|
||||
# - The short-list (abbrev. sl) maintains only the operations that are not deleted
|
||||
# - The complete-list (abbrev. cl) maintains all operations
|
||||
#
|
||||
class types.Insert extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
|
||||
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
|
||||
#
|
||||
constructor: (uid, prev_cl, next_cl, origin, parent)->
|
||||
@saveOperation 'parent', parent
|
||||
@saveOperation 'prev_cl', prev_cl
|
||||
@saveOperation 'next_cl', next_cl
|
||||
if origin?
|
||||
@saveOperation 'origin', origin
|
||||
else
|
||||
@saveOperation 'origin', prev_cl
|
||||
super uid
|
||||
|
||||
type: "Insert"
|
||||
|
||||
#
|
||||
# set content to null and other stuff
|
||||
# @private
|
||||
#
|
||||
applyDelete: (o)->
|
||||
@deleted_by ?= []
|
||||
callLater = false
|
||||
if @parent? and not @isDeleted() and o? # o? : if not o?, then the delimiter deleted this Insertion. Furthermore, it would be wrong to call it. TODO: make this more expressive and save
|
||||
# call iff wasn't deleted earlyer
|
||||
callLater = true
|
||||
if o?
|
||||
@deleted_by.push o
|
||||
garbagecollect = false
|
||||
if @next_cl.isDeleted()
|
||||
garbagecollect = true
|
||||
super garbagecollect
|
||||
if callLater
|
||||
@callOperationSpecificDeleteEvents(o)
|
||||
if @prev_cl?.isDeleted()
|
||||
# garbage collect prev_cl
|
||||
@prev_cl.applyDelete()
|
||||
|
||||
cleanup: ()->
|
||||
if @next_cl.isDeleted()
|
||||
# delete all ops that delete this insertion
|
||||
for d in @deleted_by
|
||||
d.cleanup()
|
||||
|
||||
# throw new Error "right is not deleted. inconsistency!, wrararar"
|
||||
# change origin references to the right
|
||||
o = @next_cl
|
||||
while o.type isnt "Delimiter"
|
||||
if o.origin is @
|
||||
o.origin = @prev_cl
|
||||
o = o.next_cl
|
||||
# reconnect left/right
|
||||
@prev_cl.next_cl = @next_cl
|
||||
@next_cl.prev_cl = @prev_cl
|
||||
super
|
||||
# else
|
||||
# Someone inserted something in the meantime.
|
||||
# Remember: this can only be garbage collected when next_cl is deleted
|
||||
|
||||
#
|
||||
# @private
|
||||
# The amount of positions that $this operation was moved to the right.
|
||||
#
|
||||
getDistanceToOrigin: ()->
|
||||
d = 0
|
||||
o = @prev_cl
|
||||
while true
|
||||
if @origin is o
|
||||
break
|
||||
d++
|
||||
o = o.prev_cl
|
||||
d
|
||||
|
||||
#
|
||||
# @private
|
||||
# Include this operation in the associative lists.
|
||||
execute: ()->
|
||||
if not @validateSavedOperations()
|
||||
return false
|
||||
else
|
||||
if @parent?
|
||||
if not @prev_cl?
|
||||
@prev_cl = @parent.beginning
|
||||
if not @origin?
|
||||
@origin = @parent.beginning
|
||||
if not @next_cl?
|
||||
@next_cl = @parent.end
|
||||
if @prev_cl?
|
||||
distance_to_origin = @getDistanceToOrigin() # most cases: 0
|
||||
o = @prev_cl.next_cl
|
||||
i = distance_to_origin # loop counter
|
||||
|
||||
# $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!)
|
||||
while true
|
||||
if o isnt @next_cl
|
||||
# $o happened concurrently
|
||||
if o.getDistanceToOrigin() is i
|
||||
# case 1
|
||||
if o.uid.creator < @uid.creator
|
||||
@prev_cl = o
|
||||
distance_to_origin = i + 1
|
||||
else
|
||||
# nop
|
||||
else if o.getDistanceToOrigin() < i
|
||||
# case 2
|
||||
if i - distance_to_origin <= o.getDistanceToOrigin()
|
||||
@prev_cl = o
|
||||
distance_to_origin = i + 1
|
||||
else
|
||||
#nop
|
||||
else
|
||||
# case 3
|
||||
break
|
||||
i++
|
||||
o = o.next_cl
|
||||
else
|
||||
# $this knows that $o exists,
|
||||
break
|
||||
# now reconnect everything
|
||||
@next_cl = @prev_cl.next_cl
|
||||
@prev_cl.next_cl = @
|
||||
@next_cl.prev_cl = @
|
||||
|
||||
@setParent @prev_cl.getParent() # do Insertions always have a parent?
|
||||
super # notify the execution_listeners
|
||||
@callOperationSpecificInsertEvents()
|
||||
@
|
||||
|
||||
callOperationSpecificInsertEvents: ()->
|
||||
@parent?.callEvent [
|
||||
type: "insert"
|
||||
position: @getPosition()
|
||||
object: @parent
|
||||
changedBy: @uid.creator
|
||||
value: @content
|
||||
]
|
||||
|
||||
callOperationSpecificDeleteEvents: (o)->
|
||||
@parent.callEvent [
|
||||
type: "delete"
|
||||
position: @getPosition()
|
||||
object: @parent # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
|
||||
length: 1
|
||||
changedBy: o.uid.creator
|
||||
]
|
||||
|
||||
#
|
||||
# Compute the position of this operation.
|
||||
#
|
||||
getPosition: ()->
|
||||
position = 0
|
||||
prev = @prev_cl
|
||||
while true
|
||||
if prev instanceof types.Delimiter
|
||||
break
|
||||
if not prev.isDeleted()
|
||||
position++
|
||||
prev = prev.prev_cl
|
||||
position
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
|
||||
#
|
||||
class types.ImmutableObject extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Object} content
|
||||
#
|
||||
constructor: (uid, @content)->
|
||||
super uid
|
||||
|
||||
type: "ImmutableObject"
|
||||
|
||||
#
|
||||
# @return [String] The content of this operation.
|
||||
#
|
||||
val : ()->
|
||||
@content
|
||||
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'content' : @content
|
||||
}
|
||||
json
|
||||
|
||||
types.ImmutableObject.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'content' : content
|
||||
} = json
|
||||
new this(uid, content)
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# A delimiter is placed at the end and at the beginning of the associative lists.
|
||||
# This is necessary in order to have a beginning and an end even if the content
|
||||
# of the Engine is empty.
|
||||
#
|
||||
class types.Delimiter extends types.Operation
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
|
||||
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
|
||||
#
|
||||
constructor: (prev_cl, next_cl, origin)->
|
||||
@saveOperation 'prev_cl', prev_cl
|
||||
@saveOperation 'next_cl', next_cl
|
||||
@saveOperation 'origin', prev_cl
|
||||
super {noOperation: true}
|
||||
|
||||
type: "Delimiter"
|
||||
|
||||
applyDelete: ()->
|
||||
super()
|
||||
o = @prev_cl
|
||||
while o?
|
||||
o.applyDelete()
|
||||
o = o.prev_cl
|
||||
undefined
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
#
|
||||
# @private
|
||||
#
|
||||
execute: ()->
|
||||
if @unchecked?['next_cl']?
|
||||
super
|
||||
else if @unchecked?['prev_cl']
|
||||
if @validateSavedOperations()
|
||||
if @prev_cl.next_cl?
|
||||
throw new Error "Probably duplicated operations"
|
||||
@prev_cl.next_cl = @
|
||||
super
|
||||
else
|
||||
false
|
||||
else if @prev_cl? and not @prev_cl.next_cl?
|
||||
delete @prev_cl.unchecked.next_cl
|
||||
@prev_cl.next_cl = @
|
||||
super
|
||||
else if @prev_cl? or @next_cl? or true # TODO: are you sure? This can happen right?
|
||||
super
|
||||
#else
|
||||
# throw new Error "Delimiter is unsufficient defined!"
|
||||
|
||||
#
|
||||
# @private
|
||||
#
|
||||
_encode: ()->
|
||||
{
|
||||
'type' : @type
|
||||
'uid' : @getUid()
|
||||
'prev' : @prev_cl?.getUid()
|
||||
'next' : @next_cl?.getUid()
|
||||
}
|
||||
|
||||
types.Delimiter.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'prev' : prev
|
||||
'next' : next
|
||||
} = json
|
||||
new this(uid, prev, next)
|
||||
|
||||
# This is what this module exports after initializing it with the HistoryBuffer
|
||||
{
|
||||
'types' : types
|
||||
'execution_listener' : execution_listener
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
text_types_uninitialized = require "./TextTypes"
|
||||
|
||||
module.exports = (HB)->
|
||||
text_types = text_types_uninitialized HB
|
||||
types = text_types.types
|
||||
|
||||
#
|
||||
# Manages Object-like values.
|
||||
#
|
||||
class types.Object extends types.MapManager
|
||||
|
||||
#
|
||||
# Identifies this class.
|
||||
# Use it to check whether this is a json-type or something else.
|
||||
#
|
||||
# @example
|
||||
# var x = y.val('unknown')
|
||||
# if (x.type === "Object") {
|
||||
# console.log JSON.stringify(x.toJson())
|
||||
# }
|
||||
#
|
||||
type: "Object"
|
||||
|
||||
applyDelete: ()->
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
|
||||
#
|
||||
# Transform this to a Json. If your browser supports Object.observe it will be transformed automatically when a change arrives.
|
||||
# Otherwise you will loose all the sharing-abilities (the new object will be a deep clone)!
|
||||
# @return {Json}
|
||||
#
|
||||
# TODO: at the moment you don't consider changing of properties.
|
||||
# E.g.: let x = {a:[]}. Then x.a.push 1 wouldn't change anything
|
||||
#
|
||||
toJson: (transform_to_value = false)->
|
||||
if not @bound_json? or not Object.observe? or true # TODO: currently, you are not watching mutable strings for changes, and, therefore, the @bound_json is not updated. TODO TODO wuawuawua easy
|
||||
val = @val()
|
||||
json = {}
|
||||
for name, o of val
|
||||
if o instanceof types.Object
|
||||
json[name] = o.toJson(transform_to_value)
|
||||
else if o instanceof types.Array
|
||||
json[name] = o.toJson(transform_to_value)
|
||||
else if transform_to_value and o instanceof types.Operation
|
||||
json[name] = o.val()
|
||||
else
|
||||
json[name] = o
|
||||
@bound_json = json
|
||||
if Object.observe?
|
||||
that = @
|
||||
Object.observe @bound_json, (events)->
|
||||
for event in events
|
||||
if not event.changedBy? and (event.type is "add" or event.type = "update")
|
||||
# this event is not created by Y.
|
||||
that.val(event.name, event.object[event.name])
|
||||
@observe (events)->
|
||||
for event in events
|
||||
if event.created_ isnt HB.getUserId()
|
||||
notifier = Object.getNotifier(that.bound_json)
|
||||
oldVal = that.bound_json[event.name]
|
||||
if oldVal?
|
||||
notifier.performChange 'update', ()->
|
||||
that.bound_json[event.name] = that.val(event.name)
|
||||
, that.bound_json
|
||||
notifier.notify
|
||||
object: that.bound_json
|
||||
type: 'update'
|
||||
name: event.name
|
||||
oldValue: oldVal
|
||||
changedBy: event.changedBy
|
||||
else
|
||||
notifier.performChange 'add', ()->
|
||||
that.bound_json[event.name] = that.val(event.name)
|
||||
, that.bound_json
|
||||
notifier.notify
|
||||
object: that.bound_json
|
||||
type: 'add'
|
||||
name: event.name
|
||||
oldValue: oldVal
|
||||
changedBy:event.changedBy
|
||||
@bound_json
|
||||
|
||||
#
|
||||
# @overload val()
|
||||
# Get this as a Json object.
|
||||
# @return [Json]
|
||||
#
|
||||
# @overload val(name)
|
||||
# Get value of a property.
|
||||
# @param {String} name Name of the object property.
|
||||
# @return [Object Type||String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
|
||||
#
|
||||
# @overload val(name, content)
|
||||
# Set a new property.
|
||||
# @param {String} name Name of the object property.
|
||||
# @param {Object|String} content Content of the object property.
|
||||
# @return [Object Type] This object. (supports chaining)
|
||||
#
|
||||
val: (name, content)->
|
||||
if name? and arguments.length > 1
|
||||
if content? and content.constructor?
|
||||
type = types[content.constructor.name]
|
||||
if type? and type.create?
|
||||
args = []
|
||||
for i in [1...arguments.length]
|
||||
args.push arguments[i]
|
||||
o = type.create.apply null, args
|
||||
super name, o
|
||||
else
|
||||
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Y."
|
||||
else
|
||||
super name, content
|
||||
else # is this even necessary ? I have to define every type anyway.. (see Number type below)
|
||||
super name
|
||||
|
||||
#
|
||||
# @private
|
||||
#
|
||||
_encode: ()->
|
||||
{
|
||||
'type' : @type
|
||||
'uid' : @getUid()
|
||||
}
|
||||
|
||||
types.Object.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
} = json
|
||||
new this(uid)
|
||||
|
||||
types.Object.create = (content, mutable)->
|
||||
json = new types.Object().execute()
|
||||
for n,o of content
|
||||
json.val n, o, mutable
|
||||
json
|
||||
|
||||
|
||||
types.Number = {}
|
||||
types.Number.create = (content)->
|
||||
content
|
||||
|
||||
text_types
|
||||
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
basic_types_uninitialized = require "./BasicTypes"
|
||||
|
||||
module.exports = (HB)->
|
||||
basic_types = basic_types_uninitialized HB
|
||||
types = basic_types.types
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Manages map like objects. E.g. Json-Type and XML attributes.
|
||||
#
|
||||
class types.MapManager extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
#
|
||||
constructor: (uid)->
|
||||
@map = {}
|
||||
super uid
|
||||
|
||||
type: "MapManager"
|
||||
|
||||
applyDelete: ()->
|
||||
for name,p of @map
|
||||
p.applyDelete()
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
#
|
||||
# @see JsonTypes.val
|
||||
#
|
||||
val: (name, content)->
|
||||
if arguments.length > 1
|
||||
@retrieveSub(name).replace content
|
||||
@
|
||||
else if name?
|
||||
prop = @map[name]
|
||||
if prop? and not prop.isContentDeleted()
|
||||
prop.val()
|
||||
else
|
||||
undefined
|
||||
else
|
||||
result = {}
|
||||
for name,o of @map
|
||||
if not o.isContentDeleted()
|
||||
result[name] = o.val()
|
||||
result
|
||||
|
||||
delete: (name)->
|
||||
@map[name]?.deleteContent()
|
||||
@
|
||||
|
||||
retrieveSub: (property_name)->
|
||||
if not @map[property_name]?
|
||||
event_properties =
|
||||
name: property_name
|
||||
event_this = @
|
||||
map_uid = @cloneUid()
|
||||
map_uid.sub = property_name
|
||||
rm_uid =
|
||||
noOperation: true
|
||||
alt: map_uid
|
||||
rm = new types.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
|
||||
@map[property_name] = rm
|
||||
rm.setParent @, property_name
|
||||
rm.execute()
|
||||
@map[property_name]
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Manages a list of Insert-type operations.
|
||||
#
|
||||
class types.ListManager extends types.Operation
|
||||
|
||||
#
|
||||
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Delimiter} beginning Reference or Object.
|
||||
# @param {Delimiter} end Reference or Object.
|
||||
constructor: (uid)->
|
||||
@beginning = new types.Delimiter undefined, undefined
|
||||
@end = new types.Delimiter @beginning, undefined
|
||||
@beginning.next_cl = @end
|
||||
@beginning.execute()
|
||||
@end.execute()
|
||||
super uid
|
||||
|
||||
type: "ListManager"
|
||||
|
||||
#
|
||||
# @private
|
||||
# @see Operation.execute
|
||||
#
|
||||
execute: ()->
|
||||
if @validateSavedOperations()
|
||||
@beginning.setParent @
|
||||
@end.setParent @
|
||||
super
|
||||
else
|
||||
false
|
||||
|
||||
# Get the element previous to the delemiter at the end
|
||||
getLastOperation: ()->
|
||||
@end.prev_cl
|
||||
|
||||
# similar to the above
|
||||
getFirstOperation: ()->
|
||||
@beginning.next_cl
|
||||
|
||||
# Transforms the the list to an array
|
||||
# Doesn't return left-right delimiter.
|
||||
toArray: ()->
|
||||
o = @beginning.next_cl
|
||||
result = []
|
||||
while o isnt @end
|
||||
result.push o
|
||||
o = o.next_cl
|
||||
result
|
||||
|
||||
#
|
||||
# Retrieves the x-th not deleted element.
|
||||
# e.g. "abc" : the 1th character is "a"
|
||||
# the 0th character is the left Delimiter
|
||||
#
|
||||
getOperationByPosition: (position)->
|
||||
o = @beginning
|
||||
while true
|
||||
# find the i-th op
|
||||
if o instanceof types.Delimiter and o.prev_cl?
|
||||
# the user or you gave a position parameter that is to big
|
||||
# for the current array. Therefore we reach a Delimiter.
|
||||
# Then, we'll just return the last character.
|
||||
o = o.prev_cl
|
||||
while o.isDeleted() or not (o instanceof types.Delimiter)
|
||||
o = o.prev_cl
|
||||
break
|
||||
if position <= 0 and not o.isDeleted()
|
||||
break
|
||||
|
||||
o = o.next_cl
|
||||
if not o.isDeleted()
|
||||
position -= 1
|
||||
o
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Adds support for replace. The ReplaceManager manages Replaceable operations.
|
||||
# Each Replaceable holds a value that is now replaceable.
|
||||
#
|
||||
# The TextType-type has implemented support for replace
|
||||
# @see TextType
|
||||
#
|
||||
class types.ReplaceManager extends types.ListManager
|
||||
#
|
||||
# @param {Object} event_properties Decorates the event that is thrown by the RM
|
||||
# @param {Object} event_this The object on which the event shall be executed
|
||||
# @param {Operation} initial_content Initialize this with a Replaceable that holds the initial_content.
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Delimiter} beginning Reference or Object.
|
||||
# @param {Delimiter} end Reference or Object.
|
||||
constructor: (@event_properties, @event_this, uid, beginning, end)->
|
||||
if not @event_properties['object']?
|
||||
@event_properties['object'] = @event_this
|
||||
super uid, beginning, end
|
||||
|
||||
type: "ReplaceManager"
|
||||
|
||||
applyDelete: ()->
|
||||
o = @beginning
|
||||
while o?
|
||||
o.applyDelete()
|
||||
o = o.next_cl
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
#
|
||||
# This doesn't throw the same events as the ListManager. Therefore, the
|
||||
# Replaceables also not throw the same events.
|
||||
# So, ReplaceManager and ListManager both implement
|
||||
# these functions that are called when an Insertion is executed (at the end).
|
||||
#
|
||||
#
|
||||
callEventDecorator: (events)->
|
||||
if not @isDeleted()
|
||||
for event in events
|
||||
for name,prop of @event_properties
|
||||
event[name] = prop
|
||||
@event_this.callEvent events
|
||||
undefined
|
||||
|
||||
#
|
||||
# Replace the existing word with a new word.
|
||||
#
|
||||
# @param content {Operation} The new value of this ReplaceManager.
|
||||
# @param replaceable_uid {UID} Optional: Unique id of the Replaceable that is created
|
||||
#
|
||||
replace: (content, replaceable_uid)->
|
||||
o = @getLastOperation()
|
||||
relp = (new types.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
|
||||
# TODO: delete repl (for debugging)
|
||||
undefined
|
||||
|
||||
isContentDeleted: ()->
|
||||
@getLastOperation().isDeleted()
|
||||
|
||||
deleteContent: ()->
|
||||
(new types.Delete undefined, @getLastOperation().uid).execute()
|
||||
undefined
|
||||
|
||||
#
|
||||
# Get the value of this
|
||||
# @return {String}
|
||||
#
|
||||
val: ()->
|
||||
o = @getLastOperation()
|
||||
#if o instanceof types.Delimiter
|
||||
# throw new Error "Replace Manager doesn't contain anything."
|
||||
o.val?() # ? - for the case that (currently) the RM does not contain anything (then o is a Delimiter)
|
||||
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'beginning' : @beginning.getUid()
|
||||
'end' : @end.getUid()
|
||||
}
|
||||
json
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# The ReplaceManager manages Replaceables.
|
||||
# @see ReplaceManager
|
||||
#
|
||||
class types.Replaceable extends types.Insert
|
||||
|
||||
#
|
||||
# @param {Operation} content The value that this Replaceable holds.
|
||||
# @param {ReplaceManager} parent Used to replace this Replaceable with another one.
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
#
|
||||
constructor: (content, parent, uid, prev, next, origin, is_deleted)->
|
||||
# see encode to see, why we are doing it this way
|
||||
if content? and content.creator?
|
||||
@saveOperation 'content', content
|
||||
else
|
||||
@content = content
|
||||
@saveOperation 'parent', parent
|
||||
super uid, prev, next, origin # Parent is already saved by Replaceable
|
||||
@is_deleted = is_deleted
|
||||
|
||||
type: "Replaceable"
|
||||
|
||||
#
|
||||
# Return the content that this operation holds.
|
||||
#
|
||||
val: ()->
|
||||
@content
|
||||
|
||||
applyDelete: ()->
|
||||
res = super
|
||||
if @content?
|
||||
if @next_cl.type isnt "Delimiter"
|
||||
@content.deleteAllObservers?()
|
||||
@content.applyDelete?()
|
||||
@content.dontSync?()
|
||||
@content = null
|
||||
res
|
||||
|
||||
cleanup: ()->
|
||||
super
|
||||
|
||||
#
|
||||
# This is called, when the Insert-type was successfully executed.
|
||||
# TODO: consider doing this in a more consistent manner. This could also be
|
||||
# done with execute. But currently, there are no specital Insert-types for ListManager.
|
||||
#
|
||||
callOperationSpecificInsertEvents: ()->
|
||||
if @next_cl.type is "Delimiter" and @prev_cl.type isnt "Delimiter"
|
||||
# this replaces another Replaceable
|
||||
if not @is_deleted # When this is received from the HB, this could already be deleted!
|
||||
old_value = @prev_cl.content
|
||||
@parent.callEventDecorator [
|
||||
type: "update"
|
||||
changedBy: @uid.creator
|
||||
oldValue: old_value
|
||||
]
|
||||
@prev_cl.applyDelete()
|
||||
else if @next_cl.type isnt "Delimiter"
|
||||
# This won't be recognized by the user, because another
|
||||
# concurrent operation is set as the current value of the RM
|
||||
@applyDelete()
|
||||
else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
|
||||
@parent.callEventDecorator [
|
||||
type: "add"
|
||||
changedBy: @uid.creator
|
||||
]
|
||||
undefined
|
||||
|
||||
callOperationSpecificDeleteEvents: (o)->
|
||||
if @next_cl.type is "Delimiter"
|
||||
@parent.callEventDecorator [
|
||||
type: "delete"
|
||||
changedBy: o.uid.creator
|
||||
oldValue: @content
|
||||
]
|
||||
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': @type
|
||||
'parent' : @parent.getUid()
|
||||
'prev': @prev_cl.getUid()
|
||||
'next': @next_cl.getUid()
|
||||
'origin' : @origin.getUid()
|
||||
'uid' : @getUid()
|
||||
'is_deleted': @is_deleted
|
||||
}
|
||||
if @content instanceof types.Operation
|
||||
json['content'] = @content.getUid()
|
||||
else
|
||||
# This could be a security concern.
|
||||
# Throw error if the users wants to trick us
|
||||
if @content? and @content.creator?
|
||||
throw new Error "You must not set creator here!"
|
||||
json['content'] = @content
|
||||
json
|
||||
|
||||
types.Replaceable.parse = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'parent' : parent
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
'is_deleted': is_deleted
|
||||
} = json
|
||||
new this(content, parent, uid, prev, next, origin, is_deleted)
|
||||
|
||||
|
||||
basic_types
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
structured_types_uninitialized = require "./StructuredTypes"
|
||||
|
||||
module.exports = (HB)->
|
||||
structured_types = structured_types_uninitialized HB
|
||||
types = structured_types.types
|
||||
parser = structured_types.parser
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Extends the basic Insert type to an operation that holds a text value
|
||||
#
|
||||
class types.TextInsert extends types.Insert
|
||||
#
|
||||
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
#
|
||||
constructor: (content, uid, prev, next, origin, parent)->
|
||||
if content?.creator
|
||||
@saveOperation 'content', content
|
||||
else
|
||||
@content = content
|
||||
super uid, prev, next, origin, parent
|
||||
|
||||
type: "TextInsert"
|
||||
|
||||
#
|
||||
# Retrieve the effective length of the $content of this operation.
|
||||
#
|
||||
getLength: ()->
|
||||
if @isDeleted()
|
||||
0
|
||||
else
|
||||
@content.length
|
||||
|
||||
applyDelete: ()->
|
||||
super # no braces indeed!
|
||||
if @content instanceof types.Operation
|
||||
@content.applyDelete()
|
||||
@content = null
|
||||
|
||||
execute: ()->
|
||||
if not @validateSavedOperations()
|
||||
return false
|
||||
else
|
||||
if @content instanceof types.Operation
|
||||
@content.insert_parent = @
|
||||
super()
|
||||
|
||||
#
|
||||
# The result will be concatenated with the results from the other insert operations
|
||||
# in order to retrieve the content of the engine.
|
||||
# @see HistoryBuffer.toExecutedArray
|
||||
#
|
||||
val: (current_position)->
|
||||
if @isDeleted() or not @content?
|
||||
""
|
||||
else
|
||||
@content
|
||||
|
||||
#
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be send to other clients.
|
||||
#
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'prev': @prev_cl.getUid()
|
||||
'next': @next_cl.getUid()
|
||||
'origin': @origin.getUid()
|
||||
'parent': @parent.getUid()
|
||||
}
|
||||
|
||||
if @content?.getUid?
|
||||
json['content'] = @content.getUid()
|
||||
else
|
||||
json['content'] = @content
|
||||
json
|
||||
|
||||
types.TextInsert.parse = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
'parent' : parent
|
||||
} = json
|
||||
new types.TextInsert content, uid, prev, next, origin, parent
|
||||
|
||||
|
||||
class types.Array extends types.ListManager
|
||||
|
||||
type: "Array"
|
||||
|
||||
applyDelete: ()->
|
||||
o = @end
|
||||
while o?
|
||||
o.applyDelete()
|
||||
o = o.prev_cl
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
toJson: (transform_to_value = false)->
|
||||
val = @val()
|
||||
for i, o in val
|
||||
if o instanceof types.Object
|
||||
o.toJson(transform_to_value)
|
||||
else if o instanceof types.Array
|
||||
o.toJson(transform_to_value)
|
||||
else if transform_to_value and o instanceof types.Operation
|
||||
o.val()
|
||||
else
|
||||
o
|
||||
|
||||
val: (pos)->
|
||||
if pos?
|
||||
o = @getOperationByPosition(pos+1)
|
||||
if not (o instanceof types.Delimiter)
|
||||
o.val()
|
||||
else
|
||||
throw new Error "this position does not exist"
|
||||
else
|
||||
o = @beginning.next_cl
|
||||
result = []
|
||||
while o isnt @end
|
||||
if not o.isDeleted()
|
||||
result.push o.val()
|
||||
o = o.next_cl
|
||||
result
|
||||
|
||||
push: (content)->
|
||||
@insertAfter @end.prev_cl, content
|
||||
|
||||
insertAfter: (left, content, options)->
|
||||
createContent = (content, options)->
|
||||
if content? and content.constructor?
|
||||
type = types[content.constructor.name]
|
||||
if type? and type.create?
|
||||
type.create content, options
|
||||
else
|
||||
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Y."
|
||||
else
|
||||
content
|
||||
|
||||
right = left.next_cl
|
||||
while right.isDeleted()
|
||||
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
|
||||
left = right.prev_cl
|
||||
|
||||
if content instanceof types.Operation
|
||||
(new types.TextInsert content, undefined, left, right).execute()
|
||||
else
|
||||
for c in content
|
||||
tmp = (new types.TextInsert createContent(c, options), undefined, left, right).execute()
|
||||
left = tmp
|
||||
@
|
||||
|
||||
#
|
||||
# Inserts a string into the word.
|
||||
#
|
||||
# @return {Array Type} This String object.
|
||||
#
|
||||
insert: (position, content, options)->
|
||||
ith = @getOperationByPosition position
|
||||
# the (i-1)th character. e.g. "abc" the 1th character is "a"
|
||||
# the 0th character is the left Delimiter
|
||||
@insertAfter ith, [content], options
|
||||
|
||||
#
|
||||
# Deletes a part of the word.
|
||||
#
|
||||
# @return {Array Type} This String object
|
||||
#
|
||||
delete: (position, length)->
|
||||
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
|
||||
|
||||
delete_ops = []
|
||||
for i in [0...length]
|
||||
if o instanceof types.Delimiter
|
||||
break
|
||||
d = (new types.Delete undefined, o).execute()
|
||||
o = o.next_cl
|
||||
while (not (o instanceof types.Delimiter)) and o.isDeleted()
|
||||
o = o.next_cl
|
||||
delete_ops.push d._encode()
|
||||
@
|
||||
|
||||
#
|
||||
# @private
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
}
|
||||
json
|
||||
|
||||
types.Array.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
} = json
|
||||
new this(uid)
|
||||
|
||||
types.Array.create = (content, mutable)->
|
||||
if (mutable is "mutable")
|
||||
list = new types.Array().execute()
|
||||
ith = list.getOperationByPosition 0
|
||||
list.insertAfter ith, content
|
||||
list
|
||||
else if (not mutable?) or (mutable is "immutable")
|
||||
content
|
||||
else
|
||||
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
|
||||
|
||||
#
|
||||
# Handles a String-like data structures with support for insert/delete at a word-position.
|
||||
# @note Currently, only Text is supported!
|
||||
#
|
||||
class types.String extends types.Array
|
||||
|
||||
#
|
||||
# @private
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
#
|
||||
constructor: (uid)->
|
||||
@textfields = []
|
||||
super uid
|
||||
|
||||
#
|
||||
# Identifies this class.
|
||||
# Use it to check whether this is a word-type or something else.
|
||||
#
|
||||
# @example
|
||||
# var x = y.val('unknown')
|
||||
# if (x.type === "String") {
|
||||
# console.log JSON.stringify(x.toJson())
|
||||
# }
|
||||
#
|
||||
type: "String"
|
||||
|
||||
#
|
||||
# Get the String-representation of this word.
|
||||
# @return {String} The String-representation of this object.
|
||||
#
|
||||
val: ()->
|
||||
c = for o in @toArray()
|
||||
if o.val?
|
||||
o.val()
|
||||
else
|
||||
""
|
||||
c.join('')
|
||||
|
||||
#
|
||||
# Same as String.val
|
||||
# @see String.val
|
||||
#
|
||||
toString: ()->
|
||||
@val()
|
||||
|
||||
#
|
||||
# Inserts a string into the word.
|
||||
#
|
||||
# @return {Array Type} This String object.
|
||||
#
|
||||
insert: (position, content, options)->
|
||||
ith = @getOperationByPosition position
|
||||
# the (i-1)th character. e.g. "abc" the 1th character is "a"
|
||||
# the 0th character is the left Delimiter
|
||||
@insertAfter ith, content, options
|
||||
|
||||
#
|
||||
# Bind this String to a textfield or input field.
|
||||
#
|
||||
# @example
|
||||
# var textbox = document.getElementById("textfield");
|
||||
# y.bind(textbox);
|
||||
#
|
||||
bind: (textfield, dom_root)->
|
||||
dom_root ?= window
|
||||
if (not dom_root.getSelection?)
|
||||
dom_root = window
|
||||
|
||||
# don't duplicate!
|
||||
for t in @textfields
|
||||
if t is textfield
|
||||
return
|
||||
creator_token = false;
|
||||
|
||||
word = @
|
||||
textfield.value = @val()
|
||||
@textfields.push textfield
|
||||
|
||||
if textfield.selectionStart? and textfield.setSelectionRange?
|
||||
createRange = (fix)->
|
||||
left = textfield.selectionStart
|
||||
right = textfield.selectionEnd
|
||||
if fix?
|
||||
left = fix left
|
||||
right = fix right
|
||||
{
|
||||
left: left
|
||||
right: right
|
||||
}
|
||||
|
||||
writeRange = (range)->
|
||||
writeContent word.val()
|
||||
textfield.setSelectionRange range.left, range.right
|
||||
|
||||
writeContent = (content)->
|
||||
textfield.value = content
|
||||
else
|
||||
createRange = (fix)->
|
||||
s = dom_root.getSelection()
|
||||
clength = textfield.textContent.length
|
||||
left = Math.min s.anchorOffset, clength
|
||||
right = Math.min s.focusOffset, clength
|
||||
if fix?
|
||||
left = fix left
|
||||
right = fix right
|
||||
{
|
||||
left: left
|
||||
right: right
|
||||
isReal: true
|
||||
}
|
||||
|
||||
writeRange = (range)->
|
||||
writeContent word.val()
|
||||
textnode = textfield.childNodes[0]
|
||||
if range.isReal and textnode?
|
||||
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
|
||||
r = document.createRange()
|
||||
r.setStart(textnode, range.left)
|
||||
r.setEnd(textnode, range.right)
|
||||
s = window.getSelection()
|
||||
s.removeAllRanges()
|
||||
s.addRange(r)
|
||||
writeContent = (content)->
|
||||
append = ""
|
||||
if content[content.length - 1] is " "
|
||||
content = content.slice(0,content.length-1)
|
||||
append = ' '
|
||||
textfield.textContent = content
|
||||
textfield.innerHTML += append
|
||||
|
||||
writeContent this.val()
|
||||
|
||||
@observe (events)->
|
||||
for event in events
|
||||
if not creator_token
|
||||
if event.type is "insert"
|
||||
o_pos = event.position
|
||||
fix = (cursor)->
|
||||
if cursor <= o_pos
|
||||
cursor
|
||||
else
|
||||
cursor += 1
|
||||
cursor
|
||||
r = createRange fix
|
||||
writeRange r
|
||||
|
||||
else if event.type is "delete"
|
||||
o_pos = event.position
|
||||
fix = (cursor)->
|
||||
if cursor < o_pos
|
||||
cursor
|
||||
else
|
||||
cursor -= 1
|
||||
cursor
|
||||
r = createRange fix
|
||||
writeRange r
|
||||
|
||||
# consume all text-insert changes.
|
||||
textfield.onkeypress = (event)->
|
||||
if word.is_deleted
|
||||
# if word is deleted, do not do anything ever again
|
||||
textfield.onkeypress = null
|
||||
return true
|
||||
creator_token = true
|
||||
char = null
|
||||
if event.key?
|
||||
if event.charCode is 32
|
||||
char = " "
|
||||
else if event.keyCode is 13
|
||||
char = '\n'
|
||||
else
|
||||
char = event.key
|
||||
else
|
||||
char = window.String.fromCharCode event.keyCode
|
||||
if char.length > 1
|
||||
return true
|
||||
else if char.length > 0
|
||||
r = createRange()
|
||||
pos = Math.min r.left, r.right
|
||||
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()
|
||||
creator_token = false
|
||||
false
|
||||
|
||||
textfield.onpaste = (event)->
|
||||
if word.is_deleted
|
||||
# if word is deleted, do not do anything ever again
|
||||
textfield.onpaste = null
|
||||
return true
|
||||
event.preventDefault()
|
||||
textfield.oncut = (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 = (event)->
|
||||
creator_token = true
|
||||
if word.is_deleted
|
||||
# if word is deleted, do not do anything ever again
|
||||
textfield.onkeydown = null
|
||||
return true
|
||||
r = createRange()
|
||||
pos = Math.min(r.left, r.right, word.val().length)
|
||||
diff = Math.abs(r.left - r.right)
|
||||
if event.keyCode? and event.keyCode is 8 # Backspace
|
||||
if diff > 0
|
||||
word.delete pos, diff
|
||||
r.left = pos
|
||||
r.right = pos
|
||||
writeRange r
|
||||
else
|
||||
if event.ctrlKey? and event.ctrlKey
|
||||
val = word.val()
|
||||
new_pos = pos
|
||||
del_length = 0
|
||||
if pos > 0
|
||||
new_pos--
|
||||
del_length++
|
||||
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
|
||||
new_pos--
|
||||
del_length++
|
||||
word.delete new_pos, (pos-new_pos)
|
||||
r.left = new_pos
|
||||
r.right = new_pos
|
||||
writeRange r
|
||||
else
|
||||
if pos > 0
|
||||
word.delete (pos-1), 1
|
||||
r.left = pos-1
|
||||
r.right = pos-1
|
||||
writeRange r
|
||||
event.preventDefault()
|
||||
creator_token = false
|
||||
return false
|
||||
else if event.keyCode? and event.keyCode is 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()
|
||||
creator_token = false
|
||||
return false
|
||||
else
|
||||
creator_token = false
|
||||
true
|
||||
|
||||
#
|
||||
# @private
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
}
|
||||
json
|
||||
|
||||
types.String.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
} = json
|
||||
new this(uid)
|
||||
|
||||
types.String.create = (content, mutable)->
|
||||
if (mutable is "mutable")
|
||||
word = new types.String().execute()
|
||||
word.insert 0, content
|
||||
word
|
||||
else if (not mutable?) or (mutable is "immutable")
|
||||
content
|
||||
else
|
||||
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
|
||||
|
||||
|
||||
structured_types
|
||||
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
###
|
||||
json_types_uninitialized = require "./JsonTypes"
|
||||
|
||||
# some dom implementations may call another dom.method that simulates the behavior of another.
|
||||
# For example xml.insertChild(dom) , wich inserts an element at the end, and xml.insertAfter(dom,null) wich does the same
|
||||
# But Y's proxy may be called only once!
|
||||
proxy_token = false
|
||||
dont_proxy = (f)->
|
||||
proxy_token = true
|
||||
try
|
||||
f()
|
||||
catch e
|
||||
proxy_token = false
|
||||
throw new Error e
|
||||
proxy_token = false
|
||||
|
||||
_proxy = (f_name, f)->
|
||||
old_f = @[f_name]
|
||||
if old_f?
|
||||
@[f_name] = ()->
|
||||
if not proxy_token and not @_y?.isDeleted()
|
||||
that = this
|
||||
args = arguments
|
||||
dont_proxy ()->
|
||||
f.apply that, args
|
||||
old_f.apply that, args
|
||||
else
|
||||
old_f.apply this, arguments
|
||||
#else
|
||||
# @[f_name] = f
|
||||
Element?.prototype._proxy = _proxy
|
||||
|
||||
|
||||
module.exports = (HB)->
|
||||
json_types = json_types_uninitialized HB
|
||||
types = json_types.types
|
||||
parser = json_types.parser
|
||||
|
||||
#
|
||||
# Manages XML types
|
||||
# Not supported:
|
||||
# * Attribute nodes
|
||||
# * Real replace of child elements (to much overhead). Currently, the new element is inserted after the 'replaced' element, and then it is deleted.
|
||||
# * Namespaces (*NS)
|
||||
# * Browser specific methods (webkit-* operations)
|
||||
class XmlType extends types.Insert
|
||||
|
||||
constructor: (uid, @tagname, attributes, elements, @xml)->
|
||||
### In case you make this instanceof Insert again
|
||||
if prev? and (not next?) and prev.type?
|
||||
# adjust what you actually mean. you want to insert after prev, then
|
||||
# next is not defined. but we only insert after non-deleted elements.
|
||||
# This is also handled in TextInsert.
|
||||
while prev.isDeleted()
|
||||
prev = prev.prev_cl
|
||||
next = prev.next_cl
|
||||
###
|
||||
|
||||
super(uid)
|
||||
|
||||
|
||||
if @xml?._y?
|
||||
d = new types.Delete undefined, @xml._y
|
||||
HB.addOperation(d).execute()
|
||||
@xml._y = null
|
||||
|
||||
if attributes? and elements?
|
||||
@saveOperation 'attributes', attributes
|
||||
@saveOperation 'elements', elements
|
||||
else if (not attributes?) and (not elements?)
|
||||
@attributes = new types.JsonType()
|
||||
@attributes.setMutableDefault 'immutable'
|
||||
HB.addOperation(@attributes).execute()
|
||||
@elements = new types.WordType()
|
||||
@elements.parent = @
|
||||
HB.addOperation(@elements).execute()
|
||||
else
|
||||
throw new Error "Either define attribute and elements both, or none of them"
|
||||
|
||||
if @xml?
|
||||
@tagname = @xml.tagName
|
||||
for i in [0...@xml.attributes.length]
|
||||
attr = xml.attributes[i]
|
||||
@attributes.val(attr.name, attr.value)
|
||||
for n in @xml.childNodes
|
||||
if n.nodeType is n.TEXT_NODE
|
||||
word = new TextNodeType(undefined, n)
|
||||
HB.addOperation(word).execute()
|
||||
@elements.push word
|
||||
else if n.nodeType is n.ELEMENT_NODE
|
||||
element = new XmlType undefined, undefined, undefined, undefined, n
|
||||
HB.addOperation(element).execute()
|
||||
@elements.push element
|
||||
else
|
||||
throw new Error "I don't know Node-type #{n.nodeType}!!"
|
||||
@setXmlProxy()
|
||||
undefined
|
||||
|
||||
#
|
||||
# Identifies this class.
|
||||
# Use it in order to check whether this is an xml-type or something else.
|
||||
#
|
||||
type: "XmlType"
|
||||
|
||||
applyDelete: (op)->
|
||||
if @insert_parent? and not @insert_parent.isDeleted()
|
||||
@insert_parent.applyDelete op
|
||||
else
|
||||
@attributes.applyDelete()
|
||||
@elements.applyDelete()
|
||||
super
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
setXmlProxy: ()->
|
||||
@xml._y = @
|
||||
that = @
|
||||
|
||||
@elements.on 'insert', (event, op)->
|
||||
if op.creator isnt HB.getUserId() and this is that.elements
|
||||
newNode = op.content.val()
|
||||
right = op.next_cl
|
||||
while right? and right.isDeleted()
|
||||
right = right.next_cl
|
||||
rightNode = null
|
||||
if right.type isnt 'Delimiter'
|
||||
rightNode = right.val().val()
|
||||
dont_proxy ()->
|
||||
that.xml.insertBefore newNode, rightNode
|
||||
@elements.on 'delete', (event, op)->
|
||||
del_op = op.deleted_by[0]
|
||||
if del_op? and del_op.creator isnt HB.getUserId() and this is that.elements
|
||||
deleted = op.content.val()
|
||||
dont_proxy ()->
|
||||
that.xml.removeChild deleted
|
||||
|
||||
@attributes.on ['add', 'update'], (event, property_name, op)->
|
||||
if op.creator isnt HB.getUserId() and this is that.attributes
|
||||
dont_proxy ()->
|
||||
newval = op.val().val()
|
||||
if newval?
|
||||
that.xml.setAttribute(property_name, op.val().val())
|
||||
else
|
||||
that.xml.removeAttribute(property_name)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Here are all methods that proxy the behavior of the xml
|
||||
|
||||
# you want to find a specific child element. Since they are carried by an Insert-Type, you want to find that Insert-Operation.
|
||||
# @param child {DomElement} Dom element.
|
||||
# @return {InsertType} This carries the XmlType that represents the DomElement (child). false if i couldn't find it.
|
||||
#
|
||||
findNode = (child)->
|
||||
if not child?
|
||||
throw new Error "you must specify a parameter!"
|
||||
child = child._y
|
||||
elem = that.elements.beginning.next_cl
|
||||
while elem.type isnt 'Delimiter' and elem.content isnt child
|
||||
elem = elem.next_cl
|
||||
if elem.type is 'Delimiter'
|
||||
false
|
||||
else
|
||||
elem
|
||||
|
||||
insertBefore = (insertedNode_s, adjacentNode)->
|
||||
next = null
|
||||
if adjacentNode?
|
||||
next = findNode adjacentNode
|
||||
prev = null
|
||||
if next
|
||||
prev = next.prev_cl
|
||||
else
|
||||
prev = @_y.elements.end.prev_cl
|
||||
while prev.isDeleted()
|
||||
prev = prev.prev_cl
|
||||
inserted_nodes = null
|
||||
if insertedNode_s.nodeType is insertedNode_s.DOCUMENT_FRAGMENT_NODE
|
||||
child = insertedNode_s.lastChild
|
||||
while child?
|
||||
element = new XmlType undefined, undefined, undefined, undefined, child
|
||||
HB.addOperation(element).execute()
|
||||
that.elements.insertAfter prev, element
|
||||
child = child.previousSibling
|
||||
else
|
||||
element = new XmlType undefined, undefined, undefined, undefined, insertedNode_s
|
||||
HB.addOperation(element).execute()
|
||||
that.elements.insertAfter prev, element
|
||||
|
||||
@xml._proxy 'insertBefore', insertBefore
|
||||
@xml._proxy 'appendChild', insertBefore
|
||||
@xml._proxy 'removeAttribute', (name)->
|
||||
that.attributes.val(name, undefined)
|
||||
@xml._proxy 'setAttribute', (name, value)->
|
||||
that.attributes.val name, value
|
||||
|
||||
renewClassList = (newclass)->
|
||||
dont_do_it = false
|
||||
if newclass?
|
||||
for elem in this
|
||||
if newclass is elem
|
||||
dont_do_it = true
|
||||
value = Array.prototype.join.call this, " "
|
||||
if newclass? and not dont_do_it
|
||||
value += " "+newclass
|
||||
that.attributes.val('class', value )
|
||||
_proxy.call @xml.classList, 'add', renewClassList
|
||||
_proxy.call @xml.classList, 'remove', renewClassList
|
||||
@xml.__defineSetter__ 'className', (val)->
|
||||
@setAttribute('class', val)
|
||||
@xml.__defineGetter__ 'className', ()->
|
||||
that.attributes.val('class')
|
||||
@xml.__defineSetter__ 'textContent', (val)->
|
||||
# remove all nodes
|
||||
elem = that.xml.firstChild
|
||||
while elem?
|
||||
remove = elem
|
||||
elem = elem.nextSibling
|
||||
that.xml.removeChild remove
|
||||
|
||||
# insert word content
|
||||
if val isnt ""
|
||||
text_node = document.createTextNode val
|
||||
that.xml.appendChild text_node
|
||||
|
||||
removeChild = (node)->
|
||||
elem = findNode node
|
||||
if not elem
|
||||
throw new Error "You are only allowed to delete existing (direct) child elements!"
|
||||
d = new types.Delete undefined, elem
|
||||
HB.addOperation(d).execute()
|
||||
node._y = null
|
||||
@xml._proxy 'removeChild', removeChild
|
||||
@xml._proxy 'replaceChild', (insertedNode, replacedNode)->
|
||||
insertBefore.call this, insertedNode, replacedNode
|
||||
removeChild.call this, replacedNode
|
||||
|
||||
|
||||
|
||||
val: (enforce = false)->
|
||||
if document?
|
||||
if (not @xml?) or enforce
|
||||
@xml = document.createElement @tagname
|
||||
|
||||
attr = @attributes.val()
|
||||
for attr_name, value of attr
|
||||
if value?
|
||||
a = document.createAttribute attr_name
|
||||
a.value = value
|
||||
@xml.setAttributeNode a
|
||||
|
||||
e = @elements.beginning.next_cl
|
||||
while e.type isnt "Delimiter"
|
||||
n = e.content
|
||||
if not e.isDeleted() and e.content? # TODO: how can this happen? Probably because listeners
|
||||
if n.type is "XmlType"
|
||||
@xml.appendChild n.val(enforce)
|
||||
else if n.type is "TextNodeType"
|
||||
text_node = n.val()
|
||||
@xml.appendChild text_node
|
||||
else
|
||||
throw new Error "Internal structure cannot be transformed to dom"
|
||||
e = e.next_cl
|
||||
@setXmlProxy()
|
||||
@xml
|
||||
|
||||
|
||||
execute: ()->
|
||||
super()
|
||||
###
|
||||
if not @validateSavedOperations()
|
||||
return false
|
||||
else
|
||||
|
||||
return true
|
||||
###
|
||||
|
||||
#
|
||||
# Get the parent of this JsonType.
|
||||
# @return {XmlType}
|
||||
#
|
||||
getParent: ()->
|
||||
@parent
|
||||
|
||||
#
|
||||
# @private
|
||||
#
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be send to other clients.
|
||||
#
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type' : @type
|
||||
'attributes' : @attributes.getUid()
|
||||
'elements' : @elements.getUid()
|
||||
'tagname' : @tagname
|
||||
'uid' : @getUid()
|
||||
}
|
||||
json
|
||||
|
||||
parser['XmlType'] = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'attributes' : attributes
|
||||
'elements' : elements
|
||||
'tagname' : tagname
|
||||
} = json
|
||||
|
||||
new XmlType uid, tagname, attributes, elements, undefined
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
|
||||
#
|
||||
class TextNodeType extends types.ImmutableObject
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Object} content
|
||||
#
|
||||
constructor: (uid, content)->
|
||||
if content._y?
|
||||
d = new types.Delete undefined, content._y
|
||||
HB.addOperation(d).execute()
|
||||
content._y = null
|
||||
content._y = @
|
||||
super uid, content
|
||||
|
||||
applyDelete: (op)->
|
||||
if @insert_parent? and not @insert_parent.isDeleted()
|
||||
@insert_parent.applyDelete op
|
||||
else
|
||||
super
|
||||
|
||||
|
||||
type: "TextNodeType"
|
||||
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'content' : @content.textContent
|
||||
}
|
||||
json
|
||||
|
||||
parser['TextNodeType'] = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'content' : content
|
||||
} = json
|
||||
textnode = document.createTextNode content
|
||||
new TextNodeType uid, textnode
|
||||
|
||||
types['XmlType'] = XmlType
|
||||
|
||||
json_types
|
||||
###
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
Y = require './y'
|
||||
|
||||
bindToChildren = (that)->
|
||||
for i in [0...that.children.length]
|
||||
attr = that.children.item(i)
|
||||
if attr.name?
|
||||
attr.val = that.val.val(attr.name)
|
||||
that.val.observe (events)->
|
||||
for event in events
|
||||
if event.name?
|
||||
for i in [0...that.children.length]
|
||||
attr = that.children.item(i)
|
||||
if attr.name? and attr.name is event.name
|
||||
newVal = that.val.val(attr.name)
|
||||
if attr.val isnt newVal
|
||||
attr.val = newVal
|
||||
|
||||
Polymer "y-object",
|
||||
ready: ()->
|
||||
if @connector?
|
||||
@val = new Y @connector
|
||||
bindToChildren @
|
||||
else if @val?
|
||||
bindToChildren @
|
||||
|
||||
valChanged: ()->
|
||||
if @val? and @val.type is "Object"
|
||||
bindToChildren @
|
||||
|
||||
connectorChanged: ()->
|
||||
if (not @val?)
|
||||
@val = new Y @connector
|
||||
bindToChildren @
|
||||
|
||||
Polymer "y-property",
|
||||
ready: ()->
|
||||
if @val? and @name?
|
||||
if @val.constructor is Object
|
||||
@val = @parentElement.val(@name,@val).val(@name)
|
||||
# TODO: please use instanceof instead of .type,
|
||||
# since it is more safe (consider someone putting a custom Object type here)
|
||||
else if typeof @val is "string"
|
||||
@parentElement.val(@name,@val)
|
||||
if @val.type is "Object"
|
||||
bindToChildren @
|
||||
|
||||
valChanged: ()->
|
||||
if @val? and @name?
|
||||
if @val.constructor is Object
|
||||
@val = @parentElement.val.val(@name,@val).val(@name)
|
||||
# TODO: please use instanceof instead of .type,
|
||||
# since it is more safe (consider someone putting a custom Object type here)
|
||||
else if @val.type is "Object"
|
||||
bindToChildren @
|
||||
else if @parentElement.val?.val? and @val isnt @parentElement.val.val(@name)
|
||||
@parentElement.val.val @name, @val
|
||||
|
||||
|
||||
48
lib/y.coffee
48
lib/y.coffee
@@ -1,48 +0,0 @@
|
||||
|
||||
json_types_uninitialized = require "./Types/JsonTypes"
|
||||
HistoryBuffer = require "./HistoryBuffer"
|
||||
Engine = require "./Engine"
|
||||
adaptConnector = require "./ConnectorAdapter"
|
||||
|
||||
createY = (connector)->
|
||||
user_id = null
|
||||
if connector.id?
|
||||
user_id = connector.id # TODO: change to getUniqueId()
|
||||
else
|
||||
user_id = "_temp"
|
||||
connector.whenUserIdSet (id)->
|
||||
user_id = id
|
||||
HB.resetUserId id
|
||||
HB = new HistoryBuffer user_id
|
||||
type_manager = json_types_uninitialized HB
|
||||
types = type_manager.types
|
||||
|
||||
#
|
||||
# Framework for Json data-structures.
|
||||
# Known values that are supported:
|
||||
# * String
|
||||
# * Integer
|
||||
# * Array
|
||||
#
|
||||
class Y extends types.Object
|
||||
|
||||
#
|
||||
# @param {String} user_id Unique id of the peer.
|
||||
# @param {Connector} Connector the connector class.
|
||||
#
|
||||
constructor: ()->
|
||||
@connector = connector
|
||||
@HB = HB
|
||||
@types = types
|
||||
@engine = new Engine @HB, type_manager.types
|
||||
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
|
||||
super
|
||||
|
||||
getConnector: ()->
|
||||
@connector
|
||||
|
||||
return new Y(HB.getReservedUniqueIdentifier()).execute()
|
||||
|
||||
module.exports = createY
|
||||
if window? and not window.Y?
|
||||
window.Y = createY
|
||||
107
package.json
107
package.json
@@ -1,66 +1,79 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "0.3.0",
|
||||
"description": "A Framework that enables Real-Time Collaboration on arbitrary data structures.",
|
||||
"main": "./build/node/y.js",
|
||||
"version": "11.2.6",
|
||||
"description": "A framework for real-time p2p shared editing on arbitrary complex data types",
|
||||
"main": "./src/y.js",
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/gulp test"
|
||||
"test": "node --harmony ./node_modules/.bin/gulp test",
|
||||
"lint": "./node_modules/.bin/standard"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint",
|
||||
"test"
|
||||
],
|
||||
"standard": {
|
||||
"parser": "babel-eslint",
|
||||
"ignore": [
|
||||
"build/**",
|
||||
"dist/**",
|
||||
"declarations/**",
|
||||
"./y.js",
|
||||
"./y.js.map"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rwth-acis/yjs"
|
||||
"url": "https://github.com/y-js/yjs.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Yjs",
|
||||
"OT",
|
||||
"collaboration",
|
||||
"Yata",
|
||||
"synchronization",
|
||||
"Collaboration",
|
||||
"Synchronization",
|
||||
"ShareJS",
|
||||
"Coweb",
|
||||
"concurrency"
|
||||
"Concurrency"
|
||||
],
|
||||
"author": "Kevin Jahns",
|
||||
"email": "kevin.jahns@rwth-aachen.de",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/rwth-acis/yjs/issues"
|
||||
},
|
||||
"homepage": "https://dadamonad.github.io/yjs/",
|
||||
"dependencies": {
|
||||
"url": "https://github.com/y-js/yjs/issues"
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"codo": "^2.0.9",
|
||||
"underscore": "^1.6.0",
|
||||
"chai": "^1.9.1",
|
||||
"codo": "^2.0.9",
|
||||
"coffee-errors": "~0.8.6",
|
||||
"coffee-script": "^1.7.1",
|
||||
"coffeeify": "^0.6.0",
|
||||
"gulp": "^3.8.7",
|
||||
"gulp-browserify": "^0.5.0",
|
||||
"gulp-cached": "^1.0.1",
|
||||
"gulp-coffee": "^2.1.1",
|
||||
"gulp-coffeeify": "^0.1.2",
|
||||
"gulp-coffeelint": "^0.3.3",
|
||||
"gulp-concat": "^2.3.4",
|
||||
"gulp-copy": "0.0.2",
|
||||
"gulp-debug": "^1.0.0",
|
||||
"gulp-git": "^0.5.0",
|
||||
"gulp-if": "^1.2.4",
|
||||
"gulp-ignore": "^1.2.0",
|
||||
"gulp-ljs": "^0.1.1",
|
||||
"gulp-mocha": "^0.5.2",
|
||||
"gulp-mocha-phantomjs": "^0.5.0",
|
||||
"gulp-plumber": "^0.6.6",
|
||||
"gulp-rename": "^1.2.0",
|
||||
"gulp-rimraf": "^0.1.0",
|
||||
"gulp-run": "^1.6.3",
|
||||
"gulp-sourcemaps": "^1.1.1",
|
||||
"gulp-uglify": "^0.3.1",
|
||||
"gulp-watch": "^3.0.0",
|
||||
"jquery": "^2.1.1",
|
||||
"mocha": "^1.21.4",
|
||||
"sinon": "^1.10.2",
|
||||
"sinon-chai": "^2.5.0"
|
||||
}
|
||||
"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-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.*",
|
||||
"gulp-util": "^3.0.6",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"minimist": "^1.2.0",
|
||||
"pre-commit": "^1.1.1",
|
||||
"regenerator": "^0.8.42",
|
||||
"run-sequence": "^1.1.4",
|
||||
"seedrandom": "^2.4.2",
|
||||
"standard": "^5.2.2",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
424
src/Connector.js
Normal file
424
src/Connector.js
Normal file
@@ -0,0 +1,424 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
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 (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.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
|
||||
}
|
||||
reconnect () {
|
||||
}
|
||||
disconnect () {
|
||||
this.connections = {}
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.broadcastedHB = false
|
||||
this.syncingClients = []
|
||||
this.whenSyncedListeners = []
|
||||
return this.y.db.stopGarbageCollector()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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
|
||||
})
|
||||
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._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) {
|
||||
if (!broadcastHB) { // TODO: consider to broadcast here..
|
||||
conn.send(sender, {
|
||||
type: 'update',
|
||||
ops: ops
|
||||
})
|
||||
} else {
|
||||
// broadcast only once!
|
||||
conn.broadcastOps(ops)
|
||||
}
|
||||
}
|
||||
defer.resolve()
|
||||
})
|
||||
})
|
||||
} else if (message.type === 'sync done') {
|
||||
var self = this
|
||||
this.syncStep2.then(function () {
|
||||
self._setSyncedWith(sender)
|
||||
})
|
||||
} else if (message.type === 'update') {
|
||||
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)
|
||||
}
|
||||
}
|
||||
_setSyncedWith (user) {
|
||||
var conn = this.connections[user]
|
||||
if (conn != null) {
|
||||
conn.isSynced = true
|
||||
}
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
}
|
||||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
165
src/Connectors/Test.js
Normal file
165
src/Connectors/Test.js
Normal file
@@ -0,0 +1,165 @@
|
||||
/* global getRandom, async */
|
||||
'use strict'
|
||||
|
||||
module.exports = function (Y) {
|
||||
var globalRoom = {
|
||||
users: {},
|
||||
buffers: {},
|
||||
removeUser: function (user) {
|
||||
for (var i in this.users) {
|
||||
this.users[i].userLeft(user)
|
||||
}
|
||||
delete this.users[user]
|
||||
delete this.buffers[user]
|
||||
},
|
||||
addUser: function (connector) {
|
||||
this.users[connector.userId] = connector
|
||||
this.buffers[connector.userId] = {}
|
||||
for (var uname in this.users) {
|
||||
if (uname !== connector.userId) {
|
||||
var u = this.users[uname]
|
||||
u.userJoined(connector.userId, 'master')
|
||||
connector.userJoined(u.userId, 'master')
|
||||
}
|
||||
}
|
||||
},
|
||||
whenTransactionsFinished: function () {
|
||||
var 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Y.utils.globalRoom = globalRoom
|
||||
|
||||
var userIdCounter = 0
|
||||
|
||||
class Test extends Y.AbstractConnector {
|
||||
constructor (y, options) {
|
||||
if (options === undefined) {
|
||||
throw new Error('Options must not be undefined!')
|
||||
}
|
||||
options.role = 'master'
|
||||
options.forwardToSyncingClients = false
|
||||
super(y, options)
|
||||
this.setUserId((userIdCounter++) + '').then(() => {
|
||||
globalRoom.addUser(this)
|
||||
})
|
||||
this.globalRoom = globalRoom
|
||||
this.syncingClientDuration = 0
|
||||
}
|
||||
receiveMessage (sender, m) {
|
||||
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
|
||||
}
|
||||
548
src/Database.js
Normal file
548
src/Database.js
Normal file
@@ -0,0 +1,548 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
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 {
|
||||
/* ::
|
||||
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;
|
||||
*/
|
||||
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!')
|
||||
}
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
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()
|
||||
})
|
||||
})
|
||||
}
|
||||
/*
|
||||
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
|
||||
}
|
||||
if (gc) {
|
||||
op.gc = true
|
||||
yield* this.setOperation(op)
|
||||
this.store.queueGarbageCollector(op.id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
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
|
||||
this.stopRepairCheck()
|
||||
for (var key in this.initializedTypes) {
|
||||
var type = this.initializedTypes[key]
|
||||
if (type._destroy != null) {
|
||||
type._destroy()
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.transactionIsFlushed = false
|
||||
return this.waitingTransactions.shift()
|
||||
}
|
||||
}
|
||||
requestTransaction (makeGen/* :any */, callImmediately) {
|
||||
this.waitingTransactions.push(makeGen)
|
||||
if (!this.transactionInProgress) {
|
||||
this.transactionInProgress = true
|
||||
setTimeout(() => {
|
||||
this.transact(this.getNextRequest())
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
Y.AbstractDatabase = AbstractDatabase
|
||||
}
|
||||
354
src/Database.spec.js
Normal file
354
src/Database.spec.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/* 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 () {
|
||||
var store
|
||||
describe('DeleteStore', function () {
|
||||
describe('Basic', function () {
|
||||
beforeEach(function () {
|
||||
store = new Y[database](null, {
|
||||
gcTimeout: -1,
|
||||
namespace: 'testing'
|
||||
})
|
||||
})
|
||||
afterEach(function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.store.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('Deleted operation is deleted', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.markDeleted(['u1', 10], 1)
|
||||
expect(yield* this.isDeleted(['u1', 10])).toBeTruthy()
|
||||
expect(yield* this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
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]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
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], 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], 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], 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], 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()
|
||||
})
|
||||
}))
|
||||
it('Debug #5', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
||||
yield* this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #6', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.applyDeleteSet({'40': [[0, 3, false]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
|
||||
yield* this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||
expect(yield* this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
||||
done()
|
||||
})
|
||||
}))
|
||||
it('Debug #7', async(function * (done) {
|
||||
store.requestTransaction(function * () {
|
||||
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()
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
describe('OperationStore', function () {
|
||||
describe('Basic Tests', function () {
|
||||
beforeEach(function () {
|
||||
store = new Y[database](null, {
|
||||
gcTimeout: -1,
|
||||
namespace: 'testing'
|
||||
})
|
||||
})
|
||||
afterEach(function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.store.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('debug #1', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.put({id: [2]})
|
||||
yield* this.os.put({id: [0]})
|
||||
yield* this.os.delete([2])
|
||||
yield* this.os.put({id: [1]})
|
||||
expect(yield* this.os.find([0])).toBeTruthy()
|
||||
expect(yield* this.os.find([1])).toBeTruthy()
|
||||
expect(yield* this.os.find([2])).toBeFalsy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('can add&retrieve 5 elements', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.put({val: 'four', id: [4]})
|
||||
yield* this.os.put({val: 'one', id: [1]})
|
||||
yield* this.os.put({val: 'three', id: [3]})
|
||||
yield* this.os.put({val: 'two', id: [2]})
|
||||
yield* this.os.put({val: 'five', id: [5]})
|
||||
expect((yield* this.os.find([1])).val).toEqual('one')
|
||||
expect((yield* this.os.find([2])).val).toEqual('two')
|
||||
expect((yield* this.os.find([3])).val).toEqual('three')
|
||||
expect((yield* this.os.find([4])).val).toEqual('four')
|
||||
expect((yield* this.os.find([5])).val).toEqual('five')
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('5 elements do not exist anymore after deleting them', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.put({val: 'four', id: [4]})
|
||||
yield* this.os.put({val: 'one', id: [1]})
|
||||
yield* this.os.put({val: 'three', id: [3]})
|
||||
yield* this.os.put({val: 'two', id: [2]})
|
||||
yield* this.os.put({val: 'five', id: [5]})
|
||||
yield* this.os.delete([4])
|
||||
expect(yield* this.os.find([4])).not.toBeTruthy()
|
||||
yield* this.os.delete([3])
|
||||
expect(yield* this.os.find([3])).not.toBeTruthy()
|
||||
yield* this.os.delete([2])
|
||||
expect(yield* this.os.find([2])).not.toBeTruthy()
|
||||
yield* this.os.delete([1])
|
||||
expect(yield* this.os.find([1])).not.toBeTruthy()
|
||||
yield* this.os.delete([5])
|
||||
expect(yield* this.os.find([5])).not.toBeTruthy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
var numberOfOSTests = 1000
|
||||
describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () {
|
||||
var elements = []
|
||||
beforeAll(function (done) {
|
||||
store = new Y[database](null, {
|
||||
gcTimeout: -1,
|
||||
namespace: 'testing'
|
||||
})
|
||||
store.requestTransaction(function * () {
|
||||
for (var i = 0; i < numberOfOSTests; i++) {
|
||||
var r = Math.random()
|
||||
if (r < 0.8) {
|
||||
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
|
||||
if (!(yield* this.os.find(obj))) {
|
||||
elements.push(obj)
|
||||
yield* this.os.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* this.os.delete(elem)
|
||||
}
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
afterAll(function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.store.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('can find every object', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
for (var id of elements) {
|
||||
expect((yield* this.os.find(id)).id).toEqual(id)
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('can find every object with lower bound search', function (done) {
|
||||
store.requestTransaction(function * () {
|
||||
for (var id of elements) {
|
||||
var e = yield* this.os.findWithLowerBound(id)
|
||||
expect(e.id).toEqual(id)
|
||||
}
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
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
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.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
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.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
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.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
|
||||
store.requestTransaction(function * () {
|
||||
yield* this.os.iterate(this, lowerBound, upperBound, function * (val) {
|
||||
expect(val).toBeDefined()
|
||||
actualResults++
|
||||
})
|
||||
expect(expectedResults).toEqual(actualResults)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
12
src/Notes.md
Normal file
12
src/Notes.md
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
# Notes
|
||||
|
||||
### Terminology
|
||||
|
||||
* DB: DataBase that holds all the information of the shared object. It is devided into the OS, DS, and SS. This can be a persistent database or an in-memory database. Depending on the type of database, it could make sense to store OS, DS, and SS in different tables, or maybe different databases.
|
||||
* OS: OperationStore holds all the operations. An operation is a js object with a fixed number of name fields.
|
||||
* DS: DeleteStore holds the information about which operations are deleted and which operations were garbage collected (no longer available in the OS).
|
||||
* SS: StateSet holds the current state of the OS. SS.getState(username) refers to the amount of operations that were received by that respective user.
|
||||
* Op: Operation defines an action on a shared type. But it is also the format in which we store the model of a type. This is why it is also called a Struct/Structure.
|
||||
* Type and Structure: We crearly distinguish between type and structure. Short explanation: A type (e.g. Strings, Numbers) have a number of functions that you can apply on them. (+) is well defined on both of them. They are *modeled* by a structure - the functions really change the structure of a type. Types can be implemented differently but still provide the same functionality. In Yjs, almost all types are realized as a doubly linked list (on which Yjs can provide eventual convergence)
|
||||
*
|
||||
385
src/SpecHelper.js
Normal file
385
src/SpecHelper.js
Normal file
@@ -0,0 +1,385 @@
|
||||
/* eslint-env browser, jasmine */
|
||||
|
||||
/*
|
||||
This is just a compilation of functions that help to test this library!
|
||||
*/
|
||||
|
||||
// When testing, you store everything on the global object. We call it g
|
||||
|
||||
var Y = require('./y.js')
|
||||
require('../../y-memory/src/Memory.js')(Y)
|
||||
require('../../y-array/src/Array.js')(Y)
|
||||
require('../../y-map/src/Map.js')(Y)
|
||||
require('../../y-indexeddb/src/IndexedDB.js')(Y)
|
||||
if (typeof window === 'undefined') {
|
||||
require('../../y-leveldb/src/LevelDB.js')(Y)
|
||||
}
|
||||
module.exports = Y
|
||||
|
||||
var g
|
||||
if (typeof global !== 'undefined') {
|
||||
g = global
|
||||
} else if (typeof window !== 'undefined') {
|
||||
g = window
|
||||
} else {
|
||||
throw new Error('No global object?')
|
||||
}
|
||||
g.g = g
|
||||
|
||||
// Helper methods for the random number generator
|
||||
Math.seedrandom = require('seedrandom')
|
||||
|
||||
g.generateRandomSeed = function generateRandomSeed () {
|
||||
var seed
|
||||
if (typeof window !== 'undefined' && window.location.hash.length > 1) {
|
||||
seed = window.location.hash.slice(1) // first character is the hash!
|
||||
console.warn('Using random seed that was specified in the url!')
|
||||
} else {
|
||||
seed = JSON.stringify(Math.random())
|
||||
}
|
||||
console.info('Using random seed: ' + seed)
|
||||
g.setRandomSeed(seed)
|
||||
}
|
||||
|
||||
g.setRandomSeed = function setRandomSeed (seed) {
|
||||
Math.seedrandom.currentSeed = seed
|
||||
Math.seedrandom(Math.seedrandom.currentSeed, { global: true })
|
||||
}
|
||||
|
||||
g.generateRandomSeed()
|
||||
|
||||
g.YConcurrency_TestingMode = true
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
|
||||
|
||||
g.describeManyTimes = function describeManyTimes (times, name, f) {
|
||||
for (var i = 0; i < times; i++) {
|
||||
describe(name, f)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Wait for a specified amount of time (in ms). defaults to 5ms
|
||||
*/
|
||||
function wait (t) {
|
||||
if (t == null) {
|
||||
t = 0
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(function () {
|
||||
resolve()
|
||||
}, t)
|
||||
})
|
||||
}
|
||||
g.wait = wait
|
||||
|
||||
g.databases = ['memory']
|
||||
if (typeof window !== 'undefined') {
|
||||
g.databases.push('indexeddb')
|
||||
} else {
|
||||
g.databases.push('leveldb')
|
||||
}
|
||||
/*
|
||||
returns a random element of o.
|
||||
works on Object, and Array
|
||||
*/
|
||||
function getRandom (o) {
|
||||
if (o instanceof Array) {
|
||||
return o[Math.floor(Math.random() * o.length)]
|
||||
} else if (o.constructor === Object) {
|
||||
return o[getRandom(Object.keys(o))]
|
||||
}
|
||||
}
|
||||
g.getRandom = getRandom
|
||||
|
||||
function getRandomNumber (n) {
|
||||
if (n == null) {
|
||||
n = 9999
|
||||
}
|
||||
return Math.floor(Math.random() * n)
|
||||
}
|
||||
g.getRandomNumber = getRandomNumber
|
||||
|
||||
function getRandomString () {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyzäüöABCDEFGHIJKLMNOPQRSTUVWXYZÄÜÖ'
|
||||
var char = chars[getRandomNumber(chars.length)] // ü\n\n\n\n\n\n\n'
|
||||
var length = getRandomNumber(7)
|
||||
var string = ''
|
||||
for (var i = 0; i < length; i++) {
|
||||
string += char
|
||||
}
|
||||
return string
|
||||
}
|
||||
g.getRandomString = getRandomString
|
||||
|
||||
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) {
|
||||
g.generateRandomSeed() // create a new seed, so we can re-create the behavior
|
||||
for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
|
||||
var r = Math.random()
|
||||
if (r > 0.95) {
|
||||
// 10% chance of toggling concurrent user interactions.
|
||||
// There will be an artificial delay until ops can be executed by the type,
|
||||
// therefore, operations of the database will be (pre)transformed until user operations arrive
|
||||
yield (function simulateConcurrentUserInteractions (type) {
|
||||
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
|
||||
type.eventHandler.awaiting = 1
|
||||
type.eventHandler._debuggingAwaiting = true
|
||||
} else {
|
||||
// fixAwaitingInType will handle _debuggingAwaiting
|
||||
return fixAwaitingInType(type)
|
||||
}
|
||||
})(getRandom(objects))
|
||||
} else if (r >= 0.5) {
|
||||
// 40% chance to flush
|
||||
yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0)
|
||||
} else if (noReconnect || r >= 0.05) {
|
||||
// 45% chance to create operation
|
||||
var done = getRandom(transactions)(getRandom(objects))
|
||||
if (done != null) {
|
||||
yield done
|
||||
} else {
|
||||
yield wait()
|
||||
}
|
||||
yield Y.utils.globalRoom.whenTransactionsFinished()
|
||||
} else {
|
||||
// 5% chance to disconnect/reconnect
|
||||
var u = getRandom(users)
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
if (u.connector.isDisconnected()) {
|
||||
yield u.reconnect()
|
||||
} else {
|
||||
yield u.disconnect()
|
||||
}
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fixAwaitingInType (type) {
|
||||
return new Promise(function (resolve) {
|
||||
type.os.whenTransactionsFinished().then(function () {
|
||||
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
|
||||
type.os.requestTransaction(function * () {
|
||||
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
|
||||
type.eventHandler._debuggingAwaiting = false
|
||||
yield* type.eventHandler.awaitOps(this, function * () { /* mock function */ })
|
||||
}
|
||||
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
g.fixAwaitingInType = fixAwaitingInType
|
||||
|
||||
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
})
|
||||
|
||||
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||
yield* applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
for (var u in users) {
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield users[u].reconnect()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
}
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield g.garbageCollectAllUsers(users)
|
||||
})
|
||||
|
||||
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
||||
yield* applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
for (var u in users) {
|
||||
// TODO: here, we enforce that two users never sync at the same time with u[0]
|
||||
// enforce that in the connector itself!
|
||||
yield users[u].reconnect()
|
||||
}
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
yield Promise.all(objects.map(fixAwaitingInType))
|
||||
yield g.garbageCollectAllUsers(users)
|
||||
})
|
||||
|
||||
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
for (var i in users) {
|
||||
yield users[i].db.emptyGarbageCollector()
|
||||
}
|
||||
})
|
||||
|
||||
g.compareAllUsers = async(function * compareAllUsers (users) {
|
||||
var s1, s2 // state sets
|
||||
var ds1, ds2 // delete sets
|
||||
var allDels1, allDels2 // all deletions
|
||||
var db1 = [] // operation store of user1
|
||||
|
||||
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
||||
function * t1 () {
|
||||
s1 = yield* this.getStateSet()
|
||||
ds1 = yield* this.getDeleteSet()
|
||||
allDels1 = []
|
||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
||||
allDels1.push(d)
|
||||
})
|
||||
}
|
||||
function * t2 () {
|
||||
s2 = yield* this.getStateSet()
|
||||
ds2 = yield* this.getDeleteSet()
|
||||
allDels2 = []
|
||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
||||
allDels2.push(d)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
expect(yield* this.isDeleted(o.id)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
// compare deleteStore against deleted ops
|
||||
var ds = []
|
||||
yield* this.ds.iterate(this, null, null, function * (d) {
|
||||
ds.push(d)
|
||||
})
|
||||
for (var j in ds) {
|
||||
var d = ds[j]
|
||||
for (var i = 0; i < d.len; i++) {
|
||||
var o = yield* this.getInsertion([d.id[0], d.id[1] + i])
|
||||
// gc'd or deleted
|
||||
if (d.gc) {
|
||||
expect(o).toBeFalsy()
|
||||
} else {
|
||||
expect(o.deleted).toBeTruthy()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// compare allDels tree
|
||||
if (s1 == null) {
|
||||
u.db.requestTransaction(function * () {
|
||||
yield* t1.call(this)
|
||||
yield* this.os.iterate(this, null, null, function * (o) {
|
||||
o = Y.utils.copyObject(o)
|
||||
delete o.origin
|
||||
delete o.originOf
|
||||
db1.push(o)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
u.db.requestTransaction(function * () {
|
||||
yield* t2.call(this)
|
||||
var db2 = []
|
||||
yield* this.os.iterate(this, null, null, function * (o) {
|
||||
o = Y.utils.copyObject(o)
|
||||
delete o.origin
|
||||
delete o.originOf
|
||||
db2.push(o)
|
||||
})
|
||||
expect(s1).toEqual(s2)
|
||||
expect(allDels1).toEqual(allDels2) // inner structure
|
||||
expect(ds1).toEqual(ds2) // exported structure
|
||||
db2.forEach((o, i) => {
|
||||
expect(db1[i]).toEqual(o)
|
||||
})
|
||||
})
|
||||
}
|
||||
yield u.db.whenTransactionsFinished()
|
||||
}
|
||||
})
|
||||
|
||||
g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
|
||||
if (Y.utils.globalRoom.users[0] != null) {
|
||||
yield Y.utils.globalRoom.flushAll()
|
||||
}
|
||||
// destroy old users
|
||||
for (var u in Y.utils.globalRoom.users) {
|
||||
Y.utils.globalRoom.users[u].y.destroy()
|
||||
}
|
||||
self.users = null
|
||||
|
||||
var promises = []
|
||||
for (var i = 0; i < numberOfUsers; i++) {
|
||||
promises.push(Y({
|
||||
db: {
|
||||
name: database,
|
||||
namespace: 'User ' + i,
|
||||
cleanStart: true,
|
||||
gcTimeout: -1,
|
||||
repairCheckInterval: -1
|
||||
},
|
||||
connector: {
|
||||
name: 'Test',
|
||||
debug: false
|
||||
},
|
||||
share: {
|
||||
root: initType || 'Map'
|
||||
}
|
||||
}))
|
||||
}
|
||||
self.users = yield Promise.all(promises)
|
||||
self.types = self.users.map(function (u) { return u.share.root })
|
||||
return self.users
|
||||
})
|
||||
|
||||
/*
|
||||
Until async/await arrives in js, we use this function to wait for promises
|
||||
by yielding them.
|
||||
*/
|
||||
function async (makeGenerator) {
|
||||
return function (arg) {
|
||||
var generator = makeGenerator.apply(this, arguments)
|
||||
|
||||
function handle (result) {
|
||||
if (result.done) return Promise.resolve(result.value)
|
||||
|
||||
return Promise.resolve(result.value).then(function (res) {
|
||||
return handle(generator.next(res))
|
||||
}, function (err) {
|
||||
return handle(generator.throw(err))
|
||||
})
|
||||
}
|
||||
try {
|
||||
return handle(generator.next())
|
||||
} catch (ex) {
|
||||
generator.throw(ex)
|
||||
// return Promise.reject(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
g.async = async
|
||||
|
||||
function logUsers (self) {
|
||||
if (self.constructor === Array) {
|
||||
self = {users: self}
|
||||
}
|
||||
self.users[0].db.logTable()
|
||||
self.users[1].db.logTable()
|
||||
self.users[2].db.logTable()
|
||||
}
|
||||
|
||||
g.logUsers = logUsers
|
||||
410
src/Struct.js
Normal file
410
src/Struct.js
Normal file
@@ -0,0 +1,410 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
/*
|
||||
An operation also defines the structure of a type. This is why operation and
|
||||
structure are used interchangeably here.
|
||||
|
||||
It must be of the type Object. I hope to achieve some performance
|
||||
improvements when working on databases that support the json format.
|
||||
|
||||
An operation must have the following properties:
|
||||
|
||||
* encode
|
||||
- Encode the structure in a readable format (preferably string- todo)
|
||||
* decode (todo)
|
||||
- decode structure to json
|
||||
* execute
|
||||
- Execute the semantics of an operation.
|
||||
* 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
|
||||
|
||||
op = {
|
||||
target: Id
|
||||
}
|
||||
*/
|
||||
Delete: {
|
||||
encode: function (op) {
|
||||
return op
|
||||
},
|
||||
requiredOps: function (op) {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function * (op) {
|
||||
return yield* this.deleteOperation(op.target, op.length || 1)
|
||||
}
|
||||
},
|
||||
Insert: {
|
||||
/* {
|
||||
content: [any],
|
||||
opContent: Id,
|
||||
id: Id,
|
||||
left: Id,
|
||||
origin: Id,
|
||||
right: Id,
|
||||
parent: Id,
|
||||
parentSub: string (optional), // child of Map type
|
||||
}
|
||||
*/
|
||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
||||
// TODO: you could not send the "left" property, then you also have to
|
||||
// "op.left = null" in $execute or $decode
|
||||
var e/* :any */ = {
|
||||
id: op.id,
|
||||
left: op.left,
|
||||
right: op.right,
|
||||
origin: op.origin,
|
||||
parent: op.parent,
|
||||
struct: op.struct
|
||||
}
|
||||
if (op.parentSub != null) {
|
||||
e.parentSub = op.parentSub
|
||||
}
|
||||
if (op.hasOwnProperty('opContent')) {
|
||||
e.opContent = op.opContent
|
||||
} else {
|
||||
e.content = op.content.slice()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
List: {
|
||||
/*
|
||||
{
|
||||
start: null,
|
||||
end: null,
|
||||
struct: "List",
|
||||
type: "",
|
||||
id: this.os.getNextOpId(1)
|
||||
}
|
||||
*/
|
||||
create: function (id) {
|
||||
return {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'List',
|
||||
id: id
|
||||
}
|
||||
},
|
||||
encode: function (op) {
|
||||
var e = {
|
||||
struct: 'List',
|
||||
id: op.id,
|
||||
type: op.type
|
||||
}
|
||||
if (op.requires != null) {
|
||||
e.requires = op.requires
|
||||
}
|
||||
if (op.info != null) {
|
||||
e.info = op.info
|
||||
}
|
||||
return e
|
||||
},
|
||||
requiredOps: function () {
|
||||
/*
|
||||
var ids = []
|
||||
if (op.start != null) {
|
||||
ids.push(op.start)
|
||||
}
|
||||
if (op.end != null){
|
||||
ids.push(op.end)
|
||||
}
|
||||
return ids
|
||||
*/
|
||||
return []
|
||||
},
|
||||
execute: function * (op) {
|
||||
op.start = null
|
||||
op.end = null
|
||||
},
|
||||
ref: function * (op, pos) {
|
||||
if (op.start == null) {
|
||||
return null
|
||||
}
|
||||
var res = null
|
||||
var o = yield* this.getOperation(op.start)
|
||||
|
||||
while (true) {
|
||||
if (!o.deleted) {
|
||||
res = o
|
||||
pos--
|
||||
}
|
||||
if (pos >= 0 && o.right != null) {
|
||||
o = yield* this.getOperation(o.right)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
map: function * (o, f) {
|
||||
o = o.start
|
||||
var res = []
|
||||
while (o != null) { // TODO: change to != (at least some convention)
|
||||
var operation = yield* this.getOperation(o)
|
||||
if (!operation.deleted) {
|
||||
res.push(f(operation))
|
||||
}
|
||||
o = operation.right
|
||||
}
|
||||
return res
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
1095
src/Transaction.js
Normal file
1095
src/Transaction.js
Normal file
File diff suppressed because it is too large
Load Diff
736
src/Utils.js
Normal file
736
src/Utils.js
Normal file
@@ -0,0 +1,736 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
/*
|
||||
EventHandler is an helper class for constructing custom types.
|
||||
|
||||
Why: When constructing custom types, you sometimes want your types to work
|
||||
synchronous: E.g.
|
||||
``` Synchronous
|
||||
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 helps you to make your type
|
||||
synchronous.
|
||||
*/
|
||||
module.exports = function (Y /* : any*/) {
|
||||
Y.utils = {}
|
||||
|
||||
class EventListenerHandler {
|
||||
constructor () {
|
||||
this.eventListeners = []
|
||||
}
|
||||
destroy () {
|
||||
this.eventListeners = null
|
||||
}
|
||||
/*
|
||||
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!')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 = []
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
*/
|
||||
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.CustomType = CustomType
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
||||
186
src/y.js
Normal file
186
src/y.js
Normal file
@@ -0,0 +1,186 @@
|
||||
/* @flow */
|
||||
'use strict'
|
||||
|
||||
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 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)
|
||||
}
|
||||
this.store.whenTransactionsFinished()
|
||||
.then(callback)
|
||||
})
|
||||
}
|
||||
isConnected () {
|
||||
return this.connector.isSynced
|
||||
}
|
||||
disconnect () {
|
||||
return this.connector.disconnect()
|
||||
}
|
||||
reconnect () {
|
||||
return this.connector.reconnect()
|
||||
}
|
||||
destroy () {
|
||||
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 window !== 'undefined') {
|
||||
window.Y = Y
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
chai = require('chai')
|
||||
expect = chai.expect
|
||||
should = chai.should()
|
||||
sinon = require('sinon')
|
||||
sinonChai = require('sinon-chai')
|
||||
_ = require("underscore")
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
|
||||
Y = require "../lib/y.coffee"
|
||||
|
||||
Test = require "./TestSuite"
|
||||
|
||||
class JsonTest extends Test
|
||||
makeNewUser: (userId)->
|
||||
conn = new Connector userId
|
||||
super new Y conn
|
||||
|
||||
type: "JsonTest"
|
||||
|
||||
getRandomRoot: (user_num, root)->
|
||||
root ?= @users[user_num]
|
||||
types = @users[user_num].types
|
||||
if _.random(0,1) is 1 # take root
|
||||
root
|
||||
else # take child
|
||||
elems = null
|
||||
if root.type is "Object"
|
||||
elems =
|
||||
for oname,val of root.val()
|
||||
val
|
||||
else if root.type is "Array"
|
||||
elems = root.val()
|
||||
else
|
||||
return root
|
||||
|
||||
elems = elems.filter (elem)->
|
||||
(elem.type is "Array") or (elem.type is "Object")
|
||||
if elems.length is 0
|
||||
root
|
||||
else
|
||||
p = elems[_.random(0, elems.length-1)]
|
||||
@getRandomRoot user_num, p
|
||||
|
||||
|
||||
getContent: (user_num)->
|
||||
@users[user_num].toJson(true)
|
||||
|
||||
getGeneratingFunctions: (user_num)->
|
||||
types = @users[user_num].types
|
||||
super(user_num).concat [
|
||||
f : (y)=> # SET PROPERTY
|
||||
l = y.val().length
|
||||
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
|
||||
null
|
||||
types : [types.Array]
|
||||
, f : (y)=> # Delete Array Element
|
||||
list = y.val()
|
||||
if list.length > 0
|
||||
key = list[_random(0,list.length-1)]
|
||||
y.delete(key)
|
||||
types: [types.Array]
|
||||
, f : (y)=> # insert TEXT mutable
|
||||
l = y.val().length
|
||||
y.val(_.random(0, l-1), @getRamdomObject())
|
||||
types: [types.Array]
|
||||
, f : (y)=> # insert string
|
||||
l = y.val().length
|
||||
y.val(_.random(0, l-1), @getRandomText(), 'immutable')
|
||||
null
|
||||
types : [types.Array]
|
||||
, f : (y)=> # Delete Object Property
|
||||
list = for name, o of y.val()
|
||||
name
|
||||
if list.length > 0
|
||||
key = list[_random(0,list.length-1)]
|
||||
y.delete(key)
|
||||
types: [types.Object]
|
||||
, f : (y)=> # SET Object Property
|
||||
y.val(@getRandomKey(), @getRandomObject())
|
||||
types: [types.Object]
|
||||
,
|
||||
f : (y)=> # SET PROPERTY TEXT
|
||||
y.val(@getRandomKey(), @getRandomText(), 'mutable')
|
||||
types: [types.Object]
|
||||
]
|
||||
|
||||
describe "JsonFramework", ->
|
||||
beforeEach (done)->
|
||||
@timeout 50000
|
||||
@yTest = new JsonTest()
|
||||
@users = @yTest.users
|
||||
|
||||
@test_user = @yTest.makeNewUser "test_user"
|
||||
done()
|
||||
|
||||
it "can handle many engines, many operations, concurrently (random)", ->
|
||||
console.log "" # TODO
|
||||
@yTest.run()
|
||||
|
||||
it "has a working test suite", ->
|
||||
@yTest.compareAll()
|
||||
|
||||
it "handles double-late-join", ->
|
||||
test = new JsonTest("double")
|
||||
test.run()
|
||||
@yTest.run()
|
||||
u1 = test.users[0]
|
||||
u2 = @yTest.users[1]
|
||||
ops1 = u1.HB._encode()
|
||||
ops2 = u2.HB._encode()
|
||||
u1.HB.renewStateVector u2.HB.getOperationCounter()
|
||||
u2.HB.renewStateVector u1.HB.getOperationCounter()
|
||||
u1.engine.applyOps ops2
|
||||
u2.engine.applyOps ops1
|
||||
expect(test.getContent(0)).to.deep.equal(@yTest.getContent(1))
|
||||
|
||||
it "can handle creaton of complex json (1)", ->
|
||||
@yTest.users[0].val('a', 'q', "mutable")
|
||||
@yTest.users[1].val('a', 't', "mutable")
|
||||
@yTest.compareAll()
|
||||
q = @yTest.users[2].val('a')
|
||||
q.insert(0,'A')
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().val("a").val()).to.equal("At")
|
||||
|
||||
it "can handle creaton of complex json (2)", ->
|
||||
@yTest.getSomeUser().val('x', {'a':'b'})
|
||||
@yTest.getSomeUser().val('a', {'a':{q:"dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt"}}, "mutable")
|
||||
@yTest.getSomeUser().val('b', {'a':{}})
|
||||
@yTest.getSomeUser().val('c', {'a':'c'})
|
||||
@yTest.getSomeUser().val('c', {'a':'b'})
|
||||
@yTest.compareAll()
|
||||
q = @yTest.getSomeUser().val("a").val("a").val("q")
|
||||
q.insert(0,'A')
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
|
||||
|
||||
it "can handle creaton of complex json (3)", ->
|
||||
@yTest.users[0].val('l', [1,2,3], "mutable")
|
||||
@yTest.users[1].val('l', [4,5,6], "mutable")
|
||||
@yTest.compareAll()
|
||||
@yTest.users[2].val('l').insert(0,'A')
|
||||
w = @yTest.users[1].val('l').insert(0,'B', "mutable").val(0)
|
||||
w.insert 1, "C"
|
||||
expect(w.val()).to.equal("BC")
|
||||
@yTest.compareAll()
|
||||
|
||||
it "handles immutables and primitive data types", ->
|
||||
@yTest.getSomeUser().val('string', "text", "immutable")
|
||||
@yTest.getSomeUser().val('number', 4, "immutable")
|
||||
@yTest.getSomeUser().val('object', {q:"rr"}, "immutable")
|
||||
@yTest.getSomeUser().val('null', null)
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().val('string')).to.equal "text"
|
||||
expect(@yTest.getSomeUser().val('number')).to.equal 4
|
||||
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
|
||||
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
|
||||
|
||||
it "handles immutables and primitive data types (2)", ->
|
||||
@yTest.users[0].val('string', "text", "immutable")
|
||||
@yTest.users[1].val('number', 4, "immutable")
|
||||
@yTest.users[2].val('object', {q:"rr"}, "immutable")
|
||||
@yTest.users[0].val('null', null)
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().val('string')).to.equal "text"
|
||||
expect(@yTest.getSomeUser().val('number')).to.equal 4
|
||||
expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
|
||||
expect(@yTest.getSomeUser().val('null') is null).to.be.ok
|
||||
|
||||
it "Observers work on JSON Types (add type observers, local and foreign)", ->
|
||||
u = @yTest.users[0]
|
||||
@yTest.flushAll()
|
||||
last_task = null
|
||||
observer1 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("add")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.changedBy).to.equal('0')
|
||||
expect(change.name).to.equal("newStuff")
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.val("newStuff","someStuff","mutable")
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
observer2 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("add")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.changedBy).to.equal('1')
|
||||
expect(change.name).to.equal("moreStuff")
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1]
|
||||
v.val("moreStuff","someMoreStuff")
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
|
||||
it "Observers work on JSON Types (update type observers, local and foreign)", ->
|
||||
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
|
||||
@yTest.flushAll()
|
||||
last_task = null
|
||||
observer1 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("update")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.changedBy).to.equal('0')
|
||||
expect(change.name).to.equal("newStuff")
|
||||
expect(change.oldValue.val()).to.equal("oldStuff")
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.val("newStuff","someStuff")
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
observer2 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("update")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.changedBy).to.equal('1')
|
||||
expect(change.name).to.equal("moreStuff")
|
||||
expect(change.oldValue.val()).to.equal("moreOldStuff")
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1]
|
||||
v.val("moreStuff","someMoreStuff")
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
|
||||
|
||||
it "Observers work on JSON Types (delete type observers, local and foreign)", ->
|
||||
u = @yTest.users[0].val("newStuff","oldStuff","mutable").val("moreStuff","moreOldStuff","mutable")
|
||||
@yTest.flushAll()
|
||||
last_task = null
|
||||
observer1 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("delete")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.changedBy).to.equal('0')
|
||||
expect(change.name).to.equal("newStuff")
|
||||
expect(change.oldValue.val()).to.equal("oldStuff")
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.delete("newStuff")
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
observer2 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("delete")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.changedBy).to.equal('1')
|
||||
expect(change.name).to.equal("moreStuff")
|
||||
expect(change.oldValue.val()).to.equal("moreOldStuff")
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1]
|
||||
v.delete("moreStuff")
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
|
||||
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
chai = require('chai')
|
||||
expect = chai.expect
|
||||
should = chai.should()
|
||||
sinon = require('sinon')
|
||||
sinonChai = require('sinon-chai')
|
||||
_ = require("underscore")
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
|
||||
|
||||
module.exports = class Test
|
||||
constructor: (@name_suffix = "")->
|
||||
@number_of_test_cases_multiplier = 1
|
||||
@repeat_this = 3 * @number_of_test_cases_multiplier
|
||||
@doSomething_amount = 123 * @number_of_test_cases_multiplier
|
||||
@number_of_engines = 5 + @number_of_test_cases_multiplier - 1
|
||||
|
||||
@time = 0 # denotes to the time when run was started
|
||||
@ops = 0 # number of operations (used with @time)
|
||||
@time_now = 0 # current time
|
||||
|
||||
@debug = false
|
||||
|
||||
@reinitialize()
|
||||
|
||||
reinitialize: ()->
|
||||
@users = []
|
||||
for i in [0...@number_of_engines]
|
||||
u = @makeNewUser (i+@name_suffix)
|
||||
for user in @users
|
||||
u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient
|
||||
@users.push u
|
||||
@initUsers?(@users[0])
|
||||
@flushAll()
|
||||
|
||||
# is called by implementing class
|
||||
makeNewUser: (user)->
|
||||
user.HB.setManualGarbageCollect()
|
||||
user
|
||||
|
||||
getSomeUser: ()->
|
||||
i = _.random 0, (@users.length-1)
|
||||
@users[i]
|
||||
|
||||
getRandomText: (chars, min_length = 0)->
|
||||
chars ?= "abcdefghijklmnopqrstuvwxyz"
|
||||
length = _.random min_length, 10
|
||||
#length = 1
|
||||
nextchar = chars[(_.random 0, (chars.length-1))]
|
||||
text = ""
|
||||
_(length).times ()-> text += nextchar
|
||||
text
|
||||
|
||||
getRandomObject: ()->
|
||||
result = {}
|
||||
key1 = @getRandomKey()
|
||||
key2 = @getRandomKey()
|
||||
val1 = @getRandomText()
|
||||
val2 = null
|
||||
if _.random(0,1) is 1
|
||||
val2 = @getRandomObject()
|
||||
else
|
||||
val2 = @getRandomText()
|
||||
result[key1] = val1
|
||||
result[key2] = val2
|
||||
result
|
||||
|
||||
getRandomKey: ()->
|
||||
@getRandomText [1,2,'x','y'], 1 # only 4 keys
|
||||
|
||||
getGeneratingFunctions: (user_num)=>
|
||||
types = @users[user_num].types
|
||||
[
|
||||
f : (y)=> # INSERT TEXT
|
||||
y
|
||||
pos = _.random 0, (y.val().length-1)
|
||||
y.insert pos, @getRandomText()
|
||||
null
|
||||
types: [types.String]
|
||||
,
|
||||
f : (y)-> # DELETE TEXT
|
||||
if y.val().length > 0
|
||||
pos = _.random 0, (y.val().length-1) # TODO: put here also arbitrary number (test behaviour in error cases)
|
||||
length = _.random 0, (y.val().length - pos)
|
||||
ops1 = y.delete pos, length
|
||||
undefined
|
||||
types : [types.String]
|
||||
]
|
||||
getRandomRoot: (user_num)->
|
||||
throw new Error "overwrite me!"
|
||||
|
||||
getContent: (user_num)->
|
||||
throw new Error "overwrite me!"
|
||||
|
||||
generateRandomOp: (user_num)=>
|
||||
y = @getRandomRoot(user_num)
|
||||
choices = @getGeneratingFunctions(user_num).filter (gf)->
|
||||
_.some gf.types, (type)->
|
||||
y instanceof type
|
||||
|
||||
if choices.length is 0
|
||||
console.dir(y)
|
||||
throw new Error "You forgot to specify a test generation methot for this Operation! (#{y.type})"
|
||||
i = _.random 0, (choices.length-1)
|
||||
choices[i].f y
|
||||
|
||||
applyRandomOp: (user_num)=>
|
||||
user = @users[user_num]
|
||||
user.getConnector().flushOneRandom()
|
||||
|
||||
doSomething: ()->
|
||||
user_num = _.random (@number_of_engines-1)
|
||||
choices = [@applyRandomOp, @generateRandomOp]
|
||||
choice = _.random (choices.length-1)
|
||||
choices[choice](user_num)
|
||||
|
||||
flushAll: (final)->
|
||||
# TODO:!!
|
||||
final = false
|
||||
if @users.length <= 1 or not final
|
||||
for user,user_number in @users
|
||||
user.getConnector().flushAll()
|
||||
else
|
||||
for user,user_number in @users[1..]
|
||||
user.getConnector().flushAll()
|
||||
ops = @users[1].getHistoryBuffer()._encode @users[0].HB.getOperationCounter()
|
||||
@users[0].engine.applyOpsCheckDouble ops
|
||||
|
||||
|
||||
|
||||
compareAll: (test_number)->
|
||||
@flushAll(true)
|
||||
|
||||
@time += (new Date()).getTime() - @time_now
|
||||
|
||||
number_of_created_operations = 0
|
||||
for i in [0...(@users.length)]
|
||||
number_of_created_operations += @users[i].getConnector().getOpsInExecutionOrder().length
|
||||
@ops += number_of_created_operations*@users.length
|
||||
|
||||
ops_per_msek = Math.floor(@ops/@time)
|
||||
if test_number? # and @debug
|
||||
console.log "#{test_number}/#{@repeat_this}: #{number_of_created_operations} were created and applied on (#{@users.length}) users ops in a different order." + " Over all we consumed #{@ops} operations in #{@time/1000} seconds (#{ops_per_msek} ops/msek)."
|
||||
|
||||
for i in [0...(@users.length-1)]
|
||||
if @debug
|
||||
if not _.isEqual @getContent(i), @getContent(i+1)
|
||||
printOpsInExecutionOrder = (otnumber, otherotnumber)=>
|
||||
ops = _.filter @users[otnumber].getConnector().getOpsInExecutionOrder(), (o)->
|
||||
typeof o.uid.op_name isnt 'string' and o.uid.creator isnt '_'
|
||||
for s,j in ops
|
||||
console.log "op#{j} = " + (JSON.stringify s)
|
||||
console.log ""
|
||||
s = "ops = ["
|
||||
for o,j in ops
|
||||
if j isnt 0
|
||||
s += ", "
|
||||
s += "op#{j}"
|
||||
s += "]"
|
||||
console.log s
|
||||
console.log "@test_user.engine.applyOps ops"
|
||||
console.log "expect(@test_user.val('name').val()).to.equal(\"#{@users[otherotnumber].val('name').val()}\")"
|
||||
ops
|
||||
console.log ""
|
||||
console.log "Found an OT Puzzle!"
|
||||
console.log "OT states:"
|
||||
for u,j in @users
|
||||
console.log "OT#{j}: "+u.val('name').val()
|
||||
console.log "\nOT execution order (#{i},#{i+1}):"
|
||||
printOpsInExecutionOrder i, i+1
|
||||
console.log ""
|
||||
ops = printOpsInExecutionOrder i+1, i
|
||||
|
||||
console.log ""
|
||||
expect(@getContent(i)).to.deep.equal(@getContent(i+1))
|
||||
|
||||
run: ()->
|
||||
if @debug
|
||||
console.log ''
|
||||
for times in [1..@repeat_this]
|
||||
@time_now = (new Date).getTime()
|
||||
for i in [1..Math.floor(@doSomething_amount/2)]
|
||||
@doSomething()
|
||||
@flushAll(false)
|
||||
for u in @users
|
||||
u.HB.emptyGarbage()
|
||||
for i in [1..Math.floor(@doSomething_amount/2)]
|
||||
@doSomething()
|
||||
|
||||
@compareAll(times)
|
||||
@testHBencoding()
|
||||
if times isnt @repeat_this
|
||||
@reinitialize()
|
||||
|
||||
testHBencoding: ()->
|
||||
# in case of JsonFramework, every user will create its JSON first! therefore, the testusers id must be small than all the others (see InsertType)
|
||||
@users[@users.length] = @makeNewUser (-1) # this does not want to join with anymody
|
||||
|
||||
@users[@users.length-1].HB.renewStateVector @users[0].HB.getOperationCounter()
|
||||
@users[@users.length-1].engine.applyOps @users[0].HB._encode()
|
||||
|
||||
#if @getContent(@users.length-1) isnt @getContent(0)
|
||||
# console.log "testHBencoding:"
|
||||
# console.log "Unprocessed ops first: #{@users[0].engine.unprocessed_ops.length}"
|
||||
# console.log "Unprocessed ops last: #{@users[@users.length-1].engine.unprocessed_ops.length}"
|
||||
expect(@getContent(@users.length-1)).to.deep.equal(@getContent(0))
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
chai = require('chai')
|
||||
expect = chai.expect
|
||||
should = chai.should()
|
||||
sinon = require('sinon')
|
||||
sinonChai = require('sinon-chai')
|
||||
_ = require("underscore")
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
Y = require "../lib/y"
|
||||
Connector = require "../../y-connectors/lib/y-test/y-test.coffee"
|
||||
|
||||
Test = require "./TestSuite"
|
||||
class TextTest extends Test
|
||||
|
||||
type: "TextTest"
|
||||
|
||||
makeNewUser: (userId)->
|
||||
conn = new Connector userId
|
||||
new Y conn
|
||||
|
||||
initUsers: (u)->
|
||||
u.val("TextTest","","mutable")
|
||||
|
||||
getRandomRoot: (user_num)->
|
||||
@users[user_num].val("TextTest")
|
||||
|
||||
getContent: (user_num)->
|
||||
@users[user_num].val("TextTest").val()
|
||||
|
||||
describe "TextFramework", ->
|
||||
beforeEach (done)->
|
||||
@timeout 50000
|
||||
@yTest = new TextTest()
|
||||
done()
|
||||
|
||||
it "simple multi-char insert", ->
|
||||
u = @yTest.users[0].val("TextTest")
|
||||
u.insert 0, "abc"
|
||||
u = @yTest.users[1].val("TextTest")
|
||||
u.insert 0, "xyz"
|
||||
@yTest.compareAll()
|
||||
u.delete 0, 1
|
||||
@yTest.compareAll()
|
||||
expect(u.val()).to.equal("bcxyz")
|
||||
|
||||
it "Observers work on shared Text (insert type observers, local and foreign)", ->
|
||||
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
|
||||
@yTest.flushAll()
|
||||
last_task = null
|
||||
observer1 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("insert")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.value).to.equal("a")
|
||||
expect(change.position).to.equal(1)
|
||||
expect(change.changedBy).to.equal('0')
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.insert 1, "a"
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
observer2 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("insert")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.value).to.equal("x")
|
||||
expect(change.position).to.equal(0)
|
||||
expect(change.changedBy).to.equal('1')
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1].val("TextTest")
|
||||
v.insert 0, "x"
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
|
||||
it "Observers work on shared Text (delete type observers, local and foreign)", ->
|
||||
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
|
||||
@yTest.flushAll()
|
||||
last_task = null
|
||||
observer1 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("delete")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.position).to.equal(1)
|
||||
expect(change.length).to.equal(1)
|
||||
expect(change.changedBy).to.equal('0')
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.delete 1, 1
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
observer2 = (changes)->
|
||||
expect(changes.length).to.equal(1)
|
||||
change = changes[0]
|
||||
expect(change.type).to.equal("delete")
|
||||
expect(change.object).to.equal(u)
|
||||
expect(change.position).to.equal(0)
|
||||
expect(change.length).to.equal(1)
|
||||
expect(change.changedBy).to.equal('1')
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1].val("TextTest")
|
||||
v.delete 0, 1
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
|
||||
it "can handle many engines, many operations, concurrently (random)", ->
|
||||
console.log("testiy deleted this TODO:dtrn")
|
||||
@yTest.run()
|
||||
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
chai = require('chai')
|
||||
expect = chai.expect
|
||||
should = chai.should()
|
||||
sinon = require('sinon')
|
||||
sinonChai = require('sinon-chai')
|
||||
_ = require("underscore")
|
||||
$ = require("jquery")
|
||||
document?.$ = $ # for browser
|
||||
require 'coffee-errors'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
||||
Y = require "../lib/index"
|
||||
Connector = require "../../Yatta-Connectors/lib/test-connector/test-connector.coffee"
|
||||
|
||||
Test = require "./TestSuite"
|
||||
class XmlTest extends Test
|
||||
|
||||
type: "XmlTest"
|
||||
|
||||
makeNewUser: (user, conn)->
|
||||
super new Y.XmlFramework user, conn
|
||||
|
||||
getRandomRoot: (user_num)->
|
||||
@users[user_num].getSharedObject()
|
||||
|
||||
getContent: (user_num)->
|
||||
@users[user_num].val()
|
||||
|
||||
|
||||
describe "XmlFramework", ->
|
||||
beforeEach (done)->
|
||||
@timeout 50000
|
||||
@yTest = new XmlTest()
|
||||
###
|
||||
|
||||
@users = @yTest.users
|
||||
###
|
||||
test_users = []
|
||||
connector = (new Connector 0, test_users)
|
||||
@test_user = @yTest.makeNewUser 0, connector
|
||||
test_users.push @test_user
|
||||
# test_user_listen listens to the actions of test_user. He will update his dom when he receives from test_user.
|
||||
@test_user_listen = @yTest.makeNewUser 2, connector
|
||||
test_users.push @test_user_listen
|
||||
@test_user2 = @yTest.makeNewUser 1, (Connector_uninitialized [])
|
||||
|
||||
$("#test_dom").replaceWith('<div id="test_dom" test_attribute="the test" class="stuffy" style="color: blue"><p id="replaceme">replace me</p><p id="removeme">remove me</p><p>This is a test object for <b>XmlFramework</b></p><span class="span_element"><p>span</p></span></div>')
|
||||
@$dom = $("#test_dom")
|
||||
@dom = @$dom[0]
|
||||
@test_user.val(@dom)
|
||||
@test_user_listen.getConnector().flushAll()
|
||||
@test_user_listen_dom = @test_user_listen.val()
|
||||
|
||||
@check = ()=>
|
||||
dom_ = @dom.outerHTML
|
||||
# now test if other collaborators can parse the HB and result in the same content
|
||||
hb = @test_user.HB._encode()
|
||||
@test_user2.engine.applyOps(hb)
|
||||
dom2 = @test_user2.val()
|
||||
expect(dom_).to.equal(dom2.outerHTML)
|
||||
@test_user_listen.getConnector().flushAll()
|
||||
expect(dom_).to.equal(@test_user_listen_dom.outerHTML)
|
||||
done()
|
||||
|
||||
it "can transform to a new real Dom element", ->
|
||||
dom_ = @test_user.val(true)
|
||||
expect(dom_ isnt @dom).to.be.true
|
||||
|
||||
it "supports dom.insertBefore", ->
|
||||
newdom = $("<p>dtrn</p>")[0]
|
||||
newdom2 = $("<p>dtrn2</p>")[0]
|
||||
n = $("#removeme")[0]
|
||||
@dom.insertBefore(newdom, null)
|
||||
@dom.insertBefore(newdom2, n)
|
||||
@check()
|
||||
|
||||
it "supports dom.appendChild", ->
|
||||
newdom = $("<p>dtrn</p>")[0]
|
||||
@dom.appendChild(newdom)
|
||||
@check()
|
||||
|
||||
it "supports dom.setAttribute", ->
|
||||
@dom.setAttribute("test_attribute", "newVal")
|
||||
@check()
|
||||
|
||||
it "supports dom.removeAttribute", ->
|
||||
@dom.removeAttribute("test_attribute")
|
||||
@check()
|
||||
|
||||
it "supports dom.removeChild", ->
|
||||
@dom.removeChild($("#removeme")[0])
|
||||
expect($("#removeme").length).to.equal(0)
|
||||
@check()
|
||||
|
||||
it "supports dom.replaceChild", ->
|
||||
newdom = $("<p>replaced</p>")[0]
|
||||
replace = $("#replaceme")[0]
|
||||
@dom.replaceChild(newdom,replace)
|
||||
expect($("#replaceme").length).to.equal(0)
|
||||
@check()
|
||||
|
||||
it "supports dom.classList.add", ->
|
||||
@dom.classList.add "classy"
|
||||
@check()
|
||||
|
||||
|
||||
it "supports dom.textcontent", -> #TODO!!!!
|
||||
@dom.classList.add "classy"
|
||||
@check()
|
||||
|
||||
it "supports jquery.addClass", ->
|
||||
@$dom.addClass("testy")
|
||||
@check()
|
||||
|
||||
it "supports jquery.after", ->
|
||||
d = $("#test_dom p")
|
||||
d.after("<div class=\"inserted_after\">after</div>")
|
||||
@check()
|
||||
|
||||
it "supports jquery.append", ->
|
||||
d = $("#test_dom p")
|
||||
d.after("<b>appended</b>")
|
||||
@check()
|
||||
|
||||
it "supports jquery.appendTo", ->
|
||||
$("<b>appendedTo</b>").appendTo("#test_dom p")
|
||||
$("p").appendTo("#test_dom")
|
||||
@check()
|
||||
|
||||
it "supports jquery.before", ->
|
||||
d = $("#test_dom p")
|
||||
d.before("<div>before</div>")
|
||||
@check()
|
||||
|
||||
it "supports jquery.detach", ->
|
||||
d = $(".inserted_after")
|
||||
d.detach()
|
||||
@check()
|
||||
|
||||
it "supports jquery.empty", ->
|
||||
d = $("#test_dom p")
|
||||
d.empty()
|
||||
@check()
|
||||
|
||||
it "supports jquery.insertAfter", ->
|
||||
$("<p>after span</p>").insertAfter(".span_element")
|
||||
@check()
|
||||
|
||||
it "supports jquery.insertBefore", ->
|
||||
$("<p>before span</p>").insertBefore(".span_element")
|
||||
@check()
|
||||
|
||||
it "supports jquery.prepend", ->
|
||||
d = $("#test_dom div")
|
||||
d.prepend("<p>prepended</p>")
|
||||
@check()
|
||||
|
||||
it "supports jquery.prependTo", ->
|
||||
$("<p atone=false attwo=\"dtrn\" class=\"attr_node sudo su\">prepended to</p>").prependTo("#test_dom div")
|
||||
@check()
|
||||
|
||||
it "supports jquery.remove", ->
|
||||
d = $("#test_dom b")
|
||||
d.remove()
|
||||
@check()
|
||||
|
||||
it "supports jquery.removeAttr", ->
|
||||
d = $(".attr_node")
|
||||
d.removeAttr("attwo")
|
||||
@check()
|
||||
|
||||
it "supports jquery.removeClass", ->
|
||||
d = $(".attr_node")
|
||||
d.removeClass("sudo")
|
||||
@check()
|
||||
|
||||
it "supports jquery.attr", ->
|
||||
d = $(".attr_node")
|
||||
d.attr("atone", true)
|
||||
@check()
|
||||
|
||||
it "supports jquery.replaceAll", ->
|
||||
$("<span>New span content </span>").replaceAll("#test_dom div")
|
||||
@check()
|
||||
|
||||
it "supports jquery.replaceWith", ->
|
||||
d = $("#test_dom span")
|
||||
d.replaceWith("<div>me is div again </div>")
|
||||
@check()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
|
||||
<polymer-element name="y-object" hidden attributes="val connector y">
|
||||
</polymer-element>
|
||||
<polymer-element name="y-property" hidden attributes="val name y">
|
||||
</polymer-element>
|
||||
|
||||
<script src="./build/browser/y-object.js"></script>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user