Compare commits
850 Commits
v13.0.0-25
...
v13.5.37
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a4816a1b2 | ||
|
|
4ad8af9a80 | ||
|
|
fc25136b25 | ||
|
|
ece1fe5426 | ||
|
|
40196ae0a3 | ||
|
|
bdefe0526d | ||
|
|
dbbb86adc7 | ||
|
|
1c9c97ffe6 | ||
|
|
14c14de21e | ||
|
|
71fad52854 | ||
|
|
3935ba1faa | ||
|
|
4aacb487d2 | ||
|
|
5f56baa23e | ||
|
|
8d809ebacb | ||
|
|
92624afbff | ||
|
|
1e8efd5104 | ||
|
|
7b680f1bda | ||
|
|
806bf3f6dd | ||
|
|
42fe19daf1 | ||
|
|
7d3de7fa07 | ||
|
|
63c1cb4eb9 | ||
|
|
be1449a7af | ||
|
|
a22b3cdbc1 | ||
|
|
e9a0dc4ed2 | ||
|
|
b0b276d964 | ||
|
|
d3e117702c | ||
|
|
ff5067e149 | ||
|
|
f80e39a477 | ||
|
|
f70198333a | ||
|
|
3c31b22a92 | ||
|
|
6b8cef29e2 | ||
|
|
4a06492fb1 | ||
|
|
46fbce0de8 | ||
|
|
239703fe5c | ||
|
|
5e907e3281 | ||
|
|
6aea35246b | ||
|
|
5058189a46 | ||
|
|
4db3439bb1 | ||
|
|
aa5463b06d | ||
|
|
afe8e52840 | ||
|
|
d0f9c4a27f | ||
|
|
a5ffdce342 | ||
|
|
67d27dfca2 | ||
|
|
9f1548204a | ||
|
|
46e108f345 | ||
|
|
bda622f523 | ||
|
|
fef9e39d91 | ||
|
|
5751a12c11 | ||
|
|
fddb620d41 | ||
|
|
abf3fab1b6 | ||
|
|
69e2375dc5 | ||
|
|
058a50285c | ||
|
|
8678ef62d6 | ||
|
|
db53b6c720 | ||
|
|
3f34777201 | ||
|
|
24eddb2d75 | ||
|
|
8ce107bd17 | ||
|
|
2d1e3fde43 | ||
|
|
04009f0d42 | ||
|
|
d69d93f812 | ||
|
|
931a37a331 | ||
|
|
0ec2753313 | ||
|
|
8fd1f3405a | ||
|
|
f577a8e3cf | ||
|
|
84e95f11cb | ||
|
|
f08682ddfd | ||
|
|
c20d72b886 | ||
|
|
c9414f51a7 | ||
|
|
0fee9dfff4 | ||
|
|
4cfa49d601 | ||
|
|
b6562f3e80 | ||
|
|
164b38f0cd | ||
|
|
99326f67b8 | ||
|
|
1c360f9f59 | ||
|
|
8f0d7cdfc2 | ||
|
|
b281277c67 | ||
|
|
532d5fccb2 | ||
|
|
8f421a0f42 | ||
|
|
8fec835338 | ||
|
|
81a36a2762 | ||
|
|
6403bc2bb5 | ||
|
|
20e1234af2 | ||
|
|
3aebb8db83 | ||
|
|
51bb732606 | ||
|
|
f857345451 | ||
|
|
645f05b0bb | ||
|
|
1cf709093c | ||
|
|
9569d3e297 | ||
|
|
507edccdf8 | ||
|
|
9914f48a52 | ||
|
|
d57629b36d | ||
|
|
294ba351b6 | ||
|
|
610e532868 | ||
|
|
f73fb4796b | ||
|
|
32d391d7ab | ||
|
|
28e1b19e57 | ||
|
|
e90d9de5ed | ||
|
|
9a7250f192 | ||
|
|
4154b12f14 | ||
|
|
9df5016667 | ||
|
|
1becaccdd9 | ||
|
|
ea4e9a0007 | ||
|
|
a4e48d1ddf | ||
|
|
0a39a92b33 | ||
|
|
bd819243eb | ||
|
|
2ec19defcb | ||
|
|
336f7b1b1d | ||
|
|
8abf5b85ff | ||
|
|
320e8cbe18 | ||
|
|
49150f4adb | ||
|
|
e22fed7af3 | ||
|
|
c91945228f | ||
|
|
3586d91925 | ||
|
|
f915ebda1b | ||
|
|
a9b92b9099 | ||
|
|
cbddf6ef90 | ||
|
|
491cd422c4 | ||
|
|
4b88e2aac5 | ||
|
|
e33c67fc72 | ||
|
|
085dda4cbd | ||
|
|
f382846874 | ||
|
|
9afc5cf615 | ||
|
|
ca0fb4b15d | ||
|
|
d369a771a9 | ||
|
|
995fbfa4cc | ||
|
|
7486ea7148 | ||
|
|
2c80a955da | ||
|
|
233872493b | ||
|
|
64d164a904 | ||
|
|
a08e54c2fc | ||
|
|
2b377cd46d | ||
|
|
b4b8927550 | ||
|
|
b2761b50f2 | ||
|
|
28a9ce962d | ||
|
|
0ec67170d3 | ||
|
|
df9bfbe778 | ||
|
|
f1ab417570 | ||
|
|
4922eeac56 | ||
|
|
57d6c6f831 | ||
|
|
371f2b6d55 | ||
|
|
85a7ad148f | ||
|
|
7ec1b3a19e | ||
|
|
633eb9033c | ||
|
|
4707fc46ac | ||
|
|
89b4320a8e | ||
|
|
0ea0a35521 | ||
|
|
15ea4ee805 | ||
|
|
744469d363 | ||
|
|
311dd50f1b | ||
|
|
89c5541ee6 | ||
|
|
28d8db86f0 | ||
|
|
0c34216ed0 | ||
|
|
9aa518bc14 | ||
|
|
27b1190a28 | ||
|
|
f3d8db491b | ||
|
|
e9905602f8 | ||
|
|
2b8154fa16 | ||
|
|
5ddb7eefed | ||
|
|
4b35de5ad5 | ||
|
|
097b9e8208 | ||
|
|
5cac153a17 | ||
|
|
a7e4724edd | ||
|
|
71d8da6513 | ||
|
|
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 | ||
|
|
16f84c67d5 | ||
|
|
290d3c8ffe | ||
|
|
c51e8b46c2 | ||
|
|
0cda1630d2 | ||
|
|
d232b883e9 | ||
|
|
3a0e65403f |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["latest", {
|
||||
"es2015": {
|
||||
"modules": false
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
"external-helpers"
|
||||
]
|
||||
}
|
||||
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 known state of each user (a set of
|
||||
tuples `(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`. A 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,34 +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: 1000
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && e.ctrlKey) {
|
||||
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
|
||||
10514
package-lock.json
generated
10514
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
117
package.json
117
package.json
@@ -1,72 +1,91 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-25",
|
||||
"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.37",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"type": "module",
|
||||
"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"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/yjs.mjs",
|
||||
"require": "./dist/yjs.cjs"
|
||||
},
|
||||
"./src/index.js": "./src/index.js",
|
||||
"./tests/testHelper.js": "./tests/testHelper.js",
|
||||
"./testHelper": "./dist/testHelper.mjs",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"y.*"
|
||||
"dist/yjs.*",
|
||||
"dist/src",
|
||||
"src",
|
||||
"tests/testHelper.js",
|
||||
"dist/testHelper.mjs",
|
||||
"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.49"
|
||||
},
|
||||
"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.7",
|
||||
"markdownlint-cli": "^0.23.2",
|
||||
"rollup": "^2.60.0",
|
||||
"standard": "^16.0.4",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^4.4.4",
|
||||
"y-protocols": "^1.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
`
|
||||
}
|
||||
111
rollup.config.js
Normal file
111
rollup.config.js
Normal file
@@ -0,0 +1,111 @@
|
||||
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/testHelper.js',
|
||||
output: {
|
||||
name: 'Y',
|
||||
file: 'dist/testHelper.mjs',
|
||||
format: 'esm',
|
||||
sourcemap: true
|
||||
},
|
||||
external: id => /^lib0\//.test(id) || id === 'yjs',
|
||||
plugins: [{
|
||||
resolveId (importee) {
|
||||
if (importee === '../src/index.js') {
|
||||
return 'yjs'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
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-xml.tests.js',
|
||||
moduleName: 'y-tests',
|
||||
format: 'umd',
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
],
|
||||
dest: 'y.test.js',
|
||||
sourceMap: true
|
||||
}
|
||||
@@ -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,51 +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()
|
||||
this._content[i] = JSON.parse(ctnt)
|
||||
}
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
let len = this._content.length
|
||||
encoder.writeVarUint(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
encoder.writeVarString(JSON.stringify(this._content[i]))
|
||||
}
|
||||
}
|
||||
_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,217 +0,0 @@
|
||||
import Type from '../Struct/Type.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.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) {
|
||||
return n._content[n._length - pos]
|
||||
}
|
||||
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) {
|
||||
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,226 +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) {
|
||||
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,104 +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
|
||||
}
|
||||
|
||||
export default class UndoManager {
|
||||
constructor (scope, options = {}) {
|
||||
this.options = options
|
||||
options.captureTimeout = options.captureTimeout || 0
|
||||
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.beforeState.has(y.userID) || y._transaction.deletedStructs.size > 0)) {
|
||||
let reverseOperation = new ReverseOperation(y)
|
||||
if (!this._undoing) {
|
||||
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
|
||||
if (lastUndoOp !== null && lastUndoOp.created - reverseOperation.created <= options.captureTimeout) {
|
||||
console.log('appending', lastUndoOp, reverseOperation)
|
||||
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 () {
|
||||
console.log('undoing')
|
||||
this._undoing = true
|
||||
this._applyReverseOperation(this._undoBuffer)
|
||||
this._undoing = false
|
||||
}
|
||||
redo () {
|
||||
this._redoing = true
|
||||
this._applyReverseOperation(this._redoBuffer)
|
||||
this._redoing = false
|
||||
}
|
||||
_applyReverseOperation (reverseBuffer) {
|
||||
this.y.transact(() => {
|
||||
let performedUndo = false
|
||||
while (!performedUndo && reverseBuffer.length > 0) {
|
||||
let undoOp = reverseBuffer.pop()
|
||||
// make sure that it is possible to iterate {from}-{to}
|
||||
this.y.os.getItemCleanStart(undoOp.fromState)
|
||||
this.y.os.getItemCleanEnd(undoOp.toState)
|
||||
this.y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
||||
if (!op._deleted && isStructInScope(this.y, op, this._scope)) {
|
||||
performedUndo = true
|
||||
op._delete(this.y)
|
||||
}
|
||||
})
|
||||
for (let op of undoOp.deletedStructs) {
|
||||
if (
|
||||
isStructInScope(this.y, op, this._scope) &&
|
||||
op._parent !== this.y &&
|
||||
!op._parent._deleted &&
|
||||
(
|
||||
op._parent._id.user !== this.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(this.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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._parent !== this._currentTarget && type._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
|
||||
}
|
||||
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
|
||||
117
src/index.js
Normal file
117
src/index.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/** eslint-env browser */
|
||||
|
||||
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,
|
||||
getTypeChildren,
|
||||
createRelativePositionFromTypeIndex,
|
||||
createRelativePositionFromJSON,
|
||||
createAbsolutePositionFromRelativePosition,
|
||||
compareRelativePositions,
|
||||
AbsolutePosition,
|
||||
RelativePosition,
|
||||
ID,
|
||||
createID,
|
||||
compareIDs,
|
||||
getState,
|
||||
Snapshot,
|
||||
createSnapshot,
|
||||
createDeleteSet,
|
||||
createDeleteSetFromStructStore,
|
||||
cleanupYTextFormatting,
|
||||
snapshot,
|
||||
emptySnapshot,
|
||||
findRootTypeKey,
|
||||
findIndexSS,
|
||||
getItem,
|
||||
typeListToArraySnapshot,
|
||||
typeMapGetSnapshot,
|
||||
createDocFromSnapshot,
|
||||
iterateDeletedStructs,
|
||||
applyUpdate,
|
||||
applyUpdateV2,
|
||||
readUpdate,
|
||||
readUpdateV2,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateAsUpdateV2,
|
||||
encodeStateVector,
|
||||
UndoManager,
|
||||
decodeSnapshot,
|
||||
encodeSnapshot,
|
||||
decodeSnapshotV2,
|
||||
encodeSnapshotV2,
|
||||
decodeStateVector,
|
||||
logUpdate,
|
||||
logUpdateV2,
|
||||
decodeUpdate,
|
||||
decodeUpdateV2,
|
||||
relativePositionToJSON,
|
||||
isDeleted,
|
||||
isParentOf,
|
||||
equalSnapshots,
|
||||
PermanentUserData, // @TODO experimental
|
||||
tryGc,
|
||||
transact,
|
||||
AbstractConnector,
|
||||
logType,
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
parseUpdateMeta,
|
||||
parseUpdateMetaV2,
|
||||
encodeStateVectorFromUpdate,
|
||||
encodeStateVectorFromUpdateV2,
|
||||
encodeRelativePosition,
|
||||
decodeRelativePosition,
|
||||
diffUpdate,
|
||||
diffUpdateV2,
|
||||
convertUpdateFormatV1ToV2,
|
||||
convertUpdateFormatV2ToV1
|
||||
} from './internals.js'
|
||||
|
||||
const glo = /** @type {any} */ (typeof window !== 'undefined'
|
||||
? window
|
||||
// @ts-ignore
|
||||
: typeof global !== 'undefined' ? global : {})
|
||||
|
||||
const importIdentifier = '__ $YJS$ __'
|
||||
|
||||
if (glo[importIdentifier] === true) {
|
||||
/**
|
||||
* Dear reader of this warning message. Please take this seriously.
|
||||
*
|
||||
* If you see this message, please make sure that you only import one version of Yjs. In many cases,
|
||||
* your package manager installs two versions of Yjs that are used by different packages within your project.
|
||||
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
|
||||
* and others use the EcmaScript version of Yjs.
|
||||
*
|
||||
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
|
||||
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
|
||||
* do the constructor checks anymore - which might break the CRDT algorithm.
|
||||
*/
|
||||
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.')
|
||||
}
|
||||
glo[importIdentifier] = true
|
||||
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())
|
||||
141
src/structs/ContentDoc.js
Normal file
141
src/structs/ContentDoc.js
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
import {
|
||||
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @param {string} guid
|
||||
* @param {Object<string, any>} opts
|
||||
*/
|
||||
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false })
|
||||
|
||||
/**
|
||||
* @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(createDocFromOpts(this.doc.guid, this.opts))
|
||||
}
|
||||
|
||||
/**
|
||||
* @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(createDocFromOpts(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.readKey(), decoder.readJSON())
|
||||
118
src/structs/ContentJSON.js
Normal file
118
src/structs/ContentJSON.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentJSON {
|
||||
/**
|
||||
* @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 {ContentJSON}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentJSON(this.arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentJSON(this.arr.slice(offset))
|
||||
this.arr = this.arr.slice(0, offset)
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentJSON} 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.writeString(c === undefined ? 'undefined' : JSON.stringify(c))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentJSON}
|
||||
*/
|
||||
export const readContentJSON = decoder => {
|
||||
const len = decoder.readLen()
|
||||
const cs = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = decoder.readString()
|
||||
if (c === 'undefined') {
|
||||
cs.push(undefined)
|
||||
} else {
|
||||
cs.push(JSON.parse(c))
|
||||
}
|
||||
}
|
||||
return new ContentJSON(cs)
|
||||
}
|
||||
112
src/structs/ContentString.js
Normal file
112
src/structs/ContentString.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item, StructStore // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class ContentString {
|
||||
/**
|
||||
* @param {string} str
|
||||
*/
|
||||
constructor (str) {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.str = str
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getLength () {
|
||||
return this.str.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
getContent () {
|
||||
return this.str.split('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isCountable () {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ContentString}
|
||||
*/
|
||||
copy () {
|
||||
return new ContentString(this.str)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} offset
|
||||
* @return {ContentString}
|
||||
*/
|
||||
splice (offset) {
|
||||
const right = new ContentString(this.str.slice(offset))
|
||||
this.str = this.str.slice(0, offset)
|
||||
|
||||
// Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248
|
||||
const firstCharCode = this.str.charCodeAt(offset - 1)
|
||||
if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) {
|
||||
// Last character of the left split is the start of a surrogate utf16/ucs2 pair.
|
||||
// We don't support splitting of surrogate pairs because this may lead to invalid documents.
|
||||
// Replace the invalid character with a unicode replacement character (<28> / U+FFFD)
|
||||
this.str = this.str.slice(0, offset - 1) + '<27>'
|
||||
// replace right as well
|
||||
right.str = '<27>' + right.str.slice(1)
|
||||
}
|
||||
return right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ContentString} right
|
||||
* @return {boolean}
|
||||
*/
|
||||
mergeWith (right) {
|
||||
this.str += right.str
|
||||
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) {
|
||||
encoder.writeString(offset === 0 ? this.str : this.str.slice(offset))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
getRef () {
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentString}
|
||||
*/
|
||||
export const readContentString = decoder => new ContentString(decoder.readString())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user