Compare commits
592 Commits
v0.4.1
...
v13.0.0-29
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4f06e8f62 | ||
|
|
05cd1d0575 | ||
|
|
4edc22bedb | ||
|
|
16f84c67d5 | ||
|
|
290d3c8ffe | ||
|
|
c51e8b46c2 | ||
|
|
0cda1630d2 | ||
|
|
d232b883e9 | ||
|
|
3a0e65403f | ||
|
|
224fff93ba | ||
|
|
4f55e8c655 | ||
|
|
a08624c04e | ||
|
|
9b00929172 | ||
|
|
b94267e14a | ||
|
|
e696304845 | ||
|
|
d503c9d640 | ||
|
|
e5f289506f | ||
|
|
c453593ee7 | ||
|
|
5ed1818de5 | ||
|
|
0310500c4e | ||
|
|
b7defc32e8 | ||
|
|
dbdd49af23 | ||
|
|
b7c05ba133 | ||
|
|
9298903bdb | ||
|
|
d59e30b239 | ||
|
|
d29b83a457 | ||
|
|
0208d83f91 | ||
|
|
c545118637 | ||
|
|
c619aa33d9 | ||
|
|
1dea8f394f | ||
|
|
5cf8d20cf6 | ||
|
|
74f9ceab01 | ||
|
|
ca81cdf3be | ||
|
|
96c6aa2751 | ||
|
|
e6b5e258fb | ||
|
|
e8170a09a7 | ||
|
|
9d1ad8cb28 | ||
|
|
d859fd68fe | ||
|
|
2b7d2ed1e6 | ||
|
|
142a5ada60 | ||
|
|
c92f987496 | ||
|
|
755c9eb16e | ||
|
|
1311c7a0d8 | ||
|
|
4eec8ecdd3 | ||
|
|
0e426f8928 | ||
|
|
82015d5a37 | ||
|
|
d9ee67d2f3 | ||
|
|
791f6c12f0 | ||
|
|
23d019c244 | ||
|
|
c8ca80d15f | ||
|
|
be282c8338 | ||
|
|
829a094c6d | ||
|
|
725273167e | ||
|
|
581264c5e3 | ||
|
|
be537c9f8c | ||
|
|
4028eee39d | ||
|
|
0e3e561ec7 | ||
|
|
7df46cb731 | ||
|
|
40fb16ef32 | ||
|
|
ada5d36cd5 | ||
|
|
f537a43e29 | ||
|
|
3a305fb228 | ||
|
|
1afdab376d | ||
|
|
526c862071 | ||
|
|
fdbb558ce2 | ||
|
|
76ad58bb59 | ||
|
|
c88a813bb0 | ||
|
|
ccf6d86c98 | ||
|
|
6b5c02f1ce | ||
|
|
2be6e935a4 | ||
|
|
0ddf3bf742 | ||
|
|
5f29724578 | ||
|
|
ab6cde07e6 | ||
|
|
0455eaa8ad | ||
|
|
9ed7e15d0f | ||
|
|
6e633d0bd9 | ||
|
|
e16195cb54 | ||
|
|
86c46cf0ec | ||
|
|
8770c8e934 | ||
|
|
7e12ea2db5 | ||
|
|
3ca260e0da | ||
|
|
edb5e4f719 | ||
|
|
be3b8b65ce | ||
|
|
d093ef56c8 | ||
|
|
90b2a895b8 | ||
|
|
4f57c91b82 | ||
|
|
3e1d89253f | ||
|
|
03e1a3fc12 | ||
|
|
5c33f41c30 | ||
|
|
65e8c29b33 | ||
|
|
fed77d532f | ||
|
|
d129184f7b | ||
|
|
a05bb1d4f9 | ||
|
|
65af4963e6 | ||
|
|
4dce0816a6 | ||
|
|
5384bf4faf | ||
|
|
454ac9ba16 | ||
|
|
e2ec53be65 | ||
|
|
aa6edcfd9b | ||
|
|
f31ec9a8b8 | ||
|
|
003fa735a0 | ||
|
|
574f0c3269 | ||
|
|
eb4fb3a225 | ||
|
|
c97130abc4 | ||
|
|
a19cfa1465 | ||
|
|
bb45abbb70 | ||
|
|
67b47fd868 | ||
|
|
2c18b9ffad | ||
|
|
a6b7d76544 | ||
|
|
442ea7ec70 | ||
|
|
747da52c0b | ||
|
|
6c37bd4463 | ||
|
|
dd6c196135 | ||
|
|
252bec0ad2 | ||
|
|
6c8876d282 | ||
|
|
3c317828d1 | ||
|
|
cd3f4a72d6 | ||
|
|
2c852c85c6 | ||
|
|
434ec84837 | ||
|
|
2b618cd83c | ||
|
|
f4327529b9 | ||
|
|
67189f4d44 | ||
|
|
6225fb4dfd | ||
|
|
a7550fe5d3 | ||
|
|
9d9c84f40e | ||
|
|
ae91902de3 | ||
|
|
033d24eee7 | ||
|
|
8abef69aa7 | ||
|
|
7e4dedab38 | ||
|
|
85e488bbe6 | ||
|
|
a6a321da10 | ||
|
|
008764ccdc | ||
|
|
de5f4abe32 | ||
|
|
382d06f6d4 | ||
|
|
66de422749 | ||
|
|
bbf5e39408 | ||
|
|
c8bca15d72 | ||
|
|
a64730e651 | ||
|
|
409a9414f1 | ||
|
|
24facaab09 | ||
|
|
060549f2cb | ||
|
|
dfe3b0b1d1 | ||
|
|
a5506a5ded | ||
|
|
361d4a48e1 | ||
|
|
e23154bec2 | ||
|
|
1682d43c26 | ||
|
|
68c417fe6f | ||
|
|
2ea163a5cf | ||
|
|
020dacdad4 | ||
|
|
42abcc897c | ||
|
|
0a321610aa | ||
|
|
edf47d3491 | ||
|
|
14ee42cad5 | ||
|
|
f990927d3e | ||
|
|
a1cef4662f | ||
|
|
2c343970c4 | ||
|
|
74b41e03e3 | ||
|
|
b242aab955 | ||
|
|
8e4efd9bba | ||
|
|
47d5899058 | ||
|
|
a126a29876 | ||
|
|
4aa720116f | ||
|
|
e29162c3fc | ||
|
|
aa40855953 | ||
|
|
b6545d62fc | ||
|
|
3425d95507 | ||
|
|
53682c17fb | ||
|
|
a492a83f0c | ||
|
|
d340e557c1 | ||
|
|
d5cd9d94d5 | ||
|
|
e1a160b894 | ||
|
|
f996ac83d2 | ||
|
|
922637930f | ||
|
|
ff7e9cdef2 | ||
|
|
f02641deb7 | ||
|
|
f97144356c | ||
|
|
a9fdd5df66 | ||
|
|
e90f241ae0 | ||
|
|
102bef4f92 | ||
|
|
96e9c3c166 | ||
|
|
1080f83990 | ||
|
|
66b6b2a568 | ||
|
|
7415f27fbc | ||
|
|
c9d1f34864 | ||
|
|
34997f940b | ||
|
|
4e9e21e75e | ||
|
|
6c375a37c8 | ||
|
|
cd0cddaf35 | ||
|
|
93c23ddc09 | ||
|
|
480dfdfb77 | ||
|
|
dda2a1ef82 | ||
|
|
f32ff1b613 | ||
|
|
8ab16f4ada | ||
|
|
3fdcf82bcc | ||
|
|
6dd33f4f90 | ||
|
|
0521fac8d8 | ||
|
|
666ab8285c | ||
|
|
675c7f6638 | ||
|
|
463608cb5c | ||
|
|
d1059b5d04 | ||
|
|
8b24284e25 | ||
|
|
08bcdfb008 | ||
|
|
f93d7b1e70 | ||
|
|
4d024883bc | ||
|
|
ecd412c6f6 | ||
|
|
b939cdd086 | ||
|
|
17803266d4 | ||
|
|
f0e88d192c | ||
|
|
e66c0f8a4e | ||
|
|
eba3d590cc | ||
|
|
0b31e63b82 | ||
|
|
d22fbca6cc | ||
|
|
330434ee24 | ||
|
|
2f0216bf89 | ||
|
|
f9d0625bd2 | ||
|
|
7a9d60770a | ||
|
|
059f72ffe1 | ||
|
|
d2d74a64ab | ||
|
|
a1f0140069 | ||
|
|
7bd8e81342 | ||
|
|
34f365cd8f | ||
|
|
b3ba8e7546 | ||
|
|
e1e94bcf5d | ||
|
|
4a83ff8514 | ||
|
|
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 |
12
.babelrc
Normal file
12
.babelrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers"
|
||||
]
|
||||
}
|
||||
14
.flowconfig
Normal file
14
.flowconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
[ignore]
|
||||
.*/node_modules/.*
|
||||
.*/dist/.*
|
||||
.*/build/.*
|
||||
|
||||
[include]
|
||||
./src/
|
||||
./tests-lib/
|
||||
./test/
|
||||
|
||||
[libs]
|
||||
./declarations/
|
||||
|
||||
[options]
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,4 @@
|
||||
/node_modules/
|
||||
node_modules
|
||||
bower_components
|
||||
.directory
|
||||
.c9
|
||||
.codio
|
||||
.settings
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
|
||||
@@ -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
|
||||
306
README.md
306
README.md
@@ -1,81 +1,295 @@
|
||||
|
||||
# 
|
||||
# 
|
||||
|
||||
[](http://layers.dbis.rwth-aachen.de/jenkins/job/Yatta/)
|
||||
Yjs is a framework for offline-first p2p shared editing on structured data like
|
||||
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
||||
most of the complexity of concurrent editing. For additional information, demos,
|
||||
and tutorials visit [y-js.org](http://y-js.org/).
|
||||
|
||||
Yjs 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]. Yjs was designed to take away the pain from concurrently editing complex data types like Text, Json, and XML. You can find some applications for this framework [here](https://dadamonad.github.io/yjs/examples/).
|
||||
### Extensions
|
||||
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
|
||||
* *Connector* - a communication protocol that propagates changes to the clients
|
||||
* *Database* - a database to store your changes
|
||||
* one or more *Types* - that represent the shared data
|
||||
|
||||
In the future, we want to enable users to implement their own collaborative types. Currently we provide data types for
|
||||
* Text
|
||||
* Json
|
||||
* XML
|
||||
Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
||||
is a list of the modules we know of:
|
||||
|
||||
Unlike other frameworks, Yjs supports P2P message propagation and is not bound to a specific communication protocol. Therefore, Yjs is extremely scalable and can be used in a wide range of application scenarios.
|
||||
##### Connectors
|
||||
|
||||
We support several communication protocols as so called *Connectors*. You can create your own connector too - as it is described [here](https://dadamonad.github.io/yjs/connector/Howto-create-your-own-Connector.html). Currently, we support the following 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
|
||||
|Name | Description |
|
||||
|----------------|-----------------------------------|
|
||||
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
||||
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
||||
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
||||
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|
||||
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
||||
|
||||
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide a polymer element for Yjs!
|
||||
##### Database adapters
|
||||
|
||||
The theoretical 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.
|
||||
|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 |
|
||||
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
|
||||
|
||||
|
||||
##### Types
|
||||
|
||||
| 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 the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) |
|
||||
|[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/)|
|
||||
|
||||
##### Other
|
||||
|
||||
| Name | Description |
|
||||
|-----------|-------------------|
|
||||
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
|
||||
|
||||
## 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
|
||||
[npm](https://www.npmjs.org/package/yjs).
|
||||
|
||||
### Bower
|
||||
```
|
||||
bower install rwth-acis/yjs
|
||||
bower install --save yjs y-array % add all y-* modules you want to use
|
||||
```
|
||||
Then you include the libraries directly from the installation folder.
|
||||
You only need to include the `y.js` file. Yjs is able to automatically require
|
||||
missing modules.
|
||||
```
|
||||
<script src="./bower_components/yjs/y.js"></script>
|
||||
```
|
||||
|
||||
### Npm
|
||||
```
|
||||
npm install yjs --save
|
||||
npm install --save yjs % add all y-* modules you want to use
|
||||
```
|
||||
|
||||
And use it like this with *npm*:
|
||||
If you don't include via script tag, you have to explicitly include all modules!
|
||||
(Same goes for other module systems)
|
||||
```
|
||||
Y = require("yjs");
|
||||
var Y = require('yjs')
|
||||
require('y-array')(Y) // add the y-array type to Yjs
|
||||
require('y-websockets-client')(Y)
|
||||
require('y-memory')(Y)
|
||||
require('y-array')(Y)
|
||||
require('y-map')(Y)
|
||||
require('y-text')(Y)
|
||||
// ..
|
||||
// do the same for all modules you want to use
|
||||
```
|
||||
|
||||
## 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)).
|
||||
### ES6 Syntax
|
||||
```
|
||||
import Y from 'yjs'
|
||||
import yArray from 'y-array'
|
||||
import yWebsocketsClient from 'y-webrtc'
|
||||
import yMemory from 'y-memory'
|
||||
import yArray from 'y-array'
|
||||
import yMap from 'y-map'
|
||||
import yText from 'y-text'
|
||||
// ..
|
||||
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
|
||||
```
|
||||
|
||||
### 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
|
||||
# Text editing example
|
||||
Install dependencies
|
||||
```
|
||||
bower i yjs y-memory y-webrtc y-array y-text
|
||||
```
|
||||
|
||||
Here is a simple example of a shared textarea
|
||||
```HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script src="./bower_components/yjs/y.js"></script>
|
||||
<!-- Yjs automatically includes all missing dependencies (browser only) -->
|
||||
<script>
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory' // use memory database adapter.
|
||||
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
|
||||
},
|
||||
connector: {
|
||||
name: 'webrtc', // use webrtc connector
|
||||
// name: 'websockets-client'
|
||||
// name: 'xmpp'
|
||||
room: 'my-room' // clients connecting to the same room share data
|
||||
},
|
||||
sourceDir: '/bower_components', // location of the y-* modules (browser only)
|
||||
share: {
|
||||
textarea: 'Text' // y.share.textarea is of type y-text
|
||||
}
|
||||
}).then(function (y) {
|
||||
// The Yjs instance `y` is available
|
||||
// y.share.* contains the shared types
|
||||
|
||||
## Get help
|
||||
[](https://gitter.im/rwth-acis/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
// Bind `y.share.textarea` to `<textarea/>`
|
||||
y.share.textarea.bind(document.querySelector('textarea'))
|
||||
})
|
||||
</script>
|
||||
<textarea></textarea>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Please report _any_ issues to the [Github issue page](https://github.com/rwth-acis/yjs/issues)! I try to fix them very soon, if possible.
|
||||
## Get Help & Give Help
|
||||
There are some friendly people on [](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
|
||||
|
||||
Report _any_ issues to the
|
||||
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
|
||||
soon, if possible.
|
||||
|
||||
# API
|
||||
|
||||
### Y(options)
|
||||
* Y.extend(module1, module2, ..)
|
||||
* Add extensions to Y
|
||||
* `Y.extend(require('y-webrtc'))` has the same semantics as
|
||||
`require('y-webrtc')(Y)`
|
||||
* 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.
|
||||
* Set `options.connector.generateUserId = true` in order to genenerate a
|
||||
userid, instead of receiving one from the server. This way the `Y(..)` is
|
||||
immediately going to be resolved, without waiting for any confirmation from
|
||||
the server. Use with caution.
|
||||
* Have a look at the used connector repository to see all available options.
|
||||
* *Only if you know what you are doing:* Set
|
||||
`options.connector.preferUntransformed = true` in order receive the shared
|
||||
data untransformed. This is very efficient as the database content is simply
|
||||
copied to this client. This does only work if this client receives content
|
||||
from only one client.
|
||||
* options.sourceDir (browser only)
|
||||
* Path where all y-* modules are stored
|
||||
* Defaults to `/bower_components`
|
||||
* Not required when running on `nodejs` / `iojs`
|
||||
* When using nodejs you need to manually extend Yjs:
|
||||
```
|
||||
var Y = require('yjs')
|
||||
// you have to require a 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 only share data if they specified the
|
||||
same type with the same property name
|
||||
* options.type (browser only)
|
||||
* 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.close()
|
||||
* Destroy this object.
|
||||
* Destroys all types (they will throw weird errors if you still use them)
|
||||
* Disconnects from the other instances (via connector)
|
||||
* Returns a promise
|
||||
* y.destroy()
|
||||
* calls y.close()
|
||||
* Removes all data from the database
|
||||
* Returns a promise
|
||||
* y.db.stopGarbageCollector()
|
||||
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
|
||||
collection
|
||||
* y.db.gc :: Boolean
|
||||
* Whether gc is turned on
|
||||
* 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**
|
||||
|
||||
### Logging
|
||||
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
|
||||
`y*` enables logging for all y-* components. You can selectively remove
|
||||
components you are not interested in: E.g. The flag `y*,-y:connector-message`
|
||||
will not log the long `y:connector-message` messages.
|
||||
|
||||
##### Enable logging in Node.js
|
||||
```sh
|
||||
DEBUG=y* node app.js
|
||||
```
|
||||
|
||||
Remove the colors in order to log to a file:
|
||||
```sh
|
||||
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
||||
```
|
||||
|
||||
##### Enable logging in the browser
|
||||
```js
|
||||
localStorage.debug = 'y*'
|
||||
```
|
||||
|
||||
## 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.
|
||||
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).
|
||||
|
||||
<kevin.jahns@rwth-aachen.de>
|
||||
|
||||
[ShareJs]: https://github.com/share/ShareJS
|
||||
[OpenCoweb]: https://github.com/opencoweb/coweb
|
||||
|
||||
Yjs is licensed under the [MIT License](./LICENSE).
|
||||
|
||||
<yjs@dbis.rwth-aachen.de>
|
||||
|
||||
33
bower.json
33
bower.json
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "0.4.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
# Directories
|
||||
|
||||
### build/browser
|
||||
You find the browserified (not minified) version of yjs here. This is nice for debugging, since it also includes sourcemaps. For production, however, you should use the version that you find in the main directory.
|
||||
|
||||
### build/node
|
||||
Yjs for nodejs is located here. You can only use the submodules, or require 'y' in your node project. Also works with browserify.
|
||||
|
||||
### build/test
|
||||
Start build/test/index.html' in your browser, to perform testing Yjs.
|
||||
@@ -1,7 +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="./y-object.js"></script>
|
||||
File diff suppressed because one or more lines are too long
2461
build/browser/y.js
2461
build/browser/y.js
File diff suppressed because one or more lines are too long
@@ -1,71 +0,0 @@
|
||||
var ConnectorClass, adaptConnector;
|
||||
|
||||
ConnectorClass = require("./ConnectorClass");
|
||||
|
||||
adaptConnector = function(connector, engine, HB, execution_listener) {
|
||||
var applyHB, encode_state_vector, f, getHB, getStateVector, name, parse_state_vector, send_;
|
||||
for (name in ConnectorClass) {
|
||||
f = ConnectorClass[name];
|
||||
connector[name] = f;
|
||||
}
|
||||
connector.setIsBoundToY();
|
||||
send_ = function(o) {
|
||||
if ((o.uid.creator === HB.getUserId()) && (typeof o.uid.op_number !== "string") && (o.uid.doSync === "true" || o.uid.doSync === true) && (HB.getUserId() !== "_temp")) {
|
||||
return connector.broadcast(o);
|
||||
}
|
||||
};
|
||||
if (connector.invokeSync != null) {
|
||||
HB.setInvokeSyncHandler(connector.invokeSync);
|
||||
}
|
||||
execution_listener.push(send_);
|
||||
encode_state_vector = function(v) {
|
||||
var 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, state_vector;
|
||||
state_vector = parse_state_vector(v);
|
||||
hb = HB._encode(state_vector);
|
||||
json = {
|
||||
hb: hb,
|
||||
state_vector: encode_state_vector(HB.getOperationCounter())
|
||||
};
|
||||
return json;
|
||||
};
|
||||
applyHB = function(hb, fromHB) {
|
||||
return engine.applyOp(hb, fromHB);
|
||||
};
|
||||
connector.getStateVector = getStateVector;
|
||||
connector.getHB = getHB;
|
||||
connector.applyHB = applyHB;
|
||||
if (connector.receive_handlers == null) {
|
||||
connector.receive_handlers = [];
|
||||
}
|
||||
return connector.receive_handlers.push(function(sender, op) {
|
||||
if (op.uid.creator !== HB.getUserId()) {
|
||||
return engine.applyOp(op);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = adaptConnector;
|
||||
@@ -1,355 +0,0 @@
|
||||
module.exports = {
|
||||
init: function(options) {
|
||||
var req;
|
||||
req = (function(_this) {
|
||||
return function(name, choices) {
|
||||
if (options[name] != null) {
|
||||
if ((choices == null) || choices.some(function(c) {
|
||||
return c === options[name];
|
||||
})) {
|
||||
return _this[name] = options[name];
|
||||
} else {
|
||||
throw new Error("You can set the '" + name + "' option to one of the following choices: " + JSON.encode(choices));
|
||||
}
|
||||
} else {
|
||||
throw new Error("You must specify " + name + ", when initializing the Connector!");
|
||||
}
|
||||
};
|
||||
})(this);
|
||||
req("syncMethod", ["syncAll", "master-slave"]);
|
||||
req("role", ["master", "slave"]);
|
||||
req("user_id");
|
||||
if (typeof this.on_user_id_set === "function") {
|
||||
this.on_user_id_set(this.user_id);
|
||||
}
|
||||
if (options.perform_send_again != null) {
|
||||
this.perform_send_again = options.perform_send_again;
|
||||
} else {
|
||||
this.perform_send_again = true;
|
||||
}
|
||||
if (this.role === "master") {
|
||||
this.syncMethod = "syncAll";
|
||||
}
|
||||
this.is_synced = false;
|
||||
this.connections = {};
|
||||
if (this.receive_handlers == null) {
|
||||
this.receive_handlers = [];
|
||||
}
|
||||
this.connections = {};
|
||||
this.current_sync_target = null;
|
||||
this.sent_hb_to_all_users = false;
|
||||
return this.is_initialized = true;
|
||||
},
|
||||
isRoleMaster: function() {
|
||||
return this.role === "master";
|
||||
},
|
||||
isRoleSlave: function() {
|
||||
return this.role === "slave";
|
||||
},
|
||||
findNewSyncTarget: function() {
|
||||
var c, user, _ref;
|
||||
this.current_sync_target = null;
|
||||
if (this.syncMethod === "syncAll") {
|
||||
_ref = this.connections;
|
||||
for (user in _ref) {
|
||||
c = _ref[user];
|
||||
if (!c.is_synced) {
|
||||
this.performSync(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.current_sync_target == null) {
|
||||
this.setStateSynced();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
userLeft: function(user) {
|
||||
delete this.connections[user];
|
||||
return this.findNewSyncTarget();
|
||||
},
|
||||
userJoined: function(user, role) {
|
||||
var _base;
|
||||
if (role == null) {
|
||||
throw new Error("Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')");
|
||||
}
|
||||
if ((_base = this.connections)[user] == null) {
|
||||
_base[user] = {};
|
||||
}
|
||||
this.connections[user].is_synced = false;
|
||||
if ((!this.is_synced) || this.syncMethod === "syncAll") {
|
||||
if (this.syncMethod === "syncAll") {
|
||||
return this.performSync(user);
|
||||
} else if (role === "master") {
|
||||
return this.performSyncWithMaster(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
whenSynced: function(args) {
|
||||
if (args.constructore === Function) {
|
||||
args = [args];
|
||||
}
|
||||
if (this.is_synced) {
|
||||
return args[0].apply(this, args.slice(1));
|
||||
} else {
|
||||
if (this.compute_when_synced == null) {
|
||||
this.compute_when_synced = [];
|
||||
}
|
||||
return this.compute_when_synced.push(args);
|
||||
}
|
||||
},
|
||||
onReceive: function(f) {
|
||||
return this.receive_handlers.push(f);
|
||||
},
|
||||
|
||||
/*
|
||||
* Broadcast a message to all connected peers.
|
||||
* @param message {Object} The message to broadcast.
|
||||
*
|
||||
broadcast: (message)->
|
||||
throw new Error "You must implement broadcast!"
|
||||
|
||||
*
|
||||
* Send a message to a peer, or set of peers
|
||||
*
|
||||
send: (peer_s, message)->
|
||||
throw new Error "You must implement send!"
|
||||
*/
|
||||
performSync: function(user) {
|
||||
var hb, o, _hb, _i, _len;
|
||||
if (this.current_sync_target == null) {
|
||||
this.current_sync_target = user;
|
||||
this.send(user, {
|
||||
sync_step: "getHB",
|
||||
send_again: "true",
|
||||
data: []
|
||||
});
|
||||
if (!this.sent_hb_to_all_users) {
|
||||
this.sent_hb_to_all_users = true;
|
||||
hb = this.getHB([]).hb;
|
||||
_hb = [];
|
||||
for (_i = 0, _len = hb.length; _i < _len; _i++) {
|
||||
o = hb[_i];
|
||||
_hb.push(o);
|
||||
if (_hb.length > 30) {
|
||||
this.broadcast({
|
||||
sync_step: "applyHB_",
|
||||
data: _hb
|
||||
});
|
||||
_hb = [];
|
||||
}
|
||||
}
|
||||
return this.broadcast({
|
||||
sync_step: "applyHB",
|
||||
data: _hb
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
performSyncWithMaster: function(user) {
|
||||
var hb, o, _hb, _i, _len;
|
||||
this.current_sync_target = user;
|
||||
this.send(user, {
|
||||
sync_step: "getHB",
|
||||
send_again: "true",
|
||||
data: []
|
||||
});
|
||||
hb = this.getHB([]).hb;
|
||||
_hb = [];
|
||||
for (_i = 0, _len = hb.length; _i < _len; _i++) {
|
||||
o = hb[_i];
|
||||
_hb.push(o);
|
||||
if (_hb.length > 30) {
|
||||
this.broadcast({
|
||||
sync_step: "applyHB_",
|
||||
data: _hb
|
||||
});
|
||||
_hb = [];
|
||||
}
|
||||
}
|
||||
return this.broadcast({
|
||||
sync_step: "applyHB",
|
||||
data: _hb
|
||||
});
|
||||
},
|
||||
setStateSynced: function() {
|
||||
var f, _i, _len, _ref;
|
||||
if (!this.is_synced) {
|
||||
this.is_synced = true;
|
||||
if (this.compute_when_synced != null) {
|
||||
_ref = this.compute_when_synced;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
f = _ref[_i];
|
||||
f();
|
||||
}
|
||||
delete this.compute_when_synced;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
receiveMessage: function(sender, res) {
|
||||
var data, f, hb, o, sendApplyHB, send_again, _hb, _i, _j, _len, _len1, _ref, _results;
|
||||
if (res.sync_step == null) {
|
||||
_ref = this.receive_handlers;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
f = _ref[_i];
|
||||
_results.push(f(sender, res));
|
||||
}
|
||||
return _results;
|
||||
} else {
|
||||
if (sender === this.user_id) {
|
||||
return;
|
||||
}
|
||||
if (res.sync_step === "getHB") {
|
||||
data = this.getHB(res.data);
|
||||
hb = data.hb;
|
||||
_hb = [];
|
||||
if (this.is_synced) {
|
||||
sendApplyHB = (function(_this) {
|
||||
return function(m) {
|
||||
return _this.send(sender, m);
|
||||
};
|
||||
})(this);
|
||||
} else {
|
||||
sendApplyHB = (function(_this) {
|
||||
return function(m) {
|
||||
return _this.broadcast(m);
|
||||
};
|
||||
})(this);
|
||||
}
|
||||
for (_j = 0, _len1 = hb.length; _j < _len1; _j++) {
|
||||
o = hb[_j];
|
||||
_hb.push(o);
|
||||
if (_hb.length > 30) {
|
||||
sendApplyHB({
|
||||
sync_step: "applyHB_",
|
||||
data: _hb
|
||||
});
|
||||
_hb = [];
|
||||
}
|
||||
}
|
||||
sendApplyHB({
|
||||
sync_step: "applyHB",
|
||||
data: _hb
|
||||
});
|
||||
if ((res.send_again != null) && this.perform_send_again) {
|
||||
send_again = (function(_this) {
|
||||
return function(sv) {
|
||||
return function() {
|
||||
hb = _this.getHB(sv).hb;
|
||||
return _this.send(sender, {
|
||||
sync_step: "applyHB",
|
||||
data: hb,
|
||||
sent_again: "true"
|
||||
});
|
||||
};
|
||||
};
|
||||
})(this)(data.state_vector);
|
||||
return setTimeout(send_again, 3000);
|
||||
}
|
||||
} else if (res.sync_step === "applyHB") {
|
||||
this.applyHB(res.data, sender === this.current_sync_target);
|
||||
if ((this.syncMethod === "syncAll" || (res.sent_again != null)) && (!this.is_synced) && ((this.current_sync_target === sender) || (this.current_sync_target == null))) {
|
||||
this.connections[sender].is_synced = true;
|
||||
return this.findNewSyncTarget();
|
||||
}
|
||||
} else if (res.sync_step === "applyHB_") {
|
||||
return this.applyHB(res.data, sender === this.current_sync_target);
|
||||
}
|
||||
}
|
||||
},
|
||||
parseMessageFromXml: function(m) {
|
||||
var parse_array, parse_object;
|
||||
parse_array = function(node) {
|
||||
var n, _i, _len, _ref, _results;
|
||||
_ref = node.children;
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
n = _ref[_i];
|
||||
if (n.getAttribute("isArray") === "true") {
|
||||
_results.push(parse_array(n));
|
||||
} else {
|
||||
_results.push(parse_object(n));
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
parse_object = function(node) {
|
||||
var int, json, n, name, value, _i, _len, _ref, _ref1;
|
||||
json = {};
|
||||
_ref = node.attrs;
|
||||
for (name in _ref) {
|
||||
value = _ref[name];
|
||||
int = parseInt(value);
|
||||
if (isNaN(int) || ("" + int) !== value) {
|
||||
json[name] = value;
|
||||
} else {
|
||||
json[name] = int;
|
||||
}
|
||||
}
|
||||
_ref1 = node.children;
|
||||
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
||||
n = _ref1[_i];
|
||||
name = n.name;
|
||||
if (n.getAttribute("isArray") === "true") {
|
||||
json[name] = parse_array(n);
|
||||
} else {
|
||||
json[name] = parse_object(n);
|
||||
}
|
||||
}
|
||||
return json;
|
||||
};
|
||||
return parse_object(m);
|
||||
},
|
||||
encodeMessageToXml: function(m, json) {
|
||||
var encode_array, encode_object;
|
||||
encode_object = function(m, json) {
|
||||
var name, value;
|
||||
for (name in json) {
|
||||
value = json[name];
|
||||
if (value == null) {
|
||||
|
||||
} else if (value.constructor === Object) {
|
||||
encode_object(m.c(name), value);
|
||||
} else if (value.constructor === Array) {
|
||||
encode_array(m.c(name), value);
|
||||
} else {
|
||||
m.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
};
|
||||
encode_array = function(m, array) {
|
||||
var e, _i, _len;
|
||||
m.setAttribute("isArray", "true");
|
||||
for (_i = 0, _len = array.length; _i < _len; _i++) {
|
||||
e = array[_i];
|
||||
if (e.constructor === Object) {
|
||||
encode_object(m.c("array-element"), e);
|
||||
} else {
|
||||
encode_array(m.c("array-element"), e);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
};
|
||||
if (json.constructor === Object) {
|
||||
return encode_object(m.c("y", {
|
||||
xmlns: "http://y.ninja/connector-stanza"
|
||||
}), json);
|
||||
} else if (json.constructor === Array) {
|
||||
return encode_array(m.c("y", {
|
||||
xmlns: "http://y.ninja/connector-stanza"
|
||||
}), json);
|
||||
} else {
|
||||
throw new Error("I can't encode this json!");
|
||||
}
|
||||
},
|
||||
setIsBoundToY: function() {
|
||||
if (typeof this.on_bound_to_y === "function") {
|
||||
this.on_bound_to_y();
|
||||
}
|
||||
delete this.when_bound_to_y;
|
||||
return this.is_bound_to_y = true;
|
||||
}
|
||||
};
|
||||
@@ -1,119 +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, fromHB) {
|
||||
var o, op_json, _i, _len;
|
||||
if (fromHB == null) {
|
||||
fromHB = false;
|
||||
}
|
||||
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];
|
||||
if (fromHB) {
|
||||
op_json.fromHB = "true";
|
||||
}
|
||||
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,257 +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];
|
||||
if (o.uid.creator != null) {
|
||||
o.uid.creator = id;
|
||||
}
|
||||
if (o.uid.alt != null) {
|
||||
o.uid.alt.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];
|
||||
}
|
||||
if (this.operation_counter[this.user_id] != null) {
|
||||
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.noOperation == null) && 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])) && (state_vector[user] != null)) {
|
||||
_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,495 +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() {
|
||||
var map_uid;
|
||||
if (this.uid.noOperation == null) {
|
||||
return this.uid;
|
||||
} else {
|
||||
if (this.uid.alt != null) {
|
||||
map_uid = this.uid.alt.cloneUid();
|
||||
map_uid.sub = this.uid.sub;
|
||||
map_uid.doSync = false;
|
||||
return map_uid;
|
||||
} else {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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,353 +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, rm, rm_uid;
|
||||
if (this.map[property_name] == null) {
|
||||
event_properties = {
|
||||
name: property_name
|
||||
};
|
||||
event_this = this;
|
||||
rm_uid = {
|
||||
noOperation: true,
|
||||
sub: property_name,
|
||||
alt: this
|
||||
};
|
||||
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.prev_cl != null)) {
|
||||
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,570 +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'] = JSON.stringify(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'];
|
||||
if (typeof content === "string") {
|
||||
content = JSON.parse(content);
|
||||
}
|
||||
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, edited_element, range, s;
|
||||
range = {};
|
||||
s = dom_root.getSelection();
|
||||
clength = textfield.textContent.length;
|
||||
range.left = Math.min(s.anchorOffset, clength);
|
||||
range.right = Math.min(s.focusOffset, clength);
|
||||
if (fix != null) {
|
||||
range.left = fix(range.left);
|
||||
range.right = fix(range.right);
|
||||
}
|
||||
edited_element = s.focusNode;
|
||||
if (edited_element === textfield || edited_element === textfield.childNodes[0]) {
|
||||
range.isReal = true;
|
||||
} else {
|
||||
range.isReal = false;
|
||||
}
|
||||
return range;
|
||||
};
|
||||
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 c, content_array, i, _j, _len1, _results;
|
||||
content_array = content.replace(new RegExp("\n", 'g'), " ").split(" ");
|
||||
textfield.innerText = "";
|
||||
_results = [];
|
||||
for (i = _j = 0, _len1 = content_array.length; _j < _len1; i = ++_j) {
|
||||
c = content_array[i];
|
||||
textfield.innerText += c;
|
||||
if (i !== content_array.length - 1) {
|
||||
_results.push(textfield.innerHTML += ' ');
|
||||
} else {
|
||||
_results.push(void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
}
|
||||
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.keyCode === 13) {
|
||||
char = '\n';
|
||||
} else if (event.key != null) {
|
||||
if (event.charCode === 32) {
|
||||
char = " ";
|
||||
} else {
|
||||
char = event.key;
|
||||
}
|
||||
} else {
|
||||
char = window.String.fromCharCode(event.keyCode);
|
||||
}
|
||||
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.user_id != null) {
|
||||
user_id = connector.user_id;
|
||||
} else {
|
||||
user_id = "_temp";
|
||||
connector.on_user_id_set = 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;
|
||||
}
|
||||
16862
build/test/Json_test.js
16862
build/test/Json_test.js
File diff suppressed because one or more lines are too long
16634
build/test/Text_test.js
16634
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 Yjs!</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="Text_test.js"></script>
|
||||
<script src="Json_test.js"></script>
|
||||
<!--script src="Xml_test_browser.js"></script-->
|
||||
<script>
|
||||
//mocha.checkLeaks();
|
||||
//mocha.run();
|
||||
window.onerror = null;
|
||||
if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
|
||||
else { mocha.run(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
# Examples
|
||||
|
||||
Here you find some (hopefully) usefull examples on how to use Yjs!
|
||||
|
||||
Feel free to use the code of the examples in your own project. They include basic examples how to use Yjs.
|
||||
@@ -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>
|
||||
<p> This example tests a few things like Late Join, the synchronization process while editing, and if the Connector and Yjs work together as Polymer elements. you can use this example as a starting point for your own Polymer project too. </p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +0,0 @@
|
||||
<link rel="import" href="../../build/browser/y-object.html">
|
||||
<link rel="import" href="../../../y-webrtc/build/browser/y-webrtc.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-webrtc id="connector" connector={{connector}} room="testy-webrtc-polymer" debug="true"></y-webrtc>
|
||||
<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)
|
||||
})
|
||||
|
||||
// Everything is initialized. Lets test stuff!
|
||||
window.y_test = this;
|
||||
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);
|
||||
}
|
||||
})
|
||||
</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-webrtc/build/browser/y-webrtc.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 WebRTC 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,27 +0,0 @@
|
||||
|
||||
|
||||
connector = new Y.WebRTC("testy", {debug: true});
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -1,14 +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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +0,0 @@
|
||||
<link rel="import" href="../../build/browser/y-object.html">
|
||||
<link rel="import" href="../../../y-xmpp/build/browser/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" syncMode="syncAll" debug="true"></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)
|
||||
})
|
||||
|
||||
// Everything is initialized. Lets test stuff!
|
||||
window.y_test = this;
|
||||
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);
|
||||
}
|
||||
})
|
||||
</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-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().join("testy-xmpp-json3", {syncMode: "syncAll"});
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
32
examples/ace/index.html
Normal file
32
examples/ace/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css" media="screen">
|
||||
#aceContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
.inserted {
|
||||
position:absolute;
|
||||
z-index:20;
|
||||
background-color: #FFC107;
|
||||
}
|
||||
.deleted {
|
||||
position:absolute;
|
||||
z-index:20;
|
||||
background-color: #FFC107;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="aceContainer"></div>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
examples/ace/index.js
Normal file
24
examples/ace/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/* global Y, ace */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'ace-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
ace: 'Text' // y.share.textarea is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yAce = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
var editor = ace.edit('aceContainer')
|
||||
editor.setTheme('ace/theme/chrome')
|
||||
editor.getSession().setMode('ace/mode/javascript')
|
||||
|
||||
y.share.ace.bindAce(editor)
|
||||
})
|
||||
19
examples/bower.json
Normal file
19
examples/bower.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "yjs-examples",
|
||||
"version": "0.0.0",
|
||||
"homepage": "y-js.org",
|
||||
"authors": [
|
||||
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
||||
],
|
||||
"description": "Examples for Yjs",
|
||||
"license": "MIT",
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"quill": "^1.0.0-rc.2",
|
||||
"ace": "~1.2.3",
|
||||
"ace-builds": "~1.2.3",
|
||||
"jquery": "~2.2.2",
|
||||
"d3": "^3.5.16",
|
||||
"codemirror": "^5.25.0"
|
||||
}
|
||||
}
|
||||
19
examples/chat/index.html
Normal file
19
examples/chat/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
#chat p span {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
<div id="chat"></div>
|
||||
<form id="chatform">
|
||||
<input name="username" type="text" style="width:15%;">
|
||||
<input name="message" type="text" style="width:60%;">
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
71
examples/chat/index.js
Normal file
71
examples/chat/index.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
var y = new Y({
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'chat-example'
|
||||
}
|
||||
})
|
||||
|
||||
window.yChat = y
|
||||
|
||||
let chatprotocol = y.define('chatprotocol', Y.Array)
|
||||
|
||||
let chatcontainer = document.querySelector('#chat')
|
||||
|
||||
// This functions inserts a message at the specified position in the DOM
|
||||
function appendMessage (message, position) {
|
||||
var p = document.createElement('p')
|
||||
var uname = document.createElement('span')
|
||||
uname.appendChild(document.createTextNode(message.username + ': '))
|
||||
p.appendChild(uname)
|
||||
p.appendChild(document.createTextNode(message.message))
|
||||
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
|
||||
}
|
||||
// This function makes sure that only 7 messages exist in the chat history.
|
||||
// The rest is deleted
|
||||
function cleanupChat () {
|
||||
if (chatprotocol.length > 7) {
|
||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
||||
}
|
||||
}
|
||||
// Insert the initial content
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
cleanupChat()
|
||||
|
||||
// whenever content changes, make sure to reflect the changes in the DOM
|
||||
chatprotocol.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
appendMessage(event.values[i], event.index + i)
|
||||
}
|
||||
} else if (event.type === 'delete') {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
chatcontainer.children[event.index].remove()
|
||||
}
|
||||
}
|
||||
// concurrent insertions may result in a history > 7, so cleanup here
|
||||
cleanupChat()
|
||||
})
|
||||
document.querySelector('#chatform').onsubmit = function (event) {
|
||||
// the form is submitted
|
||||
var message = {
|
||||
username: this.querySelector('[name=username]').value,
|
||||
message: this.querySelector('[name=message]').value
|
||||
}
|
||||
if (message.username.length > 0 && message.message.length > 0) {
|
||||
if (chatprotocol.length > 6) {
|
||||
// If we are goint to insert the 8th element, make sure to delete first.
|
||||
chatprotocol.delete(0)
|
||||
}
|
||||
// Here we insert a message in the shared chat type.
|
||||
// This will call the observe function (see line 40)
|
||||
// and reflect the change in the DOM
|
||||
chatprotocol.push([message])
|
||||
this.querySelector('[name=message]').value = ''
|
||||
}
|
||||
// Do not send this form!
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
23
examples/codemirror/index.html
Normal file
23
examples/codemirror/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
<style>
|
||||
.CodeMirror {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
examples/codemirror/index.js
Normal file
24
examples/codemirror/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
})
|
||||
23
examples/drawing/index.html
Normal file
23
examples/drawing/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
path {
|
||||
fill: none;
|
||||
stroke: blue;
|
||||
stroke-width: 1px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
</style>
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
84
examples/drawing/index.js
Normal file
84
examples/drawing/index.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/* globals Y, d3 */
|
||||
'strict mode'
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example',
|
||||
url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
drawing: 'Array'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yDrawing = y
|
||||
var drawing = y.share.drawing
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basis')
|
||||
|
||||
var svg = d3.select('#drawingCanvas')
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
// we only implement insert events that are appended to the end of the array
|
||||
event.values.forEach(function (value) {
|
||||
line.datum().push(value)
|
||||
})
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
y.share.drawing.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
event.values.forEach(drawLine)
|
||||
} else {
|
||||
// just remove all elements (thats what we do anyway)
|
||||
svg.selectAll('path').remove()
|
||||
}
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i))
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
sharedLine = drawing.get(drawing.length - 1)
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 33)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
})
|
||||
11
examples/html-editor/index.html
Normal file
11
examples/html-editor/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
35
examples/html-editor/index.js
Normal file
35
examples/html-editor/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
let y = new Y({
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234',
|
||||
room: 'html-editor-example6'
|
||||
// maxBufferLength: 100
|
||||
}
|
||||
})
|
||||
window.yXml = y
|
||||
window.yXmlType = y.define('xml', Y.XmlFragment)
|
||||
window.onload = function () {
|
||||
console.log('start!')
|
||||
// Bind children of XmlFragment to the document.body
|
||||
window.yXmlType.bindToDom(document.body)
|
||||
}
|
||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
||||
captureTimeout: 0
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && e.metaKey) {
|
||||
console.log('uidtaren')
|
||||
if (!e.shiftKey) {
|
||||
console.info('Undo!')
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
console.info('Redo!')
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
21
examples/indexeddb/index.html
Normal file
21
examples/indexeddb/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
<style>
|
||||
.CodeMirror {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
24
examples/indexeddb/index.js
Normal file
24
examples/indexeddb/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
})
|
||||
58
examples/infiniteyjs/index.html
Normal file
58
examples/infiniteyjs/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: 7px;
|
||||
}
|
||||
.one {
|
||||
grid-column: 1 ;
|
||||
}
|
||||
.two {
|
||||
grid-column: 2;
|
||||
}
|
||||
.three {
|
||||
grid-column: 3;
|
||||
}
|
||||
textarea {
|
||||
width: calc(100% - 10px)
|
||||
}
|
||||
.editor-container {
|
||||
background-color: #4caf50;
|
||||
padding: 4px 5px 10px 5px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
.editor-container[disconnected] {
|
||||
background-color: red;
|
||||
}
|
||||
.disconnected-info {
|
||||
display: none;
|
||||
}
|
||||
.editor-container[disconnected] .disconnected-info {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
<div class="wrapper">
|
||||
<div id="container1" class="one editor-container">
|
||||
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container2" class="two editor-container">
|
||||
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container3" class="three editor-container">
|
||||
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/dist/y-text.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
64
examples/infiniteyjs/index.js
Normal file
64
examples/infiniteyjs/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/* global Y */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y1 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea1'))
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-second.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y2 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea2'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container2').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container2').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-third.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y3 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea3'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container3').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container3').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
26
examples/jigsaw/index.html
Normal file
26
examples/jigsaw/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
|
||||
<g>
|
||||
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
|
||||
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
|
||||
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
|
||||
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
74
examples/jigsaw/index.js
Normal file
74
examples/jigsaw/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/* @flow */
|
||||
/* global Y, d3 */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Puzzle-example',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
share: {
|
||||
piece1: 'Map',
|
||||
piece2: 'Map',
|
||||
piece3: 'Map',
|
||||
piece4: 'Map'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yJigsaw = y
|
||||
var origin // mouse start position - translation of piece
|
||||
var drag = d3.behavior.drag()
|
||||
.on('dragstart', function (params) {
|
||||
// get the translation of the element
|
||||
var translation = d3
|
||||
.select(this)
|
||||
.attr('transform')
|
||||
.slice(10, -1)
|
||||
.split(',')
|
||||
.map(Number)
|
||||
// mouse coordinates
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
origin = {
|
||||
x: mouse[0] - translation[0],
|
||||
y: mouse[1] - translation[1]
|
||||
}
|
||||
})
|
||||
.on('drag', function () {
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||
var y = mouse[1] - origin.y
|
||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||
})
|
||||
.on('dragend', function (piece, i) {
|
||||
// save the current translation of the puzzle piece
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x
|
||||
var y = mouse[1] - origin.y
|
||||
piece.set('translation', {x: x, y: y})
|
||||
})
|
||||
|
||||
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||
|
||||
pieces
|
||||
.classed('draggable', true)
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
}).call(drag)
|
||||
|
||||
data.forEach(function (piece) {
|
||||
piece.observe(function () {
|
||||
// whenever a property of a piece changes, update the translation of the pieces
|
||||
pieces
|
||||
.transition()
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
24
examples/monaco/index.html
Normal file
24
examples/monaco/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="monacoContainer"></div>
|
||||
<style>
|
||||
#monacoContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/y-array/y-array.js"></script>
|
||||
<script src="../bower_components/y-text/y-text.js"></script>
|
||||
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/y-memory/y-memory.js"></script>
|
||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
30
examples/monaco/index.js
Normal file
30
examples/monaco/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* global Y, monaco */
|
||||
|
||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// Initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'monaco-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
monaco: 'Text' // y.share.monaco is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yMonaco = y
|
||||
|
||||
// Create Monaco editor
|
||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||
language: 'javascript'
|
||||
})
|
||||
|
||||
// Bind to y.share.monaco
|
||||
y.share.monaco.bindMonaco(editor)
|
||||
})
|
||||
})
|
||||
1173
examples/package-lock.json
generated
Normal file
1173
examples/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
examples/package.json
Normal file
20
examples/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "rollup -c",
|
||||
"watch": "rollup -cw"
|
||||
},
|
||||
"author": "Kevin Jahns",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"monaco-editor": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^10.0.2"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": ["bower_components"]
|
||||
}
|
||||
}
|
||||
35
examples/quill/index.html
Normal file
35
examples/quill/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
-->
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
40
examples/quill/index.js
Normal file
40
examples/quill/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example-quill-1.0-test',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yQuill = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
27
examples/rollup.config.js
Normal file
27
examples/rollup.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'yjs-dist.esm',
|
||||
dest: 'yjs-dist.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
31
examples/serviceworker/index.html
Normal file
31
examples/serviceworker/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
-->
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
49
examples/serviceworker/index.js
Normal file
49
examples/serviceworker/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// register yjs service worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Register service worker
|
||||
// it is important to copy yjs-sw-template to the root directory!
|
||||
navigator.serviceWorker.register('./yjs-sw-template.js').then(function (reg) {
|
||||
console.log('Yjs service worker registration succeeded. Scope is ' + reg.scope)
|
||||
}).catch(function (err) {
|
||||
console.error('Yjs service worker registration failed with error ' + err)
|
||||
})
|
||||
}
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'serviceworker',
|
||||
room: 'ServiceWorkerExample2'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yServiceWorker = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
22
examples/serviceworker/yjs-sw-template.js
Normal file
22
examples/serviceworker/yjs-sw-template.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-env worker */
|
||||
|
||||
// copy and modify this file
|
||||
|
||||
self.DBConfig = {
|
||||
name: 'indexeddb'
|
||||
}
|
||||
self.ConnectorConfig = {
|
||||
name: 'websockets-client',
|
||||
// url: '..',
|
||||
options: {
|
||||
jsonp: false
|
||||
}
|
||||
}
|
||||
|
||||
importScripts(
|
||||
'/bower_components/yjs/y.js',
|
||||
'/bower_components/y-memory/y-memory.js',
|
||||
'/bower_components/y-indexeddb/y-indexeddb.js',
|
||||
'/bower_components/y-websockets-client/y-websockets-client.js',
|
||||
'/bower_components/y-serviceworker/yjs-sw-include.js'
|
||||
)
|
||||
11
examples/textarea/index.html
Normal file
11
examples/textarea/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/y-text.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
examples/textarea/index.js
Normal file
23
examples/textarea/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example2',
|
||||
// url: '//localhost:1234',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
},
|
||||
timeout: 5000 // reject if no connection was established within 5 seconds
|
||||
}).then(function (y) {
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
y.share.textarea.bind(document.getElementById('textfield'))
|
||||
})
|
||||
40
examples/xml/index.html
Normal file
40
examples/xml/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Shared DOM Example </h1>
|
||||
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
|
||||
</div>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
|
||||
</div>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var commands = document.querySelectorAll(".command");
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
|
||||
var execute = function(){
|
||||
eval(command.querySelector("input").value);
|
||||
}
|
||||
command.querySelector("button").onclick = execute
|
||||
$(command.querySelector("input")).keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
execute()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
23
examples/xml/index.js
Normal file
23
examples/xml/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'Xml-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yXml = y
|
||||
// bind xml type to a dom, and put it in body
|
||||
window.sharedDom = y.share.xml.getDom()
|
||||
document.body.appendChild(window.sharedDom)
|
||||
})
|
||||
7
examples/yjs-dist.esm
Normal file
7
examples/yjs-dist.esm
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
import Y from '../src/Y.js'
|
||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||
|
||||
Y.extend(yWebsocketsClient)
|
||||
|
||||
export default Y
|
||||
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'], ->
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
|
||||
ConnectorClass = require "./ConnectorClass"
|
||||
#
|
||||
# @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)->
|
||||
|
||||
for name, f of ConnectorClass
|
||||
connector[name] = f
|
||||
|
||||
connector.setIsBoundToY()
|
||||
|
||||
send_ = (o)->
|
||||
if (o.uid.creator is HB.getUserId()) and
|
||||
(typeof o.uid.op_number isnt "string") and # TODO: i don't think that we need this anymore..
|
||||
(o.uid.doSync is "true" or o.uid.doSync is true) and # TODO: ensure, that only true is valid
|
||||
(HB.getUserId() isnt "_temp")
|
||||
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
|
||||
json =
|
||||
hb: hb
|
||||
state_vector: encode_state_vector HB.getOperationCounter()
|
||||
json
|
||||
|
||||
applyHB = (hb, fromHB)->
|
||||
engine.applyOp hb, fromHB
|
||||
|
||||
connector.getStateVector = getStateVector
|
||||
connector.getHB = getHB
|
||||
connector.applyHB = applyHB
|
||||
|
||||
connector.receive_handlers ?= []
|
||||
connector.receive_handlers.push (sender, op)->
|
||||
if op.uid.creator isnt HB.getUserId()
|
||||
engine.applyOp op
|
||||
|
||||
|
||||
module.exports = adaptConnector
|
||||
@@ -1,316 +0,0 @@
|
||||
|
||||
module.exports =
|
||||
#
|
||||
# @params new Connector(options)
|
||||
# @param options.syncMethod {String} is either "syncAll" or "master-slave".
|
||||
# @param options.role {String} The role of this client
|
||||
# (slave or master (only used when syncMethod is master-slave))
|
||||
# @param options.perform_send_again {Boolean} Whetehr to whether to resend the HB after some time period. This reduces sync errors, but has some overhead (optional)
|
||||
#
|
||||
init: (options)->
|
||||
req = (name, choices)=>
|
||||
if options[name]?
|
||||
if (not choices?) or choices.some((c)->c is options[name])
|
||||
@[name] = options[name]
|
||||
else
|
||||
throw new Error "You can set the '"+name+"' option to one of the following choices: "+JSON.encode(choices)
|
||||
else
|
||||
throw new Error "You must specify "+name+", when initializing the Connector!"
|
||||
|
||||
req "syncMethod", ["syncAll", "master-slave"]
|
||||
req "role", ["master", "slave"]
|
||||
req "user_id"
|
||||
@on_user_id_set?(@user_id)
|
||||
|
||||
# whether to resend the HB after some time period. This reduces sync errors.
|
||||
# But this is not necessary in the test-connector
|
||||
if options.perform_send_again?
|
||||
@perform_send_again = options.perform_send_again
|
||||
else
|
||||
@perform_send_again = true
|
||||
|
||||
# A Master should sync with everyone! TODO: really? - for now its safer this way!
|
||||
if @role is "master"
|
||||
@syncMethod = "syncAll"
|
||||
|
||||
# is set to true when this is synced with all other connections
|
||||
@is_synced = false
|
||||
# Peerjs Connections: key: conn-id, value: object
|
||||
@connections = {}
|
||||
# List of functions that shall process incoming data
|
||||
@receive_handlers ?= []
|
||||
|
||||
# whether this instance is bound to any y instance
|
||||
@connections = {}
|
||||
@current_sync_target = null
|
||||
@sent_hb_to_all_users = false
|
||||
@is_initialized = true
|
||||
|
||||
isRoleMaster: ->
|
||||
@role is "master"
|
||||
|
||||
isRoleSlave: ->
|
||||
@role is "slave"
|
||||
|
||||
findNewSyncTarget: ()->
|
||||
@current_sync_target = null
|
||||
if @syncMethod is "syncAll"
|
||||
for user, c of @connections
|
||||
if not c.is_synced
|
||||
@performSync user
|
||||
break
|
||||
if not @current_sync_target?
|
||||
@setStateSynced()
|
||||
null
|
||||
|
||||
userLeft: (user)->
|
||||
delete @connections[user]
|
||||
@findNewSyncTarget()
|
||||
|
||||
userJoined: (user, role)->
|
||||
if not role?
|
||||
throw new Error "Internal: You must specify the role of the joined user! E.g. userJoined('uid:3939','slave')"
|
||||
# a user joined the room
|
||||
@connections[user] ?= {}
|
||||
@connections[user].is_synced = false
|
||||
|
||||
if (not @is_synced) or @syncMethod is "syncAll"
|
||||
if @syncMethod is "syncAll"
|
||||
@performSync user
|
||||
else if role is "master"
|
||||
# TODO: What if there are two masters? Prevent sending everything two times!
|
||||
@performSyncWithMaster user
|
||||
|
||||
|
||||
#
|
||||
# Execute a function _when_ we are connected. If not connected, wait until connected.
|
||||
# @param f {Function} Will be executed on the PeerJs-Connector context.
|
||||
#
|
||||
whenSynced: (args)->
|
||||
if args.constructore is Function
|
||||
args = [args]
|
||||
if @is_synced
|
||||
args[0].apply this, args[1..]
|
||||
else
|
||||
@compute_when_synced ?= []
|
||||
@compute_when_synced.push args
|
||||
|
||||
#
|
||||
# Execute an function when a message is received.
|
||||
# @param f {Function} Will be executed on the PeerJs-Connector context. f will be called with (sender_id, broadcast {true|false}, message).
|
||||
#
|
||||
onReceive: (f)->
|
||||
@receive_handlers.push f
|
||||
|
||||
###
|
||||
# Broadcast a message to all connected peers.
|
||||
# @param message {Object} The message to broadcast.
|
||||
#
|
||||
broadcast: (message)->
|
||||
throw new Error "You must implement broadcast!"
|
||||
|
||||
#
|
||||
# Send a message to a peer, or set of peers
|
||||
#
|
||||
send: (peer_s, message)->
|
||||
throw new Error "You must implement send!"
|
||||
###
|
||||
|
||||
#
|
||||
# perform a sync with a specific user.
|
||||
#
|
||||
performSync: (user)->
|
||||
if not @current_sync_target?
|
||||
@current_sync_target = user
|
||||
@send user,
|
||||
sync_step: "getHB"
|
||||
send_again: "true"
|
||||
data: [] # @getStateVector()
|
||||
if not @sent_hb_to_all_users
|
||||
@sent_hb_to_all_users = true
|
||||
|
||||
hb = @getHB([]).hb
|
||||
_hb = []
|
||||
for o in hb
|
||||
_hb.push o
|
||||
if _hb.length > 30
|
||||
@broadcast
|
||||
sync_step: "applyHB_"
|
||||
data: _hb
|
||||
_hb = []
|
||||
@broadcast
|
||||
sync_step: "applyHB"
|
||||
data: _hb
|
||||
|
||||
|
||||
|
||||
#
|
||||
# When a master node joined the room, perform this sync with him. It will ask the master for the HB,
|
||||
# and will broadcast his own HB
|
||||
#
|
||||
performSyncWithMaster: (user)->
|
||||
@current_sync_target = user
|
||||
@send user,
|
||||
sync_step: "getHB"
|
||||
send_again: "true"
|
||||
data: []
|
||||
hb = @getHB([]).hb
|
||||
_hb = []
|
||||
for o in hb
|
||||
_hb.push o
|
||||
if _hb.length > 30
|
||||
@broadcast
|
||||
sync_step: "applyHB_"
|
||||
data: _hb
|
||||
_hb = []
|
||||
@broadcast
|
||||
sync_step: "applyHB"
|
||||
data: _hb
|
||||
|
||||
#
|
||||
# You are sure that all clients are synced, call this function.
|
||||
#
|
||||
setStateSynced: ()->
|
||||
if not @is_synced
|
||||
@is_synced = true
|
||||
if @compute_when_synced?
|
||||
for f in @compute_when_synced
|
||||
f()
|
||||
delete @compute_when_synced
|
||||
null
|
||||
|
||||
#
|
||||
# You received a raw message, and you know that it is intended for to Yjs. Then call this function.
|
||||
#
|
||||
receiveMessage: (sender, res)->
|
||||
if not res.sync_step?
|
||||
for f in @receive_handlers
|
||||
f sender, res
|
||||
else
|
||||
if sender is @user_id
|
||||
return
|
||||
if res.sync_step is "getHB"
|
||||
data = @getHB(res.data)
|
||||
hb = data.hb
|
||||
_hb = []
|
||||
# always broadcast, when not synced.
|
||||
# This reduces errors, when the clients goes offline prematurely.
|
||||
# When this client only syncs to one other clients, but looses connectors,
|
||||
# before syncing to the other clients, the online clients have different states.
|
||||
# Since we do not want to perform regular syncs, this is a good alternative
|
||||
if @is_synced
|
||||
sendApplyHB = (m)=>
|
||||
@send sender, m
|
||||
else
|
||||
sendApplyHB = (m)=>
|
||||
@broadcast m
|
||||
|
||||
for o in hb
|
||||
_hb.push o
|
||||
if _hb.length > 30
|
||||
sendApplyHB
|
||||
sync_step: "applyHB_"
|
||||
data: _hb
|
||||
_hb = []
|
||||
|
||||
sendApplyHB
|
||||
sync_step : "applyHB"
|
||||
data: _hb
|
||||
|
||||
if res.send_again? and @perform_send_again
|
||||
send_again = do (sv = data.state_vector)=>
|
||||
()=>
|
||||
hb = @getHB(sv).hb
|
||||
@send sender,
|
||||
sync_step: "applyHB",
|
||||
data: hb
|
||||
sent_again: "true"
|
||||
setTimeout send_again, 3000
|
||||
else if res.sync_step is "applyHB"
|
||||
@applyHB(res.data, sender is @current_sync_target)
|
||||
|
||||
if (@syncMethod is "syncAll" or res.sent_again?) and (not @is_synced) and ((@current_sync_target is sender) or (not @current_sync_target?))
|
||||
@connections[sender].is_synced = true
|
||||
@findNewSyncTarget()
|
||||
|
||||
else if res.sync_step is "applyHB_"
|
||||
@applyHB(res.data, sender is @current_sync_target)
|
||||
|
||||
|
||||
# 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)->
|
||||
parse_array = (node)->
|
||||
for n in node.children
|
||||
if n.getAttribute("isArray") is "true"
|
||||
parse_array n
|
||||
else
|
||||
parse_object n
|
||||
|
||||
parse_object = (node)->
|
||||
json = {}
|
||||
for name, value of node.attrs
|
||||
int = parseInt(value)
|
||||
if isNaN(int) or (""+int) isnt value
|
||||
json[name] = value
|
||||
else
|
||||
json[name] = int
|
||||
for n in node.children
|
||||
name = n.name
|
||||
if n.getAttribute("isArray") is "true"
|
||||
json[name] = parse_array n
|
||||
else
|
||||
json[name] = parse_object n
|
||||
json
|
||||
parse_object 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 - guess it ;)
|
||||
#
|
||||
encodeMessageToXml: (m, json)->
|
||||
# attributes is optional
|
||||
encode_object = (m, json)->
|
||||
for name,value of json
|
||||
if not value?
|
||||
# nop
|
||||
else if value.constructor is Object
|
||||
encode_object m.c(name), value
|
||||
else if value.constructor is Array
|
||||
encode_array m.c(name), value
|
||||
else
|
||||
m.setAttribute(name,value)
|
||||
m
|
||||
encode_array = (m, array)->
|
||||
m.setAttribute("isArray","true")
|
||||
for e in array
|
||||
if e.constructor is Object
|
||||
encode_object m.c("array-element"), e
|
||||
else
|
||||
encode_array m.c("array-element"), e
|
||||
m
|
||||
if json.constructor is Object
|
||||
encode_object m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json
|
||||
else if json.constructor is Array
|
||||
encode_array m.c("y",{xmlns:"http://y.ninja/connector-stanza"}), json
|
||||
else
|
||||
throw new Error "I can't encode this json!"
|
||||
|
||||
setIsBoundToY: ()->
|
||||
@on_bound_to_y?()
|
||||
delete @when_bound_to_y
|
||||
@is_bound_to_y = true
|
||||
@@ -1,114 +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, fromHB = false)->
|
||||
if op_json_array.constructor isnt Array
|
||||
op_json_array = [op_json_array]
|
||||
for op_json in op_json_array
|
||||
if fromHB
|
||||
op_json.fromHB = "true" # execute immediately, if
|
||||
# $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,223 +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
|
||||
if o.uid.creator?
|
||||
o.uid.creator = id
|
||||
if o.uid.alt?
|
||||
o.uid.alt.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]
|
||||
if @operation_counter[@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 (not o.uid.noOperation?) and 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])) and 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,561 +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
|
||||
if @uid.alt? # could be (safely) undefined
|
||||
map_uid = @uid.alt.cloneUid()
|
||||
map_uid.sub = @uid.sub
|
||||
map_uid.doSync = false
|
||||
map_uid
|
||||
else
|
||||
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,356 +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 = @
|
||||
rm_uid =
|
||||
noOperation: true
|
||||
sub: property_name
|
||||
alt: @
|
||||
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() and o.prev_cl?
|
||||
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,527 +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'] = JSON.stringify @content
|
||||
json
|
||||
|
||||
types.TextInsert.parse = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
'parent' : parent
|
||||
} = json
|
||||
if typeof content is "string"
|
||||
content = JSON.parse(content)
|
||||
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)->
|
||||
range = {}
|
||||
s = dom_root.getSelection()
|
||||
clength = textfield.textContent.length
|
||||
range.left = Math.min s.anchorOffset, clength
|
||||
range.right = Math.min s.focusOffset, clength
|
||||
if fix?
|
||||
range.left = fix range.left
|
||||
range.right = fix range.right
|
||||
|
||||
edited_element = s.focusNode
|
||||
if edited_element is textfield or edited_element is textfield.childNodes[0]
|
||||
range.isReal = true
|
||||
else
|
||||
range.isReal = false
|
||||
range
|
||||
|
||||
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)->
|
||||
content_array = content.replace(new RegExp("\n",'g')," ").split(" ")
|
||||
textfield.innerText = ""
|
||||
for c, i in content_array
|
||||
textfield.innerText += c
|
||||
if i isnt content_array.length-1
|
||||
textfield.innerHTML += ' '
|
||||
|
||||
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.keyCode is 13
|
||||
char = '\n'
|
||||
else if event.key?
|
||||
if event.charCode is 32
|
||||
char = " "
|
||||
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.user_id?
|
||||
user_id = connector.user_id # TODO: change to getUniqueId()
|
||||
else
|
||||
user_id = "_temp"
|
||||
connector.on_user_id_set = (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
|
||||
5245
package-lock.json
generated
Normal file
5245
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@@ -1,66 +1,72 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "0.4.1",
|
||||
"description": "A Framework that enables Real-Time Collaboration on arbitrary data structures.",
|
||||
"main": "./build/node/y.js",
|
||||
"version": "13.0.0-29",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/gulp test"
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
||||
},
|
||||
"files": [
|
||||
"y.*"
|
||||
],
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/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-cli": "^6.24.1",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-regenerator": "^6.24.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"chance": "^1.0.9",
|
||||
"concurrently": "^3.4.0",
|
||||
"cutest": "^0.1.9",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-inject": "^2.0.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-uglify": "^1.0.2",
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"standard": "^10.0.2",
|
||||
"tag-dist-files": "^0.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^2.6.8",
|
||||
"fast-diff": "^1.1.2",
|
||||
"utf-8": "^1.0.0",
|
||||
"utf8": "^2.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
44
rollup.browser.js
Normal file
44
rollup.browser.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import uglify from 'rollup-plugin-uglify'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'src/Y.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
babel(),
|
||||
uglify({
|
||||
mangle: {
|
||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
||||
},
|
||||
output: {
|
||||
comments: function (node, comment) {
|
||||
var text = comment.value
|
||||
var type = comment.type
|
||||
if (type === 'comment2') {
|
||||
// multiline comment
|
||||
return /@license/i.test(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
dest: 'y.js',
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
26
rollup.node.js
Normal file
26
rollup.node.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'src/y-dist.cjs.js',
|
||||
moduleName: 'Y',
|
||||
format: 'cjs',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
dest: 'y.node.js',
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
20
rollup.test.js
Normal file
20
rollup.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import multiEntry from 'rollup-plugin-multi-entry'
|
||||
|
||||
export default {
|
||||
entry: 'test/y-xml.tests.js',
|
||||
moduleName: 'y-tests',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
],
|
||||
dest: 'y.test.js',
|
||||
sourceMap: true
|
||||
}
|
||||
120
src/Binary/Decoder.js
Normal file
120
src/Binary/Decoder.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import utf8 from 'utf-8'
|
||||
import ID from '../Util/ID.js'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
export default class BinaryDecoder {
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.uint8arr = new Uint8Array(buffer)
|
||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
||||
this.uint8arr = buffer
|
||||
} else {
|
||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||
}
|
||||
this.pos = 0
|
||||
}
|
||||
/**
|
||||
* Clone this decoder instance
|
||||
* Optionally set a new position parameter
|
||||
*/
|
||||
clone (newPos = this.pos) {
|
||||
let decoder = new BinaryDecoder(this.uint8arr)
|
||||
decoder.pos = newPos
|
||||
return decoder
|
||||
}
|
||||
/**
|
||||
* Number of bytes
|
||||
*/
|
||||
get length () {
|
||||
return this.uint8arr.length
|
||||
}
|
||||
/**
|
||||
* Skip one byte, jump to the next position
|
||||
*/
|
||||
skip8 () {
|
||||
this.pos++
|
||||
}
|
||||
/**
|
||||
* Read one byte as unsigned integer
|
||||
*/
|
||||
readUint8 () {
|
||||
return this.uint8arr[this.pos++]
|
||||
}
|
||||
/**
|
||||
* Read 4 bytes as unsigned integer
|
||||
*/
|
||||
readUint32 () {
|
||||
let uint =
|
||||
this.uint8arr[this.pos] +
|
||||
(this.uint8arr[this.pos + 1] << 8) +
|
||||
(this.uint8arr[this.pos + 2] << 16) +
|
||||
(this.uint8arr[this.pos + 3] << 24)
|
||||
this.pos += 4
|
||||
return uint
|
||||
}
|
||||
/**
|
||||
* Look ahead without incrementing position
|
||||
* to the next byte and read it as unsigned integer
|
||||
*/
|
||||
peekUint8 () {
|
||||
return this.uint8arr[this.pos]
|
||||
}
|
||||
/**
|
||||
* Read unsigned integer (32bit) with variable length
|
||||
* 1/8th of the storage is used as encoding overhead
|
||||
* - numbers < 2^7 is stored in one byte
|
||||
* - numbers < 2^14 is stored in two bytes
|
||||
* ..
|
||||
*/
|
||||
readVarUint () {
|
||||
let num = 0
|
||||
let len = 0
|
||||
while (true) {
|
||||
let r = this.uint8arr[this.pos++]
|
||||
num = num | ((r & 0b1111111) << len)
|
||||
len += 7
|
||||
if (r < 1 << 7) {
|
||||
return num >>> 0 // return unsigned number!
|
||||
}
|
||||
if (len > 35) {
|
||||
throw new Error('Integer out of range!')
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Read string of variable length
|
||||
* - varUint is used to store the length of the string
|
||||
*/
|
||||
readVarString () {
|
||||
let len = this.readVarUint()
|
||||
let bytes = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = this.uint8arr[this.pos++]
|
||||
}
|
||||
return utf8.getStringFromBytes(bytes)
|
||||
}
|
||||
/**
|
||||
* Look ahead and read varString without incrementing position
|
||||
*/
|
||||
peekVarString () {
|
||||
let pos = this.pos
|
||||
let s = this.readVarString()
|
||||
this.pos = pos
|
||||
return s
|
||||
}
|
||||
/**
|
||||
* Read ID
|
||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
||||
* - Otherwise an ID is returned
|
||||
*/
|
||||
readID () {
|
||||
let user = this.readVarUint()
|
||||
if (user === RootFakeUserID) {
|
||||
// read property name and type id
|
||||
const rid = new RootID(this.readVarString(), null)
|
||||
rid.type = this.readVarUint()
|
||||
return rid
|
||||
}
|
||||
return new ID(user, this.readVarUint())
|
||||
}
|
||||
}
|
||||
83
src/Binary/Encoder.js
Normal file
83
src/Binary/Encoder.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import utf8 from 'utf-8'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
export default class BinaryEncoder {
|
||||
constructor () {
|
||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
||||
this.data = []
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
get pos () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
createBuffer () {
|
||||
return Uint8Array.from(this.data).buffer
|
||||
}
|
||||
|
||||
writeUint8 (num) {
|
||||
this.data.push(num & bits8)
|
||||
}
|
||||
|
||||
setUint8 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
}
|
||||
|
||||
writeUint16 (num) {
|
||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
setUint16 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
this.data[pos + 1] = (num >>> 8) & bits8
|
||||
}
|
||||
|
||||
writeUint32 (num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data.push(num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
setUint32 (pos, num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data[pos + i] = num & bits8
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
writeVarUint (num) {
|
||||
while (num >= 0b10000000) {
|
||||
this.data.push(0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
this.data.push(bits7 & num)
|
||||
}
|
||||
|
||||
writeVarString (str) {
|
||||
let bytes = utf8.setBytesFromString(str)
|
||||
let len = bytes.length
|
||||
this.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
this.data.push(bytes[i])
|
||||
}
|
||||
}
|
||||
|
||||
writeID (id) {
|
||||
const user = id.user
|
||||
this.writeVarUint(user)
|
||||
if (user !== RootFakeUserID) {
|
||||
this.writeVarUint(id.clock)
|
||||
} else {
|
||||
this.writeVarString(id.name)
|
||||
this.writeVarUint(id.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/Connector.js
Normal file
294
src/Connector.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import BinaryEncoder from './Binary/Encoder.js'
|
||||
import BinaryDecoder from './Binary/Decoder.js'
|
||||
|
||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
||||
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
export default class AbstractConnector {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
this.opts = 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.log = debug('y:connector')
|
||||
this.logMessage = debug('y:connector-message')
|
||||
this._forwardAppliedStructs = opts.forwardAppliedOperations || false // TODO: rename
|
||||
this.role = opts.role
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.userEventListeners = []
|
||||
this.whenSyncedListeners = []
|
||||
this.currentSyncTarget = null
|
||||
this.debug = opts.debug === true
|
||||
this.broadcastBuffer = new BinaryEncoder()
|
||||
this.broadcastBufferSize = 0
|
||||
this.protocolVersion = 11
|
||||
this.authInfo = opts.auth || null
|
||||
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
||||
if (opts.maxBufferLength == null) {
|
||||
this.maxBufferLength = -1
|
||||
} else {
|
||||
this.maxBufferLength = opts.maxBufferLength
|
||||
}
|
||||
}
|
||||
|
||||
reconnect () {
|
||||
this.log('reconnecting..')
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
this.log('discronnecting..')
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.whenSyncedListeners = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
onUserEvent (f) {
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
|
||||
removeUserEventListener (f) {
|
||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||
}
|
||||
|
||||
userLeft (user) {
|
||||
if (this.connections.has(user)) {
|
||||
this.log('%s: User left %s', this.y.userID, user)
|
||||
this.connections.delete(user)
|
||||
// check if isSynced event can be sent now
|
||||
this._setSyncedWith(null)
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userLeft',
|
||||
user: user
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userJoined (user, role, auth) {
|
||||
if (role == null) {
|
||||
throw new Error('You must specify the role of the joined user!')
|
||||
}
|
||||
if (this.connections.has(user)) {
|
||||
throw new Error('This user already joined!')
|
||||
}
|
||||
this.log('%s: User joined %s', this.y.userID, user)
|
||||
this.connections.set(user, {
|
||||
uid: user,
|
||||
isSynced: false,
|
||||
role: role,
|
||||
processAfterAuth: [],
|
||||
processAfterSync: [],
|
||||
auth: auth || null,
|
||||
receivedSyncStep2: false
|
||||
})
|
||||
let defer = {}
|
||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
||||
this.connections.get(user).syncStep2 = defer
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userJoined',
|
||||
user: user,
|
||||
role: role
|
||||
})
|
||||
}
|
||||
this._syncWithUser(user)
|
||||
}
|
||||
|
||||
// Execute a function _when_ we are connected.
|
||||
// If not connected, wait until connected
|
||||
whenSynced (f) {
|
||||
if (this.isSynced) {
|
||||
f()
|
||||
} else {
|
||||
this.whenSyncedListeners.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
_syncWithUser (userID) {
|
||||
if (this.role === 'slave') {
|
||||
return // "The current sync has not finished or this is controlled by a master!"
|
||||
}
|
||||
sendSyncStep1(this, userID)
|
||||
}
|
||||
|
||||
_fireIsSyncedListeners () {
|
||||
if (!this.isSynced) {
|
||||
this.isSynced = true
|
||||
// It is safer to remove this!
|
||||
// call whensynced listeners
|
||||
for (var f of this.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = []
|
||||
this.y.emit('synced')
|
||||
}
|
||||
}
|
||||
|
||||
send (uid, buffer) {
|
||||
const y = this.y
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
||||
}
|
||||
this.log('User%s to User%s: Send \'%y\'', y.userID, uid, buffer)
|
||||
this.logMessage('User%s to User%s: Send %Y', y.userID, uid, [y, buffer])
|
||||
}
|
||||
|
||||
broadcast (buffer) {
|
||||
const y = this.y
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
||||
}
|
||||
this.log('User%s: Broadcast \'%y\'', y.userID, buffer)
|
||||
this.logMessage('User%s: Broadcast: %Y', y.userID, [y, buffer])
|
||||
}
|
||||
|
||||
/*
|
||||
Buffer operations, and broadcast them when ready.
|
||||
*/
|
||||
broadcastStruct (struct) {
|
||||
const firstContent = this.broadcastBuffer.length === 0
|
||||
if (firstContent) {
|
||||
this.broadcastBuffer.writeVarString(this.y.room)
|
||||
this.broadcastBuffer.writeVarString('update')
|
||||
this.broadcastBufferSize = 0
|
||||
this.broadcastBufferSizePos = this.broadcastBuffer.pos
|
||||
this.broadcastBuffer.writeUint32(0)
|
||||
}
|
||||
this.broadcastBufferSize++
|
||||
struct._toBinary(this.broadcastBuffer)
|
||||
if (this.maxBufferLength > 0 && this.broadcastBuffer.length > this.maxBufferLength) {
|
||||
// it is necessary to send the buffer now
|
||||
// cache the buffer and check if server is responsive
|
||||
const buffer = this.broadcastBuffer
|
||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
||||
this.broadcastBuffer = new BinaryEncoder()
|
||||
this.whenRemoteResponsive().then(() => {
|
||||
this.broadcast(buffer.createBuffer())
|
||||
})
|
||||
} else if (firstContent) {
|
||||
// send the buffer when all transactions are finished
|
||||
// (or buffer exceeds maxBufferLength)
|
||||
setTimeout(() => {
|
||||
if (this.broadcastBuffer.length > 0) {
|
||||
const buffer = this.broadcastBuffer
|
||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
||||
this.broadcast(buffer.createBuffer())
|
||||
this.broadcastBuffer = new BinaryEncoder()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Somehow check the responsiveness of the remote clients/server
|
||||
* Default behavior:
|
||||
* Wait 100ms before broadcasting the next batch of operations
|
||||
*
|
||||
* Only used when maxBufferLength is set
|
||||
*
|
||||
*/
|
||||
whenRemoteResponsive () {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||
*/
|
||||
receiveMessage (sender, buffer, skipAuth) {
|
||||
const y = this.y
|
||||
const userID = y.userID
|
||||
skipAuth = skipAuth || false
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
||||
}
|
||||
if (sender === userID) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
let encoder = new BinaryEncoder()
|
||||
let roomname = decoder.readVarString() // read room name
|
||||
encoder.writeVarString(roomname)
|
||||
let messageType = decoder.readVarString()
|
||||
let senderConn = this.connections.get(sender)
|
||||
this.log('User%s from User%s: Receive \'%s\'', userID, sender, messageType)
|
||||
this.logMessage('User%s from User%s: Receive %Y', userID, sender, [y, buffer])
|
||||
if (senderConn == null && !skipAuth) {
|
||||
throw new Error('Received message from unknown peer!')
|
||||
}
|
||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||
let auth = decoder.readVarUint()
|
||||
if (senderConn.auth == null) {
|
||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
||||
// check auth
|
||||
return this.checkAuth(auth, y, sender).then(authPermissions => {
|
||||
if (senderConn.auth == null) {
|
||||
senderConn.auth = authPermissions
|
||||
y.emit('userAuthenticated', {
|
||||
user: senderConn.uid,
|
||||
auth: authPermissions
|
||||
})
|
||||
}
|
||||
let messages = senderConn.processAfterAuth
|
||||
senderConn.processAfterAuth = []
|
||||
|
||||
messages.forEach(m =>
|
||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
if ((skipAuth || senderConn.auth != null) && (messageType !== 'update' || senderConn.isSynced)) {
|
||||
this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||
} else {
|
||||
senderConn.processAfterSync.push([messageType, senderConn, decoder, encoder, sender, false])
|
||||
}
|
||||
}
|
||||
|
||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||
readSyncStep1(decoder, encoder, this.y, senderConn, sender)
|
||||
} else {
|
||||
const y = this.y
|
||||
y.transact(function () {
|
||||
if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||
readSyncStep2(decoder, encoder, y, senderConn, sender)
|
||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
||||
integrateRemoteStructs(decoder, encoder, y, senderConn, sender)
|
||||
} else {
|
||||
throw new Error('Unable to receive message')
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
}
|
||||
|
||||
_setSyncedWith (user) {
|
||||
if (user != null) {
|
||||
const userConn = this.connections.get(user)
|
||||
userConn.isSynced = true
|
||||
const messages = userConn.processAfterSync
|
||||
userConn.processAfterSync = []
|
||||
messages.forEach(m => {
|
||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
||||
})
|
||||
}
|
||||
const conns = Array.from(this.connections.values())
|
||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||
this._fireIsSyncedListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/MessageHandler/deleteSet.js
Normal file
130
src/MessageHandler/deleteSet.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import { deleteItemRange } from '../Struct/Delete.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(' -' + user + ':')
|
||||
let dvLength = decoder.readVarUint()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
|
||||
}
|
||||
}
|
||||
return strBuilder
|
||||
}
|
||||
|
||||
export function writeDeleteSet (y, encoder) {
|
||||
let currentUser = null
|
||||
let currentLength
|
||||
let lastLenPos
|
||||
|
||||
let numberOfUsers = 0
|
||||
let laterDSLenPus = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
|
||||
y.ds.iterate(null, null, function (n) {
|
||||
var user = n._id.user
|
||||
var clock = n._id.clock
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
if (currentUser !== user) {
|
||||
numberOfUsers++
|
||||
// a new user was found
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoder.setUint32(lastLenPos, currentLength)
|
||||
}
|
||||
currentUser = user
|
||||
encoder.writeVarUint(user)
|
||||
// pseudo-fill pos
|
||||
lastLenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
currentLength = 0
|
||||
}
|
||||
encoder.writeVarUint(clock)
|
||||
encoder.writeVarUint(len)
|
||||
encoder.writeUint8(gc ? 1 : 0)
|
||||
currentLength++
|
||||
})
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoder.setUint32(lastLenPos, currentLength)
|
||||
}
|
||||
encoder.setUint32(laterDSLenPus, numberOfUsers)
|
||||
}
|
||||
|
||||
export function readDeleteSet (y, decoder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let dv = []
|
||||
let dvLength = decoder.readUint32()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
dv.push([from, len, gc])
|
||||
}
|
||||
if (dvLength > 0) {
|
||||
let pos = 0
|
||||
let d = dv[pos]
|
||||
let deletions = []
|
||||
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
|
||||
// cases:
|
||||
// 1. d deletes something to the right of n
|
||||
// => go to next n (break)
|
||||
// 2. d deletes something to the left of n
|
||||
// => create deletions
|
||||
// => reset d accordingly
|
||||
// *)=> if d doesn't delete anything anymore, go to next d (continue)
|
||||
// 3. not 2) and d deletes something that also n deletes
|
||||
// => reset d so that it doesn't contain n's deletion
|
||||
// *)=> if d does not delete anything anymore, go to next d (continue)
|
||||
while (d != null) {
|
||||
var diff = 0 // describe the diff of length in 1) and 2)
|
||||
if (n._id.clock + n.len <= d[0]) {
|
||||
// 1)
|
||||
break
|
||||
} else if (d[0] < n._id.clock) {
|
||||
// 2)
|
||||
// delete maximum the len of d
|
||||
// else delete as much as possible
|
||||
diff = Math.min(n._id.clock - d[0], d[1])
|
||||
// deleteItemRange(y, user, d[0], diff)
|
||||
deletions.push([user, d[0], diff])
|
||||
} else {
|
||||
// 3)
|
||||
diff = n._id.clock + n.len - d[0] // never null (see 1)
|
||||
if (d[2] && !n.gc) {
|
||||
// d marks as gc'd but n does not
|
||||
// then delete either way
|
||||
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
|
||||
deletions.push([user, d[0], Math.min(diff, d[1])])
|
||||
}
|
||||
}
|
||||
if (d[1] <= diff) {
|
||||
// d doesn't delete anything anymore
|
||||
d = dv[++pos]
|
||||
} else {
|
||||
d[0] = d[0] + diff // reset pos
|
||||
d[1] = d[1] - diff // reset length
|
||||
}
|
||||
}
|
||||
})
|
||||
// TODO: It would be more performant to apply the deletes in the above loop
|
||||
// Adapt the Tree implementation to support delete while iterating
|
||||
for (let i = deletions.length - 1; i >= 0; i--) {
|
||||
const del = deletions[i]
|
||||
deleteItemRange(y, del[0], del[1], del[2])
|
||||
}
|
||||
// for the rest.. just apply it
|
||||
for (; pos < dv.length; pos++) {
|
||||
d = dv[pos]
|
||||
deleteItemRange(y, user, d[0], d[1])
|
||||
// deletions.push([user, d[0], d[1], d[2]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/MessageHandler/integrateRemoteStructs.js
Normal file
100
src/MessageHandler/integrateRemoteStructs.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import BinaryDecoder from '../Binary/Decoder.js'
|
||||
import { logID } from './messageToString.js'
|
||||
|
||||
class MissingEntry {
|
||||
constructor (decoder, missing, struct) {
|
||||
this.decoder = decoder
|
||||
this.missing = missing.length
|
||||
this.struct = struct
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate remote struct
|
||||
* When a remote struct is integrated, other structs might be ready to ready to
|
||||
* integrate.
|
||||
*/
|
||||
function _integrateRemoteStructHelper (y, struct) {
|
||||
const id = struct._id
|
||||
if (id === undefined) {
|
||||
struct._integrate(y)
|
||||
} else {
|
||||
if (y.ss.getState(id.user) > id.clock) {
|
||||
return
|
||||
}
|
||||
struct._integrate(y)
|
||||
let msu = y._missingStructs.get(id.user)
|
||||
if (msu != null) {
|
||||
let clock = id.clock
|
||||
const finalClock = clock + struct._length
|
||||
for (;clock < finalClock; clock++) {
|
||||
const missingStructs = msu.get(clock)
|
||||
if (missingStructs !== undefined) {
|
||||
missingStructs.forEach(missingDef => {
|
||||
missingDef.missing--
|
||||
if (missingDef.missing === 0) {
|
||||
const decoder = missingDef.decoder
|
||||
let oldPos = decoder.pos
|
||||
let missing = missingDef.struct._fromBinary(y, decoder)
|
||||
decoder.pos = oldPos
|
||||
if (missing.length === 0) {
|
||||
y._readyToIntegrate.push(missingDef.struct)
|
||||
}
|
||||
}
|
||||
})
|
||||
msu.delete(clock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stringifyStructs (y, decoder, strBuilder) {
|
||||
const len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
let logMessage = ' ' + struct._logString()
|
||||
if (missing.length > 0) {
|
||||
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
|
||||
}
|
||||
strBuilder.push(logMessage)
|
||||
}
|
||||
}
|
||||
|
||||
export function integrateRemoteStructs (decoder, encoder, y) {
|
||||
const len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let decoderPos = decoder.pos
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
if (missing.length === 0) {
|
||||
while (struct != null) {
|
||||
_integrateRemoteStructHelper(y, struct)
|
||||
struct = y._readyToIntegrate.shift()
|
||||
}
|
||||
} else {
|
||||
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
||||
_decoder.pos = decoderPos
|
||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
||||
let missingStructs = y._missingStructs
|
||||
for (let i = missing.length - 1; i >= 0; i--) {
|
||||
let m = missing[i]
|
||||
if (!missingStructs.has(m.user)) {
|
||||
missingStructs.set(m.user, new Map())
|
||||
}
|
||||
let msu = missingStructs.get(m.user)
|
||||
if (!msu.has(m.clock)) {
|
||||
msu.set(m.clock, [])
|
||||
}
|
||||
let mArray = msu = msu.get(m.clock)
|
||||
mArray.push(missingEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/MessageHandler/messageToString.js
Normal file
48
src/MessageHandler/messageToString.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import BinaryDecoder from '../Binary/Decoder.js'
|
||||
import { stringifyStructs } from './integrateRemoteStructs.js'
|
||||
import { stringifySyncStep1 } from './syncStep1.js'
|
||||
import { stringifySyncStep2 } from './syncStep2.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import RootID from '../Util/RootID.js'
|
||||
import Y from '../Y.js'
|
||||
|
||||
export function messageToString ([y, buffer]) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // read roomname
|
||||
let type = decoder.readVarString()
|
||||
let strBuilder = []
|
||||
strBuilder.push('\n === ' + type + ' ===')
|
||||
if (type === 'update') {
|
||||
stringifyStructs(y, decoder, strBuilder)
|
||||
} else if (type === 'sync step 1') {
|
||||
stringifySyncStep1(y, decoder, strBuilder)
|
||||
} else if (type === 'sync step 2') {
|
||||
stringifySyncStep2(y, decoder, strBuilder)
|
||||
} else {
|
||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
||||
}
|
||||
return strBuilder.join('\n')
|
||||
}
|
||||
|
||||
export function messageToRoomname (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // roomname
|
||||
return decoder.readVarString() // messageType
|
||||
}
|
||||
|
||||
export function logID (id) {
|
||||
if (id !== null && id._id != null) {
|
||||
id = id._id
|
||||
}
|
||||
if (id === null) {
|
||||
return '()'
|
||||
} else if (id instanceof ID) {
|
||||
return `(${id.user},${id.clock})`
|
||||
} else if (id instanceof RootID) {
|
||||
return `(${id.name},${id.type})`
|
||||
} else if (id.constructor === Y) {
|
||||
return `y`
|
||||
} else {
|
||||
throw new Error('This is not a valid ID!')
|
||||
}
|
||||
}
|
||||
23
src/MessageHandler/stateSet.js
Normal file
23
src/MessageHandler/stateSet.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
export function readStateSet (decoder) {
|
||||
let ss = new Map()
|
||||
let ssLength = decoder.readUint32()
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ss.set(user, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
export function writeStateSet (y, encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0)
|
||||
for (let [user, clock] of y.ss.state) {
|
||||
encoder.writeVarUint(user)
|
||||
encoder.writeVarUint(clock)
|
||||
len++
|
||||
}
|
||||
encoder.setUint32(lenPosition, len)
|
||||
}
|
||||
70
src/MessageHandler/syncStep1.js
Normal file
70
src/MessageHandler/syncStep1.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import BinaryEncoder from '../Binary/Encoder.js'
|
||||
import { readStateSet, writeStateSet } from './stateSet.js'
|
||||
import { writeDeleteSet } from './deleteSet.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
||||
let auth = decoder.readVarString()
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
strBuilder.push(` - auth: "${auth}"`)
|
||||
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
|
||||
// write SS
|
||||
let ssBuilder = []
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ssBuilder.push(`(${user}:${clock})`)
|
||||
}
|
||||
strBuilder.push(' == SS: ' + ssBuilder.join(','))
|
||||
}
|
||||
|
||||
export function sendSyncStep1 (connector, syncUser) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(connector.y.room)
|
||||
encoder.writeVarString('sync step 1')
|
||||
encoder.writeVarString(connector.authInfo || '')
|
||||
encoder.writeVarUint(connector.protocolVersion)
|
||||
writeStateSet(connector.y, encoder)
|
||||
connector.send(syncUser, encoder.createBuffer())
|
||||
}
|
||||
|
||||
export default function writeStructs (encoder, decoder, y, ss) {
|
||||
const lenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
let len = 0
|
||||
for (let user of y.ss.state.keys()) {
|
||||
let clock = ss.get(user) || 0
|
||||
if (user !== RootFakeUserID) {
|
||||
y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) {
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
})
|
||||
}
|
||||
}
|
||||
encoder.setUint32(lenPos, len)
|
||||
}
|
||||
|
||||
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
// check protocol version
|
||||
if (protocolVersion !== y.connector.protocolVersion) {
|
||||
console.warn(
|
||||
`You tried to sync with a Yjs instance that has a different protocol version
|
||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
||||
`)
|
||||
y.destroy()
|
||||
}
|
||||
// write sync step 2
|
||||
encoder.writeVarString('sync step 2')
|
||||
encoder.writeVarString(y.connector.authInfo || '')
|
||||
const ss = readStateSet(decoder)
|
||||
writeStructs(encoder, decoder, y, ss)
|
||||
writeDeleteSet(y, encoder)
|
||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
if (y.connector.role === 'slave') {
|
||||
sendSyncStep1(y.connector, sender)
|
||||
}
|
||||
}
|
||||
28
src/MessageHandler/syncStep2.js
Normal file
28
src/MessageHandler/syncStep2.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||
import { readDeleteSet } from './deleteSet.js'
|
||||
|
||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
||||
strBuilder.push(' - auth: ' + decoder.readVarString())
|
||||
strBuilder.push(' == OS:')
|
||||
stringifyStructs(y, decoder, strBuilder)
|
||||
// write DS to string
|
||||
strBuilder.push(' == DS:')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(` User: ${user}: `)
|
||||
let len2 = decoder.readUint32()
|
||||
for (let j = 0; j < len2; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let to = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
||||
integrateRemoteStructs(decoder, encoder, y)
|
||||
readDeleteSet(y, decoder)
|
||||
y.connector._setSyncedWith(sender)
|
||||
}
|
||||
47
src/Persistence.js
Normal file
47
src/Persistence.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// import BinaryEncoder from './Binary/Encoder.js'
|
||||
|
||||
export default function extendPersistence (Y) {
|
||||
class AbstractPersistence {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
this.opts = opts
|
||||
this.saveOperationsBuffer = []
|
||||
this.log = Y.debug('y:persistence')
|
||||
}
|
||||
|
||||
saveToMessageQueue (binary) {
|
||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
||||
}
|
||||
|
||||
saveOperations (ops) {
|
||||
ops = ops.map(function (op) {
|
||||
return Y.Struct[op.struct].encode(op)
|
||||
})
|
||||
/*
|
||||
const saveOperations = () => {
|
||||
if (this.saveOperationsBuffer.length > 0) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(this.opts.room)
|
||||
encoder.writeVarString('update')
|
||||
let ops = this.saveOperationsBuffer
|
||||
this.saveOperationsBuffer = []
|
||||
let length = ops.length
|
||||
encoder.writeUint32(length)
|
||||
for (var i = 0; i < length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
this.saveToMessageQueue(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (this.saveOperationsBuffer.length === 0) {
|
||||
this.saveOperationsBuffer = ops
|
||||
} else {
|
||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Y.AbstractPersistence = AbstractPersistence
|
||||
}
|
||||
125
src/Store/DeleteStore.js
Normal file
125
src/Store/DeleteStore.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import Tree from '../Util/Tree.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
class DSNode {
|
||||
constructor (id, len, gc) {
|
||||
this._id = id
|
||||
this.len = len
|
||||
this.gc = gc
|
||||
}
|
||||
clone () {
|
||||
return new DSNode(this._id, this.len, this.gc)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DeleteStore extends Tree {
|
||||
logTable () {
|
||||
const deletes = []
|
||||
this.iterate(null, null, function (n) {
|
||||
deletes.push({
|
||||
user: n._id.user,
|
||||
clock: n._id.clock,
|
||||
len: n.len,
|
||||
gc: n.gc
|
||||
})
|
||||
})
|
||||
console.table(deletes)
|
||||
}
|
||||
isDeleted (id) {
|
||||
var n = this.findWithUpperBound(id)
|
||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
||||
}
|
||||
/*
|
||||
* Mark an operation as deleted. returns the deleted node
|
||||
*/
|
||||
markDeleted (id, length) {
|
||||
if (length == null) {
|
||||
throw new Error('length must be defined')
|
||||
}
|
||||
var n = this.findWithUpperBound(id)
|
||||
if (n != null && n._id.user === id.user) {
|
||||
if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
|
||||
// id is in n's range
|
||||
var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
|
||||
if (diff > 0) {
|
||||
// id+length overlaps n
|
||||
if (!n.gc) {
|
||||
n.len += diff
|
||||
} else {
|
||||
diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
|
||||
if (diff < length) {
|
||||
// a partial deletion
|
||||
let nId = id.clone()
|
||||
nId.clock += diff
|
||||
n = new DSNode(nId, length - diff, false)
|
||||
this.put(n)
|
||||
} else {
|
||||
// already gc'd
|
||||
throw new Error(
|
||||
'DS reached an inconsistent state. Please report this issue!'
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no overlapping, already deleted
|
||||
return n
|
||||
}
|
||||
} else {
|
||||
// cannot extend left (there is no left!)
|
||||
n = new DSNode(id, length, false)
|
||||
this.put(n) // TODO: you double-put !!
|
||||
}
|
||||
} else {
|
||||
// cannot extend left
|
||||
n = new DSNode(id, length, false)
|
||||
this.put(n)
|
||||
}
|
||||
// can extend right?
|
||||
var next = this.findNext(n._id)
|
||||
if (
|
||||
next != null &&
|
||||
n._id.user === next._id.user &&
|
||||
n._id.clock + n.len >= next._id.clock
|
||||
) {
|
||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||
while (diff >= 0) {
|
||||
// n overlaps with next
|
||||
if (next.gc) {
|
||||
// gc is stronger, so reduce length of n
|
||||
n.len -= diff
|
||||
if (diff >= next.len) {
|
||||
// delete the missing range after next
|
||||
diff = diff - next.len // missing range after next
|
||||
if (diff > 0) {
|
||||
this.put(n) // unneccessary? TODO!
|
||||
this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
// we can extend n with next
|
||||
if (diff > next.len) {
|
||||
// n is even longer than next
|
||||
// get next.next, and try to extend it
|
||||
var _next = this.findNext(next._id)
|
||||
this.delete(next._id)
|
||||
if (_next == null || n._id.user !== _next._id.user) {
|
||||
break
|
||||
} else {
|
||||
next = _next
|
||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||
// continue!
|
||||
}
|
||||
} else {
|
||||
// n just partially overlaps with next. extend n, delete next, and break this loop
|
||||
n.len += next.len - diff
|
||||
this.delete(next._id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.put(n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
85
src/Store/OperationStore.js
Normal file
85
src/Store/OperationStore.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import Tree from '../Util/Tree.js'
|
||||
import RootID from '../Util/RootID.js'
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class OperationStore extends Tree {
|
||||
constructor (y) {
|
||||
super()
|
||||
this.y = y
|
||||
}
|
||||
logTable () {
|
||||
const items = []
|
||||
this.iterate(null, null, function (item) {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||
left: logID(item._left === null ? null : item._left._lastId),
|
||||
right: logID(item._right),
|
||||
right_origin: logID(item._right_origin),
|
||||
parent: logID(item._parent),
|
||||
parentSub: item._parentSub,
|
||||
deleted: item._deleted,
|
||||
content: JSON.stringify(item._content)
|
||||
})
|
||||
})
|
||||
console.table(items)
|
||||
}
|
||||
get (id) {
|
||||
let struct = this.find(id)
|
||||
if (struct === null && id instanceof RootID) {
|
||||
const Constr = getStruct(id.type)
|
||||
const y = this.y
|
||||
struct = new Constr()
|
||||
struct._id = id
|
||||
struct._parent = y
|
||||
y.transact(() => {
|
||||
struct._integrate(y)
|
||||
})
|
||||
this.put(struct)
|
||||
}
|
||||
return struct
|
||||
}
|
||||
// Use getItem for structs with _length > 1
|
||||
getItem (id) {
|
||||
var item = this.findWithUpperBound(id)
|
||||
if (item === null) {
|
||||
return null
|
||||
}
|
||||
const itemID = item._id
|
||||
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
|
||||
return item
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// Return an insertion such that id is the first element of content
|
||||
// This function manipulates an item, if necessary
|
||||
getItemCleanStart (id) {
|
||||
var ins = this.getItem(id)
|
||||
if (ins === null || ins._length === 1) {
|
||||
return ins
|
||||
}
|
||||
const insID = ins._id
|
||||
if (insID.clock === id.clock) {
|
||||
return ins
|
||||
} else {
|
||||
return ins._splitAt(this.y, id.clock - insID.clock)
|
||||
}
|
||||
}
|
||||
// Return an insertion such that id is the last element of content
|
||||
// This function manipulates an operation, if necessary
|
||||
getItemCleanEnd (id) {
|
||||
var ins = this.getItem(id)
|
||||
if (ins === null || ins._length === 1) {
|
||||
return ins
|
||||
}
|
||||
const insID = ins._id
|
||||
if (insID.clock + ins._length - 1 === id.clock) {
|
||||
return ins
|
||||
} else {
|
||||
ins._splitAt(this.y, id.clock - insID.clock + 1)
|
||||
return ins
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Store/StateStore.js
Normal file
47
src/Store/StateStore.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
export default class StateStore {
|
||||
constructor (y) {
|
||||
this.y = y
|
||||
this.state = new Map()
|
||||
}
|
||||
logTable () {
|
||||
const entries = []
|
||||
for (let [user, state] of this.state) {
|
||||
entries.push({
|
||||
user, state
|
||||
})
|
||||
}
|
||||
console.table(entries)
|
||||
}
|
||||
getNextID (len) {
|
||||
const user = this.y.userID
|
||||
const state = this.getState(user)
|
||||
this.setState(user, state + len)
|
||||
return new ID(user, state)
|
||||
}
|
||||
updateRemoteState (struct) {
|
||||
let user = struct._id.user
|
||||
let userState = this.state.get(user)
|
||||
while (struct !== null && struct._id.clock === userState) {
|
||||
userState += struct._length
|
||||
struct = this.y.os.get(new ID(user, userState))
|
||||
}
|
||||
this.state.set(user, userState)
|
||||
}
|
||||
getState (user) {
|
||||
let state = this.state.get(user)
|
||||
if (state == null) {
|
||||
return 0
|
||||
}
|
||||
return state
|
||||
}
|
||||
setState (user, state) {
|
||||
// TODO: modify missingi structs here
|
||||
const beforeState = this.y._transaction.beforeState
|
||||
if (!beforeState.has(user)) {
|
||||
beforeState.set(user, this.getState(user))
|
||||
}
|
||||
this.state.set(user, state)
|
||||
}
|
||||
}
|
||||
84
src/Struct/Delete.js
Normal file
84
src/Struct/Delete.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import { getReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
/**
|
||||
* Delete all items in an ID-range
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||
*/
|
||||
export function deleteItemRange (y, user, clock, range) {
|
||||
const createDelete = y.connector._forwardAppliedStructs
|
||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||
if (item !== null) {
|
||||
if (!item._deleted) {
|
||||
item._splitAt(y, range)
|
||||
item._delete(y, createDelete)
|
||||
}
|
||||
let itemLen = item._length
|
||||
range -= itemLen
|
||||
clock += itemLen
|
||||
if (range > 0) {
|
||||
let node = y.os.findNode(new ID(user, clock))
|
||||
while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||
const nodeVal = node.val
|
||||
if (!nodeVal._deleted) {
|
||||
nodeVal._splitAt(y, range)
|
||||
nodeVal._delete(y, createDelete)
|
||||
}
|
||||
const nodeLen = nodeVal._length
|
||||
range -= nodeLen
|
||||
clock += nodeLen
|
||||
node = node.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete is not a real struct. It will not be saved in OS
|
||||
*/
|
||||
export default class Delete {
|
||||
constructor () {
|
||||
this._target = null
|
||||
this._length = null
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
const targetID = decoder.readID()
|
||||
this._targetID = targetID
|
||||
this._length = decoder.readVarUint()
|
||||
if (y.os.getItem(targetID) === null) {
|
||||
return [targetID]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
encoder.writeVarUint(this._length)
|
||||
}
|
||||
/**
|
||||
* - If created remotely (a remote user deleted something),
|
||||
* this Delete is applied to all structs in id-range.
|
||||
* - If created lokally (e.g. when y-array deletes a range of elements),
|
||||
* this struct is broadcasted only (it is already executed)
|
||||
*/
|
||||
_integrate (y, locallyCreated = false) {
|
||||
if (!locallyCreated) {
|
||||
// from remote
|
||||
const id = this._targetID
|
||||
deleteItemRange(y, id.user, id.clock, this._length)
|
||||
} else {
|
||||
// from local
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveOperations(this)
|
||||
}
|
||||
}
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
327
src/Struct/Item.js
Normal file
327
src/Struct/Item.js
Normal file
@@ -0,0 +1,327 @@
|
||||
import { getReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
import Delete from './Delete.js'
|
||||
import { transactionTypeChanged } from '../Transaction.js'
|
||||
|
||||
/**
|
||||
* Helper utility to split an Item (see _splitAt)
|
||||
* - copy all properties from a to b
|
||||
* - connect a to b
|
||||
* - assigns the correct _id
|
||||
* - save b to os
|
||||
*/
|
||||
export function splitHelper (y, a, b, diff) {
|
||||
const aID = a._id
|
||||
b._id = new ID(aID.user, aID.clock + diff)
|
||||
b._origin = a
|
||||
b._left = a
|
||||
b._right = a._right
|
||||
if (b._right !== null) {
|
||||
b._right._left = b
|
||||
}
|
||||
b._right_origin = a._right_origin
|
||||
// do not set a._right_origin, as this will lead to problems when syncing
|
||||
a._right = b
|
||||
b._parent = a._parent
|
||||
b._parentSub = a._parentSub
|
||||
b._deleted = a._deleted
|
||||
// now search all relevant items to the right and update origin
|
||||
// if origin is not it foundOrigins, we don't have to search any longer
|
||||
let foundOrigins = new Set()
|
||||
foundOrigins.add(a)
|
||||
let o = b._right
|
||||
while (o !== null && foundOrigins.has(o._origin)) {
|
||||
if (o._origin === a) {
|
||||
o._origin = b
|
||||
}
|
||||
foundOrigins.add(o)
|
||||
o = o._right
|
||||
}
|
||||
y.os.put(b)
|
||||
}
|
||||
|
||||
export default class Item {
|
||||
constructor () {
|
||||
this._id = null
|
||||
this._origin = null
|
||||
this._left = null
|
||||
this._right = null
|
||||
this._right_origin = null
|
||||
this._parent = null
|
||||
this._parentSub = null
|
||||
this._deleted = false
|
||||
}
|
||||
/**
|
||||
* Copy the effect of struct
|
||||
*/
|
||||
_copy () {
|
||||
let struct = new this.constructor()
|
||||
struct._origin = this._left
|
||||
struct._left = this._left
|
||||
struct._right = this
|
||||
struct._right_origin = this
|
||||
struct._parent = this._parent
|
||||
struct._parentSub = this._parentSub
|
||||
return struct
|
||||
}
|
||||
get _lastId () {
|
||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||
}
|
||||
get _length () {
|
||||
return 1
|
||||
}
|
||||
/**
|
||||
* Splits this struct so that another struct can be inserted in-between.
|
||||
* This must be overwritten if _length > 1
|
||||
* Returns right part after split
|
||||
* - diff === 0 => this
|
||||
* - diff === length => this._right
|
||||
* - otherwise => split _content and return right part of split
|
||||
* (see ItemJSON/ItemString for implementation)
|
||||
*/
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
}
|
||||
return this._right
|
||||
}
|
||||
_delete (y, createDelete = true) {
|
||||
this._deleted = true
|
||||
y.ds.markDeleted(this._id, this._length)
|
||||
if (createDelete) {
|
||||
let del = new Delete()
|
||||
del._targetID = this._id
|
||||
del._length = this._length
|
||||
del._integrate(y, true)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
y._transaction.deletedStructs.add(this)
|
||||
}
|
||||
/**
|
||||
* This is called right before this struct receives any children.
|
||||
* It can be overwritten to apply pending changes before applying remote changes
|
||||
*/
|
||||
_beforeChange () {
|
||||
// nop
|
||||
}
|
||||
/*
|
||||
* - Integrate the struct so that other types/structs can see it
|
||||
* - Add this struct to y.os
|
||||
* - Check if this is struct deleted
|
||||
*/
|
||||
_integrate (y) {
|
||||
const parent = this._parent
|
||||
const selfID = this._id
|
||||
const userState = selfID === null ? 0 : y.ss.getState(selfID.user)
|
||||
if (selfID === null) {
|
||||
this._id = y.ss.getNextID(this._length)
|
||||
} else if (selfID.user === RootFakeUserID) {
|
||||
// nop
|
||||
} else if (selfID.clock < userState) {
|
||||
// already applied..
|
||||
return []
|
||||
} else if (selfID.clock === userState) {
|
||||
y.ss.setState(selfID.user, userState + this._length)
|
||||
} else {
|
||||
// missing content from user
|
||||
throw new Error('Can not apply yet!')
|
||||
}
|
||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
||||
// this is the first time parent is updated
|
||||
// or this types is new
|
||||
this._parent._beforeChange()
|
||||
}
|
||||
/*
|
||||
# $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!)
|
||||
*/
|
||||
// handle conflicts
|
||||
let o
|
||||
// set o to the first conflicting item
|
||||
if (this._left !== null) {
|
||||
o = this._left._right
|
||||
} else if (this._parentSub !== null) {
|
||||
o = this._parent._map.get(this._parentSub) || null
|
||||
} else {
|
||||
o = this._parent._start
|
||||
}
|
||||
let conflictingItems = new Set()
|
||||
let itemsBeforeOrigin = new Set()
|
||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
||||
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
||||
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
||||
while (o !== null && o !== this._right) {
|
||||
itemsBeforeOrigin.add(o)
|
||||
conflictingItems.add(o)
|
||||
if (this._origin === o._origin) {
|
||||
// case 1
|
||||
if (o._id.user < this._id.user) {
|
||||
this._left = o
|
||||
conflictingItems.clear()
|
||||
}
|
||||
} else if (itemsBeforeOrigin.has(o._origin)) {
|
||||
// case 2
|
||||
if (!conflictingItems.has(o._origin)) {
|
||||
this._left = o
|
||||
conflictingItems.clear()
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
// TODO: try to use right_origin instead.
|
||||
// Then you could basically omit conflictingItems!
|
||||
// Note: you probably can't use right_origin in every case.. only when setting _left
|
||||
o = o._right
|
||||
}
|
||||
// reconnect left/right + update parent map/start if necessary
|
||||
const parentSub = this._parentSub
|
||||
if (this._left === null) {
|
||||
let right
|
||||
if (parentSub !== null) {
|
||||
const pmap = parent._map
|
||||
right = pmap.get(parentSub) || null
|
||||
pmap.set(parentSub, this)
|
||||
} else {
|
||||
right = parent._start
|
||||
parent._start = this
|
||||
}
|
||||
this._right = right
|
||||
if (right !== null) {
|
||||
right._left = this
|
||||
}
|
||||
} else {
|
||||
const left = this._left
|
||||
const right = left._right
|
||||
this._right = right
|
||||
left._right = this
|
||||
if (right !== null) {
|
||||
right._left = this
|
||||
}
|
||||
}
|
||||
if (parent._deleted) {
|
||||
this._delete(y, false)
|
||||
}
|
||||
y.os.put(this)
|
||||
transactionTypeChanged(y, parent, parentSub)
|
||||
if (this._id.user !== RootFakeUserID) {
|
||||
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveOperations(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
let info = 0
|
||||
if (this._origin !== null) {
|
||||
info += 0b1 // origin is defined
|
||||
}
|
||||
// TODO: remove
|
||||
/* no longer send _left
|
||||
if (this._left !== this._origin) {
|
||||
info += 0b10 // do not copy origin to left
|
||||
}
|
||||
*/
|
||||
if (this._right_origin !== null) {
|
||||
info += 0b100
|
||||
}
|
||||
if (this._parentSub !== null) {
|
||||
info += 0b1000
|
||||
}
|
||||
encoder.writeUint8(info)
|
||||
encoder.writeID(this._id)
|
||||
if (info & 0b1) {
|
||||
encoder.writeID(this._origin._lastId)
|
||||
}
|
||||
// TODO: remove
|
||||
/* see above
|
||||
if (info & 0b10) {
|
||||
encoder.writeID(this._left._lastId)
|
||||
}
|
||||
*/
|
||||
if (info & 0b100) {
|
||||
encoder.writeID(this._right_origin._id)
|
||||
}
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
encoder.writeID(this._parent._id)
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
||||
}
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = []
|
||||
const info = decoder.readUint8()
|
||||
const id = decoder.readID()
|
||||
this._id = id
|
||||
// read origin
|
||||
if (info & 0b1) {
|
||||
// origin != null
|
||||
const originID = decoder.readID()
|
||||
// we have to query for left again because it might have been split/merged..
|
||||
const origin = y.os.getItemCleanEnd(originID)
|
||||
if (origin === null) {
|
||||
missing.push(originID)
|
||||
} else {
|
||||
this._origin = origin
|
||||
this._left = this._origin
|
||||
}
|
||||
}
|
||||
// read right
|
||||
if (info & 0b100) {
|
||||
// right != null
|
||||
const rightID = decoder.readID()
|
||||
// we have to query for right again because it might have been split/merged..
|
||||
const right = y.os.getItemCleanStart(rightID)
|
||||
if (right === null) {
|
||||
missing.push(rightID)
|
||||
} else {
|
||||
this._right = right
|
||||
this._right_origin = right
|
||||
}
|
||||
}
|
||||
// read parent
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
const parentID = decoder.readID()
|
||||
// parent does not change, so we don't have to search for it again
|
||||
if (this._parent === null) {
|
||||
const parent = y.os.get(parentID)
|
||||
if (parent === null) {
|
||||
missing.push(parentID)
|
||||
} else {
|
||||
this._parent = parent
|
||||
}
|
||||
}
|
||||
} else if (this._parent === null) {
|
||||
if (this._origin !== null) {
|
||||
this._parent = this._origin._parent
|
||||
} else if (this._right_origin !== null) {
|
||||
this._parent = this._right_origin._parent
|
||||
}
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
||||
this._parentSub = JSON.parse(decoder.readVarString())
|
||||
}
|
||||
if (y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(new ID(id.user, id.clock - 1))
|
||||
}
|
||||
return missing
|
||||
}
|
||||
}
|
||||
64
src/Struct/ItemJSON.js
Normal file
64
src/Struct/ItemJSON.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { splitHelper, default as Item } from './Item.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class ItemJSON extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this._content = null
|
||||
}
|
||||
_copy () {
|
||||
let struct = super._copy()
|
||||
struct._content = this._content
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
let len = decoder.readVarUint()
|
||||
this._content = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ctnt = decoder.readVarString()
|
||||
let parsed
|
||||
if (ctnt === 'undefined') {
|
||||
parsed = undefined
|
||||
} else {
|
||||
parsed = JSON.parse(ctnt)
|
||||
}
|
||||
this._content[i] = parsed
|
||||
}
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
encoder.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let encoded
|
||||
let content = this._content[i]
|
||||
if (content === undefined) {
|
||||
encoded = 'undefined'
|
||||
} else {
|
||||
encoded = JSON.stringify(content)
|
||||
}
|
||||
encoder.writeVarString(encoded)
|
||||
}
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
} else if (diff >= this._length) {
|
||||
return this._right
|
||||
}
|
||||
let item = new ItemJSON()
|
||||
item._content = this._content.splice(diff)
|
||||
splitHelper(y, this, item, diff)
|
||||
return item
|
||||
}
|
||||
}
|
||||
43
src/Struct/ItemString.js
Normal file
43
src/Struct/ItemString.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { splitHelper, default as Item } from './Item.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class ItemString extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this._content = null
|
||||
}
|
||||
_copy () {
|
||||
let struct = super._copy()
|
||||
struct._content = this._content
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
this._content = decoder.readVarString()
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this._content)
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
} else if (diff >= this._length) {
|
||||
return this._right
|
||||
}
|
||||
let item = new ItemString()
|
||||
item._content = this._content.slice(diff)
|
||||
this._content = this._content.slice(0, diff)
|
||||
splitHelper(y, this, item, diff)
|
||||
return item
|
||||
}
|
||||
}
|
||||
172
src/Struct/Type.js
Normal file
172
src/Struct/Type.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import Item from './Item.js'
|
||||
import EventHandler from '../Util/EventHandler.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
// restructure children as if they were inserted one after another
|
||||
function integrateChildren (y, start) {
|
||||
let right
|
||||
do {
|
||||
right = start._right
|
||||
start._right = null
|
||||
start._right_origin = null
|
||||
start._origin = start._left
|
||||
start._integrate(y)
|
||||
start = right
|
||||
} while (right !== null)
|
||||
}
|
||||
|
||||
export function getListItemIDByPosition (type, i) {
|
||||
let pos = 0
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
if (pos <= i && i < pos + n._length) {
|
||||
const id = n._id
|
||||
return new ID(id.user, id.clock + i - pos)
|
||||
}
|
||||
pos++
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
|
||||
export default class Type extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this._map = new Map()
|
||||
this._start = null
|
||||
this._y = null
|
||||
this._eventHandler = new EventHandler()
|
||||
this._deepEventHandler = new EventHandler()
|
||||
}
|
||||
getPathTo (type) {
|
||||
if (type === this) {
|
||||
return []
|
||||
}
|
||||
const path = []
|
||||
const y = this._y
|
||||
while (type._parent !== this && this._parent !== y) {
|
||||
let parent = type._parent
|
||||
if (type._parentSub !== null) {
|
||||
path.push(type._parentSub)
|
||||
} else {
|
||||
// parent is array-ish
|
||||
for (let [i, child] of parent) {
|
||||
if (child === type) {
|
||||
path.push(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
type = parent
|
||||
}
|
||||
if (this._parent !== this) {
|
||||
throw new Error('The type is not a child of this node')
|
||||
}
|
||||
return path
|
||||
}
|
||||
_callEventHandler (event) {
|
||||
const changedParentTypes = this._y._transaction.changedParentTypes
|
||||
this._eventHandler.callEventListeners(event)
|
||||
let type = this
|
||||
while (type !== this._y) {
|
||||
let events = changedParentTypes.get(type)
|
||||
if (events === undefined) {
|
||||
events = []
|
||||
changedParentTypes.set(type, events)
|
||||
}
|
||||
events.push(event)
|
||||
type = type._parent
|
||||
}
|
||||
}
|
||||
_copy (undeleteChildren) {
|
||||
let copy = super._copy()
|
||||
let map = new Map()
|
||||
copy._map = map
|
||||
for (let [key, value] of this._map) {
|
||||
if (undeleteChildren.has(value) || !value.deleted) {
|
||||
let _item = value._copy(undeleteChildren)
|
||||
_item._parent = copy
|
||||
map.set(key, value._copy(undeleteChildren))
|
||||
}
|
||||
}
|
||||
let prevUndeleted = null
|
||||
copy._start = null
|
||||
let item = this._start
|
||||
while (item !== null) {
|
||||
if (undeleteChildren.has(item) || !item.deleted) {
|
||||
let _item = item._copy(undeleteChildren)
|
||||
_item._left = prevUndeleted
|
||||
_item._origin = prevUndeleted
|
||||
_item._right = null
|
||||
_item._right_origin = null
|
||||
_item._parent = copy
|
||||
if (prevUndeleted === null) {
|
||||
copy._start = _item
|
||||
} else {
|
||||
prevUndeleted._right = _item
|
||||
}
|
||||
prevUndeleted = _item
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
return copy
|
||||
}
|
||||
_transact (f) {
|
||||
const y = this._y
|
||||
if (y !== null) {
|
||||
y.transact(f)
|
||||
} else {
|
||||
f(y)
|
||||
}
|
||||
}
|
||||
observe (f) {
|
||||
this._eventHandler.addEventListener(f)
|
||||
}
|
||||
observeDeep (f) {
|
||||
this._deepEventHandler.addEventListener(f)
|
||||
}
|
||||
unobserve (f) {
|
||||
this._eventHandler.removeEventListener(f)
|
||||
}
|
||||
unobserveDeep (f) {
|
||||
this._deepEventHandler.removeEventListener(f)
|
||||
}
|
||||
_integrate (y) {
|
||||
y._transaction.newTypes.add(this)
|
||||
super._integrate(y)
|
||||
this._y = y
|
||||
// when integrating children we must make sure to
|
||||
// integrate start
|
||||
const start = this._start
|
||||
if (start !== null) {
|
||||
this._start = null
|
||||
integrateChildren(y, start)
|
||||
}
|
||||
// integrate map children
|
||||
const map = this._map
|
||||
this._map = new Map()
|
||||
for (let t of map.values()) {
|
||||
// TODO make sure that right elements are deleted!
|
||||
integrateChildren(y, t)
|
||||
}
|
||||
}
|
||||
_delete (y, createDelete) {
|
||||
super._delete(y, createDelete)
|
||||
y._transaction.changedTypes.delete(this)
|
||||
// delete map types
|
||||
for (let value of this._map.values()) {
|
||||
if (value instanceof Item && !value._deleted) {
|
||||
value._delete(y, false)
|
||||
}
|
||||
}
|
||||
// delete array types
|
||||
let t = this._start
|
||||
while (t !== null) {
|
||||
if (!t._deleted) {
|
||||
t._delete(y, false)
|
||||
}
|
||||
t = t._right
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user