Compare commits
665 Commits
0.5
...
ydb-integr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67bbc0a3fe | ||
|
|
e1ece6dc66 | ||
|
|
fe038822a3 | ||
|
|
dece14486c | ||
|
|
2daffbc2ca | ||
|
|
4c01a34d09 | ||
|
|
3b08267daa | ||
|
|
b98ebddb69 | ||
|
|
9d5bf50676 | ||
|
|
c0972f8158 | ||
|
|
548125a944 | ||
|
|
a7b124ca6e | ||
|
|
4022374620 | ||
|
|
860e4d7af6 | ||
|
|
6376d69b58 | ||
|
|
5cf6f45f19 | ||
|
|
967903673b | ||
|
|
2d897f1844 | ||
|
|
fb2f9bc493 | ||
|
|
6f9ae0c4fc | ||
|
|
9df20fac8a | ||
|
|
a1fb1a6258 | ||
|
|
417d0ef3b5 | ||
|
|
9be256231b | ||
|
|
c122bdc750 | ||
|
|
4ef36ab81c | ||
|
|
cccc0e1015 | ||
|
|
db5312443e | ||
|
|
dbda07424b | ||
|
|
684d38d6c8 | ||
|
|
44fa064eb2 | ||
|
|
9b6fffd880 | ||
|
|
e9993b2643 | ||
|
|
762e9e8a3a | ||
|
|
6ddeb788c7 | ||
|
|
b9245f323c | ||
|
|
c0e630b635 | ||
|
|
e56457a0ef | ||
|
|
ca13849828 | ||
|
|
92c2fbd6d3 | ||
|
|
65b8921f05 | ||
|
|
1ace7f4b73 | ||
|
|
6336064516 | ||
|
|
49d2e42b41 | ||
|
|
c098e8e745 | ||
|
|
38558a7fad | ||
|
|
bdb3782f8f | ||
|
|
bc32f7348e | ||
|
|
09a94f053e | ||
|
|
0df0079fa3 | ||
|
|
a54d826d6d | ||
|
|
99f92cb9a0 | ||
|
|
e788ad1333 | ||
|
|
1fe37c565e | ||
|
|
ed2273e2ed | ||
|
|
94933a704d | ||
|
|
ef6eb08335 | ||
|
|
d915c8dd13 | ||
|
|
32207cbca0 | ||
|
|
135c6d31be | ||
|
|
61149b458a | ||
|
|
ba97bfdd9e | ||
|
|
689bca8602 | ||
|
|
6dd43cde17 | ||
|
|
026675b438 | ||
|
|
941a22b257 | ||
|
|
4aa41b98a9 | ||
|
|
acf443aacb | ||
|
|
aa8c934833 | ||
|
|
814af5a3d7 | ||
|
|
bbc207aaa6 | ||
|
|
a9b610479d | ||
|
|
079de07eff | ||
|
|
54453e87fa | ||
|
|
1b0e3659c3 | ||
|
|
dc22a79ac4 | ||
|
|
384a4b72b0 | ||
|
|
f35c056bde | ||
|
|
250050e83b | ||
|
|
248d08be30 | ||
|
|
641f426339 | ||
|
|
fcbca65d8f | ||
|
|
5f8ae0dd43 | ||
|
|
de14fe0f3e | ||
|
|
5e4b071693 | ||
|
|
937de2c59f | ||
|
|
f1f1bff901 | ||
|
|
da748a78f4 | ||
|
|
4855b2d590 | ||
|
|
908ce31e2f | ||
|
|
e4d4c23f0b | ||
|
|
fc500a8247 | ||
|
|
4b84541d76 | ||
|
|
a3ab42c157 | ||
|
|
bbd3317d62 | ||
|
|
5d3922cb64 | ||
|
|
a81a2cd553 | ||
|
|
c0d24bdba4 | ||
|
|
40e913e9c5 | ||
|
|
94f6a0fd9c | ||
|
|
41a88dbc43 | ||
|
|
1d4f283955 | ||
|
|
fc3a4c376c | ||
|
|
acb0affa33 | ||
|
|
0b510b64a3 | ||
|
|
c8f0cf5556 | ||
|
|
11a4271fd1 | ||
|
|
c7670915c7 | ||
|
|
eb2d596538 | ||
|
|
48e17ea1a7 | ||
|
|
1a22fdd45e | ||
|
|
07cf0b3436 | ||
|
|
5a68b9f4ad | ||
|
|
445dd3e0da | ||
|
|
0ba97d78f8 | ||
|
|
fc5be5c7cc | ||
|
|
f2debc150c | ||
|
|
08f37a86e3 | ||
|
|
f5d17e6236 | ||
|
|
8f3bd7170a | ||
|
|
5586334549 | ||
|
|
24c1e4dcc8 | ||
|
|
d61bbecf4e | ||
|
|
85492ad2e0 | ||
|
|
02253f9a8d | ||
|
|
8105bef1af | ||
|
|
4efa16e2dd | ||
|
|
ad44f59def | ||
|
|
9c471ea24d | ||
|
|
d9e76014f5 | ||
|
|
4091b7d004 | ||
|
|
dfc183643d | ||
|
|
cf8698f2b6 | ||
|
|
3595f14da7 | ||
|
|
c6e671b1d5 | ||
|
|
e4c10fd6b3 | ||
|
|
e70aa09f88 | ||
|
|
7808b143da | ||
|
|
b35092928e | ||
|
|
b7dbcf69d3 | ||
|
|
377df18788 | ||
|
|
26a323733d | ||
|
|
d0d1015074 | ||
|
|
2e3240b379 | ||
|
|
2558652356 | ||
|
|
783cbd63fc | ||
|
|
41be80e751 | ||
|
|
3d6050d8a2 | ||
|
|
3d5ba7b4cc | ||
|
|
415b66607c | ||
|
|
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 |
10
.esdoc.json
Normal file
10
.esdoc.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"source": "./src",
|
||||
"destination": "./docs",
|
||||
"plugins": [{
|
||||
"name": "esdoc-standard-plugin",
|
||||
"option": {
|
||||
"accessor": {"access": ["public"], "autoPrivate": true}
|
||||
}
|
||||
}]
|
||||
}
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
/node_modules/
|
||||
node_modules
|
||||
bower_components
|
||||
.directory
|
||||
.c9
|
||||
.codio
|
||||
.settings
|
||||
docs
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
.vscode
|
||||
.yjsPersisted
|
||||
build
|
||||
11
.travis.yml
11
.travis.yml
@@ -1,11 +0,0 @@
|
||||
language: node_js
|
||||
before_install:
|
||||
- "npm install -g bower coffee-script"
|
||||
- "bower install"
|
||||
node_js:
|
||||
- "0.12"
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
@@ -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
|
||||
350
README.md
350
README.md
@@ -1,137 +1,305 @@
|
||||
|
||||
# 
|
||||
# 
|
||||
|
||||
[](https://travis-ci.org/y-js/yjs)
|
||||
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 handle concurrent actions on arbitrary complex data types like Text, Json, and XML. We provide a tutorial and some applications for this framework on our [homepage](http://y-js.org/).
|
||||
### 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
|
||||
|
||||
You can create you own data types easily. Therefore, you can take matters into your own hand by defining the meaning of the shared types and ensure that it is valid, while Yjs ensures data consistency (everyone will eventually end up with the same data). We already provide data types for
|
||||
Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
||||
is a list of the modules we know of:
|
||||
|
||||
| Name | Description
|
||||
| ---------------------------------------------------- | ---------------------------------------------
|
||||
y-object | Add, update, and remove properties of an object. Circular references are supported. Included in Yjs
|
||||
[y-list](https://github.com/y-js/y-list) | A shared linked list implementation. Circular references are supported
|
||||
[y-selections](https://github.com/y-js/y-selections) | Manages selections on types that use linear structures (e.g. the y-list type). You can select a range of elements and assign meaning to them.
|
||||
[y-xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects
|
||||
[y-text](https://github.com/y-js/y-text) | Collaborate on text. You can create a two way binding to textareas, input elements, or HTML elements (e.g. *h1*, or *p*)
|
||||
[y-richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. You can create a two way binding to several editors
|
||||
##### Connectors
|
||||
|
||||
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.
|
||||
|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|
|
||||
|
||||
We support several communication protocols as so called *Connectors*. You can create your own connector too - read [this wiki page](https://github.com/y-js/yjs/wiki/Custom-Connectors). Currently, we support the following communication protocols:
|
||||
##### Database adapters
|
||||
|
||||
Name | Description
|
||||
---------------------------------------- | -------------------------------------------------------
|
||||
[y-xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))
|
||||
[y-webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC
|
||||
[y-test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios
|
||||
|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 |
|
||||
|
||||
|
||||
You can use Yjs client-, and server- side. You can get it as via npm, and bower. We even provide polymer elements for Yjs!
|
||||
##### Types
|
||||
|
||||
The advantages over similar frameworks are support for
|
||||
* .. P2P message propagation and arbitrary communication protocols
|
||||
* .. arbitrary complex data types
|
||||
* .. offline editing: Only relevant changes are propagated on rejoin (unimplemented)
|
||||
* .. AnyUndo: Undo *any* action that was executed in constant time (unimplemented)
|
||||
* .. Intention Preservation: When working on Text, the intention of your changes are preserved. This is particularily important when working offline. Every type has a notion on how we define Intention Preservation on it.
|
||||
| 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 can find a tutorial, and examples on the [website](http://y-js.org). Furthermore, the [github wiki](https://github.com/y-js/yjs/wiki) offers more information about how you can use Yjs in your application.
|
||||
|
||||
Either clone this git repository, install it with [bower](http://bower.io/), or install it with [npm](https://www.npmjs.org/package/yjs).
|
||||
Install Yjs, and its modules with [bower](http://bower.io/), or
|
||||
[npm](https://www.npmjs.org/package/yjs).
|
||||
|
||||
### Bower
|
||||
```
|
||||
bower install y-js/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>
|
||||
```
|
||||
|
||||
### CDN
|
||||
```
|
||||
<script src="https://cdn.jsdelivr.net/npm/yjs@12/src/y.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
|
||||
// ..
|
||||
// do the same for all modules you want to use
|
||||
```
|
||||
|
||||
### 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-map')(Y)
|
||||
require('y-text')(Y)
|
||||
// ..
|
||||
// do the same for all modules you want to use
|
||||
```
|
||||
|
||||
# Y()
|
||||
In order to create an instance of Y, you need to have a connection object (instance of a Connector). Then, you can create a shared data type like this:
|
||||
### ES6 Syntax
|
||||
```
|
||||
var y = new Y(connector);
|
||||
import Y from 'yjs'
|
||||
import yArray from 'y-array'
|
||||
import yWebsocketsClient from 'y-webrtc'
|
||||
import yMemory from 'y-memory'
|
||||
import yMap from 'y-map'
|
||||
import yText from 'y-text'
|
||||
// ..
|
||||
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
|
||||
```
|
||||
|
||||
|
||||
# Y.Object
|
||||
Yjs includes only one type by default - the Y.Object type. It mimics the behaviour of a JSON Object. You can create, update, and remove properies on the Y.Object type. Furthermore, you can observe changes on this type as you can observe changes on Javascript Objects with [Object.observe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) - an ECMAScript 7 proposal which is likely to become accepted by the committee. Until then, we have our own implementation.
|
||||
|
||||
|
||||
##### Reference
|
||||
* Create
|
||||
# Text editing example
|
||||
Install dependencies
|
||||
```
|
||||
var y = new Y.Object();
|
||||
bower i yjs y-memory y-webrtc y-array y-text
|
||||
```
|
||||
* Create with existing Object
|
||||
|
||||
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
|
||||
|
||||
// Bind `y.share.textarea` to `<textarea/>`
|
||||
y.share.textarea.bind(document.querySelector('textarea'))
|
||||
})
|
||||
</script>
|
||||
<textarea></textarea>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
var y = new Y.Object({number: 73});
|
||||
|
||||
## 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:
|
||||
```
|
||||
* Every instance of Y is an Y.Object
|
||||
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)
|
||||
// ..
|
||||
```
|
||||
var y = new Y(connector);
|
||||
* 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.connect()
|
||||
* 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
|
||||
```
|
||||
* .val()
|
||||
* Retrieve all properties of this type as a JSON Object
|
||||
* .val(name)
|
||||
* Retrieve the value of a property
|
||||
* .val(name, value)
|
||||
* Set/update a property. Returns `this` Y.Object
|
||||
* .delete(name)
|
||||
* Delete a property
|
||||
* .observe(observer)
|
||||
* The `observer` is called whenever something on this object changes. Throws *add*, *update*, and *delete* events
|
||||
* .unobserve(f)
|
||||
* Delete an observer
|
||||
|
||||
# A note on intention preservation
|
||||
When users create/update/delete the same property concurrently, only one change will prevail. Changes on different properties do not conflict with each other.
|
||||
Remove the colors in order to log to a file:
|
||||
```sh
|
||||
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
||||
```
|
||||
|
||||
# A note on time complexities
|
||||
* .val()
|
||||
* O(|properties|)
|
||||
* .val(name)
|
||||
* O(1)
|
||||
* .val(name, value)
|
||||
* O(1)
|
||||
* .delete(name)
|
||||
* O(1)
|
||||
* Apply a delete operation from another user
|
||||
* O(1)
|
||||
* Apply an update operation from another user (set/update a property)
|
||||
* Yjs does not transform against operations that do not conflict with each other.
|
||||
* An operation conflicts with another operation if it changes the same property.
|
||||
* Overall worst case complexety: O(|conflicts|!)
|
||||
|
||||
# Status
|
||||
Yjs is a work in progress. Different versions of the *y-* repositories may not work together. Just drop me a line if you run into troubles.
|
||||
|
||||
## Get help
|
||||
[](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
Please report _any_ issues to the [Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very soon, if possible.
|
||||
##### 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).
|
||||
Yjs is licensed under the [MIT License](./LICENSE).
|
||||
|
||||
<yjs@dbis.rwth-aachen.de>
|
||||
|
||||
[ShareJs]: https://github.com/share/ShareJS
|
||||
[OpenCoweb]: https://github.com/opencoweb/coweb
|
||||
|
||||
33
bower.json
33
bower.json
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "0.5.2",
|
||||
"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"
|
||||
],
|
||||
"devDependencies": {
|
||||
"y-test" : "y-test#~0.4.0"
|
||||
}
|
||||
}
|
||||
@@ -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
2272
build/browser/y.js
2272
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") && (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 results, value;
|
||||
results = [];
|
||||
for (name in v) {
|
||||
value = v[name];
|
||||
results.push({
|
||||
user: name,
|
||||
state: value
|
||||
});
|
||||
}
|
||||
return results;
|
||||
};
|
||||
parse_state_vector = function(v) {
|
||||
var i, len, s, state_vector;
|
||||
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,415 +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;
|
||||
},
|
||||
onUserEvent: function(f) {
|
||||
if (this.connections_listeners == null) {
|
||||
this.connections_listeners = [];
|
||||
}
|
||||
return this.connections_listeners.push(f);
|
||||
},
|
||||
isRoleMaster: function() {
|
||||
return this.role === "master";
|
||||
},
|
||||
isRoleSlave: function() {
|
||||
return this.role === "slave";
|
||||
},
|
||||
findNewSyncTarget: function() {
|
||||
var c, ref, user;
|
||||
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) {
|
||||
var f, i, len, ref, results;
|
||||
delete this.connections[user];
|
||||
this.findNewSyncTarget();
|
||||
if (this.connections_listeners != null) {
|
||||
ref = this.connections_listeners;
|
||||
results = [];
|
||||
for (i = 0, len = ref.length; i < len; i++) {
|
||||
f = ref[i];
|
||||
results.push(f({
|
||||
action: "userLeft",
|
||||
user: user
|
||||
}));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
},
|
||||
userJoined: function(user, role) {
|
||||
var base, f, i, len, ref, results;
|
||||
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") {
|
||||
this.performSync(user);
|
||||
} else if (role === "master") {
|
||||
this.performSyncWithMaster(user);
|
||||
}
|
||||
}
|
||||
if (this.connections_listeners != null) {
|
||||
ref = this.connections_listeners;
|
||||
results = [];
|
||||
for (i = 0, len = ref.length; i < len; i++) {
|
||||
f = ref[i];
|
||||
results.push(f({
|
||||
action: "userJoined",
|
||||
user: user,
|
||||
role: role
|
||||
}));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
},
|
||||
whenSynced: function(args) {
|
||||
if (args.constructor === 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, hb, i, len, o;
|
||||
if (this.current_sync_target == null) {
|
||||
this.current_sync_target = user;
|
||||
this.send(user, {
|
||||
sync_step: "getHB",
|
||||
send_again: "true",
|
||||
data: this.getStateVector()
|
||||
});
|
||||
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 > 10) {
|
||||
this.broadcast({
|
||||
sync_step: "applyHB_",
|
||||
data: _hb
|
||||
});
|
||||
_hb = [];
|
||||
}
|
||||
}
|
||||
return this.broadcast({
|
||||
sync_step: "applyHB",
|
||||
data: _hb
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
performSyncWithMaster: function(user) {
|
||||
var _hb, hb, i, len, o;
|
||||
this.current_sync_target = user;
|
||||
this.send(user, {
|
||||
sync_step: "getHB",
|
||||
send_again: "true",
|
||||
data: this.getStateVector()
|
||||
});
|
||||
hb = this.getHB([]).hb;
|
||||
_hb = [];
|
||||
for (i = 0, len = hb.length; i < len; i++) {
|
||||
o = hb[i];
|
||||
_hb.push(o);
|
||||
if (_hb.length > 10) {
|
||||
this.broadcast({
|
||||
sync_step: "applyHB_",
|
||||
data: _hb
|
||||
});
|
||||
_hb = [];
|
||||
}
|
||||
}
|
||||
return this.broadcast({
|
||||
sync_step: "applyHB",
|
||||
data: _hb
|
||||
});
|
||||
},
|
||||
setStateSynced: function() {
|
||||
var args, el, 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++) {
|
||||
el = ref[i];
|
||||
f = el[0];
|
||||
args = el.slice(1);
|
||||
f.apply(args);
|
||||
}
|
||||
delete this.compute_when_synced;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
whenReceivedStateVector: function(f) {
|
||||
if (this.when_received_state_vector_listeners == null) {
|
||||
this.when_received_state_vector_listeners = [];
|
||||
}
|
||||
return this.when_received_state_vector_listeners.push(f);
|
||||
},
|
||||
receiveMessage: function(sender, res) {
|
||||
var _hb, data, f, hb, i, j, k, len, len1, len2, o, ref, ref1, results, sendApplyHB, send_again;
|
||||
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") {
|
||||
if (this.when_received_state_vector_listeners != null) {
|
||||
ref1 = this.when_received_state_vector_listeners;
|
||||
for (j = 0, len1 = ref1.length; j < len1; j++) {
|
||||
f = ref1[j];
|
||||
f.call(this, res.data);
|
||||
}
|
||||
}
|
||||
delete this.when_received_state_vector_listeners;
|
||||
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 (k = 0, len2 = hb.length; k < len2; k++) {
|
||||
o = hb[k];
|
||||
_hb.push(o);
|
||||
if (_hb.length > 10) {
|
||||
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() {
|
||||
var l, len3;
|
||||
hb = _this.getHB(sv).hb;
|
||||
for (l = 0, len3 = hb.length; l < len3; l++) {
|
||||
o = hb[l];
|
||||
_hb.push(o);
|
||||
if (_hb.length > 10) {
|
||||
_this.send(sender, {
|
||||
sync_step: "applyHB_",
|
||||
data: _hb
|
||||
});
|
||||
_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 i, len, n, 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 i, int, json, len, n, name, ref, ref1, value;
|
||||
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,120 +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 i, len, o, 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 i, len, o, op_json;
|
||||
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);
|
||||
o.parsed_from_json = 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 i, len, old_length, op, ref, unprocessed;
|
||||
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,255 +0,0 @@
|
||||
var HistoryBuffer,
|
||||
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
HistoryBuffer = (function() {
|
||||
function HistoryBuffer(user_id1) {
|
||||
this.user_id = user_id1;
|
||||
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.setUserId = function(user_id1, state_vector) {
|
||||
var base, buff, counter_diff, name, o, o_name, ref;
|
||||
this.user_id = user_id1;
|
||||
if ((base = this.buffer)[name = this.user_id] == null) {
|
||||
base[name] = [];
|
||||
}
|
||||
buff = this.buffer[this.user_id];
|
||||
counter_diff = state_vector[this.user_id] || 0;
|
||||
if (this.buffer._temp != null) {
|
||||
ref = this.buffer._temp;
|
||||
for (o_name in ref) {
|
||||
o = ref[o_name];
|
||||
o.uid.creator = this.user_id;
|
||||
o.uid.op_number += counter_diff;
|
||||
buff[o.uid.op_number] = o;
|
||||
}
|
||||
}
|
||||
this.operation_counter[this.user_id] = (this.operation_counter._temp || 0) + counter_diff;
|
||||
delete this.operation_counter._temp;
|
||||
return delete this.buffer._temp;
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.emptyGarbage = function() {
|
||||
var i, len, o, 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 i, len, o, 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++)
|
||||
};
|
||||
};
|
||||
|
||||
HistoryBuffer.prototype.getOperationCounter = function(user_id) {
|
||||
var ctn, ref, res, user;
|
||||
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, ref, u_name, unknown, user;
|
||||
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];
|
||||
if (u_name === "_") {
|
||||
continue;
|
||||
}
|
||||
for (o_number in user) {
|
||||
o = user[o_number];
|
||||
if ((o.uid.noOperation == null) && 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]
|
||||
};
|
||||
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 results, state, user;
|
||||
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) {
|
||||
var base, name;
|
||||
if ((base = this.operation_counter)[name = o.uid.creator] == null) {
|
||||
base[name] = 0;
|
||||
}
|
||||
if (o.uid.op_number === this.operation_counter[o.uid.creator]) {
|
||||
this.operation_counter[o.uid.creator]++;
|
||||
}
|
||||
while (this.buffer[o.uid.creator][this.operation_counter[o.uid.creator]] != null) {
|
||||
this.operation_counter[o.uid.creator]++;
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
return HistoryBuffer;
|
||||
|
||||
})();
|
||||
|
||||
module.exports = HistoryBuffer;
|
||||
@@ -1,91 +0,0 @@
|
||||
var YObject;
|
||||
|
||||
YObject = (function() {
|
||||
function YObject(_object) {
|
||||
var name, ref, val;
|
||||
this._object = _object != null ? _object : {};
|
||||
if (this._object.constructor === Object) {
|
||||
ref = this._object;
|
||||
for (name in ref) {
|
||||
val = ref[name];
|
||||
if (val.constructor === Object) {
|
||||
this._object[name] = new YObject(val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("Y.Object accepts Json Objects only");
|
||||
}
|
||||
}
|
||||
|
||||
YObject.prototype._name = "Object";
|
||||
|
||||
YObject.prototype._getModel = function(types, ops) {
|
||||
var n, o, ref;
|
||||
if (this._model == null) {
|
||||
this._model = new ops.MapManager(this).execute();
|
||||
ref = this._object;
|
||||
for (n in ref) {
|
||||
o = ref[n];
|
||||
this._model.val(n, o);
|
||||
}
|
||||
}
|
||||
delete this._object;
|
||||
return this._model;
|
||||
};
|
||||
|
||||
YObject.prototype._setModel = function(_model) {
|
||||
this._model = _model;
|
||||
return delete this._object;
|
||||
};
|
||||
|
||||
YObject.prototype.observe = function(f) {
|
||||
this._model.observe(f);
|
||||
return this;
|
||||
};
|
||||
|
||||
YObject.prototype.unobserve = function(f) {
|
||||
this._model.unobserve(f);
|
||||
return this;
|
||||
};
|
||||
|
||||
YObject.prototype.val = function(name, content) {
|
||||
var n, ref, res, v;
|
||||
if (this._model != null) {
|
||||
return this._model.val.apply(this._model, arguments);
|
||||
} else {
|
||||
if (content != null) {
|
||||
return this._object[name] = content;
|
||||
} else if (name != null) {
|
||||
return this._object[name];
|
||||
} else {
|
||||
res = {};
|
||||
ref = this._object;
|
||||
for (n in ref) {
|
||||
v = ref[n];
|
||||
res[n] = v;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
YObject.prototype["delete"] = function(name) {
|
||||
this._model["delete"](name);
|
||||
return this;
|
||||
};
|
||||
|
||||
return YObject;
|
||||
|
||||
})();
|
||||
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
if (window.Y != null) {
|
||||
window.Y.Object = YObject;
|
||||
} else {
|
||||
throw new Error("You must first import Y!");
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined" && module !== null) {
|
||||
module.exports = YObject;
|
||||
}
|
||||
@@ -1,670 +0,0 @@
|
||||
var slice = [].slice,
|
||||
extend = 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; },
|
||||
hasProp = {}.hasOwnProperty;
|
||||
|
||||
module.exports = function() {
|
||||
var execution_listener, ops;
|
||||
ops = {};
|
||||
execution_listener = [];
|
||||
ops.Operation = (function() {
|
||||
function Operation(custom_type, uid, content, content_operations) {
|
||||
var name, op;
|
||||
if (custom_type != null) {
|
||||
this.custom_type = custom_type;
|
||||
}
|
||||
this.is_deleted = false;
|
||||
this.garbage_collected = false;
|
||||
this.event_listeners = [];
|
||||
if (uid != null) {
|
||||
this.uid = uid;
|
||||
}
|
||||
if (content === void 0) {
|
||||
|
||||
} else if ((content != null) && (content.creator != null)) {
|
||||
this.saveOperation('content', content);
|
||||
} else {
|
||||
this.content = content;
|
||||
}
|
||||
if (content_operations != null) {
|
||||
this.content_operations = {};
|
||||
for (name in content_operations) {
|
||||
op = content_operations[name];
|
||||
this.saveOperation(name, op, 'content_operations');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Operation.prototype.type = "Operation";
|
||||
|
||||
Operation.prototype.getContent = function(name) {
|
||||
var content, n, ref, ref1, v;
|
||||
if (this.content != null) {
|
||||
if (this.content.getCustomType != null) {
|
||||
return this.content.getCustomType();
|
||||
} else if (this.content.constructor === Object) {
|
||||
if (name != null) {
|
||||
if (this.content[name] != null) {
|
||||
return this.content[name];
|
||||
} else {
|
||||
return this.content_operations[name].getCustomType();
|
||||
}
|
||||
} else {
|
||||
content = {};
|
||||
ref = this.content;
|
||||
for (n in ref) {
|
||||
v = ref[n];
|
||||
content[n] = v;
|
||||
}
|
||||
if (this.content_operations != null) {
|
||||
ref1 = this.content_operations;
|
||||
for (n in ref1) {
|
||||
v = ref1[n];
|
||||
v = v.getCustomType();
|
||||
content[n] = v;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
} else {
|
||||
return this.content;
|
||||
}
|
||||
} else {
|
||||
return this.content;
|
||||
}
|
||||
};
|
||||
|
||||
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 ops.Delete(void 0, this)).execute();
|
||||
return null;
|
||||
};
|
||||
|
||||
Operation.prototype.callEvent = function() {
|
||||
var callon;
|
||||
if (this.custom_type != null) {
|
||||
callon = this.getCustomType();
|
||||
} else {
|
||||
callon = this;
|
||||
}
|
||||
return this.forwardEvent.apply(this, [callon].concat(slice.call(arguments)));
|
||||
};
|
||||
|
||||
Operation.prototype.forwardEvent = function() {
|
||||
var args, f, j, len, op, ref, results;
|
||||
op = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
|
||||
ref = this.event_listeners;
|
||||
results = [];
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
f = ref[j];
|
||||
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 this.HB.addToGarbageCollector(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.cleanup = function() {
|
||||
this.HB.removeOperation(this);
|
||||
return this.deleteAllObservers();
|
||||
};
|
||||
|
||||
Operation.prototype.setParent = function(parent1) {
|
||||
this.parent = parent1;
|
||||
};
|
||||
|
||||
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;
|
||||
return map_uid;
|
||||
} else {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.cloneUid = function() {
|
||||
var n, ref, uid, v;
|
||||
uid = {};
|
||||
ref = this.getUid();
|
||||
for (n in ref) {
|
||||
v = ref[n];
|
||||
uid[n] = v;
|
||||
}
|
||||
return uid;
|
||||
};
|
||||
|
||||
Operation.prototype.execute = function() {
|
||||
var j, l, len;
|
||||
if (this.validateSavedOperations()) {
|
||||
this.is_executed = true;
|
||||
if (this.uid == null) {
|
||||
this.uid = this.HB.getNextOperationIdentifier();
|
||||
}
|
||||
if (this.uid.noOperation == null) {
|
||||
this.HB.addOperation(this);
|
||||
for (j = 0, len = execution_listener.length; j < len; j++) {
|
||||
l = execution_listener[j];
|
||||
l(this._encode());
|
||||
}
|
||||
}
|
||||
return this;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.saveOperation = function(name, op, base) {
|
||||
var base1, dest, j, last_path, len, path, paths;
|
||||
if (base == null) {
|
||||
base = "this";
|
||||
}
|
||||
if ((op != null) && (op._getModel != null)) {
|
||||
op = op._getModel(this.custom_types, this.operations);
|
||||
}
|
||||
if (op == null) {
|
||||
|
||||
} else if ((op.execute != null) || !((op.op_number != null) && (op.creator != null))) {
|
||||
if (base === "this") {
|
||||
return this[name] = op;
|
||||
} else {
|
||||
dest = this[base];
|
||||
paths = name.split("/");
|
||||
last_path = paths.pop();
|
||||
for (j = 0, len = paths.length; j < len; j++) {
|
||||
path = paths[j];
|
||||
dest = dest[path];
|
||||
}
|
||||
return dest[last_path] = op;
|
||||
}
|
||||
} else {
|
||||
if (this.unchecked == null) {
|
||||
this.unchecked = {};
|
||||
}
|
||||
if ((base1 = this.unchecked)[base] == null) {
|
||||
base1[base] = {};
|
||||
}
|
||||
return this.unchecked[base][name] = op;
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.validateSavedOperations = function() {
|
||||
var base, base_name, dest, j, last_path, len, name, op, op_uid, path, paths, ref, success, uninstantiated;
|
||||
uninstantiated = {};
|
||||
success = true;
|
||||
ref = this.unchecked;
|
||||
for (base_name in ref) {
|
||||
base = ref[base_name];
|
||||
for (name in base) {
|
||||
op_uid = base[name];
|
||||
op = this.HB.getOperation(op_uid);
|
||||
if (op) {
|
||||
if (base_name === "this") {
|
||||
this[name] = op;
|
||||
} else {
|
||||
dest = this[base_name];
|
||||
paths = name.split("/");
|
||||
last_path = paths.pop();
|
||||
for (j = 0, len = paths.length; j < len; j++) {
|
||||
path = paths[j];
|
||||
dest = dest[path];
|
||||
}
|
||||
dest[last_path] = op;
|
||||
}
|
||||
} else {
|
||||
if (uninstantiated[base_name] == null) {
|
||||
uninstantiated[base_name] = {};
|
||||
}
|
||||
uninstantiated[base_name][name] = op_uid;
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
this.unchecked = uninstantiated;
|
||||
return false;
|
||||
} else {
|
||||
delete this.unchecked;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype.getCustomType = function() {
|
||||
var Type, j, len, ref, t;
|
||||
if (this.custom_type == null) {
|
||||
return this;
|
||||
} else {
|
||||
if (this.custom_type.constructor === String) {
|
||||
Type = this.custom_types;
|
||||
ref = this.custom_type.split(".");
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
t = ref[j];
|
||||
Type = Type[t];
|
||||
}
|
||||
this.custom_type = new Type();
|
||||
this.custom_type._setModel(this);
|
||||
}
|
||||
return this.custom_type;
|
||||
}
|
||||
};
|
||||
|
||||
Operation.prototype._encode = function(json) {
|
||||
var n, o, operations, ref, ref1;
|
||||
if (json == null) {
|
||||
json = {};
|
||||
}
|
||||
json.type = this.type;
|
||||
json.uid = this.getUid();
|
||||
if (this.custom_type != null) {
|
||||
if (this.custom_type.constructor === String) {
|
||||
json.custom_type = this.custom_type;
|
||||
} else {
|
||||
json.custom_type = this.custom_type._name;
|
||||
}
|
||||
}
|
||||
if (((ref = this.content) != null ? ref.getUid : void 0) != null) {
|
||||
json.content = this.content.getUid();
|
||||
} else {
|
||||
json.content = this.content;
|
||||
}
|
||||
if (this.content_operations != null) {
|
||||
operations = {};
|
||||
ref1 = this.content_operations;
|
||||
for (n in ref1) {
|
||||
o = ref1[n];
|
||||
if (o._getModel != null) {
|
||||
o = o._getModel(this.custom_types, this.operations);
|
||||
}
|
||||
operations[n] = o.getUid();
|
||||
}
|
||||
json.content_operations = operations;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
return Operation;
|
||||
|
||||
})();
|
||||
ops.Delete = (function(superClass) {
|
||||
extend(Delete, superClass);
|
||||
|
||||
function Delete(custom_type, uid, deletes) {
|
||||
this.saveOperation('deletes', deletes);
|
||||
Delete.__super__.constructor.call(this, custom_type, 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;
|
||||
|
||||
})(ops.Operation);
|
||||
ops.Delete.parse = function(o) {
|
||||
var deletes_uid, uid;
|
||||
uid = o['uid'], deletes_uid = o['deletes'];
|
||||
return new this(null, uid, deletes_uid);
|
||||
};
|
||||
ops.Insert = (function(superClass) {
|
||||
extend(Insert, superClass);
|
||||
|
||||
function Insert(custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin) {
|
||||
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, custom_type, uid, content, content_operations);
|
||||
}
|
||||
|
||||
Insert.prototype.type = "Insert";
|
||||
|
||||
Insert.prototype.val = function() {
|
||||
return this.getContent();
|
||||
};
|
||||
|
||||
Insert.prototype.getNext = function(i) {
|
||||
var n;
|
||||
if (i == null) {
|
||||
i = 1;
|
||||
}
|
||||
n = this;
|
||||
while (i > 0 && (n.next_cl != null)) {
|
||||
n = n.next_cl;
|
||||
if (!n.is_deleted) {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if (n.is_deleted) {
|
||||
null;
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
Insert.prototype.getPrev = function(i) {
|
||||
var n;
|
||||
if (i == null) {
|
||||
i = 1;
|
||||
}
|
||||
n = this;
|
||||
while (i > 0 && (n.prev_cl != null)) {
|
||||
n = n.prev_cl;
|
||||
if (!n.is_deleted) {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if (n.is_deleted) {
|
||||
return null;
|
||||
} else {
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
Insert.prototype.applyDelete = function(o) {
|
||||
var callLater, garbagecollect;
|
||||
if (this.deleted_by == null) {
|
||||
this.deleted_by = [];
|
||||
}
|
||||
callLater = false;
|
||||
if ((this.parent != null) && !this.is_deleted && (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.parent.callOperationSpecificDeleteEvents(this, o);
|
||||
}
|
||||
if ((this.prev_cl != null) && this.prev_cl.isDeleted()) {
|
||||
return this.prev_cl.applyDelete();
|
||||
}
|
||||
};
|
||||
|
||||
Insert.prototype.cleanup = function() {
|
||||
var d, j, len, o, ref;
|
||||
if (this.next_cl.isDeleted()) {
|
||||
ref = this.deleted_by;
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
d = ref[j];
|
||||
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;
|
||||
if (this.content instanceof ops.Operation && !(this.content instanceof ops.Insert)) {
|
||||
this.content.referenced_by--;
|
||||
if (this.content.referenced_by <= 0 && !this.content.is_deleted) {
|
||||
this.content.applyDelete();
|
||||
}
|
||||
}
|
||||
delete this.content;
|
||||
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 base1, distance_to_origin, i, o;
|
||||
if (!this.validateSavedOperations()) {
|
||||
return false;
|
||||
} else {
|
||||
if (this.content instanceof ops.Operation) {
|
||||
this.content.insert_parent = this;
|
||||
if ((base1 = this.content).referenced_by == null) {
|
||||
base1.referenced_by = 0;
|
||||
}
|
||||
this.content.referenced_by++;
|
||||
}
|
||||
if (this.parent != null) {
|
||||
if (this.prev_cl == null) {
|
||||
this.prev_cl = this.parent.beginning;
|
||||
}
|
||||
if (this.origin == null) {
|
||||
this.origin = this.prev_cl;
|
||||
} else if (this.origin === "Delimiter") {
|
||||
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.parent.callOperationSpecificInsertEvents(this);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
Insert.prototype.getPosition = function() {
|
||||
var position, prev;
|
||||
position = 0;
|
||||
prev = this.prev_cl;
|
||||
while (true) {
|
||||
if (prev instanceof ops.Delimiter) {
|
||||
break;
|
||||
}
|
||||
if (!prev.isDeleted()) {
|
||||
position++;
|
||||
}
|
||||
prev = prev.prev_cl;
|
||||
}
|
||||
return position;
|
||||
};
|
||||
|
||||
Insert.prototype._encode = function(json) {
|
||||
if (json == null) {
|
||||
json = {};
|
||||
}
|
||||
json.prev = this.prev_cl.getUid();
|
||||
json.next = this.next_cl.getUid();
|
||||
if (this.origin.type === "Delimiter") {
|
||||
json.origin = "Delimiter";
|
||||
} else if (this.origin !== this.prev_cl) {
|
||||
json.origin = this.origin.getUid();
|
||||
}
|
||||
json.parent = this.parent.getUid();
|
||||
return Insert.__super__._encode.call(this, json);
|
||||
};
|
||||
|
||||
return Insert;
|
||||
|
||||
})(ops.Operation);
|
||||
ops.Insert.parse = function(json) {
|
||||
var content, content_operations, next, origin, parent, prev, uid;
|
||||
content = json['content'], content_operations = json['content_operations'], uid = json['uid'], prev = json['prev'], next = json['next'], origin = json['origin'], parent = json['parent'];
|
||||
return new this(null, content, content_operations, parent, uid, prev, next, origin);
|
||||
};
|
||||
ops.Delimiter = (function(superClass) {
|
||||
extend(Delimiter, superClass);
|
||||
|
||||
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, null, {
|
||||
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;
|
||||
|
||||
})(ops.Operation);
|
||||
ops.Delimiter.parse = function(json) {
|
||||
var next, prev, uid;
|
||||
uid = json['uid'], prev = json['prev'], next = json['next'];
|
||||
return new this(uid, prev, next);
|
||||
};
|
||||
return {
|
||||
'operations': ops,
|
||||
'execution_listener': execution_listener
|
||||
};
|
||||
};
|
||||
@@ -1,579 +0,0 @@
|
||||
var basic_ops_uninitialized,
|
||||
extend = 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; },
|
||||
hasProp = {}.hasOwnProperty;
|
||||
|
||||
basic_ops_uninitialized = require("./Basic");
|
||||
|
||||
module.exports = function() {
|
||||
var basic_ops, ops;
|
||||
basic_ops = basic_ops_uninitialized();
|
||||
ops = basic_ops.operations;
|
||||
ops.MapManager = (function(superClass) {
|
||||
extend(MapManager, superClass);
|
||||
|
||||
function MapManager(custom_type, uid, content, content_operations) {
|
||||
this._map = {};
|
||||
MapManager.__super__.constructor.call(this, custom_type, uid, content, content_operations);
|
||||
}
|
||||
|
||||
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.map = function(f) {
|
||||
var n, ref, v;
|
||||
ref = this._map;
|
||||
for (n in ref) {
|
||||
v = ref[n];
|
||||
f(n, v);
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
MapManager.prototype.val = function(name, content) {
|
||||
var o, prop, ref, rep, res, result;
|
||||
if (arguments.length > 1) {
|
||||
if ((content != null) && (content._getModel != null)) {
|
||||
rep = content._getModel(this.custom_types, this.operations);
|
||||
} else {
|
||||
rep = content;
|
||||
}
|
||||
this.retrieveSub(name).replace(rep);
|
||||
return this.getCustomType();
|
||||
} else if (name != null) {
|
||||
prop = this._map[name];
|
||||
if ((prop != null) && !prop.isContentDeleted()) {
|
||||
res = prop.val();
|
||||
if (res instanceof ops.Operation) {
|
||||
return res.getCustomType();
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
} 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 ops.ReplaceManager(null, 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;
|
||||
|
||||
})(ops.Operation);
|
||||
ops.MapManager.parse = function(json) {
|
||||
var content, content_operations, custom_type, uid;
|
||||
uid = json['uid'], custom_type = json['custom_type'], content = json['content'], content_operations = json['content_operations'];
|
||||
return new this(custom_type, uid, content, content_operations);
|
||||
};
|
||||
ops.ListManager = (function(superClass) {
|
||||
extend(ListManager, superClass);
|
||||
|
||||
function ListManager(custom_type, uid, content, content_operations) {
|
||||
this.beginning = new ops.Delimiter(void 0, void 0);
|
||||
this.end = new ops.Delimiter(this.beginning, void 0);
|
||||
this.beginning.next_cl = this.end;
|
||||
this.beginning.execute();
|
||||
this.end.execute();
|
||||
ListManager.__super__.constructor.call(this, custom_type, uid, content, content_operations);
|
||||
}
|
||||
|
||||
ListManager.prototype.type = "ListManager";
|
||||
|
||||
ListManager.prototype.applyDelete = function() {
|
||||
var o;
|
||||
o = this.beginning;
|
||||
while (o != null) {
|
||||
o.applyDelete();
|
||||
o = o.next_cl;
|
||||
}
|
||||
return ListManager.__super__.applyDelete.call(this);
|
||||
};
|
||||
|
||||
ListManager.prototype.cleanup = function() {
|
||||
return ListManager.__super__.cleanup.call(this);
|
||||
};
|
||||
|
||||
ListManager.prototype.toJson = function(transform_to_value) {
|
||||
var i, j, len, o, results, val;
|
||||
if (transform_to_value == null) {
|
||||
transform_to_value = false;
|
||||
}
|
||||
val = this.val();
|
||||
results = [];
|
||||
for (o = j = 0, len = val.length; j < len; o = ++j) {
|
||||
i = val[o];
|
||||
if (o instanceof ops.Object) {
|
||||
results.push(o.toJson(transform_to_value));
|
||||
} else if (o instanceof ops.ListManager) {
|
||||
results.push(o.toJson(transform_to_value));
|
||||
} else if (transform_to_value && o instanceof ops.Operation) {
|
||||
results.push(o.val());
|
||||
} else {
|
||||
results.push(o);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!o.is_deleted) {
|
||||
result.push(o.val());
|
||||
}
|
||||
o = o.next_cl;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
ListManager.prototype.map = function(f) {
|
||||
var o, result;
|
||||
o = this.beginning.next_cl;
|
||||
result = [];
|
||||
while (o !== this.end) {
|
||||
if (!o.is_deleted) {
|
||||
result.push(f(o));
|
||||
}
|
||||
o = o.next_cl;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
ListManager.prototype.fold = function(init, f) {
|
||||
var o;
|
||||
o = this.beginning.next_cl;
|
||||
while (o !== this.end) {
|
||||
if (!o.is_deleted) {
|
||||
init = f(init, o);
|
||||
}
|
||||
o = o.next_cl;
|
||||
}
|
||||
return init;
|
||||
};
|
||||
|
||||
ListManager.prototype.val = function(pos) {
|
||||
var o;
|
||||
if (pos != null) {
|
||||
o = this.getOperationByPosition(pos + 1);
|
||||
if (!(o instanceof ops.Delimiter)) {
|
||||
return o.val();
|
||||
} else {
|
||||
throw new Error("this position does not exist");
|
||||
}
|
||||
} else {
|
||||
return this.toArray();
|
||||
}
|
||||
};
|
||||
|
||||
ListManager.prototype.ref = function(pos) {
|
||||
var o;
|
||||
if (pos != null) {
|
||||
o = this.getOperationByPosition(pos + 1);
|
||||
if (!(o instanceof ops.Delimiter)) {
|
||||
return o;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
throw new Error("you must specify a position parameter");
|
||||
}
|
||||
};
|
||||
|
||||
ListManager.prototype.getOperationByPosition = function(position) {
|
||||
var o;
|
||||
o = this.beginning;
|
||||
while (true) {
|
||||
if (o instanceof ops.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;
|
||||
};
|
||||
|
||||
ListManager.prototype.push = function(content) {
|
||||
return this.insertAfter(this.end.prev_cl, [content]);
|
||||
};
|
||||
|
||||
ListManager.prototype.insertAfter = function(left, contents) {
|
||||
var c, j, len, right, tmp;
|
||||
right = left.next_cl;
|
||||
while (right.isDeleted()) {
|
||||
right = right.next_cl;
|
||||
}
|
||||
left = right.prev_cl;
|
||||
if (contents instanceof ops.Operation) {
|
||||
(new ops.Insert(null, content, null, void 0, void 0, left, right)).execute();
|
||||
} else {
|
||||
for (j = 0, len = contents.length; j < len; j++) {
|
||||
c = contents[j];
|
||||
if ((c != null) && (c._name != null) && (c._getModel != null)) {
|
||||
c = c._getModel(this.custom_types, this.operations);
|
||||
}
|
||||
tmp = (new ops.Insert(null, c, null, void 0, void 0, left, right)).execute();
|
||||
left = tmp;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
ListManager.prototype.insert = function(position, contents) {
|
||||
var ith;
|
||||
ith = this.getOperationByPosition(position);
|
||||
return this.insertAfter(ith, contents);
|
||||
};
|
||||
|
||||
ListManager.prototype["delete"] = function(position, length) {
|
||||
var d, delete_ops, i, j, o, ref;
|
||||
if (length == null) {
|
||||
length = 1;
|
||||
}
|
||||
o = this.getOperationByPosition(position + 1);
|
||||
delete_ops = [];
|
||||
for (i = j = 0, ref = length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
|
||||
if (o instanceof ops.Delimiter) {
|
||||
break;
|
||||
}
|
||||
d = (new ops.Delete(null, void 0, o)).execute();
|
||||
o = o.next_cl;
|
||||
while ((!(o instanceof ops.Delimiter)) && o.isDeleted()) {
|
||||
o = o.next_cl;
|
||||
}
|
||||
delete_ops.push(d._encode());
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
ListManager.prototype.callOperationSpecificInsertEvents = function(op) {
|
||||
var getContentType;
|
||||
getContentType = function(content) {
|
||||
if (content instanceof ops.Operation) {
|
||||
return content.getCustomType();
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
};
|
||||
return this.callEvent([
|
||||
{
|
||||
type: "insert",
|
||||
reference: op,
|
||||
position: op.getPosition(),
|
||||
object: this.getCustomType(),
|
||||
changedBy: op.uid.creator,
|
||||
value: getContentType(op.val())
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
ListManager.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {
|
||||
return this.callEvent([
|
||||
{
|
||||
type: "delete",
|
||||
reference: op,
|
||||
position: op.getPosition(),
|
||||
object: this.getCustomType(),
|
||||
length: 1,
|
||||
changedBy: del_op.uid.creator,
|
||||
oldValue: op.val()
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
return ListManager;
|
||||
|
||||
})(ops.Operation);
|
||||
ops.ListManager.parse = function(json) {
|
||||
var content, content_operations, custom_type, uid;
|
||||
uid = json['uid'], custom_type = json['custom_type'], content = json['content'], content_operations = json['content_operations'];
|
||||
return new this(custom_type, uid, content, content_operations);
|
||||
};
|
||||
ops.Composition = (function(superClass) {
|
||||
extend(Composition, superClass);
|
||||
|
||||
function Composition(custom_type, _composition_value, composition_value_operations, uid, tmp_composition_ref) {
|
||||
var n, o;
|
||||
this._composition_value = _composition_value;
|
||||
Composition.__super__.constructor.call(this, custom_type, uid);
|
||||
if (tmp_composition_ref != null) {
|
||||
this.tmp_composition_ref = tmp_composition_ref;
|
||||
} else {
|
||||
this.composition_ref = this.end.prev_cl;
|
||||
}
|
||||
if (composition_value_operations != null) {
|
||||
this.composition_value_operations = {};
|
||||
for (n in composition_value_operations) {
|
||||
o = composition_value_operations[n];
|
||||
this.saveOperation(n, o, '_composition_value');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Composition.prototype.type = "Composition";
|
||||
|
||||
Composition.prototype.execute = function() {
|
||||
var composition_ref;
|
||||
if (this.validateSavedOperations()) {
|
||||
this.getCustomType()._setCompositionValue(this._composition_value);
|
||||
delete this._composition_value;
|
||||
if (this.tmp_composition_ref) {
|
||||
composition_ref = this.HB.getOperation(this.tmp_composition_ref);
|
||||
if (composition_ref != null) {
|
||||
delete this.tmp_composition_ref;
|
||||
this.composition_ref = composition_ref;
|
||||
}
|
||||
}
|
||||
return Composition.__super__.execute.apply(this, arguments);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Composition.prototype.callOperationSpecificInsertEvents = function(op) {
|
||||
var o;
|
||||
if (this.tmp_composition_ref != null) {
|
||||
if (op.uid.creator === this.tmp_composition_ref.creator && op.uid.op_number === this.tmp_composition_ref.op_number) {
|
||||
this.composition_ref = op;
|
||||
delete this.tmp_composition_ref;
|
||||
op = op.next_cl;
|
||||
if (op === this.end) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
o = this.end.prev_cl;
|
||||
while (o !== op) {
|
||||
this.getCustomType()._unapply(o.undo_delta);
|
||||
o = o.prev_cl;
|
||||
}
|
||||
while (o !== this.end) {
|
||||
o.undo_delta = this.getCustomType()._apply(o.val());
|
||||
o = o.next_cl;
|
||||
}
|
||||
this.composition_ref = this.end.prev_cl;
|
||||
return this.callEvent([
|
||||
{
|
||||
type: "update",
|
||||
changedBy: op.uid.creator,
|
||||
newValue: this.val()
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
Composition.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {};
|
||||
|
||||
Composition.prototype.applyDelta = function(delta, operations) {
|
||||
(new ops.Insert(null, delta, operations, this, null, this.end.prev_cl, this.end)).execute();
|
||||
return void 0;
|
||||
};
|
||||
|
||||
Composition.prototype._encode = function(json) {
|
||||
var custom, n, o, ref;
|
||||
if (json == null) {
|
||||
json = {};
|
||||
}
|
||||
custom = this.getCustomType()._getCompositionValue();
|
||||
json.composition_value = custom.composition_value;
|
||||
if (custom.composition_value_operations != null) {
|
||||
json.composition_value_operations = {};
|
||||
ref = custom.composition_value_operations;
|
||||
for (n in ref) {
|
||||
o = ref[n];
|
||||
json.composition_value_operations[n] = o.getUid();
|
||||
}
|
||||
}
|
||||
if (this.composition_ref != null) {
|
||||
json.composition_ref = this.composition_ref.getUid();
|
||||
} else {
|
||||
json.composition_ref = this.tmp_composition_ref;
|
||||
}
|
||||
return Composition.__super__._encode.call(this, json);
|
||||
};
|
||||
|
||||
return Composition;
|
||||
|
||||
})(ops.ListManager);
|
||||
ops.Composition.parse = function(json) {
|
||||
var composition_ref, composition_value, composition_value_operations, custom_type, uid;
|
||||
uid = json['uid'], custom_type = json['custom_type'], composition_value = json['composition_value'], composition_value_operations = json['composition_value_operations'], composition_ref = json['composition_ref'];
|
||||
return new this(custom_type, composition_value, composition_value_operations, uid, composition_ref);
|
||||
};
|
||||
ops.ReplaceManager = (function(superClass) {
|
||||
extend(ReplaceManager, superClass);
|
||||
|
||||
function ReplaceManager(custom_type, event_properties1, event_this1, uid) {
|
||||
this.event_properties = event_properties1;
|
||||
this.event_this = event_this1;
|
||||
if (this.event_properties['object'] == null) {
|
||||
this.event_properties['object'] = this.event_this.getCustomType();
|
||||
}
|
||||
ReplaceManager.__super__.constructor.call(this, custom_type, uid);
|
||||
}
|
||||
|
||||
ReplaceManager.prototype.type = "ReplaceManager";
|
||||
|
||||
ReplaceManager.prototype.callEventDecorator = function(events) {
|
||||
var event, j, len, name, prop, ref;
|
||||
if (!this.isDeleted()) {
|
||||
for (j = 0, len = events.length; j < len; j++) {
|
||||
event = events[j];
|
||||
ref = this.event_properties;
|
||||
for (name in ref) {
|
||||
prop = ref[name];
|
||||
event[name] = prop;
|
||||
}
|
||||
}
|
||||
this.event_this.callEvent(events);
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.callOperationSpecificInsertEvents = function(op) {
|
||||
var old_value;
|
||||
if (op.next_cl.type === "Delimiter" && op.prev_cl.type !== "Delimiter") {
|
||||
if (!op.is_deleted) {
|
||||
old_value = op.prev_cl.val();
|
||||
this.callEventDecorator([
|
||||
{
|
||||
type: "update",
|
||||
changedBy: op.uid.creator,
|
||||
oldValue: old_value
|
||||
}
|
||||
]);
|
||||
}
|
||||
op.prev_cl.applyDelete();
|
||||
} else if (op.next_cl.type !== "Delimiter") {
|
||||
op.applyDelete();
|
||||
} else {
|
||||
this.callEventDecorator([
|
||||
{
|
||||
type: "add",
|
||||
changedBy: op.uid.creator
|
||||
}
|
||||
]);
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.callOperationSpecificDeleteEvents = function(op, del_op) {
|
||||
if (op.next_cl.type === "Delimiter") {
|
||||
return this.callEventDecorator([
|
||||
{
|
||||
type: "delete",
|
||||
changedBy: del_op.uid.creator,
|
||||
oldValue: op.val()
|
||||
}
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.replace = function(content, replaceable_uid) {
|
||||
var o, relp;
|
||||
o = this.getLastOperation();
|
||||
relp = (new ops.Insert(null, content, null, this, replaceable_uid, o, o.next_cl)).execute();
|
||||
return void 0;
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.isContentDeleted = function() {
|
||||
return this.getLastOperation().isDeleted();
|
||||
};
|
||||
|
||||
ReplaceManager.prototype.deleteContent = function() {
|
||||
var last_op;
|
||||
last_op = this.getLastOperation();
|
||||
if ((!last_op.isDeleted()) && last_op.type !== "Delimiter") {
|
||||
(new ops.Delete(null, 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;
|
||||
};
|
||||
|
||||
return ReplaceManager;
|
||||
|
||||
})(ops.ListManager);
|
||||
return basic_ops;
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
var bindToChildren;
|
||||
|
||||
bindToChildren = function(that) {
|
||||
var attr, i, j, ref;
|
||||
for (i = j = 0, ref = that.children.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) {
|
||||
attr = that.children.item(i);
|
||||
if (attr.name != null) {
|
||||
attr.val = that.val.val(attr.name);
|
||||
}
|
||||
}
|
||||
return that.val.observe(function(events) {
|
||||
var event, k, len, newVal, results;
|
||||
results = [];
|
||||
for (k = 0, len = events.length; k < len; k++) {
|
||||
event = events[k];
|
||||
if (event.name != null) {
|
||||
results.push((function() {
|
||||
var l, ref1, results1;
|
||||
results1 = [];
|
||||
for (i = l = 0, ref1 = that.children.length; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) {
|
||||
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._name === "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, new Y.Object(this.val)).val(this.name);
|
||||
} else if (typeof this.val === "string") {
|
||||
this.parentElement.val(this.name, this.val);
|
||||
}
|
||||
if (this.val._name === "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, new Y.Object(this.val)).val(this.name);
|
||||
} else if (this.val._name === "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,45 +0,0 @@
|
||||
var Engine, HistoryBuffer, adaptConnector, createY, structured_ops_uninitialized;
|
||||
|
||||
structured_ops_uninitialized = require("./Operations/Structured");
|
||||
|
||||
HistoryBuffer = require("./HistoryBuffer");
|
||||
|
||||
Engine = require("./Engine");
|
||||
|
||||
adaptConnector = require("./ConnectorAdapter");
|
||||
|
||||
createY = function(connector) {
|
||||
var HB, ct, engine, model, ops, ops_manager, user_id;
|
||||
if (connector.user_id != null) {
|
||||
user_id = connector.user_id;
|
||||
} else {
|
||||
user_id = "_temp";
|
||||
connector.when_received_state_vector_listeners = [
|
||||
function(state_vector) {
|
||||
return HB.setUserId(this.user_id, state_vector);
|
||||
}
|
||||
];
|
||||
}
|
||||
HB = new HistoryBuffer(user_id);
|
||||
ops_manager = structured_ops_uninitialized(HB, this.constructor);
|
||||
ops = ops_manager.operations;
|
||||
engine = new Engine(HB, ops);
|
||||
adaptConnector(connector, engine, HB, ops_manager.execution_listener);
|
||||
ops.Operation.prototype.HB = HB;
|
||||
ops.Operation.prototype.operations = ops;
|
||||
ops.Operation.prototype.engine = engine;
|
||||
ops.Operation.prototype.connector = connector;
|
||||
ops.Operation.prototype.custom_types = this.constructor;
|
||||
ct = new createY.Object();
|
||||
model = new ops.MapManager(ct, HB.getReservedUniqueIdentifier()).execute();
|
||||
ct._setModel(model);
|
||||
return ct;
|
||||
};
|
||||
|
||||
module.exports = createY;
|
||||
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
window.Y = createY;
|
||||
}
|
||||
|
||||
createY.Object = require("./ObjectType");
|
||||
@@ -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>
|
||||
<script src="../../node_modules/mocha/mocha.js" class="awesome"></script>
|
||||
<script>
|
||||
mocha.setup('bdd');
|
||||
mocha.ui('bdd');
|
||||
mocha.reporter('html');
|
||||
</script>
|
||||
<script src="object-test.js"></script>
|
||||
<script src="xml-test.js"></script>
|
||||
<script src="list-test.js"></script>
|
||||
<script src="text-test.js"></script>
|
||||
<script>
|
||||
//mocha.checkLeaks();
|
||||
//mocha.run();
|
||||
window.onerror = null;
|
||||
if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
|
||||
else { mocha.run(); }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18723
build/test/list-test.js
18723
build/test/list-test.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
18741
build/test/text-test.js
18741
build/test/text-test.js
File diff suppressed because one or more lines are too long
@@ -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,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,57 +0,0 @@
|
||||
<script src="../../build/browser/y.js"></script>
|
||||
<script src="../../../y-text/build/browser/y-text.js"></script>
|
||||
<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",new Y.Text("stuff"));
|
||||
}
|
||||
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,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=utf-8 />
|
||||
<title>Y Example</title>
|
||||
<script src="../../build/browser/y.js"></script>
|
||||
<script src="../../../y-text/build/browser/y-text.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/y-js/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", new Y.Text("headline"));
|
||||
y.val("textfield",new Y.Text("stuff"))
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
33
examples/ace/index.html
Normal file
33
examples/ace/index.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!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="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
17
examples/ace/index.js
Normal file
17
examples/ace/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* global Y, ace */
|
||||
|
||||
let y = new Y('ace-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
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.define('ace', Y.Text).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/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
65
examples/chat/index.js
Normal file
65
examples/chat/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/* global Y */
|
||||
|
||||
let y = new Y('chat-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
cleanupChat()
|
||||
|
||||
// Insert the initial content
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
|
||||
// whenever content changes, make sure to reflect the changes in the DOM
|
||||
chatprotocol.observe(function (event) {
|
||||
// concurrent insertions may result in a history > 7, so cleanup here
|
||||
cleanupChat()
|
||||
chatcontainer.innerHTML = ''
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
})
|
||||
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
|
||||
}
|
||||
24
examples/codemirror/index.html
Normal file
24
examples/codemirror/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.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>
|
||||
16
examples/codemirror/index.js
Normal file
16
examples/codemirror/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
let y = new Y('codemirror-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
|
||||
20
examples/drawing/index.html
Normal file
20
examples/drawing/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!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-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
74
examples/drawing/index.js
Normal file
74
examples/drawing/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/* globals Y, d3 */
|
||||
|
||||
let y = new Y('drawing-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yDrawing = y
|
||||
var drawing = y.define('drawing', Y.Array)
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basic')
|
||||
|
||||
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) {
|
||||
line.remove()
|
||||
line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
drawing.observe(function (event) {
|
||||
event.removedElements.forEach(function () {
|
||||
// if one is deleted, all will be deleted!!
|
||||
svg.selectAll('path').remove()
|
||||
})
|
||||
event.addedElements.forEach(function (path) {
|
||||
drawLine(path)
|
||||
})
|
||||
})
|
||||
// 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
|
||||
}, 10)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
36
examples/html-editor-drawing-hook/index.html
Normal file
36
examples/html-editor-drawing-hook/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<style>
|
||||
magic-drawing .drawingCanvas path {
|
||||
fill: none;
|
||||
stroke: blue;
|
||||
stroke-width: 2px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
magic-drawing .drawingCanvas {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
cursor: default;
|
||||
padding:1px;
|
||||
border:1px solid #021a40;
|
||||
}
|
||||
magic-drawing .clearDrawingButton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
magic-drawing {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
134
examples/html-editor-drawing-hook/index.js
Normal file
134
examples/html-editor-drawing-hook/index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/* global Y, d3 */
|
||||
|
||||
const hooks = {
|
||||
'magic-drawing': {
|
||||
fillType: function (dom, type) {
|
||||
initDrawingBindings(type, dom)
|
||||
},
|
||||
createDom: function (type) {
|
||||
const dom = document.createElement('magic-drawing')
|
||||
initDrawingBindings(type, dom)
|
||||
return dom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks })
|
||||
}
|
||||
|
||||
window.addMagicDrawing = function addMagicDrawing () {
|
||||
let mt = document.createElement('magic-drawing')
|
||||
mt.setAttribute('data-yjs-hook', 'magic-drawing')
|
||||
document.body.append(mt)
|
||||
}
|
||||
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basic')
|
||||
|
||||
function initDrawingBindings (type, dom) {
|
||||
dom.contentEditable = 'false'
|
||||
dom.setAttribute('data-yjs-hook', 'magic-drawing')
|
||||
var drawing = type.get('drawing')
|
||||
if (drawing === undefined) {
|
||||
drawing = type.set('drawing', new Y.Array())
|
||||
}
|
||||
var canvas = dom.querySelector('.drawingCanvas')
|
||||
if (canvas == null) {
|
||||
canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
canvas.setAttribute('class', 'drawingCanvas')
|
||||
canvas.setAttribute('viewbox', '0 0 100 100')
|
||||
dom.insertBefore(canvas, null)
|
||||
}
|
||||
var clearDrawingButton = dom.querySelector('.clearDrawingButton')
|
||||
if (clearDrawingButton == null) {
|
||||
clearDrawingButton = document.createElement('button')
|
||||
clearDrawingButton.setAttribute('type', 'button')
|
||||
clearDrawingButton.setAttribute('class', 'clearDrawingButton')
|
||||
clearDrawingButton.innerText = 'Clear Drawing'
|
||||
dom.insertBefore(clearDrawingButton, null)
|
||||
}
|
||||
var svg = d3.select(canvas)
|
||||
.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, svg) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
line.remove()
|
||||
line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
drawing.observe(function (event) {
|
||||
event.removedElements.forEach(function () {
|
||||
// if one is deleted, all will be deleted!!
|
||||
svg.selectAll('path').remove()
|
||||
})
|
||||
event.addedElements.forEach(function (path) {
|
||||
drawLine(path, svg)
|
||||
})
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i), svg)
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
clearDrawingButton.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
|
||||
}, 10)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
}
|
||||
|
||||
let y = new Y('html-editor-drawing-hook-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yXml = y
|
||||
window.yXmlType = y.define('xml', Y.XmlFragment)
|
||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
||||
captureTimeout: 500
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && e.metaKey) {
|
||||
if (!e.shiftKey) {
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
11
examples/html-editor/index.html
Normal file
11
examples/html-editor/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="./index.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<label for="room">Room: </label>
|
||||
<input type="text" id="room" name="room">
|
||||
<div id="content" contenteditable style="position:absolute;top:35px;left:0;right:0;bottom:0;outline: 0px solid transparent;"></div>
|
||||
</body>
|
||||
</html>
|
||||
77
examples/html-editor/index.js
Normal file
77
examples/html-editor/index.js
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.js'
|
||||
import Y from '../../src/Y.js'
|
||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
|
||||
import UndoManager from '../../src/Util/UndoManager.js'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.js'
|
||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.js'
|
||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.js'
|
||||
|
||||
const connector = new YWebsocketsConnector()
|
||||
const persistence = new YIndexdDBPersistence()
|
||||
|
||||
const roomInput = document.querySelector('#room')
|
||||
|
||||
let currentRoomName = null
|
||||
let y = null
|
||||
let domBinding = null
|
||||
|
||||
function setRoomName (roomName) {
|
||||
if (currentRoomName !== roomName) {
|
||||
console.log(`change room: "${roomName}"`)
|
||||
roomInput.value = roomName
|
||||
currentRoomName = roomName
|
||||
location.hash = '#' + roomName
|
||||
if (y !== null) {
|
||||
domBinding.destroy()
|
||||
}
|
||||
|
||||
const room = connector._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
y = room.y
|
||||
} else {
|
||||
y = new Y(roomName, null, null, { gc: true })
|
||||
persistence.connectY(roomName, y).then(() => {
|
||||
// connect after persisted content was applied to y
|
||||
// If we don't wait for persistence, the other peer will send all data, waisting
|
||||
// network bandwidth..
|
||||
connector.connectY(roomName, y)
|
||||
})
|
||||
window.y = y
|
||||
}
|
||||
|
||||
window.y = y
|
||||
window.yXmlType = y.define('xml', YXmlFragment)
|
||||
|
||||
domBinding = new DomBinding(window.yXmlType, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||
}
|
||||
}
|
||||
window.setRoomName = setRoomName
|
||||
|
||||
window.createRooms = function (i = 0) {
|
||||
setInterval(function () {
|
||||
setRoomName(i + '')
|
||||
i++
|
||||
const nodes = []
|
||||
for (let j = 0; j < 100; j++) {
|
||||
const node = new YXmlElement('p')
|
||||
node.insert(0, [new YXmlText(`This is the ${i}th paragraph of room ${i}`)])
|
||||
nodes.push(node)
|
||||
}
|
||||
y.share.xml.insert(0, nodes)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
connector.syncPersistence(persistence)
|
||||
|
||||
window.connector = connector
|
||||
window.persistence = persistence
|
||||
|
||||
window.onload = function () {
|
||||
setRoomName((location.hash || '#default').slice(1))
|
||||
roomInput.addEventListener('input', e => {
|
||||
const roomName = e.target.value
|
||||
setRoomName(roomName)
|
||||
})
|
||||
}
|
||||
24
examples/indexeddb/index.html
Normal file
24
examples/indexeddb/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!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 src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src='../../../y-indexeddb/y-indexeddb.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
19
examples/indexeddb/index.js
Normal file
19
examples/indexeddb/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
const persistence = new Y.IndexedDB()
|
||||
const connector = {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
}
|
||||
}
|
||||
|
||||
const y = new Y('codemirror-example', connector, persistence)
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
|
||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
|
||||
55
examples/infiniteyjs/index.html
Normal file
55
examples/infiniteyjs/index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!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-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
examples/infiniteyjs/index.js
Normal file
38
examples/infiniteyjs/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/* global Y */
|
||||
|
||||
function bindYjsInstance (y, suffix) {
|
||||
y.define('textarea', Y.Text).bind(document.getElementById('textarea' + suffix))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container' + suffix).removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container' + suffix).setAttribute('disconnected', true)
|
||||
})
|
||||
}
|
||||
|
||||
let y1 = new Y('infinite-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
window.y1 = y1
|
||||
bindYjsInstance(y1, '1')
|
||||
|
||||
let y2 = new Y('infinite-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
window.y2 = y2
|
||||
bindYjsInstance(y2, '2')
|
||||
|
||||
let y3 = new Y('infinite-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
window.y3 = y3
|
||||
bindYjsInstance(y1, '3')
|
||||
24
examples/jigsaw/index.html
Normal file
24
examples/jigsaw/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!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-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
67
examples/jigsaw/index.js
Normal file
67
examples/jigsaw/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* global Y, d3 */
|
||||
|
||||
let y = new Y('jigsaw-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
let jigsaw = y.define('jigsaw', Y.Map)
|
||||
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
|
||||
jigsaw.set(piece, {x: x, y: y})
|
||||
})
|
||||
|
||||
var data = ['piece1', 'piece2', 'piece3', '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) {
|
||||
jigsaw.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(piece)
|
||||
if (translation == null || typeof translation.x !== 'number' || typeof translation.y !== 'number') {
|
||||
translation = { x: 0, y: 0 }
|
||||
}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
})
|
||||
21
examples/monaco/index.html
Normal file
21
examples/monaco/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!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="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
examples/monaco/index.js
Normal file
22
examples/monaco/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* global Y, monaco */
|
||||
|
||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
||||
|
||||
let y = new Y('monaco-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
window.yMonaco = y
|
||||
|
||||
// Create Monaco editor
|
||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||
language: 'javascript'
|
||||
})
|
||||
|
||||
// Bind to y.share.monaco
|
||||
y.define('monaco', Y.Text).bindMonaco(editor)
|
||||
})
|
||||
17
examples/notes/index.html
Normal file
17
examples/notes/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="./index.js" type="module"></script>
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<h3 id="createNoteButton">+ Create Note</h3>
|
||||
<div class="notelist"></div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<h1 id="headline"></h1>
|
||||
<div id="editor" contenteditable="true"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
132
examples/notes/index.js
Normal file
132
examples/notes/index.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
import { createYdbClient } from '../../YdbClient/index.js'
|
||||
import Y from '../../src/Y.dist.js'
|
||||
import * as ydb from '../../YdbClient/YdbClient.js'
|
||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.js'
|
||||
|
||||
const uuidv4 = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
const r = Math.random() * 16 | 0
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
|
||||
})
|
||||
|
||||
createYdbClient('ws://localhost:8899/ws').then(ydbclient => {
|
||||
const y = ydbclient.getY('notelist')
|
||||
let ynotelist = y.define('notelist', Y.Array)
|
||||
window.ynotelist = ynotelist
|
||||
const domNoteList = document.querySelector('.notelist')
|
||||
|
||||
// utils
|
||||
const addEventListener = (element, eventname, f) => element.addEventListener(eventname, f)
|
||||
|
||||
// create note button
|
||||
const createNoteButton = event => {
|
||||
ynotelist.insert(0, [{
|
||||
guid: uuidv4(),
|
||||
title: 'Note #' + ynotelist.length
|
||||
}])
|
||||
}
|
||||
addEventListener(document.querySelector('#createNoteButton'), 'click', createNoteButton)
|
||||
window.createNote = createNoteButton
|
||||
window.createNotes = n => {
|
||||
y.transact(() => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
createNoteButton()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// clear note list function
|
||||
window.clearNotes = () => ynotelist.delete(0, ynotelist.length)
|
||||
|
||||
// update editor and editor title
|
||||
let domBinding = null
|
||||
const updateEditor = () => {
|
||||
domNoteList.querySelectorAll('a').forEach(a => a.classList.remove('selected'))
|
||||
const domNote = document.querySelector('.notelist').querySelector(`[href="${location.hash}"]`)
|
||||
if (domNote !== null) {
|
||||
domNote.classList.add('selected')
|
||||
const note = ynotelist.toArray().find(note => note.guid === location.hash.slice(1))
|
||||
if (note !== undefined) {
|
||||
const ydoc = ydbclient.getY(note.guid)
|
||||
const ycontent = ydoc.define('content', Y.XmlFragment)
|
||||
if (domBinding !== null) {
|
||||
domBinding.destroy()
|
||||
}
|
||||
domBinding = new DomBinding(ycontent, document.querySelector('#editor'))
|
||||
document.querySelector('#headline').innerText = note.title
|
||||
document.querySelector('#editor').focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listen to url-hash changes
|
||||
addEventListener(window, 'hashchange', updateEditor)
|
||||
updateEditor()
|
||||
|
||||
const styleSyncedState = (div, noteSyncedState) => {
|
||||
let classes = []
|
||||
if (noteSyncedState.persisted) {
|
||||
classes.push('persisted')
|
||||
} else {
|
||||
if (noteSyncedState.upsynced) {
|
||||
classes.push('upsynced')
|
||||
} else {
|
||||
classes.push('noupsynced')
|
||||
}
|
||||
if (noteSyncedState.downsynced) {
|
||||
classes.push('downsynced')
|
||||
} else {
|
||||
classes.push('nodownsynced')
|
||||
}
|
||||
}
|
||||
div.setAttribute('class', classes.join(' '))
|
||||
}
|
||||
|
||||
ydbclient.on('syncstate', event => event.updated.forEach((state, room) => {
|
||||
const a = document.querySelector(`[href="#${room}"]`)
|
||||
if (a !== null) {
|
||||
styleSyncedState(a.firstChild, state)
|
||||
}
|
||||
}))
|
||||
|
||||
// render note list
|
||||
const renderNoteList = (elementList, insertRef = domNoteList.firstChild) => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
const addNow = elementList.splice(0, 100)
|
||||
addNow.forEach(note => {
|
||||
const a = document.createElement('a')
|
||||
const div = document.createElement('div')
|
||||
a.insertBefore(div, null)
|
||||
a.setAttribute('href', '#' + note.guid)
|
||||
div.innerText = note.title
|
||||
styleSyncedState(div, ydbclient.getRoomState(note.guid))
|
||||
fragment.insertBefore(a, null)
|
||||
})
|
||||
if (domBinding == null) {
|
||||
updateEditor()
|
||||
}
|
||||
domNoteList.insertBefore(fragment, insertRef)
|
||||
if (elementList.length > 0) {
|
||||
setTimeout(() => renderNoteList(elementList, insertRef), 100)
|
||||
}
|
||||
}
|
||||
{
|
||||
const notelist = ynotelist.toArray()
|
||||
if (notelist.length > 0) {
|
||||
renderNoteList(notelist)
|
||||
ydb.subscribeRooms(ydbclient, notelist.map(note => note.guid))
|
||||
}
|
||||
}
|
||||
ynotelist.observe(event => {
|
||||
const addedNotes = []
|
||||
event.addedElements.forEach(itemJson => itemJson._content.forEach(json => addedNotes.push(json)))
|
||||
renderNoteList(addedNotes.slice().reverse()) // renderNoteList modifies addedNotes, so first make a copy of it
|
||||
setTimeout(() => {
|
||||
ydb.subscribeRooms(ydbclient, addedNotes.map(note => note.guid))
|
||||
}, 200)
|
||||
if (domBinding === null) {
|
||||
updateEditor()
|
||||
}
|
||||
})
|
||||
})
|
||||
100
examples/notes/style.css
Normal file
100
examples/notes/style.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.sidebar {
|
||||
height: 100%; /* Full-height: remove this if you want "auto" height */
|
||||
width: 180px; /* Set the width of the sidebar */
|
||||
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
|
||||
z-index: 1; /* Stay on top */
|
||||
top: 0; /* Stay at the top */
|
||||
left: 0;
|
||||
background-color: #111; /* Black */
|
||||
overflow-x: hidden; /* Disable horizontal scroll */
|
||||
padding-top: 20px;
|
||||
color: #50abff;
|
||||
}
|
||||
|
||||
#createNoteButton {
|
||||
padding-left: .5em;
|
||||
padding-top: .5em;
|
||||
padding-bottom: .7em;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notelist > a {
|
||||
padding: 6px 8px 6px 16px;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
color: #818181;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notelist > a.selected {
|
||||
border-style: outset;
|
||||
}
|
||||
|
||||
.notelist > a > div {
|
||||
position: relative;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* When you mouse over the navigation links, change their color */
|
||||
.sidebar a:hover {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
/* Style page content */
|
||||
.main {
|
||||
margin-left: 180px; /* Same as the width of the sidebar */
|
||||
padding: 0px 10px;
|
||||
}
|
||||
|
||||
/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */
|
||||
@media screen and (max-height: 450px) {
|
||||
.sidebar {padding-top: 15px;}
|
||||
.sidebar a {font-size: 18px;}
|
||||
}
|
||||
|
||||
#editor {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
[contenteditable]:focus {
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
.persisted::before {
|
||||
content: "✔";
|
||||
color: green;
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.upsynced::before {
|
||||
content: "↑";
|
||||
color: green;
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.noupsynced::before {
|
||||
content: "↑";
|
||||
color: red;
|
||||
position: absolute;
|
||||
right: -14px;
|
||||
top: 0px;
|
||||
}
|
||||
.downsynced::after {
|
||||
content: "↓";
|
||||
color: green;
|
||||
position: absolute;
|
||||
right: -22px;
|
||||
top: 0px;
|
||||
}
|
||||
.nodownsynced::after {
|
||||
content: "↓";
|
||||
color: red;
|
||||
position: absolute;
|
||||
right: -22px;
|
||||
top: 0px;
|
||||
}
|
||||
23
examples/package.json
Normal file
23
examples/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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",
|
||||
"rollup": "^0.52.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^10.0.2"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
]
|
||||
}
|
||||
}
|
||||
21
examples/quill-cursors/index.html
Normal file
21
examples/quill-cursors/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Main quill library -->
|
||||
<script src="../../node_modules/quill/dist/quill.min.js"></script>
|
||||
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
|
||||
<!-- Quill cursors module -->
|
||||
<script src="../../node_modules/quill-cursors/dist/quill-cursors.min.js"></script>
|
||||
<link href="../../node_modules/quill-cursors/dist/quill-cursors.css" rel="stylesheet">
|
||||
<!-- Yjs Library and connector -->
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
78
examples/quill-cursors/index.js
Normal file
78
examples/quill-cursors/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/* global Y, Quill, QuillCursors */
|
||||
|
||||
Quill.register('modules/cursors', QuillCursors)
|
||||
|
||||
let y = new Y('quill-0', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
let users = y.define('users', Y.Array)
|
||||
let myUserInfo = new Y.Map()
|
||||
myUserInfo.set('name', 'dada')
|
||||
myUserInfo.set('color', 'red')
|
||||
users.push([myUserInfo])
|
||||
|
||||
let quill = new Quill('#quill-container', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||
],
|
||||
cursors: {
|
||||
hideDelay: 500
|
||||
}
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
let cursors = quill.getModule('cursors')
|
||||
|
||||
function drawCursors () {
|
||||
cursors.clearCursors()
|
||||
users.map((user, userId) => {
|
||||
if (user !== myUserInfo) {
|
||||
let relativeRange = user.get('range')
|
||||
let lastUpdated = new Date(user.get('last updated'))
|
||||
if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) {
|
||||
let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
|
||||
let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
|
||||
let range = { index: start, length: end - start }
|
||||
cursors.setCursor(userId + '', range, user.get('name'), user.get('color'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
users.observeDeep(drawCursors)
|
||||
drawCursors()
|
||||
|
||||
quill.on('selection-change', function (range) {
|
||||
if (range != null) {
|
||||
myUserInfo.set('range', {
|
||||
start: Y.utils.getRelativePosition(yText, range.index),
|
||||
end: Y.utils.getRelativePosition(yText, range.index + range.length)
|
||||
})
|
||||
} else {
|
||||
myUserInfo.delete('range')
|
||||
}
|
||||
myUserInfo.set('last updated', new Date().toString())
|
||||
})
|
||||
|
||||
let yText = y.define('quill', Y.Text)
|
||||
let quillBinding = new Y.QuillBinding(yText, quill)
|
||||
|
||||
window.quillBinding = quillBinding
|
||||
window.yText = yText
|
||||
window.y = y
|
||||
window.quill = quill
|
||||
window.users = users
|
||||
window.cursors = cursors
|
||||
18
examples/quill/index.html
Normal file
18
examples/quill/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Main Quill library -->
|
||||
<script src="../../node_modules/quill/dist/quill.min.js"></script>
|
||||
<link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
|
||||
<!-- Yjs Library and connector -->
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
33
examples/quill/index.js
Normal file
33
examples/quill/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
let y = new Y('quill-cursors-0', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
let quill = new Quill('#quill-container', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||
]
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
let yText = y.define('quill', Y.Text)
|
||||
|
||||
let quillBinding = new Y.QuillBinding(yText, quill)
|
||||
window.quillBinding = quillBinding
|
||||
window.yText = yText
|
||||
window.y = y
|
||||
window.quill = quill
|
||||
29
examples/rollup.config.js
Normal file
29
examples/rollup.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: 'yjs-dist.js',
|
||||
name: 'Y',
|
||||
output: {
|
||||
file: 'yjs-dist.js',
|
||||
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'
|
||||
)
|
||||
7
examples/textarea/index.html
Normal file
7
examples/textarea/index.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
examples/textarea/index.js
Normal file
13
examples/textarea/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-env browser */
|
||||
import * as Y from '../../src/index.js'
|
||||
import WebsocketProvider from '../../provider/websocket/WebSocketProvider.js'
|
||||
|
||||
const provider = new WebsocketProvider('ws://localhost:1234/')
|
||||
const ydocument = provider.get('textarea')
|
||||
const type = ydocument.define('textarea', Y.Text)
|
||||
const textarea = document.querySelector('textarea')
|
||||
const binding = new Y.TextareaBinding(type, textarea)
|
||||
|
||||
window.textareaExample = {
|
||||
provider, ydocument, type, textarea, binding
|
||||
}
|
||||
43
examples/xml/index.html
Normal file
43
examples/xml/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for YXml. 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="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.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>
|
||||
/* global $ */
|
||||
var commands = document.querySelectorAll('.command')
|
||||
Array.prototype.forEach.call(commands, function (command) {
|
||||
var execute = function () {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(command.querySelector('input').value)
|
||||
}
|
||||
command.querySelector('button').onclick = execute
|
||||
$(command.querySelector('input')).keyup(function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
execute()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
13
examples/xml/index.js
Normal file
13
examples/xml/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* global Y */
|
||||
|
||||
let y = new Y('xml-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yXml = y
|
||||
// bind xml type to a dom, and put it in body
|
||||
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
|
||||
document.body.appendChild(window.sharedDom)
|
||||
114
gulpfile.coffee
114
gulpfile.coffee
@@ -1,114 +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'
|
||||
cache = require 'gulp-cached'
|
||||
coffeeify = require 'gulp-coffeeify'
|
||||
exit = require 'gulp-exit'
|
||||
|
||||
gulp.task 'default', ['build_browser']
|
||||
|
||||
files =
|
||||
lib : ['./lib/**/*.coffee']
|
||||
browser : ['./lib/y.coffee','./lib/y-object.coffee']
|
||||
test : ['./test/**/*test.coffee', '../y-*/test/*test.coffee']
|
||||
#test : ['./test/Json_test.coffee', './test/Text_test.coffee']
|
||||
gulp : ['./gulpfile.coffee']
|
||||
examples : ['./examples/**/*.js']
|
||||
other: ['./lib/**/*', './test/*']
|
||||
|
||||
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', '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"
|
||||
dirname: "./"
|
||||
.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'], ->
|
||||
gulp.watch files.all, ['build']
|
||||
|
||||
gulp.task 'mocha', ->
|
||||
gulp.src files.test, { read: false }
|
||||
.pipe mocha {reporter : 'list'}
|
||||
.pipe exit()
|
||||
|
||||
gulp.task 'lint', ->
|
||||
gulp.src files.all
|
||||
.pipe ignore.include '**/*.coffee'
|
||||
.pipe coffeelint {
|
||||
"max_line_length":
|
||||
"level": "ignore"
|
||||
}
|
||||
.pipe coffeelint.reporter()
|
||||
|
||||
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 'clean', ->
|
||||
gulp.src ['./build/{browser,test,node}/**/*.{js,map}','./doc/'], { read: false }
|
||||
.pipe rimraf()
|
||||
|
||||
gulp.task 'default', ['clean','build'], ->
|
||||
@@ -1,61 +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..
|
||||
(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,355 +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
|
||||
|
||||
onUserEvent: (f)->
|
||||
@connections_listeners ?= []
|
||||
@connections_listeners.push f
|
||||
|
||||
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()
|
||||
if @connections_listeners?
|
||||
for f in @connections_listeners
|
||||
f {
|
||||
action: "userLeft"
|
||||
user: user
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
if @connections_listeners?
|
||||
for f in @connections_listeners
|
||||
f {
|
||||
action: "userJoined"
|
||||
user: user
|
||||
role: role
|
||||
}
|
||||
|
||||
#
|
||||
# Execute a function _when_ we are connected. If not connected, wait until connected.
|
||||
# @param f {Function} Will be executed on the Connector context.
|
||||
#
|
||||
whenSynced: (args)->
|
||||
if args.constructor 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 > 10
|
||||
@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: @getStateVector()
|
||||
hb = @getHB([]).hb
|
||||
_hb = []
|
||||
for o in hb
|
||||
_hb.push o
|
||||
if _hb.length > 10
|
||||
@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 el in @compute_when_synced
|
||||
f = el[0]
|
||||
args = el[1..]
|
||||
f.apply(args)
|
||||
delete @compute_when_synced
|
||||
null
|
||||
|
||||
# executed when the a state_vector is received. listener will be called only once!
|
||||
whenReceivedStateVector: (f)->
|
||||
@when_received_state_vector_listeners ?= []
|
||||
@when_received_state_vector_listeners.push f
|
||||
|
||||
|
||||
#
|
||||
# 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"
|
||||
# call listeners
|
||||
if @when_received_state_vector_listeners?
|
||||
for f in @when_received_state_vector_listeners
|
||||
f.call this, res.data
|
||||
delete @when_received_state_vector_listeners
|
||||
|
||||
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 > 10
|
||||
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
|
||||
for o in hb
|
||||
_hb.push o
|
||||
if _hb.length > 10
|
||||
@send sender,
|
||||
sync_step: "applyHB_"
|
||||
data: _hb
|
||||
_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,115 +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
|
||||
o.parsed_from_json = 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,227 +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
|
||||
|
||||
# At the beginning (when the user id was not assigned yet),
|
||||
# the operations are added to buffer._temp. When you finally get your user id,
|
||||
# the operations are copies from buffer._temp to buffer[id]. Furthermore, when buffer[id] does already contain operations
|
||||
# (because of a previous session), the uid.op_numbers of the operations have to be reassigned.
|
||||
# This is what this function does. It adds them to buffer[id],
|
||||
# and assigns them the correct uid.op_number and uid.creator
|
||||
setUserId: (@user_id, state_vector)->
|
||||
@buffer[@user_id] ?= []
|
||||
buff = @buffer[@user_id]
|
||||
|
||||
# we assumed that we started with counter = 0.
|
||||
# when we receive tha state_vector, and actually have
|
||||
# counter = 10. Then we have to add 10 to every op_counter
|
||||
counter_diff = state_vector[@user_id] or 0
|
||||
|
||||
if @buffer._temp?
|
||||
for o_name,o of @buffer._temp
|
||||
o.uid.creator = @user_id
|
||||
o.uid.op_number += counter_diff
|
||||
buff[o.uid.op_number] = o
|
||||
|
||||
@operation_counter[@user_id] = (@operation_counter._temp or 0) + counter_diff
|
||||
|
||||
delete @operation_counter._temp
|
||||
delete @buffer._temp
|
||||
|
||||
|
||||
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++}"
|
||||
}
|
||||
|
||||
#
|
||||
# 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]
|
||||
if u_name is "_"
|
||||
continue
|
||||
for o_number,o of user
|
||||
if (not o.uid.noOperation?) 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]
|
||||
@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)->
|
||||
@operation_counter[o.uid.creator] ?= 0
|
||||
# TODO: check if operations are send in order
|
||||
if o.uid.op_number is @operation_counter[o.uid.creator]
|
||||
@operation_counter[o.uid.creator]++
|
||||
while @buffer[o.uid.creator][@operation_counter[o.uid.creator]]?
|
||||
@operation_counter[o.uid.creator]++
|
||||
undefined
|
||||
|
||||
module.exports = HistoryBuffer
|
||||
113
lib/NamedEventHandler.js
Normal file
113
lib/NamedEventHandler.js
Normal file
@@ -0,0 +1,113 @@
|
||||
|
||||
/**
|
||||
* Handles named events.
|
||||
*/
|
||||
export default class NamedEventHandler {
|
||||
constructor () {
|
||||
this._eventListener = new Map()
|
||||
this._stateListener = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Returns all listeners that listen to a specified name.
|
||||
*
|
||||
* @param {String} name The query event name.
|
||||
*/
|
||||
_getListener (name) {
|
||||
let listeners = this._eventListener.get(name)
|
||||
if (listeners === undefined) {
|
||||
listeners = {
|
||||
once: new Set(),
|
||||
on: new Set()
|
||||
}
|
||||
this._eventListener.set(name, listeners)
|
||||
}
|
||||
return listeners
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a named event listener. The listener is removed after it has been
|
||||
* called once.
|
||||
*
|
||||
* @param {String} name The event name to listen to.
|
||||
* @param {Function} f The function that is executed when the event is fired.
|
||||
*/
|
||||
once (name, f) {
|
||||
let listeners = this._getListener(name)
|
||||
listeners.once.add(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a named event listener.
|
||||
*
|
||||
* @param {String} name The event name to listen to.
|
||||
* @param {Function} f The function that is executed when the event is fired.
|
||||
*/
|
||||
on (name, f) {
|
||||
let listeners = this._getListener(name)
|
||||
listeners.on.add(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Init the saved state for an event name.
|
||||
*/
|
||||
_initStateListener (name) {
|
||||
let state = this._stateListener.get(name)
|
||||
if (state === undefined) {
|
||||
state = {}
|
||||
state.promise = new Promise(function (resolve) {
|
||||
state.resolve = resolve
|
||||
})
|
||||
this._stateListener.set(name, state)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise that is resolved when the event name is called.
|
||||
* The Promise is immediately resolved when the event name was called in the
|
||||
* past.
|
||||
*/
|
||||
when (name) {
|
||||
return this._initStateListener(name).promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener that was registered with either
|
||||
* {@link EventHandler#on} or {@link EventHandler#once}.
|
||||
*/
|
||||
off (name, f) {
|
||||
if (name == null || f == null) {
|
||||
throw new Error('You must specify event name and function!')
|
||||
}
|
||||
const listener = this._eventListener.get(name)
|
||||
if (listener !== undefined) {
|
||||
listener.on.delete(f)
|
||||
listener.once.delete(f)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a named event. All registered event listeners that listen to the
|
||||
* specified name will receive the event.
|
||||
*
|
||||
* @param {String} name The event name.
|
||||
* @param {Array} args The arguments that are applied to the event listener.
|
||||
*/
|
||||
emit (name, ...args) {
|
||||
this._initStateListener(name).resolve()
|
||||
const listener = this._eventListener.get(name)
|
||||
if (listener !== undefined) {
|
||||
listener.on.forEach(f => f.apply(null, args))
|
||||
listener.once.forEach(f => f.apply(null, args))
|
||||
listener.once = new Set()
|
||||
} else if (name === 'error') {
|
||||
console.error(args[0])
|
||||
}
|
||||
}
|
||||
destroy () {
|
||||
this._eventListener = null
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
|
||||
class YObject
|
||||
|
||||
constructor: (@_object = {})->
|
||||
if @_object.constructor is Object
|
||||
for name, val of @_object
|
||||
if val.constructor is Object
|
||||
@_object[name] = new YObject(val)
|
||||
else
|
||||
throw new Error "Y.Object accepts Json Objects only"
|
||||
|
||||
_name: "Object"
|
||||
|
||||
_getModel: (types, ops)->
|
||||
if not @_model?
|
||||
@_model = new ops.MapManager(@).execute()
|
||||
for n,o of @_object
|
||||
@_model.val n, o
|
||||
delete @_object
|
||||
@_model
|
||||
|
||||
_setModel: (@_model)->
|
||||
delete @_object
|
||||
|
||||
observe: (f)->
|
||||
@_model.observe f
|
||||
@
|
||||
|
||||
unobserve: (f)->
|
||||
@_model.unobserve f
|
||||
@
|
||||
|
||||
#
|
||||
# @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 [*] Depends on the value of the property.
|
||||
#
|
||||
# @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 @_model?
|
||||
@_model.val.apply @_model, arguments
|
||||
else
|
||||
if content?
|
||||
@_object[name] = content
|
||||
else if name?
|
||||
@_object[name]
|
||||
else
|
||||
res = {}
|
||||
for n,v of @_object
|
||||
res[n] = v
|
||||
res
|
||||
|
||||
delete: (name)->
|
||||
@_model.delete(name)
|
||||
@
|
||||
|
||||
if window?
|
||||
if window.Y?
|
||||
window.Y.Object = YObject
|
||||
else
|
||||
throw new Error "You must first import Y!"
|
||||
|
||||
if module?
|
||||
module.exports = YObject
|
||||
@@ -1,678 +0,0 @@
|
||||
module.exports = ()->
|
||||
# @see Engine.parse
|
||||
ops = {}
|
||||
execution_listener = []
|
||||
|
||||
#
|
||||
# @private
|
||||
# @abstract
|
||||
# @nodoc
|
||||
# A generic interface to ops.
|
||||
#
|
||||
# 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 ops.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: (custom_type, uid, content, content_operations)->
|
||||
if custom_type?
|
||||
@custom_type = custom_type
|
||||
@is_deleted = false
|
||||
@garbage_collected = false
|
||||
@event_listeners = [] # TODO: rename to observers or sth like that
|
||||
if uid?
|
||||
@uid = uid
|
||||
|
||||
# see encode to see, why we are doing it this way
|
||||
if content is undefined
|
||||
# nop
|
||||
else if content? and content.creator?
|
||||
@saveOperation 'content', content
|
||||
else
|
||||
@content = content
|
||||
if content_operations?
|
||||
@content_operations = {}
|
||||
for name, op of content_operations
|
||||
@saveOperation name, op, 'content_operations'
|
||||
|
||||
type: "Operation"
|
||||
|
||||
getContent: (name)->
|
||||
if @content?
|
||||
if @content.getCustomType?
|
||||
@content.getCustomType()
|
||||
else if @content.constructor is Object
|
||||
if name?
|
||||
if @content[name]?
|
||||
@content[name]
|
||||
else
|
||||
@content_operations[name].getCustomType()
|
||||
else
|
||||
content = {}
|
||||
for n,v of @content
|
||||
content[n] = v
|
||||
if @content_operations?
|
||||
for n,v of @content_operations
|
||||
v = v.getCustomType()
|
||||
content[n] = v
|
||||
content
|
||||
else
|
||||
@content
|
||||
else
|
||||
@content
|
||||
|
||||
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 ops.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: ()->
|
||||
if @custom_type?
|
||||
callon = @getCustomType()
|
||||
else
|
||||
callon = @
|
||||
@forwardEvent callon, 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
|
||||
else
|
||||
undefined
|
||||
|
||||
cloneUid: ()->
|
||||
uid = {}
|
||||
for n,v of @getUid()
|
||||
uid[n] = v
|
||||
uid
|
||||
|
||||
#
|
||||
# @private
|
||||
# If not already done, set the uid
|
||||
# Add this to the HB
|
||||
# Notify the all the listeners.
|
||||
#
|
||||
execute: ()->
|
||||
if @validateSavedOperations()
|
||||
@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()
|
||||
@
|
||||
else
|
||||
false
|
||||
|
||||
#
|
||||
# @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, base = "this")->
|
||||
if op? and op._getModel?
|
||||
op = op._getModel(@custom_types, @operations)
|
||||
#
|
||||
# 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 not op?
|
||||
# nop
|
||||
else if op.execute? or not (op.op_number? and op.creator?)
|
||||
# is instantiated, or op is string. Currently "Delimiter" is saved as string
|
||||
# (in combination with @parent you can retrieve the delimiter..)
|
||||
if base is "this"
|
||||
@[name] = op
|
||||
else
|
||||
dest = @[base]
|
||||
paths = name.split("/")
|
||||
last_path = paths.pop()
|
||||
for path in paths
|
||||
dest = dest[path]
|
||||
dest[last_path] = op
|
||||
else
|
||||
# not initialized. Do it when calling $validateSavedOperations()
|
||||
@unchecked ?= {}
|
||||
@unchecked[base] ?= {}
|
||||
@unchecked[base][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 = true
|
||||
for base_name, base of @unchecked
|
||||
for name, op_uid of base
|
||||
op = @HB.getOperation op_uid
|
||||
if op
|
||||
if base_name is "this"
|
||||
@[name] = op
|
||||
else
|
||||
dest = @[base_name]
|
||||
paths = name.split("/")
|
||||
last_path = paths.pop()
|
||||
for path in paths
|
||||
dest = dest[path]
|
||||
dest[last_path] = op
|
||||
else
|
||||
uninstantiated[base_name] ?= {}
|
||||
uninstantiated[base_name][name] = op_uid
|
||||
success = false
|
||||
if not success
|
||||
@unchecked = uninstantiated
|
||||
return false
|
||||
else
|
||||
delete @unchecked
|
||||
return @
|
||||
|
||||
getCustomType: ()->
|
||||
if not @custom_type?
|
||||
# throw new Error "This operation was not initialized with a custom type"
|
||||
@
|
||||
else
|
||||
if @custom_type.constructor is String
|
||||
# has not been initialized yet (only the name is specified)
|
||||
Type = @custom_types
|
||||
for t in @custom_type.split(".")
|
||||
Type = Type[t]
|
||||
@custom_type = new Type()
|
||||
@custom_type._setModel @
|
||||
@custom_type
|
||||
|
||||
#
|
||||
# @private
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: (json = {})->
|
||||
json.type = @type
|
||||
json.uid = @getUid()
|
||||
if @custom_type?
|
||||
if @custom_type.constructor is String
|
||||
json.custom_type = @custom_type
|
||||
else
|
||||
json.custom_type = @custom_type._name
|
||||
|
||||
if @content?.getUid?
|
||||
json.content = @content.getUid()
|
||||
else
|
||||
json.content = @content
|
||||
if @content_operations?
|
||||
operations = {}
|
||||
for n,o of @content_operations
|
||||
if o._getModel?
|
||||
o = o._getModel(@custom_types, @operations)
|
||||
operations[n] = o.getUid()
|
||||
json.content_operations = operations
|
||||
json
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# A simple Delete-type operation that deletes an operation.
|
||||
#
|
||||
class ops.Delete extends ops.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: (custom_type, uid, deletes)->
|
||||
@saveOperation 'deletes', deletes
|
||||
super custom_type, 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.
|
||||
#
|
||||
ops.Delete.parse = (o)->
|
||||
{
|
||||
'uid' : uid
|
||||
'deletes': deletes_uid
|
||||
} = o
|
||||
new this(null, 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 (unimplemented, good idea?)
|
||||
# - The complete-list (abbrev. cl) maintains all operations
|
||||
#
|
||||
class ops.Insert extends ops.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: (custom_type, content, content_operations, parent, uid, prev_cl, next_cl, origin)->
|
||||
@saveOperation 'parent', parent
|
||||
@saveOperation 'prev_cl', prev_cl
|
||||
@saveOperation 'next_cl', next_cl
|
||||
if origin?
|
||||
@saveOperation 'origin', origin
|
||||
else
|
||||
@saveOperation 'origin', prev_cl
|
||||
super custom_type, uid, content, content_operations
|
||||
|
||||
type: "Insert"
|
||||
|
||||
val: ()->
|
||||
@getContent()
|
||||
|
||||
getNext: (i=1)->
|
||||
n = @
|
||||
while i > 0 and n.next_cl?
|
||||
n = n.next_cl
|
||||
if not n.is_deleted
|
||||
i--
|
||||
if n.is_deleted
|
||||
null
|
||||
n
|
||||
|
||||
getPrev: (i=1)->
|
||||
n = @
|
||||
while i > 0 and n.prev_cl?
|
||||
n = n.prev_cl
|
||||
if not n.is_deleted
|
||||
i--
|
||||
if n.is_deleted
|
||||
null
|
||||
else
|
||||
n
|
||||
|
||||
#
|
||||
# set content to null and other stuff
|
||||
# @private
|
||||
#
|
||||
applyDelete: (o)->
|
||||
@deleted_by ?= []
|
||||
callLater = false
|
||||
if @parent? and not @is_deleted 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
|
||||
@parent.callOperationSpecificDeleteEvents(this, o)
|
||||
if @prev_cl? and @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
|
||||
|
||||
# delete content
|
||||
# - we must not do this in applyDelete, because this would lead to inconsistencies
|
||||
# (e.g. the following operation order must be invertible :
|
||||
# Insert refers to content, then the content is deleted)
|
||||
# Therefore, we have to do this in the cleanup
|
||||
# * NODE: We never delete Insertions!
|
||||
if @content instanceof ops.Operation and not (@content instanceof ops.Insert)
|
||||
@content.referenced_by--
|
||||
if @content.referenced_by <= 0 and not @content.is_deleted
|
||||
@content.applyDelete()
|
||||
delete @content
|
||||
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 @content instanceof ops.Operation
|
||||
@content.insert_parent = @ # TODO: this is probably not necessary and only nice for debugging
|
||||
@content.referenced_by ?= 0
|
||||
@content.referenced_by++
|
||||
if @parent?
|
||||
if not @prev_cl?
|
||||
@prev_cl = @parent.beginning
|
||||
if not @origin?
|
||||
@origin = @prev_cl
|
||||
else if @origin is "Delimiter"
|
||||
@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
|
||||
@parent.callOperationSpecificInsertEvents(this)
|
||||
@
|
||||
|
||||
#
|
||||
# Compute the position of this operation.
|
||||
#
|
||||
getPosition: ()->
|
||||
position = 0
|
||||
prev = @prev_cl
|
||||
while true
|
||||
if prev instanceof ops.Delimiter
|
||||
break
|
||||
if not prev.isDeleted()
|
||||
position++
|
||||
prev = prev.prev_cl
|
||||
position
|
||||
|
||||
#
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be send to other clients.
|
||||
#
|
||||
_encode: (json = {})->
|
||||
json.prev = @prev_cl.getUid()
|
||||
json.next = @next_cl.getUid()
|
||||
|
||||
if @origin.type is "Delimiter"
|
||||
json.origin = "Delimiter"
|
||||
else if @origin isnt @prev_cl
|
||||
json.origin = @origin.getUid()
|
||||
|
||||
# if not (json.prev? and json.next?)
|
||||
json.parent = @parent.getUid()
|
||||
|
||||
super json
|
||||
|
||||
ops.Insert.parse = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'content_operations' : content_operations
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
'parent' : parent
|
||||
} = json
|
||||
new this null, content, content_operations, parent, uid, prev, next, origin
|
||||
|
||||
#
|
||||
# @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 ops.Delimiter extends ops.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 null, {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()
|
||||
}
|
||||
|
||||
ops.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
|
||||
{
|
||||
'operations' : ops
|
||||
'execution_listener' : execution_listener
|
||||
}
|
||||
@@ -1,533 +0,0 @@
|
||||
basic_ops_uninitialized = require "./Basic"
|
||||
|
||||
module.exports = ()->
|
||||
basic_ops = basic_ops_uninitialized()
|
||||
ops = basic_ops.operations
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Manages map like objects. E.g. Json-Type and XML attributes.
|
||||
#
|
||||
class ops.MapManager extends ops.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
#
|
||||
constructor: (custom_type, uid, content, content_operations)->
|
||||
@_map = {}
|
||||
super custom_type, uid, content, content_operations
|
||||
|
||||
type: "MapManager"
|
||||
|
||||
applyDelete: ()->
|
||||
for name,p of @_map
|
||||
p.applyDelete()
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
map: (f)->
|
||||
for n,v of @_map
|
||||
f(n,v)
|
||||
undefined
|
||||
|
||||
#
|
||||
# @see JsonOperations.val
|
||||
#
|
||||
val: (name, content)->
|
||||
if arguments.length > 1
|
||||
if content? and content._getModel?
|
||||
rep = content._getModel(@custom_types, @operations)
|
||||
else
|
||||
rep = content
|
||||
@retrieveSub(name).replace rep
|
||||
@getCustomType()
|
||||
else if name?
|
||||
prop = @_map[name]
|
||||
if prop? and not prop.isContentDeleted()
|
||||
res = prop.val()
|
||||
if res instanceof ops.Operation
|
||||
res.getCustomType()
|
||||
else
|
||||
res
|
||||
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 ops.ReplaceManager null, 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]
|
||||
|
||||
ops.MapManager.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'custom_type' : custom_type
|
||||
'content' : content
|
||||
'content_operations' : content_operations
|
||||
} = json
|
||||
new this(custom_type, uid, content, content_operations)
|
||||
|
||||
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Manages a list of Insert-type operations.
|
||||
#
|
||||
class ops.ListManager extends ops.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: (custom_type, uid, content, content_operations)->
|
||||
@beginning = new ops.Delimiter undefined, undefined
|
||||
@end = new ops.Delimiter @beginning, undefined
|
||||
@beginning.next_cl = @end
|
||||
@beginning.execute()
|
||||
@end.execute()
|
||||
super custom_type, uid, content, content_operations
|
||||
|
||||
type: "ListManager"
|
||||
|
||||
|
||||
applyDelete: ()->
|
||||
o = @beginning
|
||||
while o?
|
||||
o.applyDelete()
|
||||
o = o.next_cl
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
|
||||
toJson: (transform_to_value = false)->
|
||||
val = @val()
|
||||
for i, o in val
|
||||
if o instanceof ops.Object
|
||||
o.toJson(transform_to_value)
|
||||
else if o instanceof ops.ListManager
|
||||
o.toJson(transform_to_value)
|
||||
else if transform_to_value and o instanceof ops.Operation
|
||||
o.val()
|
||||
else
|
||||
o
|
||||
|
||||
#
|
||||
# @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
|
||||
if not o.is_deleted
|
||||
result.push o.val()
|
||||
o = o.next_cl
|
||||
result
|
||||
|
||||
map: (f)->
|
||||
o = @beginning.next_cl
|
||||
result = []
|
||||
while o isnt @end
|
||||
if not o.is_deleted
|
||||
result.push f(o)
|
||||
o = o.next_cl
|
||||
result
|
||||
|
||||
fold: (init, f)->
|
||||
o = @beginning.next_cl
|
||||
while o isnt @end
|
||||
if not o.is_deleted
|
||||
init = f(init, o)
|
||||
o = o.next_cl
|
||||
init
|
||||
|
||||
val: (pos)->
|
||||
if pos?
|
||||
o = @getOperationByPosition(pos+1)
|
||||
if not (o instanceof ops.Delimiter)
|
||||
o.val()
|
||||
else
|
||||
throw new Error "this position does not exist"
|
||||
else
|
||||
@toArray()
|
||||
|
||||
ref: (pos)->
|
||||
if pos?
|
||||
o = @getOperationByPosition(pos+1)
|
||||
if not (o instanceof ops.Delimiter)
|
||||
o
|
||||
else
|
||||
null
|
||||
# throw new Error "this position does not exist"
|
||||
else
|
||||
throw new Error "you must specify a position parameter"
|
||||
|
||||
#
|
||||
# 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 ops.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
|
||||
|
||||
push: (content)->
|
||||
@insertAfter @end.prev_cl, [content]
|
||||
|
||||
insertAfter: (left, contents)->
|
||||
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
|
||||
|
||||
# TODO: always expect an array as content. Then you can combine this with the other option (else)
|
||||
if contents instanceof ops.Operation
|
||||
(new ops.Insert null, content, null, undefined, undefined, left, right).execute()
|
||||
else
|
||||
for c in contents
|
||||
if c? and c._name? and c._getModel?
|
||||
c = c._getModel(@custom_types, @operations)
|
||||
tmp = (new ops.Insert null, c, null, undefined, undefined, left, right).execute()
|
||||
left = tmp
|
||||
@
|
||||
|
||||
#
|
||||
# Inserts an array of content into this list.
|
||||
# @Note: This expects an array as content!
|
||||
#
|
||||
# @return {ListManager Type} This String object.
|
||||
#
|
||||
insert: (position, contents)->
|
||||
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, contents
|
||||
|
||||
#
|
||||
# Deletes a part of the word.
|
||||
#
|
||||
# @return {ListManager Type} This String object
|
||||
#
|
||||
delete: (position, length = 1)->
|
||||
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 ops.Delimiter
|
||||
break
|
||||
d = (new ops.Delete null, undefined, o).execute()
|
||||
o = o.next_cl
|
||||
while (not (o instanceof ops.Delimiter)) and o.isDeleted()
|
||||
o = o.next_cl
|
||||
delete_ops.push d._encode()
|
||||
@
|
||||
|
||||
|
||||
callOperationSpecificInsertEvents: (op)->
|
||||
getContentType = (content)->
|
||||
if content instanceof ops.Operation
|
||||
content.getCustomType()
|
||||
else
|
||||
content
|
||||
@callEvent [
|
||||
type: "insert"
|
||||
reference: op
|
||||
position: op.getPosition()
|
||||
object: @getCustomType()
|
||||
changedBy: op.uid.creator
|
||||
value: getContentType op.val()
|
||||
]
|
||||
|
||||
callOperationSpecificDeleteEvents: (op, del_op)->
|
||||
@callEvent [
|
||||
type: "delete"
|
||||
reference: op
|
||||
position: op.getPosition()
|
||||
object: @getCustomType() # TODO: You can combine getPosition + getParent in a more efficient manner! (only left Delimiter will hold @parent)
|
||||
length: 1
|
||||
changedBy: del_op.uid.creator
|
||||
oldValue: op.val()
|
||||
]
|
||||
|
||||
ops.ListManager.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'custom_type': custom_type
|
||||
'content' : content
|
||||
'content_operations' : content_operations
|
||||
} = json
|
||||
new this(custom_type, uid, content, content_operations)
|
||||
|
||||
class ops.Composition extends ops.ListManager
|
||||
|
||||
constructor: (custom_type, @_composition_value, composition_value_operations, uid, tmp_composition_ref)->
|
||||
# we can't use @seveOperation 'composition_ref', tmp_composition_ref here,
|
||||
# because then there is a "loop" (insertion refers to parent, refers to insertion..)
|
||||
# This is why we have to check in @callOperationSpecificInsertEvents until we find it
|
||||
super custom_type, uid
|
||||
if tmp_composition_ref?
|
||||
@tmp_composition_ref = tmp_composition_ref
|
||||
else
|
||||
@composition_ref = @end.prev_cl
|
||||
if composition_value_operations?
|
||||
@composition_value_operations = {}
|
||||
for n,o of composition_value_operations
|
||||
@saveOperation n, o, '_composition_value'
|
||||
|
||||
type: "Composition"
|
||||
|
||||
#
|
||||
# @private
|
||||
# @see Operation.execute
|
||||
#
|
||||
execute: ()->
|
||||
if @validateSavedOperations()
|
||||
@getCustomType()._setCompositionValue @_composition_value
|
||||
delete @_composition_value
|
||||
# check if tmp_composition_ref already exists
|
||||
if @tmp_composition_ref
|
||||
composition_ref = @HB.getOperation @tmp_composition_ref
|
||||
if composition_ref?
|
||||
delete @tmp_composition_ref
|
||||
@composition_ref = composition_ref
|
||||
super
|
||||
else
|
||||
false
|
||||
|
||||
#
|
||||
# This is called, when the Insert-operation was successfully executed.
|
||||
#
|
||||
callOperationSpecificInsertEvents: (op)->
|
||||
if @tmp_composition_ref?
|
||||
if op.uid.creator is @tmp_composition_ref.creator and op.uid.op_number is @tmp_composition_ref.op_number
|
||||
@composition_ref = op
|
||||
delete @tmp_composition_ref
|
||||
op = op.next_cl
|
||||
if op is @end
|
||||
return
|
||||
else
|
||||
return
|
||||
|
||||
o = @end.prev_cl
|
||||
while o isnt op
|
||||
@getCustomType()._unapply o.undo_delta
|
||||
o = o.prev_cl
|
||||
while o isnt @end
|
||||
o.undo_delta = @getCustomType()._apply o.val()
|
||||
o = o.next_cl
|
||||
@composition_ref = @end.prev_cl
|
||||
|
||||
@callEvent [
|
||||
type: "update"
|
||||
changedBy: op.uid.creator
|
||||
newValue: @val()
|
||||
]
|
||||
|
||||
callOperationSpecificDeleteEvents: (op, del_op)->
|
||||
return
|
||||
|
||||
#
|
||||
# Create a new Delta
|
||||
# - inserts new Content at the end of the list
|
||||
# - updates the composition_value
|
||||
# - updates the composition_ref
|
||||
#
|
||||
# @param delta The delta that is applied to the composition_value
|
||||
#
|
||||
applyDelta: (delta, operations)->
|
||||
(new ops.Insert null, delta, operations, @, null, @end.prev_cl, @end).execute()
|
||||
undefined
|
||||
|
||||
#
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: (json = {})->
|
||||
custom = @getCustomType()._getCompositionValue()
|
||||
json.composition_value = custom.composition_value
|
||||
if custom.composition_value_operations?
|
||||
json.composition_value_operations = {}
|
||||
for n,o of custom.composition_value_operations
|
||||
json.composition_value_operations[n] = o.getUid()
|
||||
if @composition_ref?
|
||||
json.composition_ref = @composition_ref.getUid()
|
||||
else
|
||||
json.composition_ref = @tmp_composition_ref
|
||||
super json
|
||||
|
||||
ops.Composition.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'custom_type': custom_type
|
||||
'composition_value' : composition_value
|
||||
'composition_value_operations' : composition_value_operations
|
||||
'composition_ref' : composition_ref
|
||||
} = json
|
||||
new this(custom_type, composition_value, composition_value_operations, uid, composition_ref)
|
||||
|
||||
|
||||
#
|
||||
# @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 ops.ReplaceManager extends ops.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: (custom_type, @event_properties, @event_this, uid)->
|
||||
if not @event_properties['object']?
|
||||
@event_properties['object'] = @event_this.getCustomType()
|
||||
super custom_type, uid
|
||||
|
||||
type: "ReplaceManager"
|
||||
|
||||
#
|
||||
# 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
|
||||
|
||||
#
|
||||
# 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-ops for ListManager.
|
||||
#
|
||||
callOperationSpecificInsertEvents: (op)->
|
||||
if op.next_cl.type is "Delimiter" and op.prev_cl.type isnt "Delimiter"
|
||||
# this replaces another Replaceable
|
||||
if not op.is_deleted # When this is received from the HB, this could already be deleted!
|
||||
old_value = op.prev_cl.val()
|
||||
@callEventDecorator [
|
||||
type: "update"
|
||||
changedBy: op.uid.creator
|
||||
oldValue: old_value
|
||||
]
|
||||
op.prev_cl.applyDelete()
|
||||
else if op.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
|
||||
op.applyDelete()
|
||||
else # prev _and_ next are Delimiters. This is the first created Replaceable in the RM
|
||||
@callEventDecorator [
|
||||
type: "add"
|
||||
changedBy: op.uid.creator
|
||||
]
|
||||
undefined
|
||||
|
||||
callOperationSpecificDeleteEvents: (op, del_op)->
|
||||
if op.next_cl.type is "Delimiter"
|
||||
@callEventDecorator [
|
||||
type: "delete"
|
||||
changedBy: del_op.uid.creator
|
||||
oldValue: op.val()
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# 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 ops.Insert null, content, null, @, replaceable_uid, o, o.next_cl).execute()
|
||||
# TODO: delete repl (for debugging)
|
||||
undefined
|
||||
|
||||
isContentDeleted: ()->
|
||||
@getLastOperation().isDeleted()
|
||||
|
||||
deleteContent: ()->
|
||||
last_op = @getLastOperation()
|
||||
if (not last_op.isDeleted()) and last_op.type isnt "Delimiter"
|
||||
(new ops.Delete null, undefined, @getLastOperation().uid).execute()
|
||||
undefined
|
||||
|
||||
#
|
||||
# Get the value of this
|
||||
# @return {String}
|
||||
#
|
||||
val: ()->
|
||||
o = @getLastOperation()
|
||||
#if o instanceof ops.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)
|
||||
|
||||
|
||||
|
||||
basic_ops
|
||||
465
lib/Tree.js
Normal file
465
lib/Tree.js
Normal file
@@ -0,0 +1,465 @@
|
||||
|
||||
function rotate (tree, parent, newParent, n) {
|
||||
if (parent === null) {
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === n) {
|
||||
parent.left = newParent
|
||||
} else if (parent.right === n) {
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error('The elements are wrongly connected!')
|
||||
}
|
||||
}
|
||||
|
||||
class N {
|
||||
// A created node is always red!
|
||||
constructor (val) {
|
||||
this.val = val
|
||||
this.color = true
|
||||
this._left = null
|
||||
this._right = null
|
||||
this._parent = null
|
||||
}
|
||||
isRed () { return this.color }
|
||||
isBlack () { return !this.color }
|
||||
redden () { this.color = true; return this }
|
||||
blacken () { this.color = false; return this }
|
||||
get grandparent () {
|
||||
return this.parent.parent
|
||||
}
|
||||
get parent () {
|
||||
return this._parent
|
||||
}
|
||||
get sibling () {
|
||||
return (this === this.parent.left)
|
||||
? this.parent.right : this.parent.left
|
||||
}
|
||||
get left () {
|
||||
return this._left
|
||||
}
|
||||
get right () {
|
||||
return this._right
|
||||
}
|
||||
set left (n) {
|
||||
if (n !== null) {
|
||||
n._parent = this
|
||||
}
|
||||
this._left = n
|
||||
}
|
||||
set right (n) {
|
||||
if (n !== null) {
|
||||
n._parent = this
|
||||
}
|
||||
this._right = n
|
||||
}
|
||||
rotateLeft (tree) {
|
||||
const parent = this.parent
|
||||
const newParent = this.right
|
||||
const newRight = this.right.left
|
||||
newParent.left = this
|
||||
this.right = newRight
|
||||
rotate(tree, parent, newParent, this)
|
||||
}
|
||||
next () {
|
||||
if (this.right !== null) {
|
||||
// search the most left node in the right tree
|
||||
var o = this.right
|
||||
while (o.left !== null) {
|
||||
o = o.left
|
||||
}
|
||||
return o
|
||||
} else {
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.left) {
|
||||
p = p.parent
|
||||
}
|
||||
return p.parent
|
||||
}
|
||||
}
|
||||
prev () {
|
||||
if (this.left !== null) {
|
||||
// search the most right node in the left tree
|
||||
var o = this.left
|
||||
while (o.right !== null) {
|
||||
o = o.right
|
||||
}
|
||||
return o
|
||||
} else {
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.right) {
|
||||
p = p.parent
|
||||
}
|
||||
return p.parent
|
||||
}
|
||||
}
|
||||
rotateRight (tree) {
|
||||
const parent = this.parent
|
||||
const newParent = this.left
|
||||
const newLeft = this.left.right
|
||||
newParent.right = this
|
||||
this.left = newLeft
|
||||
rotate(tree, parent, newParent, this)
|
||||
}
|
||||
getUncle () {
|
||||
// we can assume that grandparent exists when this is called!
|
||||
if (this.parent === this.parent.parent.left) {
|
||||
return this.parent.parent.right
|
||||
} else {
|
||||
return this.parent.parent.left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a Red Black Tree implementation
|
||||
*/
|
||||
export default class Tree {
|
||||
constructor () {
|
||||
this.root = null
|
||||
this.length = 0
|
||||
}
|
||||
findNext (id) {
|
||||
var nextID = id.clone()
|
||||
nextID.clock += 1
|
||||
return this.findWithLowerBound(nextID)
|
||||
}
|
||||
findPrev (id) {
|
||||
let prevID = id.clone()
|
||||
prevID.clock -= 1
|
||||
return this.findWithUpperBound(prevID)
|
||||
}
|
||||
findNodeWithLowerBound (from) {
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return null
|
||||
} else {
|
||||
while (true) {
|
||||
if (from === null || (from.lessThan(o.val._id) && o.left !== null)) {
|
||||
// o is included in the bound
|
||||
// try to find an element that is closer to the bound
|
||||
o = o.left
|
||||
} else if (from !== null && o.val._id.lessThan(from)) {
|
||||
// o is not within the bound, maybe one of the right elements is..
|
||||
if (o.right !== null) {
|
||||
o = o.right
|
||||
} else {
|
||||
// there is no right element. Search for the next bigger element,
|
||||
// this should be within the bounds
|
||||
return o.next()
|
||||
}
|
||||
} else {
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
findNodeWithUpperBound (to) {
|
||||
if (to === void 0) {
|
||||
throw new Error('You must define from!')
|
||||
}
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return null
|
||||
} else {
|
||||
while (true) {
|
||||
if ((to === null || o.val._id.lessThan(to)) && o.right !== null) {
|
||||
// o is included in the bound
|
||||
// try to find an element that is closer to the bound
|
||||
o = o.right
|
||||
} else if (to !== null && to.lessThan(o.val._id)) {
|
||||
// o is not within the bound, maybe one of the left elements is..
|
||||
if (o.left !== null) {
|
||||
o = o.left
|
||||
} else {
|
||||
// there is no left element. Search for the prev smaller element,
|
||||
// this should be within the bounds
|
||||
return o.prev()
|
||||
}
|
||||
} else {
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
findSmallestNode () {
|
||||
var o = this.root
|
||||
while (o != null && o.left != null) {
|
||||
o = o.left
|
||||
}
|
||||
return o
|
||||
}
|
||||
findWithLowerBound (from) {
|
||||
var n = this.findNodeWithLowerBound(from)
|
||||
return n == null ? null : n.val
|
||||
}
|
||||
findWithUpperBound (to) {
|
||||
var n = this.findNodeWithUpperBound(to)
|
||||
return n == null ? null : n.val
|
||||
}
|
||||
iterate (from, to, f) {
|
||||
var o
|
||||
if (from === null) {
|
||||
o = this.findSmallestNode()
|
||||
} else {
|
||||
o = this.findNodeWithLowerBound(from)
|
||||
}
|
||||
while (
|
||||
o !== null &&
|
||||
(
|
||||
to === null || // eslint-disable-line no-unmodified-loop-condition
|
||||
o.val._id.lessThan(to) ||
|
||||
o.val._id.equals(to)
|
||||
)
|
||||
) {
|
||||
f(o.val)
|
||||
o = o.next()
|
||||
}
|
||||
}
|
||||
find (id) {
|
||||
let n = this.findNode(id)
|
||||
if (n !== null) {
|
||||
return n.val
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
findNode (id) {
|
||||
var o = this.root
|
||||
if (o === null) {
|
||||
return null
|
||||
} else {
|
||||
while (true) {
|
||||
if (o === null) {
|
||||
return null
|
||||
}
|
||||
if (id.lessThan(o.val._id)) {
|
||||
o = o.left
|
||||
} else if (o.val._id.lessThan(id)) {
|
||||
o = o.right
|
||||
} else {
|
||||
return o
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete (id) {
|
||||
var d = this.findNode(id)
|
||||
if (d == null) {
|
||||
// throw new Error('Element does not exist!')
|
||||
return
|
||||
}
|
||||
this.length--
|
||||
if (d.left !== null && d.right !== null) {
|
||||
// switch d with the greates element in the left subtree.
|
||||
// o should have at most one child.
|
||||
var o = d.left
|
||||
// find
|
||||
while (o.right !== null) {
|
||||
o = o.right
|
||||
}
|
||||
// switch
|
||||
d.val = o.val
|
||||
d = o
|
||||
}
|
||||
// d has at most one child
|
||||
// let n be the node that replaces d
|
||||
var isFakeChild
|
||||
var child = d.left || d.right
|
||||
if (child === null) {
|
||||
isFakeChild = true
|
||||
child = new N(null)
|
||||
child.blacken()
|
||||
d.right = child
|
||||
} else {
|
||||
isFakeChild = false
|
||||
}
|
||||
|
||||
if (d.parent === null) {
|
||||
if (!isFakeChild) {
|
||||
this.root = child
|
||||
child.blacken()
|
||||
child._parent = null
|
||||
} else {
|
||||
this.root = null
|
||||
}
|
||||
return
|
||||
} else if (d.parent.left === d) {
|
||||
d.parent.left = child
|
||||
} else if (d.parent.right === d) {
|
||||
d.parent.right = child
|
||||
} else {
|
||||
throw new Error('Impossible!')
|
||||
}
|
||||
if (d.isBlack()) {
|
||||
if (child.isRed()) {
|
||||
child.blacken()
|
||||
} else {
|
||||
this._fixDelete(child)
|
||||
}
|
||||
}
|
||||
this.root.blacken()
|
||||
if (isFakeChild) {
|
||||
if (child.parent.left === child) {
|
||||
child.parent.left = null
|
||||
} else if (child.parent.right === child) {
|
||||
child.parent.right = null
|
||||
} else {
|
||||
throw new Error('Impossible #3')
|
||||
}
|
||||
}
|
||||
}
|
||||
_fixDelete (n) {
|
||||
function isBlack (node) {
|
||||
return node !== null ? node.isBlack() : true
|
||||
}
|
||||
function isRed (node) {
|
||||
return node !== null ? node.isRed() : false
|
||||
}
|
||||
if (n.parent === null) {
|
||||
// this can only be called after the first iteration of fixDelete.
|
||||
return
|
||||
}
|
||||
// d was already replaced by the child
|
||||
// d is not the root
|
||||
// d and child are black
|
||||
var sibling = n.sibling
|
||||
if (isRed(sibling)) {
|
||||
// make sibling the grandfather
|
||||
n.parent.redden()
|
||||
sibling.blacken()
|
||||
if (n === n.parent.left) {
|
||||
n.parent.rotateLeft(this)
|
||||
} else if (n === n.parent.right) {
|
||||
n.parent.rotateRight(this)
|
||||
} else {
|
||||
throw new Error('Impossible #2')
|
||||
}
|
||||
sibling = n.sibling
|
||||
}
|
||||
// parent, sibling, and children of n are black
|
||||
if (n.parent.isBlack() &&
|
||||
sibling.isBlack() &&
|
||||
isBlack(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden()
|
||||
this._fixDelete(n.parent)
|
||||
} else if (n.parent.isRed() &&
|
||||
sibling.isBlack() &&
|
||||
isBlack(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden()
|
||||
n.parent.blacken()
|
||||
} else {
|
||||
if (n === n.parent.left &&
|
||||
sibling.isBlack() &&
|
||||
isRed(sibling.left) &&
|
||||
isBlack(sibling.right)
|
||||
) {
|
||||
sibling.redden()
|
||||
sibling.left.blacken()
|
||||
sibling.rotateRight(this)
|
||||
sibling = n.sibling
|
||||
} else if (n === n.parent.right &&
|
||||
sibling.isBlack() &&
|
||||
isRed(sibling.right) &&
|
||||
isBlack(sibling.left)
|
||||
) {
|
||||
sibling.redden()
|
||||
sibling.right.blacken()
|
||||
sibling.rotateLeft(this)
|
||||
sibling = n.sibling
|
||||
}
|
||||
sibling.color = n.parent.color
|
||||
n.parent.blacken()
|
||||
if (n === n.parent.left) {
|
||||
sibling.right.blacken()
|
||||
n.parent.rotateLeft(this)
|
||||
} else {
|
||||
sibling.left.blacken()
|
||||
n.parent.rotateRight(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
put (v) {
|
||||
var node = new N(v)
|
||||
if (this.root !== null) {
|
||||
var p = this.root // p abbrev. parent
|
||||
while (true) {
|
||||
if (node.val._id.lessThan(p.val._id)) {
|
||||
if (p.left === null) {
|
||||
p.left = node
|
||||
break
|
||||
} else {
|
||||
p = p.left
|
||||
}
|
||||
} else if (p.val._id.lessThan(node.val._id)) {
|
||||
if (p.right === null) {
|
||||
p.right = node
|
||||
break
|
||||
} else {
|
||||
p = p.right
|
||||
}
|
||||
} else {
|
||||
p.val = node.val
|
||||
return p
|
||||
}
|
||||
}
|
||||
this._fixInsert(node)
|
||||
} else {
|
||||
this.root = node
|
||||
}
|
||||
this.length++
|
||||
this.root.blacken()
|
||||
return node
|
||||
}
|
||||
_fixInsert (n) {
|
||||
if (n.parent === null) {
|
||||
n.blacken()
|
||||
return
|
||||
} else if (n.parent.isBlack()) {
|
||||
return
|
||||
}
|
||||
var uncle = n.getUncle()
|
||||
if (uncle !== null && uncle.isRed()) {
|
||||
// Note: parent: red, uncle: red
|
||||
n.parent.blacken()
|
||||
uncle.blacken()
|
||||
n.grandparent.redden()
|
||||
this._fixInsert(n.grandparent)
|
||||
} else {
|
||||
// Note: parent: red, uncle: black or null
|
||||
// Now we transform the tree in such a way that
|
||||
// either of these holds:
|
||||
// 1) grandparent.left.isRed
|
||||
// and grandparent.left.left.isRed
|
||||
// 2) grandparent.right.isRed
|
||||
// and grandparent.right.right.isRed
|
||||
if (n === n.parent.right && n.parent === n.grandparent.left) {
|
||||
n.parent.rotateLeft(this)
|
||||
// Since we rotated and want to use the previous
|
||||
// cases, we need to set n in such a way that
|
||||
// n.parent.isRed again
|
||||
n = n.left
|
||||
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
|
||||
n.parent.rotateRight(this)
|
||||
// see above
|
||||
n = n.right
|
||||
}
|
||||
// Case 1) or 2) hold from here on.
|
||||
// Now traverse grandparent, make parent a black node
|
||||
// on the highest level which holds two red nodes.
|
||||
n.parent.blacken()
|
||||
n.grandparent.redden()
|
||||
if (n === n.parent.left) {
|
||||
// Case 1
|
||||
n.grandparent.rotateRight(this)
|
||||
} else {
|
||||
// Case 2
|
||||
n.grandparent.rotateLeft(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
lib/binary.js
Normal file
7
lib/binary.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
export const BITS32 = 0xFFFFFFFF
|
||||
export const BITS21 = (1 << 21) - 1
|
||||
export const BITS16 = (1 << 16) - 1
|
||||
|
||||
export const BIT26 = 1 << 26
|
||||
export const BIT32 = 1 << 32
|
||||
168
lib/decoding.js
Normal file
168
lib/decoding.js
Normal file
@@ -0,0 +1,168 @@
|
||||
|
||||
/* global Buffer */
|
||||
|
||||
import * as globals from './globals.js'
|
||||
|
||||
/**
|
||||
* A Decoder handles the decoding of an ArrayBuffer.
|
||||
*/
|
||||
export class Decoder {
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer Binary data to decode
|
||||
*/
|
||||
constructor (buffer) {
|
||||
this.arr = new Uint8Array(buffer)
|
||||
this.pos = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @return {Decoder}
|
||||
*/
|
||||
export const createDecoder = buffer => new Decoder(buffer)
|
||||
|
||||
export const hasContent = decoder => decoder.pos !== decoder.arr.length
|
||||
|
||||
/**
|
||||
* Clone a decoder instance.
|
||||
* Optionally set a new position parameter.
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @return {Decoder} A clone of `decoder`
|
||||
*/
|
||||
export const clone = (decoder, newPos = decoder.pos) => {
|
||||
let _decoder = createDecoder(decoder.arr.buffer)
|
||||
_decoder.pos = newPos
|
||||
return _decoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `len` bytes as an ArrayBuffer.
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @param {number} len The length of bytes to read
|
||||
* @return {ArrayBuffer}
|
||||
*/
|
||||
export const readArrayBuffer = (decoder, len) => {
|
||||
const arrayBuffer = globals.createUint8ArrayFromLen(len)
|
||||
const view = globals.createUint8ArrayFromBuffer(decoder.arr.buffer, decoder.pos, len)
|
||||
arrayBuffer.set(view)
|
||||
decoder.pos += len
|
||||
return arrayBuffer.buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Read variable length payload as ArrayBuffer
|
||||
* @param {Decoder} decoder
|
||||
* @return {ArrayBuffer}
|
||||
*/
|
||||
export const readPayload = decoder => readArrayBuffer(decoder, readVarUint(decoder))
|
||||
|
||||
/**
|
||||
* Read the rest of the content as an ArrayBuffer
|
||||
* @param {Decoder} decoder
|
||||
* @return {ArrayBuffer}
|
||||
*/
|
||||
export const readTail = decoder => readArrayBuffer(decoder, decoder.arr.length - decoder.pos)
|
||||
|
||||
/**
|
||||
* Skip one byte, jump to the next position.
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @return {number} The next position
|
||||
*/
|
||||
export const skip8 = decoder => decoder.pos++
|
||||
|
||||
/**
|
||||
* Read one byte as unsigned integer.
|
||||
* @param {Decoder} decoder The decoder instance
|
||||
* @return {number} Unsigned 8-bit integer
|
||||
*/
|
||||
export const readUint8 = decoder => decoder.arr[decoder.pos++]
|
||||
|
||||
/**
|
||||
* Read 4 bytes as unsigned integer.
|
||||
*
|
||||
* @param {Decoder} decoder
|
||||
* @return {number} An unsigned integer.
|
||||
*/
|
||||
export const readUint32 = decoder => {
|
||||
let uint =
|
||||
decoder.arr[decoder.pos] +
|
||||
(decoder.arr[decoder.pos + 1] << 8) +
|
||||
(decoder.arr[decoder.pos + 2] << 16) +
|
||||
(decoder.arr[decoder.pos + 3] << 24)
|
||||
decoder.pos += 4
|
||||
return uint
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead without incrementing position.
|
||||
* to the next byte and read it as unsigned integer.
|
||||
*
|
||||
* @param {Decoder} decoder
|
||||
* @return {number} An unsigned integer.
|
||||
*/
|
||||
export const peekUint8 = decoder => decoder.arr[decoder.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 bytlength
|
||||
* * numbers < 2^14 is stored in two bylength
|
||||
*
|
||||
* @param {Decoder} decoder
|
||||
* @return {number} An unsigned integer.length
|
||||
*/
|
||||
export const readVarUint = decoder => {
|
||||
let num = 0
|
||||
let len = 0
|
||||
while (true) {
|
||||
let r = decoder.arr[decoder.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
|
||||
*
|
||||
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
|
||||
* when String.fromCodePoint is fed with all characters as arguments.
|
||||
* But most environments have a maximum number of arguments per functions.
|
||||
* For effiency reasons we apply a maximum of 10000 characters at once.
|
||||
*
|
||||
* @param {Decoder} decoder
|
||||
* @return {String} The read String.
|
||||
*/
|
||||
export const readVarString = decoder => {
|
||||
let remainingLen = readVarUint(decoder)
|
||||
let encodedString = ''
|
||||
while (remainingLen > 0) {
|
||||
const nextLen = remainingLen < 10000 ? remainingLen : 10000
|
||||
const bytes = new Array(nextLen)
|
||||
for (let i = 0; i < nextLen; i++) {
|
||||
bytes[i] = decoder.arr[decoder.pos++]
|
||||
}
|
||||
encodedString += String.fromCodePoint.apply(null, bytes)
|
||||
remainingLen -= nextLen
|
||||
}
|
||||
return decodeURIComponent(escape(encodedString))
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead and read varString without incrementing position
|
||||
* @param {Decoder} decoder
|
||||
* @return {string}
|
||||
*/
|
||||
export const peekVarString = decoder => {
|
||||
let pos = decoder.pos
|
||||
let s = readVarString(decoder)
|
||||
decoder.pos = pos
|
||||
return s
|
||||
}
|
||||
218
lib/encoding.js
Normal file
218
lib/encoding.js
Normal file
@@ -0,0 +1,218 @@
|
||||
|
||||
import * as globals from './globals.js'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
/**
|
||||
* A BinaryEncoder handles the encoding to an ArrayBuffer.
|
||||
*/
|
||||
export class Encoder {
|
||||
constructor () {
|
||||
this.cpos = 0
|
||||
this.cbuf = globals.createUint8ArrayFromLen(1000)
|
||||
this.bufs = []
|
||||
}
|
||||
}
|
||||
|
||||
export const createEncoder = () => new Encoder()
|
||||
|
||||
/**
|
||||
* The current length of the encoded data.
|
||||
*/
|
||||
export const length = encoder => {
|
||||
let len = encoder.cpos
|
||||
for (let i = 0; i < encoder.bufs.length; i++) {
|
||||
len += encoder.bufs[i].length
|
||||
}
|
||||
return len
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ArrayBuffer. TODO: rename to .toArrayBuffer
|
||||
* @param {Encoder} encoder
|
||||
* @return {ArrayBuffer} The created ArrayBuffer.
|
||||
*/
|
||||
export const toBuffer = encoder => {
|
||||
const uint8arr = globals.createUint8ArrayFromLen(length(encoder))
|
||||
let curPos = 0
|
||||
for (let i = 0; i < encoder.bufs.length; i++) {
|
||||
let d = encoder.bufs[i]
|
||||
uint8arr.set(d, curPos)
|
||||
curPos += d.length
|
||||
}
|
||||
uint8arr.set(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos)
|
||||
return uint8arr.buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one byte to the encoder.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} num The byte that is to be encoded.
|
||||
*/
|
||||
export const write = (encoder, num) => {
|
||||
if (encoder.cpos === encoder.cbuf.length) {
|
||||
encoder.bufs.push(encoder.cbuf)
|
||||
encoder.cbuf = globals.createUint8ArrayFromLen(encoder.cbuf.length * 2)
|
||||
encoder.cpos = 0
|
||||
}
|
||||
encoder.cbuf[encoder.cpos++] = num
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one byte at a specific position.
|
||||
* Position must already be written (i.e. encoder.length > pos)
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos Position to which to write data
|
||||
* @param {number} num Unsigned 8-bit integer
|
||||
*/
|
||||
export const set = (encoder, pos, num) => {
|
||||
let buffer = null
|
||||
// iterate all buffers and adjust position
|
||||
for (let i = 0; i < encoder.bufs.length && buffer === null; i++) {
|
||||
const b = encoder.bufs[i]
|
||||
if (pos < b.length) {
|
||||
buffer = b // found buffer
|
||||
} else {
|
||||
pos -= b.length
|
||||
}
|
||||
}
|
||||
if (buffer === null) {
|
||||
// use current buffer
|
||||
buffer = encoder.cbuf
|
||||
}
|
||||
buffer[pos] = num
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one byte as an unsigned integer.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const writeUint8 = (encoder, num) => write(encoder, num & bits8)
|
||||
|
||||
/**
|
||||
* Write one byte as an unsigned Integer at a specific location.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const setUint8 = (encoder, pos, num) => set(encoder, pos, num & bits8)
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const writeUint16 = (encoder, num) => {
|
||||
write(encoder, num & bits8)
|
||||
write(encoder, (num >>> 8) & bits8)
|
||||
}
|
||||
/**
|
||||
* Write two bytes as an unsigned integer at a specific location.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const setUint16 = (encoder, pos, num) => {
|
||||
set(encoder, pos, num & bits8)
|
||||
set(encoder, pos + 1, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const writeUint32 = (encoder, num) => {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
write(encoder, num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer at a specific location.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} pos The location where the data will be written.
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const setUint32 = (encoder, pos, num) => {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
set(encoder, pos + i, num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable length unsigned integer.
|
||||
*
|
||||
* Encodes integers in the range from [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {number} num The number that is to be encoded.
|
||||
*/
|
||||
export const writeVarUint = (encoder, num) => {
|
||||
while (num >= 0b10000000) {
|
||||
write(encoder, 0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
write(encoder, bits7 & num)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable length string.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {String} str The string that is to be encoded.
|
||||
*/
|
||||
export const writeVarString = (encoder, str) => {
|
||||
const encodedString = unescape(encodeURIComponent(str))
|
||||
const len = encodedString.length
|
||||
writeVarUint(encoder, len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
write(encoder, encodedString.codePointAt(i))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the content of another Encoder.
|
||||
*
|
||||
* TODO: can be improved!
|
||||
*
|
||||
* @param {Encoder} encoder The enUint8Arr
|
||||
* @param {Encoder} append The BinaryEncoder to be written.
|
||||
*/
|
||||
export const writeBinaryEncoder = (encoder, append) => writeArrayBuffer(encoder, toBuffer(append))
|
||||
|
||||
/**
|
||||
* Append an arrayBuffer to the encoder.
|
||||
*
|
||||
* @param {Encoder} encoder
|
||||
* @param {ArrayBuffer} arrayBuffer
|
||||
*/
|
||||
export const writeArrayBuffer = (encoder, arrayBuffer) => {
|
||||
const prevBufferLen = encoder.cbuf.length
|
||||
// TODO: Append to cbuf if possible
|
||||
encoder.bufs.push(globals.createUint8ArrayFromBuffer(encoder.cbuf.buffer, 0, encoder.cpos))
|
||||
encoder.bufs.push(globals.createUint8ArrayFromArrayBuffer(arrayBuffer))
|
||||
encoder.cbuf = globals.createUint8ArrayFromLen(prevBufferLen)
|
||||
encoder.cpos = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Encoder} encoder
|
||||
* @param {ArrayBuffer} arrayBuffer
|
||||
*/
|
||||
export const writePayload = (encoder, arrayBuffer) => {
|
||||
writeVarUint(encoder, arrayBuffer.byteLength)
|
||||
writeArrayBuffer(encoder, arrayBuffer)
|
||||
}
|
||||
49
lib/encoding.test.js
Normal file
49
lib/encoding.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as encoding from './encoding.js'
|
||||
|
||||
/**
|
||||
* Check if binary encoding is compatible with golang binary encoding - binary.PutVarUint.
|
||||
*
|
||||
* Result: is compatible up to 32 bit: [0, 4294967295] / [0, 0xffffffff]. (max 32 bit unsigned integer)
|
||||
*/
|
||||
let err = null
|
||||
try {
|
||||
const tests = [
|
||||
{ in: 0, out: [0] },
|
||||
{ in: 1, out: [1] },
|
||||
{ in: 128, out: [128, 1] },
|
||||
{ in: 200, out: [200, 1] },
|
||||
{ in: 32, out: [32] },
|
||||
{ in: 500, out: [244, 3] },
|
||||
{ in: 256, out: [128, 2] },
|
||||
{ in: 700, out: [188, 5] },
|
||||
{ in: 1024, out: [128, 8] },
|
||||
{ in: 1025, out: [129, 8] },
|
||||
{ in: 4048, out: [208, 31] },
|
||||
{ in: 5050, out: [186, 39] },
|
||||
{ in: 1000000, out: [192, 132, 61] },
|
||||
{ in: 34951959, out: [151, 166, 213, 16] },
|
||||
{ in: 2147483646, out: [254, 255, 255, 255, 7] },
|
||||
{ in: 2147483647, out: [255, 255, 255, 255, 7] },
|
||||
{ in: 2147483648, out: [128, 128, 128, 128, 8] },
|
||||
{ in: 2147483700, out: [180, 128, 128, 128, 8] },
|
||||
{ in: 4294967294, out: [254, 255, 255, 255, 15] },
|
||||
{ in: 4294967295, out: [255, 255, 255, 255, 15] }
|
||||
]
|
||||
tests.forEach(test => {
|
||||
const encoder = encoding.createEncoder()
|
||||
encoding.writeVarUint(encoder, test.in)
|
||||
const buffer = new Uint8Array(encoding.toBuffer(encoder))
|
||||
if (buffer.byteLength !== test.out.length) {
|
||||
throw new Error('Length don\'t match!')
|
||||
}
|
||||
for (let j = 0; j < buffer.length; j++) {
|
||||
if (buffer[j] !== test[1][j]) {
|
||||
throw new Error('values don\'t match!')
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
err = error
|
||||
} finally {
|
||||
console.log('YDB Client: Encoding varUint compatiblity test: ', err || 'success!')
|
||||
}
|
||||
63
lib/globals.js
Normal file
63
lib/globals.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
export const Uint8Array_ = Uint8Array
|
||||
|
||||
/**
|
||||
* @param {Array<number>} arr
|
||||
* @return {ArrayBuffer}
|
||||
*/
|
||||
export const createArrayBufferFromArray = arr => new Uint8Array_(arr).buffer
|
||||
|
||||
export const createUint8ArrayFromLen = len => new Uint8Array_(len)
|
||||
|
||||
/**
|
||||
* Create Uint8Array with initial content from buffer
|
||||
*/
|
||||
export const createUint8ArrayFromBuffer = (buffer, byteOffset, length) => new Uint8Array_(buffer, byteOffset, length)
|
||||
|
||||
/**
|
||||
* Create Uint8Array with initial content from buffer
|
||||
*/
|
||||
export const createUint8ArrayFromArrayBuffer = arraybuffer => new Uint8Array_(arraybuffer)
|
||||
export const createArrayFromArrayBuffer = arraybuffer => Array.from(createUint8ArrayFromArrayBuffer(arraybuffer))
|
||||
|
||||
export const createPromise = f => new Promise(f)
|
||||
|
||||
export const createMap = () => new Map()
|
||||
export const createSet = () => new Set()
|
||||
|
||||
/**
|
||||
* `Promise.all` wait for all promises in the array to resolve and return the result
|
||||
* @param {Array<Promise<any>>} arrp
|
||||
* @return {any}
|
||||
*/
|
||||
export const pall = arrp => Promise.all(arrp)
|
||||
export const preject = reason => Promise.reject(reason)
|
||||
export const presolve = res => Promise.resolve(res)
|
||||
|
||||
export const until = (timeout, check) => createPromise((resolve, reject) => {
|
||||
const hasTimeout = timeout > 0
|
||||
const untilInterval = () => {
|
||||
if (check()) {
|
||||
clearInterval(intervalHandle)
|
||||
resolve()
|
||||
} else if (hasTimeout) {
|
||||
timeout -= 10
|
||||
if (timeout < 0) {
|
||||
clearInterval(intervalHandle)
|
||||
reject(error('Timeout'))
|
||||
}
|
||||
}
|
||||
}
|
||||
const intervalHandle = setInterval(untilInterval, 10)
|
||||
})
|
||||
|
||||
export const error = description => new Error(description)
|
||||
|
||||
export const max = (a, b) => a > b ? a : b
|
||||
|
||||
/**
|
||||
* @param {number} t Time to wait
|
||||
* @return {Promise} Promise that is resolved after t ms
|
||||
*/
|
||||
export const wait = t => createPromise(r => setTimeout(r, t))
|
||||
159
lib/idb.js
Normal file
159
lib/idb.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
import * as globals from './globals.js'
|
||||
|
||||
/*
|
||||
* IDB Request to Promise transformer
|
||||
*/
|
||||
export const rtop = request => globals.createPromise((resolve, reject) => {
|
||||
request.onerror = event => reject(new Error(event.target.error))
|
||||
request.onblocked = () => location.reload()
|
||||
request.onsuccess = event => resolve(event.target.result)
|
||||
})
|
||||
|
||||
/**
|
||||
* @return {Promise<IDBDatabase>}
|
||||
*/
|
||||
export const openDB = (name, initDB) => globals.createPromise((resolve, reject) => {
|
||||
let request = indexedDB.open(name)
|
||||
/**
|
||||
* @param {any} event
|
||||
*/
|
||||
request.onupgradeneeded = event => initDB(event.target.result)
|
||||
/**
|
||||
* @param {any} event
|
||||
*/
|
||||
request.onerror = event => reject(new Error(event.target.error))
|
||||
request.onblocked = () => location.reload()
|
||||
/**
|
||||
* @param {any} event
|
||||
*/
|
||||
request.onsuccess = event => {
|
||||
const db = event.target.result
|
||||
db.onversionchange = () => { db.close() }
|
||||
addEventListener('unload', () => db.close())
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
|
||||
export const deleteDB = name => rtop(indexedDB.deleteDatabase(name))
|
||||
|
||||
export const createStores = (db, definitions) => definitions.forEach(d =>
|
||||
db.createObjectStore.apply(db, d)
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {String | number | ArrayBuffer | Date | Array } key
|
||||
* @return {Promise<ArrayBuffer>}
|
||||
*/
|
||||
export const get = (store, key) =>
|
||||
rtop(store.get(key))
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {String | number | ArrayBuffer | Date | IDBKeyRange | Array } key
|
||||
*/
|
||||
export const del = (store, key) =>
|
||||
rtop(store.delete(key))
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {String | number | ArrayBuffer | Date | boolean} item
|
||||
* @param {String | number | ArrayBuffer | Date | Array} [key]
|
||||
*/
|
||||
export const put = (store, item, key) =>
|
||||
rtop(store.put(item, key))
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {String | number | ArrayBuffer | Date | boolean} item
|
||||
* @param {String | number | ArrayBuffer | Date | Array} [key]
|
||||
* @return {Promise<ArrayBuffer>}
|
||||
*/
|
||||
export const add = (store, item, key) =>
|
||||
rtop(store.add(item, key))
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {String | number | ArrayBuffer | Date} item
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
export const addAutoKey = (store, item) =>
|
||||
rtop(store.add(item))
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {IDBKeyRange} [range]
|
||||
*/
|
||||
export const getAll = (store, range) =>
|
||||
rtop(store.getAll(range))
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {IDBKeyRange} [range]
|
||||
*/
|
||||
export const getAllKeys = (store, range) =>
|
||||
rtop(store.getAllKeys(range))
|
||||
|
||||
/**
|
||||
* @typedef KeyValuePair
|
||||
* @type {Object}
|
||||
* @property {any} k key
|
||||
* @property {any} v Value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {IDBKeyRange} [range]
|
||||
* @return {Promise<Array<KeyValuePair>>}
|
||||
*/
|
||||
export const getAllKeysValues = (store, range) =>
|
||||
globals.pall([getAllKeys(store, range), getAll(store, range)]).then(([ks, vs]) => ks.map((k, i) => ({ k, v: vs[i] })))
|
||||
|
||||
/**
|
||||
* Iterate on keys and values
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {IDBKeyRange?} keyrange
|
||||
* @param {function(any, any)} f Return true in order to continue the cursor
|
||||
*/
|
||||
export const iterate = (store, keyrange, f) => globals.createPromise((resolve, reject) => {
|
||||
const request = store.openCursor(keyrange)
|
||||
request.onerror = reject
|
||||
/**
|
||||
* @param {any} event
|
||||
*/
|
||||
request.onsuccess = event => {
|
||||
const cursor = event.target.result
|
||||
if (cursor === null) {
|
||||
return resolve()
|
||||
}
|
||||
f(cursor.value, cursor.key)
|
||||
cursor.continue()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Iterate on the keys (no values)
|
||||
* @param {IDBObjectStore} store
|
||||
* @param {IDBKeyRange} keyrange
|
||||
* @param {function(IDBCursor)} f Call `idbcursor.continue()` to iterate further
|
||||
*/
|
||||
export const iterateKeys = (store, keyrange, f) => {
|
||||
/**
|
||||
* @param {any} event
|
||||
*/
|
||||
store.openKeyCursor(keyrange).onsuccess = event => f(event.target.result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open store from transaction
|
||||
* @param {IDBTransaction} t
|
||||
* @param {String} store
|
||||
* @returns {IDBObjectStore}
|
||||
*/
|
||||
export const getStore = (t, store) => t.objectStore(store)
|
||||
|
||||
export const createIDBKeyRangeBound = (lower, upper, lowerOpen, upperOpen) => IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
|
||||
export const createIDBKeyRangeUpperBound = (upper, upperOpen) => IDBKeyRange.upperBound(upper, upperOpen)
|
||||
export const createIDBKeyRangeLowerBound = (lower, lowerOpen) => IDBKeyRange.lowerBound(lower, lowerOpen)
|
||||
34
lib/idb.test.js
Normal file
34
lib/idb.test.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as test from './test.js'
|
||||
import * as idb from './idb.js'
|
||||
import * as logging from './logging.js'
|
||||
|
||||
const initTestDB = db => idb.createStores(db, [['test']])
|
||||
const testDBName = 'idb-test'
|
||||
|
||||
const createTransaction = db => db.transaction(['test'], 'readwrite')
|
||||
/**
|
||||
* @param {IDBTransaction} t
|
||||
* @return {IDBObjectStore}
|
||||
*/
|
||||
const getStore = t => idb.getStore(t, 'test')
|
||||
|
||||
idb.deleteDB(testDBName).then(() => idb.openDB(testDBName, initTestDB)).then(db => {
|
||||
test.run('idb iteration', async testname => {
|
||||
const t = createTransaction(db)
|
||||
await idb.put(getStore(t), 0, ['t', 0])
|
||||
await idb.put(getStore(t), 1, ['t', 1])
|
||||
const valsGetAll = await idb.getAll(getStore(t))
|
||||
if (valsGetAll.length !== 2) {
|
||||
logging.fail('getAll does not return two values')
|
||||
}
|
||||
const valsIterate = []
|
||||
const keyrange = idb.createIDBKeyRangeBound(['t', 0], ['t', 1], false, false)
|
||||
await idb.put(getStore(t), 2, ['t', 2])
|
||||
await idb.iterate(getStore(t), keyrange, (val, key) => {
|
||||
valsIterate.push(val)
|
||||
})
|
||||
if (valsIterate.length !== 2) {
|
||||
logging.fail('iterate does not return two values')
|
||||
}
|
||||
})
|
||||
})
|
||||
23
lib/logging.js
Normal file
23
lib/logging.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
import * as globals from './globals.js'
|
||||
|
||||
let date = new Date().getTime()
|
||||
|
||||
const writeDate = () => {
|
||||
const oldDate = date
|
||||
date = new Date().getTime()
|
||||
return date - oldDate
|
||||
}
|
||||
|
||||
export const print = (...args) => console.log(...args)
|
||||
export const log = m => print(`%cydb-client: %c${m} %c+${writeDate()}ms`, 'color: blue;', '', 'color: blue')
|
||||
|
||||
export const fail = m => {
|
||||
throw new Error(m)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @return {string}
|
||||
*/
|
||||
export const arrayBufferToString = buffer => JSON.stringify(Array.from(globals.createUint8ArrayFromBuffer(buffer)))
|
||||
2
lib/math.js
Normal file
2
lib/math.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
export const floor = Math.floor
|
||||
32
lib/mutex.js
Normal file
32
lib/mutex.js
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
/**
|
||||
* Creates a mutual exclude function with the following property:
|
||||
*
|
||||
* @example
|
||||
* const mutex = createMutex()
|
||||
* mutex(function () {
|
||||
* // This function is immediately executed
|
||||
* mutex(function () {
|
||||
* // This function is never executed, as it is called with the same
|
||||
* // mutex function
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* @return {Function} A mutual exclude function
|
||||
* @public
|
||||
*/
|
||||
export const createMutex = () => {
|
||||
let token = true
|
||||
return (f, g) => {
|
||||
if (token) {
|
||||
token = false
|
||||
try {
|
||||
f()
|
||||
} finally {
|
||||
token = true
|
||||
}
|
||||
} else if (g !== undefined) {
|
||||
g()
|
||||
}
|
||||
}
|
||||
}
|
||||
2
lib/number.js
Normal file
2
lib/number.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
|
||||
export const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
|
||||
66
lib/random/PRNG/Mt19937.js
Normal file
66
lib/random/PRNG/Mt19937.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const N = 624
|
||||
const M = 397
|
||||
|
||||
function twist (u, v) {
|
||||
return ((((u & 0x80000000) | (v & 0x7fffffff)) >>> 1) ^ ((v & 1) ? 0x9908b0df : 0))
|
||||
}
|
||||
|
||||
function nextState (state) {
|
||||
let p = 0
|
||||
let j
|
||||
for (j = N - M + 1; --j; p++) {
|
||||
state[p] = state[p + M] ^ twist(state[p], state[p + 1])
|
||||
}
|
||||
for (j = M; --j; p++) {
|
||||
state[p] = state[p + M - N] ^ twist(state[p], state[p + 1])
|
||||
}
|
||||
state[p] = state[p + M - N] ^ twist(state[p], state[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a port of Shawn Cokus's implementation of the original Mersenne Twister algorithm (http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/CODES/MTARCOK/mt19937ar-cok.c).
|
||||
* MT has a very high period of 2^19937. Though the authors of xorshift describe that a high period is not
|
||||
* very relevant (http://vigna.di.unimi.it/xorshift/). It is four times slower than xoroshiro128plus and
|
||||
* needs to recompute its state after generating 624 numbers.
|
||||
*
|
||||
* @example
|
||||
* const gen = new Mt19937(new Date().getTime())
|
||||
* console.log(gen.next())
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export default class Mt19937 {
|
||||
/**
|
||||
* @param {Number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
||||
*/
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
const state = new Uint32Array(N)
|
||||
state[0] = seed
|
||||
for (let i = 1; i < N; i++) {
|
||||
state[i] = (Math.imul(1812433253, (state[i - 1] ^ (state[i - 1] >>> 30))) + i) & 0xFFFFFFFF
|
||||
}
|
||||
this._state = state
|
||||
this._i = 0
|
||||
nextState(this._state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random signed integer.
|
||||
*
|
||||
* @return {Number} A 32 bit signed integer.
|
||||
*/
|
||||
next () {
|
||||
if (this._i === N) {
|
||||
// need to compute a new state
|
||||
nextState(this._state)
|
||||
this._i = 0
|
||||
}
|
||||
let y = this._state[this._i++]
|
||||
y ^= (y >>> 11)
|
||||
y ^= (y << 7) & 0x9d2c5680
|
||||
y ^= (y << 15) & 0xefc60000
|
||||
y ^= (y >>> 18)
|
||||
return y
|
||||
}
|
||||
}
|
||||
48
lib/random/PRNG/PRNG.tests.js
Normal file
48
lib/random/PRNG/PRNG.tests.js
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
import Mt19937 from './Mt19937.js'
|
||||
import Xoroshiro128plus from './Xoroshiro128plus.js'
|
||||
import Xorshift32 from './Xorshift32.js'
|
||||
import * as time from '../../time.js'
|
||||
|
||||
const DIAMETER = 300
|
||||
const NUMBERS = 10000
|
||||
|
||||
function runPRNG (name, Gen) {
|
||||
console.log('== ' + name + ' ==')
|
||||
const gen = new Gen(1234)
|
||||
let head = 0
|
||||
let tails = 0
|
||||
const date = time.getUnixTime()
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.height = DIAMETER
|
||||
canvas.width = DIAMETER
|
||||
const ctx = canvas.getContext('2d')
|
||||
const vals = new Set()
|
||||
ctx.fillStyle = 'blue'
|
||||
for (let i = 0; i < NUMBERS; i++) {
|
||||
const n = gen.next() & 0xFFFFFF
|
||||
const x = (gen.next() >>> 0) % DIAMETER
|
||||
const y = (gen.next() >>> 0) % DIAMETER
|
||||
ctx.fillRect(x, y, 1, 2)
|
||||
if ((n & 1) === 1) {
|
||||
head++
|
||||
} else {
|
||||
tails++
|
||||
}
|
||||
if (vals.has(n)) {
|
||||
console.warn(`The generator generated a duplicate`)
|
||||
}
|
||||
vals.add(n)
|
||||
}
|
||||
console.log('time: ', time.getUnixTime() - date)
|
||||
console.log('head:', head, 'tails:', tails)
|
||||
console.log('%c ', `font-size: 200px; background: url(${canvas.toDataURL()}) no-repeat;`)
|
||||
const h1 = document.createElement('h1')
|
||||
h1.insertBefore(document.createTextNode(name), null)
|
||||
document.body.insertBefore(h1, null)
|
||||
document.body.appendChild(canvas)
|
||||
}
|
||||
|
||||
runPRNG('mt19937', Mt19937)
|
||||
runPRNG('xoroshiro128plus', Xoroshiro128plus)
|
||||
runPRNG('xorshift32', Xorshift32)
|
||||
5
lib/random/PRNG/README.md
Normal file
5
lib/random/PRNG/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Pseudo Random Number Generators (PRNG)
|
||||
|
||||
Given a seed a PRNG generates a sequence of numbers that cannot be reasonably predicted. Two PRNGs must generate the same random sequence of numbers if given the same seed.
|
||||
|
||||
TODO: explain what POINT is
|
||||
98
lib/random/PRNG/Xoroshiro128plus.js
Normal file
98
lib/random/PRNG/Xoroshiro128plus.js
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
import Xorshift32 from './Xorshift32.js'
|
||||
|
||||
/**
|
||||
* This is a variant of xoroshiro128plus - the fastest full-period generator passing BigCrush without systematic failures.
|
||||
*
|
||||
* This implementation follows the idea of the original xoroshiro128plus implementation,
|
||||
* but is optimized for the JavaScript runtime. I.e.
|
||||
* * The operations are performed on 32bit integers (the original implementation works with 64bit values).
|
||||
* * The initial 128bit state is computed based on a 32bit seed and Xorshift32.
|
||||
* * This implementation returns two 32bit values based on the 64bit value that is computed by xoroshiro128plus.
|
||||
* Caution: The last addition step works slightly different than in the original implementation - the add carry of the
|
||||
* first 32bit addition is not carried over to the last 32bit.
|
||||
*
|
||||
* [Reference implementation](http://vigna.di.unimi.it/xorshift/xoroshiro128plus.c)
|
||||
*/
|
||||
export default class Xoroshiro128plus {
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
// This is a variant of Xoroshiro128plus to fill the initial state
|
||||
const xorshift32 = new Xorshift32(seed)
|
||||
this.state = new Uint32Array(4)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.state[i] = xorshift32.next()
|
||||
}
|
||||
this._fresh = true
|
||||
}
|
||||
next () {
|
||||
const state = this.state
|
||||
if (this._fresh) {
|
||||
this._fresh = false
|
||||
return (state[0] + state[2]) & 0xFFFFFFFF
|
||||
} else {
|
||||
this._fresh = true
|
||||
const s0 = state[0]
|
||||
const s1 = state[1]
|
||||
const s2 = state[2] ^ s0
|
||||
const s3 = state[3] ^ s1
|
||||
// function js_rotl (x, k) {
|
||||
// k = k - 32
|
||||
// const x1 = x[0]
|
||||
// const x2 = x[1]
|
||||
// x[0] = x2 << k | x1 >>> (32 - k)
|
||||
// x[1] = x1 << k | x2 >>> (32 - k)
|
||||
// }
|
||||
// rotl(s0, 55) // k = 23 = 55 - 32; j = 9 = 32 - 23
|
||||
state[0] = (s1 << 23 | s0 >>> 9) ^ s2 ^ (s2 << 14 | s3 >>> 18)
|
||||
state[1] = (s0 << 23 | s1 >>> 9) ^ s3 ^ (s3 << 14)
|
||||
// rol(s1, 36) // k = 4 = 36 - 32; j = 23 = 32 - 9
|
||||
state[2] = s3 << 4 | s2 >>> 28
|
||||
state[3] = s2 << 4 | s3 >>> 28
|
||||
return (state[1] + state[3]) & 0xFFFFFFFF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
// reference implementation
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
uint64_t s[2];
|
||||
|
||||
static inline uint64_t rotl(const uint64_t x, int k) {
|
||||
return (x << k) | (x >> (64 - k));
|
||||
}
|
||||
|
||||
uint64_t next(void) {
|
||||
const uint64_t s0 = s[0];
|
||||
uint64_t s1 = s[1];
|
||||
s1 ^= s0;
|
||||
s[0] = rotl(s0, 55) ^ s1 ^ (s1 << 14); // a, b
|
||||
s[1] = rotl(s1, 36); // c
|
||||
return (s[0] + s[1]) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int i;
|
||||
s[0] = 1111 | (1337ul << 32);
|
||||
s[1] = 1234 | (9999ul << 32);
|
||||
|
||||
printf("1000 outputs of genrand_int31()\n");
|
||||
for (i=0; i<100; i++) {
|
||||
printf("%10lu ", i);
|
||||
printf("%10lu ", next());
|
||||
printf("- %10lu ", s[0] >> 32);
|
||||
printf("%10lu ", (s[0] << 32) >> 32);
|
||||
printf("%10lu ", s[1] >> 32);
|
||||
printf("%10lu ", (s[1] << 32) >> 32);
|
||||
printf("\n");
|
||||
// if (i%5==4) printf("\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
*/
|
||||
26
lib/random/PRNG/Xorshift32.js
Normal file
26
lib/random/PRNG/Xorshift32.js
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
/**
|
||||
* Xorshift32 is a very simple but elegang PRNG with a period of `2^32-1`.
|
||||
*/
|
||||
export default class Xorshift32 {
|
||||
/**
|
||||
* @param {number} seed The starting point for the random number generation. If you use the same seed, the generator will return the same sequence of random numbers.
|
||||
*/
|
||||
constructor (seed) {
|
||||
this.seed = seed
|
||||
this._state = seed
|
||||
}
|
||||
/**
|
||||
* Generate a random signed integer.
|
||||
*
|
||||
* @return {Number} A 32 bit signed integer.
|
||||
*/
|
||||
next () {
|
||||
let x = this._state
|
||||
x ^= x << 13
|
||||
x ^= x >> 17
|
||||
x ^= x << 5
|
||||
this._state = x
|
||||
return x
|
||||
}
|
||||
}
|
||||
131
lib/random/random.js
Normal file
131
lib/random/random.js
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
import * as binary from '../binary.js'
|
||||
import { fromCharCode, fromCodePoint } from '../string.js'
|
||||
import { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } from '../number.js'
|
||||
import * as math from '../math.js'
|
||||
|
||||
import DefaultPRNG from './PRNG/Xoroshiro128plus.js'
|
||||
|
||||
/**
|
||||
* Description of the function
|
||||
* @callback generatorNext
|
||||
* @return {number} A 32bit integer
|
||||
*/
|
||||
|
||||
/**
|
||||
* A random type generator.
|
||||
*
|
||||
* @typedef {Object} PRNG
|
||||
* @property {generatorNext} next Generate new number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a Xoroshiro128plus Pseudo-Random-Number-Generator.
|
||||
* This is the fastest full-period generator passing BigCrush without systematic failures.
|
||||
* But there are more PRNGs available in ./PRNG/.
|
||||
*
|
||||
* @param {number} seed A positive 32bit integer. Do not use negative numbers.
|
||||
* @return {PRNG}
|
||||
*/
|
||||
export const createPRNG = seed => new DefaultPRNG(Math.floor(seed < 1 ? seed * binary.BITS32 : seed))
|
||||
|
||||
/**
|
||||
* Generates a single random bool.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Boolean} A random boolean
|
||||
*/
|
||||
export const bool = gen => (gen.next() & 2) === 2 // brackets are non-optional!
|
||||
|
||||
/**
|
||||
* Generates a random integer with 53 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
||||
* @return {Number} A random integer on [min, max]
|
||||
*/
|
||||
export const int53 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => math.floor(real53(gen) * (max + 1 - min) + min)
|
||||
|
||||
/**
|
||||
* Generates a random integer with 32 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Number} [min = MIN_SAFE_INTEGER] The lower bound of the allowed return values (inclusive).
|
||||
* @param {Number} [max = MAX_SAFE_INTEGER] The upper bound of the allowed return values (inclusive).
|
||||
* @return {Number} A random integer on [min, max]
|
||||
*/
|
||||
export const int32 = (gen, min = MIN_SAFE_INTEGER, max = MAX_SAFE_INTEGER) => min + ((gen.next() >>> 0) % (max + 1 - min))
|
||||
|
||||
/**
|
||||
* Generates a random real on [0, 1) with 32 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Number} A random real number on [0, 1).
|
||||
*/
|
||||
export const real32 = gen => (gen.next() >>> 0) / binary.BITS32
|
||||
|
||||
/**
|
||||
* Generates a random real on [0, 1) with 53 bit resolution.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @return {Number} A random real number on [0, 1).
|
||||
*/
|
||||
export const real53 = gen => (((gen.next() >>> 5) * binary.BIT26) + (gen.next() >>> 6)) / MAX_SAFE_INTEGER
|
||||
|
||||
/**
|
||||
* Generates a random character from char code 32 - 126. I.e. Characters, Numbers, special characters, and Space:
|
||||
*
|
||||
* (Space)!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~
|
||||
*/
|
||||
export const char = gen => fromCharCode(int32(gen, 32, 126))
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @return {string} A single letter (a-z)
|
||||
*/
|
||||
export const letter = gen => fromCharCode(int32(gen, 97, 122))
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @return {string} A random word without spaces consisting of letters (a-z)
|
||||
*/
|
||||
export const word = gen => {
|
||||
const len = int32(gen, 0, 20)
|
||||
let str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += letter(gen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: this function produces invalid runes. Does not cover all of utf16!!
|
||||
*/
|
||||
export const utf16Rune = gen => {
|
||||
const codepoint = int32(gen, 0, 256)
|
||||
return fromCodePoint(codepoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PRNG} gen
|
||||
* @param {number} [maxlen = 20]
|
||||
*/
|
||||
export const utf16String = (gen, maxlen = 20) => {
|
||||
const len = int32(gen, 0, maxlen)
|
||||
let str = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += utf16Rune(gen)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one element of a given array.
|
||||
*
|
||||
* @param {PRNG} gen A random number generator.
|
||||
* @param {Array<T>} array Non empty Array of possible values.
|
||||
* @return {T} One of the values of the supplied Array.
|
||||
* @template T
|
||||
*/
|
||||
export const oneOf = (gen, array) => array[int32(gen, 0, array.length - 1)]
|
||||
110
lib/random/random.test.js
Normal file
110
lib/random/random.test.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
*TODO: enable tests
|
||||
import * as rt from '../rich-text/formatters.mjs'
|
||||
import { test } from '../test/test.mjs'
|
||||
import Xoroshiro128plus from './PRNG/Xoroshiro128plus.mjs'
|
||||
import Xorshift32 from './PRNG/Xorshift32.mjs'
|
||||
import MT19937 from './PRNG/Mt19937.mjs'
|
||||
import { generateBool, generateInt, generateInt32, generateReal, generateChar } from './random.mjs'
|
||||
import { MAX_SAFE_INTEGER } from '../number/constants.mjs'
|
||||
import { BIT32 } from '../binary/constants.mjs'
|
||||
|
||||
function init (Gen) {
|
||||
return {
|
||||
gen: new Gen(1234)
|
||||
}
|
||||
}
|
||||
|
||||
const PRNGs = [
|
||||
{ name: 'Xoroshiro128plus', Gen: Xoroshiro128plus },
|
||||
{ name: 'Xorshift32', Gen: Xorshift32 },
|
||||
{ name: 'MT19937', Gen: MT19937 }
|
||||
]
|
||||
|
||||
const ITERATONS = 1000000
|
||||
|
||||
for (const PRNG of PRNGs) {
|
||||
const prefix = rt.orange`${PRNG.name}:`
|
||||
|
||||
test(rt.plain`${prefix} generateBool`, function generateBoolTest (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let head = 0
|
||||
let tail = 0
|
||||
let b
|
||||
let i
|
||||
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
b = generateBool(gen)
|
||||
if (b) {
|
||||
head++
|
||||
} else {
|
||||
tail++
|
||||
}
|
||||
}
|
||||
t.log(`Generated ${head} heads and ${tail} tails.`)
|
||||
t.assert(tail >= Math.floor(ITERATONS * 0.49), 'Generated enough tails.')
|
||||
t.assert(head >= Math.floor(ITERATONS * 0.49), 'Generated enough heads.')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateInt integers average correctly`, function averageIntTest (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let count = 0
|
||||
let i
|
||||
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
count += generateInt(gen, 0, 100)
|
||||
}
|
||||
const average = count / ITERATONS
|
||||
const expectedAverage = 100 / 2
|
||||
t.log(`Average is: ${average}. Expected average is ${expectedAverage}.`)
|
||||
t.assert(Math.abs(average - expectedAverage) <= 1, 'Expected average is at most 1 off.')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateInt32 generates integer with 32 bits`, function generateLargeIntegers (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let num = 0
|
||||
let i
|
||||
let newNum
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
newNum = generateInt32(gen, 0, MAX_SAFE_INTEGER)
|
||||
if (newNum > num) {
|
||||
num = newNum
|
||||
}
|
||||
}
|
||||
t.log(`Largest number generated is ${num} (0b${num.toString(2)})`)
|
||||
t.assert(num > (BIT32 >>> 0), 'Largest number is 32 bits long.')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateReal has 53 bit resolution`, function real53bitResolution (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
let num = 0
|
||||
let i
|
||||
let newNum
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
newNum = generateReal(gen) * MAX_SAFE_INTEGER
|
||||
if (newNum > num) {
|
||||
num = newNum
|
||||
}
|
||||
}
|
||||
t.log(`Largest number generated is ${num}.`)
|
||||
t.assert((MAX_SAFE_INTEGER - num) / MAX_SAFE_INTEGER < 0.01, 'Largest number is close to MAX_SAFE_INTEGER (at most 1% off).')
|
||||
})
|
||||
|
||||
test(rt.plain`${prefix} generateChar generates all described characters`, function real53bitResolution (t) {
|
||||
const { gen } = init(PRNG.Gen)
|
||||
const charSet = new Set()
|
||||
const chars = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`abcdefghijklmnopqrstuvwxyz{|}~"'
|
||||
let i
|
||||
let char
|
||||
for (i = chars.length - 1; i >= 0; i--) {
|
||||
charSet.add(chars[i])
|
||||
}
|
||||
for (i = 0; i < ITERATONS; i++) {
|
||||
char = generateChar(gen)
|
||||
charSet.delete(char)
|
||||
}
|
||||
t.log(`Charactes missing: ${charSet.size} - generating all of "${chars}"`)
|
||||
t.assert(charSet.size === 0, 'Generated all documented characters.')
|
||||
})
|
||||
}
|
||||
*/
|
||||
47
lib/simpleDiff.js
Normal file
47
lib/simpleDiff.js
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
/**
|
||||
* A SimpleDiff describes a change on a String.
|
||||
*
|
||||
* @example
|
||||
* console.log(a) // the old value
|
||||
* console.log(b) // the updated value
|
||||
* // Apply changes of diff (pseudocode)
|
||||
* a.remove(diff.pos, diff.remove) // Remove `diff.remove` characters
|
||||
* a.insert(diff.pos, diff.insert) // Insert `diff.insert`
|
||||
* a === b // values match
|
||||
*
|
||||
* @typedef {Object} SimpleDiff
|
||||
* @property {Number} pos The index where changes were applied
|
||||
* @property {Number} remove The number of characters to delete starting
|
||||
* at `index`.
|
||||
* @property {String} insert The new text to insert at `index` after applying
|
||||
* `delete`
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a diff between two strings. This diff implementation is highly
|
||||
* efficient, but not very sophisticated.
|
||||
*
|
||||
* @public
|
||||
* @param {String} a The old version of the string
|
||||
* @param {String} b The updated version of the string
|
||||
* @return {SimpleDiff} The diff description.
|
||||
*/
|
||||
export default function simpleDiff (a, b) {
|
||||
let left = 0 // number of same characters counting from left
|
||||
let right = 0 // number of same characters counting from right
|
||||
while (left < a.length && left < b.length && a[left] === b[left]) {
|
||||
left++
|
||||
}
|
||||
if (left !== a.length || left !== b.length) {
|
||||
// Only check right if a !== b
|
||||
while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) {
|
||||
right++
|
||||
}
|
||||
}
|
||||
return {
|
||||
pos: left, // TODO: rename to index (also in type above)
|
||||
remove: a.length - left - right,
|
||||
insert: b.slice(left, b.length - right)
|
||||
}
|
||||
}
|
||||
2
lib/string.js
Normal file
2
lib/string.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export const fromCharCode = String.fromCharCode
|
||||
export const fromCodePoint = String.fromCodePoint
|
||||
33
lib/test.js
Normal file
33
lib/test.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as logging from './logging.js'
|
||||
import simpleDiff from './simpleDiff.js'
|
||||
|
||||
export const run = async (name, f) => {
|
||||
console.log(`%cStart:%c ${name}`, 'color:blue;', '')
|
||||
const start = new Date()
|
||||
try {
|
||||
await f(name)
|
||||
} catch (e) {
|
||||
logging.print(`%cFailure:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:red;font-weight:bold', '', 'color:grey')
|
||||
throw e
|
||||
}
|
||||
logging.print(`%cSuccess:%c ${name} in %c${new Date().getTime() - start.getTime()}ms`, 'color:green;font-weight:bold', '', 'color:grey')
|
||||
}
|
||||
|
||||
export const compareArrays = (as, bs) => {
|
||||
if (as.length !== bs.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < as.length; i++) {
|
||||
if (as[i] !== bs[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const compareStrings = (a, b) => {
|
||||
if (a !== b) {
|
||||
const diff = simpleDiff(a, b)
|
||||
logging.print(`%c${a.slice(0, diff.pos)}%c${a.slice(diff.pos, diff.remove)}%c${diff.insert}%c${a.slice(diff.pos + diff.remove)}`, 'color:grey', 'color:red', 'color:green', 'color:grey')
|
||||
}
|
||||
}
|
||||
3
lib/time.js
Normal file
3
lib/time.js
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
export const getDate = () => new Date()
|
||||
export const getUnixTime = () => getDate().getTime()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user