Compare commits
923 Commits
v13.0.0-34
...
v13.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e9a648d08 | ||
|
|
83712cb1a6 | ||
|
|
30b56d5ae9 | ||
|
|
adaa95ebb8 | ||
|
|
1f2f08ef7e | ||
|
|
39167e6e2a | ||
|
|
5a8519d2c2 | ||
|
|
d039d48b3f | ||
|
|
710ac31af3 | ||
|
|
49f435284f | ||
|
|
ba96f2fe74 | ||
|
|
99bab4a1d8 | ||
|
|
1674d3986d | ||
|
|
dc3e99e6a1 | ||
|
|
fb6664a2bc | ||
|
|
0d7e865531 | ||
|
|
e73eb0bf92 | ||
|
|
d815855450 | ||
|
|
61ba6cdde1 | ||
|
|
cb70d7bad3 | ||
|
|
2001bec8eb | ||
|
|
2e2710ded9 | ||
|
|
227018f5c7 | ||
|
|
da8bacfc78 | ||
|
|
92bad63145 | ||
|
|
52ff230dd1 | ||
|
|
fe48efe64f | ||
|
|
7e40fc442d | ||
|
|
035e350062 | ||
|
|
bf338d8040 | ||
|
|
658c520b93 | ||
|
|
2576d4efca | ||
|
|
58b754950e | ||
|
|
ea7ad07f34 | ||
|
|
1c999b250e | ||
|
|
e9189365ee | ||
|
|
e0a2f11db3 | ||
|
|
7445a9ce5f | ||
|
|
7f6c12a541 | ||
|
|
370d0c138d | ||
|
|
d29de75f85 | ||
|
|
f215866429 | ||
|
|
093b41ccc4 | ||
|
|
ab60cd1ff8 | ||
|
|
1130abe05b | ||
|
|
31b4ab8d0c | ||
|
|
ab978b2003 | ||
|
|
afc6728c9e | ||
|
|
0ef5bd42fe | ||
|
|
3ece681758 | ||
|
|
cac9407185 | ||
|
|
7ea8ffebae | ||
|
|
d7751c16fd | ||
|
|
a64c51ec06 | ||
|
|
7405057037 | ||
|
|
6208b82872 | ||
|
|
12a9134b09 | ||
|
|
7395229086 | ||
|
|
8fb73edd97 | ||
|
|
f1ad5686c1 | ||
|
|
ed9236bdc7 | ||
|
|
5405fd2d7c | ||
|
|
12667f6b66 | ||
|
|
3d7ef7e28b | ||
|
|
56267e0a7d | ||
|
|
da71f6fa45 | ||
|
|
588788fbef | ||
|
|
fb9df6efe2 | ||
|
|
a69ecb0287 | ||
|
|
923fc6e06e | ||
|
|
0fdfd93e4b | ||
|
|
e0e5f8d2ea | ||
|
|
daf034cf75 | ||
|
|
2157ebb4d0 | ||
|
|
97ef4ae1e0 | ||
|
|
df2d59e2fb | ||
|
|
7a61c90261 | ||
|
|
6fa8778fc7 | ||
|
|
6b7b3136e0 | ||
|
|
da052bdb0a | ||
|
|
1bc9308566 | ||
|
|
a5e0448a92 | ||
|
|
c0c2b3347b | ||
|
|
6258ba1ce9 | ||
|
|
5a7ee74f68 | ||
|
|
29fb4a0aab | ||
|
|
8937494bdd | ||
|
|
4504196d5c | ||
|
|
0c8d29bfff | ||
|
|
43384e4148 | ||
|
|
a2b62b0a58 | ||
|
|
6febf51b1a | ||
|
|
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 |
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]
|
|
||||||
31
.github/workflows/node.js.yml
vendored
Normal file
31
.github/workflows/node.js.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: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [16.x, 18.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
|
node_modules
|
||||||
bower_components
|
dist
|
||||||
/y.*
|
.vscode
|
||||||
/examples/yjs-dist.js*
|
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 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,43 +0,0 @@
|
|||||||
/* global Y, HTMLElement, customElements */
|
|
||||||
|
|
||||||
class MagicTable extends HTMLElement {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
var shadow = this.attachShadow({mode: 'open'})
|
|
||||||
setTimeout(() => {
|
|
||||||
shadow.append(this.childNodes[0])
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('magic-table', MagicTable)
|
|
||||||
|
|
||||||
// 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: 500
|
|
||||||
})
|
|
||||||
|
|
||||||
document.onkeydown = function interceptUndoRedo (e) {
|
|
||||||
if (e.keyCode === 90 && e.metaKey) {
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
window.undoManager.undo()
|
|
||||||
} else {
|
|
||||||
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,21 +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-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -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
|
|
||||||
9040
package-lock.json
generated
9040
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
125
package.json
125
package.json
@@ -1,73 +1,98 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-34",
|
"version": "13.6.1",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "Shared Editing Library",
|
||||||
"main": "./y.node.js",
|
"main": "./dist/yjs.cjs",
|
||||||
"browser": "./y.js",
|
"module": "./dist/yjs.mjs",
|
||||||
"module": "./src/y.js",
|
"types": "./dist/src/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"sideEffects": false,
|
||||||
|
"funding": {
|
||||||
|
"type": "GitHub Sponsors ❤",
|
||||||
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
|
||||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
|
||||||
"lint": "standard",
|
"dist": "rm -rf dist && rollup -c && tsc",
|
||||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
"watch": "rollup -wc",
|
||||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
"lint": "markdownlint README.md && standard && tsc",
|
||||||
"postversion": "npm run dist",
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
"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": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/src/index.d.ts",
|
||||||
|
"module": "./dist/yjs.mjs",
|
||||||
|
"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": [
|
"files": [
|
||||||
"y.*",
|
"dist/yjs.*",
|
||||||
"src/*"
|
"dist/src",
|
||||||
|
"src",
|
||||||
|
"tests/testHelper.js",
|
||||||
|
"dist/testHelper.mjs",
|
||||||
|
"sponsor-y.js"
|
||||||
],
|
],
|
||||||
|
"dictionaries": {
|
||||||
|
"test": "tests"
|
||||||
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"/y.js",
|
"/dist",
|
||||||
"/y.js.map"
|
"/node_modules",
|
||||||
|
"/docs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/y-js/yjs.git"
|
"url": "https://github.com/yjs/yjs.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Yjs",
|
"Yjs",
|
||||||
"OT",
|
"CRDT",
|
||||||
"Collaboration",
|
"offline",
|
||||||
"Synchronization",
|
"offline-first",
|
||||||
"ShareJS",
|
"shared-editing",
|
||||||
"Coweb",
|
"concurrency",
|
||||||
"Concurrency"
|
"collaboration"
|
||||||
],
|
],
|
||||||
"author": "Kevin Jahns",
|
"author": "Kevin Jahns",
|
||||||
"email": "kevin.jahns@rwth-aachen.de",
|
"email": "kevin.jahns@protonmail.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/y-js/yjs/issues"
|
"url": "https://github.com/yjs/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"
|
|
||||||
},
|
},
|
||||||
|
"homepage": "https://docs.yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^2.6.8",
|
"lib0": "^0.2.74"
|
||||||
"fast-diff": "^1.1.2",
|
},
|
||||||
"utf-8": "^1.0.0",
|
"devDependencies": {
|
||||||
"utf8": "^2.1.2"
|
"@rollup/plugin-commonjs": "^24.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
|
"@types/node": "^18.15.5",
|
||||||
|
"concurrently": "^3.6.1",
|
||||||
|
"http-server": "^0.12.3",
|
||||||
|
"jsdoc": "^3.6.7",
|
||||||
|
"markdownlint-cli": "^0.23.2",
|
||||||
|
"rollup": "^3.20.0",
|
||||||
|
"standard": "^16.0.4",
|
||||||
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"y-protocols": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"npm": ">=8.0.0",
|
||||||
|
"node": ">=16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +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 {
|
|
||||||
input: 'src/Y.js',
|
|
||||||
name: 'Y',
|
|
||||||
sourcemap: true,
|
|
||||||
output: {
|
|
||||||
file: 'y.js',
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
banner: `
|
|
||||||
/**
|
|
||||||
* ${pkg.name} - ${pkg.description}
|
|
||||||
* @version v${pkg.version}
|
|
||||||
* @license ${pkg.license}
|
|
||||||
*/
|
|
||||||
`
|
|
||||||
}
|
|
||||||
106
rollup.config.js
Normal file
106
rollup.config.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
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: ['browser', 'module', 'main']
|
||||||
|
}),
|
||||||
|
commonjs()
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
input: './tests/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'test',
|
||||||
|
file: 'dist/tests.cjs',
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
debugResolve,
|
||||||
|
nodeResolve({
|
||||||
|
mainFields: ['node', 'module', 'main'],
|
||||||
|
exportConditions: ['node', 'module', 'import', 'default']
|
||||||
|
}),
|
||||||
|
commonjs()
|
||||||
|
],
|
||||||
|
external: id => /^lib0\//.test(id)
|
||||||
|
}]
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
var pkg = require('./package.json')
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'src/y-dist.cjs.js',
|
|
||||||
nameame: 'Y',
|
|
||||||
sourcemap: true,
|
|
||||||
output: {
|
|
||||||
file: 'y.node.js',
|
|
||||||
format: 'cjs'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs()
|
|
||||||
],
|
|
||||||
banner: `
|
|
||||||
/**
|
|
||||||
* ${pkg.name} - ${pkg.description}
|
|
||||||
* @version v${pkg.version}
|
|
||||||
* @license ${pkg.license}
|
|
||||||
*/
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
import multiEntry from 'rollup-plugin-multi-entry'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'test/y-xml.tests.js',
|
|
||||||
name: 'y-tests',
|
|
||||||
sourcemap: true,
|
|
||||||
output: {
|
|
||||||
file: 'y.test.js',
|
|
||||||
format: 'umd'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
multiEntry()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import utf8 from 'utf-8'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
|
|
||||||
export default class BinaryDecoder {
|
|
||||||
constructor (buffer) {
|
|
||||||
if (buffer instanceof ArrayBuffer) {
|
|
||||||
this.uint8arr = new Uint8Array(buffer)
|
|
||||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
|
||||||
this.uint8arr = buffer
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
|
||||||
}
|
|
||||||
this.pos = 0
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Clone this decoder instance
|
|
||||||
* Optionally set a new position parameter
|
|
||||||
*/
|
|
||||||
clone (newPos = this.pos) {
|
|
||||||
let decoder = new BinaryDecoder(this.uint8arr)
|
|
||||||
decoder.pos = newPos
|
|
||||||
return decoder
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Number of bytes
|
|
||||||
*/
|
|
||||||
get length () {
|
|
||||||
return this.uint8arr.length
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Skip one byte, jump to the next position
|
|
||||||
*/
|
|
||||||
skip8 () {
|
|
||||||
this.pos++
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read one byte as unsigned integer
|
|
||||||
*/
|
|
||||||
readUint8 () {
|
|
||||||
return this.uint8arr[this.pos++]
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read 4 bytes as unsigned integer
|
|
||||||
*/
|
|
||||||
readUint32 () {
|
|
||||||
let uint =
|
|
||||||
this.uint8arr[this.pos] +
|
|
||||||
(this.uint8arr[this.pos + 1] << 8) +
|
|
||||||
(this.uint8arr[this.pos + 2] << 16) +
|
|
||||||
(this.uint8arr[this.pos + 3] << 24)
|
|
||||||
this.pos += 4
|
|
||||||
return uint
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Look ahead without incrementing position
|
|
||||||
* to the next byte and read it as unsigned integer
|
|
||||||
*/
|
|
||||||
peekUint8 () {
|
|
||||||
return this.uint8arr[this.pos]
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read unsigned integer (32bit) with variable length
|
|
||||||
* 1/8th of the storage is used as encoding overhead
|
|
||||||
* - numbers < 2^7 is stored in one byte
|
|
||||||
* - numbers < 2^14 is stored in two bytes
|
|
||||||
* ..
|
|
||||||
*/
|
|
||||||
readVarUint () {
|
|
||||||
let num = 0
|
|
||||||
let len = 0
|
|
||||||
while (true) {
|
|
||||||
let r = this.uint8arr[this.pos++]
|
|
||||||
num = num | ((r & 0b1111111) << len)
|
|
||||||
len += 7
|
|
||||||
if (r < 1 << 7) {
|
|
||||||
return num >>> 0 // return unsigned number!
|
|
||||||
}
|
|
||||||
if (len > 35) {
|
|
||||||
throw new Error('Integer out of range!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read string of variable length
|
|
||||||
* - varUint is used to store the length of the string
|
|
||||||
*/
|
|
||||||
readVarString () {
|
|
||||||
let len = this.readVarUint()
|
|
||||||
let bytes = new Array(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = this.uint8arr[this.pos++]
|
|
||||||
}
|
|
||||||
return utf8.getStringFromBytes(bytes)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Look ahead and read varString without incrementing position
|
|
||||||
*/
|
|
||||||
peekVarString () {
|
|
||||||
let pos = this.pos
|
|
||||||
let s = this.readVarString()
|
|
||||||
this.pos = pos
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Read ID
|
|
||||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
|
||||||
* - Otherwise an ID is returned
|
|
||||||
*/
|
|
||||||
readID () {
|
|
||||||
let user = this.readVarUint()
|
|
||||||
if (user === RootFakeUserID) {
|
|
||||||
// read property name and type id
|
|
||||||
const rid = new RootID(this.readVarString(), null)
|
|
||||||
rid.type = this.readVarUint()
|
|
||||||
return rid
|
|
||||||
}
|
|
||||||
return new ID(user, this.readVarUint())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import utf8 from 'utf-8'
|
|
||||||
import { RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
|
|
||||||
const bits7 = 0b1111111
|
|
||||||
const bits8 = 0b11111111
|
|
||||||
|
|
||||||
export default class BinaryEncoder {
|
|
||||||
constructor () {
|
|
||||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
|
||||||
this.data = []
|
|
||||||
}
|
|
||||||
|
|
||||||
get length () {
|
|
||||||
return this.data.length
|
|
||||||
}
|
|
||||||
|
|
||||||
get pos () {
|
|
||||||
return this.data.length
|
|
||||||
}
|
|
||||||
|
|
||||||
createBuffer () {
|
|
||||||
return Uint8Array.from(this.data).buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
writeUint8 (num) {
|
|
||||||
this.data.push(num & bits8)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUint8 (pos, num) {
|
|
||||||
this.data[pos] = num & bits8
|
|
||||||
}
|
|
||||||
|
|
||||||
writeUint16 (num) {
|
|
||||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUint16 (pos, num) {
|
|
||||||
this.data[pos] = num & bits8
|
|
||||||
this.data[pos + 1] = (num >>> 8) & bits8
|
|
||||||
}
|
|
||||||
|
|
||||||
writeUint32 (num) {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.data.push(num & bits8)
|
|
||||||
num >>>= 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setUint32 (pos, num) {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
this.data[pos + i] = num & bits8
|
|
||||||
num >>>= 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeVarUint (num) {
|
|
||||||
while (num >= 0b10000000) {
|
|
||||||
this.data.push(0b10000000 | (bits7 & num))
|
|
||||||
num >>>= 7
|
|
||||||
}
|
|
||||||
this.data.push(bits7 & num)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeVarString (str) {
|
|
||||||
let bytes = utf8.setBytesFromString(str)
|
|
||||||
let len = bytes.length
|
|
||||||
this.writeVarUint(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
this.data.push(bytes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeID (id) {
|
|
||||||
const user = id.user
|
|
||||||
this.writeVarUint(user)
|
|
||||||
if (user !== RootFakeUserID) {
|
|
||||||
this.writeVarUint(id.clock)
|
|
||||||
} else {
|
|
||||||
this.writeVarString(id.name)
|
|
||||||
this.writeVarUint(id.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
294
src/Connector.js
294
src/Connector.js
@@ -1,294 +0,0 @@
|
|||||||
import BinaryEncoder from './Binary/Encoder.js'
|
|
||||||
import BinaryDecoder from './Binary/Decoder.js'
|
|
||||||
|
|
||||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
|
||||||
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
|
||||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
|
||||||
|
|
||||||
import debug from 'debug'
|
|
||||||
|
|
||||||
export default class AbstractConnector {
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y
|
|
||||||
this.opts = opts
|
|
||||||
if (opts.role == null || opts.role === 'master') {
|
|
||||||
this.role = 'master'
|
|
||||||
} else if (opts.role === 'slave') {
|
|
||||||
this.role = 'slave'
|
|
||||||
} else {
|
|
||||||
throw new Error("Role must be either 'master' or 'slave'!")
|
|
||||||
}
|
|
||||||
this.log = debug('y:connector')
|
|
||||||
this.logMessage = debug('y:connector-message')
|
|
||||||
this._forwardAppliedStructs = opts.forwardAppliedOperations || false // TODO: rename
|
|
||||||
this.role = opts.role
|
|
||||||
this.connections = new Map()
|
|
||||||
this.isSynced = false
|
|
||||||
this.userEventListeners = []
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.debug = opts.debug === true
|
|
||||||
this.broadcastBuffer = new BinaryEncoder()
|
|
||||||
this.broadcastBufferSize = 0
|
|
||||||
this.protocolVersion = 11
|
|
||||||
this.authInfo = opts.auth || null
|
|
||||||
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
|
|
||||||
if (opts.maxBufferLength == null) {
|
|
||||||
this.maxBufferLength = -1
|
|
||||||
} else {
|
|
||||||
this.maxBufferLength = opts.maxBufferLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnect () {
|
|
||||||
this.log('reconnecting..')
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect () {
|
|
||||||
this.log('discronnecting..')
|
|
||||||
this.connections = new Map()
|
|
||||||
this.isSynced = false
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
onUserEvent (f) {
|
|
||||||
this.userEventListeners.push(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeUserEventListener (f) {
|
|
||||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
|
||||||
}
|
|
||||||
|
|
||||||
userLeft (user) {
|
|
||||||
if (this.connections.has(user)) {
|
|
||||||
this.log('%s: User left %s', this.y.userID, user)
|
|
||||||
this.connections.delete(user)
|
|
||||||
// check if isSynced event can be sent now
|
|
||||||
this._setSyncedWith(null)
|
|
||||||
for (var f of this.userEventListeners) {
|
|
||||||
f({
|
|
||||||
action: 'userLeft',
|
|
||||||
user: user
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userJoined (user, role, auth) {
|
|
||||||
if (role == null) {
|
|
||||||
throw new Error('You must specify the role of the joined user!')
|
|
||||||
}
|
|
||||||
if (this.connections.has(user)) {
|
|
||||||
throw new Error('This user already joined!')
|
|
||||||
}
|
|
||||||
this.log('%s: User joined %s', this.y.userID, user)
|
|
||||||
this.connections.set(user, {
|
|
||||||
uid: user,
|
|
||||||
isSynced: false,
|
|
||||||
role: role,
|
|
||||||
processAfterAuth: [],
|
|
||||||
processAfterSync: [],
|
|
||||||
auth: auth || null,
|
|
||||||
receivedSyncStep2: false
|
|
||||||
})
|
|
||||||
let defer = {}
|
|
||||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
|
||||||
this.connections.get(user).syncStep2 = defer
|
|
||||||
for (var f of this.userEventListeners) {
|
|
||||||
f({
|
|
||||||
action: 'userJoined',
|
|
||||||
user: user,
|
|
||||||
role: role
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this._syncWithUser(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute a function _when_ we are connected.
|
|
||||||
// If not connected, wait until connected
|
|
||||||
whenSynced (f) {
|
|
||||||
if (this.isSynced) {
|
|
||||||
f()
|
|
||||||
} else {
|
|
||||||
this.whenSyncedListeners.push(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_syncWithUser (userID) {
|
|
||||||
if (this.role === 'slave') {
|
|
||||||
return // "The current sync has not finished or this is controlled by a master!"
|
|
||||||
}
|
|
||||||
sendSyncStep1(this, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
_fireIsSyncedListeners () {
|
|
||||||
if (!this.isSynced) {
|
|
||||||
this.isSynced = true
|
|
||||||
// It is safer to remove this!
|
|
||||||
// call whensynced listeners
|
|
||||||
for (var f of this.whenSyncedListeners) {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
this.y.emit('synced')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
send (uid, buffer) {
|
|
||||||
const y = this.y
|
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
|
||||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
|
||||||
}
|
|
||||||
this.log('User%s to User%s: Send \'%y\'', y.userID, uid, buffer)
|
|
||||||
this.logMessage('User%s to User%s: Send %Y', y.userID, uid, [y, buffer])
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast (buffer) {
|
|
||||||
const y = this.y
|
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
|
||||||
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
|
|
||||||
}
|
|
||||||
this.log('User%s: Broadcast \'%y\'', y.userID, buffer)
|
|
||||||
this.logMessage('User%s: Broadcast: %Y', y.userID, [y, buffer])
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Buffer operations, and broadcast them when ready.
|
|
||||||
*/
|
|
||||||
broadcastStruct (struct) {
|
|
||||||
const firstContent = this.broadcastBuffer.length === 0
|
|
||||||
if (firstContent) {
|
|
||||||
this.broadcastBuffer.writeVarString(this.y.room)
|
|
||||||
this.broadcastBuffer.writeVarString('update')
|
|
||||||
this.broadcastBufferSize = 0
|
|
||||||
this.broadcastBufferSizePos = this.broadcastBuffer.pos
|
|
||||||
this.broadcastBuffer.writeUint32(0)
|
|
||||||
}
|
|
||||||
this.broadcastBufferSize++
|
|
||||||
struct._toBinary(this.broadcastBuffer)
|
|
||||||
if (this.maxBufferLength > 0 && this.broadcastBuffer.length > this.maxBufferLength) {
|
|
||||||
// it is necessary to send the buffer now
|
|
||||||
// cache the buffer and check if server is responsive
|
|
||||||
const buffer = this.broadcastBuffer
|
|
||||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
|
||||||
this.broadcastBuffer = new BinaryEncoder()
|
|
||||||
this.whenRemoteResponsive().then(() => {
|
|
||||||
this.broadcast(buffer.createBuffer())
|
|
||||||
})
|
|
||||||
} else if (firstContent) {
|
|
||||||
// send the buffer when all transactions are finished
|
|
||||||
// (or buffer exceeds maxBufferLength)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.broadcastBuffer.length > 0) {
|
|
||||||
const buffer = this.broadcastBuffer
|
|
||||||
buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
|
|
||||||
this.broadcast(buffer.createBuffer())
|
|
||||||
this.broadcastBuffer = new BinaryEncoder()
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Somehow check the responsiveness of the remote clients/server
|
|
||||||
* Default behavior:
|
|
||||||
* Wait 100ms before broadcasting the next batch of operations
|
|
||||||
*
|
|
||||||
* Only used when maxBufferLength is set
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
whenRemoteResponsive () {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
setTimeout(resolve, 100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
|
||||||
*/
|
|
||||||
receiveMessage (sender, buffer, skipAuth) {
|
|
||||||
const y = this.y
|
|
||||||
const userID = y.userID
|
|
||||||
skipAuth = skipAuth || false
|
|
||||||
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
|
|
||||||
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
|
|
||||||
}
|
|
||||||
if (sender === userID) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
let roomname = decoder.readVarString() // read room name
|
|
||||||
encoder.writeVarString(roomname)
|
|
||||||
let messageType = decoder.readVarString()
|
|
||||||
let senderConn = this.connections.get(sender)
|
|
||||||
this.log('User%s from User%s: Receive \'%s\'', userID, sender, messageType)
|
|
||||||
this.logMessage('User%s from User%s: Receive %Y', userID, sender, [y, buffer])
|
|
||||||
if (senderConn == null && !skipAuth) {
|
|
||||||
throw new Error('Received message from unknown peer!')
|
|
||||||
}
|
|
||||||
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
|
|
||||||
let auth = decoder.readVarUint()
|
|
||||||
if (senderConn.auth == null) {
|
|
||||||
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
|
|
||||||
// check auth
|
|
||||||
return this.checkAuth(auth, y, sender).then(authPermissions => {
|
|
||||||
if (senderConn.auth == null) {
|
|
||||||
senderConn.auth = authPermissions
|
|
||||||
y.emit('userAuthenticated', {
|
|
||||||
user: senderConn.uid,
|
|
||||||
auth: authPermissions
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let messages = senderConn.processAfterAuth
|
|
||||||
senderConn.processAfterAuth = []
|
|
||||||
|
|
||||||
messages.forEach(m =>
|
|
||||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((skipAuth || senderConn.auth != null) && (messageType !== 'update' || senderConn.isSynced)) {
|
|
||||||
this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
|
|
||||||
} else {
|
|
||||||
senderConn.processAfterSync.push([messageType, senderConn, decoder, encoder, sender, false])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
|
|
||||||
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
|
|
||||||
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
|
|
||||||
readSyncStep1(decoder, encoder, this.y, senderConn, sender)
|
|
||||||
} else {
|
|
||||||
const y = this.y
|
|
||||||
y.transact(function () {
|
|
||||||
if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
|
||||||
readSyncStep2(decoder, encoder, y, senderConn, sender)
|
|
||||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
|
||||||
integrateRemoteStructs(decoder, encoder, y, senderConn, sender)
|
|
||||||
} else {
|
|
||||||
throw new Error('Unable to receive message')
|
|
||||||
}
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setSyncedWith (user) {
|
|
||||||
if (user != null) {
|
|
||||||
const userConn = this.connections.get(user)
|
|
||||||
userConn.isSynced = true
|
|
||||||
const messages = userConn.processAfterSync
|
|
||||||
userConn.processAfterSync = []
|
|
||||||
messages.forEach(m => {
|
|
||||||
this.computeMessage(m[0], m[1], m[2], m[3], m[4])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const conns = Array.from(this.connections.values())
|
|
||||||
if (conns.length > 0 && conns.every(u => u.isSynced)) {
|
|
||||||
this._fireIsSyncedListeners()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { deleteItemRange } from '../Struct/Delete.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
|
||||||
let dsLength = decoder.readUint32()
|
|
||||||
for (let i = 0; i < dsLength; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
strBuilder.push(' -' + user + ':')
|
|
||||||
let dvLength = decoder.readVarUint()
|
|
||||||
for (let j = 0; j < dvLength; j++) {
|
|
||||||
let from = decoder.readVarUint()
|
|
||||||
let len = decoder.readVarUint()
|
|
||||||
let gc = decoder.readUint8() === 1
|
|
||||||
strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strBuilder
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeDeleteSet (y, encoder) {
|
|
||||||
let currentUser = null
|
|
||||||
let currentLength
|
|
||||||
let lastLenPos
|
|
||||||
|
|
||||||
let numberOfUsers = 0
|
|
||||||
let laterDSLenPus = encoder.pos
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
|
|
||||||
y.ds.iterate(null, null, function (n) {
|
|
||||||
var user = n._id.user
|
|
||||||
var clock = n._id.clock
|
|
||||||
var len = n.len
|
|
||||||
var gc = n.gc
|
|
||||||
if (currentUser !== user) {
|
|
||||||
numberOfUsers++
|
|
||||||
// a new user was found
|
|
||||||
if (currentUser !== null) { // happens on first iteration
|
|
||||||
encoder.setUint32(lastLenPos, currentLength)
|
|
||||||
}
|
|
||||||
currentUser = user
|
|
||||||
encoder.writeVarUint(user)
|
|
||||||
// pseudo-fill pos
|
|
||||||
lastLenPos = encoder.pos
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
currentLength = 0
|
|
||||||
}
|
|
||||||
encoder.writeVarUint(clock)
|
|
||||||
encoder.writeVarUint(len)
|
|
||||||
encoder.writeUint8(gc ? 1 : 0)
|
|
||||||
currentLength++
|
|
||||||
})
|
|
||||||
if (currentUser !== null) { // happens on first iteration
|
|
||||||
encoder.setUint32(lastLenPos, currentLength)
|
|
||||||
}
|
|
||||||
encoder.setUint32(laterDSLenPus, numberOfUsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readDeleteSet (y, decoder) {
|
|
||||||
let dsLength = decoder.readUint32()
|
|
||||||
for (let i = 0; i < dsLength; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
let dv = []
|
|
||||||
let dvLength = decoder.readUint32()
|
|
||||||
for (let j = 0; j < dvLength; j++) {
|
|
||||||
let from = decoder.readVarUint()
|
|
||||||
let len = decoder.readVarUint()
|
|
||||||
let gc = decoder.readUint8() === 1
|
|
||||||
dv.push([from, len, gc])
|
|
||||||
}
|
|
||||||
if (dvLength > 0) {
|
|
||||||
let pos = 0
|
|
||||||
let d = dv[pos]
|
|
||||||
let deletions = []
|
|
||||||
y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
|
|
||||||
// cases:
|
|
||||||
// 1. d deletes something to the right of n
|
|
||||||
// => go to next n (break)
|
|
||||||
// 2. d deletes something to the left of n
|
|
||||||
// => create deletions
|
|
||||||
// => reset d accordingly
|
|
||||||
// *)=> if d doesn't delete anything anymore, go to next d (continue)
|
|
||||||
// 3. not 2) and d deletes something that also n deletes
|
|
||||||
// => reset d so that it doesn't contain n's deletion
|
|
||||||
// *)=> if d does not delete anything anymore, go to next d (continue)
|
|
||||||
while (d != null) {
|
|
||||||
var diff = 0 // describe the diff of length in 1) and 2)
|
|
||||||
if (n._id.clock + n.len <= d[0]) {
|
|
||||||
// 1)
|
|
||||||
break
|
|
||||||
} else if (d[0] < n._id.clock) {
|
|
||||||
// 2)
|
|
||||||
// delete maximum the len of d
|
|
||||||
// else delete as much as possible
|
|
||||||
diff = Math.min(n._id.clock - d[0], d[1])
|
|
||||||
// deleteItemRange(y, user, d[0], diff)
|
|
||||||
deletions.push([user, d[0], diff])
|
|
||||||
} else {
|
|
||||||
// 3)
|
|
||||||
diff = n._id.clock + n.len - d[0] // never null (see 1)
|
|
||||||
if (d[2] && !n.gc) {
|
|
||||||
// d marks as gc'd but n does not
|
|
||||||
// then delete either way
|
|
||||||
// deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
|
|
||||||
deletions.push([user, d[0], Math.min(diff, d[1])])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (d[1] <= diff) {
|
|
||||||
// d doesn't delete anything anymore
|
|
||||||
d = dv[++pos]
|
|
||||||
} else {
|
|
||||||
d[0] = d[0] + diff // reset pos
|
|
||||||
d[1] = d[1] - diff // reset length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// TODO: It would be more performant to apply the deletes in the above loop
|
|
||||||
// Adapt the Tree implementation to support delete while iterating
|
|
||||||
for (let i = deletions.length - 1; i >= 0; i--) {
|
|
||||||
const del = deletions[i]
|
|
||||||
deleteItemRange(y, del[0], del[1], del[2])
|
|
||||||
}
|
|
||||||
// for the rest.. just apply it
|
|
||||||
for (; pos < dv.length; pos++) {
|
|
||||||
d = dv[pos]
|
|
||||||
deleteItemRange(y, user, d[0], d[1])
|
|
||||||
// deletions.push([user, d[0], d[1], d[2]])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { getStruct } from '../Util/structReferences.js'
|
|
||||||
import BinaryDecoder from '../Binary/Decoder.js'
|
|
||||||
import { logID } from './messageToString.js'
|
|
||||||
|
|
||||||
class MissingEntry {
|
|
||||||
constructor (decoder, missing, struct) {
|
|
||||||
this.decoder = decoder
|
|
||||||
this.missing = missing.length
|
|
||||||
this.struct = struct
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integrate remote struct
|
|
||||||
* When a remote struct is integrated, other structs might be ready to ready to
|
|
||||||
* integrate.
|
|
||||||
*/
|
|
||||||
function _integrateRemoteStructHelper (y, struct) {
|
|
||||||
const id = struct._id
|
|
||||||
if (id === undefined) {
|
|
||||||
struct._integrate(y)
|
|
||||||
} else {
|
|
||||||
if (y.ss.getState(id.user) > id.clock) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
struct._integrate(y)
|
|
||||||
let msu = y._missingStructs.get(id.user)
|
|
||||||
if (msu != null) {
|
|
||||||
let clock = id.clock
|
|
||||||
const finalClock = clock + struct._length
|
|
||||||
for (;clock < finalClock; clock++) {
|
|
||||||
const missingStructs = msu.get(clock)
|
|
||||||
if (missingStructs !== undefined) {
|
|
||||||
missingStructs.forEach(missingDef => {
|
|
||||||
missingDef.missing--
|
|
||||||
if (missingDef.missing === 0) {
|
|
||||||
const decoder = missingDef.decoder
|
|
||||||
let oldPos = decoder.pos
|
|
||||||
let missing = missingDef.struct._fromBinary(y, decoder)
|
|
||||||
decoder.pos = oldPos
|
|
||||||
if (missing.length === 0) {
|
|
||||||
y._readyToIntegrate.push(missingDef.struct)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
msu.delete(clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringifyStructs (y, decoder, strBuilder) {
|
|
||||||
const len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let reference = decoder.readVarUint()
|
|
||||||
let Constr = getStruct(reference)
|
|
||||||
let struct = new Constr()
|
|
||||||
let missing = struct._fromBinary(y, decoder)
|
|
||||||
let logMessage = ' ' + struct._logString()
|
|
||||||
if (missing.length > 0) {
|
|
||||||
logMessage += ' .. missing: ' + missing.map(logID).join(', ')
|
|
||||||
}
|
|
||||||
strBuilder.push(logMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function integrateRemoteStructs (decoder, encoder, y) {
|
|
||||||
const len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let reference = decoder.readVarUint()
|
|
||||||
let Constr = getStruct(reference)
|
|
||||||
let struct = new Constr()
|
|
||||||
let decoderPos = decoder.pos
|
|
||||||
let missing = struct._fromBinary(y, decoder)
|
|
||||||
if (missing.length === 0) {
|
|
||||||
while (struct != null) {
|
|
||||||
_integrateRemoteStructHelper(y, struct)
|
|
||||||
struct = y._readyToIntegrate.shift()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let _decoder = new BinaryDecoder(decoder.uint8arr)
|
|
||||||
_decoder.pos = decoderPos
|
|
||||||
let missingEntry = new MissingEntry(_decoder, missing, struct)
|
|
||||||
let missingStructs = y._missingStructs
|
|
||||||
for (let i = missing.length - 1; i >= 0; i--) {
|
|
||||||
let m = missing[i]
|
|
||||||
if (!missingStructs.has(m.user)) {
|
|
||||||
missingStructs.set(m.user, new Map())
|
|
||||||
}
|
|
||||||
let msu = missingStructs.get(m.user)
|
|
||||||
if (!msu.has(m.clock)) {
|
|
||||||
msu.set(m.clock, [])
|
|
||||||
}
|
|
||||||
let mArray = msu = msu.get(m.clock)
|
|
||||||
mArray.push(missingEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import BinaryDecoder from '../Binary/Decoder.js'
|
|
||||||
import { stringifyStructs } from './integrateRemoteStructs.js'
|
|
||||||
import { stringifySyncStep1 } from './syncStep1.js'
|
|
||||||
import { stringifySyncStep2 } from './syncStep2.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import RootID from '../Util/RootID.js'
|
|
||||||
import Y from '../Y.js'
|
|
||||||
|
|
||||||
export function messageToString ([y, buffer]) {
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
decoder.readVarString() // read roomname
|
|
||||||
let type = decoder.readVarString()
|
|
||||||
let strBuilder = []
|
|
||||||
strBuilder.push('\n === ' + type + ' ===')
|
|
||||||
if (type === 'update') {
|
|
||||||
stringifyStructs(y, decoder, strBuilder)
|
|
||||||
} else if (type === 'sync step 1') {
|
|
||||||
stringifySyncStep1(y, decoder, strBuilder)
|
|
||||||
} else if (type === 'sync step 2') {
|
|
||||||
stringifySyncStep2(y, decoder, strBuilder)
|
|
||||||
} else {
|
|
||||||
strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
|
|
||||||
}
|
|
||||||
return strBuilder.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function messageToRoomname (buffer) {
|
|
||||||
let decoder = new BinaryDecoder(buffer)
|
|
||||||
decoder.readVarString() // roomname
|
|
||||||
return decoder.readVarString() // messageType
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logID (id) {
|
|
||||||
if (id !== null && id._id != null) {
|
|
||||||
id = id._id
|
|
||||||
}
|
|
||||||
if (id === null) {
|
|
||||||
return '()'
|
|
||||||
} else if (id instanceof ID) {
|
|
||||||
return `(${id.user},${id.clock})`
|
|
||||||
} else if (id instanceof RootID) {
|
|
||||||
return `(${id.name},${id.type})`
|
|
||||||
} else if (id.constructor === Y) {
|
|
||||||
return `y`
|
|
||||||
} else {
|
|
||||||
throw new Error('This is not a valid ID!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
export function readStateSet (decoder) {
|
|
||||||
let ss = new Map()
|
|
||||||
let ssLength = decoder.readUint32()
|
|
||||||
for (let i = 0; i < ssLength; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
let clock = decoder.readVarUint()
|
|
||||||
ss.set(user, clock)
|
|
||||||
}
|
|
||||||
return ss
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeStateSet (y, encoder) {
|
|
||||||
let lenPosition = encoder.pos
|
|
||||||
let len = 0
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
for (let [user, clock] of y.ss.state) {
|
|
||||||
encoder.writeVarUint(user)
|
|
||||||
encoder.writeVarUint(clock)
|
|
||||||
len++
|
|
||||||
}
|
|
||||||
encoder.setUint32(lenPosition, len)
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import BinaryEncoder from '../Binary/Encoder.js'
|
|
||||||
import { readStateSet, writeStateSet } from './stateSet.js'
|
|
||||||
import { writeDeleteSet } from './deleteSet.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
|
|
||||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
|
||||||
let auth = decoder.readVarString()
|
|
||||||
let protocolVersion = decoder.readVarUint()
|
|
||||||
strBuilder.push(` - auth: "${auth}"`)
|
|
||||||
strBuilder.push(` - protocolVersion: ${protocolVersion}`)
|
|
||||||
// write SS
|
|
||||||
let ssBuilder = []
|
|
||||||
let len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
let clock = decoder.readVarUint()
|
|
||||||
ssBuilder.push(`(${user}:${clock})`)
|
|
||||||
}
|
|
||||||
strBuilder.push(' == SS: ' + ssBuilder.join(','))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sendSyncStep1 (connector, syncUser) {
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarString(connector.y.room)
|
|
||||||
encoder.writeVarString('sync step 1')
|
|
||||||
encoder.writeVarString(connector.authInfo || '')
|
|
||||||
encoder.writeVarUint(connector.protocolVersion)
|
|
||||||
writeStateSet(connector.y, encoder)
|
|
||||||
connector.send(syncUser, encoder.createBuffer())
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function writeStructs (encoder, decoder, y, ss) {
|
|
||||||
const lenPos = encoder.pos
|
|
||||||
encoder.writeUint32(0)
|
|
||||||
let len = 0
|
|
||||||
for (let user of y.ss.state.keys()) {
|
|
||||||
let clock = ss.get(user) || 0
|
|
||||||
if (user !== RootFakeUserID) {
|
|
||||||
y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) {
|
|
||||||
struct._toBinary(encoder)
|
|
||||||
len++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
encoder.setUint32(lenPos, len)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
|
||||||
let protocolVersion = decoder.readVarUint()
|
|
||||||
// check protocol version
|
|
||||||
if (protocolVersion !== y.connector.protocolVersion) {
|
|
||||||
console.warn(
|
|
||||||
`You tried to sync with a Yjs instance that has a different protocol version
|
|
||||||
(You: ${protocolVersion}, Client: ${protocolVersion}).
|
|
||||||
`)
|
|
||||||
y.destroy()
|
|
||||||
}
|
|
||||||
// write sync step 2
|
|
||||||
encoder.writeVarString('sync step 2')
|
|
||||||
encoder.writeVarString(y.connector.authInfo || '')
|
|
||||||
const ss = readStateSet(decoder)
|
|
||||||
writeStructs(encoder, decoder, y, ss)
|
|
||||||
writeDeleteSet(y, encoder)
|
|
||||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
|
||||||
senderConn.receivedSyncStep2 = true
|
|
||||||
if (y.connector.role === 'slave') {
|
|
||||||
sendSyncStep1(y.connector, sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
|
||||||
import { readDeleteSet } from './deleteSet.js'
|
|
||||||
|
|
||||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
|
||||||
strBuilder.push(' - auth: ' + decoder.readVarString())
|
|
||||||
strBuilder.push(' == OS:')
|
|
||||||
stringifyStructs(y, decoder, strBuilder)
|
|
||||||
// write DS to string
|
|
||||||
strBuilder.push(' == DS:')
|
|
||||||
let len = decoder.readUint32()
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let user = decoder.readVarUint()
|
|
||||||
strBuilder.push(` User: ${user}: `)
|
|
||||||
let len2 = decoder.readUint32()
|
|
||||||
for (let j = 0; j < len2; j++) {
|
|
||||||
let from = decoder.readVarUint()
|
|
||||||
let to = decoder.readVarUint()
|
|
||||||
let gc = decoder.readUint8() === 1
|
|
||||||
strBuilder.push(`[${from}, ${to}, ${gc}]`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
|
||||||
integrateRemoteStructs(decoder, encoder, y)
|
|
||||||
readDeleteSet(y, decoder)
|
|
||||||
y.connector._setSyncedWith(sender)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// import BinaryEncoder from './Binary/Encoder.js'
|
|
||||||
|
|
||||||
export default function extendPersistence (Y) {
|
|
||||||
class AbstractPersistence {
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y
|
|
||||||
this.opts = opts
|
|
||||||
this.saveOperationsBuffer = []
|
|
||||||
this.log = Y.debug('y:persistence')
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToMessageQueue (binary) {
|
|
||||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
|
||||||
}
|
|
||||||
|
|
||||||
saveOperations (ops) {
|
|
||||||
ops = ops.map(function (op) {
|
|
||||||
return Y.Struct[op.struct].encode(op)
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
const saveOperations = () => {
|
|
||||||
if (this.saveOperationsBuffer.length > 0) {
|
|
||||||
let encoder = new BinaryEncoder()
|
|
||||||
encoder.writeVarString(this.opts.room)
|
|
||||||
encoder.writeVarString('update')
|
|
||||||
let ops = this.saveOperationsBuffer
|
|
||||||
this.saveOperationsBuffer = []
|
|
||||||
let length = ops.length
|
|
||||||
encoder.writeUint32(length)
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
let op = ops[i]
|
|
||||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
|
||||||
}
|
|
||||||
this.saveToMessageQueue(encoder.createBuffer())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (this.saveOperationsBuffer.length === 0) {
|
|
||||||
this.saveOperationsBuffer = ops
|
|
||||||
} else {
|
|
||||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.AbstractPersistence = AbstractPersistence
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import Tree from '../Util/Tree.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
class DSNode {
|
|
||||||
constructor (id, len, gc) {
|
|
||||||
this._id = id
|
|
||||||
this.len = len
|
|
||||||
this.gc = gc
|
|
||||||
}
|
|
||||||
clone () {
|
|
||||||
return new DSNode(this._id, this.len, this.gc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeleteStore extends Tree {
|
|
||||||
logTable () {
|
|
||||||
const deletes = []
|
|
||||||
this.iterate(null, null, function (n) {
|
|
||||||
deletes.push({
|
|
||||||
user: n._id.user,
|
|
||||||
clock: n._id.clock,
|
|
||||||
len: n.len,
|
|
||||||
gc: n.gc
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.table(deletes)
|
|
||||||
}
|
|
||||||
isDeleted (id) {
|
|
||||||
var n = this.findWithUpperBound(id)
|
|
||||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Mark an operation as deleted. returns the deleted node
|
|
||||||
*/
|
|
||||||
markDeleted (id, length) {
|
|
||||||
if (length == null) {
|
|
||||||
throw new Error('length must be defined')
|
|
||||||
}
|
|
||||||
var n = this.findWithUpperBound(id)
|
|
||||||
if (n != null && n._id.user === id.user) {
|
|
||||||
if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) {
|
|
||||||
// id is in n's range
|
|
||||||
var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
|
|
||||||
if (diff > 0) {
|
|
||||||
// id+length overlaps n
|
|
||||||
if (!n.gc) {
|
|
||||||
n.len += diff
|
|
||||||
} else {
|
|
||||||
diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
|
|
||||||
if (diff < length) {
|
|
||||||
// a partial deletion
|
|
||||||
let nId = id.clone()
|
|
||||||
nId.clock += diff
|
|
||||||
n = new DSNode(nId, length - diff, false)
|
|
||||||
this.put(n)
|
|
||||||
} else {
|
|
||||||
// already gc'd
|
|
||||||
throw new Error(
|
|
||||||
'DS reached an inconsistent state. Please report this issue!'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no overlapping, already deleted
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// cannot extend left (there is no left!)
|
|
||||||
n = new DSNode(id, length, false)
|
|
||||||
this.put(n) // TODO: you double-put !!
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// cannot extend left
|
|
||||||
n = new DSNode(id, length, false)
|
|
||||||
this.put(n)
|
|
||||||
}
|
|
||||||
// can extend right?
|
|
||||||
var next = this.findNext(n._id)
|
|
||||||
if (
|
|
||||||
next != null &&
|
|
||||||
n._id.user === next._id.user &&
|
|
||||||
n._id.clock + n.len >= next._id.clock
|
|
||||||
) {
|
|
||||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
|
||||||
while (diff >= 0) {
|
|
||||||
// n overlaps with next
|
|
||||||
if (next.gc) {
|
|
||||||
// gc is stronger, so reduce length of n
|
|
||||||
n.len -= diff
|
|
||||||
if (diff >= next.len) {
|
|
||||||
// delete the missing range after next
|
|
||||||
diff = diff - next.len // missing range after next
|
|
||||||
if (diff > 0) {
|
|
||||||
this.put(n) // unneccessary? TODO!
|
|
||||||
this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
// we can extend n with next
|
|
||||||
if (diff > next.len) {
|
|
||||||
// n is even longer than next
|
|
||||||
// get next.next, and try to extend it
|
|
||||||
var _next = this.findNext(next._id)
|
|
||||||
this.delete(next._id)
|
|
||||||
if (_next == null || n._id.user !== _next._id.user) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
next = _next
|
|
||||||
diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
|
|
||||||
// continue!
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// n just partially overlaps with next. extend n, delete next, and break this loop
|
|
||||||
n.len += next.len - diff
|
|
||||||
this.delete(next._id)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.put(n)
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import Tree from '../Util/Tree.js'
|
|
||||||
import RootID from '../Util/RootID.js'
|
|
||||||
import { getStruct } from '../Util/structReferences.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class OperationStore extends Tree {
|
|
||||||
constructor (y) {
|
|
||||||
super()
|
|
||||||
this.y = y
|
|
||||||
}
|
|
||||||
logTable () {
|
|
||||||
const items = []
|
|
||||||
this.iterate(null, null, function (item) {
|
|
||||||
items.push({
|
|
||||||
id: logID(item),
|
|
||||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
|
||||||
left: logID(item._left === null ? null : item._left._lastId),
|
|
||||||
right: logID(item._right),
|
|
||||||
right_origin: logID(item._right_origin),
|
|
||||||
parent: logID(item._parent),
|
|
||||||
parentSub: item._parentSub,
|
|
||||||
deleted: item._deleted,
|
|
||||||
content: JSON.stringify(item._content)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.table(items)
|
|
||||||
}
|
|
||||||
get (id) {
|
|
||||||
let struct = this.find(id)
|
|
||||||
if (struct === null && id instanceof RootID) {
|
|
||||||
const Constr = getStruct(id.type)
|
|
||||||
const y = this.y
|
|
||||||
struct = new Constr()
|
|
||||||
struct._id = id
|
|
||||||
struct._parent = y
|
|
||||||
y.transact(() => {
|
|
||||||
struct._integrate(y)
|
|
||||||
})
|
|
||||||
this.put(struct)
|
|
||||||
}
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
// Use getItem for structs with _length > 1
|
|
||||||
getItem (id) {
|
|
||||||
var item = this.findWithUpperBound(id)
|
|
||||||
if (item === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const itemID = item._id
|
|
||||||
if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
|
|
||||||
return item
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return an insertion such that id is the first element of content
|
|
||||||
// This function manipulates an item, if necessary
|
|
||||||
getItemCleanStart (id) {
|
|
||||||
var ins = this.getItem(id)
|
|
||||||
if (ins === null || ins._length === 1) {
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
const insID = ins._id
|
|
||||||
if (insID.clock === id.clock) {
|
|
||||||
return ins
|
|
||||||
} else {
|
|
||||||
return ins._splitAt(this.y, id.clock - insID.clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return an insertion such that id is the last element of content
|
|
||||||
// This function manipulates an operation, if necessary
|
|
||||||
getItemCleanEnd (id) {
|
|
||||||
var ins = this.getItem(id)
|
|
||||||
if (ins === null || ins._length === 1) {
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
const insID = ins._id
|
|
||||||
if (insID.clock + ins._length - 1 === id.clock) {
|
|
||||||
return ins
|
|
||||||
} else {
|
|
||||||
ins._splitAt(this.y, id.clock - insID.clock + 1)
|
|
||||||
return ins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
export default class StateStore {
|
|
||||||
constructor (y) {
|
|
||||||
this.y = y
|
|
||||||
this.state = new Map()
|
|
||||||
}
|
|
||||||
logTable () {
|
|
||||||
const entries = []
|
|
||||||
for (let [user, state] of this.state) {
|
|
||||||
entries.push({
|
|
||||||
user, state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
console.table(entries)
|
|
||||||
}
|
|
||||||
getNextID (len) {
|
|
||||||
const user = this.y.userID
|
|
||||||
const state = this.getState(user)
|
|
||||||
this.setState(user, state + len)
|
|
||||||
return new ID(user, state)
|
|
||||||
}
|
|
||||||
updateRemoteState (struct) {
|
|
||||||
let user = struct._id.user
|
|
||||||
let userState = this.state.get(user)
|
|
||||||
while (struct !== null && struct._id.clock === userState) {
|
|
||||||
userState += struct._length
|
|
||||||
struct = this.y.os.get(new ID(user, userState))
|
|
||||||
}
|
|
||||||
this.state.set(user, userState)
|
|
||||||
}
|
|
||||||
getState (user) {
|
|
||||||
let state = this.state.get(user)
|
|
||||||
if (state == null) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
setState (user, state) {
|
|
||||||
// TODO: modify missingi structs here
|
|
||||||
const beforeState = this.y._transaction.beforeState
|
|
||||||
if (!beforeState.has(user)) {
|
|
||||||
beforeState.set(user, this.getState(user))
|
|
||||||
}
|
|
||||||
this.state.set(user, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { getReference } from '../Util/structReferences.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all items in an ID-range
|
|
||||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
|
||||||
*/
|
|
||||||
export function deleteItemRange (y, user, clock, range) {
|
|
||||||
const createDelete = y.connector._forwardAppliedStructs
|
|
||||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
|
||||||
if (item !== null) {
|
|
||||||
if (!item._deleted) {
|
|
||||||
item._splitAt(y, range)
|
|
||||||
item._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
let itemLen = item._length
|
|
||||||
range -= itemLen
|
|
||||||
clock += itemLen
|
|
||||||
if (range > 0) {
|
|
||||||
let node = y.os.findNode(new ID(user, clock))
|
|
||||||
while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
|
||||||
const nodeVal = node.val
|
|
||||||
if (!nodeVal._deleted) {
|
|
||||||
nodeVal._splitAt(y, range)
|
|
||||||
nodeVal._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
const nodeLen = nodeVal._length
|
|
||||||
range -= nodeLen
|
|
||||||
clock += nodeLen
|
|
||||||
node = node.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete is not a real struct. It will not be saved in OS
|
|
||||||
*/
|
|
||||||
export default class Delete {
|
|
||||||
constructor () {
|
|
||||||
this._target = null
|
|
||||||
this._length = null
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
// TODO: set target, and add it to missing if not found
|
|
||||||
// There is an edge case in p2p networks!
|
|
||||||
const targetID = decoder.readID()
|
|
||||||
this._targetID = targetID
|
|
||||||
this._length = decoder.readVarUint()
|
|
||||||
if (y.os.getItem(targetID) === null) {
|
|
||||||
return [targetID]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
encoder.writeUint8(getReference(this.constructor))
|
|
||||||
encoder.writeID(this._targetID)
|
|
||||||
encoder.writeVarUint(this._length)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* - If created remotely (a remote user deleted something),
|
|
||||||
* this Delete is applied to all structs in id-range.
|
|
||||||
* - If created lokally (e.g. when y-array deletes a range of elements),
|
|
||||||
* this struct is broadcasted only (it is already executed)
|
|
||||||
*/
|
|
||||||
_integrate (y, locallyCreated = false) {
|
|
||||||
if (!locallyCreated) {
|
|
||||||
// from remote
|
|
||||||
const id = this._targetID
|
|
||||||
deleteItemRange(y, id.user, id.clock, this._length)
|
|
||||||
} else {
|
|
||||||
// from local
|
|
||||||
y.connector.broadcastStruct(this)
|
|
||||||
}
|
|
||||||
if (y.persistence !== null) {
|
|
||||||
y.persistence.saveOperations(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
import { getReference } from '../Util/structReferences.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import { RootFakeUserID } from '../Util/RootID.js'
|
|
||||||
import Delete from './Delete.js'
|
|
||||||
import { transactionTypeChanged } from '../Transaction.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper utility to split an Item (see _splitAt)
|
|
||||||
* - copy all properties from a to b
|
|
||||||
* - connect a to b
|
|
||||||
* - assigns the correct _id
|
|
||||||
* - save b to os
|
|
||||||
*/
|
|
||||||
export function splitHelper (y, a, b, diff) {
|
|
||||||
const aID = a._id
|
|
||||||
b._id = new ID(aID.user, aID.clock + diff)
|
|
||||||
b._origin = a
|
|
||||||
b._left = a
|
|
||||||
b._right = a._right
|
|
||||||
if (b._right !== null) {
|
|
||||||
b._right._left = b
|
|
||||||
}
|
|
||||||
b._right_origin = a._right_origin
|
|
||||||
// do not set a._right_origin, as this will lead to problems when syncing
|
|
||||||
a._right = b
|
|
||||||
b._parent = a._parent
|
|
||||||
b._parentSub = a._parentSub
|
|
||||||
b._deleted = a._deleted
|
|
||||||
// now search all relevant items to the right and update origin
|
|
||||||
// if origin is not it foundOrigins, we don't have to search any longer
|
|
||||||
let foundOrigins = new Set()
|
|
||||||
foundOrigins.add(a)
|
|
||||||
let o = b._right
|
|
||||||
while (o !== null && foundOrigins.has(o._origin)) {
|
|
||||||
if (o._origin === a) {
|
|
||||||
o._origin = b
|
|
||||||
}
|
|
||||||
foundOrigins.add(o)
|
|
||||||
o = o._right
|
|
||||||
}
|
|
||||||
y.os.put(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Item {
|
|
||||||
constructor () {
|
|
||||||
this._id = null
|
|
||||||
this._origin = null
|
|
||||||
this._left = null
|
|
||||||
this._right = null
|
|
||||||
this._right_origin = null
|
|
||||||
this._parent = null
|
|
||||||
this._parentSub = null
|
|
||||||
this._deleted = false
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Copy the effect of struct
|
|
||||||
*/
|
|
||||||
_copy () {
|
|
||||||
let struct = new this.constructor()
|
|
||||||
struct._origin = this._left
|
|
||||||
struct._left = this._left
|
|
||||||
struct._right = this
|
|
||||||
struct._right_origin = this
|
|
||||||
struct._parent = this._parent
|
|
||||||
struct._parentSub = this._parentSub
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _lastId () {
|
|
||||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Splits this struct so that another struct can be inserted in-between.
|
|
||||||
* This must be overwritten if _length > 1
|
|
||||||
* Returns right part after split
|
|
||||||
* - diff === 0 => this
|
|
||||||
* - diff === length => this._right
|
|
||||||
* - otherwise => split _content and return right part of split
|
|
||||||
* (see ItemJSON/ItemString for implementation)
|
|
||||||
*/
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
_delete (y, createDelete = true) {
|
|
||||||
this._deleted = true
|
|
||||||
y.ds.markDeleted(this._id, this._length)
|
|
||||||
if (createDelete) {
|
|
||||||
let del = new Delete()
|
|
||||||
del._targetID = this._id
|
|
||||||
del._length = this._length
|
|
||||||
del._integrate(y, true)
|
|
||||||
}
|
|
||||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
|
||||||
y._transaction.deletedStructs.add(this)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This is called right before this struct receives any children.
|
|
||||||
* It can be overwritten to apply pending changes before applying remote changes
|
|
||||||
*/
|
|
||||||
_beforeChange () {
|
|
||||||
// nop
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* - Integrate the struct so that other types/structs can see it
|
|
||||||
* - Add this struct to y.os
|
|
||||||
* - Check if this is struct deleted
|
|
||||||
*/
|
|
||||||
_integrate (y) {
|
|
||||||
const parent = this._parent
|
|
||||||
const selfID = this._id
|
|
||||||
const userState = selfID === null ? 0 : y.ss.getState(selfID.user)
|
|
||||||
if (selfID === null) {
|
|
||||||
this._id = y.ss.getNextID(this._length)
|
|
||||||
} else if (selfID.user === RootFakeUserID) {
|
|
||||||
// nop
|
|
||||||
} else if (selfID.clock < userState) {
|
|
||||||
// already applied..
|
|
||||||
return []
|
|
||||||
} else if (selfID.clock === userState) {
|
|
||||||
y.ss.setState(selfID.user, userState + this._length)
|
|
||||||
} else {
|
|
||||||
// missing content from user
|
|
||||||
throw new Error('Can not apply yet!')
|
|
||||||
}
|
|
||||||
if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
|
|
||||||
// this is the first time parent is updated
|
|
||||||
// or this types is new
|
|
||||||
this._parent._beforeChange()
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
# $this has to find a unique position between origin and the next known character
|
|
||||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
|
||||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
|
||||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
|
||||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
|
||||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
|
||||||
# therefore $this would be always to the right of o3
|
|
||||||
# case 2: $origin < $o.origin
|
|
||||||
# if current $this insert_position > $o origin: $this ins
|
|
||||||
# else $insert_position will not change
|
|
||||||
# (maybe we encounter case 1 later, then this will be to the right of $o)
|
|
||||||
# case 3: $origin > $o.origin
|
|
||||||
# $this insert_position is to the left of $o (forever!)
|
|
||||||
*/
|
|
||||||
// handle conflicts
|
|
||||||
let o
|
|
||||||
// set o to the first conflicting item
|
|
||||||
if (this._left !== null) {
|
|
||||||
o = this._left._right
|
|
||||||
} else if (this._parentSub !== null) {
|
|
||||||
o = this._parent._map.get(this._parentSub) || null
|
|
||||||
} else {
|
|
||||||
o = this._parent._start
|
|
||||||
}
|
|
||||||
let conflictingItems = new Set()
|
|
||||||
let itemsBeforeOrigin = new Set()
|
|
||||||
// Let c in conflictingItems, b in itemsBeforeOrigin
|
|
||||||
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
|
|
||||||
// Note that conflictingItems is a subset of itemsBeforeOrigin
|
|
||||||
while (o !== null && o !== this._right) {
|
|
||||||
itemsBeforeOrigin.add(o)
|
|
||||||
conflictingItems.add(o)
|
|
||||||
if (this._origin === o._origin) {
|
|
||||||
// case 1
|
|
||||||
if (o._id.user < this._id.user) {
|
|
||||||
this._left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else if (itemsBeforeOrigin.has(o._origin)) {
|
|
||||||
// case 2
|
|
||||||
if (!conflictingItems.has(o._origin)) {
|
|
||||||
this._left = o
|
|
||||||
conflictingItems.clear()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// TODO: try to use right_origin instead.
|
|
||||||
// Then you could basically omit conflictingItems!
|
|
||||||
// Note: you probably can't use right_origin in every case.. only when setting _left
|
|
||||||
o = o._right
|
|
||||||
}
|
|
||||||
// reconnect left/right + update parent map/start if necessary
|
|
||||||
const parentSub = this._parentSub
|
|
||||||
if (this._left === null) {
|
|
||||||
let right
|
|
||||||
if (parentSub !== null) {
|
|
||||||
const pmap = parent._map
|
|
||||||
right = pmap.get(parentSub) || null
|
|
||||||
pmap.set(parentSub, this)
|
|
||||||
} else {
|
|
||||||
right = parent._start
|
|
||||||
parent._start = this
|
|
||||||
}
|
|
||||||
this._right = right
|
|
||||||
if (right !== null) {
|
|
||||||
right._left = this
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const left = this._left
|
|
||||||
const right = left._right
|
|
||||||
this._right = right
|
|
||||||
left._right = this
|
|
||||||
if (right !== null) {
|
|
||||||
right._left = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parent._deleted) {
|
|
||||||
this._delete(y, false)
|
|
||||||
}
|
|
||||||
y.os.put(this)
|
|
||||||
transactionTypeChanged(y, parent, parentSub)
|
|
||||||
if (this._id.user !== RootFakeUserID) {
|
|
||||||
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
|
|
||||||
y.connector.broadcastStruct(this)
|
|
||||||
}
|
|
||||||
if (y.persistence !== null) {
|
|
||||||
y.persistence.saveOperations(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
encoder.writeUint8(getReference(this.constructor))
|
|
||||||
let info = 0
|
|
||||||
if (this._origin !== null) {
|
|
||||||
info += 0b1 // origin is defined
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
/* no longer send _left
|
|
||||||
if (this._left !== this._origin) {
|
|
||||||
info += 0b10 // do not copy origin to left
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (this._right_origin !== null) {
|
|
||||||
info += 0b100
|
|
||||||
}
|
|
||||||
if (this._parentSub !== null) {
|
|
||||||
info += 0b1000
|
|
||||||
}
|
|
||||||
encoder.writeUint8(info)
|
|
||||||
encoder.writeID(this._id)
|
|
||||||
if (info & 0b1) {
|
|
||||||
encoder.writeID(this._origin._lastId)
|
|
||||||
}
|
|
||||||
// TODO: remove
|
|
||||||
/* see above
|
|
||||||
if (info & 0b10) {
|
|
||||||
encoder.writeID(this._left._lastId)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (info & 0b100) {
|
|
||||||
encoder.writeID(this._right_origin._id)
|
|
||||||
}
|
|
||||||
if ((info & 0b101) === 0) {
|
|
||||||
// neither origin nor right is defined
|
|
||||||
encoder.writeID(this._parent._id)
|
|
||||||
}
|
|
||||||
if (info & 0b1000) {
|
|
||||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = []
|
|
||||||
const info = decoder.readUint8()
|
|
||||||
const id = decoder.readID()
|
|
||||||
this._id = id
|
|
||||||
// read origin
|
|
||||||
if (info & 0b1) {
|
|
||||||
// origin != null
|
|
||||||
const originID = decoder.readID()
|
|
||||||
// we have to query for left again because it might have been split/merged..
|
|
||||||
const origin = y.os.getItemCleanEnd(originID)
|
|
||||||
if (origin === null) {
|
|
||||||
missing.push(originID)
|
|
||||||
} else {
|
|
||||||
this._origin = origin
|
|
||||||
this._left = this._origin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// read right
|
|
||||||
if (info & 0b100) {
|
|
||||||
// right != null
|
|
||||||
const rightID = decoder.readID()
|
|
||||||
// we have to query for right again because it might have been split/merged..
|
|
||||||
const right = y.os.getItemCleanStart(rightID)
|
|
||||||
if (right === null) {
|
|
||||||
missing.push(rightID)
|
|
||||||
} else {
|
|
||||||
this._right = right
|
|
||||||
this._right_origin = right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// read parent
|
|
||||||
if ((info & 0b101) === 0) {
|
|
||||||
// neither origin nor right is defined
|
|
||||||
const parentID = decoder.readID()
|
|
||||||
// parent does not change, so we don't have to search for it again
|
|
||||||
if (this._parent === null) {
|
|
||||||
const parent = y.os.get(parentID)
|
|
||||||
if (parent === null) {
|
|
||||||
missing.push(parentID)
|
|
||||||
} else {
|
|
||||||
this._parent = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this._parent === null) {
|
|
||||||
if (this._origin !== null) {
|
|
||||||
this._parent = this._origin._parent
|
|
||||||
} else if (this._right_origin !== null) {
|
|
||||||
this._parent = this._right_origin._parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (info & 0b1000) {
|
|
||||||
// TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
|
|
||||||
this._parentSub = JSON.parse(decoder.readVarString())
|
|
||||||
}
|
|
||||||
if (y.ss.getState(id.user) < id.clock) {
|
|
||||||
missing.push(new ID(id.user, id.clock - 1))
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { splitHelper, default as Item } from './Item.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class ItemJSON extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return this._content.length
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = super._fromBinary(y, decoder)
|
|
||||||
let len = decoder.readVarUint()
|
|
||||||
this._content = new Array(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
const ctnt = decoder.readVarString()
|
|
||||||
let parsed
|
|
||||||
if (ctnt === 'undefined') {
|
|
||||||
parsed = undefined
|
|
||||||
} else {
|
|
||||||
parsed = JSON.parse(ctnt)
|
|
||||||
}
|
|
||||||
this._content[i] = parsed
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
super._toBinary(encoder)
|
|
||||||
let len = this._content.length
|
|
||||||
encoder.writeVarUint(len)
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
let encoded
|
|
||||||
let content = this._content[i]
|
|
||||||
if (content === undefined) {
|
|
||||||
encoded = 'undefined'
|
|
||||||
} else {
|
|
||||||
encoded = JSON.stringify(content)
|
|
||||||
}
|
|
||||||
encoder.writeVarString(encoded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
} else if (diff >= this._length) {
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
let item = new ItemJSON()
|
|
||||||
item._content = this._content.splice(diff)
|
|
||||||
splitHelper(y, this, item, diff)
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { splitHelper, default as Item } from './Item.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
|
|
||||||
export default class ItemString extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._content = null
|
|
||||||
}
|
|
||||||
_copy () {
|
|
||||||
let struct = super._copy()
|
|
||||||
struct._content = this._content
|
|
||||||
return struct
|
|
||||||
}
|
|
||||||
get _length () {
|
|
||||||
return this._content.length
|
|
||||||
}
|
|
||||||
_fromBinary (y, decoder) {
|
|
||||||
let missing = super._fromBinary(y, decoder)
|
|
||||||
this._content = decoder.readVarString()
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
_toBinary (encoder) {
|
|
||||||
super._toBinary(encoder)
|
|
||||||
encoder.writeVarString(this._content)
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
_splitAt (y, diff) {
|
|
||||||
if (diff === 0) {
|
|
||||||
return this
|
|
||||||
} else if (diff >= this._length) {
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
let item = new ItemString()
|
|
||||||
item._content = this._content.slice(diff)
|
|
||||||
this._content = this._content.slice(0, diff)
|
|
||||||
splitHelper(y, this, item, diff)
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import Item from './Item.js'
|
|
||||||
import EventHandler from '../Util/EventHandler.js'
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
|
|
||||||
// restructure children as if they were inserted one after another
|
|
||||||
function integrateChildren (y, start) {
|
|
||||||
let right
|
|
||||||
do {
|
|
||||||
right = start._right
|
|
||||||
start._right = null
|
|
||||||
start._right_origin = null
|
|
||||||
start._origin = start._left
|
|
||||||
start._integrate(y)
|
|
||||||
start = right
|
|
||||||
} while (right !== null)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getListItemIDByPosition (type, i) {
|
|
||||||
let pos = 0
|
|
||||||
let n = type._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (pos <= i && i < pos + n._length) {
|
|
||||||
const id = n._id
|
|
||||||
return new ID(id.user, id.clock + i - pos)
|
|
||||||
}
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Type extends Item {
|
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._map = new Map()
|
|
||||||
this._start = null
|
|
||||||
this._y = null
|
|
||||||
this._eventHandler = new EventHandler()
|
|
||||||
this._deepEventHandler = new EventHandler()
|
|
||||||
}
|
|
||||||
getPathTo (type) {
|
|
||||||
if (type === this) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const path = []
|
|
||||||
const y = this._y
|
|
||||||
while (type._parent !== this && this._parent !== y) {
|
|
||||||
let parent = type._parent
|
|
||||||
if (type._parentSub !== null) {
|
|
||||||
path.push(type._parentSub)
|
|
||||||
} else {
|
|
||||||
// parent is array-ish
|
|
||||||
for (let [i, child] of parent) {
|
|
||||||
if (child === type) {
|
|
||||||
path.push(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type = parent
|
|
||||||
}
|
|
||||||
if (this._parent !== this) {
|
|
||||||
throw new Error('The type is not a child of this node')
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
_callEventHandler (transaction, event) {
|
|
||||||
const changedParentTypes = transaction.changedParentTypes
|
|
||||||
this._eventHandler.callEventListeners(transaction, event)
|
|
||||||
let type = this
|
|
||||||
while (type !== this._y) {
|
|
||||||
let events = changedParentTypes.get(type)
|
|
||||||
if (events === undefined) {
|
|
||||||
events = []
|
|
||||||
changedParentTypes.set(type, events)
|
|
||||||
}
|
|
||||||
events.push(event)
|
|
||||||
type = type._parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_copy (undeleteChildren) {
|
|
||||||
let copy = super._copy()
|
|
||||||
let map = new Map()
|
|
||||||
copy._map = map
|
|
||||||
for (let [key, value] of this._map) {
|
|
||||||
if (undeleteChildren.has(value) || !value.deleted) {
|
|
||||||
let _item = value._copy(undeleteChildren)
|
|
||||||
_item._parent = copy
|
|
||||||
map.set(key, value._copy(undeleteChildren))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let prevUndeleted = null
|
|
||||||
copy._start = null
|
|
||||||
let item = this._start
|
|
||||||
while (item !== null) {
|
|
||||||
if (undeleteChildren.has(item) || !item.deleted) {
|
|
||||||
let _item = item._copy(undeleteChildren)
|
|
||||||
_item._left = prevUndeleted
|
|
||||||
_item._origin = prevUndeleted
|
|
||||||
_item._right = null
|
|
||||||
_item._right_origin = null
|
|
||||||
_item._parent = copy
|
|
||||||
if (prevUndeleted === null) {
|
|
||||||
copy._start = _item
|
|
||||||
} else {
|
|
||||||
prevUndeleted._right = _item
|
|
||||||
}
|
|
||||||
prevUndeleted = _item
|
|
||||||
}
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
return copy
|
|
||||||
}
|
|
||||||
_transact (f) {
|
|
||||||
const y = this._y
|
|
||||||
if (y !== null) {
|
|
||||||
y.transact(f)
|
|
||||||
} else {
|
|
||||||
f(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observe (f) {
|
|
||||||
this._eventHandler.addEventListener(f)
|
|
||||||
}
|
|
||||||
observeDeep (f) {
|
|
||||||
this._deepEventHandler.addEventListener(f)
|
|
||||||
}
|
|
||||||
unobserve (f) {
|
|
||||||
this._eventHandler.removeEventListener(f)
|
|
||||||
}
|
|
||||||
unobserveDeep (f) {
|
|
||||||
this._deepEventHandler.removeEventListener(f)
|
|
||||||
}
|
|
||||||
_integrate (y) {
|
|
||||||
y._transaction.newTypes.add(this)
|
|
||||||
super._integrate(y)
|
|
||||||
this._y = y
|
|
||||||
// when integrating children we must make sure to
|
|
||||||
// integrate start
|
|
||||||
const start = this._start
|
|
||||||
if (start !== null) {
|
|
||||||
this._start = null
|
|
||||||
integrateChildren(y, start)
|
|
||||||
}
|
|
||||||
// integrate map children
|
|
||||||
const map = this._map
|
|
||||||
this._map = new Map()
|
|
||||||
for (let t of map.values()) {
|
|
||||||
// TODO make sure that right elements are deleted!
|
|
||||||
integrateChildren(y, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_delete (y, createDelete) {
|
|
||||||
super._delete(y, createDelete)
|
|
||||||
y._transaction.changedTypes.delete(this)
|
|
||||||
// delete map types
|
|
||||||
for (let value of this._map.values()) {
|
|
||||||
if (value instanceof Item && !value._deleted) {
|
|
||||||
value._delete(y, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// delete array types
|
|
||||||
let t = this._start
|
|
||||||
while (t !== null) {
|
|
||||||
if (!t._deleted) {
|
|
||||||
t._delete(y, false)
|
|
||||||
}
|
|
||||||
t = t._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
|
|
||||||
export default class Transaction {
|
|
||||||
constructor (y) {
|
|
||||||
this.y = y
|
|
||||||
// types added during transaction
|
|
||||||
this.newTypes = new Set()
|
|
||||||
// changed types (does not include new types)
|
|
||||||
// maps from type to parentSubs (item._parentSub = null for array elements)
|
|
||||||
this.changedTypes = new Map()
|
|
||||||
this.deletedStructs = new Set()
|
|
||||||
this.beforeState = new Map()
|
|
||||||
this.changedParentTypes = new Map()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transactionTypeChanged (y, type, sub) {
|
|
||||||
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
|
|
||||||
const changedTypes = y._transaction.changedTypes
|
|
||||||
let subs = changedTypes.get(type)
|
|
||||||
if (subs === undefined) {
|
|
||||||
// create if it doesn't exist yet
|
|
||||||
subs = new Set()
|
|
||||||
changedTypes.set(type, subs)
|
|
||||||
}
|
|
||||||
subs.add(sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
import Type from '../Struct/Type.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
import YEvent from '../Util/YEvent.js'
|
|
||||||
|
|
||||||
class YArrayEvent extends YEvent {
|
|
||||||
constructor (yarray, remote) {
|
|
||||||
super(yarray)
|
|
||||||
this.remote = remote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class YArray extends Type {
|
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, new YArrayEvent(this, remote))
|
|
||||||
}
|
|
||||||
get (pos) {
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (pos < n._length) {
|
|
||||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
|
||||||
return n._content[pos]
|
|
||||||
} else {
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pos -= n._length
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toArray () {
|
|
||||||
return this.map(c => c)
|
|
||||||
}
|
|
||||||
toJSON () {
|
|
||||||
return this.map(c => {
|
|
||||||
if (c instanceof Type) {
|
|
||||||
if (c.toJSON !== null) {
|
|
||||||
return c.toJSON()
|
|
||||||
} else {
|
|
||||||
return c.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
})
|
|
||||||
}
|
|
||||||
map (f) {
|
|
||||||
const res = []
|
|
||||||
this.forEach((c, i) => {
|
|
||||||
res.push(f(c, i, this))
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
forEach (f) {
|
|
||||||
let pos = 0
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
if (n instanceof Type) {
|
|
||||||
f(n, pos++, this)
|
|
||||||
} else {
|
|
||||||
const content = n._content
|
|
||||||
const contentLen = content.length
|
|
||||||
for (let i = 0; i < contentLen; i++) {
|
|
||||||
pos++
|
|
||||||
f(content[i], pos, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get length () {
|
|
||||||
let length = 0
|
|
||||||
let n = this._start
|
|
||||||
while (n !== null) {
|
|
||||||
if (!n._deleted) {
|
|
||||||
length += n._length
|
|
||||||
}
|
|
||||||
n = n._right
|
|
||||||
}
|
|
||||||
return length
|
|
||||||
}
|
|
||||||
[Symbol.iterator] () {
|
|
||||||
return {
|
|
||||||
next: function () {
|
|
||||||
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
|
||||||
// item is deleted or itemElement does not exist (is deleted)
|
|
||||||
this._item = this._item._right
|
|
||||||
this._itemElement = 0
|
|
||||||
}
|
|
||||||
if (this._item === null) {
|
|
||||||
return {
|
|
||||||
done: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let content
|
|
||||||
if (this._item instanceof Type) {
|
|
||||||
content = this._item
|
|
||||||
} else {
|
|
||||||
content = this._item._content[this._itemElement++]
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: [this._count, content],
|
|
||||||
done: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_item: this._start,
|
|
||||||
_itemElement: 0,
|
|
||||||
_count: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete (pos, length = 1) {
|
|
||||||
this._y.transact(() => {
|
|
||||||
let item = this._start
|
|
||||||
let count = 0
|
|
||||||
while (item !== null && length > 0) {
|
|
||||||
if (!item._deleted) {
|
|
||||||
if (count <= pos && pos < count + item._length) {
|
|
||||||
const diffDel = pos - count
|
|
||||||
item = item._splitAt(this._y, diffDel)
|
|
||||||
item._splitAt(this._y, length)
|
|
||||||
length -= item._length
|
|
||||||
item._delete(this._y)
|
|
||||||
count += diffDel
|
|
||||||
} else {
|
|
||||||
count += item._length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item = item._right
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (length > 0) {
|
|
||||||
throw new Error('Delete exceeds the range of the YArray')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
insertAfter (left, content) {
|
|
||||||
this._transact(y => {
|
|
||||||
let right
|
|
||||||
if (left === null) {
|
|
||||||
right = this._start
|
|
||||||
} else {
|
|
||||||
right = left._right
|
|
||||||
}
|
|
||||||
let prevJsonIns = null
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
let c = content[i]
|
|
||||||
if (typeof c === 'function') {
|
|
||||||
c = new c() // eslint-disable-line new-cap
|
|
||||||
}
|
|
||||||
if (c instanceof Type) {
|
|
||||||
if (prevJsonIns !== null) {
|
|
||||||
if (y !== null) {
|
|
||||||
prevJsonIns._integrate(y)
|
|
||||||
}
|
|
||||||
left = prevJsonIns
|
|
||||||
prevJsonIns = null
|
|
||||||
}
|
|
||||||
c._origin = left
|
|
||||||
c._left = left
|
|
||||||
c._right = right
|
|
||||||
c._right_origin = right
|
|
||||||
c._parent = this
|
|
||||||
if (y !== null) {
|
|
||||||
c._integrate(y)
|
|
||||||
} else if (left === null) {
|
|
||||||
this._start = c
|
|
||||||
} else {
|
|
||||||
left._right = c
|
|
||||||
}
|
|
||||||
left = c
|
|
||||||
} else {
|
|
||||||
if (prevJsonIns === null) {
|
|
||||||
prevJsonIns = new ItemJSON()
|
|
||||||
prevJsonIns._origin = left
|
|
||||||
prevJsonIns._left = left
|
|
||||||
prevJsonIns._right = right
|
|
||||||
prevJsonIns._right_origin = right
|
|
||||||
prevJsonIns._parent = this
|
|
||||||
prevJsonIns._content = []
|
|
||||||
}
|
|
||||||
prevJsonIns._content.push(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (prevJsonIns !== null && y !== null) {
|
|
||||||
prevJsonIns._integrate(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
insert (pos, content) {
|
|
||||||
let left = null
|
|
||||||
let right = this._start
|
|
||||||
let count = 0
|
|
||||||
const y = this._y
|
|
||||||
while (right !== null) {
|
|
||||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
|
||||||
if (count <= pos && pos <= count + rightLen) {
|
|
||||||
const splitDiff = pos - count
|
|
||||||
right = right._splitAt(y, splitDiff)
|
|
||||||
left = right._left
|
|
||||||
count += splitDiff
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (!right._deleted) {
|
|
||||||
count += right._length
|
|
||||||
}
|
|
||||||
left = right
|
|
||||||
right = right._right
|
|
||||||
}
|
|
||||||
if (pos > count) {
|
|
||||||
throw new Error('Position exceeds array range!')
|
|
||||||
}
|
|
||||||
this.insertAfter(left, content)
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `YArray(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
src/Type/YMap.js
114
src/Type/YMap.js
@@ -1,114 +0,0 @@
|
|||||||
import Type from '../Struct/Type.js'
|
|
||||||
import Item from '../Struct/Item.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import { logID } from '../MessageHandler/messageToString.js'
|
|
||||||
import YEvent from '../Util/YEvent.js'
|
|
||||||
|
|
||||||
class YMapEvent extends YEvent {
|
|
||||||
constructor (ymap, subs, remote) {
|
|
||||||
super(ymap)
|
|
||||||
this.keysChanged = subs
|
|
||||||
this.remote = remote
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class YMap extends Type {
|
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, 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,129 +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, _document) {
|
|
||||||
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, _document)
|
|
||||||
} 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, _document) {
|
|
||||||
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 {
|
|
||||||
// tag is already set in constructor
|
|
||||||
// set attributes
|
|
||||||
let attributes = new Map()
|
|
||||||
for (let i = 0; i < dom.attributes.length; i++) {
|
|
||||||
let attr = dom.attributes[i]
|
|
||||||
attributes.set(attr.name, attr.value)
|
|
||||||
}
|
|
||||||
attributes = this._domFilter(dom, attributes)
|
|
||||||
attributes.forEach((value, name) => {
|
|
||||||
this.setAttribute(name, value)
|
|
||||||
})
|
|
||||||
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes), _document)
|
|
||||||
this._bindToDom(dom, _document)
|
|
||||||
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) {
|
|
||||||
if (!value._deleted) {
|
|
||||||
obj[key] = value._content[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
getDom (_document) {
|
|
||||||
_document = _document || document
|
|
||||||
let dom = this._dom
|
|
||||||
if (dom == null) {
|
|
||||||
dom = _document.createElement(this.nodeName)
|
|
||||||
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, _document)
|
|
||||||
}
|
|
||||||
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,335 +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, _document) {
|
|
||||||
const types = []
|
|
||||||
doms.forEach(d => {
|
|
||||||
if (d._yxml != null && d._yxml !== false) {
|
|
||||||
d._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (parent._domFilter(d.nodeName, new Map()) !== 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, _document)
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
if (this._domObserver !== null) {
|
|
||||||
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 (transaction, parentSubs, remote) {
|
|
||||||
this._callEventHandler(transaction, 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, _document) {
|
|
||||||
const types = domToYXml(this, doms, _document)
|
|
||||||
this.insertAfter(prev, types)
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
insertDomElements (pos, doms, _document) {
|
|
||||||
const types = domToYXml(this, doms, _document)
|
|
||||||
this.insert(pos, types)
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
getDom () {
|
|
||||||
return this._dom
|
|
||||||
}
|
|
||||||
bindToDom (dom, _document) {
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (dom._yxml != null) {
|
|
||||||
dom._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
dom.innerHTML = ''
|
|
||||||
this.forEach(t => {
|
|
||||||
dom.insertBefore(t.getDom(_document), null)
|
|
||||||
})
|
|
||||||
this._bindToDom(dom, _document)
|
|
||||||
}
|
|
||||||
// binds to a dom element
|
|
||||||
// Only call if dom and YXml are isomorph
|
|
||||||
_bindToDom (dom, _document) {
|
|
||||||
_document = _document || document
|
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
// TODO: refine this..
|
|
||||||
if ((this.constructor !== YXmlFragment && this._parent !== this._y) || this._parent === null) {
|
|
||||||
// TODO: only top level YXmlFragment can bind. Also allow YXmlElements..
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
|
|
||||||
this._y.on('afterTransaction', afterTransactionSelectionFixer)
|
|
||||||
const applyFilter = (type) => {
|
|
||||||
if (type._deleted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if type is a child of this
|
|
||||||
let isChild = false
|
|
||||||
let p = type
|
|
||||||
while (p !== this._y) {
|
|
||||||
if (p === this) {
|
|
||||||
isChild = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p = p._parent
|
|
||||||
}
|
|
||||||
if (!isChild) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// filter attributes
|
|
||||||
let attributes = new Map()
|
|
||||||
if (type.getAttributes !== undefined) {
|
|
||||||
let attrs = type.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let result = this._domFilter(type.nodeName, new Map(attributes))
|
|
||||||
if (result === null) {
|
|
||||||
type._delete(this._y)
|
|
||||||
} else {
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
if (!result.has(key)) {
|
|
||||||
type.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._y.on('beforeObserverCalls', function (y, transaction) {
|
|
||||||
// apply dom filter to new and changed types
|
|
||||||
transaction.changedTypes.forEach(function (subs, type) {
|
|
||||||
if (subs.size > 1 || !subs.has(null)) {
|
|
||||||
// only apply changes on attributes
|
|
||||||
applyFilter(type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
transaction.newTypes.forEach(applyFilter)
|
|
||||||
})
|
|
||||||
// Apply Y.Xml events to dom
|
|
||||||
this.observeDeep(events => {
|
|
||||||
reflectChangesOnDom.call(this, events, _document)
|
|
||||||
})
|
|
||||||
// Apply Dom changes on Y.Xml
|
|
||||||
if (typeof MutationObserver !== 'undefined') {
|
|
||||||
this._y.on('beforeTransaction', () => {
|
|
||||||
this._domObserverListener(this._domObserver.takeRecords())
|
|
||||||
})
|
|
||||||
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':
|
|
||||||
if (yxml.constructor === YXmlFragment) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
let name = mutation.attributeName
|
|
||||||
let val = dom.getAttribute(name)
|
|
||||||
// check if filter accepts attribute
|
|
||||||
let attributes = new Map()
|
|
||||||
attributes.set(name, val)
|
|
||||||
if (this._domFilter(dom.nodeName, attributes).size > 0 && yxml.constructor !== YXmlFragment) {
|
|
||||||
if (yxml.getAttribute(name) !== val) {
|
|
||||||
if (val == null) {
|
|
||||||
yxml.removeAttribute(name)
|
|
||||||
} else {
|
|
||||||
yxml.setAttribute(name, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case 'childList':
|
|
||||||
diffChildren.add(mutation.target)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (let dom of diffChildren) {
|
|
||||||
if (dom._yxml != null && dom._yxml !== false) {
|
|
||||||
applyChangesFromDom(dom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
|
||||||
this._domObserver.observe(dom, {
|
|
||||||
childList: true,
|
|
||||||
attributes: true,
|
|
||||||
characterData: true,
|
|
||||||
subtree: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
_logString () {
|
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
|
||||||
return `YXml(id:${logID(this._id)},left:${logID(left)},origin:${logID(origin)},right:${this._right},parent:${logID(this._parent)},parentSub:${this._parentSub})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import YText from '../YText.js'
|
|
||||||
|
|
||||||
export default class YXmlText extends YText {
|
|
||||||
constructor (arg1) {
|
|
||||||
let dom = null
|
|
||||||
let initialText = null
|
|
||||||
if (arg1 != null) {
|
|
||||||
if (arg1.nodeType != null && arg1.nodeType === arg1.TEXT_NODE) {
|
|
||||||
dom = arg1
|
|
||||||
initialText = dom.nodeValue
|
|
||||||
} else if (typeof arg1 === 'string') {
|
|
||||||
initialText = arg1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super(initialText)
|
|
||||||
this._dom = null
|
|
||||||
this._domObserver = null
|
|
||||||
this._domObserverListener = null
|
|
||||||
this._scrollElement = null
|
|
||||||
if (dom !== null) {
|
|
||||||
this._setDom(arg1)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var token = true
|
|
||||||
this._mutualExclude = f => {
|
|
||||||
if (token) {
|
|
||||||
token = false
|
|
||||||
try {
|
|
||||||
f()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
this._domObserver.takeRecords()
|
|
||||||
token = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.observe(event => {
|
|
||||||
if (this._dom != null) {
|
|
||||||
const dom = this._dom
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
let anchorViewPosition = getAnchorViewPosition(this._scrollElement)
|
|
||||||
let anchorViewFix
|
|
||||||
if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) {
|
|
||||||
anchorViewFix = anchorViewPosition
|
|
||||||
} else {
|
|
||||||
anchorViewFix = null
|
|
||||||
}
|
|
||||||
dom.nodeValue = this.toString()
|
|
||||||
fixScrollPosition(this._scrollElement, anchorViewFix)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
setDomFilter () {}
|
|
||||||
enableSmartScrolling (scrollElement) {
|
|
||||||
this._scrollElement = scrollElement
|
|
||||||
}
|
|
||||||
_setDom (dom) {
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (dom._yxml != null) {
|
|
||||||
dom._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
// set marker
|
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
}
|
|
||||||
getDom (_document) {
|
|
||||||
_document = _document || document
|
|
||||||
if (this._dom === null) {
|
|
||||||
const dom = _document.createTextNode(this.toString())
|
|
||||||
this._setDom(dom)
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
return this._dom
|
|
||||||
}
|
|
||||||
_delete (y, createDelete) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
super._delete(y, createDelete)
|
|
||||||
}
|
|
||||||
_unbindFromDom () {
|
|
||||||
if (this._domObserver != null) {
|
|
||||||
this._domObserver.disconnect()
|
|
||||||
this._domObserver = null
|
|
||||||
}
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._dom._yxml = null
|
|
||||||
this._dom = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +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, transaction, 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, transaction, 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) {
|
|
||||||
let node = sel.type.getDom()
|
|
||||||
let offset = sel.offset
|
|
||||||
if (node !== anchorNode || offset !== anchorOffset) {
|
|
||||||
anchorNode = node
|
|
||||||
anchorOffset = offset
|
|
||||||
shouldUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (to !== null) {
|
|
||||||
let sel = fromRelativePosition(toY, to)
|
|
||||||
if (sel !== null) {
|
|
||||||
let node = sel.type.getDom()
|
|
||||||
let offset = sel.offset
|
|
||||||
if (node !== focusNode || offset !== focusOffset) {
|
|
||||||
focusNode = node
|
|
||||||
focusOffset = offset
|
|
||||||
shouldUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldUpdate) {
|
|
||||||
browserSelection.setBaseAndExtent(
|
|
||||||
anchorNode,
|
|
||||||
anchorOffset,
|
|
||||||
focusNode,
|
|
||||||
focusOffset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// delete, so the objects can be gc'd
|
|
||||||
relativeSelection = null
|
|
||||||
browserSelection = null
|
|
||||||
}
|
|
||||||
@@ -1,252 +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, _document) {
|
|
||||||
// Make sure that no filtered attributes are applied to the structure
|
|
||||||
// if they were, delete them
|
|
||||||
/*
|
|
||||||
events.forEach(event => {
|
|
||||||
const target = event.target
|
|
||||||
if (event.attributesChanged === undefined) {
|
|
||||||
// event.target is Y.XmlText
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const keys = this._domFilter(target.nodeName, Array.from(event.attributesChanged))
|
|
||||||
if (keys === null) {
|
|
||||||
target._delete()
|
|
||||||
} else {
|
|
||||||
const removeKeys = new Set() // is a copy of event.attributesChanged
|
|
||||||
event.attributesChanged.forEach(key => { removeKeys.add(key) })
|
|
||||||
keys.forEach(key => {
|
|
||||||
// remove all accepted keys from removeKeys
|
|
||||||
removeKeys.delete(key)
|
|
||||||
})
|
|
||||||
// remove the filtered attribute
|
|
||||||
removeKeys.forEach(key => {
|
|
||||||
target.removeAttribute(key)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
events.forEach(event => {
|
|
||||||
const yxml = event.target
|
|
||||||
const dom = yxml._dom
|
|
||||||
if (dom != null) {
|
|
||||||
// TODO: do this once before applying stuff
|
|
||||||
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
|
||||||
if (yxml.constructor === YXmlText) {
|
|
||||||
yxml._dom.nodeValue = yxml.toString()
|
|
||||||
} else {
|
|
||||||
// update attributes
|
|
||||||
event.attributesChanged.forEach(attributeName => {
|
|
||||||
const value = yxml.getAttribute(attributeName)
|
|
||||||
if (value === undefined) {
|
|
||||||
dom.removeAttribute(attributeName)
|
|
||||||
} else {
|
|
||||||
dom.setAttribute(attributeName, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (event.childListChanged) {
|
|
||||||
// create fragment of undeleted nodes
|
|
||||||
const fragment = _document.createDocumentFragment()
|
|
||||||
yxml.forEach(function (t) {
|
|
||||||
fragment.appendChild(t.getDom(_document))
|
|
||||||
})
|
|
||||||
// remove remainding nodes
|
|
||||||
let lastChild = dom.lastChild
|
|
||||||
while (lastChild !== null) {
|
|
||||||
dom.removeChild(lastChild)
|
|
||||||
lastChild = dom.lastChild
|
|
||||||
}
|
|
||||||
// insert fragment of undeleted nodes
|
|
||||||
dom.appendChild(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 (transaction, event) {
|
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
|
||||||
try {
|
|
||||||
const f = this.eventListeners[i]
|
|
||||||
f(event)
|
|
||||||
} catch (e) {
|
|
||||||
/*
|
|
||||||
Your observer threw an error. This error was caught so that Yjs
|
|
||||||
can ensure data consistency! In order to debug this error you
|
|
||||||
have to check "Pause On Caught Exceptions" in developer tools.
|
|
||||||
*/
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
export default class ID {
|
|
||||||
constructor (user, clock) {
|
|
||||||
this.user = user
|
|
||||||
this.clock = clock
|
|
||||||
}
|
|
||||||
clone () {
|
|
||||||
return new ID(this.user, this.clock)
|
|
||||||
}
|
|
||||||
equals (id) {
|
|
||||||
return id !== null && id.user === this.user && id.clock === this.clock
|
|
||||||
}
|
|
||||||
lessThan (id) {
|
|
||||||
return this.user < id.user || (this.user === id.user && this.clock < id.clock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
export default class NamedEventHandler {
|
|
||||||
constructor () {
|
|
||||||
this._eventListener = new Map()
|
|
||||||
}
|
|
||||||
_getListener (name) {
|
|
||||||
let listeners = this._eventListener.get(name)
|
|
||||||
if (listeners === undefined) {
|
|
||||||
listeners = {
|
|
||||||
once: new Set(),
|
|
||||||
on: new Set()
|
|
||||||
}
|
|
||||||
this._eventListener.set(name, listeners)
|
|
||||||
}
|
|
||||||
return listeners
|
|
||||||
}
|
|
||||||
once (name, f) {
|
|
||||||
let listeners = this._getListener(name)
|
|
||||||
listeners.once.add(f)
|
|
||||||
}
|
|
||||||
on (name, f) {
|
|
||||||
let listeners = this._getListener(name)
|
|
||||||
listeners.on.add(f)
|
|
||||||
}
|
|
||||||
off (name, f) {
|
|
||||||
if (name == null || f == null) {
|
|
||||||
throw new Error('You must specify event name and function!')
|
|
||||||
}
|
|
||||||
const listener = this._eventListener.get(name)
|
|
||||||
if (listener !== undefined) {
|
|
||||||
listener.remove(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit (name, ...args) {
|
|
||||||
const listener = this._eventListener.get(name)
|
|
||||||
if (listener !== undefined) {
|
|
||||||
listener.on.forEach(f => f.apply(null, args))
|
|
||||||
listener.once.forEach(f => f.apply(null, args))
|
|
||||||
listener.once = new Set()
|
|
||||||
} else if (name === 'error') {
|
|
||||||
console.error(args[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this._eventListener = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { getReference } from './structReferences.js'
|
|
||||||
|
|
||||||
export const RootFakeUserID = 0xFFFFFF
|
|
||||||
|
|
||||||
export default class RootID {
|
|
||||||
constructor (name, typeConstructor) {
|
|
||||||
this.user = RootFakeUserID
|
|
||||||
this.name = name
|
|
||||||
this.type = getReference(typeConstructor)
|
|
||||||
}
|
|
||||||
equals (id) {
|
|
||||||
return id !== null && id.user === this.user && id.name === this.name && id.type === this.type
|
|
||||||
}
|
|
||||||
lessThan (id) {
|
|
||||||
return this.user < id.user || (this.user === id.user && (this.name < id.name || (this.name === id.name && this.type < id.type)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
471
src/Util/Tree.js
471
src/Util/Tree.js
@@ -1,471 +0,0 @@
|
|||||||
|
|
||||||
class N {
|
|
||||||
// A created node is always red!
|
|
||||||
constructor (val) {
|
|
||||||
this.val = val
|
|
||||||
this.color = true
|
|
||||||
this._left = null
|
|
||||||
this._right = null
|
|
||||||
this._parent = null
|
|
||||||
}
|
|
||||||
isRed () { return this.color }
|
|
||||||
isBlack () { return !this.color }
|
|
||||||
redden () { this.color = true; return this }
|
|
||||||
blacken () { this.color = false; return this }
|
|
||||||
get grandparent () {
|
|
||||||
return this.parent.parent
|
|
||||||
}
|
|
||||||
get parent () {
|
|
||||||
return this._parent
|
|
||||||
}
|
|
||||||
get sibling () {
|
|
||||||
return (this === this.parent.left)
|
|
||||||
? this.parent.right : this.parent.left
|
|
||||||
}
|
|
||||||
get left () {
|
|
||||||
return this._left
|
|
||||||
}
|
|
||||||
get right () {
|
|
||||||
return this._right
|
|
||||||
}
|
|
||||||
set left (n) {
|
|
||||||
if (n !== null) {
|
|
||||||
n._parent = this
|
|
||||||
}
|
|
||||||
this._left = n
|
|
||||||
}
|
|
||||||
set right (n) {
|
|
||||||
if (n !== null) {
|
|
||||||
n._parent = this
|
|
||||||
}
|
|
||||||
this._right = n
|
|
||||||
}
|
|
||||||
rotateLeft (tree) {
|
|
||||||
var parent = this.parent
|
|
||||||
var newParent = this.right
|
|
||||||
var newRight = this.right.left
|
|
||||||
newParent.left = this
|
|
||||||
this.right = newRight
|
|
||||||
if (parent === null) {
|
|
||||||
tree.root = newParent
|
|
||||||
newParent._parent = null
|
|
||||||
} else if (parent.left === this) {
|
|
||||||
parent.left = newParent
|
|
||||||
} else if (parent.right === this) {
|
|
||||||
parent.right = newParent
|
|
||||||
} else {
|
|
||||||
throw new Error('The elements are wrongly connected!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next () {
|
|
||||||
if (this.right !== null) {
|
|
||||||
// search the most left node in the right tree
|
|
||||||
var o = this.right
|
|
||||||
while (o.left !== null) {
|
|
||||||
o = o.left
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
} else {
|
|
||||||
var p = this
|
|
||||||
while (p.parent !== null && p !== p.parent.left) {
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return p.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev () {
|
|
||||||
if (this.left !== null) {
|
|
||||||
// search the most right node in the left tree
|
|
||||||
var o = this.left
|
|
||||||
while (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
} else {
|
|
||||||
var p = this
|
|
||||||
while (p.parent !== null && p !== p.parent.right) {
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return p.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rotateRight (tree) {
|
|
||||||
var parent = this.parent
|
|
||||||
var newParent = this.left
|
|
||||||
var newLeft = this.left.right
|
|
||||||
newParent.right = this
|
|
||||||
this.left = newLeft
|
|
||||||
if (parent === null) {
|
|
||||||
tree.root = newParent
|
|
||||||
newParent._parent = null
|
|
||||||
} else if (parent.left === this) {
|
|
||||||
parent.left = newParent
|
|
||||||
} else if (parent.right === this) {
|
|
||||||
parent.right = newParent
|
|
||||||
} else {
|
|
||||||
throw new Error('The elements are wrongly connected!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getUncle () {
|
|
||||||
// we can assume that grandparent exists when this is called!
|
|
||||||
if (this.parent === this.parent.parent.left) {
|
|
||||||
return this.parent.parent.right
|
|
||||||
} else {
|
|
||||||
return this.parent.parent.left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is a Red Black Tree implementation
|
|
||||||
*/
|
|
||||||
export default class Tree {
|
|
||||||
constructor () {
|
|
||||||
this.root = null
|
|
||||||
this.length = 0
|
|
||||||
}
|
|
||||||
findNext (id) {
|
|
||||||
var nextID = id.clone()
|
|
||||||
nextID.clock += 1
|
|
||||||
return this.findWithLowerBound(nextID)
|
|
||||||
}
|
|
||||||
findPrev (id) {
|
|
||||||
let prevID = id.clone()
|
|
||||||
prevID.clock -= 1
|
|
||||||
return this.findWithUpperBound(prevID)
|
|
||||||
}
|
|
||||||
findNodeWithLowerBound (from) {
|
|
||||||
var o = this.root
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
while (true) {
|
|
||||||
if (from === null || (from.lessThan(o.val._id) && o.left !== null)) {
|
|
||||||
// o is included in the bound
|
|
||||||
// try to find an element that is closer to the bound
|
|
||||||
o = o.left
|
|
||||||
} else if (from !== null && o.val._id.lessThan(from)) {
|
|
||||||
// o is not within the bound, maybe one of the right elements is..
|
|
||||||
if (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
} else {
|
|
||||||
// there is no right element. Search for the next bigger element,
|
|
||||||
// this should be within the bounds
|
|
||||||
return o.next()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findNodeWithUpperBound (to) {
|
|
||||||
if (to === void 0) {
|
|
||||||
throw new Error('You must define from!')
|
|
||||||
}
|
|
||||||
var o = this.root
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
while (true) {
|
|
||||||
if ((to === null || o.val._id.lessThan(to)) && o.right !== null) {
|
|
||||||
// o is included in the bound
|
|
||||||
// try to find an element that is closer to the bound
|
|
||||||
o = o.right
|
|
||||||
} else if (to !== null && to.lessThan(o.val._id)) {
|
|
||||||
// o is not within the bound, maybe one of the left elements is..
|
|
||||||
if (o.left !== null) {
|
|
||||||
o = o.left
|
|
||||||
} else {
|
|
||||||
// there is no left element. Search for the prev smaller element,
|
|
||||||
// this should be within the bounds
|
|
||||||
return o.prev()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findSmallestNode () {
|
|
||||||
var o = this.root
|
|
||||||
while (o != null && o.left != null) {
|
|
||||||
o = o.left
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
findWithLowerBound (from) {
|
|
||||||
var n = this.findNodeWithLowerBound(from)
|
|
||||||
return n == null ? null : n.val
|
|
||||||
}
|
|
||||||
findWithUpperBound (to) {
|
|
||||||
var n = this.findNodeWithUpperBound(to)
|
|
||||||
return n == null ? null : n.val
|
|
||||||
}
|
|
||||||
iterate (from, to, f) {
|
|
||||||
var o
|
|
||||||
if (from === null) {
|
|
||||||
o = this.findSmallestNode()
|
|
||||||
} else {
|
|
||||||
o = this.findNodeWithLowerBound(from)
|
|
||||||
}
|
|
||||||
while (
|
|
||||||
o !== null &&
|
|
||||||
(
|
|
||||||
to === null || // eslint-disable-line no-unmodified-loop-condition
|
|
||||||
o.val._id.lessThan(to) ||
|
|
||||||
o.val._id.equals(to)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
f(o.val)
|
|
||||||
o = o.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
find (id) {
|
|
||||||
let n = this.findNode(id)
|
|
||||||
if (n !== null) {
|
|
||||||
return n.val
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findNode (id) {
|
|
||||||
var o = this.root
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
while (true) {
|
|
||||||
if (o === null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (id.lessThan(o.val._id)) {
|
|
||||||
o = o.left
|
|
||||||
} else if (o.val._id.lessThan(id)) {
|
|
||||||
o = o.right
|
|
||||||
} else {
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete (id) {
|
|
||||||
var d = this.findNode(id)
|
|
||||||
if (d == null) {
|
|
||||||
// throw new Error('Element does not exist!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.length--
|
|
||||||
if (d.left !== null && d.right !== null) {
|
|
||||||
// switch d with the greates element in the left subtree.
|
|
||||||
// o should have at most one child.
|
|
||||||
var o = d.left
|
|
||||||
// find
|
|
||||||
while (o.right !== null) {
|
|
||||||
o = o.right
|
|
||||||
}
|
|
||||||
// switch
|
|
||||||
d.val = o.val
|
|
||||||
d = o
|
|
||||||
}
|
|
||||||
// d has at most one child
|
|
||||||
// let n be the node that replaces d
|
|
||||||
var isFakeChild
|
|
||||||
var child = d.left || d.right
|
|
||||||
if (child === null) {
|
|
||||||
isFakeChild = true
|
|
||||||
child = new N(null)
|
|
||||||
child.blacken()
|
|
||||||
d.right = child
|
|
||||||
} else {
|
|
||||||
isFakeChild = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (d.parent === null) {
|
|
||||||
if (!isFakeChild) {
|
|
||||||
this.root = child
|
|
||||||
child.blacken()
|
|
||||||
child._parent = null
|
|
||||||
} else {
|
|
||||||
this.root = null
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else if (d.parent.left === d) {
|
|
||||||
d.parent.left = child
|
|
||||||
} else if (d.parent.right === d) {
|
|
||||||
d.parent.right = child
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible!')
|
|
||||||
}
|
|
||||||
if (d.isBlack()) {
|
|
||||||
if (child.isRed()) {
|
|
||||||
child.blacken()
|
|
||||||
} else {
|
|
||||||
this._fixDelete(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.root.blacken()
|
|
||||||
if (isFakeChild) {
|
|
||||||
if (child.parent.left === child) {
|
|
||||||
child.parent.left = null
|
|
||||||
} else if (child.parent.right === child) {
|
|
||||||
child.parent.right = null
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible #3')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_fixDelete (n) {
|
|
||||||
function isBlack (node) {
|
|
||||||
return node !== null ? node.isBlack() : true
|
|
||||||
}
|
|
||||||
function isRed (node) {
|
|
||||||
return node !== null ? node.isRed() : false
|
|
||||||
}
|
|
||||||
if (n.parent === null) {
|
|
||||||
// this can only be called after the first iteration of fixDelete.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// d was already replaced by the child
|
|
||||||
// d is not the root
|
|
||||||
// d and child are black
|
|
||||||
var sibling = n.sibling
|
|
||||||
if (isRed(sibling)) {
|
|
||||||
// make sibling the grandfather
|
|
||||||
n.parent.redden()
|
|
||||||
sibling.blacken()
|
|
||||||
if (n === n.parent.left) {
|
|
||||||
n.parent.rotateLeft(this)
|
|
||||||
} else if (n === n.parent.right) {
|
|
||||||
n.parent.rotateRight(this)
|
|
||||||
} else {
|
|
||||||
throw new Error('Impossible #2')
|
|
||||||
}
|
|
||||||
sibling = n.sibling
|
|
||||||
}
|
|
||||||
// parent, sibling, and children of n are black
|
|
||||||
if (n.parent.isBlack() &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isBlack(sibling.left) &&
|
|
||||||
isBlack(sibling.right)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
this._fixDelete(n.parent)
|
|
||||||
} else if (n.parent.isRed() &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isBlack(sibling.left) &&
|
|
||||||
isBlack(sibling.right)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
n.parent.blacken()
|
|
||||||
} else {
|
|
||||||
if (n === n.parent.left &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isRed(sibling.left) &&
|
|
||||||
isBlack(sibling.right)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
sibling.left.blacken()
|
|
||||||
sibling.rotateRight(this)
|
|
||||||
sibling = n.sibling
|
|
||||||
} else if (n === n.parent.right &&
|
|
||||||
sibling.isBlack() &&
|
|
||||||
isRed(sibling.right) &&
|
|
||||||
isBlack(sibling.left)
|
|
||||||
) {
|
|
||||||
sibling.redden()
|
|
||||||
sibling.right.blacken()
|
|
||||||
sibling.rotateLeft(this)
|
|
||||||
sibling = n.sibling
|
|
||||||
}
|
|
||||||
sibling.color = n.parent.color
|
|
||||||
n.parent.blacken()
|
|
||||||
if (n === n.parent.left) {
|
|
||||||
sibling.right.blacken()
|
|
||||||
n.parent.rotateLeft(this)
|
|
||||||
} else {
|
|
||||||
sibling.left.blacken()
|
|
||||||
n.parent.rotateRight(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put (v) {
|
|
||||||
var node = new N(v)
|
|
||||||
if (this.root !== null) {
|
|
||||||
var p = this.root // p abbrev. parent
|
|
||||||
while (true) {
|
|
||||||
if (node.val._id.lessThan(p.val._id)) {
|
|
||||||
if (p.left === null) {
|
|
||||||
p.left = node
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
p = p.left
|
|
||||||
}
|
|
||||||
} else if (p.val._id.lessThan(node.val._id)) {
|
|
||||||
if (p.right === null) {
|
|
||||||
p.right = node
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
p = p.right
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.val = node.val
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._fixInsert(node)
|
|
||||||
} else {
|
|
||||||
this.root = node
|
|
||||||
}
|
|
||||||
this.length++
|
|
||||||
this.root.blacken()
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
_fixInsert (n) {
|
|
||||||
if (n.parent === null) {
|
|
||||||
n.blacken()
|
|
||||||
return
|
|
||||||
} else if (n.parent.isBlack()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var uncle = n.getUncle()
|
|
||||||
if (uncle !== null && uncle.isRed()) {
|
|
||||||
// Note: parent: red, uncle: red
|
|
||||||
n.parent.blacken()
|
|
||||||
uncle.blacken()
|
|
||||||
n.grandparent.redden()
|
|
||||||
this._fixInsert(n.grandparent)
|
|
||||||
} else {
|
|
||||||
// Note: parent: red, uncle: black or null
|
|
||||||
// Now we transform the tree in such a way that
|
|
||||||
// either of these holds:
|
|
||||||
// 1) grandparent.left.isRed
|
|
||||||
// and grandparent.left.left.isRed
|
|
||||||
// 2) grandparent.right.isRed
|
|
||||||
// and grandparent.right.right.isRed
|
|
||||||
if (n === n.parent.right && n.parent === n.grandparent.left) {
|
|
||||||
n.parent.rotateLeft(this)
|
|
||||||
// Since we rotated and want to use the previous
|
|
||||||
// cases, we need to set n in such a way that
|
|
||||||
// n.parent.isRed again
|
|
||||||
n = n.left
|
|
||||||
} else if (n === n.parent.left && n.parent === n.grandparent.right) {
|
|
||||||
n.parent.rotateRight(this)
|
|
||||||
// see above
|
|
||||||
n = n.right
|
|
||||||
}
|
|
||||||
// Case 1) or 2) hold from here on.
|
|
||||||
// Now traverse grandparent, make parent a black node
|
|
||||||
// on the highest level which holds two red nodes.
|
|
||||||
n.parent.blacken()
|
|
||||||
n.grandparent.redden()
|
|
||||||
if (n === n.parent.left) {
|
|
||||||
// Case 1
|
|
||||||
n.grandparent.rotateRight(this)
|
|
||||||
} else {
|
|
||||||
// Case 2
|
|
||||||
n.grandparent.rotateLeft(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flush () {}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import ID from './ID.js'
|
|
||||||
|
|
||||||
class ReverseOperation {
|
|
||||||
constructor (y, transaction) {
|
|
||||||
this.created = new Date()
|
|
||||||
const beforeState = 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 = transaction.deletedStructs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isStructInScope (y, struct, scope) {
|
|
||||||
while (struct !== y) {
|
|
||||||
if (struct === scope) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
struct = struct._parent
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyReverseOperation (y, scope, reverseBuffer) {
|
|
||||||
let performedUndo = false
|
|
||||||
y.transact(() => {
|
|
||||||
while (!performedUndo && reverseBuffer.length > 0) {
|
|
||||||
let undoOp = reverseBuffer.pop()
|
|
||||||
// make sure that it is possible to iterate {from}-{to}
|
|
||||||
y.os.getItemCleanStart(undoOp.fromState)
|
|
||||||
y.os.getItemCleanEnd(undoOp.toState)
|
|
||||||
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
|
||||||
if (!op._deleted && isStructInScope(y, op, scope)) {
|
|
||||||
performedUndo = true
|
|
||||||
op._delete(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (let op of undoOp.deletedStructs) {
|
|
||||||
if (
|
|
||||||
isStructInScope(y, op, scope) &&
|
|
||||||
op._parent !== y &&
|
|
||||||
!op._parent._deleted &&
|
|
||||||
(
|
|
||||||
op._parent._id.user !== y.userID ||
|
|
||||||
op._parent._id.clock < undoOp.fromState.clock ||
|
|
||||||
op._parent._id.clock > undoOp.fromState.clock
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
performedUndo = true
|
|
||||||
op = op._copy(undoOp.deletedStructs)
|
|
||||||
op._integrate(y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return performedUndo
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UndoManager {
|
|
||||||
constructor (scope, options = {}) {
|
|
||||||
this.options = options
|
|
||||||
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
|
||||||
this._undoBuffer = []
|
|
||||||
this._redoBuffer = []
|
|
||||||
this._scope = scope
|
|
||||||
this._undoing = false
|
|
||||||
this._redoing = false
|
|
||||||
const y = scope._y
|
|
||||||
this.y = y
|
|
||||||
y.on('afterTransaction', (y, transaction, remote) => {
|
|
||||||
if (!remote && transaction.changedParentTypes.has(scope)) {
|
|
||||||
let reverseOperation = new ReverseOperation(y, transaction)
|
|
||||||
if (!this._undoing) {
|
|
||||||
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
|
|
||||||
if (lastUndoOp !== null && reverseOperation.created - lastUndoOp.created <= options.captureTimeout) {
|
|
||||||
lastUndoOp.created = reverseOperation.created
|
|
||||||
lastUndoOp.toState = reverseOperation.toState
|
|
||||||
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
|
|
||||||
} else {
|
|
||||||
this._undoBuffer.push(reverseOperation)
|
|
||||||
}
|
|
||||||
if (!this._redoing) {
|
|
||||||
this._redoBuffer = []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._redoBuffer.push(reverseOperation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
undo () {
|
|
||||||
this._undoing = true
|
|
||||||
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
|
||||||
this._undoing = false
|
|
||||||
return performedUndo
|
|
||||||
}
|
|
||||||
redo () {
|
|
||||||
this._redoing = true
|
|
||||||
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
|
||||||
this._redoing = false
|
|
||||||
return performedRedo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
export default class YEvent {
|
|
||||||
constructor (target) {
|
|
||||||
this.target = target
|
|
||||||
this.currentTarget = target
|
|
||||||
}
|
|
||||||
get path () {
|
|
||||||
const path = []
|
|
||||||
let type = this.target
|
|
||||||
const y = type._y
|
|
||||||
while (type !== this.currentTarget && type !== y) {
|
|
||||||
let parent = type._parent
|
|
||||||
if (type._parentSub !== null) {
|
|
||||||
path.unshift(type._parentSub)
|
|
||||||
} else {
|
|
||||||
// parent is array-ish
|
|
||||||
for (let [i, child] of parent) {
|
|
||||||
if (child === type) {
|
|
||||||
path.unshift(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type = parent
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
import ID from '../Util/ID.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to merge all items in os with their successors.
|
|
||||||
*
|
|
||||||
* Some transformations (like delete) fragment items.
|
|
||||||
* Item(c: 'ab') + Delete(1,1) + Delete(0, 1) -> Item(c: 'a',deleted);Item(c: 'b',deleted)
|
|
||||||
*
|
|
||||||
* This functions merges the fragmented nodes together:
|
|
||||||
* Item(c: 'a',deleted);Item(c: 'b',deleted) -> Item(c: 'ab', deleted)
|
|
||||||
*
|
|
||||||
* TODO: The Tree implementation does not support deletions in-spot.
|
|
||||||
* This is why all deletions must be performed after the traversal.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function defragmentItemContent (y) {
|
|
||||||
const os = y.os
|
|
||||||
if (os.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let deletes = []
|
|
||||||
let node = os.findSmallestNode()
|
|
||||||
let next = node.next()
|
|
||||||
while (next !== null) {
|
|
||||||
let a = node.val
|
|
||||||
let b = next.val
|
|
||||||
if (
|
|
||||||
(a instanceof ItemJSON || a instanceof ItemString) &&
|
|
||||||
a.constructor === b.constructor &&
|
|
||||||
a._deleted === b._deleted &&
|
|
||||||
a._right === b &&
|
|
||||||
(new ID(a._id.user, a._id.clock + a._length)).equals(b._id)
|
|
||||||
) {
|
|
||||||
a._right = b._right
|
|
||||||
if (a instanceof ItemJSON) {
|
|
||||||
a._content = a._content.concat(b._content)
|
|
||||||
} else if (a instanceof ItemString) {
|
|
||||||
a._content += b._content
|
|
||||||
}
|
|
||||||
// delete b later
|
|
||||||
deletes.push(b._id)
|
|
||||||
// do not iterate node!
|
|
||||||
// !(node = next)
|
|
||||||
} else {
|
|
||||||
// not able to merge node, get next node
|
|
||||||
node = next
|
|
||||||
}
|
|
||||||
// update next
|
|
||||||
next = next.next()
|
|
||||||
}
|
|
||||||
for (let i = deletes.length - 1; i >= 0; i--) {
|
|
||||||
os.delete(deletes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/* global crypto */
|
|
||||||
|
|
||||||
export function generateUserID () {
|
|
||||||
if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) {
|
|
||||||
// browser
|
|
||||||
let arr = new Uint32Array(1)
|
|
||||||
crypto.getRandomValues(arr)
|
|
||||||
return arr[0]
|
|
||||||
} else if (typeof crypto !== 'undefined' && crypto.randomBytes != null) {
|
|
||||||
// node
|
|
||||||
let buf = crypto.randomBytes(4)
|
|
||||||
return new Uint32Array(buf.buffer)[0]
|
|
||||||
} else {
|
|
||||||
return Math.ceil(Math.random() * 0xFFFFFFFF)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import ID from './ID.js'
|
|
||||||
import RootID from './RootID.js'
|
|
||||||
|
|
||||||
export function getRelativePosition (type, offset) {
|
|
||||||
if (offset === 0) {
|
|
||||||
return ['startof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
|
|
||||||
} else {
|
|
||||||
let t = type._start
|
|
||||||
while (t !== null) {
|
|
||||||
if (t._length >= offset) {
|
|
||||||
return [t._id.user, t._id.clock + offset - 1]
|
|
||||||
}
|
|
||||||
if (t._right === null) {
|
|
||||||
return [t._id.user, t._id.clock + t._length - 1]
|
|
||||||
}
|
|
||||||
if (!t._deleted) {
|
|
||||||
offset -= t._length
|
|
||||||
}
|
|
||||||
t = t._right
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromRelativePosition (y, rpos) {
|
|
||||||
if (rpos[0] === 'startof') {
|
|
||||||
let id
|
|
||||||
if (rpos[3] === null) {
|
|
||||||
id = new ID(rpos[1], rpos[2])
|
|
||||||
} else {
|
|
||||||
id = new RootID(rpos[3], rpos[4])
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: y.os.get(id),
|
|
||||||
offset: 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let offset = 0
|
|
||||||
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
|
|
||||||
const parent = struct._parent
|
|
||||||
if (parent._deleted) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!struct._deleted) {
|
|
||||||
offset = rpos[1] - struct._id.clock + 1
|
|
||||||
}
|
|
||||||
struct = struct._left
|
|
||||||
while (struct !== null) {
|
|
||||||
if (!struct._deleted) {
|
|
||||||
offset += struct._length
|
|
||||||
}
|
|
||||||
struct = struct._left
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: parent,
|
|
||||||
offset: offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import YArray from '../Type/YArray.js'
|
|
||||||
import YMap from '../Type/YMap.js'
|
|
||||||
import YText from '../Type/YText.js'
|
|
||||||
import YXmlFragment from '../Type/y-xml/YXmlFragment.js'
|
|
||||||
import YXmlElement from '../Type/y-xml/YXmlElement.js'
|
|
||||||
import YXmlText from '../Type/y-xml/YXmlText.js'
|
|
||||||
|
|
||||||
import Delete from '../Struct/Delete.js'
|
|
||||||
import ItemJSON from '../Struct/ItemJSON.js'
|
|
||||||
import ItemString from '../Struct/ItemString.js'
|
|
||||||
|
|
||||||
const structs = new Map()
|
|
||||||
const references = new Map()
|
|
||||||
|
|
||||||
function addStruct (reference, structConstructor) {
|
|
||||||
structs.set(reference, structConstructor)
|
|
||||||
references.set(structConstructor, reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStruct (reference) {
|
|
||||||
return structs.get(reference)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReference (typeConstructor) {
|
|
||||||
return references.get(typeConstructor)
|
|
||||||
}
|
|
||||||
|
|
||||||
addStruct(0, ItemJSON)
|
|
||||||
addStruct(1, ItemString)
|
|
||||||
addStruct(2, Delete)
|
|
||||||
|
|
||||||
addStruct(3, YArray)
|
|
||||||
addStruct(4, YMap)
|
|
||||||
addStruct(5, YText)
|
|
||||||
addStruct(6, YXmlFragment)
|
|
||||||
addStruct(7, YXmlElement)
|
|
||||||
addStruct(8, YXmlText)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
import YMap from '../Type/YMap'
|
|
||||||
import YArray from '../Type/YArray'
|
|
||||||
|
|
||||||
export function writeObjectToYMap (object, type) {
|
|
||||||
for (var key in object) {
|
|
||||||
var val = object[key]
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
type.set(key, YArray)
|
|
||||||
writeArrayToYArray(val, type.get(key))
|
|
||||||
} else if (typeof val === 'object') {
|
|
||||||
type.set(key, YMap)
|
|
||||||
writeObjectToYMap(val, type.get(key))
|
|
||||||
} else {
|
|
||||||
type.set(key, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeArrayToYArray (array, type) {
|
|
||||||
for (var i = array.length - 1; i >= 0; i--) {
|
|
||||||
var val = array[i]
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
type.insert(0, [YArray])
|
|
||||||
writeArrayToYArray(val, type.get(0))
|
|
||||||
} else if (typeof val === 'object') {
|
|
||||||
type.insert(0, [YMap])
|
|
||||||
writeObjectToYMap(val, type.get(0))
|
|
||||||
} else {
|
|
||||||
type.insert(0, [val])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
174
src/Y.js
174
src/Y.js
@@ -1,174 +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 { getRelativePosition, fromRelativePosition } from './Util/relativePosition.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._transaction = new Transaction(this)
|
|
||||||
this.emit('beforeTransaction', this, this._transaction, remote)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
f(this)
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
if (initialCall) {
|
|
||||||
this.emit('beforeObserverCalls', this, this._transaction, remote)
|
|
||||||
const transaction = this._transaction
|
|
||||||
this._transaction = null
|
|
||||||
// emit change events on changed types
|
|
||||||
transaction.changedTypes.forEach(function (subs, type) {
|
|
||||||
if (!type._deleted) {
|
|
||||||
type._callObserver(transaction, subs, remote)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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(transaction, events)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// when all changes & events are processed, emit afterTransaction event
|
|
||||||
this.emit('afterTransaction', this, transaction, remote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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 (this.share[name] === undefined) {
|
|
||||||
this.share[name] = type
|
|
||||||
} else if (this.share[name] !== type) {
|
|
||||||
throw new Error('Type is already defined with a different constructor')
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
getRelativePosition,
|
|
||||||
fromRelativePosition
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.debug = debug
|
|
||||||
debug.formatters.Y = messageToString
|
|
||||||
debug.formatters.y = messageToRoomname
|
|
||||||
124
src/index.js
Normal file
124
src/index.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/** 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,
|
||||||
|
obfuscateUpdate,
|
||||||
|
obfuscateUpdateV2,
|
||||||
|
UpdateEncoderV1
|
||||||
|
} from './internals.js'
|
||||||
|
|
||||||
|
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
|
||||||
|
? globalThis
|
||||||
|
: typeof window !== 'undefined'
|
||||||
|
? window
|
||||||
|
// @ts-ignore
|
||||||
|
: typeof global !== 'undefined' ? global : {})
|
||||||
|
|
||||||
|
const importIdentifier = '__ $YJS$ __'
|
||||||
|
|
||||||
|
if (glo[importIdentifier] === true) {
|
||||||
|
/**
|
||||||
|
* Dear reader of this message. Please take this seriously.
|
||||||
|
*
|
||||||
|
* If you see this message, 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.
|
||||||
|
*
|
||||||
|
* https://github.com/yjs/yjs/issues/438
|
||||||
|
*/
|
||||||
|
console.error('Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438')
|
||||||
|
}
|
||||||
|
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())
|
||||||
172
src/structs/ContentType.js
Normal file
172
src/structs/ContentType.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
readYArray,
|
||||||
|
readYMap,
|
||||||
|
readYText,
|
||||||
|
readYXmlElement,
|
||||||
|
readYXmlFragment,
|
||||||
|
readYXmlHook,
|
||||||
|
readYXmlText,
|
||||||
|
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as error from 'lib0/error'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const typeRefs = [
|
||||||
|
readYArray,
|
||||||
|
readYMap,
|
||||||
|
readYText,
|
||||||
|
readYXmlElement,
|
||||||
|
readYXmlFragment,
|
||||||
|
readYXmlHook,
|
||||||
|
readYXmlText
|
||||||
|
]
|
||||||
|
|
||||||
|
export const YArrayRefID = 0
|
||||||
|
export const YMapRefID = 1
|
||||||
|
export const YTextRefID = 2
|
||||||
|
export const YXmlElementRefID = 3
|
||||||
|
export const YXmlFragmentRefID = 4
|
||||||
|
export const YXmlHookRefID = 5
|
||||||
|
export const YXmlTextRefID = 6
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class ContentType {
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
*/
|
||||||
|
constructor (type) {
|
||||||
|
/**
|
||||||
|
* @type {AbstractType<any>}
|
||||||
|
*/
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getLength () {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Array<any>}
|
||||||
|
*/
|
||||||
|
getContent () {
|
||||||
|
return [this.type]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isCountable () {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {ContentType}
|
||||||
|
*/
|
||||||
|
copy () {
|
||||||
|
return new ContentType(this.type._copy())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} offset
|
||||||
|
* @return {ContentType}
|
||||||
|
*/
|
||||||
|
splice (offset) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ContentType} right
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
mergeWith (right) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
integrate (transaction, item) {
|
||||||
|
this.type._integrate(transaction.doc, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
delete (transaction) {
|
||||||
|
let item = this.type._start
|
||||||
|
while (item !== null) {
|
||||||
|
if (!item.deleted) {
|
||||||
|
item.delete(transaction)
|
||||||
|
} else {
|
||||||
|
// This will be gc'd later and we want to merge it if possible
|
||||||
|
// We try to merge all deleted items after each transaction,
|
||||||
|
// but we have no knowledge about that this needs to be merged
|
||||||
|
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
|
||||||
|
transaction._mergeStructs.push(item)
|
||||||
|
}
|
||||||
|
item = item.right
|
||||||
|
}
|
||||||
|
this.type._map.forEach(item => {
|
||||||
|
if (!item.deleted) {
|
||||||
|
item.delete(transaction)
|
||||||
|
} else {
|
||||||
|
// same as above
|
||||||
|
transaction._mergeStructs.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
transaction.changed.delete(this.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
gc (store) {
|
||||||
|
let item = this.type._start
|
||||||
|
while (item !== null) {
|
||||||
|
item.gc(store, true)
|
||||||
|
item = item.right
|
||||||
|
}
|
||||||
|
this.type._start = null
|
||||||
|
this.type._map.forEach(/** @param {Item | null} item */ (item) => {
|
||||||
|
while (item !== null) {
|
||||||
|
item.gc(store, true)
|
||||||
|
item = item.left
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.type._map = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
this.type._write(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||||
|
* @return {ContentType}
|
||||||
|
*/
|
||||||
|
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user