Compare commits
681 Commits
v13.0.0-28
...
v13.5.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72ac448e9 | ||
|
|
da21fca334 | ||
|
|
d80512d690 | ||
|
|
6886881b76 | ||
|
|
dc9717ecd0 | ||
|
|
7bd764fba7 | ||
|
|
4047890a6e | ||
|
|
1627e7b3f6 | ||
|
|
e55d3cc510 | ||
|
|
55bd0b16f7 | ||
|
|
ab7de51064 | ||
|
|
d4917bb567 | ||
|
|
4e343ccace | ||
|
|
4efd47447b | ||
|
|
5aa1aaebb3 | ||
|
|
7656f897d6 | ||
|
|
5244755879 | ||
|
|
3a7a324a24 | ||
|
|
9e98fec504 | ||
|
|
b1c7022890 | ||
|
|
c67428d715 | ||
|
|
45a9af96af | ||
|
|
249c4f9c45 | ||
|
|
cdc7d3ffe6 | ||
|
|
ac6a0e7667 | ||
|
|
12881e2be7 | ||
|
|
77958da657 | ||
|
|
8a8a60efde | ||
|
|
7a1d648e79 | ||
|
|
3af420e790 | ||
|
|
4f2d13e3ce | ||
|
|
e0b76cd2f4 | ||
|
|
d812636c5b | ||
|
|
21fee0fe96 | ||
|
|
fab14a09de | ||
|
|
710b4ba145 | ||
|
|
34091ae614 | ||
|
|
feb8ec1afc | ||
|
|
ce9139c9f4 | ||
|
|
e2e5d0870c | ||
|
|
04cff60931 | ||
|
|
5dfe4e8af2 | ||
|
|
05ca0b0208 | ||
|
|
ee7c189fdc | ||
|
|
01c08ef202 | ||
|
|
894c0d7731 | ||
|
|
fdf632f03e | ||
|
|
ce80cb4a0d | ||
|
|
ae3c4cc050 | ||
|
|
27a78047c5 | ||
|
|
7a128c271b | ||
|
|
263cc0856e | ||
|
|
2199ac3e4e | ||
|
|
275d52b19d | ||
|
|
7edbb2485f | ||
|
|
304812fb07 | ||
|
|
baca852733 | ||
|
|
7cbf204143 | ||
|
|
c8a59118b5 | ||
|
|
bee397f1e5 | ||
|
|
1e97cf8323 | ||
|
|
c28ad0608e | ||
|
|
e19f16f22c | ||
|
|
6f074a873d | ||
|
|
4af04d6a29 | ||
|
|
97d9714710 | ||
|
|
ca667be68b | ||
|
|
8086a4f816 | ||
|
|
186f7140b6 | ||
|
|
edc1f9418f | ||
|
|
32b734b24d | ||
|
|
656328631c | ||
|
|
dbd1b3cb59 | ||
|
|
8fadec4dcd | ||
|
|
8013b4ef5c | ||
|
|
0a40b541e8 | ||
|
|
728bb6f1b2 | ||
|
|
fd59696b9a | ||
|
|
bfacd2e63a | ||
|
|
6bc9c220b9 | ||
|
|
7c0b98bbb2 | ||
|
|
034463798d | ||
|
|
4c929c6808 | ||
|
|
0fc213e92e | ||
|
|
bbc688975d | ||
|
|
ab9373c188 | ||
|
|
af576788f1 | ||
|
|
fbbf085278 | ||
|
|
d8868c47e1 | ||
|
|
47221c26c4 | ||
|
|
ba83398374 | ||
|
|
0b23d5aeeb | ||
|
|
072947c0bb | ||
|
|
22aef63d8a | ||
|
|
f8341220c3 | ||
|
|
50e5964fcb | ||
|
|
004a781a56 | ||
|
|
31dee48f63 | ||
|
|
c8534ea6bc | ||
|
|
1e0fd60df4 | ||
|
|
3404d22d12 | ||
|
|
d3b56702ad | ||
|
|
d5e6c26420 | ||
|
|
e497f07f7a | ||
|
|
510354d99f | ||
|
|
c3342d0b34 | ||
|
|
45af21f31e | ||
|
|
320da29b69 | ||
|
|
783c4d8209 | ||
|
|
2c708b647d | ||
|
|
7a45be8c88 | ||
|
|
972d15dda5 | ||
|
|
fdf2063943 | ||
|
|
e81267d4df | ||
|
|
563c34f81a | ||
|
|
ba713983e3 | ||
|
|
bf2ee3680b | ||
|
|
b812a3dd6c | ||
|
|
b3f5b50377 | ||
|
|
7bcd4a828d | ||
|
|
cb705922b4 | ||
|
|
1ed58909d3 | ||
|
|
0aca7bbefa | ||
|
|
e1f0324840 | ||
|
|
7bac783490 | ||
|
|
1508c44f68 | ||
|
|
3dd843372f | ||
|
|
d6be4d9391 | ||
|
|
53f2344017 | ||
|
|
86f7631d1e | ||
|
|
3bb107504f | ||
|
|
4c46ebfb45 | ||
|
|
9d0d63ead7 | ||
|
|
39803c1d11 | ||
|
|
46fae57036 | ||
|
|
e9cb07da55 | ||
|
|
114f28f48e | ||
|
|
a1da486c8a | ||
|
|
4fb9cc2a30 | ||
|
|
e2c9eb7f01 | ||
|
|
6fd33c0720 | ||
|
|
72f3ce75b2 | ||
|
|
fd211731cc | ||
|
|
8049776074 | ||
|
|
32b1338d48 | ||
|
|
c2f0ca3fae | ||
|
|
dfc6b879de | ||
|
|
81f16ff0b5 | ||
|
|
e1a2ccd7f6 | ||
|
|
be8cc8a20c | ||
|
|
a253cfc090 | ||
|
|
992c0b5e32 | ||
|
|
e17d661769 | ||
|
|
fef3fc2a4a | ||
|
|
eee695eeeb | ||
|
|
38e38a92dc | ||
|
|
dadc08597d | ||
|
|
e769a2a354 | ||
|
|
0dd0a4be14 | ||
|
|
7193ae63b7 | ||
|
|
4d48224518 | ||
|
|
b4fc073aa5 | ||
|
|
9c0d1eb209 | ||
|
|
6a9f853d12 | ||
|
|
ce3b0f3043 | ||
|
|
94646b2f45 | ||
|
|
29c2ad4492 | ||
|
|
637fadf38e | ||
|
|
0c6c11d583 | ||
|
|
6f9a2c9df7 | ||
|
|
7876a96163 | ||
|
|
ceba4b1837 | ||
|
|
22653c799c | ||
|
|
68109b033f | ||
|
|
38eb2e502c | ||
|
|
270a69fcf6 | ||
|
|
6e3b708599 | ||
|
|
6e8167fe51 | ||
|
|
3449687280 | ||
|
|
3406247a3e | ||
|
|
076d550dfa | ||
|
|
bb45816f05 | ||
|
|
5414ac7f6e | ||
|
|
0b8f032364 | ||
|
|
dc136ff56a | ||
|
|
b73a720fdc | ||
|
|
cf420d6241 | ||
|
|
859e169c91 | ||
|
|
6c2cf0f769 | ||
|
|
1a942aa4e0 | ||
|
|
368dc6b36a | ||
|
|
2151c514e5 | ||
|
|
bb25ce7731 | ||
|
|
e31e968f0d | ||
|
|
1a494761a3 | ||
|
|
b434501d11 | ||
|
|
d1d86277b8 | ||
|
|
d7a11ccf4d | ||
|
|
4c48116947 | ||
|
|
6dd26d3b48 | ||
|
|
6b0154f046 | ||
|
|
7fb63de8fc | ||
|
|
c4d80d133d | ||
|
|
cebe96c001 | ||
|
|
4d2369ce21 | ||
|
|
5293ab4df1 | ||
|
|
e53c01c6c5 | ||
|
|
03faa27787 | ||
|
|
868dd5f0a5 | ||
|
|
fa58ce53cd | ||
|
|
0a0098fdfb | ||
|
|
a5a48d07f6 | ||
|
|
7b16d5c92d | ||
|
|
ee147c14f1 | ||
|
|
e86d5ba25b | ||
|
|
149ca6f636 | ||
|
|
e4223760b0 | ||
|
|
9d3dd4e082 | ||
|
|
5a4ff33bf4 | ||
|
|
a059fa12e9 | ||
|
|
0628d8f1c9 | ||
|
|
19e2d51190 | ||
|
|
60fab42b3f | ||
|
|
469404c6e1 | ||
|
|
c9756e5b57 | ||
|
|
601d24e930 | ||
|
|
b2c16674f2 | ||
|
|
13da804b5e | ||
|
|
c5ca7b6f8c | ||
|
|
f4b68c0dd4 | ||
|
|
4407f70052 | ||
|
|
8bb52a485a | ||
|
|
9fc18d5ce0 | ||
|
|
ada4f400b5 | ||
|
|
06048b87ee | ||
|
|
05dde1db01 | ||
|
|
b5b32c5b3c | ||
|
|
3f0e2078de | ||
|
|
21470bb409 | ||
|
|
772bb87d5c | ||
|
|
dab172fa1d | ||
|
|
a70c5112cd | ||
|
|
7cb423c046 | ||
|
|
4547b35641 | ||
|
|
4c87f9a021 | ||
|
|
4b08c67e06 | ||
|
|
9f5bc9ddfe | ||
|
|
8221db795a | ||
|
|
68b4418956 | ||
|
|
fa09ebfd82 | ||
|
|
b399ffa765 | ||
|
|
180f4667c1 | ||
|
|
9455373611 | ||
|
|
aa804d89c0 | ||
|
|
3ef51a5d1a | ||
|
|
e61089c659 | ||
|
|
97625cf29b | ||
|
|
a5dc6c27aa | ||
|
|
26a51bafc9 | ||
|
|
f40e09d156 | ||
|
|
81650bc8f6 | ||
|
|
c87caafeb6 | ||
|
|
195b26d90f | ||
|
|
7e0189ca84 | ||
|
|
192706f2a8 | ||
|
|
a4ce8ae07d | ||
|
|
e04a980af1 | ||
|
|
47d40eb6b0 | ||
|
|
fc4a39cc7d | ||
|
|
44e1fd9f14 | ||
|
|
02cc5a215f | ||
|
|
d1e8d50c43 | ||
|
|
18bb2d0719 | ||
|
|
45df311dd7 | ||
|
|
62888b4004 | ||
|
|
76c389dba0 | ||
|
|
78fa98c000 | ||
|
|
e9f9e08450 | ||
|
|
e3c59b0aa7 | ||
|
|
705dce7838 | ||
|
|
0fb55981ba | ||
|
|
89378e29ae | ||
|
|
cce35270ec | ||
|
|
d78180bf97 | ||
|
|
0ab415de3e | ||
|
|
ff3969caeb | ||
|
|
c82cc9f8d6 | ||
|
|
ef5c71bd8b | ||
|
|
bd6be3d23b | ||
|
|
0e6deab9c9 | ||
|
|
6cd9e2be32 | ||
|
|
ac8dab1e88 | ||
|
|
38ed725c2c | ||
|
|
a210bad25e | ||
|
|
6929a4f0f8 | ||
|
|
52dacfa5f2 | ||
|
|
27efe86f9c | ||
|
|
882b9055c7 | ||
|
|
e089089413 | ||
|
|
197932752e | ||
|
|
f0b2bdaf34 | ||
|
|
b96362c0f1 | ||
|
|
67f241cd7a | ||
|
|
c8af0bebf7 | ||
|
|
4f35e799a6 | ||
|
|
eb2a52dd26 | ||
|
|
189b1068ae | ||
|
|
7a3b60a5d7 | ||
|
|
99f06fc093 | ||
|
|
22917bca19 | ||
|
|
7f0e25dcba | ||
|
|
d90c9b1cb2 | ||
|
|
c426055f17 | ||
|
|
18c9010b63 | ||
|
|
c3edac62ef | ||
|
|
755de18fd5 | ||
|
|
641dc25076 | ||
|
|
1d58ea785f | ||
|
|
f53dff5043 | ||
|
|
74d1a31f49 | ||
|
|
d1063ab70b | ||
|
|
f4c919d9ec | ||
|
|
aeb23dbaa9 | ||
|
|
6d4f0c0cdd | ||
|
|
303138f309 | ||
|
|
ad373a3dce | ||
|
|
2150fa58f2 | ||
|
|
ece4841b5c | ||
|
|
8103220c05 | ||
|
|
66d500f08d | ||
|
|
5f8e7c7ba7 | ||
|
|
7b8eee6b25 | ||
|
|
1d5947c602 | ||
|
|
53e4028952 | ||
|
|
b38a8d99e5 | ||
|
|
6c4971ae25 | ||
|
|
d1f5ff0f59 | ||
|
|
1d297601e8 | ||
|
|
d9fface0be | ||
|
|
7d5db917da | ||
|
|
6e7529723d | ||
|
|
6cb64b3707 | ||
|
|
bb1c0b809f | ||
|
|
8bcff6138c | ||
|
|
e78d84ee59 | ||
|
|
c23bcb66ce | ||
|
|
5fddcef3ea | ||
|
|
e1e46c6eb1 | ||
|
|
13ad0c8464 | ||
|
|
7700b50470 | ||
|
|
fc4d6165b4 | ||
|
|
251c8aaefc | ||
|
|
1337d38ada | ||
|
|
f5c66e41cb | ||
|
|
0e7da017fe | ||
|
|
f0262ffaae | ||
|
|
36203af88e | ||
|
|
dd2b8bc6c7 | ||
|
|
463065ac21 | ||
|
|
d064e6e96e | ||
|
|
b1ed2df208 | ||
|
|
1fe4ef135c | ||
|
|
e376b5d472 | ||
|
|
952a9b2c41 | ||
|
|
03458dc641 | ||
|
|
14df5b72af | ||
|
|
338968031b | ||
|
|
1aac245b93 | ||
|
|
1faff323c1 | ||
|
|
e7280c7ae2 | ||
|
|
4c38619b5d | ||
|
|
b4e5c5cc1f | ||
|
|
b0dbd84f7f | ||
|
|
4a990963d9 | ||
|
|
7e7c9d5b11 | ||
|
|
775f6eed1d | ||
|
|
1e83b9418c | ||
|
|
ac3f672c80 | ||
|
|
2192aa5821 | ||
|
|
70bb523005 | ||
|
|
10ce6de57a | ||
|
|
3fba4f25a5 | ||
|
|
66c35d8499 | ||
|
|
4c14157dcf | ||
|
|
ef6c382e20 | ||
|
|
ee45b4fdd6 | ||
|
|
668e9e8a9b | ||
|
|
37a6d68543 | ||
|
|
f893198769 | ||
|
|
d3ee1a0ec2 | ||
|
|
d6593412a2 | ||
|
|
d31bf36531 | ||
|
|
a485f550db | ||
|
|
0610b16227 | ||
|
|
72e470c5f0 | ||
|
|
4d12a02e2f | ||
|
|
4a7d6f0a2d | ||
|
|
c80f446b5f | ||
|
|
81a529d8dc | ||
|
|
4f0ab78914 | ||
|
|
8c36f67f0b | ||
|
|
77687d94e6 | ||
|
|
4644511303 | ||
|
|
20005eecdb | ||
|
|
c9dda245bf | ||
|
|
1417470156 | ||
|
|
584e5dfd40 | ||
|
|
805acbb9f5 | ||
|
|
32c4c09072 | ||
|
|
8c5a06bbf8 | ||
|
|
a336cc167c | ||
|
|
21d86cd2be | ||
|
|
1d0f9faa91 | ||
|
|
45237571b7 | ||
|
|
bb6f6cd141 | ||
|
|
729c1f16b8 | ||
|
|
b6059704aa | ||
|
|
fa3c92f44c | ||
|
|
cd82de7742 | ||
|
|
07a6a0044b | ||
|
|
4582832a71 | ||
|
|
07ac1d03e3 | ||
|
|
cbcf1facb8 | ||
|
|
31ff7ac78c | ||
|
|
ed3b31e58f | ||
|
|
759ecb21f7 | ||
|
|
9c29d820c8 | ||
|
|
2ef11a5344 | ||
|
|
9fe47e98d5 | ||
|
|
654510f3ff | ||
|
|
52ec698635 | ||
|
|
1b06f59d1c | ||
|
|
12bcc4d080 | ||
|
|
e1a9f314a7 | ||
|
|
7a111de186 | ||
|
|
90b3fa9dd9 | ||
|
|
c635963747 | ||
|
|
1b17b5e400 | ||
|
|
61d9d96d15 | ||
|
|
7d0c048708 | ||
|
|
8a7416ad50 | ||
|
|
e56899a02c | ||
|
|
30bf3742c9 | ||
|
|
8dbd2c4696 | ||
|
|
6578727c9c | ||
|
|
92ca001cdc | ||
|
|
415de1cc4c | ||
|
|
e23582b1cd | ||
|
|
73c28952c2 | ||
|
|
1bc1e88d6a | ||
|
|
c188f813a4 | ||
|
|
ff981a8697 | ||
|
|
d9ab593b07 | ||
|
|
293527e62b | ||
|
|
5a42a94cf4 | ||
|
|
040808300c | ||
|
|
57975d409e | ||
|
|
306b2c64f3 | ||
|
|
585265e9a5 | ||
|
|
777ae9503a | ||
|
|
4c1798e5fa | ||
|
|
f4d85e2a3e | ||
|
|
a0f0c9c377 | ||
|
|
95ec2a435a | ||
|
|
da9836fe59 | ||
|
|
3a7411f9e8 | ||
|
|
39cee7c6e7 | ||
|
|
0a5753c191 | ||
|
|
76b7d0b651 | ||
|
|
99e3e95a00 | ||
|
|
93ee4ee287 | ||
|
|
c5cc403a29 | ||
|
|
75f4a0a5f0 | ||
|
|
591df5c00a | ||
|
|
f6b4819ae3 | ||
|
|
d483d9cc83 | ||
|
|
453407b93d | ||
|
|
e699f92333 | ||
|
|
6ff47719ef | ||
|
|
3a0694c35c | ||
|
|
74e5243742 | ||
|
|
dcf43b9797 | ||
|
|
77e479c03b | ||
|
|
ec58a99748 | ||
|
|
f1eb66655b | ||
|
|
7f4ae9fe14 | ||
|
|
c0ba56a21f | ||
|
|
4063e28b5e | ||
|
|
b6f7cd7869 | ||
|
|
1a79e429ed | ||
|
|
04066a5678 | ||
|
|
e09ef15349 | ||
|
|
3d70eee959 | ||
|
|
582095e5a3 | ||
|
|
c9ea3a412e | ||
|
|
a2c51c36e9 | ||
|
|
ab3dba5b06 | ||
|
|
3ddff186c2 | ||
|
|
9bd199a6e7 | ||
|
|
01d0825ae6 | ||
|
|
e2f98525d2 | ||
|
|
70a0a03130 | ||
|
|
656d85c62e | ||
|
|
e168dd48fb | ||
|
|
12d43199d5 | ||
|
|
539fa8b21d | ||
|
|
f572f94586 | ||
|
|
c12d00b227 | ||
|
|
e4a5f2caec | ||
|
|
9f9f465238 | ||
|
|
8450ff86d7 | ||
|
|
70139262c5 | ||
|
|
9c0da271eb | ||
|
|
ade3e1949d | ||
|
|
eec63a008f | ||
|
|
52abcdd043 | ||
|
|
f94653424a | ||
|
|
d67a794e2c | ||
|
|
60318083a6 | ||
|
|
7607070452 | ||
|
|
28fb7b6e9c | ||
|
|
aafe15757f | ||
|
|
31d6ef6296 | ||
|
|
32b8fac37f | ||
|
|
e8060de914 | ||
|
|
22b036527c | ||
|
|
feb1e030d7 | ||
|
|
bd271e3952 | ||
|
|
df80938190 | ||
|
|
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 |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers"
|
||||
]
|
||||
}
|
||||
7
.circleci/config.yml
Normal file
7
.circleci/config.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
node: circleci/node@3.0.0
|
||||
workflows:
|
||||
node-tests:
|
||||
jobs:
|
||||
- node/test
|
||||
14
.flowconfig
14
.flowconfig
@@ -1,14 +0,0 @@
|
||||
[ignore]
|
||||
.*/node_modules/.*
|
||||
.*/dist/.*
|
||||
.*/build/.*
|
||||
|
||||
[include]
|
||||
./src/
|
||||
./tests-lib/
|
||||
./test/
|
||||
|
||||
[libs]
|
||||
./declarations/
|
||||
|
||||
[options]
|
||||
29
.github/workflows/node.js.yml
vendored
Normal file
29
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
31
.github/workflows/nodejs.yml
vendored
Normal file
31
.github/workflows/nodejs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 13.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run test-extensive
|
||||
env:
|
||||
CI: true
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
node_modules
|
||||
bower_components
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
dist
|
||||
.vscode
|
||||
docs
|
||||
|
||||
52
.jsdoc.json
Normal file
52
.jsdoc.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"sourceType": "module",
|
||||
"tags": {
|
||||
"allowUnknownTags": true,
|
||||
"dictionaries": ["jsdoc"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["./src"],
|
||||
"includePattern": ".js$"
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
],
|
||||
"templates": {
|
||||
"referenceTitle": "Yjs",
|
||||
"disableSort": false,
|
||||
"useCollapsibles": true,
|
||||
"collapse": true,
|
||||
"resources": {
|
||||
"yjs.dev": "Website",
|
||||
"docs.yjs.dev": "Docs",
|
||||
"discuss.yjs.dev": "Forum",
|
||||
"https://gitter.im/Yjs/community": "Chat"
|
||||
},
|
||||
"logo": {
|
||||
"url": "https://yjs.dev/images/logo/yjs-512x512.png",
|
||||
"width": "162px",
|
||||
"height": "162px",
|
||||
"link": "/"
|
||||
},
|
||||
"tabNames": {
|
||||
"api": "API",
|
||||
"tutorials": "Examples"
|
||||
},
|
||||
"footerText": "Shared Editing",
|
||||
"css": [
|
||||
"./style.css"
|
||||
],
|
||||
"default": {
|
||||
"staticFiles": {
|
||||
"include": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"opts": {
|
||||
"destination": "./docs/",
|
||||
"encoding": "utf8",
|
||||
"private": false,
|
||||
"recurse": true,
|
||||
"template": "./node_modules/tui-jsdoc-template"
|
||||
}
|
||||
}
|
||||
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"default": true,
|
||||
"no-inline-html": false
|
||||
}
|
||||
179
INTERNALS.md
Normal file
179
INTERNALS.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Yjs Internals
|
||||
|
||||
This document roughly explains how Yjs works internally. There is a complete
|
||||
walkthrough of the Yjs codebase available as a recording:
|
||||
https://youtu.be/0l5XgnQ6rB4
|
||||
|
||||
The Yjs CRDT algorithm is described in the [YATA
|
||||
paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types)
|
||||
from 2016. For an algorithmic view of how it works, the paper is a reasonable
|
||||
place to start. There are a handful of small improvements implemented in Yjs
|
||||
which aren't described in the paper. The most notable is that items have an
|
||||
`originRight` as well as an `origin` property, which improves performance when
|
||||
many concurrent inserts happen after the same character.
|
||||
|
||||
At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
|
||||
reuse the CRDT resolution algorithm:
|
||||
|
||||
- Arrays are easy - they're lists of arbitrary items.
|
||||
- Text is a list of characters, optionally punctuated by formatting markers and
|
||||
embeds for rich text support. Several characters can be wrapped in a single
|
||||
linked list `Item` (this is also known as the compound representation of
|
||||
CRDTs). More information about this in [this blog
|
||||
article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/).
|
||||
- Maps are lists of entries. The last inserted entry for each key is used, and
|
||||
all other duplicates for each key are flagged as deleted.
|
||||
|
||||
Each client is assigned a unique *clientID* property on first insert. This is a
|
||||
random 53-bit integer (53 bits because that fits in the javascript safe integer
|
||||
range).
|
||||
|
||||
## List items
|
||||
|
||||
Each item in a Yjs list is made up of two objects:
|
||||
|
||||
- An `Item` (*src/structs/Item.js*). This is used to relate the item to other
|
||||
adjacent items.
|
||||
- An object in the `AbstractType` hierarchy (subclasses of
|
||||
*src/types/AbstractType.js* - eg `YText`). This stores the actual content in
|
||||
the Yjs document.
|
||||
|
||||
The item and type object pair have a 1-1 mapping. The item's `content` field
|
||||
references the AbstractType object and the AbstractType object's `_item` field
|
||||
references the item.
|
||||
|
||||
Everything inserted in a Yjs document is given a unique ID, formed from a
|
||||
*ID(clientID, clock)* pair (also known as a [Lamport
|
||||
Timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp)). The clock counts
|
||||
up from 0 with the first inserted character or item a client makes. This is
|
||||
similar to automerge's operation IDs, but note that the clock is only
|
||||
incremented by inserts. Deletes are handled in a very different way (see
|
||||
below).
|
||||
|
||||
If a run of characters is inserted into a document (eg `"abc"`), the clock will
|
||||
be incremented for each character (eg 3 times here). But Yjs will only add a
|
||||
single `Item` into the list. This has no effect on the core CRDT algorithm, but
|
||||
the optimization dramatically decreases the number of javascript objects
|
||||
created during normal text editing. This optimization only applies if the
|
||||
characters share the same clientID, they're inserted in order, and all
|
||||
characters have either been deleted or all characters are not deleted. The item
|
||||
will be split if the run is interrupted for any reason (eg a character in the
|
||||
middle of the run is deleted).
|
||||
|
||||
When an item is created, it stores a reference to the IDs of the preceeding and
|
||||
succeeding item. These are stored in the item's `origin` and `originRight`
|
||||
fields, respectively. These are used when peers concurrently insert at the same
|
||||
location in a document. Though quite rare in practice, Yjs needs to make sure
|
||||
the list items always resolve to the same order on all peers. The actual logic
|
||||
is relatively simple - its only a couple dozen lines of code and it lives in
|
||||
the `Item#integrate()` method. The YATA paper has much more detail on the this
|
||||
algorithm.
|
||||
|
||||
### Item Storage
|
||||
|
||||
The items themselves are stored in two data structures and a cache:
|
||||
|
||||
- The items are stored in a tree of doubly-linked lists in *document order*.
|
||||
Each item has `left` and `right` properties linking to its siblings in the
|
||||
document. Items also have a `parent` property to reference their parent in the
|
||||
document tree (null at the root). (And you can access an item's children, if
|
||||
any, through `item.content`).
|
||||
- All items are referenced in *insertion order* inside the struct store
|
||||
(*src/utils/StructStore.js*). This references the list of items inserted by
|
||||
for each client, in chronological order. This is used to find an item in the
|
||||
tree with a given ID (using a binary search). It is also used to efficiently
|
||||
gather the operations a peer is missing during sync (more on this below).
|
||||
|
||||
When a local insert happens, Yjs needs to map the insert position in the
|
||||
document (eg position 1000) to an ID. With just the linked list, this would
|
||||
require a slow O(n) linear scan of the list. But when editing a document, most
|
||||
inserts are either at the same position as the last insert, or nearby. To
|
||||
improve performance, Yjs stores a cache of the 10 most recently looked up
|
||||
insert positions in the document. This is consulted and updated when a position
|
||||
is looked up to improve performance in the average case. The cache is updated
|
||||
using a heuristic that is still changing (currently, it is updated when a new
|
||||
position significantly diverges from existing markers in the cache). Internally
|
||||
this is referred to as the skip list / fast search marker.
|
||||
|
||||
### Deletions
|
||||
|
||||
Deletions in Yjs are treated very differently from insertions. Insertions are
|
||||
implemented as a sequential operation based CRDT, but deletions are treated as
|
||||
a simpler state based CRDT.
|
||||
|
||||
When an item has been deleted by any peer, at any point in history, it is
|
||||
flagged as deleted on the item. (Internally Yjs uses the `info` bitfield.) Yjs
|
||||
does not record metadata about a deletion:
|
||||
|
||||
- No data is kept on *when* an item was deleted, or which user deleted it.
|
||||
- The struct store does not contain deletion records
|
||||
- The clientID's clock is not incremented
|
||||
|
||||
If garbage collection is enabled in Yjs, when an object is deleted its content
|
||||
is discarded. If a deleted object contains children (eg a field is deleted in
|
||||
an object), the content is replaced with a `GC` object (*src/structs/GC.js*).
|
||||
This is a very lightweight structure - it only stores the length of the removed
|
||||
content.
|
||||
|
||||
Yjs has some special logic to share which content in a document has been
|
||||
deleted:
|
||||
|
||||
- When a delete happens, as well as marking the item, the deleted IDs are
|
||||
listed locally within the transaction. (See below for more information about
|
||||
transactions.) When a transaction has been committed locally, the set of
|
||||
deleted items is appended to a transaction's update message.
|
||||
- A snapshot (a marked point in time in the Yjs history) is specified using
|
||||
both the set of (clientID, clock) pairs *and* the set of all deleted item
|
||||
IDs. The deleted set is O(n), but because deletions usually happen in runs,
|
||||
this data set is usually tiny in practice. (The real world editing trace from
|
||||
the B4 benchmark document contains 182k inserts and 77k deleted characters. The
|
||||
deleted set size in a snapshot is only 4.5Kb).
|
||||
|
||||
## Transactions
|
||||
|
||||
All updates in Yjs happen within a *transaction*. (Defined in
|
||||
*src/utils/Transaction.js*.)
|
||||
|
||||
The transaction collects a set of updates to the Yjs document to be applied on
|
||||
remote peers atomically. Once a transaction has been committed locally, it
|
||||
generates a compressed *update message* which is broadcast to synchronized
|
||||
remote peers to notify them of the local change. The update message contains:
|
||||
|
||||
- The set of newly inserted items
|
||||
- The set of items deleted within the transaction.
|
||||
|
||||
## Network protocol
|
||||
|
||||
The network protocol is not really a part of Yjs. There are a few relevant
|
||||
concepts that can be used to create a custom network protocol:
|
||||
|
||||
* `update`: The Yjs document can be encoded to an *update* object that can be
|
||||
parsed to reconstruct the document. Also every change on the document fires
|
||||
an incremental document updates that allows clients to sync with each other.
|
||||
The update object is an Uint8Array that efficiently encodes `Item` objects and
|
||||
the delete set.
|
||||
* `state vector`: A state vector defines the know state of each user (a set of
|
||||
tubles `(client, clock)`). This object is also efficiently encoded as a
|
||||
Uint8Array.
|
||||
|
||||
The client can ask a remote client for missing document updates by sending
|
||||
their state vector (often referred to as *sync step 1*). The remote peer can
|
||||
compute the missing `Item` objects using the `clocks` of the respective clients
|
||||
and compute a minimal update message that reflects all missing updates (sync
|
||||
step 2).
|
||||
|
||||
An implementation of the syncing process is in
|
||||
[y-protocols](https://github.com/yjs/y-protocols).
|
||||
|
||||
## Snapshots
|
||||
|
||||
A snapshot can be used to restore an old document state. It is a `state vector`
|
||||
+ `delete set`. I client can restore an old document state by iterating through
|
||||
the sequence CRDT and ignoring all Items that have an `id.clock >
|
||||
stateVector[id.client].clock`. Instead of using `item.deleted` the client will
|
||||
use the delete set to find out if an item was deleted or not.
|
||||
|
||||
It is not recommended to restore an old document state using snapshots,
|
||||
although that would certainly be possible. Instead, the old state should be
|
||||
computed by iterating through the newest state and using the additional
|
||||
information from the state vector.
|
||||
@@ -1,32 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css" media="screen">
|
||||
#aceContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
.inserted {
|
||||
position:absolute;
|
||||
z-index:20;
|
||||
background-color: #FFC107;
|
||||
}
|
||||
.deleted {
|
||||
position:absolute;
|
||||
z-index:20;
|
||||
background-color: #FFC107;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="aceContainer"></div>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
/* global Y, ace */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'ace-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
ace: 'Text' // y.share.textarea is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yAce = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
var editor = ace.edit('aceContainer')
|
||||
editor.setTheme('ace/theme/chrome')
|
||||
editor.getSession().setMode('ace/mode/javascript')
|
||||
|
||||
y.share.ace.bindAce(editor)
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
#chat p span {
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
<div id="chat"></div>
|
||||
<form id="chatform">
|
||||
<input name="username" type="text" style="width:15%;">
|
||||
<input name="message" type="text" style="width:60%;">
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
var y = new Y({
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'chat-example'
|
||||
}
|
||||
})
|
||||
|
||||
window.yChat = y
|
||||
|
||||
let chatprotocol = y.define('chatprotocol', Y.Array)
|
||||
|
||||
let chatcontainer = document.querySelector('#chat')
|
||||
|
||||
// This functions inserts a message at the specified position in the DOM
|
||||
function appendMessage (message, position) {
|
||||
var p = document.createElement('p')
|
||||
var uname = document.createElement('span')
|
||||
uname.appendChild(document.createTextNode(message.username + ': '))
|
||||
p.appendChild(uname)
|
||||
p.appendChild(document.createTextNode(message.message))
|
||||
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
|
||||
}
|
||||
// This function makes sure that only 7 messages exist in the chat history.
|
||||
// The rest is deleted
|
||||
function cleanupChat () {
|
||||
if (chatprotocol.length > 7) {
|
||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
||||
}
|
||||
}
|
||||
// Insert the initial content
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
cleanupChat()
|
||||
|
||||
// whenever content changes, make sure to reflect the changes in the DOM
|
||||
chatprotocol.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
appendMessage(event.values[i], event.index + i)
|
||||
}
|
||||
} else if (event.type === 'delete') {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
chatcontainer.children[event.index].remove()
|
||||
}
|
||||
}
|
||||
// concurrent insertions may result in a history > 7, so cleanup here
|
||||
cleanupChat()
|
||||
})
|
||||
document.querySelector('#chatform').onsubmit = function (event) {
|
||||
// the form is submitted
|
||||
var message = {
|
||||
username: this.querySelector('[name=username]').value,
|
||||
message: this.querySelector('[name=message]').value
|
||||
}
|
||||
if (message.username.length > 0 && message.message.length > 0) {
|
||||
if (chatprotocol.length > 6) {
|
||||
// If we are goint to insert the 8th element, make sure to delete first.
|
||||
chatprotocol.delete(0)
|
||||
}
|
||||
// Here we insert a message in the shared chat type.
|
||||
// This will call the observe function (see line 40)
|
||||
// and reflect the change in the DOM
|
||||
chatprotocol.push([message])
|
||||
this.querySelector('[name=message]').value = ''
|
||||
}
|
||||
// Do not send this form!
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
<style>
|
||||
.CodeMirror {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
path {
|
||||
fill: none;
|
||||
stroke: blue;
|
||||
stroke-width: 1px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
</style>
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,84 +0,0 @@
|
||||
/* globals Y, d3 */
|
||||
'strict mode'
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example',
|
||||
url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
drawing: 'Array'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yDrawing = y
|
||||
var drawing = y.share.drawing
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basis')
|
||||
|
||||
var svg = d3.select('#drawingCanvas')
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
// we only implement insert events that are appended to the end of the array
|
||||
event.values.forEach(function (value) {
|
||||
line.datum().push(value)
|
||||
})
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
y.share.drawing.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
event.values.forEach(drawLine)
|
||||
} else {
|
||||
// just remove all elements (thats what we do anyway)
|
||||
svg.selectAll('path').remove()
|
||||
}
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i))
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
sharedLine = drawing.get(drawing.length - 1)
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 33)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,35 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
let y = new Y({
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234',
|
||||
room: 'html-editor-example6'
|
||||
// maxBufferLength: 100
|
||||
}
|
||||
})
|
||||
window.yXml = y
|
||||
window.yXmlType = y.define('xml', Y.XmlFragment)
|
||||
window.onload = function () {
|
||||
console.log('start!')
|
||||
// Bind children of XmlFragment to the document.body
|
||||
window.yXmlType.bindToDom(document.body)
|
||||
}
|
||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
||||
captureTimeout: 0
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && e.metaKey) {
|
||||
console.log('uidtaren')
|
||||
if (!e.shiftKey) {
|
||||
console.info('Undo!')
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
console.info('Redo!')
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
<style>
|
||||
.CodeMirror {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
})
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
.wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: 7px;
|
||||
}
|
||||
.one {
|
||||
grid-column: 1 ;
|
||||
}
|
||||
.two {
|
||||
grid-column: 2;
|
||||
}
|
||||
.three {
|
||||
grid-column: 3;
|
||||
}
|
||||
textarea {
|
||||
width: calc(100% - 10px)
|
||||
}
|
||||
.editor-container {
|
||||
background-color: #4caf50;
|
||||
padding: 4px 5px 10px 5px;
|
||||
border-radius: 11px;
|
||||
}
|
||||
.editor-container[disconnected] {
|
||||
background-color: red;
|
||||
}
|
||||
.disconnected-info {
|
||||
display: none;
|
||||
}
|
||||
.editor-container[disconnected] .disconnected-info {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
<div class="wrapper">
|
||||
<div id="container1" class="one editor-container">
|
||||
<h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container2" class="two editor-container">
|
||||
<h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
<div id="container3" class="three editor-container">
|
||||
<h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
|
||||
<textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/dist/y-text.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,64 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y1 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea1'))
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-second.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y2 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea2'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container2').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container2').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-third.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y3 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea3'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container3').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container3').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
|
||||
<g>
|
||||
<path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
|
||||
<path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
|
||||
<path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
|
||||
<path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,74 +0,0 @@
|
||||
/* @flow */
|
||||
/* global Y, d3 */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Puzzle-example',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
share: {
|
||||
piece1: 'Map',
|
||||
piece2: 'Map',
|
||||
piece3: 'Map',
|
||||
piece4: 'Map'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yJigsaw = y
|
||||
var origin // mouse start position - translation of piece
|
||||
var drag = d3.behavior.drag()
|
||||
.on('dragstart', function (params) {
|
||||
// get the translation of the element
|
||||
var translation = d3
|
||||
.select(this)
|
||||
.attr('transform')
|
||||
.slice(10, -1)
|
||||
.split(',')
|
||||
.map(Number)
|
||||
// mouse coordinates
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
origin = {
|
||||
x: mouse[0] - translation[0],
|
||||
y: mouse[1] - translation[1]
|
||||
}
|
||||
})
|
||||
.on('drag', function () {
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||
var y = mouse[1] - origin.y
|
||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||
})
|
||||
.on('dragend', function (piece, i) {
|
||||
// save the current translation of the puzzle piece
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x
|
||||
var y = mouse[1] - origin.y
|
||||
piece.set('translation', {x: x, y: y})
|
||||
})
|
||||
|
||||
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||
|
||||
pieces
|
||||
.classed('draggable', true)
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
}).call(drag)
|
||||
|
||||
data.forEach(function (piece) {
|
||||
piece.observe(function () {
|
||||
// whenever a property of a piece changes, update the translation of the pieces
|
||||
pieces
|
||||
.transition()
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<div id="monacoContainer"></div>
|
||||
<style>
|
||||
#monacoContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/y-array/y-array.js"></script>
|
||||
<script src="../bower_components/y-text/y-text.js"></script>
|
||||
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/y-memory/y-memory.js"></script>
|
||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
/* global Y, monaco */
|
||||
|
||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// Initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'monaco-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
monaco: 'Text' // y.share.monaco is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yMonaco = y
|
||||
|
||||
// Create Monaco editor
|
||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||
language: 'javascript'
|
||||
})
|
||||
|
||||
// Bind to y.share.monaco
|
||||
y.share.monaco.bindMonaco(editor)
|
||||
})
|
||||
})
|
||||
1173
examples/package-lock.json
generated
1173
examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"dist": "rollup -c",
|
||||
"watch": "rollup -cw"
|
||||
},
|
||||
"author": "Kevin Jahns",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"monaco-editor": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^10.0.2"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": ["bower_components"]
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead (see above)
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
-->
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,40 +0,0 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example-quill-1.0-test',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yQuill = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'yjs-dist.esm',
|
||||
dest: 'yjs-dist.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,49 +0,0 @@
|
||||
/* 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)
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
/* 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'
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/y-text.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example2',
|
||||
// url: '//localhost:1234',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
},
|
||||
timeout: 5000 // reject if no connection was established within 5 seconds
|
||||
}).then(function (y) {
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
y.share.textarea.bind(document.getElementById('textfield'))
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Shared DOM Example </h1>
|
||||
<p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
|
||||
</div>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
|
||||
</div>
|
||||
<div class="command">
|
||||
<button type="button">Execute</button>
|
||||
<input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var commands = document.querySelectorAll(".command");
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
|
||||
var execute = function(){
|
||||
eval(command.querySelector("input").value);
|
||||
}
|
||||
command.querySelector("button").onclick = execute
|
||||
$(command.querySelector("input")).keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
execute()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'Xml-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yXml = y
|
||||
// bind xml type to a dom, and put it in body
|
||||
window.sharedDom = y.share.xml.getDom()
|
||||
document.body.appendChild(window.sharedDom)
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
import Y from '../src/Y.js'
|
||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||
|
||||
Y.extend(yWebsocketsClient)
|
||||
|
||||
export default Y
|
||||
6281
package-lock.json
generated
6281
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
115
package.json
115
package.json
@@ -1,72 +1,89 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-28",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"version": "13.5.8",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"funding": {
|
||||
"type": "GitHub Sponsors ❤",
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
||||
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
|
||||
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
|
||||
"dist": "rm -rf dist && rollup -c && tsc",
|
||||
"watch": "rollup -wc",
|
||||
"lint": "markdownlint README.md && standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||
"serve-docs": "npm run docs && http-server ./docs/",
|
||||
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
|
||||
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
|
||||
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
|
||||
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
|
||||
"postinstall": "node ./sponsor-y.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/yjs.mjs",
|
||||
"require": "./dist/yjs.cjs"
|
||||
},
|
||||
"./src/index.js": "./src/index.js",
|
||||
"./tests/testHelper.js": "./tests/testHelper.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"y.*"
|
||||
"dist/yjs.*",
|
||||
"dist/src",
|
||||
"src",
|
||||
"tests/testHelper.js",
|
||||
"sponsor-y.js"
|
||||
],
|
||||
"dictionaries": {
|
||||
"test": "tests"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/y.js",
|
||||
"/y.js.map"
|
||||
"/dist",
|
||||
"/node_modules",
|
||||
"/docs"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/y-js/yjs.git"
|
||||
"url": "https://github.com/yjs/yjs.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Yjs",
|
||||
"OT",
|
||||
"Collaboration",
|
||||
"Synchronization",
|
||||
"ShareJS",
|
||||
"Coweb",
|
||||
"Concurrency"
|
||||
"CRDT",
|
||||
"offline",
|
||||
"offline-first",
|
||||
"shared-editing",
|
||||
"concurrency",
|
||||
"collaboration"
|
||||
],
|
||||
"author": "Kevin Jahns",
|
||||
"email": "kevin.jahns@rwth-aachen.de",
|
||||
"email": "kevin.jahns@protonmail.com",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/y-js/yjs/issues"
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-plugin-external-helpers": "^6.22.0",
|
||||
"babel-plugin-transform-regenerator": "^6.24.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"chance": "^1.0.9",
|
||||
"concurrently": "^3.4.0",
|
||||
"cutest": "^0.1.9",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-inject": "^2.0.0",
|
||||
"rollup-plugin-multi-entry": "^2.0.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-uglify": "^1.0.2",
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"standard": "^10.0.2",
|
||||
"tag-dist-files": "^0.1.6"
|
||||
"url": "https://github.com/yjs/yjs/issues"
|
||||
},
|
||||
"homepage": "https://docs.yjs.dev",
|
||||
"dependencies": {
|
||||
"debug": "^2.6.8",
|
||||
"fast-diff": "^1.1.2",
|
||||
"utf-8": "^1.0.0",
|
||||
"utf8": "^2.1.2"
|
||||
"lib0": "^0.2.41"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||
"concurrently": "^3.6.1",
|
||||
"http-server": "^0.12.3",
|
||||
"jsdoc": "^3.6.6",
|
||||
"markdownlint-cli": "^0.23.2",
|
||||
"rollup": "^2.47.0",
|
||||
"standard": "^14.3.4",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^4.2.4",
|
||||
"y-protocols": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import uglify from 'rollup-plugin-uglify'
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'src/Y.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
babel(),
|
||||
uglify({
|
||||
mangle: {
|
||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
||||
},
|
||||
output: {
|
||||
comments: function (node, comment) {
|
||||
var text = comment.value
|
||||
var type = comment.type
|
||||
if (type === 'comment2') {
|
||||
// multiline comment
|
||||
return /@license/i.test(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
dest: 'y.js',
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
94
rollup.config.js
Normal file
94
rollup.config.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import nodeResolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
|
||||
const localImports = process.env.LOCALIMPORTS
|
||||
|
||||
const customModules = new Set([
|
||||
'y-websocket',
|
||||
'y-codemirror',
|
||||
'y-ace',
|
||||
'y-textarea',
|
||||
'y-quill',
|
||||
'y-dom',
|
||||
'y-prosemirror'
|
||||
])
|
||||
/**
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const customLibModules = new Set([
|
||||
'lib0',
|
||||
'y-protocols'
|
||||
])
|
||||
const debugResolve = {
|
||||
resolveId (importee) {
|
||||
if (importee === 'yjs') {
|
||||
return `${process.cwd()}/src/index.js`
|
||||
}
|
||||
if (localImports) {
|
||||
if (customModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}/src/${importee}.js`
|
||||
}
|
||||
if (customLibModules.has(importee.split('/')[0])) {
|
||||
return `${process.cwd()}/../${importee}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default [{
|
||||
input: './src/index.js',
|
||||
output: {
|
||||
name: 'Y',
|
||||
file: 'dist/yjs.cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
paths: path => {
|
||||
if (/^lib0\//.test(path)) {
|
||||
return `lib0/dist/${path.slice(5)}.cjs`
|
||||
}
|
||||
return path
|
||||
}
|
||||
},
|
||||
external: id => /^lib0\//.test(id)
|
||||
}, {
|
||||
input: './src/index.js',
|
||||
output: {
|
||||
name: 'Y',
|
||||
file: 'dist/yjs.mjs',
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
},
|
||||
external: id => /^lib0\//.test(id)
|
||||
}, {
|
||||
input: './tests/index.js',
|
||||
output: {
|
||||
name: 'test',
|
||||
file: 'dist/tests.js',
|
||||
format: 'iife',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
mainFields: ['module', 'browser', 'main']
|
||||
}),
|
||||
commonjs()
|
||||
]
|
||||
}, {
|
||||
input: './tests/index.js',
|
||||
output: {
|
||||
name: 'test',
|
||||
file: 'dist/tests.cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
debugResolve,
|
||||
nodeResolve({
|
||||
mainFields: ['module', 'main']
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
external: ['isomorphic.js']
|
||||
}]
|
||||
@@ -1,26 +0,0 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'src/y-dist.cjs.js',
|
||||
moduleName: 'Y',
|
||||
format: 'cjs',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
dest: 'y.node.js',
|
||||
sourceMap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
* @version v${pkg.version}
|
||||
* @license ${pkg.license}
|
||||
*/
|
||||
`
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs'
|
||||
import multiEntry from 'rollup-plugin-multi-entry'
|
||||
|
||||
export default {
|
||||
entry: 'test/y-map.tests.js',
|
||||
moduleName: 'y-tests',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
],
|
||||
dest: 'y.test.js',
|
||||
sourceMap: true
|
||||
}
|
||||
12
sponsor-y.js
Normal file
12
sponsor-y.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
try {
|
||||
const log = require('lib0/dist/logging.cjs')
|
||||
|
||||
log.print()
|
||||
log.print(log.BOLD, log.GREEN, log.BOLD, 'Thank you for using Yjs ', log.RED, '❤\n')
|
||||
log.print(
|
||||
log.GREY,
|
||||
'The project has grown considerably in the past year. Too much for me to maintain\nin my spare time. Several companies built their products with Yjs.\nYet, this project receives very little funding. Yjs is far from done. I want to\ncreate more awesome extensions and work on the growing number of open issues.\n', log.BOLD, 'Dear user, the future of this project entirely depends on you.\n')
|
||||
log.print(log.BLUE, log.BOLD, 'Please start funding the project now: https://github.com/sponsors/dmonad \n')
|
||||
log.print(log.GREY, '(This message will be removed when I achieved my funding goal)\n\n')
|
||||
} catch (e) { }
|
||||
@@ -1,120 +0,0 @@
|
||||
import utf8 from 'utf-8'
|
||||
import ID from '../Util/ID.js'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
export default class BinaryDecoder {
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.uint8arr = new Uint8Array(buffer)
|
||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
||||
this.uint8arr = buffer
|
||||
} else {
|
||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||
}
|
||||
this.pos = 0
|
||||
}
|
||||
/**
|
||||
* Clone this decoder instance
|
||||
* Optionally set a new position parameter
|
||||
*/
|
||||
clone (newPos = this.pos) {
|
||||
let decoder = new BinaryDecoder(this.uint8arr)
|
||||
decoder.pos = newPos
|
||||
return decoder
|
||||
}
|
||||
/**
|
||||
* Number of bytes
|
||||
*/
|
||||
get length () {
|
||||
return this.uint8arr.length
|
||||
}
|
||||
/**
|
||||
* Skip one byte, jump to the next position
|
||||
*/
|
||||
skip8 () {
|
||||
this.pos++
|
||||
}
|
||||
/**
|
||||
* Read one byte as unsigned integer
|
||||
*/
|
||||
readUint8 () {
|
||||
return this.uint8arr[this.pos++]
|
||||
}
|
||||
/**
|
||||
* Read 4 bytes as unsigned integer
|
||||
*/
|
||||
readUint32 () {
|
||||
let uint =
|
||||
this.uint8arr[this.pos] +
|
||||
(this.uint8arr[this.pos + 1] << 8) +
|
||||
(this.uint8arr[this.pos + 2] << 16) +
|
||||
(this.uint8arr[this.pos + 3] << 24)
|
||||
this.pos += 4
|
||||
return uint
|
||||
}
|
||||
/**
|
||||
* Look ahead without incrementing position
|
||||
* to the next byte and read it as unsigned integer
|
||||
*/
|
||||
peekUint8 () {
|
||||
return this.uint8arr[this.pos]
|
||||
}
|
||||
/**
|
||||
* Read unsigned integer (32bit) with variable length
|
||||
* 1/8th of the storage is used as encoding overhead
|
||||
* - numbers < 2^7 is stored in one byte
|
||||
* - numbers < 2^14 is stored in two bytes
|
||||
* ..
|
||||
*/
|
||||
readVarUint () {
|
||||
let num = 0
|
||||
let len = 0
|
||||
while (true) {
|
||||
let r = this.uint8arr[this.pos++]
|
||||
num = num | ((r & 0b1111111) << len)
|
||||
len += 7
|
||||
if (r < 1 << 7) {
|
||||
return num >>> 0 // return unsigned number!
|
||||
}
|
||||
if (len > 35) {
|
||||
throw new Error('Integer out of range!')
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Read string of variable length
|
||||
* - varUint is used to store the length of the string
|
||||
*/
|
||||
readVarString () {
|
||||
let len = this.readVarUint()
|
||||
let bytes = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = this.uint8arr[this.pos++]
|
||||
}
|
||||
return utf8.getStringFromBytes(bytes)
|
||||
}
|
||||
/**
|
||||
* Look ahead and read varString without incrementing position
|
||||
*/
|
||||
peekVarString () {
|
||||
let pos = this.pos
|
||||
let s = this.readVarString()
|
||||
this.pos = pos
|
||||
return s
|
||||
}
|
||||
/**
|
||||
* Read ID
|
||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
||||
* - Otherwise an ID is returned
|
||||
*/
|
||||
readID () {
|
||||
let user = this.readVarUint()
|
||||
if (user === RootFakeUserID) {
|
||||
// read property name and type id
|
||||
const rid = new RootID(this.readVarString(), null)
|
||||
rid.type = this.readVarUint()
|
||||
return rid
|
||||
}
|
||||
return new ID(user, this.readVarUint())
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import utf8 from 'utf-8'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
export default class BinaryEncoder {
|
||||
constructor () {
|
||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
||||
this.data = []
|
||||
}
|
||||
|
||||
get length () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
get pos () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
createBuffer () {
|
||||
return Uint8Array.from(this.data).buffer
|
||||
}
|
||||
|
||||
writeUint8 (num) {
|
||||
this.data.push(num & bits8)
|
||||
}
|
||||
|
||||
setUint8 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
}
|
||||
|
||||
writeUint16 (num) {
|
||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||
}
|
||||
|
||||
setUint16 (pos, num) {
|
||||
this.data[pos] = num & bits8
|
||||
this.data[pos + 1] = (num >>> 8) & bits8
|
||||
}
|
||||
|
||||
writeUint32 (num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data.push(num & bits8)
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
setUint32 (pos, num) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.data[pos + i] = num & bits8
|
||||
num >>>= 8
|
||||
}
|
||||
}
|
||||
|
||||
writeVarUint (num) {
|
||||
while (num >= 0b10000000) {
|
||||
this.data.push(0b10000000 | (bits7 & num))
|
||||
num >>>= 7
|
||||
}
|
||||
this.data.push(bits7 & num)
|
||||
}
|
||||
|
||||
writeVarString (str) {
|
||||
let bytes = utf8.setBytesFromString(str)
|
||||
let len = bytes.length
|
||||
this.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
this.data.push(bytes[i])
|
||||
}
|
||||
}
|
||||
|
||||
writeID (id) {
|
||||
const user = id.user
|
||||
this.writeVarUint(user)
|
||||
if (user !== RootFakeUserID) {
|
||||
this.writeVarUint(id.clock)
|
||||
} else {
|
||||
this.writeVarString(id.name)
|
||||
this.writeVarUint(id.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/Connector.js
294
src/Connector.js
@@ -1,294 +0,0 @@
|
||||
import BinaryEncoder from './Binary/Encoder.js'
|
||||
import BinaryDecoder from './Binary/Decoder.js'
|
||||
|
||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
||||
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
export default class AbstractConnector {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
this.opts = opts
|
||||
if (opts.role == null || opts.role === 'master') {
|
||||
this.role = 'master'
|
||||
} else if (opts.role === 'slave') {
|
||||
this.role = 'slave'
|
||||
} else {
|
||||
throw new Error("Role must be either 'master' or 'slave'!")
|
||||
}
|
||||
this.log = debug('y:connector')
|
||||
this.logMessage = debug('y:connector-message')
|
||||
this._forwardAppliedStructs = opts.forwardAppliedOperations || false // TODO: rename
|
||||
this.role = opts.role
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.userEventListeners = []
|
||||
this.whenSyncedListeners = []
|
||||
this.currentSyncTarget = null
|
||||
this.debug = opts.debug === true
|
||||
this.broadcastBuffer = new BinaryEncoder()
|
||||
this.broadcastBufferSize = 0
|
||||
this.protocolVersion = 11
|
||||
this.authInfo = opts.auth || null
|
||||
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
||||
if (opts.maxBufferLength == null) {
|
||||
this.maxBufferLength = -1
|
||||
} else {
|
||||
this.maxBufferLength = opts.maxBufferLength
|
||||
}
|
||||
}
|
||||
|
||||
reconnect () {
|
||||
this.log('reconnecting..')
|
||||
}
|
||||
|
||||
disconnect () {
|
||||
this.log('discronnecting..')
|
||||
this.connections = new Map()
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.whenSyncedListeners = []
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
onUserEvent (f) {
|
||||
this.userEventListeners.push(f)
|
||||
}
|
||||
|
||||
removeUserEventListener (f) {
|
||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
||||
}
|
||||
|
||||
userLeft (user) {
|
||||
if (this.connections.has(user)) {
|
||||
this.log('%s: User left %s', this.y.userID, user)
|
||||
this.connections.delete(user)
|
||||
// check if isSynced event can be sent now
|
||||
this._setSyncedWith(null)
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userLeft',
|
||||
user: user
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userJoined (user, role, auth) {
|
||||
if (role == null) {
|
||||
throw new Error('You must specify the role of the joined user!')
|
||||
}
|
||||
if (this.connections.has(user)) {
|
||||
throw new Error('This user already joined!')
|
||||
}
|
||||
this.log('%s: User joined %s', this.y.userID, user)
|
||||
this.connections.set(user, {
|
||||
uid: user,
|
||||
isSynced: false,
|
||||
role: role,
|
||||
processAfterAuth: [],
|
||||
processAfterSync: [],
|
||||
auth: auth || null,
|
||||
receivedSyncStep2: false
|
||||
})
|
||||
let defer = {}
|
||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
||||
this.connections.get(user).syncStep2 = defer
|
||||
for (var f of this.userEventListeners) {
|
||||
f({
|
||||
action: 'userJoined',
|
||||
user: user,
|
||||
role: role
|
||||
})
|
||||
}
|
||||
this._syncWithUser(user)
|
||||
}
|
||||
|
||||
// Execute a function _when_ we are connected.
|
||||
// If not connected, wait until connected
|
||||
whenSynced (f) {
|
||||
if (this.isSynced) {
|
||||
f()
|
||||
} else {
|
||||
this.whenSyncedListeners.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
_syncWithUser (userID) {
|
||||
if (this.role === 'slave') {
|
||||
return // "The current sync has not finished or this is controlled by a master!"
|
||||
}
|
||||
sendSyncStep1(this, userID)
|
||||
}
|
||||
|
||||
_fireIsSyncedListeners () {
|
||||
if (!this.isSynced) {
|
||||
this.isSynced = true
|
||||
// It is safer to remove this!
|
||||
// call whensynced listeners
|
||||
for (var f of this.whenSyncedListeners) {
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = []
|
||||
this.y.emit('synced')
|
||||
}
|
||||
}
|
||||
|
||||
send (uid, buffer) {
|
||||
const y = this.y
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
||||
}
|
||||
this.log('User%s to User%s: Send \'%y\'', y.userID, uid, buffer)
|
||||
this.logMessage('User%s to User%s: Send %Y', y.userID, uid, [y, buffer])
|
||||
}
|
||||
|
||||
broadcast (buffer) {
|
||||
const y = this.y
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
||||
}
|
||||
this.log('User%s: Broadcast \'%y\'', y.userID, buffer)
|
||||
this.logMessage('User%s: Broadcast: %Y', y.userID, [y, buffer])
|
||||
}
|
||||
|
||||
/*
|
||||
Buffer operations, and broadcast them when ready.
|
||||
*/
|
||||
broadcastStruct (struct) {
|
||||
const firstContent = this.broadcastBuffer.length === 0
|
||||
if (firstContent) {
|
||||
this.broadcastBuffer.writeVarString(this.y.room)
|
||||
this.broadcastBuffer.writeVarString('update')
|
||||
this.broadcastBufferSize = 0
|
||||
this.broadcastBufferSizePos = this.broadcastBuffer.pos
|
||||
this.broadcastBuffer.writeUint32(0)
|
||||
}
|
||||
this.broadcastBufferSize++
|
||||
struct._toBinary(this.broadcastBuffer)
|
||||
if (this.maxBufferLength > 0 && this.broadcastBuffer.length > this.maxBufferLength) {
|
||||
// it is necessary to send the buffer now
|
||||
// cache the buffer and check if server is responsive
|
||||
const buffer = this.broadcastBuffer
|
||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
||||
this.broadcastBuffer = new BinaryEncoder()
|
||||
this.whenRemoteResponsive().then(() => {
|
||||
this.broadcast(buffer.createBuffer())
|
||||
})
|
||||
} else if (firstContent) {
|
||||
// send the buffer when all transactions are finished
|
||||
// (or buffer exceeds maxBufferLength)
|
||||
setTimeout(() => {
|
||||
if (this.broadcastBuffer.length > 0) {
|
||||
const buffer = this.broadcastBuffer
|
||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
||||
this.broadcast(buffer.createBuffer())
|
||||
this.broadcastBuffer = new BinaryEncoder()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Somehow check the responsiveness of the remote clients/server
|
||||
* Default behavior:
|
||||
* Wait 100ms before broadcasting the next batch of operations
|
||||
*
|
||||
* Only used when maxBufferLength is set
|
||||
*
|
||||
*/
|
||||
whenRemoteResponsive () {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
||||
*/
|
||||
receiveMessage (sender, buffer, skipAuth) {
|
||||
const y = this.y
|
||||
const userID = y.userID
|
||||
skipAuth = skipAuth || false
|
||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
||||
}
|
||||
if (sender === userID) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
let encoder = new BinaryEncoder()
|
||||
let roomname = decoder.readVarString() // read room name
|
||||
encoder.writeVarString(roomname)
|
||||
let messageType = decoder.readVarString()
|
||||
let senderConn = this.connections.get(sender)
|
||||
this.log('User%s from User%s: Receive \'%s\'', userID, sender, messageType)
|
||||
this.logMessage('User%s from User%s: Receive %Y', userID, sender, [y, buffer])
|
||||
if (senderConn == null && !skipAuth) {
|
||||
throw new Error('Received message from unknown peer!')
|
||||
}
|
||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
||||
let auth = decoder.readVarUint()
|
||||
if (senderConn.auth == null) {
|
||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
||||
// check auth
|
||||
return this.checkAuth(auth, y, sender).then(authPermissions => {
|
||||
if (senderConn.auth == null) {
|
||||
senderConn.auth = authPermissions
|
||||
y.emit('userAuthenticated', {
|
||||
user: senderConn.uid,
|
||||
auth: authPermissions
|
||||
})
|
||||
}
|
||||
let messages = senderConn.processAfterAuth
|
||||
senderConn.processAfterAuth = []
|
||||
|
||||
messages.forEach(m =>
|
||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
if ((skipAuth || senderConn.auth != null) && (messageType !== 'update' || senderConn.isSynced)) {
|
||||
this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
||||
} else {
|
||||
senderConn.processAfterSync.push([messageType, senderConn, decoder, encoder, sender, false])
|
||||
}
|
||||
}
|
||||
|
||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
||||
readSyncStep1(decoder, encoder, this.y, senderConn, sender)
|
||||
} else {
|
||||
const y = this.y
|
||||
y.transact(function () {
|
||||
if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||
readSyncStep2(decoder, encoder, y, senderConn, sender)
|
||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
||||
integrateRemoteStructs(decoder, encoder, y, senderConn, sender)
|
||||
} else {
|
||||
throw new Error('Unable to receive message')
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
}
|
||||
|
||||
_setSyncedWith (user) {
|
||||
if (user != null) {
|
||||
const userConn = this.connections.get(user)
|
||||
userConn.isSynced = true
|
||||
const messages = userConn.processAfterSync
|
||||
userConn.processAfterSync = []
|
||||
messages.forEach(m => {
|
||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
||||
})
|
||||
}
|
||||
const conns = Array.from(this.connections.values())
|
||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
||||
this._fireIsSyncedListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { deleteItemRange } from '../Struct/Delete.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(' -' + user + ':')
|
||||
let dvLength = decoder.readVarUint()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
|
||||
}
|
||||
}
|
||||
return strBuilder
|
||||
}
|
||||
|
||||
export function writeDeleteSet (y, encoder) {
|
||||
let currentUser = null
|
||||
let currentLength
|
||||
let lastLenPos
|
||||
|
||||
let numberOfUsers = 0
|
||||
let laterDSLenPus = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
|
||||
y.ds.iterate(null, null, function (n) {
|
||||
var user = n._id.user
|
||||
var clock = n._id.clock
|
||||
var len = n.len
|
||||
var gc = n.gc
|
||||
if (currentUser !== user) {
|
||||
numberOfUsers++
|
||||
// a new user was found
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoder.setUint32(lastLenPos, currentLength)
|
||||
}
|
||||
currentUser = user
|
||||
encoder.writeVarUint(user)
|
||||
// pseudo-fill pos
|
||||
lastLenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
currentLength = 0
|
||||
}
|
||||
encoder.writeVarUint(clock)
|
||||
encoder.writeVarUint(len)
|
||||
encoder.writeUint8(gc ? 1 : 0)
|
||||
currentLength++
|
||||
})
|
||||
if (currentUser !== null) { // happens on first iteration
|
||||
encoder.setUint32(lastLenPos, currentLength)
|
||||
}
|
||||
encoder.setUint32(laterDSLenPus, numberOfUsers)
|
||||
}
|
||||
|
||||
export function readDeleteSet (y, decoder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
for (let i = 0; i < dsLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let dv = []
|
||||
let dvLength = decoder.readUint32()
|
||||
for (let j = 0; j < dvLength; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let len = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
dv.push([from, len, gc])
|
||||
}
|
||||
if (dvLength > 0) {
|
||||
let pos = 0
|
||||
let d = dv[pos]
|
||||
let deletions = []
|
||||
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
|
||||
// cases:
|
||||
// 1. d deletes something to the right of n
|
||||
// => go to next n (break)
|
||||
// 2. d deletes something to the left of n
|
||||
// => create deletions
|
||||
// => reset d accordingly
|
||||
// *)=> if d doesn't delete anything anymore, go to next d (continue)
|
||||
// 3. not 2) and d deletes something that also n deletes
|
||||
// => reset d so that it doesn't contain n's deletion
|
||||
// *)=> if d does not delete anything anymore, go to next d (continue)
|
||||
while (d != null) {
|
||||
var diff = 0 // describe the diff of length in 1) and 2)
|
||||
if (n._id.clock + n.len <= d[0]) {
|
||||
// 1)
|
||||
break
|
||||
} else if (d[0] < n._id.clock) {
|
||||
// 2)
|
||||
// delete maximum the len of d
|
||||
// else delete as much as possible
|
||||
diff = Math.min(n._id.clock - d[0], d[1])
|
||||
// deleteItemRange(y, user, d[0], diff)
|
||||
deletions.push([user, d[0], diff])
|
||||
} else {
|
||||
// 3)
|
||||
diff = n._id.clock + n.len - d[0] // never null (see 1)
|
||||
if (d[2] && !n.gc) {
|
||||
// d marks as gc'd but n does not
|
||||
// then delete either way
|
||||
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
|
||||
deletions.push([user, d[0], Math.min(diff, d[1])])
|
||||
}
|
||||
}
|
||||
if (d[1] <= diff) {
|
||||
// d doesn't delete anything anymore
|
||||
d = dv[++pos]
|
||||
} else {
|
||||
d[0] = d[0] + diff // reset pos
|
||||
d[1] = d[1] - diff // reset length
|
||||
}
|
||||
}
|
||||
})
|
||||
// TODO: It would be more performant to apply the deletes in the above loop
|
||||
// Adapt the Tree implementation to support delete while iterating
|
||||
for (let i = deletions.length - 1; i >= 0; i--) {
|
||||
const del = deletions[i]
|
||||
deleteItemRange(y, del[0], del[1], del[2])
|
||||
}
|
||||
// for the rest.. just apply it
|
||||
for (; pos < dv.length; pos++) {
|
||||
d = dv[pos]
|
||||
deleteItemRange(y, user, d[0], d[1])
|
||||
// deletions.push([user, d[0], d[1], d[2]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import BinaryDecoder from '../Binary/Decoder.js'
|
||||
import { logID } from './messageToString.js'
|
||||
|
||||
class MissingEntry {
|
||||
constructor (decoder, missing, struct) {
|
||||
this.decoder = decoder
|
||||
this.missing = missing.length
|
||||
this.struct = struct
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate remote struct
|
||||
* When a remote struct is integrated, other structs might be ready to ready to
|
||||
* integrate.
|
||||
*/
|
||||
function _integrateRemoteStructHelper (y, struct) {
|
||||
const id = struct._id
|
||||
if (id === undefined) {
|
||||
struct._integrate(y)
|
||||
} else {
|
||||
if (y.ss.getState(id.user) > id.clock) {
|
||||
return
|
||||
}
|
||||
struct._integrate(y)
|
||||
let msu = y._missingStructs.get(id.user)
|
||||
if (msu != null) {
|
||||
let clock = id.clock
|
||||
const finalClock = clock + struct._length
|
||||
for (;clock < finalClock; clock++) {
|
||||
const missingStructs = msu.get(clock)
|
||||
if (missingStructs !== undefined) {
|
||||
missingStructs.forEach(missingDef => {
|
||||
missingDef.missing--
|
||||
if (missingDef.missing === 0) {
|
||||
const decoder = missingDef.decoder
|
||||
let oldPos = decoder.pos
|
||||
let missing = missingDef.struct._fromBinary(y, decoder)
|
||||
decoder.pos = oldPos
|
||||
if (missing.length === 0) {
|
||||
y._readyToIntegrate.push(missingDef.struct)
|
||||
}
|
||||
}
|
||||
})
|
||||
msu.delete(clock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function stringifyStructs (y, decoder, strBuilder) {
|
||||
const len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
let logMessage = ' ' + struct._logString()
|
||||
if (missing.length > 0) {
|
||||
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
|
||||
}
|
||||
strBuilder.push(logMessage)
|
||||
}
|
||||
}
|
||||
|
||||
export function integrateRemoteStructs (decoder, encoder, y) {
|
||||
const len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
let Constr = getStruct(reference)
|
||||
let struct = new Constr()
|
||||
let decoderPos = decoder.pos
|
||||
let missing = struct._fromBinary(y, decoder)
|
||||
if (missing.length === 0) {
|
||||
while (struct != null) {
|
||||
_integrateRemoteStructHelper(y, struct)
|
||||
struct = y._readyToIntegrate.shift()
|
||||
}
|
||||
} else {
|
||||
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
||||
_decoder.pos = decoderPos
|
||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
||||
let missingStructs = y._missingStructs
|
||||
for (let i = missing.length - 1; i >= 0; i--) {
|
||||
let m = missing[i]
|
||||
if (!missingStructs.has(m.user)) {
|
||||
missingStructs.set(m.user, new Map())
|
||||
}
|
||||
let msu = missingStructs.get(m.user)
|
||||
if (!msu.has(m.clock)) {
|
||||
msu.set(m.clock, [])
|
||||
}
|
||||
let mArray = msu = msu.get(m.clock)
|
||||
mArray.push(missingEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import BinaryDecoder from '../Binary/Decoder.js'
|
||||
import { stringifyStructs } from './integrateRemoteStructs.js'
|
||||
import { stringifySyncStep1 } from './syncStep1.js'
|
||||
import { stringifySyncStep2 } from './syncStep2.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import RootID from '../Util/RootID.js'
|
||||
import Y from '../Y.js'
|
||||
|
||||
export function messageToString ([y, buffer]) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // read roomname
|
||||
let type = decoder.readVarString()
|
||||
let strBuilder = []
|
||||
strBuilder.push('\n === ' + type + ' ===')
|
||||
if (type === 'update') {
|
||||
stringifyStructs(y, decoder, strBuilder)
|
||||
} else if (type === 'sync step 1') {
|
||||
stringifySyncStep1(y, decoder, strBuilder)
|
||||
} else if (type === 'sync step 2') {
|
||||
stringifySyncStep2(y, decoder, strBuilder)
|
||||
} else {
|
||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
||||
}
|
||||
return strBuilder.join('\n')
|
||||
}
|
||||
|
||||
export function messageToRoomname (buffer) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
decoder.readVarString() // roomname
|
||||
return decoder.readVarString() // messageType
|
||||
}
|
||||
|
||||
export function logID (id) {
|
||||
if (id !== null && id._id != null) {
|
||||
id = id._id
|
||||
}
|
||||
if (id === null) {
|
||||
return '()'
|
||||
} else if (id instanceof ID) {
|
||||
return `(${id.user},${id.clock})`
|
||||
} else if (id instanceof RootID) {
|
||||
return `(${id.name},${id.type})`
|
||||
} else if (id.constructor === Y) {
|
||||
return `y`
|
||||
} else {
|
||||
throw new Error('This is not a valid ID!')
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
export function readStateSet (decoder) {
|
||||
let ss = new Map()
|
||||
let ssLength = decoder.readUint32()
|
||||
for (let i = 0; i < ssLength; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ss.set(user, clock)
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
export function writeStateSet (y, encoder) {
|
||||
let lenPosition = encoder.pos
|
||||
let len = 0
|
||||
encoder.writeUint32(0)
|
||||
for (let [user, clock] of y.ss.state) {
|
||||
encoder.writeVarUint(user)
|
||||
encoder.writeVarUint(clock)
|
||||
len++
|
||||
}
|
||||
encoder.setUint32(lenPosition, len)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import BinaryEncoder from '../Binary/Encoder.js'
|
||||
import { readStateSet, writeStateSet } from './stateSet.js'
|
||||
import { writeDeleteSet } from './deleteSet.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
||||
let auth = decoder.readVarString()
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
strBuilder.push(` - auth: "${auth}"`)
|
||||
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
|
||||
// write SS
|
||||
let ssBuilder = []
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
let clock = decoder.readVarUint()
|
||||
ssBuilder.push(`(${user}:${clock})`)
|
||||
}
|
||||
strBuilder.push(' == SS: ' + ssBuilder.join(','))
|
||||
}
|
||||
|
||||
export function sendSyncStep1 (connector, syncUser) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(connector.y.room)
|
||||
encoder.writeVarString('sync step 1')
|
||||
encoder.writeVarString(connector.authInfo || '')
|
||||
encoder.writeVarUint(connector.protocolVersion)
|
||||
writeStateSet(connector.y, encoder)
|
||||
connector.send(syncUser, encoder.createBuffer())
|
||||
}
|
||||
|
||||
export default function writeStructs (encoder, decoder, y, ss) {
|
||||
const lenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
let len = 0
|
||||
for (let user of y.ss.state.keys()) {
|
||||
let clock = ss.get(user) || 0
|
||||
if (user !== RootFakeUserID) {
|
||||
y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) {
|
||||
struct._toBinary(encoder)
|
||||
len++
|
||||
})
|
||||
}
|
||||
}
|
||||
encoder.setUint32(lenPos, len)
|
||||
}
|
||||
|
||||
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
||||
let protocolVersion = decoder.readVarUint()
|
||||
// check protocol version
|
||||
if (protocolVersion !== y.connector.protocolVersion) {
|
||||
console.warn(
|
||||
`You tried to sync with a Yjs instance that has a different protocol version
|
||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
||||
`)
|
||||
y.destroy()
|
||||
}
|
||||
// write sync step 2
|
||||
encoder.writeVarString('sync step 2')
|
||||
encoder.writeVarString(y.connector.authInfo || '')
|
||||
const ss = readStateSet(decoder)
|
||||
writeStructs(encoder, decoder, y, ss)
|
||||
writeDeleteSet(y, encoder)
|
||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
if (y.connector.role === 'slave') {
|
||||
sendSyncStep1(y.connector, sender)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||
import { readDeleteSet } from './deleteSet.js'
|
||||
|
||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
||||
strBuilder.push(' - auth: ' + decoder.readVarString())
|
||||
strBuilder.push(' == OS:')
|
||||
stringifyStructs(y, decoder, strBuilder)
|
||||
// write DS to string
|
||||
strBuilder.push(' == DS:')
|
||||
let len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let user = decoder.readVarUint()
|
||||
strBuilder.push(` User: ${user}: `)
|
||||
let len2 = decoder.readUint32()
|
||||
for (let j = 0; j < len2; j++) {
|
||||
let from = decoder.readVarUint()
|
||||
let to = decoder.readVarUint()
|
||||
let gc = decoder.readUint8() === 1
|
||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
||||
integrateRemoteStructs(decoder, encoder, y)
|
||||
readDeleteSet(y, decoder)
|
||||
y.connector._setSyncedWith(sender)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// import BinaryEncoder from './Binary/Encoder.js'
|
||||
|
||||
export default function extendPersistence (Y) {
|
||||
class AbstractPersistence {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
this.opts = opts
|
||||
this.saveOperationsBuffer = []
|
||||
this.log = Y.debug('y:persistence')
|
||||
}
|
||||
|
||||
saveToMessageQueue (binary) {
|
||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
||||
}
|
||||
|
||||
saveOperations (ops) {
|
||||
ops = ops.map(function (op) {
|
||||
return Y.Struct[op.struct].encode(op)
|
||||
})
|
||||
/*
|
||||
const saveOperations = () => {
|
||||
if (this.saveOperationsBuffer.length > 0) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(this.opts.room)
|
||||
encoder.writeVarString('update')
|
||||
let ops = this.saveOperationsBuffer
|
||||
this.saveOperationsBuffer = []
|
||||
let length = ops.length
|
||||
encoder.writeUint32(length)
|
||||
for (var i = 0; i < length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
this.saveToMessageQueue(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (this.saveOperationsBuffer.length === 0) {
|
||||
this.saveOperationsBuffer = ops
|
||||
} else {
|
||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Y.AbstractPersistence = AbstractPersistence
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import Tree from '../Util/Tree.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
class DSNode {
|
||||
constructor (id, len, gc) {
|
||||
this._id = id
|
||||
this.len = len
|
||||
this.gc = gc
|
||||
}
|
||||
clone () {
|
||||
return new DSNode(this._id, this.len, this.gc)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DeleteStore extends Tree {
|
||||
logTable () {
|
||||
const deletes = []
|
||||
this.iterate(null, null, function (n) {
|
||||
deletes.push({
|
||||
user: n._id.user,
|
||||
clock: n._id.clock,
|
||||
len: n.len,
|
||||
gc: n.gc
|
||||
})
|
||||
})
|
||||
console.table(deletes)
|
||||
}
|
||||
isDeleted (id) {
|
||||
var n = this.findWithUpperBound(id)
|
||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
||||
}
|
||||
/*
|
||||
* Mark an operation as deleted. returns the deleted node
|
||||
*/
|
||||
markDeleted (id, length) {
|
||||
if (length == null) {
|
||||
throw new Error('length must be defined')
|
||||
}
|
||||
var n = this.findWithUpperBound(id)
|
||||
if (n != null && n._id.user === id.user) {
|
||||
if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
|
||||
// id is in n's range
|
||||
var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
|
||||
if (diff > 0) {
|
||||
// id+length overlaps n
|
||||
if (!n.gc) {
|
||||
n.len += diff
|
||||
} else {
|
||||
diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
|
||||
if (diff < length) {
|
||||
// a partial deletion
|
||||
let nId = id.clone()
|
||||
nId.clock += diff
|
||||
n = new DSNode(nId, length - diff, false)
|
||||
this.put(n)
|
||||
} else {
|
||||
// already gc'd
|
||||
throw new Error(
|
||||
'DS reached an inconsistent state. Please report this issue!'
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no overlapping, already deleted
|
||||
return n
|
||||
}
|
||||
} else {
|
||||
// cannot extend left (there is no left!)
|
||||
n = new DSNode(id, length, false)
|
||||
this.put(n) // TODO: you double-put !!
|
||||
}
|
||||
} else {
|
||||
// cannot extend left
|
||||
n = new DSNode(id, length, false)
|
||||
this.put(n)
|
||||
}
|
||||
// can extend right?
|
||||
var next = this.findNext(n._id)
|
||||
if (
|
||||
next != null &&
|
||||
n._id.user === next._id.user &&
|
||||
n._id.clock + n.len >= next._id.clock
|
||||
) {
|
||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||
while (diff >= 0) {
|
||||
// n overlaps with next
|
||||
if (next.gc) {
|
||||
// gc is stronger, so reduce length of n
|
||||
n.len -= diff
|
||||
if (diff >= next.len) {
|
||||
// delete the missing range after next
|
||||
diff = diff - next.len // missing range after next
|
||||
if (diff > 0) {
|
||||
this.put(n) // unneccessary? TODO!
|
||||
this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
// we can extend n with next
|
||||
if (diff > next.len) {
|
||||
// n is even longer than next
|
||||
// get next.next, and try to extend it
|
||||
var _next = this.findNext(next._id)
|
||||
this.delete(next._id)
|
||||
if (_next == null || n._id.user !== _next._id.user) {
|
||||
break
|
||||
} else {
|
||||
next = _next
|
||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
||||
// continue!
|
||||
}
|
||||
} else {
|
||||
// n just partially overlaps with next. extend n, delete next, and break this loop
|
||||
n.len += next.len - diff
|
||||
this.delete(next._id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.put(n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import Tree from '../Util/Tree.js'
|
||||
import RootID from '../Util/RootID.js'
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class OperationStore extends Tree {
|
||||
constructor (y) {
|
||||
super()
|
||||
this.y = y
|
||||
}
|
||||
logTable () {
|
||||
const items = []
|
||||
this.iterate(null, null, function (item) {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||
left: logID(item._left === null ? null : item._left._lastId),
|
||||
right: logID(item._right),
|
||||
right_origin: logID(item._right_origin),
|
||||
parent: logID(item._parent),
|
||||
parentSub: item._parentSub,
|
||||
deleted: item._deleted,
|
||||
content: JSON.stringify(item._content)
|
||||
})
|
||||
})
|
||||
console.table(items)
|
||||
}
|
||||
get (id) {
|
||||
let struct = this.find(id)
|
||||
if (struct === null && id instanceof RootID) {
|
||||
const Constr = getStruct(id.type)
|
||||
const y = this.y
|
||||
struct = new Constr()
|
||||
struct._id = id
|
||||
struct._parent = y
|
||||
y.transact(() => {
|
||||
struct._integrate(y)
|
||||
})
|
||||
this.put(struct)
|
||||
}
|
||||
return struct
|
||||
}
|
||||
// Use getItem for structs with _length > 1
|
||||
getItem (id) {
|
||||
var item = this.findWithUpperBound(id)
|
||||
if (item === null) {
|
||||
return null
|
||||
}
|
||||
const itemID = item._id
|
||||
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
|
||||
return item
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
// Return an insertion such that id is the first element of content
|
||||
// This function manipulates an item, if necessary
|
||||
getItemCleanStart (id) {
|
||||
var ins = this.getItem(id)
|
||||
if (ins === null || ins._length === 1) {
|
||||
return ins
|
||||
}
|
||||
const insID = ins._id
|
||||
if (insID.clock === id.clock) {
|
||||
return ins
|
||||
} else {
|
||||
return ins._splitAt(this.y, id.clock - insID.clock)
|
||||
}
|
||||
}
|
||||
// Return an insertion such that id is the last element of content
|
||||
// This function manipulates an operation, if necessary
|
||||
getItemCleanEnd (id) {
|
||||
var ins = this.getItem(id)
|
||||
if (ins === null || ins._length === 1) {
|
||||
return ins
|
||||
}
|
||||
const insID = ins._id
|
||||
if (insID.clock + ins._length - 1 === id.clock) {
|
||||
return ins
|
||||
} else {
|
||||
ins._splitAt(this.y, id.clock - insID.clock + 1)
|
||||
return ins
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
export default class StateStore {
|
||||
constructor (y) {
|
||||
this.y = y
|
||||
this.state = new Map()
|
||||
}
|
||||
logTable () {
|
||||
const entries = []
|
||||
for (let [user, state] of this.state) {
|
||||
entries.push({
|
||||
user, state
|
||||
})
|
||||
}
|
||||
console.table(entries)
|
||||
}
|
||||
getNextID (len) {
|
||||
const user = this.y.userID
|
||||
const state = this.getState(user)
|
||||
this.setState(user, state + len)
|
||||
return new ID(user, state)
|
||||
}
|
||||
updateRemoteState (struct) {
|
||||
let user = struct._id.user
|
||||
let userState = this.state.get(user)
|
||||
while (struct !== null && struct._id.clock === userState) {
|
||||
userState += struct._length
|
||||
struct = this.y.os.get(new ID(user, userState))
|
||||
}
|
||||
this.state.set(user, userState)
|
||||
}
|
||||
getState (user) {
|
||||
let state = this.state.get(user)
|
||||
if (state == null) {
|
||||
return 0
|
||||
}
|
||||
return state
|
||||
}
|
||||
setState (user, state) {
|
||||
// TODO: modify missingi structs here
|
||||
const beforeState = this.y._transaction.beforeState
|
||||
if (!beforeState.has(user)) {
|
||||
beforeState.set(user, this.getState(user))
|
||||
}
|
||||
this.state.set(user, state)
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { getReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
/**
|
||||
* Delete all items in an ID-range
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||
*/
|
||||
export function deleteItemRange (y, user, clock, range) {
|
||||
const createDelete = y.connector._forwardAppliedStructs
|
||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||
if (item !== null) {
|
||||
if (!item._deleted) {
|
||||
item._splitAt(y, range)
|
||||
item._delete(y, createDelete)
|
||||
}
|
||||
let itemLen = item._length
|
||||
range -= itemLen
|
||||
clock += itemLen
|
||||
if (range > 0) {
|
||||
let node = y.os.findNode(new ID(user, clock))
|
||||
while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||
const nodeVal = node.val
|
||||
if (!nodeVal._deleted) {
|
||||
nodeVal._splitAt(y, range)
|
||||
nodeVal._delete(y, createDelete)
|
||||
}
|
||||
const nodeLen = nodeVal._length
|
||||
range -= nodeLen
|
||||
clock += nodeLen
|
||||
node = node.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete is not a real struct. It will not be saved in OS
|
||||
*/
|
||||
export default class Delete {
|
||||
constructor () {
|
||||
this._target = null
|
||||
this._length = null
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
const targetID = decoder.readID()
|
||||
this._targetID = targetID
|
||||
this._length = decoder.readVarUint()
|
||||
if (y.os.getItem(targetID) === null) {
|
||||
return [targetID]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
encoder.writeVarUint(this._length)
|
||||
}
|
||||
/**
|
||||
* - If created remotely (a remote user deleted something),
|
||||
* this Delete is applied to all structs in id-range.
|
||||
* - If created lokally (e.g. when y-array deletes a range of elements),
|
||||
* this struct is broadcasted only (it is already executed)
|
||||
*/
|
||||
_integrate (y, locallyCreated = false) {
|
||||
if (!locallyCreated) {
|
||||
// from remote
|
||||
const id = this._targetID
|
||||
deleteItemRange(y, id.user, id.clock, this._length)
|
||||
} else {
|
||||
// from local
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveOperations(this)
|
||||
}
|
||||
}
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
import { getReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
import Delete from './Delete.js'
|
||||
import { transactionTypeChanged } from '../Transaction.js'
|
||||
|
||||
/**
|
||||
* Helper utility to split an Item (see _splitAt)
|
||||
* - copy all properties from a to b
|
||||
* - connect a to b
|
||||
* - assigns the correct _id
|
||||
* - save b to os
|
||||
*/
|
||||
export function splitHelper (y, a, b, diff) {
|
||||
const aID = a._id
|
||||
b._id = new ID(aID.user, aID.clock + diff)
|
||||
b._origin = a
|
||||
b._left = a
|
||||
b._right = a._right
|
||||
if (b._right !== null) {
|
||||
b._right._left = b
|
||||
}
|
||||
b._right_origin = a._right_origin
|
||||
// do not set a._right_origin, as this will lead to problems when syncing
|
||||
a._right = b
|
||||
b._parent = a._parent
|
||||
b._parentSub = a._parentSub
|
||||
b._deleted = a._deleted
|
||||
// now search all relevant items to the right and update origin
|
||||
// if origin is not it foundOrigins, we don't have to search any longer
|
||||
let foundOrigins = new Set()
|
||||
foundOrigins.add(a)
|
||||
let o = b._right
|
||||
while (o !== null && foundOrigins.has(o._origin)) {
|
||||
if (o._origin === a) {
|
||||
o._origin = b
|
||||
}
|
||||
foundOrigins.add(o)
|
||||
o = o._right
|
||||
}
|
||||
y.os.put(b)
|
||||
}
|
||||
|
||||
export default class Item {
|
||||
constructor () {
|
||||
this._id = null
|
||||
this._origin = null
|
||||
this._left = null
|
||||
this._right = null
|
||||
this._right_origin = null
|
||||
this._parent = null
|
||||
this._parentSub = null
|
||||
this._deleted = false
|
||||
}
|
||||
/**
|
||||
* Copy the effect of struct
|
||||
*/
|
||||
_copy () {
|
||||
let struct = new this.constructor()
|
||||
struct._origin = this._left
|
||||
struct._left = this._left
|
||||
struct._right = this
|
||||
struct._right_origin = this
|
||||
struct._parent = this._parent
|
||||
struct._parentSub = this._parentSub
|
||||
return struct
|
||||
}
|
||||
get _lastId () {
|
||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||
}
|
||||
get _length () {
|
||||
return 1
|
||||
}
|
||||
/**
|
||||
* Splits this struct so that another struct can be inserted in-between.
|
||||
* This must be overwritten if _length > 1
|
||||
* Returns right part after split
|
||||
* - diff === 0 => this
|
||||
* - diff === length => this._right
|
||||
* - otherwise => split _content and return right part of split
|
||||
* (see ItemJSON/ItemString for implementation)
|
||||
*/
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
}
|
||||
return this._right
|
||||
}
|
||||
_delete (y, createDelete = true) {
|
||||
this._deleted = true
|
||||
y.ds.markDeleted(this._id, this._length)
|
||||
if (createDelete) {
|
||||
let del = new Delete()
|
||||
del._targetID = this._id
|
||||
del._length = this._length
|
||||
del._integrate(y, true)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
y._transaction.deletedStructs.add(this)
|
||||
}
|
||||
/**
|
||||
* This is called right before this struct receives any children.
|
||||
* It can be overwritten to apply pending changes before applying remote changes
|
||||
*/
|
||||
_beforeChange () {
|
||||
// nop
|
||||
}
|
||||
/*
|
||||
* - Integrate the struct so that other types/structs can see it
|
||||
* - Add this struct to y.os
|
||||
* - Check if this is struct deleted
|
||||
*/
|
||||
_integrate (y) {
|
||||
const parent = this._parent
|
||||
const selfID = this._id
|
||||
const userState = selfID === null ? 0 : y.ss.getState(selfID.user)
|
||||
if (selfID === null) {
|
||||
this._id = y.ss.getNextID(this._length)
|
||||
} else if (selfID.user === RootFakeUserID) {
|
||||
// nop
|
||||
} else if (selfID.clock < userState) {
|
||||
// already applied..
|
||||
return []
|
||||
} else if (selfID.clock === userState) {
|
||||
y.ss.setState(selfID.user, userState + this._length)
|
||||
} else {
|
||||
// missing content from user
|
||||
throw new Error('Can not apply yet!')
|
||||
}
|
||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
||||
// this is the first time parent is updated
|
||||
// or this types is new
|
||||
this._parent._beforeChange()
|
||||
}
|
||||
/*
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change
|
||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
*/
|
||||
// handle conflicts
|
||||
let o
|
||||
// set o to the first conflicting item
|
||||
if (this._left !== null) {
|
||||
o = this._left._right
|
||||
} else if (this._parentSub !== null) {
|
||||
o = this._parent._map.get(this._parentSub) || null
|
||||
} else {
|
||||
o = this._parent._start
|
||||
}
|
||||
let conflictingItems = new Set()
|
||||
let itemsBeforeOrigin = new Set()
|
||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
||||
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
||||
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
||||
while (o !== null && o !== this._right) {
|
||||
itemsBeforeOrigin.add(o)
|
||||
conflictingItems.add(o)
|
||||
if (this._origin === o._origin) {
|
||||
// case 1
|
||||
if (o._id.user < this._id.user) {
|
||||
this._left = o
|
||||
conflictingItems.clear()
|
||||
}
|
||||
} else if (itemsBeforeOrigin.has(o._origin)) {
|
||||
// case 2
|
||||
if (!conflictingItems.has(o._origin)) {
|
||||
this._left = o
|
||||
conflictingItems.clear()
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
// TODO: try to use right_origin instead.
|
||||
// Then you could basically omit conflictingItems!
|
||||
// Note: you probably can't use right_origin in every case.. only when setting _left
|
||||
o = o._right
|
||||
}
|
||||
// reconnect left/right + update parent map/start if necessary
|
||||
const parentSub = this._parentSub
|
||||
if (this._left === null) {
|
||||
let right
|
||||
if (parentSub !== null) {
|
||||
const pmap = parent._map
|
||||
right = pmap.get(parentSub) || null
|
||||
pmap.set(parentSub, this)
|
||||
} else {
|
||||
right = parent._start
|
||||
parent._start = this
|
||||
}
|
||||
this._right = right
|
||||
if (right !== null) {
|
||||
right._left = this
|
||||
}
|
||||
} else {
|
||||
const left = this._left
|
||||
const right = left._right
|
||||
this._right = right
|
||||
left._right = this
|
||||
if (right !== null) {
|
||||
right._left = this
|
||||
}
|
||||
}
|
||||
if (parent._deleted) {
|
||||
this._delete(y, false)
|
||||
}
|
||||
y.os.put(this)
|
||||
transactionTypeChanged(y, parent, parentSub)
|
||||
if (this._id.user !== RootFakeUserID) {
|
||||
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveOperations(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
let info = 0
|
||||
if (this._origin !== null) {
|
||||
info += 0b1 // origin is defined
|
||||
}
|
||||
// TODO: remove
|
||||
/* no longer send _left
|
||||
if (this._left !== this._origin) {
|
||||
info += 0b10 // do not copy origin to left
|
||||
}
|
||||
*/
|
||||
if (this._right_origin !== null) {
|
||||
info += 0b100
|
||||
}
|
||||
if (this._parentSub !== null) {
|
||||
info += 0b1000
|
||||
}
|
||||
encoder.writeUint8(info)
|
||||
encoder.writeID(this._id)
|
||||
if (info & 0b1) {
|
||||
encoder.writeID(this._origin._lastId)
|
||||
}
|
||||
// TODO: remove
|
||||
/* see above
|
||||
if (info & 0b10) {
|
||||
encoder.writeID(this._left._lastId)
|
||||
}
|
||||
*/
|
||||
if (info & 0b100) {
|
||||
encoder.writeID(this._right_origin._id)
|
||||
}
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
encoder.writeID(this._parent._id)
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
||||
}
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = []
|
||||
const info = decoder.readUint8()
|
||||
const id = decoder.readID()
|
||||
this._id = id
|
||||
// read origin
|
||||
if (info & 0b1) {
|
||||
// origin != null
|
||||
const originID = decoder.readID()
|
||||
// we have to query for left again because it might have been split/merged..
|
||||
const origin = y.os.getItemCleanEnd(originID)
|
||||
if (origin === null) {
|
||||
missing.push(originID)
|
||||
} else {
|
||||
this._origin = origin
|
||||
this._left = this._origin
|
||||
}
|
||||
}
|
||||
// read right
|
||||
if (info & 0b100) {
|
||||
// right != null
|
||||
const rightID = decoder.readID()
|
||||
// we have to query for right again because it might have been split/merged..
|
||||
const right = y.os.getItemCleanStart(rightID)
|
||||
if (right === null) {
|
||||
missing.push(rightID)
|
||||
} else {
|
||||
this._right = right
|
||||
this._right_origin = right
|
||||
}
|
||||
}
|
||||
// read parent
|
||||
if ((info & 0b101) === 0) {
|
||||
// neither origin nor right is defined
|
||||
const parentID = decoder.readID()
|
||||
// parent does not change, so we don't have to search for it again
|
||||
if (this._parent === null) {
|
||||
const parent = y.os.get(parentID)
|
||||
if (parent === null) {
|
||||
missing.push(parentID)
|
||||
} else {
|
||||
this._parent = parent
|
||||
}
|
||||
}
|
||||
} else if (this._parent === null) {
|
||||
if (this._origin !== null) {
|
||||
this._parent = this._origin._parent
|
||||
} else if (this._right_origin !== null) {
|
||||
this._parent = this._right_origin._parent
|
||||
}
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
||||
this._parentSub = JSON.parse(decoder.readVarString())
|
||||
}
|
||||
if (y.ss.getState(id.user) < id.clock) {
|
||||
missing.push(new ID(id.user, id.clock - 1))
|
||||
}
|
||||
return missing
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { splitHelper, default as Item } from './Item.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class ItemJSON extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this._content = null
|
||||
}
|
||||
_copy () {
|
||||
let struct = super._copy()
|
||||
struct._content = this._content
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
let len = decoder.readVarUint()
|
||||
this._content = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const ctnt = decoder.readVarString()
|
||||
let parsed
|
||||
if (ctnt === 'undefined') {
|
||||
parsed = undefined
|
||||
} else {
|
||||
parsed = JSON.parse(ctnt)
|
||||
}
|
||||
this._content[i] = parsed
|
||||
}
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
encoder.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
let encoded
|
||||
let content = this._content[i]
|
||||
if (content === undefined) {
|
||||
encoded = 'undefined'
|
||||
} else {
|
||||
encoded = JSON.stringify(content)
|
||||
}
|
||||
encoder.writeVarString(encoded)
|
||||
}
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
} else if (diff >= this._length) {
|
||||
return this._right
|
||||
}
|
||||
let item = new ItemJSON()
|
||||
item._content = this._content.splice(diff)
|
||||
splitHelper(y, this, item, diff)
|
||||
return item
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { splitHelper, default as Item } from './Item.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class ItemString extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this._content = null
|
||||
}
|
||||
_copy () {
|
||||
let struct = super._copy()
|
||||
struct._content = this._content
|
||||
return struct
|
||||
}
|
||||
get _length () {
|
||||
return this._content.length
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
let missing = super._fromBinary(y, decoder)
|
||||
this._content = decoder.readVarString()
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this._content)
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
return this
|
||||
} else if (diff >= this._length) {
|
||||
return this._right
|
||||
}
|
||||
let item = new ItemString()
|
||||
item._content = this._content.slice(diff)
|
||||
this._content = this._content.slice(0, diff)
|
||||
splitHelper(y, this, item, diff)
|
||||
return item
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import Item from './Item.js'
|
||||
import EventHandler from '../Util/EventHandler.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
// restructure children as if they were inserted one after another
|
||||
function integrateChildren (y, start) {
|
||||
let right
|
||||
do {
|
||||
right = start._right
|
||||
start._right = null
|
||||
start._right_origin = null
|
||||
start._origin = start._left
|
||||
start._integrate(y)
|
||||
start = right
|
||||
} while (right !== null)
|
||||
}
|
||||
|
||||
export function getListItemIDByPosition (type, i) {
|
||||
let pos = 0
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
if (pos <= i && i < pos + n._length) {
|
||||
const id = n._id
|
||||
return new ID(id.user, id.clock + i - pos)
|
||||
}
|
||||
pos++
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
|
||||
export default class Type extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
this._map = new Map()
|
||||
this._start = null
|
||||
this._y = null
|
||||
this._eventHandler = new EventHandler()
|
||||
this._deepEventHandler = new EventHandler()
|
||||
}
|
||||
getPathTo (type) {
|
||||
if (type === this) {
|
||||
return []
|
||||
}
|
||||
const path = []
|
||||
const y = this._y
|
||||
while (type._parent !== this && this._parent !== y) {
|
||||
let parent = type._parent
|
||||
if (type._parentSub !== null) {
|
||||
path.push(type._parentSub)
|
||||
} else {
|
||||
// parent is array-ish
|
||||
for (let [i, child] of parent) {
|
||||
if (child === type) {
|
||||
path.push(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
type = parent
|
||||
}
|
||||
if (this._parent !== this) {
|
||||
throw new Error('The type is not a child of this node')
|
||||
}
|
||||
return path
|
||||
}
|
||||
_callEventHandler (event) {
|
||||
const changedParentTypes = this._y._transaction.changedParentTypes
|
||||
this._eventHandler.callEventListeners(event)
|
||||
let type = this
|
||||
while (type !== this._y) {
|
||||
let events = changedParentTypes.get(type)
|
||||
if (events === undefined) {
|
||||
events = []
|
||||
changedParentTypes.set(type, events)
|
||||
}
|
||||
events.push(event)
|
||||
type = type._parent
|
||||
}
|
||||
}
|
||||
_copy (undeleteChildren) {
|
||||
let copy = super._copy()
|
||||
let map = new Map()
|
||||
copy._map = map
|
||||
for (let [key, value] of this._map) {
|
||||
if (undeleteChildren.has(value) || !value.deleted) {
|
||||
let _item = value._copy(undeleteChildren)
|
||||
_item._parent = copy
|
||||
map.set(key, value._copy(undeleteChildren))
|
||||
}
|
||||
}
|
||||
let prevUndeleted = null
|
||||
copy._start = null
|
||||
let item = this._start
|
||||
while (item !== null) {
|
||||
if (undeleteChildren.has(item) || !item.deleted) {
|
||||
let _item = item._copy(undeleteChildren)
|
||||
_item._left = prevUndeleted
|
||||
_item._origin = prevUndeleted
|
||||
_item._right = null
|
||||
_item._right_origin = null
|
||||
_item._parent = copy
|
||||
if (prevUndeleted === null) {
|
||||
copy._start = _item
|
||||
} else {
|
||||
prevUndeleted._right = _item
|
||||
}
|
||||
prevUndeleted = _item
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
return copy
|
||||
}
|
||||
_transact (f) {
|
||||
const y = this._y
|
||||
if (y !== null) {
|
||||
y.transact(f)
|
||||
} else {
|
||||
f(y)
|
||||
}
|
||||
}
|
||||
observe (f) {
|
||||
this._eventHandler.addEventListener(f)
|
||||
}
|
||||
observeDeep (f) {
|
||||
this._deepEventHandler.addEventListener(f)
|
||||
}
|
||||
unobserve (f) {
|
||||
this._eventHandler.removeEventListener(f)
|
||||
}
|
||||
unobserveDeep (f) {
|
||||
this._deepEventHandler.removeEventListener(f)
|
||||
}
|
||||
_integrate (y) {
|
||||
y._transaction.newTypes.add(this)
|
||||
super._integrate(y)
|
||||
this._y = y
|
||||
// when integrating children we must make sure to
|
||||
// integrate start
|
||||
const start = this._start
|
||||
if (start !== null) {
|
||||
this._start = null
|
||||
integrateChildren(y, start)
|
||||
}
|
||||
// integrate map children
|
||||
const map = this._map
|
||||
this._map = new Map()
|
||||
for (let t of map.values()) {
|
||||
// TODO make sure that right elements are deleted!
|
||||
integrateChildren(y, t)
|
||||
}
|
||||
}
|
||||
_delete (y, createDelete) {
|
||||
super._delete(y, createDelete)
|
||||
y._transaction.changedTypes.delete(this)
|
||||
// delete map types
|
||||
for (let value of this._map.values()) {
|
||||
if (value instanceof Item && !value._deleted) {
|
||||
value._delete(y, false)
|
||||
}
|
||||
}
|
||||
// delete array types
|
||||
let t = this._start
|
||||
while (t !== null) {
|
||||
if (!t._deleted) {
|
||||
t._delete(y, false)
|
||||
}
|
||||
t = t._right
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
|
||||
export default class Transaction {
|
||||
constructor (y) {
|
||||
this.y = y
|
||||
// types added during transaction
|
||||
this.newTypes = new Set()
|
||||
// changed types (does not include new types)
|
||||
// maps from type to parentSubs (item._parentSub = null for array elements)
|
||||
this.changedTypes = new Map()
|
||||
this.deletedStructs = new Set()
|
||||
this.beforeState = new Map()
|
||||
this.changedParentTypes = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
export function transactionTypeChanged (y, type, sub) {
|
||||
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
|
||||
const changedTypes = y._transaction.changedTypes
|
||||
let subs = changedTypes.get(type)
|
||||
if (subs === undefined) {
|
||||
// create if it doesn't exist yet
|
||||
subs = new Set()
|
||||
changedTypes.set(type, subs)
|
||||
}
|
||||
subs.add(sub)
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import Type from '../Struct/Type.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import YEvent from '../Util/YEvent.js'
|
||||
|
||||
class YArrayEvent extends YEvent {
|
||||
constructor (yarray, remote) {
|
||||
super(yarray)
|
||||
this.remote = remote
|
||||
}
|
||||
}
|
||||
|
||||
export default class YArray extends Type {
|
||||
_callObserver (parentSubs, remote) {
|
||||
this._callEventHandler(new YArrayEvent(this, remote))
|
||||
}
|
||||
get (pos) {
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
if (pos < n._length) {
|
||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
||||
return n._content[pos]
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
}
|
||||
pos -= n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
toArray () {
|
||||
return this.map(c => c)
|
||||
}
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
if (c.toJSON !== null) {
|
||||
return c.toJSON()
|
||||
} else {
|
||||
return c.toString()
|
||||
}
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
map (f) {
|
||||
const res = []
|
||||
this.forEach((c, i) => {
|
||||
res.push(f(c, i, this))
|
||||
})
|
||||
return res
|
||||
}
|
||||
forEach (f) {
|
||||
let pos = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
if (n instanceof Type) {
|
||||
f(n, pos++, this)
|
||||
} else {
|
||||
const content = n._content
|
||||
const contentLen = content.length
|
||||
for (let i = 0; i < contentLen; i++) {
|
||||
pos++
|
||||
f(content[i], pos, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
get length () {
|
||||
let length = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
length += n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
return length
|
||||
}
|
||||
[Symbol.iterator] () {
|
||||
return {
|
||||
next: function () {
|
||||
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
||||
// item is deleted or itemElement does not exist (is deleted)
|
||||
this._item = this._item._right
|
||||
this._itemElement = 0
|
||||
}
|
||||
if (this._item === null) {
|
||||
return {
|
||||
done: true
|
||||
}
|
||||
}
|
||||
let content
|
||||
if (this._item instanceof Type) {
|
||||
content = this._item
|
||||
} else {
|
||||
content = this._item._content[this._itemElement++]
|
||||
}
|
||||
return {
|
||||
value: [this._count, content],
|
||||
done: false
|
||||
}
|
||||
},
|
||||
_item: this._start,
|
||||
_itemElement: 0,
|
||||
_count: 0
|
||||
}
|
||||
}
|
||||
delete (pos, length = 1) {
|
||||
this._y.transact(() => {
|
||||
let item = this._start
|
||||
let count = 0
|
||||
while (item !== null && length > 0) {
|
||||
if (!item._deleted) {
|
||||
if (count <= pos && pos < count + item._length) {
|
||||
const diffDel = pos - count
|
||||
item = item._splitAt(this._y, diffDel)
|
||||
item._splitAt(this._y, length)
|
||||
length -= item._length
|
||||
item._delete(this._y)
|
||||
count += diffDel
|
||||
} else {
|
||||
count += item._length
|
||||
}
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
})
|
||||
if (length > 0) {
|
||||
throw new Error('Delete exceeds the range of the YArray')
|
||||
}
|
||||
}
|
||||
insertAfter (left, content) {
|
||||
this._transact(y => {
|
||||
let right
|
||||
if (left === null) {
|
||||
right = this._start
|
||||
} else {
|
||||
right = left._right
|
||||
}
|
||||
let prevJsonIns = null
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let c = content[i]
|
||||
if (typeof c === 'function') {
|
||||
c = new c() // eslint-disable-line new-cap
|
||||
}
|
||||
if (c instanceof Type) {
|
||||
if (prevJsonIns !== null) {
|
||||
if (y !== null) {
|
||||
prevJsonIns._integrate(y)
|
||||
}
|
||||
left = prevJsonIns
|
||||
prevJsonIns = null
|
||||
}
|
||||
c._origin = left
|
||||
c._left = left
|
||||
c._right = right
|
||||
c._right_origin = right
|
||||
c._parent = this
|
||||
if (y !== null) {
|
||||
c._integrate(y)
|
||||
} else if (left === null) {
|
||||
this._start = c
|
||||
} else {
|
||||
left._right = c
|
||||
}
|
||||
left = c
|
||||
} else {
|
||||
if (prevJsonIns === null) {
|
||||
prevJsonIns = new ItemJSON()
|
||||
prevJsonIns._origin = left
|
||||
prevJsonIns._left = left
|
||||
prevJsonIns._right = right
|
||||
prevJsonIns._right_origin = right
|
||||
prevJsonIns._parent = this
|
||||
prevJsonIns._content = []
|
||||
}
|
||||
prevJsonIns._content.push(c)
|
||||
}
|
||||
}
|
||||
if (prevJsonIns !== null && y !== null) {
|
||||
prevJsonIns._integrate(y)
|
||||
}
|
||||
})
|
||||
}
|
||||
insert (pos, content) {
|
||||
let left = null
|
||||
let right = this._start
|
||||
let count = 0
|
||||
const y = this._y
|
||||
while (right !== null) {
|
||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||
if (count <= pos && pos <= count + rightLen) {
|
||||
const splitDiff = pos - count
|
||||
right = right._splitAt(y, splitDiff)
|
||||
left = right._left
|
||||
count += splitDiff
|
||||
break
|
||||
}
|
||||
if (!right._deleted) {
|
||||
count += right._length
|
||||
}
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
if (pos > count) {
|
||||
throw new Error('Position exceeds array range!')
|
||||
}
|
||||
this.insertAfter(left, content)
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `YArray(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
}
|
||||
114
src/Type/YMap.js
114
src/Type/YMap.js
@@ -1,114 +0,0 @@
|
||||
import Type from '../Struct/Type.js'
|
||||
import Item from '../Struct/Item.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import YEvent from '../Util/YEvent.js'
|
||||
|
||||
class YMapEvent extends YEvent {
|
||||
constructor (ymap, subs, remote) {
|
||||
super(ymap)
|
||||
this.keysChanged = subs
|
||||
this.remote = remote
|
||||
}
|
||||
}
|
||||
|
||||
export default class YMap extends Type {
|
||||
_callObserver (parentSubs, remote) {
|
||||
this._callEventHandler(new YMapEvent(this, parentSubs, remote))
|
||||
}
|
||||
toJSON () {
|
||||
const map = {}
|
||||
for (let [key, item] of this._map) {
|
||||
if (!item._deleted) {
|
||||
let res
|
||||
if (item instanceof Type) {
|
||||
if (item.toJSON !== undefined) {
|
||||
res = item.toJSON()
|
||||
} else {
|
||||
res = item.toString()
|
||||
}
|
||||
} else {
|
||||
res = item._content[0]
|
||||
}
|
||||
map[key] = res
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
keys () {
|
||||
let keys = []
|
||||
for (let [key, value] of this._map) {
|
||||
if (!value._deleted) {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
delete (key) {
|
||||
this._transact((y) => {
|
||||
let c = this._map.get(key)
|
||||
if (y !== null && c !== undefined) {
|
||||
c._delete(y)
|
||||
}
|
||||
})
|
||||
}
|
||||
set (key, value) {
|
||||
this._transact(y => {
|
||||
const old = this._map.get(key) || null
|
||||
if (old !== null) {
|
||||
if (old instanceof ItemJSON && old._content[0] === value) {
|
||||
// Trying to overwrite with same value
|
||||
// break here
|
||||
return value
|
||||
}
|
||||
if (y !== null) {
|
||||
old._delete(y)
|
||||
}
|
||||
}
|
||||
let v
|
||||
if (typeof value === 'function') {
|
||||
v = new value() // eslint-disable-line new-cap
|
||||
value = v
|
||||
} else if (value instanceof Item) {
|
||||
v = value
|
||||
} else {
|
||||
v = new ItemJSON()
|
||||
v._content = [value]
|
||||
}
|
||||
v._right = old
|
||||
v._right_origin = old
|
||||
v._parent = this
|
||||
v._parentSub = key
|
||||
if (y !== null) {
|
||||
v._integrate(y)
|
||||
} else {
|
||||
this._map.set(key, v)
|
||||
}
|
||||
})
|
||||
return value
|
||||
}
|
||||
get (key) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
return undefined
|
||||
}
|
||||
if (v instanceof Type) {
|
||||
return v
|
||||
} else {
|
||||
return v._content[v._content.length - 1]
|
||||
}
|
||||
}
|
||||
has (key) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `YMap(id:${logID(this._id)},mapSize:${this._map.size},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
import YArray from './YArray.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
export default class YText extends YArray {
|
||||
constructor (string) {
|
||||
super()
|
||||
if (typeof string === 'string') {
|
||||
const start = new ItemString()
|
||||
start._parent = this
|
||||
start._content = string
|
||||
this._start = start
|
||||
}
|
||||
}
|
||||
toString () {
|
||||
const strBuilder = []
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
strBuilder.push(n._content)
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
return strBuilder.join('')
|
||||
}
|
||||
insert (pos, text) {
|
||||
this._transact(y => {
|
||||
let left = null
|
||||
let right = this._start
|
||||
let count = 0
|
||||
while (right !== null) {
|
||||
if (count <= pos && pos < count + right._length) {
|
||||
const splitDiff = pos - count
|
||||
right = right._splitAt(this._y, pos - count)
|
||||
left = right._left
|
||||
count += splitDiff
|
||||
break
|
||||
}
|
||||
count += right._length
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
if (pos > count) {
|
||||
throw new Error('Position exceeds array range!')
|
||||
}
|
||||
let item = new ItemString()
|
||||
item._origin = left
|
||||
item._left = left
|
||||
item._right = right
|
||||
item._right_origin = right
|
||||
item._parent = this
|
||||
item._content = text
|
||||
if (y !== null) {
|
||||
item._integrate(this._y)
|
||||
} else if (left === null) {
|
||||
this._start = item
|
||||
} else {
|
||||
left._right = item
|
||||
}
|
||||
})
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `YText(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
// import diff from 'fast-diff'
|
||||
import { defaultDomFilter } from './utils.js'
|
||||
|
||||
import YMap from '../YMap.js'
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
|
||||
export default class YXmlElement extends YXmlFragment {
|
||||
constructor (arg1, arg2) {
|
||||
super()
|
||||
this.nodeName = null
|
||||
this._scrollElement = null
|
||||
if (typeof arg1 === 'string') {
|
||||
this.nodeName = arg1.toUpperCase()
|
||||
} else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === arg1.ELEMENT_NODE) {
|
||||
this.nodeName = arg1.nodeName
|
||||
this._setDom(arg1)
|
||||
} else {
|
||||
this.nodeName = 'UNDEFINED'
|
||||
}
|
||||
if (typeof arg2 === 'function') {
|
||||
this._domFilter = arg2
|
||||
}
|
||||
}
|
||||
_copy (undeleteChildren) {
|
||||
let struct = super._copy(undeleteChildren)
|
||||
struct.nodeName = this.nodeName
|
||||
return struct
|
||||
}
|
||||
_setDom (dom) {
|
||||
if (this._dom != null) {
|
||||
throw new Error('Only call this method if you know what you are doing ;)')
|
||||
} else if (dom._yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
|
||||
throw new Error('Already bound to an YXml type')
|
||||
} else {
|
||||
this._dom = dom
|
||||
dom._yxml = this
|
||||
// tag is already set in constructor
|
||||
// set attributes
|
||||
let attrNames = []
|
||||
for (let i = 0; i < dom.attributes.length; i++) {
|
||||
attrNames.push(dom.attributes[i].name)
|
||||
}
|
||||
attrNames = this._domFilter(dom, attrNames)
|
||||
for (let i = 0; i < attrNames.length; i++) {
|
||||
let attrName = attrNames[i]
|
||||
let attrValue = dom.getAttribute(attrName)
|
||||
this.setAttribute(attrName, attrValue)
|
||||
}
|
||||
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes))
|
||||
this._bindToDom(dom)
|
||||
return dom
|
||||
}
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.nodeName = decoder.readVarString()
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.nodeName)
|
||||
}
|
||||
_integrate (y) {
|
||||
if (this.nodeName === null) {
|
||||
throw new Error('nodeName must be defined!')
|
||||
}
|
||||
if (this._domFilter === defaultDomFilter && this._parent instanceof YXmlFragment) {
|
||||
this._domFilter = this._parent._domFilter
|
||||
}
|
||||
super._integrate(y)
|
||||
}
|
||||
/**
|
||||
* Returns the string representation of the XML document.
|
||||
* The attributes are ordered by attribute-name, so you can easily use this
|
||||
* method to compare YXmlElements
|
||||
*/
|
||||
toString () {
|
||||
const attrs = this.getAttributes()
|
||||
const stringBuilder = []
|
||||
const keys = []
|
||||
for (let key in attrs) {
|
||||
keys.push(key)
|
||||
}
|
||||
keys.sort()
|
||||
const keysLen = keys.length
|
||||
for (let i = 0; i < keysLen; i++) {
|
||||
const key = keys[i]
|
||||
stringBuilder.push(key + '="' + attrs[key] + '"')
|
||||
}
|
||||
const nodeName = this.nodeName.toLocaleLowerCase()
|
||||
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
||||
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
||||
}
|
||||
removeAttribute () {
|
||||
return YMap.prototype.delete.apply(this, arguments)
|
||||
}
|
||||
|
||||
setAttribute () {
|
||||
return YMap.prototype.set.apply(this, arguments)
|
||||
}
|
||||
|
||||
getAttribute () {
|
||||
return YMap.prototype.get.apply(this, arguments)
|
||||
}
|
||||
|
||||
getAttributes () {
|
||||
const obj = {}
|
||||
for (let [key, value] of this._map) {
|
||||
obj[key] = value._content[0]
|
||||
}
|
||||
return obj
|
||||
}
|
||||
getDom (_document) {
|
||||
_document = _document || document
|
||||
let dom = this._dom
|
||||
if (dom == null) {
|
||||
dom = _document.createElement(this.nodeName)
|
||||
this._dom = dom
|
||||
dom._yxml = this
|
||||
let attrs = this.getAttributes()
|
||||
for (let key in attrs) {
|
||||
dom.setAttribute(key, attrs[key])
|
||||
}
|
||||
this.forEach(yxml => {
|
||||
dom.appendChild(yxml.getDom(_document))
|
||||
})
|
||||
this._bindToDom(dom)
|
||||
}
|
||||
return dom
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
export default class YXmlEvent extends YEvent {
|
||||
constructor (target, subs, remote) {
|
||||
super(target)
|
||||
this.childListChanged = false
|
||||
this.attributesChanged = new Set()
|
||||
this.remote = remote
|
||||
subs.forEach((sub) => {
|
||||
if (sub === null) {
|
||||
this.childListChanged = true
|
||||
} else {
|
||||
this.attributesChanged.add(sub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
/* global MutationObserver */
|
||||
|
||||
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
||||
|
||||
import YArray from '../YArray.js'
|
||||
import YXmlText from './YXmlText.js'
|
||||
import YXmlEvent from './YXmlEvent.js'
|
||||
import { logID } from '../../MessageHandler/messageToString.js'
|
||||
import diff from 'fast-diff'
|
||||
|
||||
function domToYXml (parent, doms) {
|
||||
const types = []
|
||||
doms.forEach(d => {
|
||||
if (d._yxml != null && d._yxml !== false) {
|
||||
d._yxml._unbindFromDom()
|
||||
}
|
||||
if (parent._domFilter(d, []) !== null) {
|
||||
let type
|
||||
if (d.nodeType === d.TEXT_NODE) {
|
||||
type = new YXmlText(d)
|
||||
} else if (d.nodeType === d.ELEMENT_NODE) {
|
||||
type = new YXmlFragment._YXmlElement(d, parent._domFilter)
|
||||
} else {
|
||||
throw new Error('Unsupported node!')
|
||||
}
|
||||
type.enableSmartScrolling(parent._scrollElement)
|
||||
types.push(type)
|
||||
} else {
|
||||
d._yxml = false
|
||||
}
|
||||
})
|
||||
return types
|
||||
}
|
||||
|
||||
class YXmlTreeWalker {
|
||||
constructor (root, f) {
|
||||
this._filter = f || (() => true)
|
||||
this._root = root
|
||||
this._currentNode = root
|
||||
this._firstCall = true
|
||||
}
|
||||
[Symbol.iterator] () {
|
||||
return this
|
||||
}
|
||||
next () {
|
||||
let n = this._currentNode
|
||||
if (this._firstCall) {
|
||||
this._firstCall = false
|
||||
if (!n._deleted && this._filter(n)) {
|
||||
return { value: n, done: false }
|
||||
}
|
||||
}
|
||||
do {
|
||||
if (!n._deleted && n.constructor === YXmlFragment._YXmlElement && n._start !== null) {
|
||||
// walk down in the tree
|
||||
n = n._start
|
||||
} else {
|
||||
// walk right or up in the tree
|
||||
while (n !== this._root) {
|
||||
if (n._right !== null) {
|
||||
n = n._right
|
||||
break
|
||||
}
|
||||
n = n._parent
|
||||
}
|
||||
if (n === this._root) {
|
||||
n = null
|
||||
}
|
||||
}
|
||||
if (n === this._root) {
|
||||
break
|
||||
}
|
||||
} while (n !== null && (n._deleted || !this._filter(n)))
|
||||
this._currentNode = n
|
||||
if (n === null) {
|
||||
return { done: true }
|
||||
} else {
|
||||
return { value: n, done: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class YXmlFragment extends YArray {
|
||||
constructor () {
|
||||
super()
|
||||
this._dom = null
|
||||
this._domFilter = defaultDomFilter
|
||||
this._domObserver = null
|
||||
// this function makes sure that either the
|
||||
// dom event is executed, or the yjs observer is executed
|
||||
var token = true
|
||||
this._mutualExclude = f => {
|
||||
if (token) {
|
||||
token = false
|
||||
try {
|
||||
f()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this._domObserver.takeRecords()
|
||||
token = true
|
||||
}
|
||||
}
|
||||
}
|
||||
createTreeWalker (filter) {
|
||||
return new YXmlTreeWalker(this, filter)
|
||||
}
|
||||
/**
|
||||
* Retrieve first element that matches *query*
|
||||
* Similar to DOM's querySelector, but only accepts a subset of its queries
|
||||
*
|
||||
* Query support:
|
||||
* - tagname
|
||||
* TODO:
|
||||
* - id
|
||||
* - attribute
|
||||
*/
|
||||
querySelector (query) {
|
||||
query = query.toUpperCase()
|
||||
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
|
||||
const next = iterator.next()
|
||||
if (next.done) {
|
||||
return null
|
||||
} else {
|
||||
return next.value
|
||||
}
|
||||
}
|
||||
querySelectorAll (query) {
|
||||
query = query.toUpperCase()
|
||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
||||
}
|
||||
enableSmartScrolling (scrollElement) {
|
||||
this._scrollElement = scrollElement
|
||||
this.forEach(xml => {
|
||||
xml.enableSmartScrolling(scrollElement)
|
||||
})
|
||||
}
|
||||
setDomFilter (f) {
|
||||
this._domFilter = f
|
||||
this.forEach(xml => {
|
||||
xml.setDomFilter(f)
|
||||
})
|
||||
}
|
||||
_callObserver (parentSubs, remote) {
|
||||
this._callEventHandler(new YXmlEvent(this, parentSubs, remote))
|
||||
}
|
||||
toString () {
|
||||
return this.map(xml => xml.toString()).join('')
|
||||
}
|
||||
_delete (y, createDelete) {
|
||||
this._unbindFromDom()
|
||||
super._delete(y, createDelete)
|
||||
}
|
||||
_unbindFromDom () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserver.disconnect()
|
||||
this._domObserver = null
|
||||
}
|
||||
if (this._dom != null) {
|
||||
this._dom._yxml = null
|
||||
this._dom = null
|
||||
}
|
||||
}
|
||||
insertDomElementsAfter (prev, doms) {
|
||||
const types = domToYXml(this, doms)
|
||||
this.insertAfter(prev, types)
|
||||
return types
|
||||
}
|
||||
insertDomElements (pos, doms) {
|
||||
const types = domToYXml(this, doms)
|
||||
this.insert(pos, types)
|
||||
return types
|
||||
}
|
||||
getDom () {
|
||||
return this._dom
|
||||
}
|
||||
bindToDom (dom) {
|
||||
if (this._dom != null) {
|
||||
this._unbindFromDom()
|
||||
}
|
||||
if (dom._yxml != null) {
|
||||
dom._yxml._unbindFromDom()
|
||||
}
|
||||
if (MutationObserver == null) {
|
||||
throw new Error('Not able to bind to a DOM element, because MutationObserver is not available!')
|
||||
}
|
||||
dom.innerHTML = ''
|
||||
this._dom = dom
|
||||
dom._yxml = this
|
||||
this.forEach(t => {
|
||||
dom.insertBefore(t.getDom(), null)
|
||||
})
|
||||
this._bindToDom(dom)
|
||||
}
|
||||
// binds to a dom element
|
||||
// Only call if dom and YXml are isomorph
|
||||
_bindToDom (dom) {
|
||||
if (this._parent === null || this._parent._dom != null || typeof MutationObserver === 'undefined') {
|
||||
// only bind if parent did not already bind
|
||||
return
|
||||
}
|
||||
this._y.on('beforeTransaction', () => {
|
||||
this._domObserverListener(this._domObserver.takeRecords())
|
||||
})
|
||||
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
|
||||
this._y.on('afterTransaction', afterTransactionSelectionFixer)
|
||||
// Apply Y.Xml events to dom
|
||||
this.observeDeep(reflectChangesOnDom.bind(this))
|
||||
// Apply Dom changes on Y.Xml
|
||||
this._domObserverListener = mutations => {
|
||||
this._mutualExclude(() => {
|
||||
this._y.transact(() => {
|
||||
let diffChildren = new Set()
|
||||
mutations.forEach(mutation => {
|
||||
const dom = mutation.target
|
||||
const yxml = dom._yxml
|
||||
if (yxml == null) {
|
||||
// dom element is filtered
|
||||
return
|
||||
}
|
||||
switch (mutation.type) {
|
||||
case 'characterData':
|
||||
var diffs = diff(yxml.toString(), dom.nodeValue)
|
||||
var pos = 0
|
||||
for (var i = 0; i < diffs.length; i++) {
|
||||
var d = diffs[i]
|
||||
if (d[0] === 0) { // EQUAL
|
||||
pos += d[1].length
|
||||
} else if (d[0] === -1) { // DELETE
|
||||
yxml.delete(pos, d[1].length)
|
||||
} else { // INSERT
|
||||
yxml.insert(pos, d[1])
|
||||
pos += d[1].length
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'attributes':
|
||||
let name = mutation.attributeName
|
||||
// check if filter accepts attribute
|
||||
if (this._domFilter(dom, [name]).length > 0 && this.constructor !== YXmlFragment) {
|
||||
var val = dom.getAttribute(name)
|
||||
if (yxml.getAttribute(name) !== val) {
|
||||
if (val == null) {
|
||||
yxml.removeAttribute(name)
|
||||
} else {
|
||||
yxml.setAttribute(name, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'childList':
|
||||
diffChildren.add(mutation.target)
|
||||
break
|
||||
}
|
||||
})
|
||||
for (let dom of diffChildren) {
|
||||
if (dom._yxml != null && dom._yxml !== false) {
|
||||
applyChangesFromDom(dom)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
||||
this._domObserver.observe(dom, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
})
|
||||
return dom
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
return `YXml(id:${logID(this._id)},left:${logID(left)},origin:${logID(origin)},right:${this._right},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import YText from '../YText.js'
|
||||
|
||||
export default class YXmlText extends YText {
|
||||
constructor (arg1) {
|
||||
let dom = null
|
||||
let initialText = null
|
||||
if (arg1 != null) {
|
||||
if (arg1.nodeType != null && arg1.nodeType === arg1.TEXT_NODE) {
|
||||
dom = arg1
|
||||
initialText = dom.nodeValue
|
||||
} else if (typeof arg1 === 'string') {
|
||||
initialText = arg1
|
||||
}
|
||||
}
|
||||
super(initialText)
|
||||
this._dom = null
|
||||
this._domObserver = null
|
||||
this._domObserverListener = null
|
||||
this._scrollElement = null
|
||||
if (dom !== null) {
|
||||
this._setDom(arg1)
|
||||
}
|
||||
/*
|
||||
var token = true
|
||||
this._mutualExclude = f => {
|
||||
if (token) {
|
||||
token = false
|
||||
try {
|
||||
f()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this._domObserver.takeRecords()
|
||||
token = true
|
||||
}
|
||||
}
|
||||
this.observe(event => {
|
||||
if (this._dom != null) {
|
||||
const dom = this._dom
|
||||
this._mutualExclude(() => {
|
||||
let anchorViewPosition = getAnchorViewPosition(this._scrollElement)
|
||||
let anchorViewFix
|
||||
if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) {
|
||||
anchorViewFix = anchorViewPosition
|
||||
} else {
|
||||
anchorViewFix = null
|
||||
}
|
||||
dom.nodeValue = this.toString()
|
||||
fixScrollPosition(this._scrollElement, anchorViewFix)
|
||||
})
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
setDomFilter () {}
|
||||
enableSmartScrolling (scrollElement) {
|
||||
this._scrollElement = scrollElement
|
||||
}
|
||||
_setDom (dom) {
|
||||
if (this._dom != null) {
|
||||
this._unbindFromDom()
|
||||
}
|
||||
if (dom._yxml != null) {
|
||||
dom._yxml._unbindFromDom()
|
||||
}
|
||||
// set marker
|
||||
this._dom = dom
|
||||
dom._yxml = this
|
||||
}
|
||||
getDom (_document) {
|
||||
_document = _document || document
|
||||
if (this._dom === null) {
|
||||
const dom = _document.createTextNode(this.toString())
|
||||
this._setDom(dom)
|
||||
return dom
|
||||
}
|
||||
return this._dom
|
||||
}
|
||||
_delete (y, createDelete) {
|
||||
this._unbindFromDom()
|
||||
super._delete(y, createDelete)
|
||||
}
|
||||
_unbindFromDom () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserver.disconnect()
|
||||
this._domObserver = null
|
||||
}
|
||||
if (this._dom != null) {
|
||||
this._dom._yxml = null
|
||||
this._dom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/* globals getSelection */
|
||||
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
|
||||
let browserSelection = null
|
||||
let relativeSelection = null
|
||||
|
||||
export let beforeTransactionSelectionFixer
|
||||
if (typeof getSelection !== 'undefined') {
|
||||
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, remote) {
|
||||
if (!remote) {
|
||||
return
|
||||
}
|
||||
relativeSelection = { from: null, to: null, fromY: null, toY: null }
|
||||
browserSelection = getSelection()
|
||||
const anchorNode = browserSelection.anchorNode
|
||||
if (anchorNode !== null && anchorNode._yxml != null) {
|
||||
const yxml = anchorNode._yxml
|
||||
relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset)
|
||||
relativeSelection.fromY = yxml._y
|
||||
}
|
||||
const focusNode = browserSelection.focusNode
|
||||
if (focusNode !== null && focusNode._yxml != null) {
|
||||
const yxml = focusNode._yxml
|
||||
relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset)
|
||||
relativeSelection.toY = yxml._y
|
||||
}
|
||||
}
|
||||
} else {
|
||||
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
||||
}
|
||||
|
||||
export function afterTransactionSelectionFixer (y, remote) {
|
||||
if (relativeSelection === null || !remote) {
|
||||
return
|
||||
}
|
||||
const to = relativeSelection.to
|
||||
const from = relativeSelection.from
|
||||
const fromY = relativeSelection.fromY
|
||||
const toY = relativeSelection.toY
|
||||
let shouldUpdate = false
|
||||
let anchorNode = browserSelection.anchorNode
|
||||
let anchorOffset = browserSelection.anchorOffset
|
||||
let focusNode = browserSelection.focusNode
|
||||
let focusOffset = browserSelection.focusOffset
|
||||
if (from !== null) {
|
||||
let sel = fromRelativePosition(fromY, from)
|
||||
if (sel !== null) {
|
||||
shouldUpdate = true
|
||||
anchorNode = sel.type.getDom()
|
||||
anchorOffset = sel.offset
|
||||
}
|
||||
}
|
||||
if (to !== null) {
|
||||
let sel = fromRelativePosition(toY, to)
|
||||
if (sel !== null) {
|
||||
focusNode = sel.type.getDom()
|
||||
focusOffset = sel.offset
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
browserSelection.setBaseAndExtent(
|
||||
anchorNode,
|
||||
anchorOffset,
|
||||
focusNode,
|
||||
focusOffset
|
||||
)
|
||||
}
|
||||
// delete, so the objects can be gc'd
|
||||
relativeSelection = null
|
||||
browserSelection = null
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
import YXmlText from './YXmlText.js'
|
||||
|
||||
export function defaultDomFilter (node, attributes) {
|
||||
return attributes
|
||||
}
|
||||
|
||||
export function getAnchorViewPosition (scrollElement) {
|
||||
if (scrollElement == null) {
|
||||
return null
|
||||
}
|
||||
let anchor = document.getSelection().anchorNode
|
||||
if (anchor != null) {
|
||||
let top = getBoundingClientRect(anchor).top
|
||||
if (top >= 0 && top <= document.documentElement.clientHeight) {
|
||||
return {
|
||||
anchor: anchor,
|
||||
top: top
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
anchor: null,
|
||||
scrollTop: scrollElement.scrollTop,
|
||||
scrollHeight: scrollElement.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// get BoundingClientRect that works on text nodes
|
||||
export function getBoundingClientRect (element) {
|
||||
if (element.getBoundingClientRect != null) {
|
||||
// is element node
|
||||
return element.getBoundingClientRect()
|
||||
} else {
|
||||
// is text node
|
||||
if (element.parentNode == null) {
|
||||
// range requires that text nodes have a parent
|
||||
let span = document.createElement('span')
|
||||
span.appendChild(element)
|
||||
}
|
||||
let range = document.createRange()
|
||||
range.selectNode(element)
|
||||
return range.getBoundingClientRect()
|
||||
}
|
||||
}
|
||||
|
||||
export function fixScrollPosition (scrollElement, fix) {
|
||||
if (scrollElement !== null && fix !== null) {
|
||||
if (fix.anchor === null) {
|
||||
if (scrollElement.scrollTop === fix.scrollTop) {
|
||||
scrollElement.scrollTop = scrollElement.scrollHeight - fix.scrollHeight
|
||||
}
|
||||
} else {
|
||||
scrollElement.scrollTop = getBoundingClientRect(fix.anchor).top - fix.top
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function iterateUntilUndeleted (item) {
|
||||
while (item !== null && item._deleted) {
|
||||
item = item._right
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
function _insertNodeHelper (yxml, prevExpectedNode, child) {
|
||||
let insertedNodes = yxml.insertDomElementsAfter(prevExpectedNode, [child])
|
||||
if (insertedNodes.length > 0) {
|
||||
return insertedNodes[0]
|
||||
} else {
|
||||
return prevExpectedNode
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Check if any of the nodes was deleted
|
||||
* 2. Iterate over the children.
|
||||
* 2.1 If a node exists without _yxml property, insert a new node
|
||||
* 2.2 If _contents.length < dom.childNodes.length, fill the
|
||||
* rest of _content with childNodes
|
||||
* 2.3 If a node was moved, delete it and
|
||||
* recreate a new yxml element that is bound to that node.
|
||||
* You can detect that a node was moved because expectedId
|
||||
* !== actualId in the list
|
||||
*/
|
||||
export function applyChangesFromDom (dom) {
|
||||
const yxml = dom._yxml
|
||||
const y = yxml._y
|
||||
let knownChildren =
|
||||
new Set(
|
||||
Array.prototype.map.call(dom.childNodes, child => child._yxml)
|
||||
.filter(id => id !== undefined)
|
||||
)
|
||||
// 1. Check if any of the nodes was deleted
|
||||
yxml.forEach(function (childType, i) {
|
||||
if (!knownChildren.has(childType)) {
|
||||
childType._delete(y)
|
||||
}
|
||||
})
|
||||
// 2. iterate
|
||||
let childNodes = dom.childNodes
|
||||
let len = childNodes.length
|
||||
let prevExpectedNode = null
|
||||
let expectedNode = iterateUntilUndeleted(yxml._start)
|
||||
for (let domCnt = 0; domCnt < len; domCnt++) {
|
||||
const child = childNodes[domCnt]
|
||||
const childYXml = child._yxml
|
||||
if (childYXml != null) {
|
||||
if (childYXml === false) {
|
||||
// should be ignored or is going to be deleted
|
||||
continue
|
||||
}
|
||||
if (expectedNode !== null) {
|
||||
if (expectedNode !== childYXml) {
|
||||
// 2.3 Not expected node
|
||||
if (childYXml._parent !== this) {
|
||||
// element is going to be deleted by its previous parent
|
||||
child._yxml = null
|
||||
} else {
|
||||
childYXml._delete(y)
|
||||
}
|
||||
prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child)
|
||||
} else {
|
||||
prevExpectedNode = expectedNode
|
||||
expectedNode = iterateUntilUndeleted(expectedNode._right)
|
||||
}
|
||||
// if this is the expected node id, just continue
|
||||
} else {
|
||||
// 2.2 fill _conten with child nodes
|
||||
prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child)
|
||||
}
|
||||
} else {
|
||||
// 2.1 A new node was found
|
||||
prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function reflectChangesOnDom (events) {
|
||||
// Make sure that no filtered attributes are applied to the structure
|
||||
// if they were, delete them
|
||||
events.forEach(event => {
|
||||
const target = event.target
|
||||
const keys = this._domFilter(target.nodeName, Array.from(event.keysChanged))
|
||||
if (keys === null) {
|
||||
target._delete()
|
||||
} else {
|
||||
const removeKeys = new Set() // is a copy of event.keysChanged
|
||||
event.keysChanged.forEach(key => { removeKeys.add(key) })
|
||||
keys.forEach(key => {
|
||||
// remove all accepted keys from removeKeys
|
||||
removeKeys.delete(key)
|
||||
})
|
||||
// remove the filtered attribute
|
||||
removeKeys.forEach(key => {
|
||||
target.removeAttribute(key)
|
||||
})
|
||||
}
|
||||
})
|
||||
this._mutualExclude(() => {
|
||||
events.forEach(event => {
|
||||
const yxml = event.target
|
||||
const dom = yxml._dom
|
||||
if (dom != null) {
|
||||
// TODO: do this once before applying stuff
|
||||
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
||||
if (yxml.constructor === YXmlText) {
|
||||
yxml._dom.nodeValue = yxml.toString()
|
||||
} else {
|
||||
// update attributes
|
||||
event.attributesChanged.forEach(attributeName => {
|
||||
const value = yxml.getAttribute(attributeName)
|
||||
if (value === undefined) {
|
||||
dom.removeAttribute(attributeName)
|
||||
} else {
|
||||
dom.setAttribute(attributeName, value)
|
||||
}
|
||||
})
|
||||
if (event.childListChanged) {
|
||||
// create fragment of undeleted nodes
|
||||
const fragment = document.createDocumentFragment()
|
||||
yxml.forEach(function (t) {
|
||||
fragment.append(t.getDom())
|
||||
})
|
||||
// remove remainding nodes
|
||||
let lastChild = dom.lastChild
|
||||
while (lastChild !== null) {
|
||||
dom.removeChild(lastChild)
|
||||
lastChild = dom.lastChild
|
||||
}
|
||||
// insert fragment of undeleted nodes
|
||||
dom.append(fragment)
|
||||
}
|
||||
}
|
||||
/* TODO: smartscrolling
|
||||
.. else if (event.type === 'childInserted' || event.type === 'insert') {
|
||||
let nodes = event.values
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
let node = nodes[i]
|
||||
node.setDomFilter(yxml._domFilter)
|
||||
node.enableSmartScrolling(yxml._scrollElement)
|
||||
let dom = node.getDom()
|
||||
let fixPosition = null
|
||||
let nextDom = null
|
||||
if (yxml._content.length > event.index + i + 1) {
|
||||
nextDom = yxml.get(event.index + i + 1).getDom()
|
||||
}
|
||||
yxml._dom.insertBefore(dom, nextDom)
|
||||
if (anchorViewPosition === null) {
|
||||
// nop
|
||||
} else if (anchorViewPosition.anchor !== null) {
|
||||
// no scrolling when current selection
|
||||
if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
|
||||
fixPosition = anchorViewPosition
|
||||
}
|
||||
} else if (getBoundingClientRect(dom).top <= 0) {
|
||||
// adjust scrolling if modified element is out of view,
|
||||
// there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
|
||||
fixPosition = anchorViewPosition
|
||||
}
|
||||
fixScrollPosition(yxml._scrollElement, fixPosition)
|
||||
}
|
||||
} else if (event.type === 'childRemoved' || event.type === 'delete') {
|
||||
for (let i = event.values.length - 1; i >= 0; i--) {
|
||||
let dom = event.values[i]._dom
|
||||
let fixPosition = null
|
||||
if (anchorViewPosition === null) {
|
||||
// nop
|
||||
} else if (anchorViewPosition.anchor !== null) {
|
||||
// no scrolling when current selection
|
||||
if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
|
||||
fixPosition = anchorViewPosition
|
||||
}
|
||||
} else if (getBoundingClientRect(dom).top <= 0) {
|
||||
// adjust scrolling if modified element is out of view,
|
||||
// there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
|
||||
fixPosition = anchorViewPosition
|
||||
}
|
||||
dom.remove()
|
||||
fixScrollPosition(yxml._scrollElement, fixPosition)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
import YXmlElement from './YXmlElement.js'
|
||||
|
||||
export { default as YXmlFragment } from './YXmlFragment.js'
|
||||
export { default as YXmlElement } from './YXmlElement.js'
|
||||
export { default as YXmlText } from './YXmlText.js'
|
||||
|
||||
YXmlFragment._YXmlElement = YXmlElement
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
export default class EventHandler {
|
||||
constructor () {
|
||||
this.eventListeners = []
|
||||
}
|
||||
destroy () {
|
||||
this.eventListeners = null
|
||||
}
|
||||
addEventListener (f) {
|
||||
this.eventListeners.push(f)
|
||||
}
|
||||
removeEventListener (f) {
|
||||
this.eventListeners = this.eventListeners.filter(function (g) {
|
||||
return f !== g
|
||||
})
|
||||
}
|
||||
removeAllEventListeners () {
|
||||
this.eventListeners = []
|
||||
}
|
||||
callEventListeners (event) {
|
||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||
try {
|
||||
const f = this.eventListeners[i]
|
||||
f(event)
|
||||
} catch (e) {
|
||||
/*
|
||||
Your observer threw an error. This error was caught so that Yjs
|
||||
can ensure data consistency! In order to debug this error you
|
||||
have to check "Pause On Caught Exceptions" in developer tools.
|
||||
*/
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
export default class ID {
|
||||
constructor (user, clock) {
|
||||
this.user = user
|
||||
this.clock = clock
|
||||
}
|
||||
clone () {
|
||||
return new ID(this.user, this.clock)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.clock === this.clock
|
||||
}
|
||||
lessThan (id) {
|
||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
export default class NamedEventHandler {
|
||||
constructor () {
|
||||
this._eventListener = new Map()
|
||||
}
|
||||
_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
|
||||
}
|
||||
once (name, f) {
|
||||
let listeners = this._getListener(name)
|
||||
listeners.once.add(f)
|
||||
}
|
||||
on (name, f) {
|
||||
let listeners = this._getListener(name)
|
||||
listeners.on.add(f)
|
||||
}
|
||||
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.remove(f)
|
||||
}
|
||||
}
|
||||
emit (name, ...args) {
|
||||
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,17 +0,0 @@
|
||||
import { getReference } from './structReferences.js'
|
||||
|
||||
export const RootFakeUserID = 0xFFFFFF
|
||||
|
||||
export default class RootID {
|
||||
constructor (name, typeConstructor) {
|
||||
this.user = RootFakeUserID
|
||||
this.name = name
|
||||
this.type = getReference(typeConstructor)
|
||||
}
|
||||
equals (id) {
|
||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
||||
}
|
||||
lessThan (id) {
|
||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
||||
}
|
||||
}
|
||||
471
src/Util/Tree.js
471
src/Util/Tree.js
@@ -1,471 +0,0 @@
|
||||
|
||||
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) {
|
||||
var parent = this.parent
|
||||
var newParent = this.right
|
||||
var newRight = this.right.left
|
||||
newParent.left = this
|
||||
this.right = newRight
|
||||
if (parent === null) {
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === this) {
|
||||
parent.left = newParent
|
||||
} else if (parent.right === this) {
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error('The elements are wrongly connected!')
|
||||
}
|
||||
}
|
||||
next () {
|
||||
if (this.right !== null) {
|
||||
// search the most left node in the right tree
|
||||
var o = this.right
|
||||
while (o.left !== null) {
|
||||
o = o.left
|
||||
}
|
||||
return o
|
||||
} else {
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.left) {
|
||||
p = p.parent
|
||||
}
|
||||
return p.parent
|
||||
}
|
||||
}
|
||||
prev () {
|
||||
if (this.left !== null) {
|
||||
// search the most right node in the left tree
|
||||
var o = this.left
|
||||
while (o.right !== null) {
|
||||
o = o.right
|
||||
}
|
||||
return o
|
||||
} else {
|
||||
var p = this
|
||||
while (p.parent !== null && p !== p.parent.right) {
|
||||
p = p.parent
|
||||
}
|
||||
return p.parent
|
||||
}
|
||||
}
|
||||
rotateRight (tree) {
|
||||
var parent = this.parent
|
||||
var newParent = this.left
|
||||
var newLeft = this.left.right
|
||||
newParent.right = this
|
||||
this.left = newLeft
|
||||
if (parent === null) {
|
||||
tree.root = newParent
|
||||
newParent._parent = null
|
||||
} else if (parent.left === this) {
|
||||
parent.left = newParent
|
||||
} else if (parent.right === this) {
|
||||
parent.right = newParent
|
||||
} else {
|
||||
throw new Error('The elements are wrongly connected!')
|
||||
}
|
||||
}
|
||||
getUncle () {
|
||||
// we can assume that grandparent exists when this is called!
|
||||
if (this.parent === this.parent.parent.left) {
|
||||
return this.parent.parent.right
|
||||
} else {
|
||||
return this.parent.parent.left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
flush () {}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import ID from './ID.js'
|
||||
|
||||
class ReverseOperation {
|
||||
constructor (y) {
|
||||
this.created = new Date()
|
||||
const beforeState = y._transaction.beforeState
|
||||
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1)
|
||||
if (beforeState.has(y.userID)) {
|
||||
this.fromState = new ID(y.userID, beforeState.get(y.userID))
|
||||
} else {
|
||||
this.fromState = this.toState
|
||||
}
|
||||
this.deletedStructs = y._transaction.deletedStructs
|
||||
}
|
||||
}
|
||||
|
||||
function isStructInScope (y, struct, scope) {
|
||||
while (struct !== y) {
|
||||
if (struct === scope) {
|
||||
return true
|
||||
}
|
||||
struct = struct._parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
let performedUndo = false
|
||||
y.transact(() => {
|
||||
while (!performedUndo && reverseBuffer.length > 0) {
|
||||
let undoOp = reverseBuffer.pop()
|
||||
// make sure that it is possible to iterate {from}-{to}
|
||||
y.os.getItemCleanStart(undoOp.fromState)
|
||||
y.os.getItemCleanEnd(undoOp.toState)
|
||||
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
||||
if (!op._deleted && isStructInScope(y, op, scope)) {
|
||||
performedUndo = true
|
||||
op._delete(y)
|
||||
}
|
||||
})
|
||||
for (let op of undoOp.deletedStructs) {
|
||||
if (
|
||||
isStructInScope(y, op, scope) &&
|
||||
op._parent !== y &&
|
||||
!op._parent._deleted &&
|
||||
(
|
||||
op._parent._id.user !== y.userID ||
|
||||
op._parent._id.clock < undoOp.fromState.clock ||
|
||||
op._parent._id.clock > undoOp.fromState.clock
|
||||
)
|
||||
) {
|
||||
performedUndo = true
|
||||
op = op._copy(undoOp.deletedStructs)
|
||||
op._integrate(y)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return performedUndo
|
||||
}
|
||||
|
||||
export default class UndoManager {
|
||||
constructor (scope, options = {}) {
|
||||
this.options = options
|
||||
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
||||
this._undoBuffer = []
|
||||
this._redoBuffer = []
|
||||
this._scope = scope
|
||||
this._undoing = false
|
||||
this._redoing = false
|
||||
const y = scope._y
|
||||
this.y = y
|
||||
y.on('afterTransaction', (y, remote) => {
|
||||
if (!remote && y._transaction.changedParentTypes.has(scope)) {
|
||||
let reverseOperation = new ReverseOperation(y)
|
||||
if (!this._undoing) {
|
||||
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
|
||||
if (lastUndoOp !== null && reverseOperation.created - lastUndoOp.created <= options.captureTimeout) {
|
||||
lastUndoOp.created = reverseOperation.created
|
||||
lastUndoOp.toState = reverseOperation.toState
|
||||
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
|
||||
} else {
|
||||
this._undoBuffer.push(reverseOperation)
|
||||
}
|
||||
if (!this._redoing) {
|
||||
this._redoBuffer = []
|
||||
}
|
||||
} else {
|
||||
this._redoBuffer.push(reverseOperation)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
undo () {
|
||||
this._undoing = true
|
||||
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
||||
this._undoing = false
|
||||
return performedUndo
|
||||
}
|
||||
redo () {
|
||||
this._redoing = true
|
||||
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
||||
this._redoing = false
|
||||
return performedRedo
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
|
||||
export default class YEvent {
|
||||
constructor (target) {
|
||||
this.target = target
|
||||
this.currentTarget = target
|
||||
}
|
||||
get path () {
|
||||
const path = []
|
||||
let type = this.target
|
||||
const y = type._y
|
||||
while (type !== this.currentTarget && type !== y) {
|
||||
let parent = type._parent
|
||||
if (type._parentSub !== null) {
|
||||
path.unshift(type._parentSub)
|
||||
} else {
|
||||
// parent is array-ish
|
||||
for (let [i, child] of parent) {
|
||||
if (child === type) {
|
||||
path.unshift(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
type = parent
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
|
||||
import ID from '../Util/ID.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
|
||||
/**
|
||||
* Try to merge all items in os with their successors.
|
||||
*
|
||||
* Some transformations (like delete) fragment items.
|
||||
* Item(c: 'ab') + Delete(1,1) + Delete(0, 1) -> Item(c: 'a',deleted);Item(c: 'b',deleted)
|
||||
*
|
||||
* This functions merges the fragmented nodes together:
|
||||
* Item(c: 'a',deleted);Item(c: 'b',deleted) -> Item(c: 'ab', deleted)
|
||||
*
|
||||
* TODO: The Tree implementation does not support deletions in-spot.
|
||||
* This is why all deletions must be performed after the traversal.
|
||||
*
|
||||
*/
|
||||
export function defragmentItemContent (y) {
|
||||
const os = y.os
|
||||
if (os.length < 2) {
|
||||
return
|
||||
}
|
||||
let deletes = []
|
||||
let node = os.findSmallestNode()
|
||||
let next = node.next()
|
||||
while (next !== null) {
|
||||
let a = node.val
|
||||
let b = next.val
|
||||
if (
|
||||
(a instanceof ItemJSON || a instanceof ItemString) &&
|
||||
a.constructor === b.constructor &&
|
||||
a._deleted === b._deleted &&
|
||||
a._right === b &&
|
||||
(new ID(a._id.user, a._id.clock + a._length)).equals(b._id)
|
||||
) {
|
||||
a._right = b._right
|
||||
if (a instanceof ItemJSON) {
|
||||
a._content = a._content.concat(b._content)
|
||||
} else if (a instanceof ItemString) {
|
||||
a._content += b._content
|
||||
}
|
||||
// delete b later
|
||||
deletes.push(b._id)
|
||||
// do not iterate node!
|
||||
// !(node = next)
|
||||
} else {
|
||||
// not able to merge node, get next node
|
||||
node = next
|
||||
}
|
||||
// update next
|
||||
next = next.next()
|
||||
}
|
||||
for (let i = deletes.length - 1; i >= 0; i--) {
|
||||
os.delete(deletes[i])
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/* global crypto */
|
||||
|
||||
export function generateUserID () {
|
||||
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
|
||||
// browser
|
||||
let arr = new Uint32Array(1)
|
||||
crypto.getRandomValues(arr)
|
||||
return arr[0]
|
||||
} else if (typeof crypto !== 'undefined' && crypto.randomBytes != null) {
|
||||
// node
|
||||
let buf = crypto.randomBytes(4)
|
||||
return new Uint32Array(buf.buffer)[0]
|
||||
} else {
|
||||
return Math.ceil(Math.random() * 0xFFFFFFFF)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import ID from './ID.js'
|
||||
import RootID from './RootID.js'
|
||||
|
||||
export function getRelativePosition (type, offset) {
|
||||
if (offset === 0) {
|
||||
return ['startof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
|
||||
} else {
|
||||
let t = type._start
|
||||
while (t !== null) {
|
||||
if (t._length >= offset) {
|
||||
return [t._id.user, t._id.clock + offset - 1]
|
||||
}
|
||||
if (t._right === null) {
|
||||
return [t._id.user, t._id.clock + t._length - 1]
|
||||
}
|
||||
if (!t._deleted) {
|
||||
offset -= t._length
|
||||
}
|
||||
t = t._right
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function fromRelativePosition (y, rpos) {
|
||||
if (rpos[0] === 'startof') {
|
||||
let id
|
||||
if (rpos[3] === null) {
|
||||
id = new ID(rpos[1], rpos[2])
|
||||
} else {
|
||||
id = new RootID(rpos[3], rpos[4])
|
||||
}
|
||||
return {
|
||||
type: y.os.get(id),
|
||||
offset: 0
|
||||
}
|
||||
} else {
|
||||
let offset = 0
|
||||
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
|
||||
const parent = struct._parent
|
||||
if (parent._deleted) {
|
||||
return null
|
||||
}
|
||||
if (!struct._deleted) {
|
||||
offset = rpos[1] - struct._id.clock + 1
|
||||
}
|
||||
struct = struct._left
|
||||
while (struct !== null) {
|
||||
if (!struct._deleted) {
|
||||
offset += struct._length
|
||||
}
|
||||
struct = struct._left
|
||||
}
|
||||
return {
|
||||
type: parent,
|
||||
offset: offset
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import YArray from '../Type/YArray.js'
|
||||
import YMap from '../Type/YMap.js'
|
||||
import YText from '../Type/YText.js'
|
||||
import YXmlFragment from '../Type/y-xml/YXmlFragment.js'
|
||||
import YXmlElement from '../Type/y-xml/YXmlElement.js'
|
||||
import YXmlText from '../Type/y-xml/YXmlText.js'
|
||||
|
||||
import Delete from '../Struct/Delete.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
|
||||
const structs = new Map()
|
||||
const references = new Map()
|
||||
|
||||
function addStruct (reference, structConstructor) {
|
||||
structs.set(reference, structConstructor)
|
||||
references.set(structConstructor, reference)
|
||||
}
|
||||
|
||||
export function getStruct (reference) {
|
||||
return structs.get(reference)
|
||||
}
|
||||
|
||||
export function getReference (typeConstructor) {
|
||||
return references.get(typeConstructor)
|
||||
}
|
||||
|
||||
addStruct(0, ItemJSON)
|
||||
addStruct(1, ItemString)
|
||||
addStruct(2, Delete)
|
||||
|
||||
addStruct(3, YArray)
|
||||
addStruct(4, YMap)
|
||||
addStruct(5, YText)
|
||||
addStruct(6, YXmlFragment)
|
||||
addStruct(7, YXmlElement)
|
||||
addStruct(8, YXmlText)
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
import YMap from '../Type/YMap'
|
||||
import YArray from '../Type/YArray'
|
||||
|
||||
export function writeObjectToYMap (object, type) {
|
||||
for (var key in object) {
|
||||
var val = object[key]
|
||||
if (Array.isArray(val)) {
|
||||
type.set(key, YArray)
|
||||
writeArrayToYArray(val, type.get(key))
|
||||
} else if (typeof val === 'object') {
|
||||
type.set(key, YMap)
|
||||
writeObjectToYMap(val, type.get(key))
|
||||
} else {
|
||||
type.set(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function writeArrayToYArray (array, type) {
|
||||
for (var i = array.length - 1; i >= 0; i--) {
|
||||
var val = array[i]
|
||||
if (Array.isArray(val)) {
|
||||
type.insert(0, [YArray])
|
||||
writeArrayToYArray(val, type.get(0))
|
||||
} else if (typeof val === 'object') {
|
||||
type.insert(0, [YMap])
|
||||
writeObjectToYMap(val, type.get(0))
|
||||
} else {
|
||||
type.insert(0, [val])
|
||||
}
|
||||
}
|
||||
}
|
||||
176
src/Y.js
176
src/Y.js
@@ -1,176 +0,0 @@
|
||||
import DeleteStore from './Store/DeleteStore.js'
|
||||
import OperationStore from './Store/OperationStore.js'
|
||||
import StateStore from './Store/StateStore.js'
|
||||
import { generateUserID } from './Util/generateUserID.js'
|
||||
import RootID from './Util/RootID.js'
|
||||
import NamedEventHandler from './Util/NamedEventHandler.js'
|
||||
import UndoManager from './Util/UndoManager.js'
|
||||
|
||||
import { messageToString, messageToRoomname } from './MessageHandler/messageToString.js'
|
||||
|
||||
import Connector from './Connector.js'
|
||||
import Persistence from './Persistence.js'
|
||||
import YArray from './Type/YArray.js'
|
||||
import YMap from './Type/YMap.js'
|
||||
import YText from './Type/YText.js'
|
||||
import { YXmlFragment, YXmlElement, YXmlText } from './Type/y-xml/y-xml.js'
|
||||
import BinaryDecoder from './Binary/Decoder.js'
|
||||
|
||||
import debug from 'debug'
|
||||
import Transaction from './Transaction.js'
|
||||
|
||||
export default class Y extends NamedEventHandler {
|
||||
constructor (opts) {
|
||||
super()
|
||||
this._opts = opts
|
||||
this.userID = opts._userID != null ? opts._userID : generateUserID()
|
||||
this.share = {}
|
||||
this.ds = new DeleteStore(this)
|
||||
this.os = new OperationStore(this)
|
||||
this.ss = new StateStore(this)
|
||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
||||
if (opts.persistence != null) {
|
||||
this.persistence = new Y[opts.persistence.name](this, opts.persistence)
|
||||
this.persistence.retrieveContent()
|
||||
} else {
|
||||
this.persistence = null
|
||||
}
|
||||
this.connected = true
|
||||
this._missingStructs = new Map()
|
||||
this._readyToIntegrate = []
|
||||
this._transaction = null
|
||||
}
|
||||
_beforeChange () {}
|
||||
transact (f, remote = false) {
|
||||
let initialCall = this._transaction === null
|
||||
if (initialCall) {
|
||||
this.emit('beforeTransaction', this, remote)
|
||||
this._transaction = new Transaction(this)
|
||||
}
|
||||
try {
|
||||
f(this)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (initialCall) {
|
||||
// emit change events on changed types
|
||||
this._transaction.changedTypes.forEach(function (subs, type) {
|
||||
if (!type._deleted) {
|
||||
type._callObserver(subs, remote)
|
||||
}
|
||||
})
|
||||
this._transaction.changedParentTypes.forEach(function (events, type) {
|
||||
if (!type._deleted) {
|
||||
events = events
|
||||
.filter(event =>
|
||||
!event.target._deleted
|
||||
)
|
||||
events
|
||||
.forEach(event => {
|
||||
event.currentTarget = type
|
||||
})
|
||||
// we don't have to check for events.length
|
||||
// because there is no way events is empty..
|
||||
type._deepEventHandler.callEventListeners(events)
|
||||
}
|
||||
})
|
||||
// when all changes & events are processed, emit afterTransaction event
|
||||
this.emit('afterTransaction', this, remote)
|
||||
this._transaction = null
|
||||
}
|
||||
}
|
||||
// fake _start for root properties (y.set('name', type))
|
||||
get _start () {
|
||||
return null
|
||||
}
|
||||
set _start (start) {
|
||||
return null
|
||||
}
|
||||
get room () {
|
||||
return this._opts.connector.room
|
||||
}
|
||||
define (name, TypeConstructor) {
|
||||
let id = new RootID(name, TypeConstructor)
|
||||
let type = this.os.get(id)
|
||||
if (type === null) {
|
||||
type = new TypeConstructor()
|
||||
type._id = id
|
||||
type._parent = this
|
||||
type._integrate(this)
|
||||
if (this.share[name] !== undefined) {
|
||||
throw new Error('Type is already defined with a different constructor!')
|
||||
}
|
||||
}
|
||||
if (this.share[name] === undefined) {
|
||||
this.share[name] = type
|
||||
}
|
||||
return type
|
||||
}
|
||||
get (name) {
|
||||
return this.share[name]
|
||||
}
|
||||
disconnect () {
|
||||
if (this.connected) {
|
||||
this.connected = false
|
||||
return this.connector.disconnect()
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
reconnect () {
|
||||
if (!this.connected) {
|
||||
this.connected = true
|
||||
return this.connector.reconnect()
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
destroy () {
|
||||
this.share = null
|
||||
if (this.connector.destroy != null) {
|
||||
this.connector.destroy()
|
||||
} else {
|
||||
this.connector.disconnect()
|
||||
}
|
||||
this.os = null
|
||||
this.ds = null
|
||||
this.ss = null
|
||||
}
|
||||
whenSynced () {
|
||||
return new Promise(resolve => {
|
||||
this.once('synced', () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Y.extend = function extendYjs () {
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var f = arguments[i]
|
||||
if (typeof f === 'function') {
|
||||
f(Y)
|
||||
} else {
|
||||
throw new Error('Expected a function!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: The following assignments should be moved to yjs-dist
|
||||
Y.AbstractConnector = Connector
|
||||
Y.Persisence = Persistence
|
||||
Y.Array = YArray
|
||||
Y.Map = YMap
|
||||
Y.Text = YText
|
||||
Y.XmlElement = YXmlElement
|
||||
Y.XmlFragment = YXmlFragment
|
||||
Y.XmlText = YXmlText
|
||||
|
||||
Y.utils = {
|
||||
BinaryDecoder,
|
||||
UndoManager
|
||||
}
|
||||
|
||||
Y.debug = debug
|
||||
debug.formatters.Y = messageToString
|
||||
debug.formatters.y = messageToRoomname
|
||||
85
src/index.js
Normal file
85
src/index.js
Normal file
@@ -0,0 +1,85 @@
|
||||
|
||||
export {
|
||||
Doc,
|
||||
Transaction,
|
||||
YArray as Array,
|
||||
YMap as Map,
|
||||
YText as Text,
|
||||
YXmlText as XmlText,
|
||||
YXmlHook as XmlHook,
|
||||
YXmlElement as XmlElement,
|
||||
YXmlFragment as XmlFragment,
|
||||
YXmlEvent,
|
||||
YMapEvent,
|
||||
YArrayEvent,
|
||||
YTextEvent,
|
||||
YEvent,
|
||||
Item,
|
||||
AbstractStruct,
|
||||
GC,
|
||||
ContentBinary,
|
||||
ContentDeleted,
|
||||
ContentEmbed,
|
||||
ContentFormat,
|
||||
ContentJSON,
|
||||
ContentAny,
|
||||
ContentString,
|
||||
ContentType,
|
||||
AbstractType,
|
||||
RelativePosition,
|
||||
getTypeChildren,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
compareRelativePositions,
|
||||
ID,
|
||||
createID,
|
||||
compareIDs,
|
||||
getState,
|
||||
Snapshot,
|
||||
createSnapshot,
|
||||
createDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
snapshot,
|
||||
emptySnapshot,
|
||||
findRootTypeKey,
|
||||
getItem,
|
||||
typeListToArraySnapshot,
|
||||
typeMapGetSnapshot,
|
||||
createDocFromSnapshot,
|
||||
iterateDeletedStructs,
|
||||
applyUpdate,
|
||||
applyUpdateV2,
|
||||
readUpdate,
|
||||
readUpdateV2,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateAsUpdateV2,
|
||||
encodeStateVector,
|
||||
UndoManager,
|
||||
decodeSnapshot,
|
||||
encodeSnapshot,
|
||||
decodeSnapshotV2,
|
||||
encodeSnapshotV2,
|
||||
decodeStateVector,
|
||||
logUpdate,
|
||||
logUpdateV2,
|
||||
relativePositionToJSON,
|
||||
isDeleted,
|
||||
isParentOf,
|
||||
equalSnapshots,
|
||||
PermanentUserData, // @TODO experimental
|
||||
tryGc,
|
||||
transact,
|
||||
AbstractConnector,
|
||||
logType,
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
parseUpdateMeta,
|
||||
parseUpdateMetaV2,
|
||||
encodeStateVectorFromUpdate,
|
||||
encodeStateVectorFromUpdateV2,
|
||||
encodeRelativePosition,
|
||||
decodeRelativePosition,
|
||||
diffUpdate,
|
||||
diffUpdateV2
|
||||
} from './internals.js'
|
||||
43
src/internals.js
Normal file
43
src/internals.js
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
export * from './utils/AbstractConnector.js'
|
||||
export * from './utils/DeleteSet.js'
|
||||
export * from './utils/Doc.js'
|
||||
export * from './utils/UpdateDecoder.js'
|
||||
export * from './utils/UpdateEncoder.js'
|
||||
export * from './utils/encoding.js'
|
||||
export * from './utils/EventHandler.js'
|
||||
export * from './utils/ID.js'
|
||||
export * from './utils/isParentOf.js'
|
||||
export * from './utils/logging.js'
|
||||
export * from './utils/PermanentUserData.js'
|
||||
export * from './utils/RelativePosition.js'
|
||||
export * from './utils/Snapshot.js'
|
||||
export * from './utils/StructStore.js'
|
||||
export * from './utils/Transaction.js'
|
||||
export * from './utils/UndoManager.js'
|
||||
export * from './utils/updates.js'
|
||||
export * from './utils/YEvent.js'
|
||||
|
||||
export * from './types/AbstractType.js'
|
||||
export * from './types/YArray.js'
|
||||
export * from './types/YMap.js'
|
||||
export * from './types/YText.js'
|
||||
export * from './types/YXmlFragment.js'
|
||||
export * from './types/YXmlElement.js'
|
||||
export * from './types/YXmlEvent.js'
|
||||
export * from './types/YXmlHook.js'
|
||||
export * from './types/YXmlText.js'
|
||||
|
||||
export * from './structs/AbstractStruct.js'
|
||||
export * from './structs/GC.js'
|
||||
export * from './structs/ContentBinary.js'
|
||||
export * from './structs/ContentDeleted.js'
|
||||
export * from './structs/ContentDoc.js'
|
||||
export * from './structs/ContentEmbed.js'
|
||||
export * from './structs/ContentFormat.js'
|
||||
export * from './structs/ContentJSON.js'
|
||||
export * from './structs/ContentAny.js'
|
||||
export * from './structs/ContentString.js'
|
||||
export * from './structs/ContentType.js'
|
||||
export * from './structs/Item.js'
|
||||
export * from './structs/Skip.js'
|
||||
52
src/structs/AbstractStruct.js
Normal file
52
src/structs/AbstractStruct.js
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
import {
|
||||
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class AbstractStruct {
|
||||
/**
|
||||
* @param {ID} id
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor (id, length) {
|
||||
this.id = id
|
||||
this.length = length
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get deleted () {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge this struct with the item to the right.
|
||||
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
|
||||
* Also this method does *not* remove right from StructStore!
|
||||
* @param {AbstractStruct} right
|
||||
* @return {boolean} wether this merged with right
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||
* @param {number} offset
|
||||
* @param {number} encodingRef
|
||||
*/
|
||||
write (encoder, offset, encodingRef) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {number} offset
|
||||
*/
|
||||
integrate (transaction, offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
}
|
||||
108
src/structs/ContentAny.js
Normal file
108
src/structs/ContentAny.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
UpdateEncoderV1, UpdateEncoderV2, UpdateDecoderV1, UpdateDecoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export class ContentAny {
|
||||
/**
|
||||
* @param {Array<any>} arr
|
||||
*/
|
||||
constructor (arr) {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
this.arr = arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.arr
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentAny(this.arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentAny(this.arr.slice(offset))
|
||||
this.arr = this.arr.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentAny} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.arr = this.arr.concat(right.arr)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
const len = this.arr.length
|
||||
encoder.writeLen(len - offset)
|
||||
for (let i = offset; i < len; i++) {
|
||||
const c = this.arr[i]
|
||||
encoder.writeAny(c)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentAny}
|
||||
*/
|
||||
export const readContentAny = decoder => {
|
||||
const len = decoder.readLen()
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
cs.push(decoder.readAny())
|
||||
}
|
||||
return new ContentAny(cs)
|
||||
}
|
||||
92
src/structs/ContentBinary.js
Normal file
92
src/structs/ContentBinary.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class ContentBinary {
|
||||
/**
|
||||
* @param {Uint8Array} content
|
||||
*/
|
||||
constructor (content) {
|
||||
this.content = content
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.content]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentBinary(this.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentBinary} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoder.writeBuf(this.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentBinary}
|
||||
*/
|
||||
export const readContentBinary = decoder => new ContentBinary(decoder.readBuf())
|
||||
101
src/structs/ContentDeleted.js
Normal file
101
src/structs/ContentDeleted.js
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
import {
|
||||
addToDeleteSet,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
export class ContentDeleted {
|
||||
/**
|
||||
* @param {number} len
|
||||
*/
|
||||
constructor (len) {
|
||||
this.len = len
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.len
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentDeleted(this.len)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentDeleted(this.len - offset)
|
||||
this.len = offset
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentDeleted} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.len += right.len
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len)
|
||||
item.markDeleted()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoder.writeLen(this.len - offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
|
||||
* @return {ContentDeleted}
|
||||
*/
|
||||
export const readContentDeleted = decoder => new ContentDeleted(decoder.readLen())
|
||||
135
src/structs/ContentDoc.js
Normal file
135
src/structs/ContentDoc.js
Normal file
@@ -0,0 +1,135 @@
|
||||
|
||||
import {
|
||||
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentDoc {
|
||||
/**
|
||||
* @param {Doc} doc
|
||||
*/
|
||||
constructor (doc) {
|
||||
if (doc._item) {
|
||||
console.error('This document was already integrated as a sub-document. You should create a second instance instead with the same guid.')
|
||||
}
|
||||
/**
|
||||
* @type {Doc}
|
||||
*/
|
||||
this.doc = doc
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const opts = {}
|
||||
this.opts = opts
|
||||
if (!doc.gc) {
|
||||
opts.gc = false
|
||||
}
|
||||
if (doc.autoLoad) {
|
||||
opts.autoLoad = true
|
||||
}
|
||||
if (doc.meta !== null) {
|
||||
opts.meta = doc.meta
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.doc]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentDoc(this.doc)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentDoc} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
// this needs to be reflected in doc.destroy as well
|
||||
this.doc._item = item
|
||||
transaction.subdocsAdded.add(this.doc)
|
||||
if (this.doc.shouldLoad) {
|
||||
transaction.subdocsLoaded.add(this.doc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {
|
||||
if (transaction.subdocsAdded.has(this.doc)) {
|
||||
transaction.subdocsAdded.delete(this.doc)
|
||||
} else {
|
||||
transaction.subdocsRemoved.add(this.doc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) { }
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoder.writeString(this.doc.guid)
|
||||
encoder.writeAny(this.opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 9
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentDoc}
|
||||
*/
|
||||
export const readContentDoc = decoder => new ContentDoc(new Doc({ guid: decoder.readString(), ...decoder.readAny() }))
|
||||
98
src/structs/ContentEmbed.js
Normal file
98
src/structs/ContentEmbed.js
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentEmbed {
|
||||
/**
|
||||
* @param {Object} embed
|
||||
*/
|
||||
constructor (embed) {
|
||||
this.embed = embed
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return [this.embed]
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentEmbed(this.embed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentEmbed} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {}
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoder.writeJSON(this.embed)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentEmbed}
|
||||
*/
|
||||
export const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON())
|
||||
103
src/structs/ContentFormat.js
Normal file
103
src/structs/ContentFormat.js
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
import {
|
||||
AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentFormat {
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {Object} value
|
||||
*/
|
||||
constructor (key, value) {
|
||||
this.key = key
|
||||
this.value = value
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentFormat(this.key, this.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
splice (offset) {
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentFormat} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} item
|
||||
*/
|
||||
integrate (transaction, item) {
|
||||
// @todo searchmarker are currently unsupported for rich text documents
|
||||
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
delete (transaction) {}
|
||||
/**
|
||||
* @param {StructStore} store
|
||||
*/
|
||||
gc (store) {}
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
* @param {number} offset
|
||||
*/
|
||||
write (encoder, offset) {
|
||||
encoder.writeKey(this.key)
|
||||
encoder.writeJSON(this.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 6
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user