Compare commits
549 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c87f9a021 | ||
|
|
4b08c67e06 | ||
|
|
9f5bc9ddfe | ||
|
|
b399ffa765 | ||
|
|
180f4667c1 | ||
|
|
9455373611 | ||
|
|
aa804d89c0 | ||
|
|
3ef51a5d1a | ||
|
|
e61089c659 | ||
|
|
97625cf29b | ||
|
|
a5dc6c27aa | ||
|
|
26a51bafc9 | ||
|
|
f40e09d156 | ||
|
|
81650bc8f6 | ||
|
|
c87caafeb6 | ||
|
|
195b26d90f | ||
|
|
7e0189ca84 | ||
|
|
192706f2a8 | ||
|
|
a4ce8ae07d | ||
|
|
e04a980af1 | ||
|
|
47d40eb6b0 | ||
|
|
fc4a39cc7d | ||
|
|
44e1fd9f14 | ||
|
|
02cc5a215f | ||
|
|
d1e8d50c43 | ||
|
|
18bb2d0719 | ||
|
|
45df311dd7 | ||
|
|
62888b4004 | ||
|
|
76c389dba0 | ||
|
|
78fa98c000 | ||
|
|
e9f9e08450 | ||
|
|
e3c59b0aa7 | ||
|
|
705dce7838 | ||
|
|
0fb55981ba | ||
|
|
89378e29ae | ||
|
|
cce35270ec | ||
|
|
d78180bf97 | ||
|
|
0ab415de3e | ||
|
|
ff3969caeb | ||
|
|
c82cc9f8d6 | ||
|
|
ef5c71bd8b | ||
|
|
bd6be3d23b | ||
|
|
0e6deab9c9 | ||
|
|
6cd9e2be32 | ||
|
|
ac8dab1e88 | ||
|
|
38ed725c2c | ||
|
|
a210bad25e | ||
|
|
6929a4f0f8 | ||
|
|
52dacfa5f2 | ||
|
|
27efe86f9c | ||
|
|
882b9055c7 | ||
|
|
e089089413 | ||
|
|
197932752e | ||
|
|
f0b2bdaf34 | ||
|
|
b96362c0f1 | ||
|
|
67f241cd7a | ||
|
|
c8af0bebf7 | ||
|
|
4f35e799a6 | ||
|
|
eb2a52dd26 | ||
|
|
189b1068ae | ||
|
|
7a3b60a5d7 | ||
|
|
99f06fc093 | ||
|
|
22917bca19 | ||
|
|
7f0e25dcba | ||
|
|
d90c9b1cb2 | ||
|
|
c426055f17 | ||
|
|
18c9010b63 | ||
|
|
c3edac62ef | ||
|
|
755de18fd5 | ||
|
|
641dc25076 | ||
|
|
1d58ea785f | ||
|
|
f53dff5043 | ||
|
|
74d1a31f49 | ||
|
|
d1063ab70b | ||
|
|
f4c919d9ec | ||
|
|
aeb23dbaa9 | ||
|
|
6d4f0c0cdd | ||
|
|
303138f309 | ||
|
|
ad373a3dce | ||
|
|
2150fa58f2 | ||
|
|
ece4841b5c | ||
|
|
8103220c05 | ||
|
|
66d500f08d | ||
|
|
5f8e7c7ba7 | ||
|
|
7b8eee6b25 | ||
|
|
1d5947c602 | ||
|
|
53e4028952 | ||
|
|
b38a8d99e5 | ||
|
|
6c4971ae25 | ||
|
|
d1f5ff0f59 | ||
|
|
1d297601e8 | ||
|
|
d9fface0be | ||
|
|
7d5db917da | ||
|
|
6e7529723d | ||
|
|
6cb64b3707 | ||
|
|
bb1c0b809f | ||
|
|
8bcff6138c | ||
|
|
e78d84ee59 | ||
|
|
c23bcb66ce | ||
|
|
5fddcef3ea | ||
|
|
e1e46c6eb1 | ||
|
|
13ad0c8464 | ||
|
|
7700b50470 | ||
|
|
fc4d6165b4 | ||
|
|
251c8aaefc | ||
|
|
1337d38ada | ||
|
|
f5c66e41cb | ||
|
|
0e7da017fe | ||
|
|
f0262ffaae | ||
|
|
36203af88e | ||
|
|
dd2b8bc6c7 | ||
|
|
463065ac21 | ||
|
|
d064e6e96e | ||
|
|
b1ed2df208 | ||
|
|
1fe4ef135c | ||
|
|
e376b5d472 | ||
|
|
952a9b2c41 | ||
|
|
03458dc641 | ||
|
|
14df5b72af | ||
|
|
338968031b | ||
|
|
1aac245b93 | ||
|
|
1faff323c1 | ||
|
|
e7280c7ae2 | ||
|
|
4c38619b5d | ||
|
|
b4e5c5cc1f | ||
|
|
b0dbd84f7f | ||
|
|
4a990963d9 | ||
|
|
7e7c9d5b11 | ||
|
|
775f6eed1d | ||
|
|
1e83b9418c | ||
|
|
ac3f672c80 | ||
|
|
2192aa5821 | ||
|
|
70bb523005 | ||
|
|
10ce6de57a | ||
|
|
3fba4f25a5 | ||
|
|
66c35d8499 | ||
|
|
4c14157dcf | ||
|
|
ef6c382e20 | ||
|
|
ee45b4fdd6 | ||
|
|
668e9e8a9b | ||
|
|
37a6d68543 | ||
|
|
f893198769 | ||
|
|
d3ee1a0ec2 | ||
|
|
d6593412a2 | ||
|
|
d31bf36531 | ||
|
|
a485f550db | ||
|
|
0610b16227 | ||
|
|
72e470c5f0 | ||
|
|
4d12a02e2f | ||
|
|
4a7d6f0a2d | ||
|
|
c80f446b5f | ||
|
|
81a529d8dc | ||
|
|
4f0ab78914 | ||
|
|
8c36f67f0b | ||
|
|
77687d94e6 | ||
|
|
4644511303 | ||
|
|
20005eecdb | ||
|
|
c9dda245bf | ||
|
|
1417470156 | ||
|
|
584e5dfd40 | ||
|
|
805acbb9f5 | ||
|
|
32c4c09072 | ||
|
|
8c5a06bbf8 | ||
|
|
a336cc167c | ||
|
|
21d86cd2be | ||
|
|
1d0f9faa91 | ||
|
|
45237571b7 | ||
|
|
bb6f6cd141 | ||
|
|
729c1f16b8 | ||
|
|
b6059704aa | ||
|
|
fa3c92f44c | ||
|
|
cd82de7742 | ||
|
|
07a6a0044b | ||
|
|
4582832a71 | ||
|
|
07ac1d03e3 | ||
|
|
cbcf1facb8 | ||
|
|
31ff7ac78c | ||
|
|
ed3b31e58f | ||
|
|
759ecb21f7 | ||
|
|
9c29d820c8 | ||
|
|
2ef11a5344 | ||
|
|
9fe47e98d5 | ||
|
|
654510f3ff | ||
|
|
52ec698635 | ||
|
|
1b06f59d1c | ||
|
|
12bcc4d080 | ||
|
|
e1a9f314a7 | ||
|
|
7a111de186 | ||
|
|
90b3fa9dd9 | ||
|
|
c635963747 | ||
|
|
1b17b5e400 | ||
|
|
61d9d96d15 | ||
|
|
7d0c048708 | ||
|
|
8a7416ad50 | ||
|
|
e56899a02c | ||
|
|
30bf3742c9 | ||
|
|
8dbd2c4696 | ||
|
|
6578727c9c | ||
|
|
92ca001cdc | ||
|
|
415de1cc4c | ||
|
|
e23582b1cd | ||
|
|
73c28952c2 | ||
|
|
1bc1e88d6a | ||
|
|
c188f813a4 | ||
|
|
ff981a8697 | ||
|
|
d9ab593b07 | ||
|
|
293527e62b | ||
|
|
5a42a94cf4 | ||
|
|
040808300c | ||
|
|
57975d409e | ||
|
|
306b2c64f3 | ||
|
|
585265e9a5 | ||
|
|
777ae9503a | ||
|
|
4c1798e5fa | ||
|
|
f4d85e2a3e | ||
|
|
a0f0c9c377 | ||
|
|
95ec2a435a | ||
|
|
da9836fe59 | ||
|
|
3a7411f9e8 | ||
|
|
39cee7c6e7 | ||
|
|
0a5753c191 | ||
|
|
76b7d0b651 | ||
|
|
99e3e95a00 | ||
|
|
93ee4ee287 | ||
|
|
c5cc403a29 | ||
|
|
75f4a0a5f0 | ||
|
|
591df5c00a | ||
|
|
f6b4819ae3 | ||
|
|
d483d9cc83 | ||
|
|
453407b93d | ||
|
|
e699f92333 | ||
|
|
6ff47719ef | ||
|
|
3a0694c35c | ||
|
|
74e5243742 | ||
|
|
dcf43b9797 | ||
|
|
77e479c03b | ||
|
|
ec58a99748 | ||
|
|
f1eb66655b | ||
|
|
7f4ae9fe14 | ||
|
|
c0ba56a21f | ||
|
|
4063e28b5e | ||
|
|
b6f7cd7869 | ||
|
|
1a79e429ed | ||
|
|
04066a5678 | ||
|
|
e09ef15349 | ||
|
|
3d70eee959 | ||
|
|
582095e5a3 | ||
|
|
c9ea3a412e | ||
|
|
a2c51c36e9 | ||
|
|
ab3dba5b06 | ||
|
|
3ddff186c2 | ||
|
|
9bd199a6e7 | ||
|
|
01d0825ae6 | ||
|
|
e2f98525d2 | ||
|
|
70a0a03130 | ||
|
|
656d85c62e | ||
|
|
e168dd48fb | ||
|
|
12d43199d5 | ||
|
|
539fa8b21d | ||
|
|
f572f94586 | ||
|
|
c12d00b227 | ||
|
|
e4a5f2caec | ||
|
|
9f9f465238 | ||
|
|
8450ff86d7 | ||
|
|
70139262c5 | ||
|
|
9c0da271eb | ||
|
|
ade3e1949d | ||
|
|
eec63a008f | ||
|
|
52abcdd043 | ||
|
|
f94653424a | ||
|
|
d67a794e2c | ||
|
|
60318083a6 | ||
|
|
7607070452 | ||
|
|
28fb7b6e9c | ||
|
|
aafe15757f | ||
|
|
31d6ef6296 | ||
|
|
32b8fac37f | ||
|
|
e8060de914 | ||
|
|
22b036527c | ||
|
|
feb1e030d7 | ||
|
|
bd271e3952 | ||
|
|
df80938190 | ||
|
|
67bbc0a3fe | ||
|
|
e1ece6dc66 | ||
|
|
fe038822a3 | ||
|
|
dece14486c | ||
|
|
2daffbc2ca | ||
|
|
4c01a34d09 | ||
|
|
3b08267daa | ||
|
|
b98ebddb69 | ||
|
|
9d5bf50676 | ||
|
|
c0972f8158 | ||
|
|
548125a944 | ||
|
|
a7b124ca6e | ||
|
|
4022374620 | ||
|
|
860e4d7af6 | ||
|
|
6376d69b58 | ||
|
|
5cf6f45f19 | ||
|
|
967903673b | ||
|
|
2d897f1844 | ||
|
|
fb2f9bc493 | ||
|
|
6f9ae0c4fc | ||
|
|
9df20fac8a | ||
|
|
a1fb1a6258 | ||
|
|
417d0ef3b5 | ||
|
|
9be256231b | ||
|
|
c122bdc750 | ||
|
|
4ef36ab81c | ||
|
|
cccc0e1015 | ||
|
|
db5312443e | ||
|
|
dbda07424b | ||
|
|
684d38d6c8 | ||
|
|
44fa064eb2 | ||
|
|
9b6fffd880 | ||
|
|
e9993b2643 | ||
|
|
762e9e8a3a | ||
|
|
6ddeb788c7 | ||
|
|
b9245f323c | ||
|
|
c0e630b635 | ||
|
|
e56457a0ef | ||
|
|
ca13849828 | ||
|
|
92c2fbd6d3 | ||
|
|
65b8921f05 | ||
|
|
1ace7f4b73 | ||
|
|
6336064516 | ||
|
|
49d2e42b41 | ||
|
|
c098e8e745 | ||
|
|
38558a7fad | ||
|
|
bdb3782f8f | ||
|
|
bc32f7348e | ||
|
|
09a94f053e | ||
|
|
0df0079fa3 | ||
|
|
a54d826d6d | ||
|
|
99f92cb9a0 | ||
|
|
e788ad1333 | ||
|
|
1fe37c565e | ||
|
|
ed2273e2ed | ||
|
|
94933a704d | ||
|
|
ef6eb08335 | ||
|
|
d915c8dd13 | ||
|
|
32207cbca0 | ||
|
|
135c6d31be | ||
|
|
61149b458a | ||
|
|
ba97bfdd9e | ||
|
|
689bca8602 | ||
|
|
6dd43cde17 | ||
|
|
026675b438 | ||
|
|
941a22b257 | ||
|
|
4aa41b98a9 | ||
|
|
acf443aacb | ||
|
|
aa8c934833 | ||
|
|
814af5a3d7 | ||
|
|
bbc207aaa6 | ||
|
|
a9b610479d | ||
|
|
079de07eff | ||
|
|
54453e87fa | ||
|
|
1b0e3659c3 | ||
|
|
dc22a79ac4 | ||
|
|
384a4b72b0 | ||
|
|
f35c056bde | ||
|
|
250050e83b | ||
|
|
248d08be30 | ||
|
|
641f426339 | ||
|
|
fcbca65d8f | ||
|
|
5f8ae0dd43 | ||
|
|
de14fe0f3e | ||
|
|
5e4b071693 | ||
|
|
937de2c59f | ||
|
|
f1f1bff901 | ||
|
|
da748a78f4 | ||
|
|
4855b2d590 | ||
|
|
908ce31e2f | ||
|
|
e4d4c23f0b | ||
|
|
fc500a8247 | ||
|
|
4b84541d76 | ||
|
|
a3ab42c157 | ||
|
|
bbd3317d62 | ||
|
|
5d3922cb64 | ||
|
|
a81a2cd553 | ||
|
|
c0d24bdba4 | ||
|
|
40e913e9c5 | ||
|
|
94f6a0fd9c | ||
|
|
41a88dbc43 | ||
|
|
1d4f283955 | ||
|
|
fc3a4c376c | ||
|
|
acb0affa33 | ||
|
|
0b510b64a3 | ||
|
|
c8f0cf5556 | ||
|
|
11a4271fd1 | ||
|
|
c7670915c7 | ||
|
|
eb2d596538 | ||
|
|
48e17ea1a7 | ||
|
|
1a22fdd45e | ||
|
|
07cf0b3436 | ||
|
|
5a68b9f4ad | ||
|
|
445dd3e0da | ||
|
|
0ba97d78f8 | ||
|
|
fc5be5c7cc | ||
|
|
f2debc150c | ||
|
|
08f37a86e3 | ||
|
|
f5d17e6236 | ||
|
|
8f3bd7170a | ||
|
|
5586334549 | ||
|
|
24c1e4dcc8 | ||
|
|
d61bbecf4e | ||
|
|
85492ad2e0 | ||
|
|
02253f9a8d | ||
|
|
8105bef1af | ||
|
|
4efa16e2dd | ||
|
|
ad44f59def | ||
|
|
9c471ea24d | ||
|
|
d9e76014f5 | ||
|
|
4091b7d004 | ||
|
|
dfc183643d | ||
|
|
cf8698f2b6 | ||
|
|
3595f14da7 | ||
|
|
c6e671b1d5 | ||
|
|
e4c10fd6b3 | ||
|
|
e70aa09f88 | ||
|
|
7808b143da | ||
|
|
b35092928e | ||
|
|
b7dbcf69d3 | ||
|
|
377df18788 | ||
|
|
26a323733d | ||
|
|
d0d1015074 | ||
|
|
2e3240b379 | ||
|
|
2558652356 | ||
|
|
783cbd63fc | ||
|
|
41be80e751 | ||
|
|
3d6050d8a2 | ||
|
|
3d5ba7b4cc | ||
|
|
415b66607c | ||
|
|
05cd1d0575 | ||
|
|
4edc22bedb | ||
|
|
16f84c67d5 | ||
|
|
290d3c8ffe | ||
|
|
c51e8b46c2 | ||
|
|
0cda1630d2 | ||
|
|
d232b883e9 | ||
|
|
3a0e65403f | ||
|
|
224fff93ba | ||
|
|
4f55e8c655 | ||
|
|
a08624c04e | ||
|
|
9b00929172 | ||
|
|
b94267e14a | ||
|
|
e696304845 | ||
|
|
d503c9d640 | ||
|
|
e5f289506f | ||
|
|
c453593ee7 | ||
|
|
5ed1818de5 | ||
|
|
0310500c4e | ||
|
|
b7defc32e8 | ||
|
|
dbdd49af23 | ||
|
|
b7c05ba133 | ||
|
|
9298903bdb | ||
|
|
d59e30b239 | ||
|
|
d29b83a457 | ||
|
|
0208d83f91 | ||
|
|
c545118637 | ||
|
|
c619aa33d9 | ||
|
|
1dea8f394f | ||
|
|
5cf8d20cf6 | ||
|
|
74f9ceab01 | ||
|
|
ca81cdf3be | ||
|
|
96c6aa2751 | ||
|
|
e6b5e258fb | ||
|
|
e8170a09a7 | ||
|
|
9d1ad8cb28 | ||
|
|
d859fd68fe | ||
|
|
2b7d2ed1e6 | ||
|
|
142a5ada60 | ||
|
|
c92f987496 | ||
|
|
755c9eb16e | ||
|
|
1311c7a0d8 | ||
|
|
4eec8ecdd3 | ||
|
|
0e426f8928 | ||
|
|
82015d5a37 | ||
|
|
d9ee67d2f3 | ||
|
|
791f6c12f0 | ||
|
|
23d019c244 | ||
|
|
c8ca80d15f | ||
|
|
be282c8338 | ||
|
|
829a094c6d | ||
|
|
725273167e | ||
|
|
581264c5e3 | ||
|
|
be537c9f8c | ||
|
|
4028eee39d | ||
|
|
0e3e561ec7 | ||
|
|
7df46cb731 | ||
|
|
40fb16ef32 | ||
|
|
ada5d36cd5 | ||
|
|
f537a43e29 | ||
|
|
3a305fb228 | ||
|
|
1afdab376d | ||
|
|
526c862071 | ||
|
|
fdbb558ce2 | ||
|
|
76ad58bb59 | ||
|
|
c88a813bb0 | ||
|
|
ccf6d86c98 | ||
|
|
6b5c02f1ce | ||
|
|
2be6e935a4 | ||
|
|
0ddf3bf742 | ||
|
|
5f29724578 | ||
|
|
ab6cde07e6 | ||
|
|
0455eaa8ad | ||
|
|
9ed7e15d0f | ||
|
|
6e633d0bd9 | ||
|
|
e16195cb54 | ||
|
|
86c46cf0ec | ||
|
|
8770c8e934 | ||
|
|
7e12ea2db5 | ||
|
|
3ca260e0da | ||
|
|
edb5e4f719 | ||
|
|
be3b8b65ce | ||
|
|
d093ef56c8 | ||
|
|
90b2a895b8 | ||
|
|
4f57c91b82 | ||
|
|
3e1d89253f | ||
|
|
03e1a3fc12 | ||
|
|
5c33f41c30 | ||
|
|
65e8c29b33 | ||
|
|
fed77d532f | ||
|
|
d129184f7b | ||
|
|
a05bb1d4f9 | ||
|
|
65af4963e6 | ||
|
|
4dce0816a6 | ||
|
|
5384bf4faf | ||
|
|
454ac9ba16 | ||
|
|
e2ec53be65 | ||
|
|
aa6edcfd9b | ||
|
|
f31ec9a8b8 | ||
|
|
003fa735a0 | ||
|
|
574f0c3269 | ||
|
|
eb4fb3a225 | ||
|
|
c97130abc4 | ||
|
|
a19cfa1465 | ||
|
|
bb45abbb70 | ||
|
|
67b47fd868 | ||
|
|
2c18b9ffad | ||
|
|
a6b7d76544 | ||
|
|
442ea7ec70 | ||
|
|
747da52c0b | ||
|
|
6c37bd4463 | ||
|
|
dd6c196135 | ||
|
|
252bec0ad2 | ||
|
|
6c8876d282 | ||
|
|
3c317828d1 | ||
|
|
cd3f4a72d6 | ||
|
|
2c852c85c6 |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
["latest", {
|
|
||||||
"es2015": {
|
|
||||||
"modules": false
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"external-helpers"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
12
.flowconfig
12
.flowconfig
@@ -1,12 +0,0 @@
|
|||||||
[ignore]
|
|
||||||
.*/node_modules/.*
|
|
||||||
.*/dist/.*
|
|
||||||
.*/build/.*
|
|
||||||
|
|
||||||
[include]
|
|
||||||
./src/
|
|
||||||
|
|
||||||
[libs]
|
|
||||||
./declarations/
|
|
||||||
|
|
||||||
[options]
|
|
||||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: dmonad
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
31
.github/workflows/nodejs.yml
vendored
Normal file
31
.github/workflows/nodejs.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Node.js CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x, 13.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm run test-extensive
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,15 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
bower_components
|
dist
|
||||||
build
|
|
||||||
build_test
|
|
||||||
.directory
|
|
||||||
.codio
|
|
||||||
.settings
|
|
||||||
.jshintignore
|
|
||||||
.jshintrc
|
|
||||||
.validate.json
|
|
||||||
/y.js
|
|
||||||
/y.js.map
|
|
||||||
/y-*
|
|
||||||
.vscode
|
.vscode
|
||||||
jsconfig.json
|
docs
|
||||||
|
|||||||
50
.jsdoc.json
Normal file
50
.jsdoc.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"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": "Yjs website"
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"url": "https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png",
|
||||||
|
"width": "162px",
|
||||||
|
"height": "162px",
|
||||||
|
"link": "/"
|
||||||
|
},
|
||||||
|
"tabNames": {
|
||||||
|
"api": "API",
|
||||||
|
"tutorials": "Examples"
|
||||||
|
},
|
||||||
|
"footerText": "Shared Editing",
|
||||||
|
"css": [
|
||||||
|
"./style.css"
|
||||||
|
],
|
||||||
|
"default": {
|
||||||
|
"staticFiles": {
|
||||||
|
"include": ["examples/"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opts": {
|
||||||
|
"destination": "./docs/",
|
||||||
|
"encoding": "utf8",
|
||||||
|
"private": false,
|
||||||
|
"recurse": true,
|
||||||
|
"template": "./node_modules/tui-jsdoc-template",
|
||||||
|
"tutorials": "./examples"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"no-inline-html": false
|
||||||
|
}
|
||||||
@@ -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,29 +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": {
|
|
||||||
"yjs": "latest",
|
|
||||||
"y-array": "latest",
|
|
||||||
"y-map": "latest",
|
|
||||||
"y-memory": "latest",
|
|
||||||
"y-richtext": "latest",
|
|
||||||
"y-webrtc": "latest",
|
|
||||||
"y-websockets-client": "latest",
|
|
||||||
"y-text": "latest",
|
|
||||||
"y-indexeddb": "latest",
|
|
||||||
"y-xml": "latest",
|
|
||||||
"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,18 +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="../bower_components/yjs/y.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/* global Y, chat */
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'chat-example'
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
|
||||||
chat: 'Array'
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.yChat = y
|
|
||||||
// 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))
|
|
||||||
document.querySelector('#chat').insertBefore(p, chat.children[position] || null)
|
|
||||||
}
|
|
||||||
// This function makes sure that only 7 messages exist in the chat history.
|
|
||||||
// The rest is deleted
|
|
||||||
function cleanupChat () {
|
|
||||||
if (y.share.chat.length > 7) {
|
|
||||||
y.share.chat.delete(0, y.chat.length - 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Insert the initial content
|
|
||||||
y.share.chat.toArray().forEach(appendMessage)
|
|
||||||
cleanupChat()
|
|
||||||
|
|
||||||
// whenever content changes, make sure to reflect the changes in the DOM
|
|
||||||
y.share.chat.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++) {
|
|
||||||
chat.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 (y.share.chat.length > 6) {
|
|
||||||
// If we are goint to insert the 8th element, make sure to delete first.
|
|
||||||
y.share.chat.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
|
|
||||||
y.share.chat.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,19 +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="../bower_components/yjs/y.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,23 +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="../bower_components/yjs/y.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'
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
|
||||||
piece1: 'Map',
|
|
||||||
piece2: 'Map',
|
|
||||||
piece3: 'Map',
|
|
||||||
piece4: 'Map'
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.yJigsaw = y
|
|
||||||
var origin // mouse start position - translation of piece
|
|
||||||
var drag = d3.behavior.drag()
|
|
||||||
.on('dragstart', function (params) {
|
|
||||||
// get the translation of the element
|
|
||||||
var translation = d3
|
|
||||||
.select(this)
|
|
||||||
.attr('transform')
|
|
||||||
.slice(10, -1)
|
|
||||||
.split(',')
|
|
||||||
.map(Number)
|
|
||||||
// mouse coordinates
|
|
||||||
var mouse = d3.mouse(this.parentNode)
|
|
||||||
origin = {
|
|
||||||
x: mouse[0] - translation[0],
|
|
||||||
y: mouse[1] - translation[1]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on('drag', function () {
|
|
||||||
var mouse = d3.mouse(this.parentNode)
|
|
||||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
|
||||||
var y = mouse[1] - origin.y
|
|
||||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
|
||||||
})
|
|
||||||
.on('dragend', function (piece, i) {
|
|
||||||
// save the current translation of the puzzle piece
|
|
||||||
var mouse = d3.mouse(this.parentNode)
|
|
||||||
var x = mouse[0] - origin.x
|
|
||||||
var y = mouse[1] - origin.y
|
|
||||||
piece.set('translation', {x: x, y: y})
|
|
||||||
})
|
|
||||||
|
|
||||||
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
|
||||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
|
||||||
|
|
||||||
pieces
|
|
||||||
.classed('draggable', true)
|
|
||||||
.attr('transform', function (piece) {
|
|
||||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
|
||||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
|
||||||
}).call(drag)
|
|
||||||
|
|
||||||
data.forEach(function (piece) {
|
|
||||||
piece.observe(function () {
|
|
||||||
// whenever a property of a piece changes, update the translation of the pieces
|
|
||||||
pieces
|
|
||||||
.transition()
|
|
||||||
.attr('transform', function (piece) {
|
|
||||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
|
||||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="monacoContainer"></div>
|
|
||||||
<style>
|
|
||||||
#monacoContainer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script src="../bower_components/yjs/y.js"></script>
|
|
||||||
<script src="../bower_components/y-array/y-array.js"></script>
|
|
||||||
<script src="../bower_components/y-text/y-text.js"></script>
|
|
||||||
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
|
|
||||||
<script src="../bower_components/y-memory/y-memory.js"></script>
|
|
||||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/* global Y, monaco */
|
|
||||||
|
|
||||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
|
||||||
|
|
||||||
require(['vs/editor/editor.main'], function () {
|
|
||||||
// Initialize a shared object. This function call returns a promise!
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: 'monaco-example'
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
|
||||||
monaco: 'Text' // y.share.monaco is of type Y.Text
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.yMonaco = y
|
|
||||||
|
|
||||||
// Create Monaco editor
|
|
||||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
|
||||||
language: 'javascript'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Bind to y.share.monaco
|
|
||||||
y.share.monaco.bindMonaco(editor)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
1173
examples/package-lock.json
generated
1173
examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "examples",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "",
|
|
||||||
"author": "Kevin Jahns",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"monaco-editor": "^0.8.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"standard": "^10.0.2"
|
|
||||||
},
|
|
||||||
"standard": {
|
|
||||||
"ignore": ["bower_components"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,39 +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'
|
|
||||||
},
|
|
||||||
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,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,12 +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/dist/y-text.js"></script>
|
|
||||||
<script src="../../../y-memory/y-memory.js"></script>
|
|
||||||
<script src="../../../y-websockets-client/dist/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-example'
|
|
||||||
// url: '127.0.0.1:1234'
|
|
||||||
},
|
|
||||||
sourceDir: '/bower_components',
|
|
||||||
share: {
|
|
||||||
textarea: 'Text' // y.share.textarea is of type Y.Text
|
|
||||||
}
|
|
||||||
}).then(function (y) {
|
|
||||||
window.yTextarea = y
|
|
||||||
|
|
||||||
// bind the textarea to a shared text element
|
|
||||||
y.share.textarea.bind(document.getElementById('textfield'))
|
|
||||||
// thats it..
|
|
||||||
})
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
</head>
|
|
||||||
<script src="../bower_components/yjs/y.js"></script>
|
|
||||||
<script src="../bower_components/jquery/dist/jquery.min.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,21 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
|
|
||||||
// initialize a shared object. This function call returns a promise!
|
|
||||||
Y({
|
|
||||||
db: {
|
|
||||||
name: 'memory'
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'websockets-client',
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
4580
package-lock.json
generated
4580
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@@ -1,65 +1,75 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.0.0-3",
|
"version": "13.0.6",
|
||||||
"description": "A framework for real-time p2p shared editing on any data",
|
"description": "Shared Editing Library",
|
||||||
"main": "./y.js",
|
"main": "./dist/yjs.cjs",
|
||||||
|
"module": "./dist/yjs.mjs",
|
||||||
|
"types": "./dist/src/index.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run dist && node ./dist/tests.cjs --repitition-time 50",
|
||||||
"lint": "standard",
|
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repitition-time 10000",
|
||||||
"dist": "rollup -c rollup.dist.js",
|
"dist": "rm -rf dist && rollup -c && tsc",
|
||||||
"serve": "concurrently 'serve ..' 'rollup -wc rollup.dist.js -o examples/bower_components/yjs/y.js'",
|
"watch": "rollup -wc",
|
||||||
"postversion": "npm run dist",
|
"lint": "markdownlint README.md && standard && tsc",
|
||||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||||
|
"serve-docs": "npm run docs && http-server ./docs/",
|
||||||
|
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repitition-time 1000",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"y.*"
|
"dist/*",
|
||||||
|
"src/*",
|
||||||
|
"tests/*",
|
||||||
|
"docs/*"
|
||||||
],
|
],
|
||||||
|
"dictionaries": {
|
||||||
|
"doc": "docs",
|
||||||
|
"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",
|
"shared editing",
|
||||||
"ShareJS",
|
"concurrency",
|
||||||
"Coweb",
|
"collaboration"
|
||||||
"Concurrency"
|
|
||||||
],
|
],
|
||||||
"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",
|
|
||||||
"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://yjs.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^2.6.8"
|
"lib0": "^0.2.26"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^11.0.1",
|
||||||
|
"@rollup/plugin-node-resolve": "^7.0.0",
|
||||||
|
"concurrently": "^3.6.1",
|
||||||
|
"http-server": "^0.12.1",
|
||||||
|
"jsdoc": "^3.6.3",
|
||||||
|
"markdownlint-cli": "^0.19.0",
|
||||||
|
"rollup": "^1.30.0",
|
||||||
|
"rollup-cli": "^1.0.9",
|
||||||
|
"standard": "^14.0.0",
|
||||||
|
"tui-jsdoc-template": "^1.2.2",
|
||||||
|
"typescript": "^3.7.5",
|
||||||
|
"y-protocols": "^0.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
rollup.config.js
Normal file
94
rollup.config.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import nodeResolve from '@rollup/plugin-node-resolve'
|
||||||
|
import commonjs from '@rollup/plugin-commonjs'
|
||||||
|
|
||||||
|
const localImports = process.env.LOCALIMPORTS
|
||||||
|
|
||||||
|
const customModules = new Set([
|
||||||
|
'y-websocket',
|
||||||
|
'y-codemirror',
|
||||||
|
'y-ace',
|
||||||
|
'y-textarea',
|
||||||
|
'y-quill',
|
||||||
|
'y-dom',
|
||||||
|
'y-prosemirror'
|
||||||
|
])
|
||||||
|
/**
|
||||||
|
* @type {Set<any>}
|
||||||
|
*/
|
||||||
|
const customLibModules = new Set([
|
||||||
|
'lib0',
|
||||||
|
'y-protocols'
|
||||||
|
])
|
||||||
|
const debugResolve = {
|
||||||
|
resolveId (importee) {
|
||||||
|
if (importee === 'yjs') {
|
||||||
|
return `${process.cwd()}/src/index.js`
|
||||||
|
}
|
||||||
|
if (localImports) {
|
||||||
|
if (customModules.has(importee.split('/')[0])) {
|
||||||
|
return `${process.cwd()}/../${importee}/src/${importee}.js`
|
||||||
|
}
|
||||||
|
if (customLibModules.has(importee.split('/')[0])) {
|
||||||
|
return `${process.cwd()}/../${importee}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
input: './src/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'Y',
|
||||||
|
file: 'dist/yjs.cjs',
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: true,
|
||||||
|
paths: path => {
|
||||||
|
if (/^lib0\//.test(path)) {
|
||||||
|
return `lib0/dist/${path.slice(5, -3)}.cjs`
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
},
|
||||||
|
external: id => /^lib0\//.test(id)
|
||||||
|
}, {
|
||||||
|
input: './src/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'Y',
|
||||||
|
file: 'dist/yjs.mjs',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
external: id => /^lib0\//.test(id)
|
||||||
|
}, {
|
||||||
|
input: './tests/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'test',
|
||||||
|
file: 'dist/tests.js',
|
||||||
|
format: 'iife',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
debugResolve,
|
||||||
|
nodeResolve({
|
||||||
|
mainFields: ['module', 'browser', 'main']
|
||||||
|
}),
|
||||||
|
commonjs()
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
input: './tests/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'test',
|
||||||
|
file: 'dist/tests.cjs',
|
||||||
|
format: 'cjs',
|
||||||
|
sourcemap: true
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
debugResolve,
|
||||||
|
nodeResolve({
|
||||||
|
mainFields: ['module', 'main']
|
||||||
|
}),
|
||||||
|
commonjs()
|
||||||
|
],
|
||||||
|
external: ['isomorphic.js']
|
||||||
|
}]
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import inject from 'rollup-plugin-inject'
|
|
||||||
import babel from 'rollup-plugin-babel'
|
|
||||||
import uglify from 'rollup-plugin-uglify'
|
|
||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
var pkg = require('./package.json')
|
|
||||||
|
|
||||||
export default {
|
|
||||||
entry: 'src/y.js',
|
|
||||||
moduleName: 'Y',
|
|
||||||
format: 'umd',
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
babel({
|
|
||||||
runtimeHelpers: true
|
|
||||||
}),
|
|
||||||
inject({
|
|
||||||
regeneratorRuntime: 'regenerator-runtime'
|
|
||||||
}),
|
|
||||||
uglify({
|
|
||||||
output: {
|
|
||||||
comments: function (node, comment) {
|
|
||||||
var text = comment.value
|
|
||||||
var type = comment.type
|
|
||||||
if (type === 'comment2') {
|
|
||||||
// multiline comment
|
|
||||||
return /@license/i.test(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
dest: 'y.js',
|
|
||||||
sourceMap: true,
|
|
||||||
banner: `
|
|
||||||
/**
|
|
||||||
* ${pkg.name} - ${pkg.description}
|
|
||||||
* @version v${pkg.version}
|
|
||||||
* @license ${pkg.license}
|
|
||||||
*/
|
|
||||||
`
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
|
||||||
import commonjs from 'rollup-plugin-commonjs'
|
|
||||||
import multiEntry from 'rollup-plugin-multi-entry'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
entry: 'tests/*.js',
|
|
||||||
moduleName: 'y-array-tests',
|
|
||||||
format: 'umd',
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({
|
|
||||||
main: true,
|
|
||||||
module: true,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
multiEntry()
|
|
||||||
],
|
|
||||||
dest: 'y-array.test.js',
|
|
||||||
sourceMap: true
|
|
||||||
}
|
|
||||||
493
src/Connector.js
493
src/Connector.js
@@ -1,493 +0,0 @@
|
|||||||
/* @flow */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
function canRead (auth) { return auth === 'read' || auth === 'write' }
|
|
||||||
function canWrite (auth) { return auth === 'write' }
|
|
||||||
|
|
||||||
export default function extendConnector (Y/* :any */) {
|
|
||||||
class AbstractConnector {
|
|
||||||
/* ::
|
|
||||||
y: YConfig;
|
|
||||||
role: SyncRole;
|
|
||||||
connections: Object;
|
|
||||||
isSynced: boolean;
|
|
||||||
userEventListeners: Array<Function>;
|
|
||||||
whenSyncedListeners: Array<Function>;
|
|
||||||
currentSyncTarget: ?UserId;
|
|
||||||
syncingClients: Array<UserId>;
|
|
||||||
forwardToSyncingClients: boolean;
|
|
||||||
debug: boolean;
|
|
||||||
syncStep2: Promise;
|
|
||||||
userId: UserId;
|
|
||||||
send: Function;
|
|
||||||
broadcast: Function;
|
|
||||||
broadcastOpBuffer: Array<Operation>;
|
|
||||||
protocolVersion: number;
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
opts contains the following information:
|
|
||||||
role : String Role of this client ("master" or "slave")
|
|
||||||
userId : String Uniquely defines the user.
|
|
||||||
debug: Boolean Whether to print debug messages (optional)
|
|
||||||
*/
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y
|
|
||||||
if (opts == null) {
|
|
||||||
opts = {}
|
|
||||||
}
|
|
||||||
// Prefer to receive untransformed operations. This does only work if
|
|
||||||
// this client receives operations from only one other client.
|
|
||||||
// In particular, this does not work with y-webrtc.
|
|
||||||
// It will work with y-websockets-client
|
|
||||||
this.preferUntransformed = opts.preferUntransformed || false
|
|
||||||
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 = Y.debug('y:connector')
|
|
||||||
this.logMessage = Y.debug('y:connector-message')
|
|
||||||
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false
|
|
||||||
this.role = opts.role
|
|
||||||
this.connections = {}
|
|
||||||
this.isSynced = false
|
|
||||||
this.userEventListeners = []
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.syncingClients = []
|
|
||||||
this.forwardToSyncingClients = opts.forwardToSyncingClients !== false
|
|
||||||
this.debug = opts.debug === true
|
|
||||||
this.broadcastOpBuffer = []
|
|
||||||
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.generateUserId !== false) {
|
|
||||||
this.setUserId(Y.utils.generateGuid())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resetAuth (auth) {
|
|
||||||
if (this.authInfo !== auth) {
|
|
||||||
this.authInfo = auth
|
|
||||||
this.broadcast({
|
|
||||||
type: 'auth',
|
|
||||||
auth: this.authInfo
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reconnect () {
|
|
||||||
this.log('reconnecting..')
|
|
||||||
return this.y.db.startGarbageCollector()
|
|
||||||
}
|
|
||||||
disconnect () {
|
|
||||||
this.log('discronnecting..')
|
|
||||||
this.connections = {}
|
|
||||||
this.isSynced = false
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.syncingClients = []
|
|
||||||
this.whenSyncedListeners = []
|
|
||||||
this.y.db.stopGarbageCollector()
|
|
||||||
return this.y.db.whenTransactionsFinished()
|
|
||||||
}
|
|
||||||
repair () {
|
|
||||||
this.log('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
|
|
||||||
for (var name in this.connections) {
|
|
||||||
this.connections[name].isSynced = false
|
|
||||||
}
|
|
||||||
this.isSynced = false
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.findNextSyncTarget()
|
|
||||||
}
|
|
||||||
setUserId (userId) {
|
|
||||||
if (this.userId == null) {
|
|
||||||
this.log('Set userId to "%s"', userId)
|
|
||||||
this.userId = userId
|
|
||||||
return this.y.db.setUserId(userId)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onUserEvent (f) {
|
|
||||||
this.userEventListeners.push(f)
|
|
||||||
}
|
|
||||||
removeUserEventListener (f) {
|
|
||||||
this.userEventListeners = this.userEventListeners.filter(g => f !== g)
|
|
||||||
}
|
|
||||||
userLeft (user) {
|
|
||||||
if (this.connections[user] != null) {
|
|
||||||
this.log('User left: %s', user)
|
|
||||||
delete this.connections[user]
|
|
||||||
if (user === this.currentSyncTarget) {
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.findNextSyncTarget()
|
|
||||||
}
|
|
||||||
this.syncingClients = this.syncingClients.filter(function (cli) {
|
|
||||||
return cli !== user
|
|
||||||
})
|
|
||||||
for (var f of this.userEventListeners) {
|
|
||||||
f({
|
|
||||||
action: 'userLeft',
|
|
||||||
user: user
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userJoined (user, role) {
|
|
||||||
if (role == null) {
|
|
||||||
throw new Error('You must specify the role of the joined user!')
|
|
||||||
}
|
|
||||||
if (this.connections[user] != null) {
|
|
||||||
throw new Error('This user already joined!')
|
|
||||||
}
|
|
||||||
this.log('User joined: %s', user)
|
|
||||||
this.connections[user] = {
|
|
||||||
isSynced: false,
|
|
||||||
role: role
|
|
||||||
}
|
|
||||||
let defer = {}
|
|
||||||
defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
|
|
||||||
this.connections[user].syncStep2 = defer
|
|
||||||
for (var f of this.userEventListeners) {
|
|
||||||
f({
|
|
||||||
action: 'userJoined',
|
|
||||||
user: user,
|
|
||||||
role: role
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (this.currentSyncTarget == null) {
|
|
||||||
this.findNextSyncTarget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Execute a function _when_ we are connected.
|
|
||||||
// If not connected, wait until connected
|
|
||||||
whenSynced (f) {
|
|
||||||
if (this.isSynced) {
|
|
||||||
f()
|
|
||||||
} else {
|
|
||||||
this.whenSyncedListeners.push(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findNextSyncTarget () {
|
|
||||||
if (this.currentSyncTarget != null) {
|
|
||||||
return // "The current sync has not finished!"
|
|
||||||
}
|
|
||||||
|
|
||||||
var syncUser = null
|
|
||||||
for (var uid in this.connections) {
|
|
||||||
if (!this.connections[uid].isSynced) {
|
|
||||||
syncUser = uid
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var conn = this
|
|
||||||
if (syncUser != null) {
|
|
||||||
this.currentSyncTarget = syncUser
|
|
||||||
this.y.db.requestTransaction(function * () {
|
|
||||||
var stateSet = yield * this.getStateSet()
|
|
||||||
// var deleteSet = yield * this.getDeleteSet()
|
|
||||||
var answer = {
|
|
||||||
type: 'sync step 1',
|
|
||||||
stateSet: stateSet,
|
|
||||||
// deleteSet: deleteSet,
|
|
||||||
protocolVersion: conn.protocolVersion,
|
|
||||||
auth: conn.authInfo
|
|
||||||
}
|
|
||||||
if (conn.preferUntransformed && Object.keys(stateSet).length === 0) {
|
|
||||||
answer.preferUntransformed = true
|
|
||||||
}
|
|
||||||
conn.send(syncUser, answer)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (!conn.isSynced) {
|
|
||||||
this.y.db.requestTransaction(function * () {
|
|
||||||
if (!conn.isSynced) {
|
|
||||||
// it is crucial that isSynced is set at the time garbageCollectAfterSync is called
|
|
||||||
conn.isSynced = true
|
|
||||||
// It is safer to remove this!
|
|
||||||
// TODO: remove: yield * this.garbageCollectAfterSync()
|
|
||||||
// call whensynced listeners
|
|
||||||
for (var f of conn.whenSyncedListeners) {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
conn.whenSyncedListeners = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send (uid, message) {
|
|
||||||
this.log('Send \'%s\' to %s', message.type, uid)
|
|
||||||
this.logMessage('Message: %j', message)
|
|
||||||
}
|
|
||||||
broadcast (message) {
|
|
||||||
this.log('Broadcast \'%s\'', message.type)
|
|
||||||
this.logMessage('Message: %j', message)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Buffer operations, and broadcast them when ready.
|
|
||||||
*/
|
|
||||||
broadcastOps (ops) {
|
|
||||||
ops = ops.map(function (op) {
|
|
||||||
return Y.Struct[op.struct].encode(op)
|
|
||||||
})
|
|
||||||
var self = this
|
|
||||||
function broadcastOperations () {
|
|
||||||
if (self.broadcastOpBuffer.length > 0) {
|
|
||||||
self.broadcast({
|
|
||||||
type: 'update',
|
|
||||||
ops: self.broadcastOpBuffer
|
|
||||||
})
|
|
||||||
self.broadcastOpBuffer = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.broadcastOpBuffer.length === 0) {
|
|
||||||
this.broadcastOpBuffer = ops
|
|
||||||
this.y.db.whenTransactionsFinished().then(broadcastOperations)
|
|
||||||
} else {
|
|
||||||
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
You received a raw message, and you know that it is intended for Yjs. Then call this function.
|
|
||||||
*/
|
|
||||||
receiveMessage (sender/* :UserId */, message/* :Message */) {
|
|
||||||
if (sender === this.userId) {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
this.log('Receive \'%s\' from %s', message.type, sender)
|
|
||||||
this.logMessage('Message: %j', message)
|
|
||||||
if (message.protocolVersion != null && message.protocolVersion !== this.protocolVersion) {
|
|
||||||
this.log(
|
|
||||||
`You tried to sync with a yjs instance that has a different protocol version
|
|
||||||
(You: ${this.protocolVersion}, Client: ${message.protocolVersion}).
|
|
||||||
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
|
|
||||||
`)
|
|
||||||
this.send(sender, {
|
|
||||||
type: 'sync stop',
|
|
||||||
protocolVersion: this.protocolVersion
|
|
||||||
})
|
|
||||||
return Promise.reject(new Error('Incompatible protocol version'))
|
|
||||||
}
|
|
||||||
if (message.auth != null && this.connections[sender] != null) {
|
|
||||||
// authenticate using auth in message
|
|
||||||
var auth = this.checkAuth(message.auth, this.y)
|
|
||||||
this.connections[sender].auth = auth
|
|
||||||
auth.then(auth => {
|
|
||||||
for (var f of this.userEventListeners) {
|
|
||||||
f({
|
|
||||||
action: 'userAuthenticated',
|
|
||||||
user: sender,
|
|
||||||
auth: auth
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (this.connections[sender] != null && this.connections[sender].auth == null) {
|
|
||||||
// authenticate without otherwise
|
|
||||||
this.connections[sender].auth = this.checkAuth(null, this.y)
|
|
||||||
}
|
|
||||||
if (this.connections[sender] != null && this.connections[sender].auth != null) {
|
|
||||||
return this.connections[sender].auth.then((auth) => {
|
|
||||||
if (message.type === 'sync step 1' && canRead(auth)) {
|
|
||||||
let conn = this
|
|
||||||
let m = message
|
|
||||||
let wait // wait for sync step 2 to complete
|
|
||||||
if (this.role === 'slave') {
|
|
||||||
wait = Promise.all(Object.keys(this.connections)
|
|
||||||
.map(uid => this.connections[uid])
|
|
||||||
.filter(conn => conn.role === 'master')
|
|
||||||
.map(conn => conn.syncStep2.promise)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
wait = Promise.resolve()
|
|
||||||
}
|
|
||||||
wait.then(() => {
|
|
||||||
this.y.db.requestTransaction(function * () {
|
|
||||||
var currentStateSet = yield * this.getStateSet()
|
|
||||||
// TODO: remove
|
|
||||||
// if (canWrite(auth)) {
|
|
||||||
// yield * this.applyDeleteSet(m.deleteSet)
|
|
||||||
// }
|
|
||||||
|
|
||||||
var ds = yield * this.getDeleteSet()
|
|
||||||
var answer = {
|
|
||||||
type: 'sync step 2',
|
|
||||||
stateSet: currentStateSet,
|
|
||||||
deleteSet: ds,
|
|
||||||
protocolVersion: this.protocolVersion,
|
|
||||||
auth: this.authInfo
|
|
||||||
}
|
|
||||||
if (message.preferUntransformed === true && Object.keys(m.stateSet).length === 0) {
|
|
||||||
answer.osUntransformed = yield * this.getOperationsUntransformed()
|
|
||||||
} else {
|
|
||||||
answer.os = yield * this.getOperations(m.stateSet)
|
|
||||||
}
|
|
||||||
conn.send(sender, answer)
|
|
||||||
if (this.forwardToSyncingClients) {
|
|
||||||
conn.syncingClients.push(sender)
|
|
||||||
setTimeout(function () {
|
|
||||||
conn.syncingClients = conn.syncingClients.filter(function (cli) {
|
|
||||||
return cli !== sender
|
|
||||||
})
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
|
||||||
}, 5000) // TODO: conn.syncingClientDuration)
|
|
||||||
} else {
|
|
||||||
conn.send(sender, {
|
|
||||||
type: 'sync done'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else if (message.type === 'sync step 2' && canWrite(auth)) {
|
|
||||||
var db = this.y.db
|
|
||||||
let defer = this.connections[sender].syncStep2
|
|
||||||
let m = message
|
|
||||||
// apply operations first
|
|
||||||
db.requestTransaction(function * () {
|
|
||||||
// yield * this.applyDeleteSet(m.deleteSet)
|
|
||||||
if (m.osUntransformed != null) {
|
|
||||||
yield * this.applyOperationsUntransformed(m.osUntransformed, m.stateSet)
|
|
||||||
} else {
|
|
||||||
this.store.apply(m.os)
|
|
||||||
}
|
|
||||||
// defer.resolve()
|
|
||||||
})
|
|
||||||
// then apply ds
|
|
||||||
db.whenTransactionsFinished().then(() => {
|
|
||||||
db.requestTransaction(function * () {
|
|
||||||
yield * this.applyDeleteSet(m.deleteSet)
|
|
||||||
})
|
|
||||||
defer.resolve()
|
|
||||||
})
|
|
||||||
return defer.promise
|
|
||||||
} else if (message.type === 'sync done') {
|
|
||||||
var self = this
|
|
||||||
this.connections[sender].syncStep2.promise.then(function () {
|
|
||||||
self._setSyncedWith(sender)
|
|
||||||
})
|
|
||||||
} else if (message.type === 'update' && canWrite(auth)) {
|
|
||||||
if (this.forwardToSyncingClients) {
|
|
||||||
for (var client of this.syncingClients) {
|
|
||||||
this.send(client, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.y.db.forwardAppliedOperations) {
|
|
||||||
var delops = message.ops.filter(function (o) {
|
|
||||||
return o.struct === 'Delete'
|
|
||||||
})
|
|
||||||
if (delops.length > 0) {
|
|
||||||
this.broadcastOps(delops)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.y.db.apply(message.ops)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return Promise.reject(new Error('Unable to deliver message'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_setSyncedWith (user) {
|
|
||||||
var conn = this.connections[user]
|
|
||||||
if (conn != null) {
|
|
||||||
conn.isSynced = true
|
|
||||||
}
|
|
||||||
if (user === this.currentSyncTarget) {
|
|
||||||
this.currentSyncTarget = null
|
|
||||||
this.findNextSyncTarget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
|
||||||
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
|
||||||
too much overhead. Y is very likely to get changed a lot in the future
|
|
||||||
|
|
||||||
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
|
|
||||||
we encode the JSON as XML.
|
|
||||||
|
|
||||||
When the HB support encoding as XML, the format should look pretty much like this.
|
|
||||||
|
|
||||||
does not support primitive values as array elements
|
|
||||||
expects an ltx (less than xml) object
|
|
||||||
*/
|
|
||||||
parseMessageFromXml (m/* :any */) {
|
|
||||||
function parseArray (node) {
|
|
||||||
for (var n of node.children) {
|
|
||||||
if (n.getAttribute('isArray') === 'true') {
|
|
||||||
return parseArray(n)
|
|
||||||
} else {
|
|
||||||
return parseObject(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function parseObject (node/* :any */) {
|
|
||||||
var json = {}
|
|
||||||
for (var attrName in node.attrs) {
|
|
||||||
var value = node.attrs[attrName]
|
|
||||||
var int = parseInt(value, 10)
|
|
||||||
if (isNaN(int) || ('' + int) !== value) {
|
|
||||||
json[attrName] = value
|
|
||||||
} else {
|
|
||||||
json[attrName] = int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var n/* :any */ in node.children) {
|
|
||||||
var name = n.name
|
|
||||||
if (n.getAttribute('isArray') === 'true') {
|
|
||||||
json[name] = parseArray(n)
|
|
||||||
} else {
|
|
||||||
json[name] = parseObject(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json
|
|
||||||
}
|
|
||||||
parseObject(m)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
encode message in xml
|
|
||||||
we use string because Strophe only accepts an "xml-string"..
|
|
||||||
So {a:4,b:{c:5}} will look like
|
|
||||||
<y a="4">
|
|
||||||
<b c="5"></b>
|
|
||||||
</y>
|
|
||||||
m - ltx element
|
|
||||||
json - Object
|
|
||||||
*/
|
|
||||||
encodeMessageToXml (msg, obj) {
|
|
||||||
// attributes is optional
|
|
||||||
function encodeObject (m, json) {
|
|
||||||
for (var name in json) {
|
|
||||||
var value = json[name]
|
|
||||||
if (name == null) {
|
|
||||||
// nop
|
|
||||||
} else if (value.constructor === Object) {
|
|
||||||
encodeObject(m.c(name), value)
|
|
||||||
} else if (value.constructor === Array) {
|
|
||||||
encodeArray(m.c(name), value)
|
|
||||||
} else {
|
|
||||||
m.setAttribute(name, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function encodeArray (m, array) {
|
|
||||||
m.setAttribute('isArray', 'true')
|
|
||||||
for (var e of array) {
|
|
||||||
if (e.constructor === Object) {
|
|
||||||
encodeObject(m.c('array-element'), e)
|
|
||||||
} else {
|
|
||||||
encodeArray(m.c('array-element'), e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (obj.constructor === Object) {
|
|
||||||
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
|
||||||
} else if (obj.constructor === Array) {
|
|
||||||
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj)
|
|
||||||
} else {
|
|
||||||
throw new Error("I can't encode this json!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.AbstractConnector = AbstractConnector
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
/* global getRandom, async */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function (Y) {
|
|
||||||
var globalRoom = {
|
|
||||||
users: {},
|
|
||||||
buffers: {},
|
|
||||||
removeUser: function (user) {
|
|
||||||
for (var i in this.users) {
|
|
||||||
this.users[i].userLeft(user)
|
|
||||||
}
|
|
||||||
delete this.users[user]
|
|
||||||
delete this.buffers[user]
|
|
||||||
},
|
|
||||||
addUser: function (connector) {
|
|
||||||
this.users[connector.userId] = connector
|
|
||||||
this.buffers[connector.userId] = {}
|
|
||||||
for (var uname in this.users) {
|
|
||||||
if (uname !== connector.userId) {
|
|
||||||
var u = this.users[uname]
|
|
||||||
u.userJoined(connector.userId, 'master')
|
|
||||||
connector.userJoined(u.userId, 'master')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
whenTransactionsFinished: function () {
|
|
||||||
var self = this
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
// The connector first has to send the messages to the db.
|
|
||||||
// Wait for the checkAuth-function to resolve
|
|
||||||
// The test lib only has a simple checkAuth function: `() => Promise.resolve()`
|
|
||||||
// Just add a function to the event-queue, in order to wait for the event.
|
|
||||||
// TODO: this may be buggy in test applications (but it isn't be for real-life apps)
|
|
||||||
setTimeout(function () {
|
|
||||||
var ps = []
|
|
||||||
for (var name in self.users) {
|
|
||||||
ps.push(self.users[name].y.db.whenTransactionsFinished())
|
|
||||||
}
|
|
||||||
Promise.all(ps).then(resolve, reject)
|
|
||||||
}, 10)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
flushOne: function flushOne () {
|
|
||||||
var bufs = []
|
|
||||||
for (var receiver in globalRoom.buffers) {
|
|
||||||
let buff = globalRoom.buffers[receiver]
|
|
||||||
var push = false
|
|
||||||
for (let sender in buff) {
|
|
||||||
if (buff[sender].length > 0) {
|
|
||||||
push = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (push) {
|
|
||||||
bufs.push(receiver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bufs.length > 0) {
|
|
||||||
var userId = getRandom(bufs)
|
|
||||||
let buff = globalRoom.buffers[userId]
|
|
||||||
let sender = getRandom(Object.keys(buff))
|
|
||||||
var m = buff[sender].shift()
|
|
||||||
if (buff[sender].length === 0) {
|
|
||||||
delete buff[sender]
|
|
||||||
}
|
|
||||||
var user = globalRoom.users[userId]
|
|
||||||
return user.receiveMessage(m[0], m[1]).then(function () {
|
|
||||||
return user.y.db.whenTransactionsFinished()
|
|
||||||
}, function () {})
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
flushAll: function () {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
// flushes may result in more created operations,
|
|
||||||
// flush until there is nothing more to flush
|
|
||||||
function nextFlush () {
|
|
||||||
var c = globalRoom.flushOne()
|
|
||||||
if (c) {
|
|
||||||
while (c) {
|
|
||||||
c = globalRoom.flushOne()
|
|
||||||
}
|
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
|
||||||
} else {
|
|
||||||
c = globalRoom.flushOne()
|
|
||||||
if (c) {
|
|
||||||
c.then(function () {
|
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.globalRoom = globalRoom
|
|
||||||
|
|
||||||
var userIdCounter = 0
|
|
||||||
|
|
||||||
class Test extends Y.AbstractConnector {
|
|
||||||
constructor (y, options) {
|
|
||||||
if (options === undefined) {
|
|
||||||
throw new Error('Options must not be undefined!')
|
|
||||||
}
|
|
||||||
options.role = 'master'
|
|
||||||
options.forwardToSyncingClients = false
|
|
||||||
super(y, options)
|
|
||||||
this.setUserId((userIdCounter++) + '').then(() => {
|
|
||||||
globalRoom.addUser(this)
|
|
||||||
})
|
|
||||||
this.globalRoom = globalRoom
|
|
||||||
this.syncingClientDuration = 0
|
|
||||||
}
|
|
||||||
receiveMessage (sender, m) {
|
|
||||||
return super.receiveMessage(sender, JSON.parse(JSON.stringify(m)))
|
|
||||||
}
|
|
||||||
send (userId, message) {
|
|
||||||
var buffer = globalRoom.buffers[userId]
|
|
||||||
if (buffer != null) {
|
|
||||||
if (buffer[this.userId] == null) {
|
|
||||||
buffer[this.userId] = []
|
|
||||||
}
|
|
||||||
buffer[this.userId].push(JSON.parse(JSON.stringify([this.userId, message])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
broadcast (message) {
|
|
||||||
for (var key in globalRoom.buffers) {
|
|
||||||
var buff = globalRoom.buffers[key]
|
|
||||||
if (buff[this.userId] == null) {
|
|
||||||
buff[this.userId] = []
|
|
||||||
}
|
|
||||||
buff[this.userId].push(JSON.parse(JSON.stringify([this.userId, message])))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDisconnected () {
|
|
||||||
return globalRoom.users[this.userId] == null
|
|
||||||
}
|
|
||||||
reconnect () {
|
|
||||||
if (this.isDisconnected()) {
|
|
||||||
globalRoom.addUser(this)
|
|
||||||
super.reconnect()
|
|
||||||
}
|
|
||||||
return Y.utils.globalRoom.flushAll()
|
|
||||||
}
|
|
||||||
disconnect () {
|
|
||||||
var waitForMe = Promise.resolve()
|
|
||||||
if (!this.isDisconnected()) {
|
|
||||||
globalRoom.removeUser(this.userId)
|
|
||||||
waitForMe = super.disconnect()
|
|
||||||
}
|
|
||||||
var self = this
|
|
||||||
return waitForMe.then(function () {
|
|
||||||
return self.y.db.whenTransactionsFinished()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
flush () {
|
|
||||||
var self = this
|
|
||||||
return async(function * () {
|
|
||||||
var buff = globalRoom.buffers[self.userId]
|
|
||||||
while (Object.keys(buff).length > 0) {
|
|
||||||
var sender = getRandom(Object.keys(buff))
|
|
||||||
var m = buff[sender].shift()
|
|
||||||
if (buff[sender].length === 0) {
|
|
||||||
delete buff[sender]
|
|
||||||
}
|
|
||||||
yield this.receiveMessage(m[0], m[1])
|
|
||||||
}
|
|
||||||
yield self.whenTransactionsFinished()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.Test = Test
|
|
||||||
}
|
|
||||||
605
src/Database.js
605
src/Database.js
@@ -1,605 +0,0 @@
|
|||||||
/* @flow */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
export default function extendDatabase (Y /* :any */) {
|
|
||||||
/*
|
|
||||||
Partial definition of an OperationStore.
|
|
||||||
TODO: name it Database, operation store only holds operations.
|
|
||||||
|
|
||||||
A database definition must alse define the following methods:
|
|
||||||
* logTable() (optional)
|
|
||||||
- show relevant information information in a table
|
|
||||||
* requestTransaction(makeGen)
|
|
||||||
- request a transaction
|
|
||||||
* destroy()
|
|
||||||
- destroy the database
|
|
||||||
*/
|
|
||||||
class AbstractDatabase {
|
|
||||||
/* ::
|
|
||||||
y: YConfig;
|
|
||||||
forwardAppliedOperations: boolean;
|
|
||||||
listenersById: Object;
|
|
||||||
listenersByIdExecuteNow: Array<Object>;
|
|
||||||
listenersByIdRequestPending: boolean;
|
|
||||||
initializedTypes: Object;
|
|
||||||
whenUserIdSetListener: ?Function;
|
|
||||||
waitingTransactions: Array<Transaction>;
|
|
||||||
transactionInProgress: boolean;
|
|
||||||
executeOrder: Array<Object>;
|
|
||||||
gc1: Array<Struct>;
|
|
||||||
gc2: Array<Struct>;
|
|
||||||
gcTimeout: number;
|
|
||||||
gcInterval: any;
|
|
||||||
garbageCollect: Function;
|
|
||||||
executeOrder: Array<any>; // for debugging only
|
|
||||||
userId: UserId;
|
|
||||||
opClock: number;
|
|
||||||
transactionsFinished: ?{promise: Promise, resolve: any};
|
|
||||||
transact: (x: ?Generator) => any;
|
|
||||||
*/
|
|
||||||
constructor (y, opts) {
|
|
||||||
this.y = y
|
|
||||||
opts.gc = opts.gc === true
|
|
||||||
this.dbOpts = opts
|
|
||||||
var os = this
|
|
||||||
this.userId = null
|
|
||||||
var resolve_
|
|
||||||
this.userIdPromise = new Promise(function (resolve) {
|
|
||||||
resolve_ = resolve
|
|
||||||
})
|
|
||||||
this.userIdPromise.resolve = resolve_
|
|
||||||
// whether to broadcast all applied operations (insert & delete hook)
|
|
||||||
this.forwardAppliedOperations = false
|
|
||||||
// E.g. this.listenersById[id] : Array<Listener>
|
|
||||||
this.listenersById = {}
|
|
||||||
// Execute the next time a transaction is requested
|
|
||||||
this.listenersByIdExecuteNow = []
|
|
||||||
// A transaction is requested
|
|
||||||
this.listenersByIdRequestPending = false
|
|
||||||
/* To make things more clear, the following naming conventions:
|
|
||||||
* ls : we put this.listenersById on ls
|
|
||||||
* l : Array<Listener>
|
|
||||||
* id : Id (can't use as property name)
|
|
||||||
* sid : String (converted from id via JSON.stringify
|
|
||||||
so we can use it as a property name)
|
|
||||||
|
|
||||||
Always remember to first overwrite
|
|
||||||
a property before you iterate over it!
|
|
||||||
*/
|
|
||||||
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
|
||||||
// wont be kept in memory.
|
|
||||||
this.initializedTypes = {}
|
|
||||||
this.waitingTransactions = []
|
|
||||||
this.transactionInProgress = false
|
|
||||||
this.transactionIsFlushed = false
|
|
||||||
if (typeof YConcurrencyTestingMode !== 'undefined') {
|
|
||||||
this.executeOrder = []
|
|
||||||
}
|
|
||||||
this.gc1 = [] // first stage
|
|
||||||
this.gc2 = [] // second stage -> after that, remove the op
|
|
||||||
|
|
||||||
function garbageCollect () {
|
|
||||||
return os.whenTransactionsFinished().then(function () {
|
|
||||||
if (os.gcTimeout > 0 && (os.gc1.length > 0 || os.gc2.length > 0)) {
|
|
||||||
if (!os.y.connector.isSynced) {
|
|
||||||
console.warn('gc should be empty when not synced!')
|
|
||||||
}
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
os.requestTransaction(function * () {
|
|
||||||
if (os.y.connector != null && os.y.connector.isSynced) {
|
|
||||||
for (var i = 0; i < os.gc2.length; i++) {
|
|
||||||
var oid = os.gc2[i]
|
|
||||||
yield * this.garbageCollectOperation(oid)
|
|
||||||
}
|
|
||||||
os.gc2 = os.gc1
|
|
||||||
os.gc1 = []
|
|
||||||
}
|
|
||||||
// TODO: Use setInterval here instead (when garbageCollect is called several times there will be several timeouts..)
|
|
||||||
if (os.gcTimeout > 0) {
|
|
||||||
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// TODO: see above
|
|
||||||
if (os.gcTimeout > 0) {
|
|
||||||
os.gcInterval = setTimeout(garbageCollect, os.gcTimeout)
|
|
||||||
}
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.garbageCollect = garbageCollect
|
|
||||||
this.startGarbageCollector()
|
|
||||||
|
|
||||||
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
|
|
||||||
this.opsReceivedTimestamp = new Date()
|
|
||||||
this.startRepairCheck()
|
|
||||||
}
|
|
||||||
startGarbageCollector () {
|
|
||||||
this.gc = this.dbOpts.gc
|
|
||||||
if (this.gc) {
|
|
||||||
this.gcTimeout = !this.dbOpts.gcTimeout ? 100000 : this.dbOpts.gcTimeout
|
|
||||||
} else {
|
|
||||||
this.gcTimeout = -1
|
|
||||||
}
|
|
||||||
if (this.gcTimeout > 0) {
|
|
||||||
this.garbageCollect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startRepairCheck () {
|
|
||||||
var os = this
|
|
||||||
if (this.repairCheckInterval > 0) {
|
|
||||||
this.repairCheckIntervalHandler = setInterval(function repairOnMissingOperations () {
|
|
||||||
/*
|
|
||||||
Case 1. No ops have been received in a while (new Date() - os.opsReceivedTimestamp > os.repairCheckInterval)
|
|
||||||
- 1.1 os.listenersById is empty. Then the state was correct the whole time. -> Nothing to do (nor to update)
|
|
||||||
- 1.2 os.listenersById is not empty.
|
|
||||||
* Then the state was incorrect for at least {os.repairCheckInterval} seconds.
|
|
||||||
* -> Remove everything in os.listenersById and sync again (connector.repair())
|
|
||||||
Case 2. An op has been received in the last {os.repairCheckInterval } seconds.
|
|
||||||
It is not yet necessary to check for faulty behavior. Everything can still resolve itself. Wait for more messages.
|
|
||||||
If nothing was received for a while and os.listenersById is still not emty, we are in case 1.2
|
|
||||||
-> Do nothing
|
|
||||||
|
|
||||||
Baseline here is: we really only have to catch case 1.2..
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
new Date() - os.opsReceivedTimestamp > os.repairCheckInterval &&
|
|
||||||
Object.keys(os.listenersById).length > 0 // os.listenersById is not empty
|
|
||||||
) {
|
|
||||||
// haven't received operations for over {os.repairCheckInterval} seconds, resend state vector
|
|
||||||
os.listenersById = {}
|
|
||||||
os.opsReceivedTimestamp = new Date() // update so you don't send repair several times in a row
|
|
||||||
os.y.connector.repair()
|
|
||||||
}
|
|
||||||
}, this.repairCheckInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stopRepairCheck () {
|
|
||||||
clearInterval(this.repairCheckIntervalHandler)
|
|
||||||
}
|
|
||||||
queueGarbageCollector (id) {
|
|
||||||
if (this.y.connector.isSynced && this.gc) {
|
|
||||||
this.gc1.push(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emptyGarbageCollector () {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
var check = () => {
|
|
||||||
if (this.gc1.length > 0 || this.gc2.length > 0) {
|
|
||||||
this.garbageCollect().then(check)
|
|
||||||
} else {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTimeout(check, 0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
addToDebug () {
|
|
||||||
if (typeof YConcurrencyTestingMode !== 'undefined') {
|
|
||||||
var command /* :string */ = Array.prototype.map.call(arguments, function (s) {
|
|
||||||
if (typeof s === 'string') {
|
|
||||||
return s
|
|
||||||
} else {
|
|
||||||
return JSON.stringify(s)
|
|
||||||
}
|
|
||||||
}).join('').replace(/"/g, "'").replace(/,/g, ', ').replace(/:/g, ': ')
|
|
||||||
this.executeOrder.push(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getDebugData () {
|
|
||||||
console.log(this.executeOrder.join('\n'))
|
|
||||||
}
|
|
||||||
stopGarbageCollector () {
|
|
||||||
var self = this
|
|
||||||
this.gc = false
|
|
||||||
this.gcTimeout = -1
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
self.requestTransaction(function * () {
|
|
||||||
var ungc /* :Array<Struct> */ = self.gc1.concat(self.gc2)
|
|
||||||
self.gc1 = []
|
|
||||||
self.gc2 = []
|
|
||||||
for (var i = 0; i < ungc.length; i++) {
|
|
||||||
var op = yield * this.getOperation(ungc[i])
|
|
||||||
if (op != null) {
|
|
||||||
delete op.gc
|
|
||||||
yield * this.setOperation(op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Try to add to GC.
|
|
||||||
|
|
||||||
TODO: rename this function
|
|
||||||
|
|
||||||
Rulez:
|
|
||||||
* Only gc if this user is online & gc turned on
|
|
||||||
* The most left element in a list must not be gc'd.
|
|
||||||
=> There is at least one element in the list
|
|
||||||
|
|
||||||
returns true iff op was added to GC
|
|
||||||
*/
|
|
||||||
* addToGarbageCollector (op, left) {
|
|
||||||
if (
|
|
||||||
op.gc == null &&
|
|
||||||
op.deleted === true &&
|
|
||||||
this.store.gc &&
|
|
||||||
this.store.y.connector.isSynced
|
|
||||||
) {
|
|
||||||
var gc = false
|
|
||||||
if (left != null && left.deleted === true) {
|
|
||||||
gc = true
|
|
||||||
} else if (op.content != null && op.content.length > 1) {
|
|
||||||
op = yield * this.getInsertionCleanStart([op.id[0], op.id[1] + 1])
|
|
||||||
gc = true
|
|
||||||
}
|
|
||||||
if (gc) {
|
|
||||||
op.gc = true
|
|
||||||
yield * this.setOperation(op)
|
|
||||||
this.store.queueGarbageCollector(op.id)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
removeFromGarbageCollector (op) {
|
|
||||||
function filter (o) {
|
|
||||||
return !Y.utils.compareIds(o, op.id)
|
|
||||||
}
|
|
||||||
this.gc1 = this.gc1.filter(filter)
|
|
||||||
this.gc2 = this.gc2.filter(filter)
|
|
||||||
delete op.gc
|
|
||||||
}
|
|
||||||
destroyTypes () {
|
|
||||||
for (var key in this.initializedTypes) {
|
|
||||||
var type = this.initializedTypes[key]
|
|
||||||
if (type._destroy != null) {
|
|
||||||
type._destroy()
|
|
||||||
} else {
|
|
||||||
console.error('The type you included does not provide destroy functionality, it will remain in memory (updating your packages will help).')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
* destroy () {
|
|
||||||
clearTimeout(this.gcInterval)
|
|
||||||
this.gcInterval = null
|
|
||||||
this.stopRepairCheck()
|
|
||||||
}
|
|
||||||
setUserId (userId) {
|
|
||||||
if (!this.userIdPromise.inProgress) {
|
|
||||||
this.userIdPromise.inProgress = true
|
|
||||||
var self = this
|
|
||||||
self.requestTransaction(function * () {
|
|
||||||
self.userId = userId
|
|
||||||
var state = yield * this.getState(userId)
|
|
||||||
self.opClock = state.clock
|
|
||||||
self.userIdPromise.resolve(userId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return this.userIdPromise
|
|
||||||
}
|
|
||||||
whenUserIdSet (f) {
|
|
||||||
this.userIdPromise.then(f)
|
|
||||||
}
|
|
||||||
getNextOpId (numberOfIds) {
|
|
||||||
if (numberOfIds == null) {
|
|
||||||
throw new Error('getNextOpId expects the number of created ids to create!')
|
|
||||||
} else if (this.userId == null) {
|
|
||||||
throw new Error('OperationStore not yet initialized!')
|
|
||||||
} else {
|
|
||||||
var id = [this.userId, this.opClock]
|
|
||||||
this.opClock += numberOfIds
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Apply a list of operations.
|
|
||||||
|
|
||||||
* we save a timestamp, because we received new operations that could resolve ops in this.listenersById (see this.startRepairCheck)
|
|
||||||
* get a transaction
|
|
||||||
* check whether all Struct.*.requiredOps are in the OS
|
|
||||||
* check if it is an expected op (otherwise wait for it)
|
|
||||||
* check if was deleted, apply a delete operation after op was applied
|
|
||||||
*/
|
|
||||||
apply (ops) {
|
|
||||||
this.opsReceivedTimestamp = new Date()
|
|
||||||
for (var i = 0; i < ops.length; i++) {
|
|
||||||
var o = ops[i]
|
|
||||||
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
|
||||||
var required = Y.Struct[o.struct].requiredOps(o)
|
|
||||||
if (o.requires != null) {
|
|
||||||
required = required.concat(o.requires)
|
|
||||||
}
|
|
||||||
this.whenOperationsExist(required, o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
op is executed as soon as every operation requested is available.
|
|
||||||
Note that Transaction can (and should) buffer requests.
|
|
||||||
*/
|
|
||||||
whenOperationsExist (ids, op) {
|
|
||||||
if (ids.length > 0) {
|
|
||||||
let listener = {
|
|
||||||
op: op,
|
|
||||||
missing: ids.length
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < ids.length; i++) {
|
|
||||||
let id = ids[i]
|
|
||||||
let sid = JSON.stringify(id)
|
|
||||||
let l = this.listenersById[sid]
|
|
||||||
if (l == null) {
|
|
||||||
l = []
|
|
||||||
this.listenersById[sid] = l
|
|
||||||
}
|
|
||||||
l.push(listener)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.listenersByIdExecuteNow.push({
|
|
||||||
op: op
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.listenersByIdRequestPending) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listenersByIdRequestPending = true
|
|
||||||
var store = this
|
|
||||||
|
|
||||||
this.requestTransaction(function * () {
|
|
||||||
var exeNow = store.listenersByIdExecuteNow
|
|
||||||
store.listenersByIdExecuteNow = []
|
|
||||||
|
|
||||||
var ls = store.listenersById
|
|
||||||
store.listenersById = {}
|
|
||||||
|
|
||||||
store.listenersByIdRequestPending = false
|
|
||||||
|
|
||||||
for (let key = 0; key < exeNow.length; key++) {
|
|
||||||
let o = exeNow[key].op
|
|
||||||
yield * store.tryExecute.call(this, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var sid in ls) {
|
|
||||||
var l = ls[sid]
|
|
||||||
var id = JSON.parse(sid)
|
|
||||||
var op
|
|
||||||
if (typeof id[1] === 'string') {
|
|
||||||
op = yield * this.getOperation(id)
|
|
||||||
} else {
|
|
||||||
op = yield * this.getInsertion(id)
|
|
||||||
}
|
|
||||||
if (op == null) {
|
|
||||||
store.listenersById[sid] = l
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < l.length; i++) {
|
|
||||||
let listener = l[i]
|
|
||||||
let o = listener.op
|
|
||||||
if (--listener.missing === 0) {
|
|
||||||
yield * store.tryExecute.call(this, o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Actually execute an operation, when all expected operations are available.
|
|
||||||
*/
|
|
||||||
/* :: // TODO: this belongs somehow to transaction
|
|
||||||
store: Object;
|
|
||||||
getOperation: any;
|
|
||||||
isGarbageCollected: any;
|
|
||||||
addOperation: any;
|
|
||||||
whenOperationsExist: any;
|
|
||||||
*/
|
|
||||||
* tryExecute (op) {
|
|
||||||
this.store.addToDebug('yield * this.store.tryExecute.call(this, ', JSON.stringify(op), ')')
|
|
||||||
if (op.struct === 'Delete') {
|
|
||||||
yield * Y.Struct.Delete.execute.call(this, op)
|
|
||||||
// this is now called in Transaction.deleteOperation!
|
|
||||||
// yield * this.store.operationAdded(this, op)
|
|
||||||
} else {
|
|
||||||
// check if this op was defined
|
|
||||||
var defined = yield * this.getInsertion(op.id)
|
|
||||||
while (defined != null && defined.content != null) {
|
|
||||||
// check if this op has a longer content in the case it is defined
|
|
||||||
if (defined.id[1] + defined.content.length < op.id[1] + op.content.length) {
|
|
||||||
var overlapSize = defined.content.length - (op.id[1] - defined.id[1])
|
|
||||||
op.content.splice(0, overlapSize)
|
|
||||||
op.id = [op.id[0], op.id[1] + overlapSize]
|
|
||||||
op.left = Y.utils.getLastId(defined)
|
|
||||||
op.origin = op.left
|
|
||||||
defined = yield * this.getOperation(op.id) // getOperation suffices here
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (defined == null) {
|
|
||||||
var opid = op.id
|
|
||||||
var isGarbageCollected = yield * this.isGarbageCollected(opid)
|
|
||||||
if (!isGarbageCollected) {
|
|
||||||
// TODO: reduce number of get / put calls for op ..
|
|
||||||
yield * Y.Struct[op.struct].execute.call(this, op)
|
|
||||||
yield * this.addOperation(op)
|
|
||||||
yield * this.store.operationAdded(this, op)
|
|
||||||
// operationAdded can change op..
|
|
||||||
op = yield * this.getOperation(opid)
|
|
||||||
// if insertion, try to combine with left
|
|
||||||
yield * this.tryCombineWithLeft(op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Called by a transaction when an operation is added.
|
|
||||||
* This function is especially important for y-indexeddb, where several instances may share a single database.
|
|
||||||
* Every time an operation is created by one instance, it is send to all other instances and operationAdded is called
|
|
||||||
*
|
|
||||||
* If it's not a Delete operation:
|
|
||||||
* * Checks if another operation is executable (listenersById)
|
|
||||||
* * Update state, if possible
|
|
||||||
*
|
|
||||||
* Always:
|
|
||||||
* * Call type
|
|
||||||
*/
|
|
||||||
* operationAdded (transaction, op) {
|
|
||||||
if (op.struct === 'Delete') {
|
|
||||||
var type = this.initializedTypes[JSON.stringify(op.targetParent)]
|
|
||||||
if (type != null) {
|
|
||||||
yield * type._changed(transaction, op)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// increase SS
|
|
||||||
yield * transaction.updateState(op.id[0])
|
|
||||||
var opLen = op.content != null ? op.content.length : 1
|
|
||||||
for (let i = 0; i < opLen; i++) {
|
|
||||||
// notify whenOperation listeners (by id)
|
|
||||||
var sid = JSON.stringify([op.id[0], op.id[1] + i])
|
|
||||||
var l = this.listenersById[sid]
|
|
||||||
delete this.listenersById[sid]
|
|
||||||
if (l != null) {
|
|
||||||
for (var key in l) {
|
|
||||||
var listener = l[key]
|
|
||||||
if (--listener.missing === 0) {
|
|
||||||
this.whenOperationsExist([], listener.op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var t = this.initializedTypes[JSON.stringify(op.parent)]
|
|
||||||
|
|
||||||
// if parent is deleted, mark as gc'd and return
|
|
||||||
if (op.parent != null) {
|
|
||||||
var parentIsDeleted = yield * transaction.isDeleted(op.parent)
|
|
||||||
if (parentIsDeleted) {
|
|
||||||
yield * transaction.deleteList(op.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notify parent, if it was instanciated as a custom type
|
|
||||||
if (t != null) {
|
|
||||||
let o = Y.utils.copyOperation(op)
|
|
||||||
yield * t._changed(transaction, o)
|
|
||||||
}
|
|
||||||
if (!op.deleted) {
|
|
||||||
// Delete if DS says this is actually deleted
|
|
||||||
var len = op.content != null ? op.content.length : 1
|
|
||||||
var startId = op.id // You must not use op.id in the following loop, because op will change when deleted
|
|
||||||
// TODO: !! console.log('TODO: change this before commiting')
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
var id = [startId[0], startId[1] + i]
|
|
||||||
var opIsDeleted = yield * transaction.isDeleted(id)
|
|
||||||
if (opIsDeleted) {
|
|
||||||
var delop = {
|
|
||||||
struct: 'Delete',
|
|
||||||
target: id
|
|
||||||
}
|
|
||||||
yield * this.tryExecute.call(transaction, delop)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
whenTransactionsFinished () {
|
|
||||||
if (this.transactionInProgress) {
|
|
||||||
if (this.transactionsFinished == null) {
|
|
||||||
var resolve_
|
|
||||||
var promise = new Promise(function (resolve) {
|
|
||||||
resolve_ = resolve
|
|
||||||
})
|
|
||||||
this.transactionsFinished = {
|
|
||||||
resolve: resolve_,
|
|
||||||
promise: promise
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.transactionsFinished.promise
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check if there is another transaction request.
|
|
||||||
// * the last transaction is always a flush :)
|
|
||||||
getNextRequest () {
|
|
||||||
if (this.waitingTransactions.length === 0) {
|
|
||||||
if (this.transactionIsFlushed) {
|
|
||||||
this.transactionInProgress = false
|
|
||||||
this.transactionIsFlushed = false
|
|
||||||
if (this.transactionsFinished != null) {
|
|
||||||
this.transactionsFinished.resolve()
|
|
||||||
this.transactionsFinished = null
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
this.transactionIsFlushed = true
|
|
||||||
return function * () {
|
|
||||||
yield * this.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.transactionIsFlushed = false
|
|
||||||
return this.waitingTransactions.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestTransaction (makeGen/* :any */, callImmediately) {
|
|
||||||
this.waitingTransactions.push(makeGen)
|
|
||||||
if (!this.transactionInProgress) {
|
|
||||||
this.transactionInProgress = true
|
|
||||||
setTimeout(() => {
|
|
||||||
this.transact(this.getNextRequest())
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Get a created/initialized type.
|
|
||||||
*/
|
|
||||||
getType (id) {
|
|
||||||
return this.initializedTypes[JSON.stringify(id)]
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Init type. This is called when a remote operation is retrieved, and transformed to a type
|
|
||||||
TODO: delete type from store.initializedTypes[id] when corresponding id was deleted!
|
|
||||||
*/
|
|
||||||
* initType (id, args) {
|
|
||||||
var sid = JSON.stringify(id)
|
|
||||||
var t = this.store.initializedTypes[sid]
|
|
||||||
if (t == null) {
|
|
||||||
var op/* :MapStruct | ListStruct */ = yield * this.getOperation(id)
|
|
||||||
if (op != null) {
|
|
||||||
t = yield * Y[op.type].typeDefinition.initType.call(this, this.store, op, args)
|
|
||||||
this.store.initializedTypes[sid] = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Create type. This is called when the local user creates a type (which is a synchronous action)
|
|
||||||
*/
|
|
||||||
createType (typedefinition, id) {
|
|
||||||
var structname = typedefinition[0].struct
|
|
||||||
id = id || this.getNextOpId(1)
|
|
||||||
var op = Y.Struct[structname].create(id)
|
|
||||||
op.type = typedefinition[0].name
|
|
||||||
|
|
||||||
this.requestTransaction(function * () {
|
|
||||||
if (op.id[0] === '_') {
|
|
||||||
yield * this.setOperation(op)
|
|
||||||
} else {
|
|
||||||
yield * this.applyCreatedOperations([op])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
var t = Y[op.type].typeDefinition.createType(this, op, typedefinition[1])
|
|
||||||
this.initializedTypes[JSON.stringify(op.id)] = t
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.AbstractDatabase = AbstractDatabase
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
/* global async, databases, describe, beforeEach, afterEach */
|
|
||||||
/* eslint-env browser,jasmine,console */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
var Y = require('./SpecHelper.js')
|
|
||||||
|
|
||||||
for (let database of databases) {
|
|
||||||
describe(`Database (${database})`, function () {
|
|
||||||
var store
|
|
||||||
describe('DeleteStore', function () {
|
|
||||||
describe('Basic', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
store = new Y[database](null, {
|
|
||||||
gcTimeout: -1,
|
|
||||||
namespace: 'testing'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
afterEach(function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.store.destroy()
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('Deleted operation is deleted', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['u1', 10], 1)
|
|
||||||
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 1, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['u1', 10], 1)
|
|
||||||
yield * this.markDeleted(['u1', 11], 1)
|
|
||||||
expect(yield * this.isDeleted(['u1', 10])).toBeTruthy()
|
|
||||||
expect(yield * this.isDeleted(['u1', 11])).toBeTruthy()
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'u1': [[10, 2, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Deleted operation extends other deleted operation', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['0', 3], 1)
|
|
||||||
yield * this.markDeleted(['0', 4], 1)
|
|
||||||
yield * this.markDeleted(['0', 2], 1)
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'0': [[2, 3, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #1', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['166', 0], 1)
|
|
||||||
yield * this.markDeleted(['166', 2], 1)
|
|
||||||
yield * this.markDeleted(['166', 0], 1)
|
|
||||||
yield * this.markDeleted(['166', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['166', 2], 1)
|
|
||||||
yield * this.markDeleted(['166', 1], 1)
|
|
||||||
yield * this.markDeleted(['166', 3], 1)
|
|
||||||
yield * this.markGarbageCollected(['166', 3], 1)
|
|
||||||
yield * this.markDeleted(['166', 0], 1)
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'166': [[0, 2, false], [2, 2, true]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #2', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['293', 0], 1)
|
|
||||||
yield * this.markDeleted(['291', 2], 1)
|
|
||||||
yield * this.markDeleted(['291', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['293', 0], 1)
|
|
||||||
yield * this.markDeleted(['293', 1], 1)
|
|
||||||
yield * this.markGarbageCollected(['291', 2], 1)
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'291': [[2, 1, true]], '293': [[0, 1, true], [1, 1, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #3', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['581', 0], 1)
|
|
||||||
yield * this.markDeleted(['581', 1], 1)
|
|
||||||
yield * this.markDeleted(['580', 0], 1)
|
|
||||||
yield * this.markDeleted(['580', 0], 1)
|
|
||||||
yield * this.markGarbageCollected(['581', 0], 1)
|
|
||||||
yield * this.markDeleted(['581', 2], 1)
|
|
||||||
yield * this.markDeleted(['580', 1], 1)
|
|
||||||
yield * this.markDeleted(['580', 2], 1)
|
|
||||||
yield * this.markDeleted(['580', 1], 1)
|
|
||||||
yield * this.markDeleted(['580', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['581', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['581', 1], 1)
|
|
||||||
yield * this.markGarbageCollected(['580', 1], 1)
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'580': [[0, 1, false], [1, 1, true], [2, 1, false]], '581': [[0, 3, true]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #4', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['544', 0], 1)
|
|
||||||
yield * this.markDeleted(['543', 2], 1)
|
|
||||||
yield * this.markDeleted(['544', 0], 1)
|
|
||||||
yield * this.markDeleted(['543', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['544', 0], 1)
|
|
||||||
yield * this.markDeleted(['545', 1], 1)
|
|
||||||
yield * this.markDeleted(['543', 4], 1)
|
|
||||||
yield * this.markDeleted(['543', 3], 1)
|
|
||||||
yield * this.markDeleted(['544', 1], 1)
|
|
||||||
yield * this.markDeleted(['544', 2], 1)
|
|
||||||
yield * this.markDeleted(['544', 1], 1)
|
|
||||||
yield * this.markDeleted(['544', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['543', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['543', 4], 1)
|
|
||||||
yield * this.markGarbageCollected(['544', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['543', 3], 1)
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'543': [[2, 3, true]], '544': [[0, 1, true], [1, 1, false], [2, 1, true]], '545': [[1, 1, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #5', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 1, true], [1, 3, false]]})
|
|
||||||
yield * this.applyDeleteSet({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'16': [[1, 2, false]], '17': [[0, 4, true]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #6', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.applyDeleteSet({'40': [[0, 3, false]]})
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'40': [[0, 3, false]]})
|
|
||||||
yield * this.applyDeleteSet({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'39': [[2, 2, false]], '40': [[0, 1, true], [1, 2, false]], '41': [[2, 1, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
it('Debug #7', async(function * (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.markDeleted(['9', 2], 1)
|
|
||||||
yield * this.markDeleted(['11', 2], 1)
|
|
||||||
yield * this.markDeleted(['11', 4], 1)
|
|
||||||
yield * this.markDeleted(['11', 1], 1)
|
|
||||||
yield * this.markDeleted(['9', 4], 1)
|
|
||||||
yield * this.markDeleted(['10', 0], 1)
|
|
||||||
yield * this.markGarbageCollected(['11', 2], 1)
|
|
||||||
yield * this.markDeleted(['11', 2], 1)
|
|
||||||
yield * this.markGarbageCollected(['11', 3], 1)
|
|
||||||
yield * this.markDeleted(['11', 3], 1)
|
|
||||||
yield * this.markDeleted(['11', 3], 1)
|
|
||||||
yield * this.markDeleted(['9', 4], 1)
|
|
||||||
yield * this.markDeleted(['10', 0], 1)
|
|
||||||
yield * this.markGarbageCollected(['11', 1], 1)
|
|
||||||
yield * this.markDeleted(['11', 1], 1)
|
|
||||||
expect(yield * this.getDeleteSet()).toEqual({'9': [[2, 1, false], [4, 1, false]], '10': [[0, 1, false]], '11': [[1, 3, true], [4, 1, false]]})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe('OperationStore', function () {
|
|
||||||
describe('Basic Tests', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
store = new Y[database](null, {
|
|
||||||
gcTimeout: -1,
|
|
||||||
namespace: 'testing'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
afterEach(function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.store.destroy()
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('debug #1', function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.put({id: [2]})
|
|
||||||
yield * this.os.put({id: [0]})
|
|
||||||
yield * this.os.delete([2])
|
|
||||||
yield * this.os.put({id: [1]})
|
|
||||||
expect(yield * this.os.find([0])).toBeTruthy()
|
|
||||||
expect(yield * this.os.find([1])).toBeTruthy()
|
|
||||||
expect(yield * this.os.find([2])).toBeFalsy()
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('can add&retrieve 5 elements', function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.put({val: 'four', id: [4]})
|
|
||||||
yield * this.os.put({val: 'one', id: [1]})
|
|
||||||
yield * this.os.put({val: 'three', id: [3]})
|
|
||||||
yield * this.os.put({val: 'two', id: [2]})
|
|
||||||
yield * this.os.put({val: 'five', id: [5]})
|
|
||||||
expect((yield * this.os.find([1])).val).toEqual('one')
|
|
||||||
expect((yield * this.os.find([2])).val).toEqual('two')
|
|
||||||
expect((yield * this.os.find([3])).val).toEqual('three')
|
|
||||||
expect((yield * this.os.find([4])).val).toEqual('four')
|
|
||||||
expect((yield * this.os.find([5])).val).toEqual('five')
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('5 elements do not exist anymore after deleting them', function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.put({val: 'four', id: [4]})
|
|
||||||
yield * this.os.put({val: 'one', id: [1]})
|
|
||||||
yield * this.os.put({val: 'three', id: [3]})
|
|
||||||
yield * this.os.put({val: 'two', id: [2]})
|
|
||||||
yield * this.os.put({val: 'five', id: [5]})
|
|
||||||
yield * this.os.delete([4])
|
|
||||||
expect(yield * this.os.find([4])).not.toBeTruthy()
|
|
||||||
yield * this.os.delete([3])
|
|
||||||
expect(yield * this.os.find([3])).not.toBeTruthy()
|
|
||||||
yield * this.os.delete([2])
|
|
||||||
expect(yield * this.os.find([2])).not.toBeTruthy()
|
|
||||||
yield * this.os.delete([1])
|
|
||||||
expect(yield * this.os.find([1])).not.toBeTruthy()
|
|
||||||
yield * this.os.delete([5])
|
|
||||||
expect(yield * this.os.find([5])).not.toBeTruthy()
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
var numberOfOSTests = 1000
|
|
||||||
describe(`Random Tests - after adding&deleting (0.8/0.2) ${numberOfOSTests} times`, function () {
|
|
||||||
var elements = []
|
|
||||||
beforeAll(function (done) {
|
|
||||||
store = new Y[database](null, {
|
|
||||||
gcTimeout: -1,
|
|
||||||
namespace: 'testing'
|
|
||||||
})
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
for (var i = 0; i < numberOfOSTests; i++) {
|
|
||||||
var r = Math.random()
|
|
||||||
if (r < 0.8) {
|
|
||||||
var obj = [Math.floor(Math.random() * numberOfOSTests * 10000)]
|
|
||||||
if (!(yield * this.os.find(obj))) {
|
|
||||||
elements.push(obj)
|
|
||||||
yield * this.os.put({id: obj})
|
|
||||||
}
|
|
||||||
} else if (elements.length > 0) {
|
|
||||||
var elemid = Math.floor(Math.random() * elements.length)
|
|
||||||
var elem = elements[elemid]
|
|
||||||
elements = elements.filter(function (e) {
|
|
||||||
return !Y.utils.compareIds(e, elem)
|
|
||||||
})
|
|
||||||
yield * this.os.delete(elem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
afterAll(function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.store.destroy()
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('can find every object', function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
for (var id of elements) {
|
|
||||||
expect((yield * this.os.find(id)).id).toEqual(id)
|
|
||||||
}
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can find every object with lower bound search', function (done) {
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
for (var id of elements) {
|
|
||||||
var e = yield * this.os.findWithLowerBound(id)
|
|
||||||
expect(e.id).toEqual(id)
|
|
||||||
}
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('iterating over a tree with lower bound yields the right amount of results', function (done) {
|
|
||||||
var lowerBound = elements[Math.floor(Math.random() * elements.length)]
|
|
||||||
var expectedResults = elements.filter(function (e, pos) {
|
|
||||||
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) && elements.indexOf(e) === pos
|
|
||||||
}).length
|
|
||||||
|
|
||||||
var actualResults = 0
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.iterate(this, lowerBound, null, function * (val) {
|
|
||||||
expect(val).toBeDefined()
|
|
||||||
actualResults++
|
|
||||||
})
|
|
||||||
expect(expectedResults).toEqual(actualResults)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('iterating over a tree without bounds yield the right amount of results', function (done) {
|
|
||||||
var lowerBound = null
|
|
||||||
var expectedResults = elements.filter(function (e, pos) {
|
|
||||||
return elements.indexOf(e) === pos
|
|
||||||
}).length
|
|
||||||
var actualResults = 0
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.iterate(this, lowerBound, null, function * (val) {
|
|
||||||
expect(val).toBeDefined()
|
|
||||||
actualResults++
|
|
||||||
})
|
|
||||||
expect(expectedResults).toEqual(actualResults)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('iterating over a tree with upper bound yields the right amount of results', function (done) {
|
|
||||||
var upperBound = elements[Math.floor(Math.random() * elements.length)]
|
|
||||||
var expectedResults = elements.filter(function (e, pos) {
|
|
||||||
return (Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
|
||||||
}).length
|
|
||||||
|
|
||||||
var actualResults = 0
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.iterate(this, null, upperBound, function * (val) {
|
|
||||||
expect(val).toBeDefined()
|
|
||||||
actualResults++
|
|
||||||
})
|
|
||||||
expect(expectedResults).toEqual(actualResults)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('iterating over a tree with upper and lower bounds yield the right amount of results', function (done) {
|
|
||||||
var b1 = elements[Math.floor(Math.random() * elements.length)]
|
|
||||||
var b2 = elements[Math.floor(Math.random() * elements.length)]
|
|
||||||
var upperBound, lowerBound
|
|
||||||
if (Y.utils.smaller(b1, b2)) {
|
|
||||||
lowerBound = b1
|
|
||||||
upperBound = b2
|
|
||||||
} else {
|
|
||||||
lowerBound = b2
|
|
||||||
upperBound = b1
|
|
||||||
}
|
|
||||||
var expectedResults = elements.filter(function (e, pos) {
|
|
||||||
return (Y.utils.smaller(lowerBound, e) || Y.utils.compareIds(e, lowerBound)) &&
|
|
||||||
(Y.utils.smaller(e, upperBound) || Y.utils.compareIds(e, upperBound)) && elements.indexOf(e) === pos
|
|
||||||
}).length
|
|
||||||
var actualResults = 0
|
|
||||||
store.requestTransaction(function * () {
|
|
||||||
yield * this.os.iterate(this, lowerBound, upperBound, function * (val) {
|
|
||||||
expect(val).toBeDefined()
|
|
||||||
actualResults++
|
|
||||||
})
|
|
||||||
expect(expectedResults).toEqual(actualResults)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
12
src/Notes.md
12
src/Notes.md
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
# Notes
|
|
||||||
|
|
||||||
### Terminology
|
|
||||||
|
|
||||||
* DB: DataBase that holds all the information of the shared object. It is devided into the OS, DS, and SS. This can be a persistent database or an in-memory database. Depending on the type of database, it could make sense to store OS, DS, and SS in different tables, or maybe different databases.
|
|
||||||
* OS: OperationStore holds all the operations. An operation is a js object with a fixed number of name fields.
|
|
||||||
* DS: DeleteStore holds the information about which operations are deleted and which operations were garbage collected (no longer available in the OS).
|
|
||||||
* SS: StateSet holds the current state of the OS. SS.getState(username) refers to the amount of operations that were received by that respective user.
|
|
||||||
* Op: Operation defines an action on a shared type. But it is also the format in which we store the model of a type. This is why it is also called a Struct/Structure.
|
|
||||||
* Type and Structure: We crearly distinguish between type and structure. Short explanation: A type (e.g. Strings, Numbers) have a number of functions that you can apply on them. (+) is well defined on both of them. They are *modeled* by a structure - the functions really change the structure of a type. Types can be implemented differently but still provide the same functionality. In Yjs, almost all types are realized as a doubly linked list (on which Yjs can provide eventual convergence)
|
|
||||||
*
|
|
||||||
@@ -1,404 +0,0 @@
|
|||||||
/* eslint-env browser, jasmine */
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is just a compilation of functions that help to test this library!
|
|
||||||
*/
|
|
||||||
|
|
||||||
// When testing, you store everything on the global object. We call it g
|
|
||||||
|
|
||||||
var Y = require('./y.js')
|
|
||||||
require('../../y-memory/src/Memory.js')(Y)
|
|
||||||
require('../../y-array/src/Array.js')(Y)
|
|
||||||
require('../../y-map/src/Map.js')(Y)
|
|
||||||
require('../../y-indexeddb/src/IndexedDB.js')(Y)
|
|
||||||
|
|
||||||
module.exports = Y
|
|
||||||
|
|
||||||
var g
|
|
||||||
if (typeof global !== 'undefined') {
|
|
||||||
g = global
|
|
||||||
} else if (typeof window !== 'undefined') {
|
|
||||||
g = window
|
|
||||||
} else {
|
|
||||||
throw new Error('No global object?')
|
|
||||||
}
|
|
||||||
g.g = g
|
|
||||||
|
|
||||||
// Helper methods for the random number generator
|
|
||||||
Math.seedrandom = require('seedrandom')
|
|
||||||
|
|
||||||
g.generateRandomSeed = function generateRandomSeed () {
|
|
||||||
var seed
|
|
||||||
if (typeof window !== 'undefined' && window.location.hash.length > 1) {
|
|
||||||
seed = window.location.hash.slice(1) // first character is the hash!
|
|
||||||
console.warn('Using random seed that was specified in the url!')
|
|
||||||
} else {
|
|
||||||
seed = JSON.stringify(Math.random())
|
|
||||||
}
|
|
||||||
console.info('Using random seed: ' + seed)
|
|
||||||
g.setRandomSeed(seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
g.setRandomSeed = function setRandomSeed (seed) {
|
|
||||||
Math.seedrandom.currentSeed = seed
|
|
||||||
Math.seedrandom(Math.seedrandom.currentSeed, { global: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
g.generateRandomSeed()
|
|
||||||
|
|
||||||
g.YConcurrencyTestingMode = true
|
|
||||||
|
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000
|
|
||||||
|
|
||||||
g.describeManyTimes = function describeManyTimes (times, name, f) {
|
|
||||||
for (var i = 0; i < times; i++) {
|
|
||||||
describe(name, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Wait for a specified amount of time (in ms). defaults to 5ms
|
|
||||||
*/
|
|
||||||
function wait (t) {
|
|
||||||
if (t == null) {
|
|
||||||
t = 0
|
|
||||||
}
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
setTimeout(function () {
|
|
||||||
resolve()
|
|
||||||
}, t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
g.wait = wait
|
|
||||||
|
|
||||||
g.databases = ['memory']
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
g.databases.push('indexeddb')
|
|
||||||
} else {
|
|
||||||
g.databases.push('leveldb')
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
returns a random element of o.
|
|
||||||
works on Object, and Array
|
|
||||||
*/
|
|
||||||
function getRandom (o) {
|
|
||||||
if (o instanceof Array) {
|
|
||||||
return o[Math.floor(Math.random() * o.length)]
|
|
||||||
} else if (o.constructor === Object) {
|
|
||||||
return o[getRandom(Object.keys(o))]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.getRandom = getRandom
|
|
||||||
|
|
||||||
function getRandomNumber (n) {
|
|
||||||
if (n == null) {
|
|
||||||
n = 9999
|
|
||||||
}
|
|
||||||
return Math.floor(Math.random() * n)
|
|
||||||
}
|
|
||||||
g.getRandomNumber = getRandomNumber
|
|
||||||
|
|
||||||
function getRandomString () {
|
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyzäüöABCDEFGHIJKLMNOPQRSTUVWXYZÄÜÖ'
|
|
||||||
var char = chars[getRandomNumber(chars.length)] // ü\n\n\n\n\n\n\n'
|
|
||||||
var length = getRandomNumber(7)
|
|
||||||
var string = ''
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
string += char
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
g.getRandomString = getRandomString
|
|
||||||
|
|
||||||
function * applyTransactions (relAmount, numberOfTransactions, objects, users, transactions, noReconnect) {
|
|
||||||
g.generateRandomSeed() // create a new seed, so we can re-create the behavior
|
|
||||||
for (var i = 0; i < numberOfTransactions * relAmount + 1; i++) {
|
|
||||||
var r = Math.random()
|
|
||||||
if (r > 0.95) {
|
|
||||||
// 10% chance of toggling concurrent user interactions.
|
|
||||||
// There will be an artificial delay until ops can be executed by the type,
|
|
||||||
// therefore, operations of the database will be (pre)transformed until user operations arrive
|
|
||||||
yield (function simulateConcurrentUserInteractions (type) {
|
|
||||||
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
|
|
||||||
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
|
|
||||||
type = type.y
|
|
||||||
}
|
|
||||||
if (type.eventHandler.awaiting === 0 && type.eventHandler._debuggingAwaiting !== true) {
|
|
||||||
type.eventHandler.awaiting = 1
|
|
||||||
type.eventHandler._debuggingAwaiting = true
|
|
||||||
} else {
|
|
||||||
// fixAwaitingInType will handle _debuggingAwaiting
|
|
||||||
return fixAwaitingInType(type)
|
|
||||||
}
|
|
||||||
})(getRandom(objects))
|
|
||||||
} else if (r >= 0.5) {
|
|
||||||
// 40% chance to flush
|
|
||||||
yield Y.utils.globalRoom.flushOne() // flushes for some user.. (not necessarily 0)
|
|
||||||
} else if (noReconnect || r >= 0.05) {
|
|
||||||
// 45% chance to create operation
|
|
||||||
var done = getRandom(transactions)(getRandom(objects))
|
|
||||||
if (done != null) {
|
|
||||||
yield done
|
|
||||||
} else {
|
|
||||||
yield wait()
|
|
||||||
}
|
|
||||||
yield Y.utils.globalRoom.whenTransactionsFinished()
|
|
||||||
} else {
|
|
||||||
// 5% chance to disconnect/reconnect
|
|
||||||
var u = getRandom(users)
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
if (u.connector.isDisconnected()) {
|
|
||||||
yield u.reconnect()
|
|
||||||
} else {
|
|
||||||
yield u.disconnect()
|
|
||||||
}
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixAwaitingInType (type) {
|
|
||||||
if (!(type instanceof Y.utils.CustomType) && type.y instanceof Y.utils.CustomType) {
|
|
||||||
// usually we expect type to be a custom type. But in YXml we share an object {y: YXml, dom: Dom} instead
|
|
||||||
type = type.y
|
|
||||||
}
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
type.os.whenTransactionsFinished().then(function () {
|
|
||||||
// _debuggingAwaiting artificially increases the awaiting property. We need to make sure that we only do that once / reverse the effect once
|
|
||||||
type.os.requestTransaction(function * () {
|
|
||||||
if (type.eventHandler.awaiting > 0 && type.eventHandler._debuggingAwaiting === true) {
|
|
||||||
type.eventHandler._debuggingAwaiting = false
|
|
||||||
yield * type.eventHandler.awaitOps(this, function * () { /* mock function */ })
|
|
||||||
}
|
|
||||||
wait(50).then(type.os.whenTransactionsFinished()).then(wait(50)).then(resolve)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
g.fixAwaitingInType = fixAwaitingInType
|
|
||||||
|
|
||||||
g.applyRandomTransactionsNoGCNoDisconnect = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
|
||||||
yield * applyTransactions(1, numberOfTransactions, objects, users, transactions, true)
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.applyRandomTransactionsAllRejoinNoGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
|
||||||
yield * applyTransactions(1, numberOfTransactions, objects, users, transactions)
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
for (var u in users) {
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
yield users[u].reconnect()
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
}
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.applyRandomTransactionsWithGC = async(function * applyRandomTransactions (users, objects, transactions, numberOfTransactions) {
|
|
||||||
yield * applyTransactions(1, numberOfTransactions, objects, users.slice(1), transactions)
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
for (var u in users) {
|
|
||||||
// TODO: here, we enforce that two users never sync at the same time with u[0]
|
|
||||||
// enforce that in the connector itself!
|
|
||||||
yield users[u].reconnect()
|
|
||||||
}
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
yield Promise.all(objects.map(fixAwaitingInType))
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
})
|
|
||||||
|
|
||||||
g.garbageCollectAllUsers = async(function * garbageCollectAllUsers (users) {
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
for (var i in users) {
|
|
||||||
yield users[i].db.emptyGarbageCollector()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
g.compareAllUsers = async(function * compareAllUsers (users) {
|
|
||||||
var s1, s2 // state sets
|
|
||||||
var ds1, ds2 // delete sets
|
|
||||||
var allDels1, allDels2 // all deletions
|
|
||||||
var db1 = [] // operation store of user1
|
|
||||||
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
yield g.garbageCollectAllUsers(users)
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
|
|
||||||
// disconnect, then reconnect all users
|
|
||||||
// We do this to make sure that the gc is updated by everyone
|
|
||||||
for (var i = 0; i < users.length; i++) {
|
|
||||||
yield users[i].disconnect()
|
|
||||||
yield wait()
|
|
||||||
yield users[i].reconnect()
|
|
||||||
}
|
|
||||||
yield wait()
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
|
|
||||||
// t1 and t2 basically do the same. They define t[1,2], ds[1,2], and allDels[1,2]
|
|
||||||
function * t1 () {
|
|
||||||
s1 = yield * this.getStateSet()
|
|
||||||
ds1 = yield * this.getDeleteSet()
|
|
||||||
allDels1 = []
|
|
||||||
yield * this.ds.iterate(this, null, null, function * (d) {
|
|
||||||
allDels1.push(d)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function * t2 () {
|
|
||||||
s2 = yield * this.getStateSet()
|
|
||||||
ds2 = yield * this.getDeleteSet()
|
|
||||||
allDels2 = []
|
|
||||||
yield * this.ds.iterate(this, null, null, function * (d) {
|
|
||||||
allDels2.push(d)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = Y.utils.globalRoom.buffers
|
|
||||||
for (var name in buffer) {
|
|
||||||
if (buffer[name].length > 0) {
|
|
||||||
// not all ops were transmitted..
|
|
||||||
debugger // eslint-disable-line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var uid = 0; uid < users.length; uid++) {
|
|
||||||
var u = users[uid]
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
var sv = yield * this.getStateVector()
|
|
||||||
for (var s of sv) {
|
|
||||||
yield * this.updateState(s.user)
|
|
||||||
}
|
|
||||||
// compare deleted ops against deleteStore
|
|
||||||
yield * this.os.iterate(this, null, null, function * (o) {
|
|
||||||
if (o.deleted === true) {
|
|
||||||
expect(yield * this.isDeleted(o.id)).toBeTruthy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// compare deleteStore against deleted ops
|
|
||||||
var ds = []
|
|
||||||
yield * this.ds.iterate(this, null, null, function * (d) {
|
|
||||||
ds.push(d)
|
|
||||||
})
|
|
||||||
for (var j in ds) {
|
|
||||||
var d = ds[j]
|
|
||||||
for (var i = 0; i < d.len; i++) {
|
|
||||||
var o = yield * this.getInsertion([d.id[0], d.id[1] + i])
|
|
||||||
// gc'd or deleted
|
|
||||||
if (d.gc) {
|
|
||||||
expect(o).toBeFalsy()
|
|
||||||
} else {
|
|
||||||
expect(o.deleted).toBeTruthy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// compare allDels tree
|
|
||||||
if (s1 == null) {
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
yield * t1.call(this)
|
|
||||||
yield * this.os.iterate(this, null, null, function * (o) {
|
|
||||||
o = Y.utils.copyObject(o)
|
|
||||||
delete o.origin
|
|
||||||
delete o.originOf
|
|
||||||
db1.push(o)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
yield * t2.call(this)
|
|
||||||
var db2 = []
|
|
||||||
yield * this.os.iterate(this, null, null, function * (o) {
|
|
||||||
o = Y.utils.copyObject(o)
|
|
||||||
delete o.origin
|
|
||||||
delete o.originOf
|
|
||||||
db2.push(o)
|
|
||||||
})
|
|
||||||
expect(s1).toEqual(s2)
|
|
||||||
expect(allDels1).toEqual(allDels2) // inner structure
|
|
||||||
expect(ds1).toEqual(ds2) // exported structure
|
|
||||||
db2.forEach((o, i) => {
|
|
||||||
expect(db1[i]).toEqual(o)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
yield u.db.whenTransactionsFinished()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
g.createUsers = async(function * createUsers (self, numberOfUsers, database, initType) {
|
|
||||||
if (Y.utils.globalRoom.users[0] != null) {
|
|
||||||
yield Y.utils.globalRoom.flushAll()
|
|
||||||
}
|
|
||||||
// destroy old users
|
|
||||||
for (var u in Y.utils.globalRoom.users) {
|
|
||||||
Y.utils.globalRoom.users[u].y.destroy()
|
|
||||||
}
|
|
||||||
self.users = null
|
|
||||||
|
|
||||||
var promises = []
|
|
||||||
for (var i = 0; i < numberOfUsers; i++) {
|
|
||||||
promises.push(Y({
|
|
||||||
db: {
|
|
||||||
name: database,
|
|
||||||
namespace: 'User ' + i,
|
|
||||||
cleanStart: true,
|
|
||||||
gcTimeout: -1,
|
|
||||||
gc: true,
|
|
||||||
repairCheckInterval: -1
|
|
||||||
},
|
|
||||||
connector: {
|
|
||||||
name: 'Test',
|
|
||||||
debug: false
|
|
||||||
},
|
|
||||||
share: {
|
|
||||||
root: initType || 'Map'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
self.users = yield Promise.all(promises)
|
|
||||||
self.types = self.users.map(function (u) { return u.share.root })
|
|
||||||
return self.users
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
Until async/await arrives in js, we use this function to wait for promises
|
|
||||||
by yielding them.
|
|
||||||
*/
|
|
||||||
function async (makeGenerator) {
|
|
||||||
return function (arg) {
|
|
||||||
var generator = makeGenerator.apply(this, arguments)
|
|
||||||
|
|
||||||
function handle (result) {
|
|
||||||
if (result.done) return Promise.resolve(result.value)
|
|
||||||
|
|
||||||
return Promise.resolve(result.value).then(function (res) {
|
|
||||||
return handle(generator.next(res))
|
|
||||||
}, function (err) {
|
|
||||||
return handle(generator.throw(err))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return handle(generator.next())
|
|
||||||
} catch (ex) {
|
|
||||||
generator.throw(ex)
|
|
||||||
// return Promise.reject(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.async = async
|
|
||||||
|
|
||||||
function logUsers (self) {
|
|
||||||
if (self.constructor === Array) {
|
|
||||||
self = {users: self}
|
|
||||||
}
|
|
||||||
self.users[0].db.logTable()
|
|
||||||
self.users[1].db.logTable()
|
|
||||||
self.users[2].db.logTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
g.logUsers = logUsers
|
|
||||||
414
src/Struct.js
414
src/Struct.js
@@ -1,414 +0,0 @@
|
|||||||
/* @flow */
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/*
|
|
||||||
An operation also defines the structure of a type. This is why operation and
|
|
||||||
structure are used interchangeably here.
|
|
||||||
|
|
||||||
It must be of the type Object. I hope to achieve some performance
|
|
||||||
improvements when working on databases that support the json format.
|
|
||||||
|
|
||||||
An operation must have the following properties:
|
|
||||||
|
|
||||||
* encode
|
|
||||||
- Encode the structure in a readable format (preferably string- todo)
|
|
||||||
* decode (todo)
|
|
||||||
- decode structure to json
|
|
||||||
* execute
|
|
||||||
- Execute the semantics of an operation.
|
|
||||||
* requiredOps
|
|
||||||
- Operations that are required to execute this operation.
|
|
||||||
*/
|
|
||||||
export default function extendStruct (Y) {
|
|
||||||
var Struct = {
|
|
||||||
/* This is the only operation that is actually not a structure, because
|
|
||||||
it is not stored in the OS. This is why it _does not_ have an id
|
|
||||||
|
|
||||||
op = {
|
|
||||||
target: Id
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
Delete: {
|
|
||||||
encode: function (op) {
|
|
||||||
return {
|
|
||||||
target: op.target,
|
|
||||||
length: op.length || 0,
|
|
||||||
struct: 'Delete'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requiredOps: function (op) {
|
|
||||||
return [] // [op.target]
|
|
||||||
},
|
|
||||||
execute: function * (op) {
|
|
||||||
return yield * this.deleteOperation(op.target, op.length || 1)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Insert: {
|
|
||||||
/* {
|
|
||||||
content: [any],
|
|
||||||
opContent: Id,
|
|
||||||
id: Id,
|
|
||||||
left: Id,
|
|
||||||
origin: Id,
|
|
||||||
right: Id,
|
|
||||||
parent: Id,
|
|
||||||
parentSub: string (optional), // child of Map type
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
encode: function (op/* :Insertion */) /* :Insertion */ {
|
|
||||||
// TODO: you could not send the "left" property, then you also have to
|
|
||||||
// "op.left = null" in $execute or $decode
|
|
||||||
var e/* :any */ = {
|
|
||||||
id: op.id,
|
|
||||||
left: op.left,
|
|
||||||
right: op.right,
|
|
||||||
origin: op.origin,
|
|
||||||
parent: op.parent,
|
|
||||||
struct: op.struct
|
|
||||||
}
|
|
||||||
if (op.parentSub != null) {
|
|
||||||
e.parentSub = op.parentSub
|
|
||||||
}
|
|
||||||
if (op.hasOwnProperty('opContent')) {
|
|
||||||
e.opContent = op.opContent
|
|
||||||
} else {
|
|
||||||
e.content = op.content.slice()
|
|
||||||
}
|
|
||||||
|
|
||||||
return e
|
|
||||||
},
|
|
||||||
requiredOps: function (op) {
|
|
||||||
var ids = []
|
|
||||||
if (op.left != null) {
|
|
||||||
ids.push(op.left)
|
|
||||||
}
|
|
||||||
if (op.right != null) {
|
|
||||||
ids.push(op.right)
|
|
||||||
}
|
|
||||||
if (op.origin != null && !Y.utils.compareIds(op.left, op.origin)) {
|
|
||||||
ids.push(op.origin)
|
|
||||||
}
|
|
||||||
// if (op.right == null && op.left == null) {
|
|
||||||
ids.push(op.parent)
|
|
||||||
|
|
||||||
if (op.opContent != null) {
|
|
||||||
ids.push(op.opContent)
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
},
|
|
||||||
getDistanceToOrigin: function * (op) {
|
|
||||||
if (op.left == null) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
var d = 0
|
|
||||||
var o = yield * this.getInsertion(op.left)
|
|
||||||
while (!Y.utils.matchesId(o, op.origin)) {
|
|
||||||
d++
|
|
||||||
if (o.left == null) {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
o = yield * this.getInsertion(o.left)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
# $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!)
|
|
||||||
*/
|
|
||||||
execute: function * (op) {
|
|
||||||
var i // loop counter
|
|
||||||
|
|
||||||
// during this function some ops may get split into two pieces (e.g. with getInsertionCleanEnd)
|
|
||||||
// We try to merge them later, if possible
|
|
||||||
var tryToRemergeLater = []
|
|
||||||
|
|
||||||
if (op.origin != null) { // TODO: !== instead of !=
|
|
||||||
// we save in origin that op originates in it
|
|
||||||
// we need that later when we eventually garbage collect origin (see transaction)
|
|
||||||
var origin = yield * this.getInsertionCleanEnd(op.origin)
|
|
||||||
if (origin.originOf == null) {
|
|
||||||
origin.originOf = []
|
|
||||||
}
|
|
||||||
origin.originOf.push(op.id)
|
|
||||||
yield * this.setOperation(origin)
|
|
||||||
if (origin.right != null) {
|
|
||||||
tryToRemergeLater.push(origin.right)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var distanceToOrigin = i = yield * Struct.Insert.getDistanceToOrigin.call(this, op) // most cases: 0 (starts from 0)
|
|
||||||
|
|
||||||
// now we begin to insert op in the list of insertions..
|
|
||||||
var o
|
|
||||||
var parent
|
|
||||||
var start
|
|
||||||
|
|
||||||
// find o. o is the first conflicting operation
|
|
||||||
if (op.left != null) {
|
|
||||||
o = yield * this.getInsertionCleanEnd(op.left)
|
|
||||||
if (!Y.utils.compareIds(op.left, op.origin) && o.right != null) {
|
|
||||||
// only if not added previously
|
|
||||||
tryToRemergeLater.push(o.right)
|
|
||||||
}
|
|
||||||
o = (o.right == null) ? null : yield * this.getOperation(o.right)
|
|
||||||
} else { // left == null
|
|
||||||
parent = yield * this.getOperation(op.parent)
|
|
||||||
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start
|
|
||||||
start = startId == null ? null : yield * this.getOperation(startId)
|
|
||||||
o = start
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to split op.right if necessary (also add to tryCombineWithLeft)
|
|
||||||
if (op.right != null) {
|
|
||||||
tryToRemergeLater.push(op.right)
|
|
||||||
yield * this.getInsertionCleanStart(op.right)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle conflicts
|
|
||||||
while (true) {
|
|
||||||
if (o != null && !Y.utils.compareIds(o.id, op.right)) {
|
|
||||||
var oOriginDistance = yield * Struct.Insert.getDistanceToOrigin.call(this, o)
|
|
||||||
if (oOriginDistance === i) {
|
|
||||||
// case 1
|
|
||||||
if (o.id[0] < op.id[0]) {
|
|
||||||
op.left = Y.utils.getLastId(o)
|
|
||||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
|
||||||
}
|
|
||||||
} else if (oOriginDistance < i) {
|
|
||||||
// case 2
|
|
||||||
if (i - distanceToOrigin <= oOriginDistance) {
|
|
||||||
op.left = Y.utils.getLastId(o)
|
|
||||||
distanceToOrigin = i + 1 // just ignore o.content.length, doesn't make a difference
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
if (o.right != null) {
|
|
||||||
o = yield * this.getInsertion(o.right)
|
|
||||||
} else {
|
|
||||||
o = null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconnect..
|
|
||||||
var left = null
|
|
||||||
var right = null
|
|
||||||
if (parent == null) {
|
|
||||||
parent = yield * this.getOperation(op.parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconnect left and set right of op
|
|
||||||
if (op.left != null) {
|
|
||||||
left = yield * this.getInsertion(op.left)
|
|
||||||
// link left
|
|
||||||
op.right = left.right
|
|
||||||
left.right = op.id
|
|
||||||
|
|
||||||
yield * this.setOperation(left)
|
|
||||||
} else {
|
|
||||||
// set op.right from parent, if necessary
|
|
||||||
op.right = op.parentSub ? parent.map[op.parentSub] || null : parent.start
|
|
||||||
}
|
|
||||||
// reconnect right
|
|
||||||
if (op.right != null) {
|
|
||||||
// TODO: wanna connect right too?
|
|
||||||
right = yield * this.getOperation(op.right)
|
|
||||||
right.left = Y.utils.getLastId(op)
|
|
||||||
|
|
||||||
// if right exists, and it is supposed to be gc'd. Remove it from the gc
|
|
||||||
if (right.gc != null) {
|
|
||||||
if (right.content != null && right.content.length > 1) {
|
|
||||||
right = yield * this.getInsertionCleanEnd(right.id)
|
|
||||||
}
|
|
||||||
this.store.removeFromGarbageCollector(right)
|
|
||||||
}
|
|
||||||
yield * this.setOperation(right)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update parents .map/start/end properties
|
|
||||||
if (op.parentSub != null) {
|
|
||||||
if (left == null) {
|
|
||||||
parent.map[op.parentSub] = op.id
|
|
||||||
yield * this.setOperation(parent)
|
|
||||||
}
|
|
||||||
// is a child of a map struct.
|
|
||||||
// Then also make sure that only the most left element is not deleted
|
|
||||||
// We do not call the type in this case (this is what the third parameter is for)
|
|
||||||
if (op.right != null) {
|
|
||||||
yield * this.deleteOperation(op.right, 1, true)
|
|
||||||
}
|
|
||||||
if (op.left != null) {
|
|
||||||
yield * this.deleteOperation(op.id, 1, true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (right == null || left == null) {
|
|
||||||
if (right == null) {
|
|
||||||
parent.end = Y.utils.getLastId(op)
|
|
||||||
}
|
|
||||||
if (left == null) {
|
|
||||||
parent.start = op.id
|
|
||||||
}
|
|
||||||
yield * this.setOperation(parent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to merge original op.left and op.origin
|
|
||||||
for (i = 0; i < tryToRemergeLater.length; i++) {
|
|
||||||
var m = yield * this.getOperation(tryToRemergeLater[i])
|
|
||||||
yield * this.tryCombineWithLeft(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
List: {
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
start: null,
|
|
||||||
end: null,
|
|
||||||
struct: "List",
|
|
||||||
type: "",
|
|
||||||
id: this.os.getNextOpId(1)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
create: function (id) {
|
|
||||||
return {
|
|
||||||
start: null,
|
|
||||||
end: null,
|
|
||||||
struct: 'List',
|
|
||||||
id: id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
encode: function (op) {
|
|
||||||
var e = {
|
|
||||||
struct: 'List',
|
|
||||||
id: op.id,
|
|
||||||
type: op.type
|
|
||||||
}
|
|
||||||
if (op.requires != null) {
|
|
||||||
e.requires = op.requires
|
|
||||||
}
|
|
||||||
if (op.info != null) {
|
|
||||||
e.info = op.info
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
},
|
|
||||||
requiredOps: function () {
|
|
||||||
/*
|
|
||||||
var ids = []
|
|
||||||
if (op.start != null) {
|
|
||||||
ids.push(op.start)
|
|
||||||
}
|
|
||||||
if (op.end != null){
|
|
||||||
ids.push(op.end)
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
*/
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
execute: function * (op) {
|
|
||||||
op.start = null
|
|
||||||
op.end = null
|
|
||||||
},
|
|
||||||
ref: function * (op, pos) {
|
|
||||||
if (op.start == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var res = null
|
|
||||||
var o = yield * this.getOperation(op.start)
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (!o.deleted) {
|
|
||||||
res = o
|
|
||||||
pos--
|
|
||||||
}
|
|
||||||
if (pos >= 0 && o.right != null) {
|
|
||||||
o = yield * this.getOperation(o.right)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
},
|
|
||||||
map: function * (o, f) {
|
|
||||||
o = o.start
|
|
||||||
var res = []
|
|
||||||
while (o != null) { // TODO: change to != (at least some convention)
|
|
||||||
var operation = yield * this.getOperation(o)
|
|
||||||
if (!operation.deleted) {
|
|
||||||
res.push(f(operation))
|
|
||||||
}
|
|
||||||
o = operation.right
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Map: {
|
|
||||||
/*
|
|
||||||
{
|
|
||||||
map: {},
|
|
||||||
struct: "Map",
|
|
||||||
type: "",
|
|
||||||
id: this.os.getNextOpId(1)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
create: function (id) {
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
map: {},
|
|
||||||
struct: 'Map'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
encode: function (op) {
|
|
||||||
var e = {
|
|
||||||
struct: 'Map',
|
|
||||||
type: op.type,
|
|
||||||
id: op.id,
|
|
||||||
map: {} // overwrite map!!
|
|
||||||
}
|
|
||||||
if (op.requires != null) {
|
|
||||||
e.requires = op.requires
|
|
||||||
}
|
|
||||||
if (op.info != null) {
|
|
||||||
e.info = op.info
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
},
|
|
||||||
requiredOps: function () {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
execute: function * () {},
|
|
||||||
/*
|
|
||||||
Get a property by name
|
|
||||||
*/
|
|
||||||
get: function * (op, name) {
|
|
||||||
var oid = op.map[name]
|
|
||||||
if (oid != null) {
|
|
||||||
var res = yield * this.getOperation(oid)
|
|
||||||
if (res == null || res.deleted) {
|
|
||||||
return void 0
|
|
||||||
} else if (res.opContent == null) {
|
|
||||||
return res.content[0]
|
|
||||||
} else {
|
|
||||||
return yield * this.getType(res.opContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.Struct = Struct
|
|
||||||
}
|
|
||||||
1108
src/Transaction.js
1108
src/Transaction.js
File diff suppressed because it is too large
Load Diff
825
src/Utils.js
825
src/Utils.js
@@ -1,825 +0,0 @@
|
|||||||
/*
|
|
||||||
EventHandler is an helper class for constructing custom types.
|
|
||||||
|
|
||||||
Why: When constructing custom types, you sometimes want your types to work
|
|
||||||
synchronous: E.g.
|
|
||||||
``` Synchronous
|
|
||||||
mytype.setSomething("yay")
|
|
||||||
mytype.getSomething() === "yay"
|
|
||||||
```
|
|
||||||
versus
|
|
||||||
``` Asynchronous
|
|
||||||
mytype.setSomething("yay")
|
|
||||||
mytype.getSomething() === undefined
|
|
||||||
mytype.waitForSomething().then(function(){
|
|
||||||
mytype.getSomething() === "yay"
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
The structures usually work asynchronously (you have to wait for the
|
|
||||||
database request to finish). EventHandler helps you to make your type
|
|
||||||
synchronous.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Utils (Y) {
|
|
||||||
Y.utils = {}
|
|
||||||
|
|
||||||
Y.utils.bubbleEvent = function (type, event) {
|
|
||||||
type.eventHandler.callEventListeners(event)
|
|
||||||
event.path = []
|
|
||||||
while (type != null && type._deepEventHandler != null) {
|
|
||||||
type._deepEventHandler.callEventListeners(event)
|
|
||||||
var parent = null
|
|
||||||
if (type._parent != null) {
|
|
||||||
parent = type.os.getType(type._parent)
|
|
||||||
}
|
|
||||||
if (parent != null && parent._getPathToChild != null) {
|
|
||||||
event.path = [parent._getPathToChild(type._model)].concat(event.path)
|
|
||||||
type = parent
|
|
||||||
} else {
|
|
||||||
type = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NamedEventHandler {
|
|
||||||
constructor () {
|
|
||||||
this._eventListener = {}
|
|
||||||
}
|
|
||||||
on (name, f) {
|
|
||||||
if (this._eventListener[name] == null) {
|
|
||||||
this._eventListener[name] = []
|
|
||||||
}
|
|
||||||
this._eventListener[name].push(f)
|
|
||||||
}
|
|
||||||
off (name, f) {
|
|
||||||
if (name == null || f == null) {
|
|
||||||
throw new Error('You must specify event name and function!')
|
|
||||||
}
|
|
||||||
let listener = this._eventListener[name] || []
|
|
||||||
this._eventListener[name] = listener.filter(e => e !== f)
|
|
||||||
}
|
|
||||||
emit (name, value) {
|
|
||||||
(this._eventListener[name] || []).forEach(l => l(value))
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this._eventListener = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.NamedEventHandler = NamedEventHandler
|
|
||||||
|
|
||||||
class EventListenerHandler {
|
|
||||||
constructor () {
|
|
||||||
this.eventListeners = []
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
this.eventListeners = null
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Basic event listener boilerplate...
|
|
||||||
*/
|
|
||||||
addEventListener (f) {
|
|
||||||
this.eventListeners.push(f)
|
|
||||||
}
|
|
||||||
removeEventListener (f) {
|
|
||||||
this.eventListeners = this.eventListeners.filter(function (g) {
|
|
||||||
return f !== g
|
|
||||||
})
|
|
||||||
}
|
|
||||||
removeAllEventListeners () {
|
|
||||||
this.eventListeners = []
|
|
||||||
}
|
|
||||||
callEventListeners (event) {
|
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
|
||||||
try {
|
|
||||||
var _event = {}
|
|
||||||
for (var name in event) {
|
|
||||||
_event[name] = event[name]
|
|
||||||
}
|
|
||||||
this.eventListeners[i](_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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.EventListenerHandler = EventListenerHandler
|
|
||||||
|
|
||||||
class EventHandler extends EventListenerHandler {
|
|
||||||
/* ::
|
|
||||||
waiting: Array<Insertion | Deletion>;
|
|
||||||
awaiting: number;
|
|
||||||
onevent: Function;
|
|
||||||
eventListeners: Array<Function>;
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
onevent: is called when the structure changes.
|
|
||||||
|
|
||||||
Note: "awaiting opertations" is used to denote operations that were
|
|
||||||
prematurely called. Events for received operations can not be executed until
|
|
||||||
all prematurely called operations were executed ("waiting operations")
|
|
||||||
*/
|
|
||||||
constructor (onevent /* : Function */) {
|
|
||||||
super()
|
|
||||||
this.waiting = []
|
|
||||||
this.awaiting = 0
|
|
||||||
this.onevent = onevent
|
|
||||||
}
|
|
||||||
destroy () {
|
|
||||||
super.destroy()
|
|
||||||
this.waiting = null
|
|
||||||
this.onevent = null
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Call this when a new operation arrives. It will be executed right away if
|
|
||||||
there are no waiting operations, that you prematurely executed
|
|
||||||
*/
|
|
||||||
receivedOp (op) {
|
|
||||||
if (this.awaiting <= 0) {
|
|
||||||
this.onevent(op)
|
|
||||||
} else if (op.struct === 'Delete') {
|
|
||||||
var self = this
|
|
||||||
var checkDelete = function checkDelete (d) {
|
|
||||||
if (d.length == null) {
|
|
||||||
throw new Error('This shouldn\'t happen! d.length must be defined!')
|
|
||||||
}
|
|
||||||
// we check if o deletes something in self.waiting
|
|
||||||
// if so, we remove the deleted operation
|
|
||||||
for (var w = 0; w < self.waiting.length; w++) {
|
|
||||||
var i = self.waiting[w]
|
|
||||||
if (i.struct === 'Insert' && i.id[0] === d.target[0]) {
|
|
||||||
var iLength = i.hasOwnProperty('content') ? i.content.length : 1
|
|
||||||
var dStart = d.target[1]
|
|
||||||
var dEnd = d.target[1] + (d.length || 1)
|
|
||||||
var iStart = i.id[1]
|
|
||||||
var iEnd = i.id[1] + iLength
|
|
||||||
// Check if they don't overlap
|
|
||||||
if (iEnd <= dStart || dEnd <= iStart) {
|
|
||||||
// no overlapping
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// we check all overlapping cases. All cases:
|
|
||||||
/*
|
|
||||||
1) iiiii
|
|
||||||
ddddd
|
|
||||||
--> modify i and d
|
|
||||||
2) iiiiiii
|
|
||||||
ddddd
|
|
||||||
--> modify i, remove d
|
|
||||||
3) iiiiiii
|
|
||||||
ddd
|
|
||||||
--> remove d, modify i, and create another i (for the right hand side)
|
|
||||||
4) iiiii
|
|
||||||
ddddddd
|
|
||||||
--> remove i, modify d
|
|
||||||
5) iiiiiii
|
|
||||||
ddddddd
|
|
||||||
--> remove both i and d (**)
|
|
||||||
6) iiiiiii
|
|
||||||
ddddd
|
|
||||||
--> modify i, remove d
|
|
||||||
7) iii
|
|
||||||
ddddddd
|
|
||||||
--> remove i, create and apply two d with checkDelete(d) (**)
|
|
||||||
8) iiiii
|
|
||||||
ddddddd
|
|
||||||
--> remove i, modify d (**)
|
|
||||||
9) iiiii
|
|
||||||
ddddd
|
|
||||||
--> modify i and d
|
|
||||||
(**) (also check if i contains content or type)
|
|
||||||
*/
|
|
||||||
// TODO: I left some debugger statements, because I want to debug all cases once in production. REMEMBER END TODO
|
|
||||||
if (iStart < dStart) {
|
|
||||||
if (dStart < iEnd) {
|
|
||||||
if (iEnd < dEnd) {
|
|
||||||
// Case 1
|
|
||||||
// remove the right part of i's content
|
|
||||||
i.content.splice(dStart - iStart)
|
|
||||||
// remove the start of d's deletion
|
|
||||||
d.length = dEnd - iEnd
|
|
||||||
d.target = [d.target[0], iEnd]
|
|
||||||
continue
|
|
||||||
} else if (iEnd === dEnd) {
|
|
||||||
// Case 2
|
|
||||||
i.content.splice(dStart - iStart)
|
|
||||||
// remove d, we do that by simply ending this function
|
|
||||||
return
|
|
||||||
} else { // (dEnd < iEnd)
|
|
||||||
// Case 3
|
|
||||||
var newI = {
|
|
||||||
id: [i.id[0], dEnd],
|
|
||||||
content: i.content.slice(dEnd - iStart),
|
|
||||||
struct: 'Insert'
|
|
||||||
}
|
|
||||||
self.waiting.push(newI)
|
|
||||||
i.content.splice(dStart - iStart)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (dStart === iStart) {
|
|
||||||
if (iEnd < dEnd) {
|
|
||||||
// Case 4
|
|
||||||
d.length = dEnd - iEnd
|
|
||||||
d.target = [d.target[0], iEnd]
|
|
||||||
i.content = []
|
|
||||||
continue
|
|
||||||
} else if (iEnd === dEnd) {
|
|
||||||
// Case 5
|
|
||||||
self.waiting.splice(w, 1)
|
|
||||||
return
|
|
||||||
} else { // (dEnd < iEnd)
|
|
||||||
// Case 6
|
|
||||||
i.content = i.content.slice(dEnd - iStart)
|
|
||||||
i.id = [i.id[0], dEnd]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else { // (dStart < iStart)
|
|
||||||
if (iStart < dEnd) {
|
|
||||||
// they overlap
|
|
||||||
/*
|
|
||||||
7) iii
|
|
||||||
ddddddd
|
|
||||||
--> remove i, create and apply two d with checkDelete(d) (**)
|
|
||||||
8) iiiii
|
|
||||||
ddddddd
|
|
||||||
--> remove i, modify d (**)
|
|
||||||
9) iiiii
|
|
||||||
ddddd
|
|
||||||
--> modify i and d
|
|
||||||
*/
|
|
||||||
if (iEnd < dEnd) {
|
|
||||||
// Case 7
|
|
||||||
// debugger // TODO: You did not test this case yet!!!! (add the debugger here)
|
|
||||||
self.waiting.splice(w, 1)
|
|
||||||
checkDelete({
|
|
||||||
target: [d.target[0], dStart],
|
|
||||||
length: iStart - dStart,
|
|
||||||
struct: 'Delete'
|
|
||||||
})
|
|
||||||
checkDelete({
|
|
||||||
target: [d.target[0], iEnd],
|
|
||||||
length: iEnd - dEnd,
|
|
||||||
struct: 'Delete'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
} else if (iEnd === dEnd) {
|
|
||||||
// Case 8
|
|
||||||
self.waiting.splice(w, 1)
|
|
||||||
w--
|
|
||||||
d.length -= iLength
|
|
||||||
continue
|
|
||||||
} else { // dEnd < iEnd
|
|
||||||
// Case 9
|
|
||||||
d.length = iStart - dStart
|
|
||||||
i.content.splice(0, dEnd - iStart)
|
|
||||||
i.id = [i.id[0], dEnd]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// finished with remaining operations
|
|
||||||
self.waiting.push(d)
|
|
||||||
}
|
|
||||||
if (op.key == null) {
|
|
||||||
// deletes in list
|
|
||||||
checkDelete(op)
|
|
||||||
} else {
|
|
||||||
// deletes in map
|
|
||||||
this.waiting.push(op)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.waiting.push(op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
You created some operations, and you want the `onevent` function to be
|
|
||||||
called right away. Received operations will not be executed untill all
|
|
||||||
prematurely called operations are executed
|
|
||||||
*/
|
|
||||||
awaitAndPrematurelyCall (ops) {
|
|
||||||
this.awaiting++
|
|
||||||
ops.map(Y.utils.copyOperation).forEach(this.onevent)
|
|
||||||
}
|
|
||||||
* awaitOps (transaction, f, args) {
|
|
||||||
function notSoSmartSort (array) {
|
|
||||||
// this function sorts insertions in a executable order
|
|
||||||
var result = []
|
|
||||||
while (array.length > 0) {
|
|
||||||
for (var i = 0; i < array.length; i++) {
|
|
||||||
var independent = true
|
|
||||||
for (var j = 0; j < array.length; j++) {
|
|
||||||
if (Y.utils.matchesId(array[j], array[i].left)) {
|
|
||||||
// array[i] depends on array[j]
|
|
||||||
independent = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (independent) {
|
|
||||||
result.push(array.splice(i, 1)[0])
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
var before = this.waiting.length
|
|
||||||
// somehow create new operations
|
|
||||||
yield * f.apply(transaction, args)
|
|
||||||
// remove all appended ops / awaited ops
|
|
||||||
this.waiting.splice(before)
|
|
||||||
if (this.awaiting > 0) this.awaiting--
|
|
||||||
// if there are no awaited ops anymore, we can update all waiting ops, and send execute them (if there are still no awaited ops)
|
|
||||||
if (this.awaiting === 0 && this.waiting.length > 0) {
|
|
||||||
// update all waiting ops
|
|
||||||
for (let i = 0; i < this.waiting.length; i++) {
|
|
||||||
var o = this.waiting[i]
|
|
||||||
if (o.struct === 'Insert') {
|
|
||||||
var _o = yield * transaction.getInsertion(o.id)
|
|
||||||
if (_o.parentSub != null && _o.left != null) {
|
|
||||||
// if o is an insertion of a map struc (parentSub is defined), then it shouldn't be necessary to compute left
|
|
||||||
this.waiting.splice(i, 1)
|
|
||||||
i-- // update index
|
|
||||||
} else if (!Y.utils.compareIds(_o.id, o.id)) {
|
|
||||||
// o got extended
|
|
||||||
o.left = [o.id[0], o.id[1] - 1]
|
|
||||||
} else if (_o.left == null) {
|
|
||||||
o.left = null
|
|
||||||
} else {
|
|
||||||
// find next undeleted op
|
|
||||||
var left = yield * transaction.getInsertion(_o.left)
|
|
||||||
while (left.deleted != null) {
|
|
||||||
if (left.left != null) {
|
|
||||||
left = yield * transaction.getInsertion(left.left)
|
|
||||||
} else {
|
|
||||||
left = null
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o.left = left != null ? Y.utils.getLastId(left) : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// the previous stuff was async, so we have to check again!
|
|
||||||
// We also pull changes from the bindings, if there exists such a method, this could increase awaiting too
|
|
||||||
if (this._pullChanges != null) {
|
|
||||||
this._pullChanges()
|
|
||||||
}
|
|
||||||
if (this.awaiting === 0) {
|
|
||||||
// sort by type, execute inserts first
|
|
||||||
var ins = []
|
|
||||||
var dels = []
|
|
||||||
this.waiting.forEach(function (o) {
|
|
||||||
if (o.struct === 'Delete') {
|
|
||||||
dels.push(o)
|
|
||||||
} else {
|
|
||||||
ins.push(o)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.waiting = []
|
|
||||||
// put in executable order
|
|
||||||
ins = notSoSmartSort(ins)
|
|
||||||
// this.onevent can trigger the creation of another operation
|
|
||||||
// -> check if this.awaiting increased & stop computation if it does
|
|
||||||
for (var i = 0; i < ins.length; i++) {
|
|
||||||
if (this.awaiting === 0) {
|
|
||||||
this.onevent(ins[i])
|
|
||||||
} else {
|
|
||||||
this.waiting = this.waiting.concat(ins.slice(i))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i = 0; i < dels.length; i++) {
|
|
||||||
if (this.awaiting === 0) {
|
|
||||||
this.onevent(dels[i])
|
|
||||||
} else {
|
|
||||||
this.waiting = this.waiting.concat(dels.slice(i))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: Remove awaitedInserts and awaitedDeletes in favor of awaitedOps, as they are deprecated and do not always work
|
|
||||||
// Do this in one of the coming releases that are breaking anyway
|
|
||||||
/*
|
|
||||||
Call this when you successfully awaited the execution of n Insert operations
|
|
||||||
*/
|
|
||||||
awaitedInserts (n) {
|
|
||||||
var ops = this.waiting.splice(this.waiting.length - n)
|
|
||||||
for (var oid = 0; oid < ops.length; oid++) {
|
|
||||||
var op = ops[oid]
|
|
||||||
if (op.struct === 'Insert') {
|
|
||||||
for (var i = this.waiting.length - 1; i >= 0; i--) {
|
|
||||||
let w = this.waiting[i]
|
|
||||||
// TODO: do I handle split operations correctly here? Super unlikely, but yeah..
|
|
||||||
// Also: can this case happen? Can op be inserted in the middle of a larger op that is in $waiting?
|
|
||||||
if (w.struct === 'Insert') {
|
|
||||||
if (Y.utils.matchesId(w, op.left)) {
|
|
||||||
// include the effect of op in w
|
|
||||||
w.right = op.id
|
|
||||||
// exclude the effect of w in op
|
|
||||||
op.left = w.left
|
|
||||||
} else if (Y.utils.compareIds(w.id, op.right)) {
|
|
||||||
// similar..
|
|
||||||
w.left = Y.utils.getLastId(op)
|
|
||||||
op.right = w.right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected Insert Operation!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._tryCallEvents(n)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Call this when you successfully awaited the execution of n Delete operations
|
|
||||||
*/
|
|
||||||
awaitedDeletes (n, newLeft) {
|
|
||||||
var ops = this.waiting.splice(this.waiting.length - n)
|
|
||||||
for (var j = 0; j < ops.length; j++) {
|
|
||||||
var del = ops[j]
|
|
||||||
if (del.struct === 'Delete') {
|
|
||||||
if (newLeft != null) {
|
|
||||||
for (var i = 0; i < this.waiting.length; i++) {
|
|
||||||
let w = this.waiting[i]
|
|
||||||
// We will just care about w.left
|
|
||||||
if (w.struct === 'Insert' && Y.utils.compareIds(del.target, w.left)) {
|
|
||||||
w.left = newLeft
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected Delete Operation!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._tryCallEvents(n)
|
|
||||||
}
|
|
||||||
/* (private)
|
|
||||||
Try to execute the events for the waiting operations
|
|
||||||
*/
|
|
||||||
_tryCallEvents () {
|
|
||||||
function notSoSmartSort (array) {
|
|
||||||
var result = []
|
|
||||||
while (array.length > 0) {
|
|
||||||
for (var i = 0; i < array.length; i++) {
|
|
||||||
var independent = true
|
|
||||||
for (var j = 0; j < array.length; j++) {
|
|
||||||
if (Y.utils.matchesId(array[j], array[i].left)) {
|
|
||||||
// array[i] depends on array[j]
|
|
||||||
independent = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (independent) {
|
|
||||||
result.push(array.splice(i, 1)[0])
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
if (this.awaiting > 0) this.awaiting--
|
|
||||||
if (this.awaiting === 0 && this.waiting.length > 0) {
|
|
||||||
var ins = []
|
|
||||||
var dels = []
|
|
||||||
this.waiting.forEach(function (o) {
|
|
||||||
if (o.struct === 'Delete') {
|
|
||||||
dels.push(o)
|
|
||||||
} else {
|
|
||||||
ins.push(o)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ins = notSoSmartSort(ins)
|
|
||||||
ins.forEach(this.onevent)
|
|
||||||
dels.forEach(this.onevent)
|
|
||||||
this.waiting = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.EventHandler = EventHandler
|
|
||||||
|
|
||||||
/*
|
|
||||||
Default class of custom types!
|
|
||||||
*/
|
|
||||||
class CustomType {
|
|
||||||
getPath () {
|
|
||||||
var parent = null
|
|
||||||
if (this._parent != null) {
|
|
||||||
parent = this.os.getType(this._parent)
|
|
||||||
}
|
|
||||||
if (parent != null && parent._getPathToChild != null) {
|
|
||||||
var firstKey = parent._getPathToChild(this._model)
|
|
||||||
var parentKeys = parent.getPath()
|
|
||||||
parentKeys.push(firstKey)
|
|
||||||
return parentKeys
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.CustomType = CustomType
|
|
||||||
|
|
||||||
/*
|
|
||||||
A wrapper for the definition of a custom type.
|
|
||||||
Every custom type must have three properties:
|
|
||||||
|
|
||||||
* struct
|
|
||||||
- Structname of this type
|
|
||||||
* initType
|
|
||||||
- Given a model, creates a custom type
|
|
||||||
* class
|
|
||||||
- the constructor of the custom type (e.g. in order to inherit from a type)
|
|
||||||
*/
|
|
||||||
class CustomTypeDefinition { // eslint-disable-line
|
|
||||||
/* ::
|
|
||||||
struct: any;
|
|
||||||
initType: any;
|
|
||||||
class: Function;
|
|
||||||
name: String;
|
|
||||||
*/
|
|
||||||
constructor (def) {
|
|
||||||
if (def.struct == null ||
|
|
||||||
def.initType == null ||
|
|
||||||
def.class == null ||
|
|
||||||
def.name == null ||
|
|
||||||
def.createType == null
|
|
||||||
) {
|
|
||||||
throw new Error('Custom type was not initialized correctly!')
|
|
||||||
}
|
|
||||||
this.struct = def.struct
|
|
||||||
this.initType = def.initType
|
|
||||||
this.createType = def.createType
|
|
||||||
this.class = def.class
|
|
||||||
this.name = def.name
|
|
||||||
if (def.appendAdditionalInfo != null) {
|
|
||||||
this.appendAdditionalInfo = def.appendAdditionalInfo
|
|
||||||
}
|
|
||||||
this.parseArguments = (def.parseArguments || function () {
|
|
||||||
return [this]
|
|
||||||
}).bind(this)
|
|
||||||
this.parseArguments.typeDefinition = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.CustomTypeDefinition = CustomTypeDefinition
|
|
||||||
|
|
||||||
Y.utils.isTypeDefinition = function isTypeDefinition (v) {
|
|
||||||
if (v != null) {
|
|
||||||
if (v instanceof Y.utils.CustomTypeDefinition) return [v]
|
|
||||||
else if (v.constructor === Array && v[0] instanceof Y.utils.CustomTypeDefinition) return v
|
|
||||||
else if (v instanceof Function && v.typeDefinition instanceof Y.utils.CustomTypeDefinition) return [v.typeDefinition]
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Make a flat copy of an object
|
|
||||||
(just copy properties)
|
|
||||||
*/
|
|
||||||
function copyObject (o) {
|
|
||||||
var c = {}
|
|
||||||
for (var key in o) {
|
|
||||||
c[key] = o[key]
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
Y.utils.copyObject = copyObject
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copy an operation, so that it can be manipulated.
|
|
||||||
Note: You must not change subproperties (except o.content)!
|
|
||||||
*/
|
|
||||||
function copyOperation (o) {
|
|
||||||
o = copyObject(o)
|
|
||||||
if (o.content != null) {
|
|
||||||
o.content = o.content.map(function (c) { return c })
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.utils.copyOperation = copyOperation
|
|
||||||
|
|
||||||
/*
|
|
||||||
Defines a smaller relation on Id's
|
|
||||||
*/
|
|
||||||
function smaller (a, b) {
|
|
||||||
return a[0] < b[0] || (a[0] === b[0] && (a[1] < b[1] || typeof a[1] < typeof b[1]))
|
|
||||||
}
|
|
||||||
Y.utils.smaller = smaller
|
|
||||||
|
|
||||||
function inDeletionRange (del, ins) {
|
|
||||||
return del.target[0] === ins[0] && del.target[1] <= ins[1] && ins[1] < del.target[1] + (del.length || 1)
|
|
||||||
}
|
|
||||||
Y.utils.inDeletionRange = inDeletionRange
|
|
||||||
|
|
||||||
function compareIds (id1, id2) {
|
|
||||||
if (id1 == null || id2 == null) {
|
|
||||||
return id1 === id2
|
|
||||||
} else {
|
|
||||||
return id1[0] === id2[0] && id1[1] === id2[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.compareIds = compareIds
|
|
||||||
|
|
||||||
function matchesId (op, id) {
|
|
||||||
if (id == null || op == null) {
|
|
||||||
return id === op
|
|
||||||
} else {
|
|
||||||
if (id[0] === op.id[0]) {
|
|
||||||
if (op.content == null) {
|
|
||||||
return id[1] === op.id[1]
|
|
||||||
} else {
|
|
||||||
return id[1] >= op.id[1] && id[1] < op.id[1] + op.content.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
Y.utils.matchesId = matchesId
|
|
||||||
|
|
||||||
function getLastId (op) {
|
|
||||||
if (op.content == null || op.content.length === 1) {
|
|
||||||
return op.id
|
|
||||||
} else {
|
|
||||||
return [op.id[0], op.id[1] + op.content.length - 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.utils.getLastId = getLastId
|
|
||||||
|
|
||||||
function createEmptyOpsArray (n) {
|
|
||||||
var a = new Array(n)
|
|
||||||
for (var i = 0; i < a.length; i++) {
|
|
||||||
a[i] = {
|
|
||||||
id: [null, null]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSmallLookupBuffer (Store) {
|
|
||||||
/*
|
|
||||||
This buffer implements a very small buffer that temporarily stores operations
|
|
||||||
after they are read / before they are written.
|
|
||||||
The buffer basically implements FIFO. Often requested lookups will be re-queued every time they are looked up / written.
|
|
||||||
|
|
||||||
It can speed up lookups on Operation Stores and State Stores. But it does not require notable use of memory or processing power.
|
|
||||||
|
|
||||||
Good for os and ss, bot not for ds (because it often uses methods that require a flush)
|
|
||||||
|
|
||||||
I tried to optimize this for performance, therefore no highlevel operations.
|
|
||||||
*/
|
|
||||||
class SmallLookupBuffer extends Store {
|
|
||||||
constructor (arg1, arg2) {
|
|
||||||
// super(...arguments) -- do this when this is supported by stable nodejs
|
|
||||||
super(arg1, arg2)
|
|
||||||
this.writeBuffer = createEmptyOpsArray(5)
|
|
||||||
this.readBuffer = createEmptyOpsArray(10)
|
|
||||||
}
|
|
||||||
* find (id, noSuperCall) {
|
|
||||||
var i, r
|
|
||||||
for (i = this.readBuffer.length - 1; i >= 0; i--) {
|
|
||||||
r = this.readBuffer[i]
|
|
||||||
// we don't have to use compareids, because id is always defined!
|
|
||||||
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
|
||||||
// found r
|
|
||||||
// move r to the end of readBuffer
|
|
||||||
for (; i < this.readBuffer.length - 1; i++) {
|
|
||||||
this.readBuffer[i] = this.readBuffer[i + 1]
|
|
||||||
}
|
|
||||||
this.readBuffer[this.readBuffer.length - 1] = r
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var o
|
|
||||||
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
|
|
||||||
r = this.writeBuffer[i]
|
|
||||||
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
|
||||||
o = r
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i < 0 && noSuperCall === undefined) {
|
|
||||||
// did not reach break in last loop
|
|
||||||
// read id and put it to the end of readBuffer
|
|
||||||
o = yield * super.find(id)
|
|
||||||
}
|
|
||||||
if (o != null) {
|
|
||||||
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
|
||||||
this.readBuffer[i] = this.readBuffer[i + 1]
|
|
||||||
}
|
|
||||||
this.readBuffer[this.readBuffer.length - 1] = o
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
* put (o) {
|
|
||||||
var id = o.id
|
|
||||||
var i, r // helper variables
|
|
||||||
for (i = this.writeBuffer.length - 1; i >= 0; i--) {
|
|
||||||
r = this.writeBuffer[i]
|
|
||||||
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
|
||||||
// is already in buffer
|
|
||||||
// forget r, and move o to the end of writeBuffer
|
|
||||||
for (; i < this.writeBuffer.length - 1; i++) {
|
|
||||||
this.writeBuffer[i] = this.writeBuffer[i + 1]
|
|
||||||
}
|
|
||||||
this.writeBuffer[this.writeBuffer.length - 1] = o
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i < 0) {
|
|
||||||
// did not reach break in last loop
|
|
||||||
// write writeBuffer[0]
|
|
||||||
var write = this.writeBuffer[0]
|
|
||||||
if (write.id[0] !== null) {
|
|
||||||
yield * super.put(write)
|
|
||||||
}
|
|
||||||
// put o to the end of writeBuffer
|
|
||||||
for (i = 0; i < this.writeBuffer.length - 1; i++) {
|
|
||||||
this.writeBuffer[i] = this.writeBuffer[i + 1]
|
|
||||||
}
|
|
||||||
this.writeBuffer[this.writeBuffer.length - 1] = o
|
|
||||||
}
|
|
||||||
// check readBuffer for every occurence of o.id, overwrite if found
|
|
||||||
// whether found or not, we'll append o to the readbuffer
|
|
||||||
for (i = 0; i < this.readBuffer.length - 1; i++) {
|
|
||||||
r = this.readBuffer[i + 1]
|
|
||||||
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
|
||||||
this.readBuffer[i] = o
|
|
||||||
} else {
|
|
||||||
this.readBuffer[i] = r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.readBuffer[this.readBuffer.length - 1] = o
|
|
||||||
}
|
|
||||||
* delete (id) {
|
|
||||||
var i, r
|
|
||||||
for (i = 0; i < this.readBuffer.length; i++) {
|
|
||||||
r = this.readBuffer[i]
|
|
||||||
if (r.id[1] === id[1] && r.id[0] === id[0]) {
|
|
||||||
this.readBuffer[i] = {
|
|
||||||
id: [null, null]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield * this.flush()
|
|
||||||
yield * super.delete(id)
|
|
||||||
}
|
|
||||||
* findWithLowerBound (id) {
|
|
||||||
var o = yield * this.find(id, true)
|
|
||||||
if (o != null) {
|
|
||||||
return o
|
|
||||||
} else {
|
|
||||||
yield * this.flush()
|
|
||||||
return yield * super.findWithLowerBound.apply(this, arguments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
* findWithUpperBound (id) {
|
|
||||||
var o = yield * this.find(id, true)
|
|
||||||
if (o != null) {
|
|
||||||
return o
|
|
||||||
} else {
|
|
||||||
yield * this.flush()
|
|
||||||
return yield * super.findWithUpperBound.apply(this, arguments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
* findNext () {
|
|
||||||
yield * this.flush()
|
|
||||||
return yield * super.findNext.apply(this, arguments)
|
|
||||||
}
|
|
||||||
* findPrev () {
|
|
||||||
yield * this.flush()
|
|
||||||
return yield * super.findPrev.apply(this, arguments)
|
|
||||||
}
|
|
||||||
* iterate () {
|
|
||||||
yield * this.flush()
|
|
||||||
yield * super.iterate.apply(this, arguments)
|
|
||||||
}
|
|
||||||
* flush () {
|
|
||||||
for (var i = 0; i < this.writeBuffer.length; i++) {
|
|
||||||
var write = this.writeBuffer[i]
|
|
||||||
if (write.id[0] !== null) {
|
|
||||||
yield * super.put(write)
|
|
||||||
this.writeBuffer[i] = {
|
|
||||||
id: [null, null]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return SmallLookupBuffer
|
|
||||||
}
|
|
||||||
Y.utils.createSmallLookupBuffer = createSmallLookupBuffer
|
|
||||||
|
|
||||||
// Generates a unique id, for use as a user id.
|
|
||||||
// Thx to @jed for this script https://gist.github.com/jed/982883
|
|
||||||
function generateGuid(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,generateGuid)} // eslint-disable-line
|
|
||||||
Y.utils.generateGuid = generateGuid
|
|
||||||
}
|
|
||||||
62
src/index.js
Normal file
62
src/index.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
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,
|
||||||
|
YEvent,
|
||||||
|
Item,
|
||||||
|
AbstractStruct,
|
||||||
|
GC,
|
||||||
|
ContentBinary,
|
||||||
|
ContentDeleted,
|
||||||
|
ContentEmbed,
|
||||||
|
ContentFormat,
|
||||||
|
ContentJSON,
|
||||||
|
ContentAny,
|
||||||
|
ContentString,
|
||||||
|
ContentType,
|
||||||
|
AbstractType,
|
||||||
|
RelativePosition,
|
||||||
|
createRelativePositionFromTypeIndex,
|
||||||
|
createRelativePositionFromJSON,
|
||||||
|
createAbsolutePositionFromRelativePosition,
|
||||||
|
compareRelativePositions,
|
||||||
|
writeRelativePosition,
|
||||||
|
readRelativePosition,
|
||||||
|
ID,
|
||||||
|
createID,
|
||||||
|
compareIDs,
|
||||||
|
getState,
|
||||||
|
Snapshot,
|
||||||
|
createSnapshot,
|
||||||
|
createDeleteSet,
|
||||||
|
createDeleteSetFromStructStore,
|
||||||
|
snapshot,
|
||||||
|
emptySnapshot,
|
||||||
|
findRootTypeKey,
|
||||||
|
typeListToArraySnapshot,
|
||||||
|
typeMapGetSnapshot,
|
||||||
|
iterateDeletedStructs,
|
||||||
|
applyUpdate,
|
||||||
|
readUpdate,
|
||||||
|
encodeStateAsUpdate,
|
||||||
|
encodeStateVector,
|
||||||
|
UndoManager,
|
||||||
|
decodeSnapshot,
|
||||||
|
encodeSnapshot,
|
||||||
|
isDeleted,
|
||||||
|
isParentOf,
|
||||||
|
equalSnapshots,
|
||||||
|
PermanentUserData, // @TODO experimental
|
||||||
|
tryGc,
|
||||||
|
transact
|
||||||
|
} from './internals.js'
|
||||||
36
src/internals.js
Normal file
36
src/internals.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
export * from './utils/DeleteSet.js'
|
||||||
|
export * from './utils/Doc.js'
|
||||||
|
export * from './utils/encoding.js'
|
||||||
|
export * from './utils/EventHandler.js'
|
||||||
|
export * from './utils/ID.js'
|
||||||
|
export * from './utils/isParentOf.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/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/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'
|
||||||
86
src/structs/AbstractStruct.js
Normal file
86
src/structs/AbstractStruct.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
StructStore, ID, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
export class AbstractStruct {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {number} length
|
||||||
|
*/
|
||||||
|
constructor (id, length) {
|
||||||
|
/**
|
||||||
|
* The uniqe identifier of this struct.
|
||||||
|
* @type {ID}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.id = id
|
||||||
|
this.length = length
|
||||||
|
this.deleted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {encoding.Encoder} encoder The encoder to write data to.
|
||||||
|
* @param {number} offset
|
||||||
|
* @param {number} encodingRef
|
||||||
|
*/
|
||||||
|
write (encoder, offset, encodingRef) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
integrate (transaction) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractStructRef {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
*/
|
||||||
|
constructor (id) {
|
||||||
|
/**
|
||||||
|
* @type {Array<ID>}
|
||||||
|
*/
|
||||||
|
this._missing = []
|
||||||
|
/**
|
||||||
|
* The uniqe identifier of this type.
|
||||||
|
* @type {ID}
|
||||||
|
*/
|
||||||
|
this.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @return {Array<ID|null>}
|
||||||
|
*/
|
||||||
|
getMissing (transaction) {
|
||||||
|
return this._missing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {number} offset
|
||||||
|
* @return {AbstractStruct}
|
||||||
|
*/
|
||||||
|
toStruct (transaction, store, offset) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/structs/ContentAny.js
Normal file
111
src/structs/ContentAny.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
Transaction, Item, StructStore // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.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 {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
const len = this.arr.length
|
||||||
|
encoding.writeVarUint(encoder, len - offset)
|
||||||
|
for (let i = offset; i < len; i++) {
|
||||||
|
const c = this.arr[i]
|
||||||
|
encoding.writeAny(encoder, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentAny}
|
||||||
|
*/
|
||||||
|
export const readContentAny = decoder => {
|
||||||
|
const len = decoding.readVarUint(decoder)
|
||||||
|
const cs = []
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
cs.push(decoding.readAny(decoder))
|
||||||
|
}
|
||||||
|
return new ContentAny(cs)
|
||||||
|
}
|
||||||
95
src/structs/ContentBinary.js
Normal file
95
src/structs/ContentBinary.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
StructStore, Item, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as buffer from 'lib0/buffer.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
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 {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoding.writeVarUint8Array(encoder, this.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentBinary}
|
||||||
|
*/
|
||||||
|
export const readContentBinary = decoder => new ContentBinary(buffer.copyUint8Array(decoding.readVarUint8Array(decoder)))
|
||||||
104
src/structs/ContentDeleted.js
Normal file
104
src/structs/ContentDeleted.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
addToDeleteSet,
|
||||||
|
StructStore, Item, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.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, this.len)
|
||||||
|
item.deleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
delete (transaction) {}
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
gc (store) {}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoding.writeVarUint(encoder, this.len - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentDeleted}
|
||||||
|
*/
|
||||||
|
export const readContentDeleted = decoder => new ContentDeleted(decoding.readVarUint(decoder))
|
||||||
100
src/structs/ContentEmbed.js
Normal file
100
src/structs/ContentEmbed.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
StructStore, Item, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoding.writeVarString(encoder, JSON.stringify(this.embed))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentEmbed}
|
||||||
|
*/
|
||||||
|
export const readContentEmbed = decoder => new ContentEmbed(JSON.parse(decoding.readVarString(decoder)))
|
||||||
101
src/structs/ContentFormat.js
Normal file
101
src/structs/ContentFormat.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
Item, StructStore, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {}
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
delete (transaction) {}
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
gc (store) {}
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoding.writeVarString(encoder, this.key)
|
||||||
|
encoding.writeVarString(encoder, JSON.stringify(this.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentFormat}
|
||||||
|
*/
|
||||||
|
export const readContentFormat = decoder => new ContentFormat(decoding.readVarString(decoder), JSON.parse(decoding.readVarString(decoder)))
|
||||||
121
src/structs/ContentJSON.js
Normal file
121
src/structs/ContentJSON.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import {
|
||||||
|
Transaction, Item, StructStore // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.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 {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
const len = this.arr.length
|
||||||
|
encoding.writeVarUint(encoder, len - offset)
|
||||||
|
for (let i = offset; i < len; i++) {
|
||||||
|
const c = this.arr[i]
|
||||||
|
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentJSON}
|
||||||
|
*/
|
||||||
|
export const readContentJSON = decoder => {
|
||||||
|
const len = decoding.readVarUint(decoder)
|
||||||
|
const cs = []
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const c = decoding.readVarString(decoder)
|
||||||
|
if (c === 'undefined') {
|
||||||
|
cs.push(undefined)
|
||||||
|
} else {
|
||||||
|
cs.push(JSON.parse(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ContentJSON(cs)
|
||||||
|
}
|
||||||
104
src/structs/ContentString.js
Normal file
104
src/structs/ContentString.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
Transaction, Item, StructStore // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.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)
|
||||||
|
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 {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentString}
|
||||||
|
*/
|
||||||
|
export const readContentString = decoder => new ContentString(decoding.readVarString(decoder))
|
||||||
174
src/structs/ContentType.js
Normal file
174
src/structs/ContentType.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
readYArray,
|
||||||
|
readYMap,
|
||||||
|
readYText,
|
||||||
|
readYXmlElement,
|
||||||
|
readYXmlFragment,
|
||||||
|
readYXmlHook,
|
||||||
|
readYXmlText,
|
||||||
|
StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<function(decoding.Decoder):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<YEvent>} 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 {
|
||||||
|
// Whis 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.add(item.id)
|
||||||
|
}
|
||||||
|
item = item.right
|
||||||
|
}
|
||||||
|
this.type._map.forEach(item => {
|
||||||
|
if (!item.deleted) {
|
||||||
|
item.delete(transaction)
|
||||||
|
} else {
|
||||||
|
// same as above
|
||||||
|
transaction._mergeStructs.add(item.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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 {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
this.type._write(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ContentType}
|
||||||
|
*/
|
||||||
|
export const readContentType = decoder => new ContentType(typeRefs[decoding.readVarUint(decoder)](decoder))
|
||||||
90
src/structs/GC.js
Normal file
90
src/structs/GC.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
AbstractStructRef,
|
||||||
|
AbstractStruct,
|
||||||
|
createID,
|
||||||
|
addStruct,
|
||||||
|
StructStore, Transaction, ID // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
|
||||||
|
export const structGCRefNumber = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class GC extends AbstractStruct {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {number} length
|
||||||
|
*/
|
||||||
|
constructor (id, length) {
|
||||||
|
super(id, length)
|
||||||
|
this.deleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
delete () {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GC} right
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
mergeWith (right) {
|
||||||
|
this.length += right.length
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
integrate (transaction) {
|
||||||
|
addStruct(transaction.doc.store, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
encoding.writeUint8(encoder, structGCRefNumber)
|
||||||
|
encoding.writeVarUint(encoder, this.length - offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class GCRef extends AbstractStructRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, id, info) {
|
||||||
|
super(id)
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.length = decoding.readVarUint(decoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {number} offset
|
||||||
|
* @return {GC}
|
||||||
|
*/
|
||||||
|
toStruct (transaction, store, offset) {
|
||||||
|
if (offset > 0) {
|
||||||
|
// @ts-ignore
|
||||||
|
this.id = createID(this.id.client, this.id.clock + offset)
|
||||||
|
this.length -= offset
|
||||||
|
}
|
||||||
|
return new GC(
|
||||||
|
this.id,
|
||||||
|
this.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
778
src/structs/Item.js
Normal file
778
src/structs/Item.js
Normal file
@@ -0,0 +1,778 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
readID,
|
||||||
|
createID,
|
||||||
|
writeID,
|
||||||
|
GC,
|
||||||
|
nextID,
|
||||||
|
AbstractStructRef,
|
||||||
|
AbstractStruct,
|
||||||
|
replaceStruct,
|
||||||
|
addStruct,
|
||||||
|
addToDeleteSet,
|
||||||
|
findRootTypeKey,
|
||||||
|
compareIDs,
|
||||||
|
getItem,
|
||||||
|
getItemCleanEnd,
|
||||||
|
getItemCleanStart,
|
||||||
|
readContentDeleted,
|
||||||
|
readContentBinary,
|
||||||
|
readContentJSON,
|
||||||
|
readContentAny,
|
||||||
|
readContentString,
|
||||||
|
readContentEmbed,
|
||||||
|
readContentFormat,
|
||||||
|
readContentType,
|
||||||
|
addChangedTypeToTransaction,
|
||||||
|
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as maplib from 'lib0/map.js'
|
||||||
|
import * as set from 'lib0/set.js'
|
||||||
|
import * as binary from 'lib0/binary.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo This should return several items
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {{item:Item, diff:number}}
|
||||||
|
*/
|
||||||
|
export const followRedone = (store, id) => {
|
||||||
|
/**
|
||||||
|
* @type {ID|null}
|
||||||
|
*/
|
||||||
|
let nextID = id
|
||||||
|
let diff = 0
|
||||||
|
let item
|
||||||
|
do {
|
||||||
|
if (diff > 0) {
|
||||||
|
nextID = createID(nextID.client, nextID.clock + diff)
|
||||||
|
}
|
||||||
|
item = getItem(store, nextID)
|
||||||
|
diff = nextID.clock - item.id.clock
|
||||||
|
nextID = item.redone
|
||||||
|
} while (nextID !== null && item instanceof Item)
|
||||||
|
return {
|
||||||
|
item, diff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure that neither item nor any of its parents is ever deleted.
|
||||||
|
*
|
||||||
|
* This property does not persist when storing it into a database or when
|
||||||
|
* sending it to other peers
|
||||||
|
*
|
||||||
|
* @param {Item|null} item
|
||||||
|
* @param {boolean} keep
|
||||||
|
*/
|
||||||
|
export const keepItem = (item, keep) => {
|
||||||
|
while (item !== null && item.keep !== keep) {
|
||||||
|
item.keep = keep
|
||||||
|
item = item.parent._item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split leftItem into two items
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Item} leftItem
|
||||||
|
* @param {number} diff
|
||||||
|
* @return {Item}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const splitItem = (transaction, leftItem, diff) => {
|
||||||
|
const id = leftItem.id
|
||||||
|
// create rightItem
|
||||||
|
const rightItem = new Item(
|
||||||
|
createID(id.client, id.clock + diff),
|
||||||
|
leftItem,
|
||||||
|
createID(id.client, id.clock + diff - 1),
|
||||||
|
leftItem.right,
|
||||||
|
leftItem.rightOrigin,
|
||||||
|
leftItem.parent,
|
||||||
|
leftItem.parentSub,
|
||||||
|
leftItem.content.splice(diff)
|
||||||
|
)
|
||||||
|
if (leftItem.deleted) {
|
||||||
|
rightItem.deleted = true
|
||||||
|
}
|
||||||
|
if (leftItem.keep) {
|
||||||
|
rightItem.keep = true
|
||||||
|
}
|
||||||
|
if (leftItem.redone !== null) {
|
||||||
|
rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff)
|
||||||
|
}
|
||||||
|
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
|
||||||
|
leftItem.right = rightItem
|
||||||
|
// update right
|
||||||
|
if (rightItem.right !== null) {
|
||||||
|
rightItem.right.left = rightItem
|
||||||
|
}
|
||||||
|
// right is more specific.
|
||||||
|
transaction._mergeStructs.add(rightItem.id)
|
||||||
|
// update parent._map
|
||||||
|
if (rightItem.parentSub !== null && rightItem.right === null) {
|
||||||
|
rightItem.parent._map.set(rightItem.parentSub, rightItem)
|
||||||
|
}
|
||||||
|
leftItem.length = diff
|
||||||
|
return rightItem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redoes the effect of this operation.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction The Yjs instance.
|
||||||
|
* @param {Item} item
|
||||||
|
* @param {Set<Item>} redoitems
|
||||||
|
*
|
||||||
|
* @return {Item|null}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export const redoItem = (transaction, item, redoitems) => {
|
||||||
|
if (item.redone !== null) {
|
||||||
|
return getItemCleanStart(transaction, item.redone)
|
||||||
|
}
|
||||||
|
let parentItem = item.parent._item
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let left
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let right
|
||||||
|
if (item.parentSub === null) {
|
||||||
|
// Is an array item. Insert at the old position
|
||||||
|
left = item.left
|
||||||
|
right = item
|
||||||
|
} else {
|
||||||
|
// Is a map item. Insert as current value
|
||||||
|
left = item
|
||||||
|
while (left.right !== null) {
|
||||||
|
left = left.right
|
||||||
|
if (left.id.client !== transaction.doc.clientID) {
|
||||||
|
// It is not possible to redo this item because it conflicts with a
|
||||||
|
// change from another client
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (left.right !== null) {
|
||||||
|
left = /** @type {Item} */ (item.parent._map.get(item.parentSub))
|
||||||
|
}
|
||||||
|
right = null
|
||||||
|
}
|
||||||
|
// make sure that parent is redone
|
||||||
|
if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) {
|
||||||
|
// try to undo parent if it will be undone anyway
|
||||||
|
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems) === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentItem !== null && parentItem.redone !== null) {
|
||||||
|
while (parentItem.redone !== null) {
|
||||||
|
parentItem = getItemCleanStart(transaction, parentItem.redone)
|
||||||
|
}
|
||||||
|
// find next cloned_redo items
|
||||||
|
while (left !== null) {
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let leftTrace = left
|
||||||
|
// trace redone until parent matches
|
||||||
|
while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
|
||||||
|
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
|
||||||
|
}
|
||||||
|
if (leftTrace !== null && leftTrace.parent._item === parentItem) {
|
||||||
|
left = leftTrace
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left = left.left
|
||||||
|
}
|
||||||
|
while (right !== null) {
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let rightTrace = right
|
||||||
|
// trace redone until parent matches
|
||||||
|
while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
|
||||||
|
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
|
||||||
|
}
|
||||||
|
if (rightTrace !== null && rightTrace.parent._item === parentItem) {
|
||||||
|
right = rightTrace
|
||||||
|
break
|
||||||
|
}
|
||||||
|
right = right.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const redoneItem = new Item(
|
||||||
|
nextID(transaction),
|
||||||
|
left, left === null ? null : left.lastId,
|
||||||
|
right, right === null ? null : right.id,
|
||||||
|
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
|
||||||
|
item.parentSub,
|
||||||
|
item.content.copy()
|
||||||
|
)
|
||||||
|
item.redone = redoneItem.id
|
||||||
|
keepItem(redoneItem, true)
|
||||||
|
redoneItem.integrate(transaction)
|
||||||
|
return redoneItem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that represents any content.
|
||||||
|
*/
|
||||||
|
export class Item extends AbstractStruct {
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {Item | null} left
|
||||||
|
* @param {ID | null} origin
|
||||||
|
* @param {Item | null} right
|
||||||
|
* @param {ID | null} rightOrigin
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {string | null} parentSub
|
||||||
|
* @param {AbstractContent} content
|
||||||
|
*/
|
||||||
|
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
|
||||||
|
super(id, content.getLength())
|
||||||
|
/**
|
||||||
|
* The item that was originally to the left of this item.
|
||||||
|
* @type {ID | null}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.origin = origin
|
||||||
|
/**
|
||||||
|
* The item that is currently to the left of this item.
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.left = left
|
||||||
|
/**
|
||||||
|
* The item that is currently to the right of this item.
|
||||||
|
* @type {Item | null}
|
||||||
|
*/
|
||||||
|
this.right = right
|
||||||
|
/**
|
||||||
|
* The item that was originally to the right of this item.
|
||||||
|
* @readonly
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.rightOrigin = rightOrigin
|
||||||
|
/**
|
||||||
|
* The parent type.
|
||||||
|
* @type {AbstractType<any>}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.parent = parent
|
||||||
|
/**
|
||||||
|
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
||||||
|
* key is specified here. The key is then used to refer to the list in which
|
||||||
|
* to insert this item. If `parentSub = null` type._start is the list in
|
||||||
|
* which to insert to. Otherwise it is `parent._map`.
|
||||||
|
* @type {String | null}
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
this.parentSub = parentSub
|
||||||
|
/**
|
||||||
|
* Whether this item was deleted or not.
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
this.deleted = false
|
||||||
|
/**
|
||||||
|
* If this type's effect is reundone this type refers to the type that undid
|
||||||
|
* this operation.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.redone = null
|
||||||
|
/**
|
||||||
|
* @type {AbstractContent}
|
||||||
|
*/
|
||||||
|
this.content = content
|
||||||
|
this.length = content.getLength()
|
||||||
|
this.countable = content.isCountable()
|
||||||
|
/**
|
||||||
|
* If true, do not garbage collect this Item.
|
||||||
|
*/
|
||||||
|
this.keep = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
integrate (transaction) {
|
||||||
|
const store = transaction.doc.store
|
||||||
|
const id = this.id
|
||||||
|
const parent = this.parent
|
||||||
|
const parentSub = this.parentSub
|
||||||
|
const length = this.length
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let o
|
||||||
|
// set o to the first conflicting item
|
||||||
|
if (this.left !== null) {
|
||||||
|
o = this.left.right
|
||||||
|
} else if (parentSub !== null) {
|
||||||
|
o = parent._map.get(parentSub) || null
|
||||||
|
while (o !== null && o.left !== null) {
|
||||||
|
o = o.left
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
o = parent._start
|
||||||
|
}
|
||||||
|
// TODO: use something like DeleteSet here (a tree implementation would be best)
|
||||||
|
/**
|
||||||
|
* @type {Set<Item>}
|
||||||
|
*/
|
||||||
|
const conflictingItems = new Set()
|
||||||
|
/**
|
||||||
|
* @type {Set<Item>}
|
||||||
|
*/
|
||||||
|
const 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 (compareIDs(this.origin, o.origin)) {
|
||||||
|
// case 1
|
||||||
|
if (o.id.client < id.client) {
|
||||||
|
this.left = o
|
||||||
|
conflictingItems.clear()
|
||||||
|
}
|
||||||
|
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
|
||||||
|
// case 2
|
||||||
|
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
|
||||||
|
this.left = o
|
||||||
|
conflictingItems.clear()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
o = o.right
|
||||||
|
}
|
||||||
|
// reconnect left/right + update parent map/start if necessary
|
||||||
|
if (this.left !== null) {
|
||||||
|
const right = this.left.right
|
||||||
|
this.right = right
|
||||||
|
this.left.right = this
|
||||||
|
} else {
|
||||||
|
let r
|
||||||
|
if (parentSub !== null) {
|
||||||
|
r = parent._map.get(parentSub) || null
|
||||||
|
while (r !== null && r.left !== null) {
|
||||||
|
r = r.left
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r = parent._start
|
||||||
|
parent._start = this
|
||||||
|
}
|
||||||
|
this.right = r
|
||||||
|
}
|
||||||
|
if (this.right !== null) {
|
||||||
|
this.right.left = this
|
||||||
|
} else if (parentSub !== null) {
|
||||||
|
// set as current parent value if right === null and this is parentSub
|
||||||
|
parent._map.set(parentSub, this)
|
||||||
|
if (this.left !== null) {
|
||||||
|
// this is the current attribute value of parent. delete right
|
||||||
|
this.left.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// adjust length of parent
|
||||||
|
if (parentSub === null && this.countable && !this.deleted) {
|
||||||
|
parent._length += length
|
||||||
|
}
|
||||||
|
addStruct(store, this)
|
||||||
|
this.content.integrate(transaction, this)
|
||||||
|
// add parent to transaction.changed
|
||||||
|
addChangedTypeToTransaction(transaction, parent, parentSub)
|
||||||
|
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
|
||||||
|
// delete if parent is deleted or if this is not the current attribute value of parent
|
||||||
|
this.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next non-deleted item
|
||||||
|
*/
|
||||||
|
get next () {
|
||||||
|
let n = this.right
|
||||||
|
while (n !== null && n.deleted) {
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the previous non-deleted item
|
||||||
|
*/
|
||||||
|
get prev () {
|
||||||
|
let n = this.left
|
||||||
|
while (n !== null && n.deleted) {
|
||||||
|
n = n.left
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the last content address of this Item.
|
||||||
|
*/
|
||||||
|
get lastId () {
|
||||||
|
return createID(this.id.client, this.id.clock + this.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to merge two items
|
||||||
|
*
|
||||||
|
* @param {Item} right
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
mergeWith (right) {
|
||||||
|
if (
|
||||||
|
compareIDs(right.origin, this.lastId) &&
|
||||||
|
this.right === right &&
|
||||||
|
compareIDs(this.rightOrigin, right.rightOrigin) &&
|
||||||
|
this.id.client === right.id.client &&
|
||||||
|
this.id.clock + this.length === right.id.clock &&
|
||||||
|
this.deleted === right.deleted &&
|
||||||
|
this.redone === null &&
|
||||||
|
right.redone === null &&
|
||||||
|
this.content.constructor === right.content.constructor &&
|
||||||
|
this.content.mergeWith(right.content)
|
||||||
|
) {
|
||||||
|
if (right.keep) {
|
||||||
|
this.keep = true
|
||||||
|
}
|
||||||
|
this.right = right.right
|
||||||
|
if (this.right !== null) {
|
||||||
|
this.right.left = this
|
||||||
|
}
|
||||||
|
this.length += right.length
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
delete (transaction) {
|
||||||
|
if (!this.deleted) {
|
||||||
|
const parent = this.parent
|
||||||
|
// adjust the length of parent
|
||||||
|
if (this.countable && this.parentSub === null) {
|
||||||
|
parent._length -= this.length
|
||||||
|
}
|
||||||
|
this.deleted = true
|
||||||
|
addToDeleteSet(transaction.deleteSet, this.id, this.length)
|
||||||
|
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
|
||||||
|
this.content.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {boolean} parentGCd
|
||||||
|
*/
|
||||||
|
gc (store, parentGCd) {
|
||||||
|
if (!this.deleted) {
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
|
this.content.gc(store)
|
||||||
|
if (parentGCd) {
|
||||||
|
replaceStruct(store, this, new GC(this.id, this.length))
|
||||||
|
} else {
|
||||||
|
this.content = new ContentDeleted(this.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the properties of this type to binary and write it to an
|
||||||
|
* BinaryEncoder.
|
||||||
|
*
|
||||||
|
* This is called when this Item is sent to a remote peer.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin
|
||||||
|
const rightOrigin = this.rightOrigin
|
||||||
|
const parentSub = this.parentSub
|
||||||
|
const info = (this.content.getRef() & binary.BITS5) |
|
||||||
|
(origin === null ? 0 : binary.BIT8) | // origin is defined
|
||||||
|
(rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined
|
||||||
|
(parentSub === null ? 0 : binary.BIT6) // parentSub is non-null
|
||||||
|
encoding.writeUint8(encoder, info)
|
||||||
|
if (origin !== null) {
|
||||||
|
writeID(encoder, origin)
|
||||||
|
}
|
||||||
|
if (rightOrigin !== null) {
|
||||||
|
writeID(encoder, rightOrigin)
|
||||||
|
}
|
||||||
|
if (origin === null && rightOrigin === null) {
|
||||||
|
const parent = this.parent
|
||||||
|
if (parent._item === null) {
|
||||||
|
// parent type on y._map
|
||||||
|
// find the correct key
|
||||||
|
const ykey = findRootTypeKey(parent)
|
||||||
|
encoding.writeVarUint(encoder, 1) // write parentYKey
|
||||||
|
encoding.writeVarString(encoder, ykey)
|
||||||
|
} else {
|
||||||
|
encoding.writeVarUint(encoder, 0) // write parent id
|
||||||
|
writeID(encoder, parent._item.id)
|
||||||
|
}
|
||||||
|
if (parentSub !== null) {
|
||||||
|
encoding.writeVarString(encoder, parentSub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.content.write(encoder, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lookup map for reading Item content.
|
||||||
|
*
|
||||||
|
* @type {Array<function(decoding.Decoder):AbstractContent>}
|
||||||
|
*/
|
||||||
|
export const contentRefs = [
|
||||||
|
() => { throw error.unexpectedCase() }, // GC is not ItemContent
|
||||||
|
readContentDeleted,
|
||||||
|
readContentJSON,
|
||||||
|
readContentBinary,
|
||||||
|
readContentString,
|
||||||
|
readContentEmbed,
|
||||||
|
readContentFormat,
|
||||||
|
readContentType,
|
||||||
|
readContentAny
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do not implement this class!
|
||||||
|
*/
|
||||||
|
export class AbstractContent {
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getLength () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Array<any>}
|
||||||
|
*/
|
||||||
|
getContent () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return false if this Item is some kind of meta information
|
||||||
|
* (e.g. format information).
|
||||||
|
*
|
||||||
|
* * Whether this Item should be addressable via `yarray.get(i)`
|
||||||
|
* * Whether this Item should be counted when computing yarray.length
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isCountable () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {AbstractContent}
|
||||||
|
*/
|
||||||
|
copy () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} offset
|
||||||
|
* @return {AbstractContent}
|
||||||
|
*/
|
||||||
|
splice (offset) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractContent} right
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
mergeWith (right) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
integrate (transaction, item) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
delete (transaction) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
gc (store) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {number} offset
|
||||||
|
*/
|
||||||
|
write (encoder, offset) {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
getRef () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class ItemRef extends AbstractStructRef {
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {number} info
|
||||||
|
*/
|
||||||
|
constructor (decoder, id, info) {
|
||||||
|
super(id)
|
||||||
|
/**
|
||||||
|
* The item that was originally to the left of this item.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.left = (info & binary.BIT8) === binary.BIT8 ? readID(decoder) : null
|
||||||
|
/**
|
||||||
|
* The item that was originally to the right of this item.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.right = (info & binary.BIT7) === binary.BIT7 ? readID(decoder) : null
|
||||||
|
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
|
const hasParentYKey = canCopyParentInfo ? decoding.readVarUint(decoder) === 1 : false
|
||||||
|
/**
|
||||||
|
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
|
* and we read the next string as parentYKey.
|
||||||
|
* It indicates how we store/retrieve parent from `y.share`
|
||||||
|
* @type {string|null}
|
||||||
|
*/
|
||||||
|
this.parentYKey = canCopyParentInfo && hasParentYKey ? decoding.readVarString(decoder) : null
|
||||||
|
/**
|
||||||
|
* The parent type.
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.parent = canCopyParentInfo && !hasParentYKey ? readID(decoder) : null
|
||||||
|
/**
|
||||||
|
* If the parent refers to this item with some kind of key (e.g. YMap, the
|
||||||
|
* key is specified here. The key is then used to refer to the list in which
|
||||||
|
* to insert this item. If `parentSub = null` type._start is the list in
|
||||||
|
* which to insert to. Otherwise it is `parent._map`.
|
||||||
|
* @type {String | null}
|
||||||
|
*/
|
||||||
|
this.parentSub = canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoding.readVarString(decoder) : null
|
||||||
|
const missing = this._missing
|
||||||
|
if (this.left !== null) {
|
||||||
|
missing.push(this.left)
|
||||||
|
}
|
||||||
|
if (this.right !== null) {
|
||||||
|
missing.push(this.right)
|
||||||
|
}
|
||||||
|
if (this.parent !== null) {
|
||||||
|
missing.push(this.parent)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @type {AbstractContent}
|
||||||
|
*/
|
||||||
|
this.content = readItemContent(decoder, info)
|
||||||
|
this.length = this.content.getLength()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {number} offset
|
||||||
|
* @return {Item|GC}
|
||||||
|
*/
|
||||||
|
toStruct (transaction, store, offset) {
|
||||||
|
if (offset > 0) {
|
||||||
|
/**
|
||||||
|
* @type {ID}
|
||||||
|
*/
|
||||||
|
const id = this.id
|
||||||
|
this.id = createID(id.client, id.clock + offset)
|
||||||
|
this.left = createID(this.id.client, this.id.clock - 1)
|
||||||
|
this.content = this.content.splice(offset)
|
||||||
|
this.length -= offset
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
|
||||||
|
const right = this.right === null ? null : getItemCleanStart(transaction, this.right)
|
||||||
|
let parent = null
|
||||||
|
let parentSub = this.parentSub
|
||||||
|
if (this.parent !== null) {
|
||||||
|
const parentItem = getItem(store, this.parent)
|
||||||
|
// Edge case: toStruct is called with an offset > 0. In this case left is defined.
|
||||||
|
// Depending in which order structs arrive, left may be GC'd and the parent not
|
||||||
|
// deleted. This is why we check if left is GC'd. Strictly we don't have
|
||||||
|
// to check if right is GC'd, but we will in case we run into future issues
|
||||||
|
if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
|
||||||
|
parent = /** @type {ContentType} */ (parentItem.content).type
|
||||||
|
}
|
||||||
|
} else if (this.parentYKey !== null) {
|
||||||
|
parent = transaction.doc.get(this.parentYKey)
|
||||||
|
} else if (left !== null) {
|
||||||
|
if (left.constructor !== GC) {
|
||||||
|
parent = left.parent
|
||||||
|
parentSub = left.parentSub
|
||||||
|
}
|
||||||
|
} else if (right !== null) {
|
||||||
|
if (right.constructor !== GC) {
|
||||||
|
parent = right.parent
|
||||||
|
parentSub = right.parentSub
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent === null
|
||||||
|
? new GC(this.id, this.length)
|
||||||
|
: new Item(
|
||||||
|
this.id,
|
||||||
|
left,
|
||||||
|
this.left,
|
||||||
|
right,
|
||||||
|
this.right,
|
||||||
|
parent,
|
||||||
|
parentSub,
|
||||||
|
this.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
592
src/types/AbstractType.js
Normal file
592
src/types/AbstractType.js
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
removeEventHandlerListener,
|
||||||
|
callEventHandlerListeners,
|
||||||
|
addEventHandlerListener,
|
||||||
|
createEventHandler,
|
||||||
|
nextID,
|
||||||
|
isVisible,
|
||||||
|
ContentType,
|
||||||
|
ContentAny,
|
||||||
|
ContentBinary,
|
||||||
|
createID,
|
||||||
|
getItemCleanStart,
|
||||||
|
Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as iterator from 'lib0/iterator.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call event listeners with an event. This will also add an event to all
|
||||||
|
* parents (for `.observeDeep` handlers).
|
||||||
|
*
|
||||||
|
* @template EventType
|
||||||
|
* @param {AbstractType<EventType>} type
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {EventType} event
|
||||||
|
*/
|
||||||
|
export const callTypeObservers = (type, transaction, event) => {
|
||||||
|
const changedType = type
|
||||||
|
const changedParentTypes = transaction.changedParentTypes
|
||||||
|
while (true) {
|
||||||
|
// @ts-ignore
|
||||||
|
map.setIfUndefined(changedParentTypes, type, () => []).push(event)
|
||||||
|
if (type._item === null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
type = type._item.parent
|
||||||
|
}
|
||||||
|
callEventHandlerListeners(changedType._eH, event, transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template EventType
|
||||||
|
* Abstract Yjs Type class
|
||||||
|
*/
|
||||||
|
export class AbstractType {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
this._item = null
|
||||||
|
/**
|
||||||
|
* @type {Map<string,Item>}
|
||||||
|
*/
|
||||||
|
this._map = new Map()
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
this._start = null
|
||||||
|
/**
|
||||||
|
* @type {Doc|null}
|
||||||
|
*/
|
||||||
|
this.doc = null
|
||||||
|
this._length = 0
|
||||||
|
/**
|
||||||
|
* Event handlers
|
||||||
|
* @type {EventHandler<EventType,Transaction>}
|
||||||
|
*/
|
||||||
|
this._eH = createEventHandler()
|
||||||
|
/**
|
||||||
|
* Deep event handlers
|
||||||
|
* @type {EventHandler<Array<YEvent>,Transaction>}
|
||||||
|
*/
|
||||||
|
this._dEH = createEventHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrate this type into the Yjs instance.
|
||||||
|
*
|
||||||
|
* * Save this struct in the os
|
||||||
|
* * This type is sent to other client
|
||||||
|
* * Observer functions are fired
|
||||||
|
*
|
||||||
|
* @param {Doc} y The Yjs instance
|
||||||
|
* @param {Item|null} item
|
||||||
|
*/
|
||||||
|
_integrate (y, item) {
|
||||||
|
this.doc = y
|
||||||
|
this._item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {AbstractType<EventType>}
|
||||||
|
*/
|
||||||
|
_copy () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
_write (encoder) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first non-deleted item
|
||||||
|
*/
|
||||||
|
get _first () {
|
||||||
|
let n = this._start
|
||||||
|
while (n !== null && n.deleted) {
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates YEvent and calls all type observers.
|
||||||
|
* Must be implemented by each type.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||||
|
*/
|
||||||
|
_callObserver (transaction, parentSubs) { /* skip if no type is specified */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe all events that are created on this type.
|
||||||
|
*
|
||||||
|
* @param {function(EventType, Transaction):void} f Observer function
|
||||||
|
*/
|
||||||
|
observe (f) {
|
||||||
|
addEventHandlerListener(this._eH, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe all events that are created by this type and its children.
|
||||||
|
*
|
||||||
|
* @param {function(Array<YEvent>,Transaction):void} f Observer function
|
||||||
|
*/
|
||||||
|
observeDeep (f) {
|
||||||
|
addEventHandlerListener(this._dEH, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an observer function.
|
||||||
|
*
|
||||||
|
* @param {function(EventType,Transaction):void} f Observer function
|
||||||
|
*/
|
||||||
|
unobserve (f) {
|
||||||
|
removeEventHandlerListener(this._eH, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an observer function.
|
||||||
|
*
|
||||||
|
* @param {function(Array<YEvent>,Transaction):void} f Observer function
|
||||||
|
*/
|
||||||
|
unobserveDeep (f) {
|
||||||
|
removeEventHandlerListener(this._dEH, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
toJSON () {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @return {Array<any>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListToArray = type => {
|
||||||
|
const cs = []
|
||||||
|
let n = type._start
|
||||||
|
while (n !== null) {
|
||||||
|
if (n.countable && !n.deleted) {
|
||||||
|
const c = n.content.getContent()
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
cs.push(c[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @return {Array<any>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListToArraySnapshot = (type, snapshot) => {
|
||||||
|
const cs = []
|
||||||
|
let n = type._start
|
||||||
|
while (n !== null) {
|
||||||
|
if (n.countable && isVisible(n, snapshot)) {
|
||||||
|
const c = n.content.getContent()
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
cs.push(c[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
|
*
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListForEach = (type, f) => {
|
||||||
|
let index = 0
|
||||||
|
let n = type._start
|
||||||
|
while (n !== null) {
|
||||||
|
if (n.countable && !n.deleted) {
|
||||||
|
const c = n.content.getContent()
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
f(c[i], index++, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template C,R
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {function(C,number,AbstractType<any>):R} f
|
||||||
|
* @return {Array<R>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListMap = (type, f) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<any>}
|
||||||
|
*/
|
||||||
|
const result = []
|
||||||
|
typeListForEach(type, (c, i) => {
|
||||||
|
result.push(f(c, i, type))
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @return {IterableIterator<any>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListCreateIterator = type => {
|
||||||
|
let n = type._start
|
||||||
|
/**
|
||||||
|
* @type {Array<any>|null}
|
||||||
|
*/
|
||||||
|
let currentContent = null
|
||||||
|
let currentContentIndex = 0
|
||||||
|
return {
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
next: () => {
|
||||||
|
// find some content
|
||||||
|
if (currentContent === null) {
|
||||||
|
while (n !== null && n.deleted) {
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||||
|
if (n === null) {
|
||||||
|
return {
|
||||||
|
done: true,
|
||||||
|
value: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we found n, so we can set currentContent
|
||||||
|
currentContent = n.content.getContent()
|
||||||
|
currentContentIndex = 0
|
||||||
|
n = n.right // we used the content of n, now iterate to next
|
||||||
|
}
|
||||||
|
const value = currentContent[currentContentIndex++]
|
||||||
|
// check if we need to empty currentContent
|
||||||
|
if (currentContent.length <= currentContentIndex) {
|
||||||
|
currentContent = null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
done: false,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
|
* Operates on a snapshotted state of the document.
|
||||||
|
*
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {function(any,number,AbstractType<any>):void} f A function to execute on every element of this YArray.
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListForEachSnapshot = (type, f, snapshot) => {
|
||||||
|
let index = 0
|
||||||
|
let n = type._start
|
||||||
|
while (n !== null) {
|
||||||
|
if (n.countable && isVisible(n, snapshot)) {
|
||||||
|
const c = n.content.getContent()
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
f(c[i], index++, type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {number} index
|
||||||
|
* @return {any}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListGet = (type, index) => {
|
||||||
|
for (let n = type._start; n !== null; n = n.right) {
|
||||||
|
if (!n.deleted && n.countable) {
|
||||||
|
if (index < n.length) {
|
||||||
|
return n.content.getContent()[index]
|
||||||
|
}
|
||||||
|
index -= n.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Item?} referenceItem
|
||||||
|
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
|
||||||
|
let left = referenceItem
|
||||||
|
const right = referenceItem === null ? parent._start : referenceItem.right
|
||||||
|
/**
|
||||||
|
* @type {Array<Object|Array<any>|number>}
|
||||||
|
*/
|
||||||
|
let jsonContent = []
|
||||||
|
const packJsonContent = () => {
|
||||||
|
if (jsonContent.length > 0) {
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentAny(jsonContent))
|
||||||
|
left.integrate(transaction)
|
||||||
|
jsonContent = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content.forEach(c => {
|
||||||
|
switch (c.constructor) {
|
||||||
|
case Number:
|
||||||
|
case Object:
|
||||||
|
case Boolean:
|
||||||
|
case Array:
|
||||||
|
case String:
|
||||||
|
jsonContent.push(c)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
packJsonContent()
|
||||||
|
switch (c.constructor) {
|
||||||
|
case Uint8Array:
|
||||||
|
case ArrayBuffer:
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
|
||||||
|
left.integrate(transaction)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (c instanceof AbstractType) {
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentType(c))
|
||||||
|
left.integrate(transaction)
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected content type in insert operation')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
packJsonContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {number} index
|
||||||
|
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListInsertGenerics = (transaction, parent, index, content) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return typeListInsertGenericsAfter(transaction, parent, null, content)
|
||||||
|
}
|
||||||
|
let n = parent._start
|
||||||
|
for (; n !== null; n = n.right) {
|
||||||
|
if (!n.deleted && n.countable) {
|
||||||
|
if (index <= n.length) {
|
||||||
|
if (index < n.length) {
|
||||||
|
// insert in-between
|
||||||
|
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
index -= n.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return typeListInsertGenericsAfter(transaction, parent, n, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {number} index
|
||||||
|
* @param {number} length
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListDelete = (transaction, parent, index, length) => {
|
||||||
|
if (length === 0) { return }
|
||||||
|
let n = parent._start
|
||||||
|
// compute the first item to be deleted
|
||||||
|
for (; n !== null && index > 0; n = n.right) {
|
||||||
|
if (!n.deleted && n.countable) {
|
||||||
|
if (index < n.length) {
|
||||||
|
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index))
|
||||||
|
}
|
||||||
|
index -= n.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete all items until done
|
||||||
|
while (length > 0 && n !== null) {
|
||||||
|
if (!n.deleted) {
|
||||||
|
if (length < n.length) {
|
||||||
|
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length))
|
||||||
|
}
|
||||||
|
n.delete(transaction)
|
||||||
|
length -= n.length
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
if (length > 0) {
|
||||||
|
throw error.create('array length exceeded')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {string} key
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapDelete = (transaction, parent, key) => {
|
||||||
|
const c = parent._map.get(key)
|
||||||
|
if (c !== undefined) {
|
||||||
|
c.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapSet = (transaction, parent, key, value) => {
|
||||||
|
const left = parent._map.get(key) || null
|
||||||
|
let content
|
||||||
|
if (value == null) {
|
||||||
|
content = new ContentAny([value])
|
||||||
|
} else {
|
||||||
|
switch (value.constructor) {
|
||||||
|
case Number:
|
||||||
|
case Object:
|
||||||
|
case Boolean:
|
||||||
|
case Array:
|
||||||
|
case String:
|
||||||
|
content = new ContentAny([value])
|
||||||
|
break
|
||||||
|
case Uint8Array:
|
||||||
|
content = new ContentBinary(/** @type {Uint8Array} */ (value))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (value instanceof AbstractType) {
|
||||||
|
content = new ContentType(value)
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected content type')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new Item(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, content).integrate(transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {string} key
|
||||||
|
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapGet = (parent, key) => {
|
||||||
|
const val = parent._map.get(key)
|
||||||
|
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapGetAll = (parent) => {
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const res = {}
|
||||||
|
for (const [key, value] of parent._map) {
|
||||||
|
if (!value.deleted) {
|
||||||
|
res[key] = value.content.getContent()[value.length - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {string} key
|
||||||
|
* @return {boolean}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapHas = (parent, key) => {
|
||||||
|
const val = parent._map.get(key)
|
||||||
|
return val !== undefined && !val.deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapGetSnapshot = (parent, key, snapshot) => {
|
||||||
|
let v = parent._map.get(key) || null
|
||||||
|
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
|
||||||
|
v = v.left
|
||||||
|
}
|
||||||
|
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<string,Item>} map
|
||||||
|
* @return {IterableIterator<Array<any>>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)
|
||||||
211
src/types/YArray.js
Normal file
211
src/types/YArray.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
/**
|
||||||
|
* @module YArray
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
YEvent,
|
||||||
|
AbstractType,
|
||||||
|
typeListGet,
|
||||||
|
typeListToArray,
|
||||||
|
typeListForEach,
|
||||||
|
typeListCreateIterator,
|
||||||
|
typeListInsertGenerics,
|
||||||
|
typeListDelete,
|
||||||
|
typeListMap,
|
||||||
|
YArrayRefID,
|
||||||
|
callTypeObservers,
|
||||||
|
transact,
|
||||||
|
Doc, Transaction, Item // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that describes the changes on a YArray
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
export class YArrayEvent extends YEvent {
|
||||||
|
/**
|
||||||
|
* @param {YArray<T>} yarray The changed type
|
||||||
|
* @param {Transaction} transaction The transaction object
|
||||||
|
*/
|
||||||
|
constructor (yarray, transaction) {
|
||||||
|
super(yarray, transaction)
|
||||||
|
this._transaction = transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shared Array implementation.
|
||||||
|
* @template T
|
||||||
|
* @extends AbstractType<YArrayEvent<T>>
|
||||||
|
* @implements {IterableIterator<T>}
|
||||||
|
*/
|
||||||
|
export class YArray extends AbstractType {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
/**
|
||||||
|
* @type {Array<any>?}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._prelimContent = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrate this type into the Yjs instance.
|
||||||
|
*
|
||||||
|
* * Save this struct in the os
|
||||||
|
* * This type is sent to other client
|
||||||
|
* * Observer functions are fired
|
||||||
|
*
|
||||||
|
* @param {Doc} y The Yjs instance
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
_integrate (y, item) {
|
||||||
|
super._integrate(y, item)
|
||||||
|
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||||
|
this._prelimContent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_copy () {
|
||||||
|
return new YArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
get length () {
|
||||||
|
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates YArrayEvent and calls observers.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||||
|
*/
|
||||||
|
_callObserver (transaction, parentSubs) {
|
||||||
|
callTypeObservers(this, transaction, new YArrayEvent(this, transaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts new content at an index.
|
||||||
|
*
|
||||||
|
* Important: This function expects an array of content. Not just a content
|
||||||
|
* object. The reason for this "weirdness" is that inserting several elements
|
||||||
|
* is very efficient when it is done as a single operation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Insert character 'a' at position 0
|
||||||
|
* yarray.insert(0, ['a'])
|
||||||
|
* // Insert numbers 1, 2 at position 1
|
||||||
|
* yarray.insert(1, [1, 2])
|
||||||
|
*
|
||||||
|
* @param {number} index The index to insert content at.
|
||||||
|
* @param {Array<T>} content The array of content
|
||||||
|
*/
|
||||||
|
insert (index, content) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeListInsertGenerics(transaction, this, index, content)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends content to this YArray.
|
||||||
|
*
|
||||||
|
* @param {Array<T>} content Array of content to append.
|
||||||
|
*/
|
||||||
|
push (content) {
|
||||||
|
this.insert(this.length, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes elements starting from an index.
|
||||||
|
*
|
||||||
|
* @param {number} index Index at which to start deleting elements
|
||||||
|
* @param {number} length The number of elements to remove. Defaults to 1.
|
||||||
|
*/
|
||||||
|
delete (index, length = 1) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeListDelete(transaction, this, index, length)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the i-th element from a YArray.
|
||||||
|
*
|
||||||
|
* @param {number} index The index of the element to return from the YArray
|
||||||
|
* @return {T}
|
||||||
|
*/
|
||||||
|
get (index) {
|
||||||
|
return typeListGet(this, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this YArray to a JavaScript Array.
|
||||||
|
*
|
||||||
|
* @return {Array<T>}
|
||||||
|
*/
|
||||||
|
toArray () {
|
||||||
|
return typeListToArray(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this Shared Type to a JSON object.
|
||||||
|
*
|
||||||
|
* @return {Array<any>}
|
||||||
|
*/
|
||||||
|
toJSON () {
|
||||||
|
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Array with the result of calling a provided function on every
|
||||||
|
* element of this YArray.
|
||||||
|
*
|
||||||
|
* @template T,M
|
||||||
|
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
|
||||||
|
* @return {Array<M>} A new array with each element being the result of the
|
||||||
|
* callback function
|
||||||
|
*/
|
||||||
|
map (f) {
|
||||||
|
return typeListMap(this, /** @type {any} */ (f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
|
*
|
||||||
|
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||||
|
*/
|
||||||
|
forEach (f) {
|
||||||
|
typeListForEach(this, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {IterableIterator<T>}
|
||||||
|
*/
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return typeListCreateIterator(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
encoding.writeVarUint(encoder, YArrayRefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYArray = decoder => new YArray()
|
||||||
226
src/types/YMap.js
Normal file
226
src/types/YMap.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @module YMap
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
YEvent,
|
||||||
|
AbstractType,
|
||||||
|
typeMapDelete,
|
||||||
|
typeMapSet,
|
||||||
|
typeMapGet,
|
||||||
|
typeMapHas,
|
||||||
|
createMapIterator,
|
||||||
|
YMapRefID,
|
||||||
|
callTypeObservers,
|
||||||
|
transact,
|
||||||
|
Doc, Transaction, Item // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||||
|
import * as iterator from 'lib0/iterator.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* Event that describes the changes on a YMap.
|
||||||
|
*/
|
||||||
|
export class YMapEvent extends YEvent {
|
||||||
|
/**
|
||||||
|
* @param {YMap<T>} ymap The YArray that changed.
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<any>} subs The keys that changed.
|
||||||
|
*/
|
||||||
|
constructor (ymap, transaction, subs) {
|
||||||
|
super(ymap, transaction)
|
||||||
|
this.keysChanged = subs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T number|string|Object|Array|Uint8Array
|
||||||
|
* A shared Map implementation.
|
||||||
|
*
|
||||||
|
* @extends AbstractType<YMapEvent<T>>
|
||||||
|
* @implements {IterableIterator}
|
||||||
|
*/
|
||||||
|
export class YMap extends AbstractType {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
/**
|
||||||
|
* @type {Map<string,any>?}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._prelimContent = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrate this type into the Yjs instance.
|
||||||
|
*
|
||||||
|
* * Save this struct in the os
|
||||||
|
* * This type is sent to other client
|
||||||
|
* * Observer functions are fired
|
||||||
|
*
|
||||||
|
* @param {Doc} y The Yjs instance
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
_integrate (y, item) {
|
||||||
|
super._integrate(y, item)
|
||||||
|
for (const [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) {
|
||||||
|
this.set(key, value)
|
||||||
|
}
|
||||||
|
this._prelimContent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_copy () {
|
||||||
|
return new YMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates YMapEvent and calls observers.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||||
|
*/
|
||||||
|
_callObserver (transaction, parentSubs) {
|
||||||
|
callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this Shared Type to a JSON object.
|
||||||
|
*
|
||||||
|
* @return {Object<string,T>}
|
||||||
|
*/
|
||||||
|
toJSON () {
|
||||||
|
/**
|
||||||
|
* @type {Object<string,T>}
|
||||||
|
*/
|
||||||
|
const map = {}
|
||||||
|
for (const [key, item] of this._map) {
|
||||||
|
if (!item.deleted) {
|
||||||
|
const v = item.content.getContent()[item.length - 1]
|
||||||
|
map[key] = v instanceof AbstractType ? v.toJSON() : v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the keys for each element in the YMap Type.
|
||||||
|
*
|
||||||
|
* @return {IterableIterator<string>}
|
||||||
|
*/
|
||||||
|
keys () {
|
||||||
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the keys for each element in the YMap Type.
|
||||||
|
*
|
||||||
|
* @return {IterableIterator<string>}
|
||||||
|
*/
|
||||||
|
values () {
|
||||||
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Iterator of [key, value] pairs
|
||||||
|
*
|
||||||
|
* @return {IterableIterator<any>}
|
||||||
|
*/
|
||||||
|
entries () {
|
||||||
|
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a provided function on once on overy key-value pair.
|
||||||
|
*
|
||||||
|
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
|
||||||
|
*/
|
||||||
|
forEach (f) {
|
||||||
|
/**
|
||||||
|
* @type {Object<string,T>}
|
||||||
|
*/
|
||||||
|
const map = {}
|
||||||
|
for (const [key, item] of this._map) {
|
||||||
|
if (!item.deleted) {
|
||||||
|
f(item.content.getContent()[item.length - 1], key, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {IterableIterator<T>}
|
||||||
|
*/
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this.entries()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a specified element from this YMap.
|
||||||
|
*
|
||||||
|
* @param {string} key The key of the element to remove.
|
||||||
|
*/
|
||||||
|
delete (key) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeMapDelete(transaction, this, key)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Map<string, any>} */ (this._prelimContent).delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates an element with a specified key and value.
|
||||||
|
*
|
||||||
|
* @param {string} key The key of the element to add to this YMap
|
||||||
|
* @param {T} value The value of the element to add
|
||||||
|
*/
|
||||||
|
set (key, value) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeMapSet(transaction, this, key, value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a specified element from this YMap.
|
||||||
|
*
|
||||||
|
* @param {string} key
|
||||||
|
* @return {T|undefined}
|
||||||
|
*/
|
||||||
|
get (key) {
|
||||||
|
return /** @type {any} */ (typeMapGet(this, key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating whether the specified key exists or not.
|
||||||
|
*
|
||||||
|
* @param {string} key The key to test.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
has (key) {
|
||||||
|
return typeMapHas(this, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
encoding.writeVarUint(encoder, YMapRefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYMap = decoder => new YMap()
|
||||||
948
src/types/YText.js
Normal file
948
src/types/YText.js
Normal file
@@ -0,0 +1,948 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @module YText
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
YEvent,
|
||||||
|
AbstractType,
|
||||||
|
nextID,
|
||||||
|
createID,
|
||||||
|
getItemCleanStart,
|
||||||
|
isVisible,
|
||||||
|
YTextRefID,
|
||||||
|
callTypeObservers,
|
||||||
|
transact,
|
||||||
|
ContentEmbed,
|
||||||
|
ContentFormat,
|
||||||
|
ContentString,
|
||||||
|
splitSnapshotAffectedStructs,
|
||||||
|
ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as object from 'lib0/object.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} a
|
||||||
|
* @param {any} b
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
|
||||||
|
|
||||||
|
export class ItemListPosition {
|
||||||
|
/**
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
*/
|
||||||
|
constructor (left, right) {
|
||||||
|
this.left = left
|
||||||
|
this.right = right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemTextListPosition extends ItemListPosition {
|
||||||
|
/**
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
*/
|
||||||
|
constructor (left, right, currentAttributes) {
|
||||||
|
super(left, right)
|
||||||
|
this.currentAttributes = currentAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemInsertionResult extends ItemListPosition {
|
||||||
|
/**
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} negatedAttributes
|
||||||
|
*/
|
||||||
|
constructor (left, right, negatedAttributes) {
|
||||||
|
super(left, right)
|
||||||
|
this.negatedAttributes = negatedAttributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {number} count
|
||||||
|
* @return {ItemTextListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const findNextPosition = (transaction, currentAttributes, left, right, count) => {
|
||||||
|
while (right !== null && count > 0) {
|
||||||
|
switch (right.content.constructor) {
|
||||||
|
case ContentEmbed:
|
||||||
|
case ContentString:
|
||||||
|
if (!right.deleted) {
|
||||||
|
if (count < right.length) {
|
||||||
|
// split right
|
||||||
|
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + count))
|
||||||
|
}
|
||||||
|
count -= right.length
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ContentFormat:
|
||||||
|
if (!right.deleted) {
|
||||||
|
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
right = right.right
|
||||||
|
}
|
||||||
|
return new ItemTextListPosition(left, right, currentAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {number} index
|
||||||
|
* @return {ItemTextListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const findPosition = (transaction, parent, index) => {
|
||||||
|
const currentAttributes = new Map()
|
||||||
|
const right = parent._start
|
||||||
|
return findNextPosition(transaction, currentAttributes, null, right, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Negate applied formats
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} negatedAttributes
|
||||||
|
* @return {ItemListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const insertNegatedAttributes = (transaction, parent, left, right, negatedAttributes) => {
|
||||||
|
// check if we really need to remove attributes
|
||||||
|
while (
|
||||||
|
right !== null && (
|
||||||
|
right.deleted === true || (
|
||||||
|
right.content.constructor === ContentFormat &&
|
||||||
|
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (right.content).key), /** @type {ContentFormat} */ (right.content).value)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!right.deleted) {
|
||||||
|
negatedAttributes.delete(/** @type {ContentFormat} */ (right.content).key)
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
right = right.right
|
||||||
|
}
|
||||||
|
for (const [key, val] of negatedAttributes) {
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
|
||||||
|
left.integrate(transaction)
|
||||||
|
}
|
||||||
|
return { left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {ContentFormat} format
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const updateCurrentAttributes = (currentAttributes, format) => {
|
||||||
|
const { key, value } = format
|
||||||
|
if (value === null) {
|
||||||
|
currentAttributes.delete(key)
|
||||||
|
} else {
|
||||||
|
currentAttributes.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {Object<string,any>} attributes
|
||||||
|
* @return {ItemListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const minimizeAttributeChanges = (left, right, currentAttributes, attributes) => {
|
||||||
|
// go right while attributes[right.key] === right.value (or right is deleted)
|
||||||
|
while (true) {
|
||||||
|
if (right === null) {
|
||||||
|
break
|
||||||
|
} else if (right.deleted) {
|
||||||
|
// continue
|
||||||
|
} else if (right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (right.content)).key] || null, /** @type {ContentFormat} */ (right.content).value)) {
|
||||||
|
// found a format, update currentAttributes and continue
|
||||||
|
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
right = right.right
|
||||||
|
}
|
||||||
|
return new ItemListPosition(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {Object<string,any>} attributes
|
||||||
|
* @return {ItemInsertionResult}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
**/
|
||||||
|
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
|
||||||
|
const negatedAttributes = new Map()
|
||||||
|
// insert format-start items
|
||||||
|
for (const key in attributes) {
|
||||||
|
const val = attributes[key]
|
||||||
|
const currentVal = currentAttributes.get(key) || null
|
||||||
|
if (!equalAttrs(currentVal, val)) {
|
||||||
|
// save negated attribute (set null if currentVal undefined)
|
||||||
|
negatedAttributes.set(key, currentVal)
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
|
||||||
|
left.integrate(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ItemInsertionResult(left, right, negatedAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {string|object} text
|
||||||
|
* @param {Object<string,any>} attributes
|
||||||
|
* @return {ItemListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
**/
|
||||||
|
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
|
||||||
|
for (const [key] of currentAttributes) {
|
||||||
|
if (attributes[key] === undefined) {
|
||||||
|
attributes[key] = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
|
||||||
|
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
|
||||||
|
left = insertPos.left
|
||||||
|
right = insertPos.right
|
||||||
|
// insert content
|
||||||
|
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
|
||||||
|
left.integrate(transaction)
|
||||||
|
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {number} length
|
||||||
|
* @param {Object<string,any>} attributes
|
||||||
|
* @return {ItemListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => {
|
||||||
|
const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
|
||||||
|
const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
|
||||||
|
const negatedAttributes = insertPos.negatedAttributes
|
||||||
|
left = insertPos.left
|
||||||
|
right = insertPos.right
|
||||||
|
// iterate until first non-format or null is found
|
||||||
|
// delete all formats with attributes[format.key] != null
|
||||||
|
while (length > 0 && right !== null) {
|
||||||
|
if (!right.deleted) {
|
||||||
|
switch (right.content.constructor) {
|
||||||
|
case ContentFormat: {
|
||||||
|
const { key, value } = /** @type {ContentFormat} */ (right.content)
|
||||||
|
const attr = attributes[key]
|
||||||
|
if (attr !== undefined) {
|
||||||
|
if (equalAttrs(attr, value)) {
|
||||||
|
negatedAttributes.delete(key)
|
||||||
|
} else {
|
||||||
|
negatedAttributes.set(key, value)
|
||||||
|
}
|
||||||
|
right.delete(transaction)
|
||||||
|
}
|
||||||
|
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentEmbed:
|
||||||
|
case ContentString:
|
||||||
|
if (length < right.length) {
|
||||||
|
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
|
||||||
|
}
|
||||||
|
length -= right.length
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
right = right.right
|
||||||
|
}
|
||||||
|
// Quill just assumes that the editor starts with a newline and that it always
|
||||||
|
// ends with a newline. We only insert that newline when a new newline is
|
||||||
|
// inserted - i.e when length is bigger than type.length
|
||||||
|
if (length > 0) {
|
||||||
|
let newlines = ''
|
||||||
|
for (; length > 0; length--) {
|
||||||
|
newlines += '\n'
|
||||||
|
}
|
||||||
|
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentString(newlines))
|
||||||
|
left.integrate(transaction)
|
||||||
|
}
|
||||||
|
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Item|null} left
|
||||||
|
* @param {Item|null} right
|
||||||
|
* @param {Map<string,any>} currentAttributes
|
||||||
|
* @param {number} length
|
||||||
|
* @return {ItemListPosition}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const deleteText = (transaction, left, right, currentAttributes, length) => {
|
||||||
|
while (length > 0 && right !== null) {
|
||||||
|
if (right.deleted === false) {
|
||||||
|
switch (right.content.constructor) {
|
||||||
|
case ContentFormat:
|
||||||
|
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
|
||||||
|
break
|
||||||
|
case ContentEmbed:
|
||||||
|
case ContentString:
|
||||||
|
if (length < right.length) {
|
||||||
|
getItemCleanStart(transaction, createID(right.id.client, right.id.clock + length))
|
||||||
|
}
|
||||||
|
length -= right.length
|
||||||
|
right.delete(transaction)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
left = right
|
||||||
|
right = right.right
|
||||||
|
}
|
||||||
|
return { left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Quill Delta format represents changes on a text document with
|
||||||
|
* formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* ops: [
|
||||||
|
* { insert: 'Gandalf', attributes: { bold: true } },
|
||||||
|
* { insert: ' the ' },
|
||||||
|
* { insert: 'Grey', attributes: { color: '#cccccc' } }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes that can be assigned to a selection of text.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* bold: true,
|
||||||
|
* font-size: '40px'
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @typedef {Object} TextAttributes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} DeltaItem
|
||||||
|
* @property {number|undefined} DeltaItem.delete
|
||||||
|
* @property {number|undefined} DeltaItem.retain
|
||||||
|
* @property {string|undefined} DeltaItem.string
|
||||||
|
* @property {Object<string,any>} DeltaItem.attributes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that describes the changes on a YText type.
|
||||||
|
*/
|
||||||
|
export class YTextEvent extends YEvent {
|
||||||
|
/**
|
||||||
|
* @param {YText} ytext
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
constructor (ytext, transaction) {
|
||||||
|
super(ytext, transaction)
|
||||||
|
/**
|
||||||
|
* @type {Array<DeltaItem>|null}
|
||||||
|
*/
|
||||||
|
this._delta = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the changes in the delta format.
|
||||||
|
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||||
|
*
|
||||||
|
* @type {Array<DeltaItem>}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
get delta () {
|
||||||
|
if (this._delta === null) {
|
||||||
|
const y = /** @type {Doc} */ (this.target.doc)
|
||||||
|
this._delta = []
|
||||||
|
transact(y, transaction => {
|
||||||
|
const delta = /** @type {Array<DeltaItem>} */ (this._delta)
|
||||||
|
const currentAttributes = new Map() // saves all current attributes for insert
|
||||||
|
const oldAttributes = new Map()
|
||||||
|
let item = this.target._start
|
||||||
|
/**
|
||||||
|
* @type {string?}
|
||||||
|
*/
|
||||||
|
let action = null
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const attributes = {} // counts added or removed new attributes for retain
|
||||||
|
/**
|
||||||
|
* @type {string|object}
|
||||||
|
*/
|
||||||
|
let insert = ''
|
||||||
|
let retain = 0
|
||||||
|
let deleteLen = 0
|
||||||
|
const addOp = () => {
|
||||||
|
if (action !== null) {
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let op
|
||||||
|
switch (action) {
|
||||||
|
case 'delete':
|
||||||
|
op = { delete: deleteLen }
|
||||||
|
deleteLen = 0
|
||||||
|
break
|
||||||
|
case 'insert':
|
||||||
|
op = { insert }
|
||||||
|
if (currentAttributes.size > 0) {
|
||||||
|
op.attributes = {}
|
||||||
|
for (const [key, value] of currentAttributes) {
|
||||||
|
if (value !== null) {
|
||||||
|
op.attributes[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insert = ''
|
||||||
|
break
|
||||||
|
case 'retain':
|
||||||
|
op = { retain }
|
||||||
|
if (Object.keys(attributes).length > 0) {
|
||||||
|
op.attributes = {}
|
||||||
|
for (const key in attributes) {
|
||||||
|
op.attributes[key] = attributes[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retain = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delta.push(op)
|
||||||
|
action = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (item !== null) {
|
||||||
|
switch (item.content.constructor) {
|
||||||
|
case ContentEmbed:
|
||||||
|
if (this.adds(item)) {
|
||||||
|
if (!this.deletes(item)) {
|
||||||
|
addOp()
|
||||||
|
action = 'insert'
|
||||||
|
insert = /** @type {ContentEmbed} */ (item.content).embed
|
||||||
|
addOp()
|
||||||
|
}
|
||||||
|
} else if (this.deletes(item)) {
|
||||||
|
if (action !== 'delete') {
|
||||||
|
addOp()
|
||||||
|
action = 'delete'
|
||||||
|
}
|
||||||
|
deleteLen += 1
|
||||||
|
} else if (!item.deleted) {
|
||||||
|
if (action !== 'retain') {
|
||||||
|
addOp()
|
||||||
|
action = 'retain'
|
||||||
|
}
|
||||||
|
retain += 1
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ContentString:
|
||||||
|
if (this.adds(item)) {
|
||||||
|
if (!this.deletes(item)) {
|
||||||
|
if (action !== 'insert') {
|
||||||
|
addOp()
|
||||||
|
action = 'insert'
|
||||||
|
}
|
||||||
|
insert += /** @type {ContentString} */ (item.content).str
|
||||||
|
}
|
||||||
|
} else if (this.deletes(item)) {
|
||||||
|
if (action !== 'delete') {
|
||||||
|
addOp()
|
||||||
|
action = 'delete'
|
||||||
|
}
|
||||||
|
deleteLen += item.length
|
||||||
|
} else if (!item.deleted) {
|
||||||
|
if (action !== 'retain') {
|
||||||
|
addOp()
|
||||||
|
action = 'retain'
|
||||||
|
}
|
||||||
|
retain += item.length
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case ContentFormat: {
|
||||||
|
const { key, value } = /** @type {ContentFormat} */ (item.content)
|
||||||
|
if (this.adds(item)) {
|
||||||
|
if (!this.deletes(item)) {
|
||||||
|
const curVal = currentAttributes.get(key) || null
|
||||||
|
if (!equalAttrs(curVal, value)) {
|
||||||
|
if (action === 'retain') {
|
||||||
|
addOp()
|
||||||
|
}
|
||||||
|
if (equalAttrs(value, (oldAttributes.get(key) || null))) {
|
||||||
|
delete attributes[key]
|
||||||
|
} else {
|
||||||
|
attributes[key] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.deletes(item)) {
|
||||||
|
oldAttributes.set(key, value)
|
||||||
|
const curVal = currentAttributes.get(key) || null
|
||||||
|
if (!equalAttrs(curVal, value)) {
|
||||||
|
if (action === 'retain') {
|
||||||
|
addOp()
|
||||||
|
}
|
||||||
|
attributes[key] = curVal
|
||||||
|
}
|
||||||
|
} else if (!item.deleted) {
|
||||||
|
oldAttributes.set(key, value)
|
||||||
|
const attr = attributes[key]
|
||||||
|
if (attr !== undefined) {
|
||||||
|
if (!equalAttrs(attr, value)) {
|
||||||
|
if (action === 'retain') {
|
||||||
|
addOp()
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
attributes[key] = value
|
||||||
|
} else {
|
||||||
|
delete attributes[key]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!item.deleted) {
|
||||||
|
if (action === 'insert') {
|
||||||
|
addOp()
|
||||||
|
}
|
||||||
|
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item = item.right
|
||||||
|
}
|
||||||
|
addOp()
|
||||||
|
while (delta.length > 0) {
|
||||||
|
const lastOp = delta[delta.length - 1]
|
||||||
|
if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
|
||||||
|
// retain delta's if they don't assign attributes
|
||||||
|
delta.pop()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return this._delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type that represents text with formatting information.
|
||||||
|
*
|
||||||
|
* This type replaces y-richtext as this implementation is able to handle
|
||||||
|
* block formats (format information on a paragraph), embeds (complex elements
|
||||||
|
* like pictures and videos), and text formats (**bold**, *italic*).
|
||||||
|
*
|
||||||
|
* @extends AbstractType<YTextEvent>
|
||||||
|
*/
|
||||||
|
export class YText extends AbstractType {
|
||||||
|
/**
|
||||||
|
* @param {String} [string] The initial value of the YText.
|
||||||
|
*/
|
||||||
|
constructor (string) {
|
||||||
|
super()
|
||||||
|
/**
|
||||||
|
* Array of pending operations on this type
|
||||||
|
* @type {Array<function():void>?}
|
||||||
|
*/
|
||||||
|
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of characters of this text type.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
get length () {
|
||||||
|
return this._length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Doc} y
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
_integrate (y, item) {
|
||||||
|
super._integrate(y, item)
|
||||||
|
try {
|
||||||
|
/** @type {Array<function>} */ (this._pending).forEach(f => f())
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
this._pending = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_copy () {
|
||||||
|
return new YText()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates YTextEvent and calls observers.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||||
|
*/
|
||||||
|
_callObserver (transaction, parentSubs) {
|
||||||
|
callTypeObservers(this, transaction, new YTextEvent(this, transaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the unformatted string representation of this YText type.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toString () {
|
||||||
|
let str = ''
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let n = this._start
|
||||||
|
while (n !== null) {
|
||||||
|
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
|
||||||
|
str += /** @type {ContentString} */ (n.content).str
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the unformatted string representation of this YText type.
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toJSON () {
|
||||||
|
return this.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a {@link Delta} on this shared YText type.
|
||||||
|
*
|
||||||
|
* @param {any} delta The changes to apply on this element.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
applyDelta (delta) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
/**
|
||||||
|
* @type {ItemListPosition}
|
||||||
|
*/
|
||||||
|
let pos = new ItemListPosition(null, this._start)
|
||||||
|
const currentAttributes = new Map()
|
||||||
|
for (let i = 0; i < delta.length; i++) {
|
||||||
|
const op = delta[i]
|
||||||
|
if (op.insert !== undefined) {
|
||||||
|
// Quill assumes that the content starts with an empty paragraph.
|
||||||
|
// Yjs/Y.Text assumes that it starts empty. We always hide that
|
||||||
|
// there is a newline at the end of the content.
|
||||||
|
// If we omit this step, clients will see a different number of
|
||||||
|
// paragraphs, but nothing bad will happen.
|
||||||
|
const ins = (typeof op.insert === 'string' && i === delta.length - 1 && pos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
|
||||||
|
if (typeof ins !== 'string' || ins.length > 0) {
|
||||||
|
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, ins, op.attributes || {})
|
||||||
|
}
|
||||||
|
} else if (op.retain !== undefined) {
|
||||||
|
pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {})
|
||||||
|
} else if (op.delete !== undefined) {
|
||||||
|
pos = deleteText(transaction, pos.left, pos.right, currentAttributes, op.delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(delta))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Delta representation of this YText type.
|
||||||
|
*
|
||||||
|
* @param {Snapshot} [snapshot]
|
||||||
|
* @param {Snapshot} [prevSnapshot]
|
||||||
|
* @param {function('removed' | 'added', ID):any} [computeYChange]
|
||||||
|
* @return {any} The Delta representation of this type.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toDelta (snapshot, prevSnapshot, computeYChange) {
|
||||||
|
/**
|
||||||
|
* @type{Array<any>}
|
||||||
|
*/
|
||||||
|
const ops = []
|
||||||
|
const currentAttributes = new Map()
|
||||||
|
const doc = /** @type {Doc} */ (this.doc)
|
||||||
|
let str = ''
|
||||||
|
let n = this._start
|
||||||
|
function packStr () {
|
||||||
|
if (str.length > 0) {
|
||||||
|
// pack str with attributes to ops
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const attributes = {}
|
||||||
|
let addAttributes = false
|
||||||
|
for (const [key, value] of currentAttributes) {
|
||||||
|
addAttributes = true
|
||||||
|
attributes[key] = value
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const op = { insert: str }
|
||||||
|
if (addAttributes) {
|
||||||
|
op.attributes = attributes
|
||||||
|
}
|
||||||
|
ops.push(op)
|
||||||
|
str = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// snapshots are merged again after the transaction, so we need to keep the
|
||||||
|
// transalive until we are done
|
||||||
|
transact(doc, transaction => {
|
||||||
|
if (snapshot) {
|
||||||
|
splitSnapshotAffectedStructs(transaction, snapshot)
|
||||||
|
}
|
||||||
|
if (prevSnapshot) {
|
||||||
|
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
||||||
|
}
|
||||||
|
while (n !== null) {
|
||||||
|
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||||
|
switch (n.content.constructor) {
|
||||||
|
case ContentString: {
|
||||||
|
const cur = currentAttributes.get('ychange')
|
||||||
|
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||||
|
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
|
||||||
|
packStr()
|
||||||
|
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
|
||||||
|
}
|
||||||
|
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||||
|
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'added') {
|
||||||
|
packStr()
|
||||||
|
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
|
||||||
|
}
|
||||||
|
} else if (cur !== undefined) {
|
||||||
|
packStr()
|
||||||
|
currentAttributes.delete('ychange')
|
||||||
|
}
|
||||||
|
str += /** @type {ContentString} */ (n.content).str
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentEmbed: {
|
||||||
|
packStr()
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const op = {
|
||||||
|
insert: /** @type {ContentEmbed} */ (n.content).embed
|
||||||
|
}
|
||||||
|
if (currentAttributes.size > 0) {
|
||||||
|
const attrs = /** @type {Object<string,any>} */ ({})
|
||||||
|
op.attributes = attrs
|
||||||
|
for (const [key, value] of currentAttributes) {
|
||||||
|
attrs[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.push(op)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ContentFormat:
|
||||||
|
if (isVisible(n, snapshot)) {
|
||||||
|
packStr()
|
||||||
|
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
packStr()
|
||||||
|
}, splitSnapshotAffectedStructs)
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert text at a given index.
|
||||||
|
*
|
||||||
|
* @param {number} index The index at which to start inserting.
|
||||||
|
* @param {String} text The text to insert at the specified position.
|
||||||
|
* @param {TextAttributes} [attributes] Optionally define some formatting
|
||||||
|
* information to apply on the inserted
|
||||||
|
* Text.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
insert (index, text, attributes) {
|
||||||
|
if (text.length <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const y = this.doc
|
||||||
|
if (y !== null) {
|
||||||
|
transact(y, transaction => {
|
||||||
|
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||||
|
if (!attributes) {
|
||||||
|
attributes = {}
|
||||||
|
// @ts-ignore
|
||||||
|
currentAttributes.forEach((v, k) => { attributes[k] = v })
|
||||||
|
}
|
||||||
|
insertText(transaction, this, left, right, currentAttributes, text, attributes)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts an embed at a index.
|
||||||
|
*
|
||||||
|
* @param {number} index The index to insert the embed at.
|
||||||
|
* @param {Object} embed The Object that represents the embed.
|
||||||
|
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||||
|
* embed
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
insertEmbed (index, embed, attributes = {}) {
|
||||||
|
if (embed.constructor !== Object) {
|
||||||
|
throw new Error('Embed must be an Object')
|
||||||
|
}
|
||||||
|
const y = this.doc
|
||||||
|
if (y !== null) {
|
||||||
|
transact(y, transaction => {
|
||||||
|
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||||
|
insertText(transaction, this, left, right, currentAttributes, embed, attributes)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes text starting from an index.
|
||||||
|
*
|
||||||
|
* @param {number} index Index at which to start deleting.
|
||||||
|
* @param {number} length The number of characters to remove. Defaults to 1.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
delete (index, length) {
|
||||||
|
if (length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const y = this.doc
|
||||||
|
if (y !== null) {
|
||||||
|
transact(y, transaction => {
|
||||||
|
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||||
|
deleteText(transaction, left, right, currentAttributes, length)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns properties to a range of text.
|
||||||
|
*
|
||||||
|
* @param {number} index The position where to start formatting.
|
||||||
|
* @param {number} length The amount of characters to assign properties to.
|
||||||
|
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||||
|
* text.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
format (index, length, attributes) {
|
||||||
|
const y = this.doc
|
||||||
|
if (y !== null) {
|
||||||
|
transact(y, transaction => {
|
||||||
|
const { left, right, currentAttributes } = findPosition(transaction, this, index)
|
||||||
|
if (right === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
formatText(transaction, this, left, right, currentAttributes, length, attributes)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
encoding.writeVarUint(encoder, YTextRefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {YText}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYText = decoder => new YText()
|
||||||
198
src/types/YXmlElement.js
Normal file
198
src/types/YXmlElement.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
YXmlFragment,
|
||||||
|
transact,
|
||||||
|
typeMapDelete,
|
||||||
|
typeMapSet,
|
||||||
|
typeMapGet,
|
||||||
|
typeMapGetAll,
|
||||||
|
typeListForEach,
|
||||||
|
YXmlElementRefID,
|
||||||
|
Snapshot, Doc, Item // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An YXmlElement imitates the behavior of a
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
|
||||||
|
*
|
||||||
|
* * An YXmlElement has attributes (key value pairs)
|
||||||
|
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||||
|
*/
|
||||||
|
export class YXmlElement extends YXmlFragment {
|
||||||
|
constructor (nodeName = 'UNDEFINED') {
|
||||||
|
super()
|
||||||
|
this.nodeName = nodeName
|
||||||
|
/**
|
||||||
|
* @type {Map<string, any>|null}
|
||||||
|
*/
|
||||||
|
this._prelimAttrs = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrate this type into the Yjs instance.
|
||||||
|
*
|
||||||
|
* * Save this struct in the os
|
||||||
|
* * This type is sent to other client
|
||||||
|
* * Observer functions are fired
|
||||||
|
*
|
||||||
|
* @param {Doc} y The Yjs instance
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
_integrate (y, item) {
|
||||||
|
super._integrate(y, item)
|
||||||
|
;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
|
||||||
|
this.setAttribute(key, value)
|
||||||
|
})
|
||||||
|
this._prelimAttrs = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
|
*
|
||||||
|
* @return {YXmlElement}
|
||||||
|
*/
|
||||||
|
_copy () {
|
||||||
|
return new YXmlElement(this.nodeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the XML serialization of this YXmlElement.
|
||||||
|
* The attributes are ordered by attribute-name, so you can easily use this
|
||||||
|
* method to compare YXmlElements
|
||||||
|
*
|
||||||
|
* @return {string} The string representation of this type.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toString () {
|
||||||
|
const attrs = this.getAttributes()
|
||||||
|
const stringBuilder = []
|
||||||
|
const keys = []
|
||||||
|
for (const 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}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an attribute from this YXmlElement.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that is to be removed.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
removeAttribute (attributeName) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeMapDelete(transaction, this, attributeName)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or updates an attribute.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that is to be set.
|
||||||
|
* @param {String} attributeValue The attribute value that is to be set.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
setAttribute (attributeName, attributeValue) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeMapSet(transaction, this, attributeName, attributeValue)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an attribute value that belongs to the attribute name.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that identifies the
|
||||||
|
* queried value.
|
||||||
|
* @return {String} The queried attribute value.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getAttribute (attributeName) {
|
||||||
|
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
|
*
|
||||||
|
* @param {Snapshot} [snapshot]
|
||||||
|
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getAttributes (snapshot) {
|
||||||
|
return typeMapGetAll(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Dom Element that mirrors this YXmlElement.
|
||||||
|
*
|
||||||
|
* @param {Document} [_document=document] The document object (you must define
|
||||||
|
* this when calling this method in
|
||||||
|
* nodejs)
|
||||||
|
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
|
||||||
|
* are presented in the DOM
|
||||||
|
* @param {any} [binding] You should not set this property. This is
|
||||||
|
* used if DomBinding wants to create a
|
||||||
|
* association to the created DOM type.
|
||||||
|
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toDOM (_document = document, hooks = {}, binding) {
|
||||||
|
const dom = _document.createElement(this.nodeName)
|
||||||
|
const attrs = this.getAttributes()
|
||||||
|
for (const key in attrs) {
|
||||||
|
dom.setAttribute(key, attrs[key])
|
||||||
|
}
|
||||||
|
typeListForEach(this, yxml => {
|
||||||
|
dom.appendChild(yxml.toDOM(_document, hooks, binding))
|
||||||
|
})
|
||||||
|
if (binding !== undefined) {
|
||||||
|
binding._createAssociation(dom, this)
|
||||||
|
}
|
||||||
|
return dom
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the properties of this type to binary and write it to an
|
||||||
|
* BinaryEncoder.
|
||||||
|
*
|
||||||
|
* This is called when this Item is sent to a remote peer.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
encoding.writeVarUint(encoder, YXmlElementRefID)
|
||||||
|
encoding.writeVarString(encoder, this.nodeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {YXmlElement}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))
|
||||||
39
src/types/YXmlEvent.js
Normal file
39
src/types/YXmlEvent.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
YEvent,
|
||||||
|
YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Event that describes changes on a YXml Element or Yxml Fragment
|
||||||
|
*/
|
||||||
|
export class YXmlEvent extends YEvent {
|
||||||
|
/**
|
||||||
|
* @param {YXmlElement|YXmlFragment} target The target on which the event is created.
|
||||||
|
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
|
||||||
|
* child list changed.
|
||||||
|
* @param {Transaction} transaction The transaction instance with wich the
|
||||||
|
* change was created.
|
||||||
|
*/
|
||||||
|
constructor (target, subs, transaction) {
|
||||||
|
super(target, transaction)
|
||||||
|
/**
|
||||||
|
* Whether the children changed.
|
||||||
|
* @type {Boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.childListChanged = false
|
||||||
|
/**
|
||||||
|
* Set of all changed attributes.
|
||||||
|
* @type {Set<string|null>}
|
||||||
|
*/
|
||||||
|
this.attributesChanged = new Set()
|
||||||
|
subs.forEach((sub) => {
|
||||||
|
if (sub === null) {
|
||||||
|
this.childListChanged = true
|
||||||
|
} else {
|
||||||
|
this.attributesChanged.add(sub)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
342
src/types/YXmlFragment.js
Normal file
342
src/types/YXmlFragment.js
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* @module YXml
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
YXmlEvent,
|
||||||
|
YXmlElement,
|
||||||
|
AbstractType,
|
||||||
|
typeListMap,
|
||||||
|
typeListForEach,
|
||||||
|
typeListInsertGenerics,
|
||||||
|
typeListDelete,
|
||||||
|
typeListToArray,
|
||||||
|
YXmlFragmentRefID,
|
||||||
|
callTypeObservers,
|
||||||
|
transact,
|
||||||
|
Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the elements to which a set of CSS queries apply.
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* query = '.classSelector'
|
||||||
|
* query = 'nodeSelector'
|
||||||
|
* query = '#idSelector'
|
||||||
|
*
|
||||||
|
* @typedef {string} CSS_Selector
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dom filter function.
|
||||||
|
*
|
||||||
|
* @callback domFilter
|
||||||
|
* @param {string} nodeName The nodeName of the element
|
||||||
|
* @param {Map} attributes The map of attributes.
|
||||||
|
* @return {boolean} Whether to include the Dom node in the YXmlElement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
|
||||||
|
* position within them.
|
||||||
|
*
|
||||||
|
* Can be created with {@link YXmlFragment#createTreeWalker}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @implements {IterableIterator}
|
||||||
|
*/
|
||||||
|
export class YXmlTreeWalker {
|
||||||
|
/**
|
||||||
|
* @param {YXmlFragment | YXmlElement} root
|
||||||
|
* @param {function(AbstractType<any>):boolean} [f]
|
||||||
|
*/
|
||||||
|
constructor (root, f = () => true) {
|
||||||
|
this._filter = f
|
||||||
|
this._root = root
|
||||||
|
/**
|
||||||
|
* @type {Item}
|
||||||
|
*/
|
||||||
|
this._currentNode = /** @type {Item} */ (root._start)
|
||||||
|
this._firstCall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator] () {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next node.
|
||||||
|
*
|
||||||
|
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
next () {
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let n = this._currentNode
|
||||||
|
let type = /** @type {ContentType} */ (n.content).type
|
||||||
|
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
|
||||||
|
do {
|
||||||
|
type = /** @type {ContentType} */ (n.content).type
|
||||||
|
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
|
||||||
|
// walk down in the tree
|
||||||
|
n = type._start
|
||||||
|
} else {
|
||||||
|
// walk right or up in the tree
|
||||||
|
while (n !== null) {
|
||||||
|
if (n.right !== null) {
|
||||||
|
n = n.right
|
||||||
|
break
|
||||||
|
} else if (n.parent === this._root) {
|
||||||
|
n = null
|
||||||
|
} else {
|
||||||
|
n = n.parent._item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type)))
|
||||||
|
}
|
||||||
|
this._firstCall = false
|
||||||
|
if (n === null) {
|
||||||
|
// @ts-ignore
|
||||||
|
return { value: undefined, done: true }
|
||||||
|
}
|
||||||
|
this._currentNode = n
|
||||||
|
return { value: /** @type {any} */ (n.content).type, done: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
|
||||||
|
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
|
||||||
|
* nodeName and it does not have attributes. Though it can be bound to a DOM
|
||||||
|
* element - in this case the attributes and the nodeName are not shared.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @extends AbstractType<YXmlEvent>
|
||||||
|
*/
|
||||||
|
export class YXmlFragment extends AbstractType {
|
||||||
|
constructor () {
|
||||||
|
super()
|
||||||
|
/**
|
||||||
|
* @type {Array<any>|null}
|
||||||
|
*/
|
||||||
|
this._prelimContent = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrate this type into the Yjs instance.
|
||||||
|
*
|
||||||
|
* * Save this struct in the os
|
||||||
|
* * This type is sent to other client
|
||||||
|
* * Observer functions are fired
|
||||||
|
*
|
||||||
|
* @param {Doc} y The Yjs instance
|
||||||
|
* @param {Item} item
|
||||||
|
*/
|
||||||
|
_integrate (y, item) {
|
||||||
|
super._integrate(y, item)
|
||||||
|
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||||
|
this._prelimContent = null
|
||||||
|
}
|
||||||
|
|
||||||
|
_copy () {
|
||||||
|
return new YXmlFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
get length () {
|
||||||
|
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subtree of childNodes.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
|
||||||
|
* for (let node in walker) {
|
||||||
|
* // `node` is a div node
|
||||||
|
* nop(node)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
|
||||||
|
* returns a Boolean indicating whether the child
|
||||||
|
* is to be included in the subtree.
|
||||||
|
* @return {YXmlTreeWalker} A subtree and a position within it.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
createTreeWalker (filter) {
|
||||||
|
return new YXmlTreeWalker(this, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first YXmlElement that matches the query.
|
||||||
|
* Similar to DOM's {@link querySelector}.
|
||||||
|
*
|
||||||
|
* Query support:
|
||||||
|
* - tagname
|
||||||
|
* TODO:
|
||||||
|
* - id
|
||||||
|
* - attribute
|
||||||
|
*
|
||||||
|
* @param {CSS_Selector} query The query on the children.
|
||||||
|
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
querySelector (query) {
|
||||||
|
query = query.toUpperCase()
|
||||||
|
// @ts-ignore
|
||||||
|
const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query)
|
||||||
|
const next = iterator.next()
|
||||||
|
if (next.done) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return next.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all YXmlElements that match the query.
|
||||||
|
* Similar to Dom's {@link querySelectorAll}.
|
||||||
|
*
|
||||||
|
* @todo Does not yet support all queries. Currently only query by tagName.
|
||||||
|
*
|
||||||
|
* @param {CSS_Selector} query The query on the children
|
||||||
|
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
querySelectorAll (query) {
|
||||||
|
query = query.toUpperCase()
|
||||||
|
// @ts-ignore
|
||||||
|
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates YXmlEvent and calls observers.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||||
|
*/
|
||||||
|
_callObserver (transaction, parentSubs) {
|
||||||
|
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the string representation of all the children of this YXmlFragment.
|
||||||
|
*
|
||||||
|
* @return {string} The string representation of all children.
|
||||||
|
*/
|
||||||
|
toString () {
|
||||||
|
return typeListMap(this, xml => xml.toString()).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
toJSON () {
|
||||||
|
return this.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Dom Element that mirrors this YXmlElement.
|
||||||
|
*
|
||||||
|
* @param {Document} [_document=document] The document object (you must define
|
||||||
|
* this when calling this method in
|
||||||
|
* nodejs)
|
||||||
|
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
|
||||||
|
* are presented in the DOM
|
||||||
|
* @param {any} [binding] You should not set this property. This is
|
||||||
|
* used if DomBinding wants to create a
|
||||||
|
* association to the created DOM type.
|
||||||
|
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toDOM (_document = document, hooks = {}, binding) {
|
||||||
|
const fragment = _document.createDocumentFragment()
|
||||||
|
if (binding !== undefined) {
|
||||||
|
binding._createAssociation(fragment, this)
|
||||||
|
}
|
||||||
|
typeListForEach(this, xmlType => {
|
||||||
|
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
|
||||||
|
})
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts new content at an index.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Insert character 'a' at position 0
|
||||||
|
* xml.insert(0, [new Y.XmlText('text')])
|
||||||
|
*
|
||||||
|
* @param {number} index The index to insert content at
|
||||||
|
* @param {Array<YXmlElement|YXmlText>} content The array of content
|
||||||
|
*/
|
||||||
|
insert (index, content) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeListInsertGenerics(transaction, this, index, content)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||||
|
this._prelimContent.splice(index, 0, ...content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes elements starting from an index.
|
||||||
|
*
|
||||||
|
* @param {number} index Index at which to start deleting elements
|
||||||
|
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
|
||||||
|
*/
|
||||||
|
delete (index, length = 1) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeListDelete(transaction, this, index, length)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||||
|
this._prelimContent.splice(index, length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this YArray to a JavaScript Array.
|
||||||
|
*
|
||||||
|
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
|
||||||
|
*/
|
||||||
|
toArray () {
|
||||||
|
return typeListToArray(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the properties of this type to binary and write it to an
|
||||||
|
* BinaryEncoder.
|
||||||
|
*
|
||||||
|
* This is called when this Item is sent to a remote peer.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
encoding.writeVarUint(encoder, YXmlFragmentRefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {YXmlFragment}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYXmlFragment = decoder => new YXmlFragment()
|
||||||
86
src/types/YXmlHook.js
Normal file
86
src/types/YXmlHook.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
YMap,
|
||||||
|
YXmlHookRefID
|
||||||
|
} from '../internals.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can manage binding to a custom type with YXmlHook.
|
||||||
|
*
|
||||||
|
* @extends {YMap<any>}
|
||||||
|
*/
|
||||||
|
export class YXmlHook extends YMap {
|
||||||
|
/**
|
||||||
|
* @param {string} hookName nodeName of the Dom Node.
|
||||||
|
*/
|
||||||
|
constructor (hookName) {
|
||||||
|
super()
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
this.hookName = hookName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
|
*/
|
||||||
|
_copy () {
|
||||||
|
return new YXmlHook(this.hookName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Dom Element that mirrors this YXmlElement.
|
||||||
|
*
|
||||||
|
* @param {Document} [_document=document] The document object (you must define
|
||||||
|
* this when calling this method in
|
||||||
|
* nodejs)
|
||||||
|
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
|
||||||
|
* are presented in the DOM
|
||||||
|
* @param {any} [binding] You should not set this property. This is
|
||||||
|
* used if DomBinding wants to create a
|
||||||
|
* association to the created DOM type
|
||||||
|
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toDOM (_document = document, hooks = {}, binding) {
|
||||||
|
const hook = hooks[this.hookName]
|
||||||
|
let dom
|
||||||
|
if (hook !== undefined) {
|
||||||
|
dom = hook.createDom(this)
|
||||||
|
} else {
|
||||||
|
dom = document.createElement(this.hookName)
|
||||||
|
}
|
||||||
|
dom.setAttribute('data-yjs-hook', this.hookName)
|
||||||
|
if (binding !== undefined) {
|
||||||
|
binding._createAssociation(dom, this)
|
||||||
|
}
|
||||||
|
return dom
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the properties of this type to binary and write it to an
|
||||||
|
* BinaryEncoder.
|
||||||
|
*
|
||||||
|
* This is called when this Item is sent to a remote peer.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
super._write(encoder)
|
||||||
|
encoding.writeVarUint(encoder, YXmlHookRefID)
|
||||||
|
encoding.writeVarString(encoder, this.hookName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {YXmlHook}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYXmlHook = decoder =>
|
||||||
|
new YXmlHook(decoding.readVarString(decoder))
|
||||||
95
src/types/YXmlText.js
Normal file
95
src/types/YXmlText.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
import { YText, YXmlTextRefID } from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents text in a Dom Element. In the future this type will also handle
|
||||||
|
* simple formatting information like bold and italic.
|
||||||
|
*/
|
||||||
|
export class YXmlText extends YText {
|
||||||
|
_copy () {
|
||||||
|
return new YXmlText()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Dom Element that mirrors this YXmlText.
|
||||||
|
*
|
||||||
|
* @param {Document} [_document=document] The document object (you must define
|
||||||
|
* this when calling this method in
|
||||||
|
* nodejs)
|
||||||
|
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
|
||||||
|
* are presented in the DOM
|
||||||
|
* @param {any} [binding] You should not set this property. This is
|
||||||
|
* used if DomBinding wants to create a
|
||||||
|
* association to the created DOM type.
|
||||||
|
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toDOM (_document = document, hooks, binding) {
|
||||||
|
const dom = _document.createTextNode(this.toString())
|
||||||
|
if (binding !== undefined) {
|
||||||
|
binding._createAssociation(dom, this)
|
||||||
|
}
|
||||||
|
return dom
|
||||||
|
}
|
||||||
|
|
||||||
|
toString () {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.toDelta().map(delta => {
|
||||||
|
const nestedNodes = []
|
||||||
|
for (const nodeName in delta.attributes) {
|
||||||
|
const attrs = []
|
||||||
|
for (const key in delta.attributes[nodeName]) {
|
||||||
|
attrs.push({ key, value: delta.attributes[nodeName][key] })
|
||||||
|
}
|
||||||
|
// sort attributes to get a unique order
|
||||||
|
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
|
||||||
|
nestedNodes.push({ nodeName, attrs })
|
||||||
|
}
|
||||||
|
// sort node order to get a unique order
|
||||||
|
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
|
||||||
|
// now convert to dom string
|
||||||
|
let str = ''
|
||||||
|
for (let i = 0; i < nestedNodes.length; i++) {
|
||||||
|
const node = nestedNodes[i]
|
||||||
|
str += `<${node.nodeName}`
|
||||||
|
for (let j = 0; j < node.attrs.length; j++) {
|
||||||
|
const attr = node.attrs[j]
|
||||||
|
str += ` ${attr.key}="${attr.value}"`
|
||||||
|
}
|
||||||
|
str += '>'
|
||||||
|
}
|
||||||
|
str += delta.insert
|
||||||
|
for (let i = nestedNodes.length - 1; i >= 0; i--) {
|
||||||
|
str += `</${nestedNodes[i].nodeName}>`
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
toJSON () {
|
||||||
|
return this.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
*/
|
||||||
|
_write (encoder) {
|
||||||
|
encoding.writeVarUint(encoder, YXmlTextRefID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {YXmlText}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readYXmlText = decoder => new YXmlText()
|
||||||
313
src/utils/DeleteSet.js
Normal file
313
src/utils/DeleteSet.js
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
findIndexSS,
|
||||||
|
createID,
|
||||||
|
getState,
|
||||||
|
splitItem,
|
||||||
|
iterateStructs,
|
||||||
|
Item, GC, StructStore, Transaction, ID // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as array from 'lib0/array.js'
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
|
||||||
|
export class DeleteItem {
|
||||||
|
/**
|
||||||
|
* @param {number} clock
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
constructor (clock, len) {
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.clock = clock
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.len = len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed.
|
||||||
|
* - When created in a transaction, it must only be accessed after sorting, and merging
|
||||||
|
* - This DeleteSet is send to other clients
|
||||||
|
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
|
||||||
|
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
|
||||||
|
*/
|
||||||
|
export class DeleteSet {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {Map<number,Array<DeleteItem>>}
|
||||||
|
*/
|
||||||
|
this.clients = new Map()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over all structs that the DeleteSet gc's.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {function(GC|Item):void} f
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const iterateDeletedStructs = (transaction, ds, f) =>
|
||||||
|
ds.clients.forEach((deletes, clientid) => {
|
||||||
|
const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid))
|
||||||
|
for (let i = 0; i < deletes.length; i++) {
|
||||||
|
const del = deletes[i]
|
||||||
|
iterateStructs(transaction, structs, del.clock, del.len, f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<DeleteItem>} dis
|
||||||
|
* @param {number} clock
|
||||||
|
* @return {number|null}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const findIndexDS = (dis, clock) => {
|
||||||
|
let left = 0
|
||||||
|
let right = dis.length - 1
|
||||||
|
while (left <= right) {
|
||||||
|
const midindex = math.floor((left + right) / 2)
|
||||||
|
const mid = dis[midindex]
|
||||||
|
const midclock = mid.clock
|
||||||
|
if (midclock <= clock) {
|
||||||
|
if (clock < midclock + mid.len) {
|
||||||
|
return midindex
|
||||||
|
}
|
||||||
|
left = midindex + 1
|
||||||
|
} else {
|
||||||
|
right = midindex - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {boolean}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const isDeleted = (ds, id) => {
|
||||||
|
const dis = ds.clients.get(id.client)
|
||||||
|
return dis !== undefined && findIndexDS(dis, id.clock) !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const sortAndMergeDeleteSet = ds => {
|
||||||
|
ds.clients.forEach(dels => {
|
||||||
|
dels.sort((a, b) => a.clock - b.clock)
|
||||||
|
// merge items without filtering or splicing the array
|
||||||
|
// i is the current pointer
|
||||||
|
// j refers to the current insert position for the pointed item
|
||||||
|
// try to merge dels[i] into dels[j-1] or set dels[j]=dels[i]
|
||||||
|
let i, j
|
||||||
|
for (i = 1, j = 1; i < dels.length; i++) {
|
||||||
|
const left = dels[j - 1]
|
||||||
|
const right = dels[i]
|
||||||
|
if (left.clock + left.len === right.clock) {
|
||||||
|
left.len += right.len
|
||||||
|
} else {
|
||||||
|
if (j < i) {
|
||||||
|
dels[j] = right
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dels.length = j
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<DeleteSet>} dss
|
||||||
|
* @return {DeleteSet} A fresh DeleteSet
|
||||||
|
*/
|
||||||
|
export const mergeDeleteSets = dss => {
|
||||||
|
const merged = new DeleteSet()
|
||||||
|
for (let dssI = 0; dssI < dss.length; dssI++) {
|
||||||
|
dss[dssI].clients.forEach((delsLeft, client) => {
|
||||||
|
if (!merged.clients.has(client)) {
|
||||||
|
// Write all missing keys from current ds and all following.
|
||||||
|
// If merged already contains `client` current ds has already been added.
|
||||||
|
/**
|
||||||
|
* @type {Array<DeleteItem>}
|
||||||
|
*/
|
||||||
|
const dels = delsLeft.slice()
|
||||||
|
for (let i = dssI + 1; i < dss.length; i++) {
|
||||||
|
array.appendTo(dels, dss[i].clients.get(client) || [])
|
||||||
|
}
|
||||||
|
merged.clients.set(client, dels)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sortAndMergeDeleteSet(merged)
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {ID} id
|
||||||
|
* @param {number} length
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const addToDeleteSet = (ds, id, length) => {
|
||||||
|
map.setIfUndefined(ds.clients, id.client, () => []).push(new DeleteItem(id.clock, length))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDeleteSet = () => new DeleteSet()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} ss
|
||||||
|
* @return {DeleteSet} Merged and sorted DeleteSet
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createDeleteSetFromStructStore = ss => {
|
||||||
|
const ds = createDeleteSet()
|
||||||
|
ss.clients.forEach((structs, client) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<DeleteItem>}
|
||||||
|
*/
|
||||||
|
const dsitems = []
|
||||||
|
for (let i = 0; i < structs.length; i++) {
|
||||||
|
const struct = structs[i]
|
||||||
|
if (struct.deleted) {
|
||||||
|
const clock = struct.id.clock
|
||||||
|
let len = struct.length
|
||||||
|
if (i + 1 < structs.length) {
|
||||||
|
for (let next = structs[i + 1]; i + 1 < structs.length && next.id.clock === clock + len && next.deleted; next = structs[++i + 1]) {
|
||||||
|
len += next.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dsitems.push(new DeleteItem(clock, len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dsitems.length > 0) {
|
||||||
|
ds.clients.set(client, dsitems)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeDeleteSet = (encoder, ds) => {
|
||||||
|
encoding.writeVarUint(encoder, ds.clients.size)
|
||||||
|
ds.clients.forEach((dsitems, client) => {
|
||||||
|
encoding.writeVarUint(encoder, client)
|
||||||
|
const len = dsitems.length
|
||||||
|
encoding.writeVarUint(encoder, len)
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const item = dsitems[i]
|
||||||
|
encoding.writeVarUint(encoder, item.clock)
|
||||||
|
encoding.writeVarUint(encoder, item.len)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {DeleteSet}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readDeleteSet = decoder => {
|
||||||
|
const ds = new DeleteSet()
|
||||||
|
const numClients = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < numClients; i++) {
|
||||||
|
const client = decoding.readVarUint(decoder)
|
||||||
|
const numberOfDeletes = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < numberOfDeletes; i++) {
|
||||||
|
addToDeleteSet(ds, createID(client, decoding.readVarUint(decoder)), decoding.readVarUint(decoder))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readAndApplyDeleteSet = (decoder, transaction, store) => {
|
||||||
|
const unappliedDS = new DeleteSet()
|
||||||
|
const numClients = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < numClients; i++) {
|
||||||
|
const client = decoding.readVarUint(decoder)
|
||||||
|
const numberOfDeletes = decoding.readVarUint(decoder)
|
||||||
|
const structs = store.clients.get(client) || []
|
||||||
|
const state = getState(store, client)
|
||||||
|
for (let i = 0; i < numberOfDeletes; i++) {
|
||||||
|
const clock = decoding.readVarUint(decoder)
|
||||||
|
const len = decoding.readVarUint(decoder)
|
||||||
|
if (clock < state) {
|
||||||
|
if (state < clock + len) {
|
||||||
|
addToDeleteSet(unappliedDS, createID(client, state), clock + len - state)
|
||||||
|
}
|
||||||
|
let index = findIndexSS(structs, clock)
|
||||||
|
/**
|
||||||
|
* We can ignore the case of GC and Delete structs, because we are going to skip them
|
||||||
|
* @type {Item}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
let struct = structs[index]
|
||||||
|
// split the first item if necessary
|
||||||
|
if (!struct.deleted && struct.id.clock < clock) {
|
||||||
|
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
|
||||||
|
index++ // increase we now want to use the next struct
|
||||||
|
}
|
||||||
|
while (index < structs.length) {
|
||||||
|
// @ts-ignore
|
||||||
|
struct = structs[index++]
|
||||||
|
if (struct.id.clock < clock + len) {
|
||||||
|
if (!struct.deleted) {
|
||||||
|
if (clock + len < struct.id.clock + struct.length) {
|
||||||
|
structs.splice(index, 0, splitItem(transaction, struct, clock + len - struct.id.clock))
|
||||||
|
}
|
||||||
|
struct.delete(transaction)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addToDeleteSet(unappliedDS, createID(client, clock), len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unappliedDS.clients.size > 0) {
|
||||||
|
// TODO: no need for encoding+decoding ds anymore
|
||||||
|
const unappliedDSEncoder = encoding.createEncoder()
|
||||||
|
writeDeleteSet(unappliedDSEncoder, unappliedDS)
|
||||||
|
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/utils/Doc.js
Normal file
195
src/utils/Doc.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* @module Y
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
StructStore,
|
||||||
|
AbstractType,
|
||||||
|
YArray,
|
||||||
|
YText,
|
||||||
|
YMap,
|
||||||
|
YXmlFragment,
|
||||||
|
transact,
|
||||||
|
Item, Transaction, YEvent // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import { Observable } from 'lib0/observable.js'
|
||||||
|
import * as random from 'lib0/random.js'
|
||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
|
||||||
|
export const generateNewClientId = random.uint32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Yjs instance handles the state of shared data.
|
||||||
|
* @extends Observable<string>
|
||||||
|
*/
|
||||||
|
export class Doc extends Observable {
|
||||||
|
/**
|
||||||
|
* @param {Object} conf configuration
|
||||||
|
* @param {boolean} [conf.gc] Disable garbage collection (default: gc=true)
|
||||||
|
* @param {function(Item):boolean} [conf.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
|
||||||
|
*/
|
||||||
|
constructor ({ gc = true, gcFilter = () => true } = {}) {
|
||||||
|
super()
|
||||||
|
this.gc = gc
|
||||||
|
this.gcFilter = gcFilter
|
||||||
|
this.clientID = generateNewClientId()
|
||||||
|
/**
|
||||||
|
* @type {Map<string, AbstractType<YEvent>>}
|
||||||
|
*/
|
||||||
|
this.share = new Map()
|
||||||
|
this.store = new StructStore()
|
||||||
|
/**
|
||||||
|
* @type {Transaction | null}
|
||||||
|
*/
|
||||||
|
this._transaction = null
|
||||||
|
/**
|
||||||
|
* @type {Array<Transaction>}
|
||||||
|
*/
|
||||||
|
this._transactionCleanups = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes that happen inside of a transaction are bundled. This means that
|
||||||
|
* the observer fires _after_ the transaction is finished and that all changes
|
||||||
|
* that happened inside of the transaction are sent as one message to the
|
||||||
|
* other peers.
|
||||||
|
*
|
||||||
|
* @param {function(Transaction):void} f The function that should be executed as a transaction
|
||||||
|
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
transact (f, origin = null) {
|
||||||
|
transact(this, f, origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a shared data type.
|
||||||
|
*
|
||||||
|
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
|
||||||
|
* and do not overwrite each other. I.e.
|
||||||
|
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
|
||||||
|
*
|
||||||
|
* After this method is called, the type is also available on `y.share.get(name)`.
|
||||||
|
*
|
||||||
|
* *Best Practices:*
|
||||||
|
* Define all types right after the Yjs instance is created and store them in a separate object.
|
||||||
|
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const y = new Y(..)
|
||||||
|
* const appState = {
|
||||||
|
* document: y.getText('document')
|
||||||
|
* comments: y.getArray('comments')
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||||
|
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
get (name, TypeConstructor = AbstractType) {
|
||||||
|
const type = map.setIfUndefined(this.share, name, () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const t = new TypeConstructor()
|
||||||
|
t._integrate(this, null)
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
const Constr = type.constructor
|
||||||
|
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
|
||||||
|
if (Constr === AbstractType) {
|
||||||
|
// @ts-ignore
|
||||||
|
const t = new TypeConstructor()
|
||||||
|
t._map = type._map
|
||||||
|
type._map.forEach(/** @param {Item?} n */ n => {
|
||||||
|
for (; n !== null; n = n.left) {
|
||||||
|
// @ts-ignore
|
||||||
|
n.parent = t
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t._start = type._start
|
||||||
|
for (let n = t._start; n !== null; n = n.right) {
|
||||||
|
n.parent = t
|
||||||
|
}
|
||||||
|
t._length = type._length
|
||||||
|
this.share.set(name, t)
|
||||||
|
t._integrate(this, null)
|
||||||
|
return t
|
||||||
|
} else {
|
||||||
|
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {string} name
|
||||||
|
* @return {YArray<T>}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getArray (name) {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.get(name, YArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @return {YText}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getText (name) {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.get(name, YText)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @return {YMap<any>}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getMap (name) {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.get(name, YMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @return {YXmlFragment}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getXmlFragment (name) {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.get(name, YXmlFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit `destroy` event and unregister all event handlers.
|
||||||
|
*/
|
||||||
|
destroy () {
|
||||||
|
this.emit('destroyed', [true])
|
||||||
|
super.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {function} f
|
||||||
|
*/
|
||||||
|
on (eventName, f) {
|
||||||
|
super.on(eventName, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} eventName
|
||||||
|
* @param {function} f
|
||||||
|
*/
|
||||||
|
off (eventName, f) {
|
||||||
|
super.off(eventName, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/utils/EventHandler.js
Normal file
82
src/utils/EventHandler.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import * as f from 'lib0/function.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General event handler implementation.
|
||||||
|
*
|
||||||
|
* @template ARG0, ARG1
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export class EventHandler {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {Array<function(ARG0, ARG1):void>}
|
||||||
|
*/
|
||||||
|
this.l = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template ARG0,ARG1
|
||||||
|
* @returns {EventHandler<ARG0,ARG1>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createEventHandler = () => new EventHandler()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event listener that is called when
|
||||||
|
* {@link EventHandler#callEventListeners} is called.
|
||||||
|
*
|
||||||
|
* @template ARG0,ARG1
|
||||||
|
* @param {EventHandler<ARG0,ARG1>} eventHandler
|
||||||
|
* @param {function(ARG0,ARG1):void} f The event handler.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const addEventHandlerListener = (eventHandler, f) =>
|
||||||
|
eventHandler.l.push(f)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an event listener.
|
||||||
|
*
|
||||||
|
* @template ARG0,ARG1
|
||||||
|
* @param {EventHandler<ARG0,ARG1>} eventHandler
|
||||||
|
* @param {function(ARG0,ARG1):void} f The event handler that was added with
|
||||||
|
* {@link EventHandler#addEventListener}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const removeEventHandlerListener = (eventHandler, f) => {
|
||||||
|
eventHandler.l = eventHandler.l.filter(g => f !== g)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all event listeners.
|
||||||
|
* @template ARG0,ARG1
|
||||||
|
* @param {EventHandler<ARG0,ARG1>} eventHandler
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const removeAllEventHandlerListeners = eventHandler => {
|
||||||
|
eventHandler.l.length = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call all event listeners that were added via
|
||||||
|
* {@link EventHandler#addEventListener}.
|
||||||
|
*
|
||||||
|
* @template ARG0,ARG1
|
||||||
|
* @param {EventHandler<ARG0,ARG1>} eventHandler
|
||||||
|
* @param {ARG0} arg0
|
||||||
|
* @param {ARG1} arg1
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const callEventHandlerListeners = (eventHandler, arg0, arg1) =>
|
||||||
|
f.callAll(eventHandler.l, [arg0, arg1])
|
||||||
90
src/utils/ID.js
Normal file
90
src/utils/ID.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
import { AbstractType } from '../internals.js' // eslint-disable-line
|
||||||
|
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
export class ID {
|
||||||
|
/**
|
||||||
|
* @param {number} client client id
|
||||||
|
* @param {number} clock unique per client id, continuous number
|
||||||
|
*/
|
||||||
|
constructor (client, clock) {
|
||||||
|
/**
|
||||||
|
* Client id
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.client = client
|
||||||
|
/**
|
||||||
|
* unique per client id, continuous number
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.clock = clock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ID | null} a
|
||||||
|
* @param {ID | null} b
|
||||||
|
* @return {boolean}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} client
|
||||||
|
* @param {number} clock
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createID = (client, clock) => new ID(client, clock)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {ID} id
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeID = (encoder, id) => {
|
||||||
|
encoding.writeVarUint(encoder, id.client)
|
||||||
|
encoding.writeVarUint(encoder, id.clock)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read ID.
|
||||||
|
* * If first varUint read is 0xFFFFFF a RootID is returned.
|
||||||
|
* * Otherwise an ID is returned
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {ID}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readID = decoder =>
|
||||||
|
createID(decoding.readVarUint(decoder), decoding.readVarUint(decoder))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top types are mapped from y.share.get(keyname) => type.
|
||||||
|
* `type` does not store any information about the `keyname`.
|
||||||
|
* This function finds the correct `keyname` for `type` and throws otherwise.
|
||||||
|
*
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @return {string}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const findRootTypeKey = type => {
|
||||||
|
// @ts-ignore _y must be defined, otherwise unexpected case
|
||||||
|
for (const [key, value] of type.doc.share) {
|
||||||
|
if (value === type) {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
142
src/utils/PermanentUserData.js
Normal file
142
src/utils/PermanentUserData.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
YArray,
|
||||||
|
YMap,
|
||||||
|
readDeleteSet,
|
||||||
|
writeDeleteSet,
|
||||||
|
createDeleteSet,
|
||||||
|
ID, DeleteSet, YArrayEvent, Transaction, Doc // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import { mergeDeleteSets, isDeleted } from './DeleteSet.js'
|
||||||
|
|
||||||
|
export class PermanentUserData {
|
||||||
|
/**
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @param {YMap<any>} [storeType]
|
||||||
|
*/
|
||||||
|
constructor (doc, storeType = doc.getMap('users')) {
|
||||||
|
/**
|
||||||
|
* @type {Map<string,DeleteSet>}
|
||||||
|
*/
|
||||||
|
const dss = new Map()
|
||||||
|
this.yusers = storeType
|
||||||
|
this.doc = doc
|
||||||
|
/**
|
||||||
|
* Maps from clientid to userDescription
|
||||||
|
*
|
||||||
|
* @type {Map<number,string>}
|
||||||
|
*/
|
||||||
|
this.clients = new Map()
|
||||||
|
this.dss = dss
|
||||||
|
/**
|
||||||
|
* @param {YMap<any>} user
|
||||||
|
* @param {string} userDescription
|
||||||
|
*/
|
||||||
|
const initUser = (user, userDescription) => {
|
||||||
|
/**
|
||||||
|
* @type {YArray<Uint8Array>}
|
||||||
|
*/
|
||||||
|
const ds = user.get('ds')
|
||||||
|
const ids = user.get('ids')
|
||||||
|
const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription)
|
||||||
|
ds.observe(/** @param {YArrayEvent<any>} event */ event => {
|
||||||
|
event.changes.added.forEach(item => {
|
||||||
|
item.content.getContent().forEach(encodedDs => {
|
||||||
|
if (encodedDs instanceof Uint8Array) {
|
||||||
|
this.dss.set(userDescription, mergeDeleteSets([this.dss.get(userDescription) || createDeleteSet(), readDeleteSet(decoding.createDecoder(encodedDs))]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(decoding.createDecoder(encodedDs)))))
|
||||||
|
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
|
||||||
|
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
|
||||||
|
)
|
||||||
|
ids.forEach(addClientId)
|
||||||
|
}
|
||||||
|
// observe users
|
||||||
|
storeType.observe(event => {
|
||||||
|
event.keysChanged.forEach(userDescription =>
|
||||||
|
initUser(storeType.get(userDescription), userDescription)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
// add intial data
|
||||||
|
storeType.forEach(initUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @param {number} clientid
|
||||||
|
* @param {string} userDescription
|
||||||
|
* @param {Object} [conf]
|
||||||
|
* @param {function(Transaction, DeleteSet):boolean} [conf.filter]
|
||||||
|
*/
|
||||||
|
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
|
||||||
|
const users = this.yusers
|
||||||
|
let user = users.get(userDescription)
|
||||||
|
if (!user) {
|
||||||
|
user = new YMap()
|
||||||
|
user.set('ids', new YArray())
|
||||||
|
user.set('ds', new YArray())
|
||||||
|
users.set(userDescription, user)
|
||||||
|
}
|
||||||
|
user.get('ids').push([clientid])
|
||||||
|
users.observe(event => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const userOverwrite = users.get(userDescription)
|
||||||
|
if (userOverwrite !== user) {
|
||||||
|
// user was overwritten, port all data over to the next user object
|
||||||
|
// @todo Experiment with Y.Sets here
|
||||||
|
user = userOverwrite
|
||||||
|
// @todo iterate over old type
|
||||||
|
this.clients.forEach((_userDescription, clientid) => {
|
||||||
|
if (userDescription === _userDescription) {
|
||||||
|
user.get('ids').push([clientid])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
const ds = this.dss.get(userDescription)
|
||||||
|
if (ds) {
|
||||||
|
writeDeleteSet(encoder, ds)
|
||||||
|
user.get('ds').push([encoding.toUint8Array(encoder)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const yds = user.get('ds')
|
||||||
|
const ds = transaction.deleteSet
|
||||||
|
if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
writeDeleteSet(encoder, ds)
|
||||||
|
yds.push([encoding.toUint8Array(encoder)])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} clientid
|
||||||
|
* @return {any}
|
||||||
|
*/
|
||||||
|
getUserByClientId (clientid) {
|
||||||
|
return this.clients.get(clientid) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {string | null}
|
||||||
|
*/
|
||||||
|
getUserByDeletedId (id) {
|
||||||
|
for (const [userDescription, ds] of this.dss) {
|
||||||
|
if (isDeleted(ds, id)) {
|
||||||
|
return userDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
272
src/utils/RelativePosition.js
Normal file
272
src/utils/RelativePosition.js
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
createID,
|
||||||
|
writeID,
|
||||||
|
readID,
|
||||||
|
compareIDs,
|
||||||
|
getState,
|
||||||
|
findRootTypeKey,
|
||||||
|
Item,
|
||||||
|
ContentType,
|
||||||
|
followRedone,
|
||||||
|
ID, Doc, AbstractType // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A relative position is based on the Yjs model and is not affected by document changes.
|
||||||
|
* E.g. If you place a relative position before a certain character, it will always point to this character.
|
||||||
|
* If you place a relative position at the end of a type, it will always point to the end of the type.
|
||||||
|
*
|
||||||
|
* A numeric position is often unsuited for user selections, because it does not change when content is inserted
|
||||||
|
* before or after.
|
||||||
|
*
|
||||||
|
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
|
||||||
|
*
|
||||||
|
* One of the properties must be defined.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Current cursor position is at position 10
|
||||||
|
* const relativePosition = createRelativePositionFromIndex(yText, 10)
|
||||||
|
* // modify yText
|
||||||
|
* yText.insert(0, 'abc')
|
||||||
|
* yText.delete(3, 10)
|
||||||
|
* // Compute the cursor position
|
||||||
|
* const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
|
||||||
|
* absolutePosition.type === yText // => true
|
||||||
|
* console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class RelativePosition {
|
||||||
|
/**
|
||||||
|
* @param {ID|null} type
|
||||||
|
* @param {string|null} tname
|
||||||
|
* @param {ID|null} item
|
||||||
|
*/
|
||||||
|
constructor (type, tname, item) {
|
||||||
|
/**
|
||||||
|
* @type {ID|null}
|
||||||
|
*/
|
||||||
|
this.type = type
|
||||||
|
/**
|
||||||
|
* @type {string|null}
|
||||||
|
*/
|
||||||
|
this.tname = tname
|
||||||
|
/**
|
||||||
|
* @type {ID | null}
|
||||||
|
*/
|
||||||
|
this.item = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} json
|
||||||
|
* @return {RelativePosition}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
|
||||||
|
|
||||||
|
export class AbsolutePosition {
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
constructor (type, index) {
|
||||||
|
/**
|
||||||
|
* @type {AbstractType<any>}
|
||||||
|
*/
|
||||||
|
this.type = type
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.index = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {number} index
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {ID|null} item
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createRelativePosition = (type, item) => {
|
||||||
|
let typeid = null
|
||||||
|
let tname = null
|
||||||
|
if (type._item === null) {
|
||||||
|
tname = findRootTypeKey(type)
|
||||||
|
} else {
|
||||||
|
typeid = type._item.id
|
||||||
|
}
|
||||||
|
return new RelativePosition(typeid, tname, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a relativePosition based on a absolute position.
|
||||||
|
*
|
||||||
|
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
|
||||||
|
* @param {number} index The absolute position.
|
||||||
|
* @return {RelativePosition}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createRelativePositionFromTypeIndex = (type, index) => {
|
||||||
|
let t = type._start
|
||||||
|
while (t !== null) {
|
||||||
|
if (!t.deleted && t.countable) {
|
||||||
|
if (t.length > index) {
|
||||||
|
// case 1: found position somewhere in the linked list
|
||||||
|
return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
|
||||||
|
}
|
||||||
|
index -= t.length
|
||||||
|
}
|
||||||
|
t = t.right
|
||||||
|
}
|
||||||
|
return createRelativePosition(type, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {RelativePosition} rpos
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeRelativePosition = (encoder, rpos) => {
|
||||||
|
const { type, tname, item } = rpos
|
||||||
|
if (item !== null) {
|
||||||
|
encoding.writeVarUint(encoder, 0)
|
||||||
|
writeID(encoder, item)
|
||||||
|
} else if (tname !== null) {
|
||||||
|
// case 2: found position at the end of the list and type is stored in y.share
|
||||||
|
encoding.writeUint8(encoder, 1)
|
||||||
|
encoding.writeVarString(encoder, tname)
|
||||||
|
} else if (type !== null) {
|
||||||
|
// case 3: found position at the end of the list and type is attached to an item
|
||||||
|
encoding.writeUint8(encoder, 2)
|
||||||
|
writeID(encoder, type)
|
||||||
|
} else {
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
|
return encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RelativePosition} rpos
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const encodeRelativePosition = rpos => {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
writeRelativePosition(encoder, rpos)
|
||||||
|
return encoding.toUint8Array(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {RelativePosition|null}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readRelativePosition = decoder => {
|
||||||
|
let type = null
|
||||||
|
let tname = null
|
||||||
|
let itemID = null
|
||||||
|
switch (decoding.readVarUint(decoder)) {
|
||||||
|
case 0:
|
||||||
|
// case 1: found position somewhere in the linked list
|
||||||
|
itemID = readID(decoder)
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
// case 2: found position at the end of the list and type is stored in y.share
|
||||||
|
tname = decoding.readVarString(decoder)
|
||||||
|
break
|
||||||
|
case 2: {
|
||||||
|
// case 3: found position at the end of the list and type is attached to an item
|
||||||
|
type = readID(decoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RelativePosition(type, tname, itemID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} uint8Array
|
||||||
|
* @return {RelativePosition|null}
|
||||||
|
*/
|
||||||
|
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RelativePosition} rpos
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @return {AbsolutePosition|null}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||||
|
const store = doc.store
|
||||||
|
const rightID = rpos.item
|
||||||
|
const typeID = rpos.type
|
||||||
|
const tname = rpos.tname
|
||||||
|
let type = null
|
||||||
|
let index = 0
|
||||||
|
if (rightID !== null) {
|
||||||
|
if (getState(store, rightID.client) <= rightID.clock) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const res = followRedone(store, rightID)
|
||||||
|
const right = res.item
|
||||||
|
if (!(right instanceof Item)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
type = right.parent
|
||||||
|
if (type._item === null || !type._item.deleted) {
|
||||||
|
index = right.deleted || !right.countable ? 0 : res.diff
|
||||||
|
let n = right.left
|
||||||
|
while (n !== null) {
|
||||||
|
if (!n.deleted && n.countable) {
|
||||||
|
index += n.length
|
||||||
|
}
|
||||||
|
n = n.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (tname !== null) {
|
||||||
|
type = doc.get(tname)
|
||||||
|
} else if (typeID !== null) {
|
||||||
|
if (getState(store, typeID.client) <= typeID.clock) {
|
||||||
|
// type does not exist yet
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const { item } = followRedone(store, typeID)
|
||||||
|
if (item instanceof Item && item.content instanceof ContentType) {
|
||||||
|
type = item.content.type
|
||||||
|
} else {
|
||||||
|
// struct is garbage collected
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
|
index = type._length
|
||||||
|
}
|
||||||
|
return createAbsolutePosition(type, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RelativePosition|null} a
|
||||||
|
* @param {RelativePosition|null} b
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const compareRelativePositions = (a, b) => a === b || (
|
||||||
|
a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
|
||||||
|
)
|
||||||
138
src/utils/Snapshot.js
Normal file
138
src/utils/Snapshot.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
isDeleted,
|
||||||
|
createDeleteSetFromStructStore,
|
||||||
|
getStateVector,
|
||||||
|
getItemCleanStart,
|
||||||
|
createID,
|
||||||
|
iterateDeletedStructs,
|
||||||
|
writeDeleteSet,
|
||||||
|
writeStateVector,
|
||||||
|
readDeleteSet,
|
||||||
|
readStateVector,
|
||||||
|
createDeleteSet,
|
||||||
|
getState,
|
||||||
|
Transaction, Doc, DeleteSet, Item // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as set from 'lib0/set.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
|
||||||
|
export class Snapshot {
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {Map<number,number>} sv state map
|
||||||
|
*/
|
||||||
|
constructor (ds, sv) {
|
||||||
|
/**
|
||||||
|
* @type {DeleteSet}
|
||||||
|
*/
|
||||||
|
this.ds = ds
|
||||||
|
/**
|
||||||
|
* State Map
|
||||||
|
* @type {Map<number,number>}
|
||||||
|
*/
|
||||||
|
this.sv = sv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Snapshot} snap1
|
||||||
|
* @param {Snapshot} snap2
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const equalSnapshots = (snap1, snap2) => {
|
||||||
|
const ds1 = snap1.ds.clients
|
||||||
|
const ds2 = snap2.ds.clients
|
||||||
|
const sv1 = snap1.sv
|
||||||
|
const sv2 = snap2.sv
|
||||||
|
if (sv1.size !== sv2.size || ds1.size !== ds2.size) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const [key, value] of sv1) {
|
||||||
|
if (sv2.get(key) !== value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [client, dsitems1] of ds1) {
|
||||||
|
const dsitems2 = ds2.get(client) || []
|
||||||
|
if (dsitems1.length !== dsitems2.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < dsitems1.length; i++) {
|
||||||
|
const dsitem1 = dsitems1[i]
|
||||||
|
const dsitem2 = dsitems2[i]
|
||||||
|
if (dsitem1.clock !== dsitem2.clock || dsitem1.len !== dsitem2.len) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
|
export const encodeSnapshot = snapshot => {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
writeDeleteSet(encoder, snapshot.ds)
|
||||||
|
writeStateVector(encoder, snapshot.sv)
|
||||||
|
return encoding.toUint8Array(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} buf
|
||||||
|
* @return {Snapshot}
|
||||||
|
*/
|
||||||
|
export const decodeSnapshot = buf => {
|
||||||
|
const decoder = decoding.createDecoder(buf)
|
||||||
|
return new Snapshot(readDeleteSet(decoder), readStateVector(decoder))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {Map<number,number>} sm
|
||||||
|
* @return {Snapshot}
|
||||||
|
*/
|
||||||
|
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
|
||||||
|
|
||||||
|
export const emptySnapshot = createSnapshot(createDeleteSet(), new Map())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @return {Snapshot}
|
||||||
|
*/
|
||||||
|
export const snapshot = doc => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Item} item
|
||||||
|
* @param {Snapshot|undefined} snapshot
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : (
|
||||||
|
snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
*/
|
||||||
|
export const splitSnapshotAffectedStructs = (transaction, snapshot) => {
|
||||||
|
const meta = map.setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, set.create)
|
||||||
|
const store = transaction.doc.store
|
||||||
|
// check if we already split for this snapshot
|
||||||
|
if (!meta.has(snapshot)) {
|
||||||
|
snapshot.sv.forEach((clock, client) => {
|
||||||
|
if (clock < getState(store, client)) {
|
||||||
|
getItemCleanStart(transaction, createID(client, clock))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
iterateDeletedStructs(transaction, snapshot.ds, item => {})
|
||||||
|
meta.add(snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
271
src/utils/StructStore.js
Normal file
271
src/utils/StructStore.js
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
GC,
|
||||||
|
splitItem,
|
||||||
|
GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
|
||||||
|
|
||||||
|
export class StructStore {
|
||||||
|
constructor () {
|
||||||
|
/**
|
||||||
|
* @type {Map<number,Array<GC|Item>>}
|
||||||
|
*/
|
||||||
|
this.clients = new Map()
|
||||||
|
/**
|
||||||
|
* Store incompleted struct reads here
|
||||||
|
* `i` denotes to the next read operation
|
||||||
|
* We could shift the array of refs instead, but shift is incredible
|
||||||
|
* slow in Chrome for arrays with more than 100k elements
|
||||||
|
* @see tryResumePendingStructRefs
|
||||||
|
* @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
|
||||||
|
*/
|
||||||
|
this.pendingClientsStructRefs = new Map()
|
||||||
|
/**
|
||||||
|
* Stack of pending structs waiting for struct dependencies
|
||||||
|
* Maximum length of stack is structReaders.size
|
||||||
|
* @type {Array<GCRef|ItemRef>}
|
||||||
|
*/
|
||||||
|
this.pendingStack = []
|
||||||
|
/**
|
||||||
|
* @type {Array<decoding.Decoder>}
|
||||||
|
*/
|
||||||
|
this.pendingDeleteReaders = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the states as a Map<client,clock>.
|
||||||
|
* Note that clock refers to the next expected clock id.
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @return {Map<number,number>}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const getStateVector = store => {
|
||||||
|
const sm = new Map()
|
||||||
|
store.clients.forEach((structs, client) => {
|
||||||
|
const struct = structs[structs.length - 1]
|
||||||
|
sm.set(client, struct.id.clock + struct.length)
|
||||||
|
})
|
||||||
|
return sm
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {number} client
|
||||||
|
* @return {number}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const getState = (store, client) => {
|
||||||
|
const structs = store.clients.get(client)
|
||||||
|
if (structs === undefined) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const lastStruct = structs[structs.length - 1]
|
||||||
|
return lastStruct.id.clock + lastStruct.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const integretyCheck = store => {
|
||||||
|
store.clients.forEach(structs => {
|
||||||
|
for (let i = 1; i < structs.length; i++) {
|
||||||
|
const l = structs[i - 1]
|
||||||
|
const r = structs[i]
|
||||||
|
if (l.id.clock + l.length !== r.id.clock) {
|
||||||
|
throw new Error('StructStore failed integrety check')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {GC|Item} struct
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const addStruct = (store, struct) => {
|
||||||
|
let structs = store.clients.get(struct.id.client)
|
||||||
|
if (structs === undefined) {
|
||||||
|
structs = []
|
||||||
|
store.clients.set(struct.id.client, structs)
|
||||||
|
} else {
|
||||||
|
const lastStruct = structs[structs.length - 1]
|
||||||
|
if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) {
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
structs.push(struct)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a binary search on a sorted array
|
||||||
|
* @param {Array<any>} structs
|
||||||
|
* @param {number} clock
|
||||||
|
* @return {number}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const findIndexSS = (structs, clock) => {
|
||||||
|
let left = 0
|
||||||
|
let right = structs.length - 1
|
||||||
|
while (left <= right) {
|
||||||
|
const midindex = math.floor((left + right) / 2)
|
||||||
|
const mid = structs[midindex]
|
||||||
|
const midclock = mid.id.clock
|
||||||
|
if (midclock <= clock) {
|
||||||
|
if (clock < midclock + mid.length) {
|
||||||
|
return midindex
|
||||||
|
}
|
||||||
|
left = midindex + 1
|
||||||
|
} else {
|
||||||
|
right = midindex - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always check state before looking for a struct in StructStore
|
||||||
|
// Therefore the case of not finding a struct is unexpected
|
||||||
|
throw error.unexpectedCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {GC|Item}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const find = (store, id) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<GC|Item>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(id.client)
|
||||||
|
return structs[findIndexSS(structs, id.clock)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
*
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {Item}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
export const getItem = (store, id) => find(store, id)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Array<Item|GC>} structs
|
||||||
|
* @param {number} clock
|
||||||
|
*/
|
||||||
|
export const findIndexCleanStart = (transaction, structs, clock) => {
|
||||||
|
const index = findIndexSS(structs, clock)
|
||||||
|
const struct = structs[index]
|
||||||
|
if (struct.id.clock < clock && struct instanceof Item) {
|
||||||
|
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
|
||||||
|
return index + 1
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {Item}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const getItemCleanStart = (transaction, id) => {
|
||||||
|
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client))
|
||||||
|
return structs[findIndexCleanStart(transaction, structs, id.clock)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {ID} id
|
||||||
|
* @return {Item}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const getItemCleanEnd = (transaction, store, id) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<Item>}
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
const structs = store.clients.get(id.client)
|
||||||
|
const index = findIndexSS(structs, id.clock)
|
||||||
|
const struct = structs[index]
|
||||||
|
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
|
||||||
|
structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1))
|
||||||
|
}
|
||||||
|
return struct
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace `item` with `newitem` in store
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {GC|Item} struct
|
||||||
|
* @param {GC|Item} newStruct
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const replaceStruct = (store, struct, newStruct) => {
|
||||||
|
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(struct.id.client))
|
||||||
|
structs[findIndexSS(structs, struct.id.clock)] = newStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over a range of structs
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {Array<Item|GC>} structs
|
||||||
|
* @param {number} clockStart Inclusive start
|
||||||
|
* @param {number} len
|
||||||
|
* @param {function(GC|Item):void} f
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const iterateStructs = (transaction, structs, clockStart, len, f) => {
|
||||||
|
if (len === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const clockEnd = clockStart + len
|
||||||
|
let index = findIndexCleanStart(transaction, structs, clockStart)
|
||||||
|
let struct
|
||||||
|
do {
|
||||||
|
struct = structs[index++]
|
||||||
|
if (clockEnd < struct.id.clock + struct.length) {
|
||||||
|
findIndexCleanStart(transaction, structs, clockEnd)
|
||||||
|
}
|
||||||
|
f(struct)
|
||||||
|
} while (index < structs.length && structs[index].id.clock < clockEnd)
|
||||||
|
}
|
||||||
372
src/utils/Transaction.js
Normal file
372
src/utils/Transaction.js
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
getState,
|
||||||
|
createID,
|
||||||
|
writeStructsFromTransaction,
|
||||||
|
writeDeleteSet,
|
||||||
|
DeleteSet,
|
||||||
|
sortAndMergeDeleteSet,
|
||||||
|
getStateVector,
|
||||||
|
findIndexSS,
|
||||||
|
callEventHandlerListeners,
|
||||||
|
Item,
|
||||||
|
generateNewClientId,
|
||||||
|
StructStore, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as map from 'lib0/map.js'
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
|
import * as set from 'lib0/set.js'
|
||||||
|
import * as logging from 'lib0/logging.js'
|
||||||
|
import { callAll } from 'lib0/function.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transaction is created for every change on the Yjs model. It is possible
|
||||||
|
* to bundle changes on the Yjs model in a single transaction to
|
||||||
|
* minimize the number on messages sent and the number of observer calls.
|
||||||
|
* If possible the user of this library should bundle as many changes as
|
||||||
|
* possible. Here is an example to illustrate the advantages of bundling:
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const map = y.define('map', YMap)
|
||||||
|
* // Log content when change is triggered
|
||||||
|
* map.observe(() => {
|
||||||
|
* console.log('change triggered')
|
||||||
|
* })
|
||||||
|
* // Each change on the map type triggers a log message:
|
||||||
|
* map.set('a', 0) // => "change triggered"
|
||||||
|
* map.set('b', 0) // => "change triggered"
|
||||||
|
* // When put in a transaction, it will trigger the log after the transaction:
|
||||||
|
* y.transact(() => {
|
||||||
|
* map.set('a', 1)
|
||||||
|
* map.set('b', 1)
|
||||||
|
* }) // => "change triggered"
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class Transaction {
|
||||||
|
/**
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @param {any} origin
|
||||||
|
* @param {boolean} local
|
||||||
|
*/
|
||||||
|
constructor (doc, origin, local) {
|
||||||
|
/**
|
||||||
|
* The Yjs instance.
|
||||||
|
* @type {Doc}
|
||||||
|
*/
|
||||||
|
this.doc = doc
|
||||||
|
/**
|
||||||
|
* Describes the set of deleted items by ids
|
||||||
|
* @type {DeleteSet}
|
||||||
|
*/
|
||||||
|
this.deleteSet = new DeleteSet()
|
||||||
|
/**
|
||||||
|
* Holds the state before the transaction started.
|
||||||
|
* @type {Map<Number,Number>}
|
||||||
|
*/
|
||||||
|
this.beforeState = getStateVector(doc.store)
|
||||||
|
/**
|
||||||
|
* Holds the state after the transaction.
|
||||||
|
* @type {Map<Number,Number>}
|
||||||
|
*/
|
||||||
|
this.afterState = new Map()
|
||||||
|
/**
|
||||||
|
* All types that were directly modified (property added or child
|
||||||
|
* inserted/deleted). New types are not included in this Set.
|
||||||
|
* Maps from type to parentSubs (`item.parentSub = null` for YArray)
|
||||||
|
* @type {Map<AbstractType<YEvent>,Set<String|null>>}
|
||||||
|
*/
|
||||||
|
this.changed = new Map()
|
||||||
|
/**
|
||||||
|
* Stores the events for the types that observe also child elements.
|
||||||
|
* It is mainly used by `observeDeep`.
|
||||||
|
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
|
||||||
|
*/
|
||||||
|
this.changedParentTypes = new Map()
|
||||||
|
/**
|
||||||
|
* @type {Set<ID>}
|
||||||
|
*/
|
||||||
|
this._mergeStructs = new Set()
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
this.origin = origin
|
||||||
|
/**
|
||||||
|
* Stores meta information on the transaction
|
||||||
|
* @type {Map<any,any>}
|
||||||
|
*/
|
||||||
|
this.meta = new Map()
|
||||||
|
/**
|
||||||
|
* Whether this change originates from this doc.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.local = local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
export const computeUpdateMessageFromTransaction = transaction => {
|
||||||
|
if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
sortAndMergeDeleteSet(transaction.deleteSet)
|
||||||
|
writeStructsFromTransaction(encoder, transaction)
|
||||||
|
writeDeleteSet(encoder, transaction.deleteSet)
|
||||||
|
return encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const nextID = transaction => {
|
||||||
|
const y = transaction.doc
|
||||||
|
return createID(y.clientID, getState(y.store, y.clientID))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If `type.parent` was added in current transaction, `type` technically
|
||||||
|
* did not change, it was just added and we should not fire events for `type`.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {AbstractType<YEvent>} type
|
||||||
|
* @param {string|null} parentSub
|
||||||
|
*/
|
||||||
|
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
|
||||||
|
const item = type._item
|
||||||
|
if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) {
|
||||||
|
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<AbstractStruct>} structs
|
||||||
|
* @param {number} pos
|
||||||
|
*/
|
||||||
|
const tryToMergeWithLeft = (structs, pos) => {
|
||||||
|
const left = structs[pos - 1]
|
||||||
|
const right = structs[pos]
|
||||||
|
if (left.deleted === right.deleted && left.constructor === right.constructor) {
|
||||||
|
if (left.mergeWith(right)) {
|
||||||
|
structs.splice(pos, 1)
|
||||||
|
if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
|
||||||
|
right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {function(Item):boolean} gcFilter
|
||||||
|
*/
|
||||||
|
const tryGcDeleteSet = (ds, store, gcFilter) => {
|
||||||
|
for (const [client, deleteItems] of ds.clients) {
|
||||||
|
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||||
|
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||||
|
const deleteItem = deleteItems[di]
|
||||||
|
const endDeleteItemClock = deleteItem.clock + deleteItem.len
|
||||||
|
for (
|
||||||
|
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
|
||||||
|
si < structs.length && struct.id.clock < endDeleteItemClock;
|
||||||
|
struct = structs[++si]
|
||||||
|
) {
|
||||||
|
const struct = structs[si]
|
||||||
|
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
|
||||||
|
struct.gc(store, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {StructStore} store
|
||||||
|
*/
|
||||||
|
const tryMergeDeleteSet = (ds, store) => {
|
||||||
|
// try to merge deleted / gc'd items
|
||||||
|
// merge from right to left for better efficiecy and so we don't miss any merge targets
|
||||||
|
for (const [client, deleteItems] of ds.clients) {
|
||||||
|
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||||
|
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||||
|
const deleteItem = deleteItems[di]
|
||||||
|
// start with merging the item next to the last deleted item
|
||||||
|
const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
|
||||||
|
for (
|
||||||
|
let si = mostRightIndexToCheck, struct = structs[si];
|
||||||
|
si > 0 && struct.id.clock >= deleteItem.clock;
|
||||||
|
struct = structs[--si]
|
||||||
|
) {
|
||||||
|
tryToMergeWithLeft(structs, si)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {function(Item):boolean} gcFilter
|
||||||
|
*/
|
||||||
|
export const tryGc = (ds, store, gcFilter) => {
|
||||||
|
tryGcDeleteSet(ds, store, gcFilter)
|
||||||
|
tryMergeDeleteSet(ds, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<Transaction>} transactionCleanups
|
||||||
|
* @param {number} i
|
||||||
|
*/
|
||||||
|
const cleanupTransactions = (transactionCleanups, i) => {
|
||||||
|
if (i < transactionCleanups.length) {
|
||||||
|
const transaction = transactionCleanups[i]
|
||||||
|
const doc = transaction.doc
|
||||||
|
const store = doc.store
|
||||||
|
const ds = transaction.deleteSet
|
||||||
|
try {
|
||||||
|
sortAndMergeDeleteSet(ds)
|
||||||
|
transaction.afterState = getStateVector(transaction.doc.store)
|
||||||
|
doc._transaction = null
|
||||||
|
doc.emit('beforeObserverCalls', [transaction, doc])
|
||||||
|
/**
|
||||||
|
* An array of event callbacks.
|
||||||
|
*
|
||||||
|
* Each callback is called even if the other ones throw errors.
|
||||||
|
*
|
||||||
|
* @type {Array<function():void>}
|
||||||
|
*/
|
||||||
|
const fs = []
|
||||||
|
// observe events on changed types
|
||||||
|
transaction.changed.forEach((subs, itemtype) =>
|
||||||
|
fs.push(() => {
|
||||||
|
if (itemtype._item === null || !itemtype._item.deleted) {
|
||||||
|
itemtype._callObserver(transaction, subs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
fs.push(() => {
|
||||||
|
// deep observe events
|
||||||
|
transaction.changedParentTypes.forEach((events, type) =>
|
||||||
|
fs.push(() => {
|
||||||
|
// We need to think about the possibility that the user transforms the
|
||||||
|
// Y.Doc in the event.
|
||||||
|
if (type._item === null || !type._item.deleted) {
|
||||||
|
events = events
|
||||||
|
.filter(event =>
|
||||||
|
event.target._item === null || !event.target._item.deleted
|
||||||
|
)
|
||||||
|
events
|
||||||
|
.forEach(event => {
|
||||||
|
event.currentTarget = type
|
||||||
|
})
|
||||||
|
// We don't need to check for events.length
|
||||||
|
// because we know it has at least one element
|
||||||
|
callEventHandlerListeners(type._dEH, events, transaction)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
|
||||||
|
})
|
||||||
|
callAll(fs, [])
|
||||||
|
} finally {
|
||||||
|
// Replace deleted items with ItemDeleted / GC.
|
||||||
|
// This is where content is actually remove from the Yjs Doc.
|
||||||
|
if (doc.gc) {
|
||||||
|
tryGcDeleteSet(ds, store, doc.gcFilter)
|
||||||
|
}
|
||||||
|
tryMergeDeleteSet(ds, store)
|
||||||
|
|
||||||
|
// on all affected store.clients props, try to merge
|
||||||
|
for (const [client, clock] of transaction.afterState) {
|
||||||
|
const beforeClock = transaction.beforeState.get(client) || 0
|
||||||
|
if (beforeClock !== clock) {
|
||||||
|
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||||
|
// we iterate from right to left so we can safely remove entries
|
||||||
|
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
|
||||||
|
for (let i = structs.length - 1; i >= firstChangePos; i--) {
|
||||||
|
tryToMergeWithLeft(structs, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// try to merge mergeStructs
|
||||||
|
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
|
||||||
|
// but at the moment DS does not handle duplicates
|
||||||
|
for (const mid of transaction._mergeStructs) {
|
||||||
|
const client = mid.client
|
||||||
|
const clock = mid.clock
|
||||||
|
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
|
||||||
|
const replacedStructPos = findIndexSS(structs, clock)
|
||||||
|
if (replacedStructPos + 1 < structs.length) {
|
||||||
|
tryToMergeWithLeft(structs, replacedStructPos + 1)
|
||||||
|
}
|
||||||
|
if (replacedStructPos > 0) {
|
||||||
|
tryToMergeWithLeft(structs, replacedStructPos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
|
||||||
|
doc.clientID = generateNewClientId()
|
||||||
|
logging.print(logging.ORANGE, logging.BOLD, '[yjs] ', logging.UNBOLD, logging.RED, 'Changed the client-id because another client seems to be using it.')
|
||||||
|
}
|
||||||
|
// @todo Merge all the transactions into one and provide send the data as a single update message
|
||||||
|
doc.emit('afterTransactionCleanup', [transaction, doc])
|
||||||
|
if (doc._observers.has('update')) {
|
||||||
|
const updateMessage = computeUpdateMessageFromTransaction(transaction)
|
||||||
|
if (updateMessage !== null) {
|
||||||
|
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (transactionCleanups.length <= i + 1) {
|
||||||
|
doc._transactionCleanups = []
|
||||||
|
} else {
|
||||||
|
cleanupTransactions(transactionCleanups, i + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the functionality of `y.transact(()=>{..})`
|
||||||
|
*
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @param {function(Transaction):void} f
|
||||||
|
* @param {any} [origin=true]
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const transact = (doc, f, origin = null, local = true) => {
|
||||||
|
const transactionCleanups = doc._transactionCleanups
|
||||||
|
let initialCall = false
|
||||||
|
if (doc._transaction === null) {
|
||||||
|
initialCall = true
|
||||||
|
doc._transaction = new Transaction(doc, origin, local)
|
||||||
|
transactionCleanups.push(doc._transaction)
|
||||||
|
doc.emit('beforeTransaction', [doc._transaction, doc])
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
f(doc._transaction)
|
||||||
|
} finally {
|
||||||
|
if (initialCall && transactionCleanups[0] === doc._transaction) {
|
||||||
|
// The first transaction ended, now process observer calls.
|
||||||
|
// Observer call may create new transactions for which we need to call the observers and do cleanup.
|
||||||
|
// We don't want to nest these calls, so we execute these calls one after
|
||||||
|
// another.
|
||||||
|
// Also we need to ensure that all cleanups are called, even if the
|
||||||
|
// observes throw errors.
|
||||||
|
// This file is full of hacky try {} finally {} blocks to ensure that an
|
||||||
|
// event can throw errors and also that the cleanup is called.
|
||||||
|
cleanupTransactions(transactionCleanups, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
283
src/utils/UndoManager.js
Normal file
283
src/utils/UndoManager.js
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import {
|
||||||
|
mergeDeleteSets,
|
||||||
|
iterateDeletedStructs,
|
||||||
|
keepItem,
|
||||||
|
transact,
|
||||||
|
redoItem,
|
||||||
|
iterateStructs,
|
||||||
|
isParentOf,
|
||||||
|
createID,
|
||||||
|
followRedone,
|
||||||
|
getItemCleanStart,
|
||||||
|
getState,
|
||||||
|
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as time from 'lib0/time.js'
|
||||||
|
import { Observable } from 'lib0/observable.js'
|
||||||
|
|
||||||
|
class StackItem {
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds
|
||||||
|
* @param {number} start clock start of the local client
|
||||||
|
* @param {number} len
|
||||||
|
*/
|
||||||
|
constructor (ds, start, len) {
|
||||||
|
this.ds = ds
|
||||||
|
this.start = start
|
||||||
|
this.len = len
|
||||||
|
/**
|
||||||
|
* Use this to save and restore metadata like selection range
|
||||||
|
*/
|
||||||
|
this.meta = new Map()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UndoManager} undoManager
|
||||||
|
* @param {Array<StackItem>} stack
|
||||||
|
* @param {string} eventType
|
||||||
|
* @return {StackItem?}
|
||||||
|
*/
|
||||||
|
const popStackItem = (undoManager, stack, eventType) => {
|
||||||
|
/**
|
||||||
|
* Whether a change happened
|
||||||
|
* @type {StackItem?}
|
||||||
|
*/
|
||||||
|
let result = null
|
||||||
|
const doc = undoManager.doc
|
||||||
|
const scope = undoManager.scope
|
||||||
|
transact(doc, transaction => {
|
||||||
|
while (stack.length > 0 && result === null) {
|
||||||
|
const store = doc.store
|
||||||
|
const clientID = doc.clientID
|
||||||
|
const stackItem = /** @type {StackItem} */ (stack.pop())
|
||||||
|
const stackStartClock = stackItem.start
|
||||||
|
const stackEndClock = stackItem.start + stackItem.len
|
||||||
|
const itemsToRedo = new Set()
|
||||||
|
// @todo iterateStructs should not need the structs parameter
|
||||||
|
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientID))
|
||||||
|
let performedChange = false
|
||||||
|
if (stackStartClock !== stackEndClock) {
|
||||||
|
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
|
||||||
|
getItemCleanStart(transaction, createID(clientID, stackStartClock))
|
||||||
|
if (stackEndClock < getState(doc.store, clientID)) {
|
||||||
|
getItemCleanStart(transaction, createID(clientID, stackEndClock))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iterateDeletedStructs(transaction, stackItem.ds, struct => {
|
||||||
|
if (
|
||||||
|
struct instanceof Item &&
|
||||||
|
scope.some(type => isParentOf(type, struct)) &&
|
||||||
|
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
|
||||||
|
!(struct.id.client === clientID && struct.id.clock >= stackStartClock && struct.id.clock < stackEndClock)
|
||||||
|
) {
|
||||||
|
itemsToRedo.add(struct)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
itemsToRedo.forEach(struct => {
|
||||||
|
performedChange = redoItem(transaction, struct, itemsToRedo) !== null || performedChange
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* @type {Array<Item>}
|
||||||
|
*/
|
||||||
|
const itemsToDelete = []
|
||||||
|
iterateStructs(transaction, structs, stackStartClock, stackItem.len, struct => {
|
||||||
|
if (struct instanceof Item) {
|
||||||
|
if (struct.redone !== null) {
|
||||||
|
let { item, diff } = followRedone(store, struct.id)
|
||||||
|
if (diff > 0) {
|
||||||
|
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
|
||||||
|
}
|
||||||
|
if (item.length > stackItem.len) {
|
||||||
|
getItemCleanStart(transaction, createID(item.id.client, stackEndClock))
|
||||||
|
}
|
||||||
|
struct = item
|
||||||
|
}
|
||||||
|
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
|
||||||
|
itemsToDelete.push(struct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// We want to delete in reverse order so that children are deleted before
|
||||||
|
// parents, so we have more information available when items are filtered.
|
||||||
|
for (let i = itemsToDelete.length - 1; i >= 0; i--) {
|
||||||
|
const item = itemsToDelete[i]
|
||||||
|
if (undoManager.deleteFilter(item)) {
|
||||||
|
item.delete(transaction)
|
||||||
|
performedChange = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = stackItem
|
||||||
|
if (result != null) {
|
||||||
|
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, undoManager)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} UndoManagerOptions
|
||||||
|
* @property {number} [UndoManagerOptions.captureTimeout=500]
|
||||||
|
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
|
||||||
|
* it is necessary to filter whan an Undo/Redo operation can delete. If this
|
||||||
|
* filter returns false, the type/item won't be deleted even it is in the
|
||||||
|
* undo/redo scope.
|
||||||
|
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires 'stack-item-added' event when a stack item was added to either the undo- or
|
||||||
|
* the redo-stack. You may store additional stack information via the
|
||||||
|
* metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties).
|
||||||
|
* Fires 'stack-item-popped' event when a stack item was popped from either the
|
||||||
|
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
|
||||||
|
*
|
||||||
|
* @extends {Observable<'stack-item-added'|'stack-item-popped'>}
|
||||||
|
*/
|
||||||
|
export class UndoManager extends Observable {
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
|
||||||
|
* @param {UndoManagerOptions} options
|
||||||
|
*/
|
||||||
|
constructor (typeScope, { captureTimeout, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
|
||||||
|
if (captureTimeout == null) {
|
||||||
|
captureTimeout = 500
|
||||||
|
}
|
||||||
|
super()
|
||||||
|
this.scope = typeScope instanceof Array ? typeScope : [typeScope]
|
||||||
|
this.deleteFilter = deleteFilter
|
||||||
|
trackedOrigins.add(this)
|
||||||
|
this.trackedOrigins = trackedOrigins
|
||||||
|
/**
|
||||||
|
* @type {Array<StackItem>}
|
||||||
|
*/
|
||||||
|
this.undoStack = []
|
||||||
|
/**
|
||||||
|
* @type {Array<StackItem>}
|
||||||
|
*/
|
||||||
|
this.redoStack = []
|
||||||
|
/**
|
||||||
|
* Whether the client is currently undoing (calling UndoManager.undo)
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.undoing = false
|
||||||
|
this.redoing = false
|
||||||
|
this.doc = /** @type {Doc} */ (this.scope[0].doc)
|
||||||
|
this.lastChange = 0
|
||||||
|
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
|
||||||
|
// Only track certain transactions
|
||||||
|
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const undoing = this.undoing
|
||||||
|
const redoing = this.redoing
|
||||||
|
const stack = undoing ? this.redoStack : this.undoStack
|
||||||
|
if (undoing) {
|
||||||
|
this.stopCapturing() // next undo should not be appended to last stack item
|
||||||
|
} else if (!redoing) {
|
||||||
|
// neither undoing nor redoing: delete redoStack
|
||||||
|
this.redoStack = []
|
||||||
|
}
|
||||||
|
const beforeState = transaction.beforeState.get(this.doc.clientID) || 0
|
||||||
|
const afterState = transaction.afterState.get(this.doc.clientID) || 0
|
||||||
|
const now = time.getUnixTime()
|
||||||
|
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||||
|
// append change to last stack op
|
||||||
|
const lastOp = stack[stack.length - 1]
|
||||||
|
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
|
||||||
|
lastOp.len = afterState - lastOp.start
|
||||||
|
} else {
|
||||||
|
// create a new stack op
|
||||||
|
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState - beforeState))
|
||||||
|
}
|
||||||
|
if (!undoing && !redoing) {
|
||||||
|
this.lastChange = now
|
||||||
|
}
|
||||||
|
// make sure that deleted structs are not gc'd
|
||||||
|
iterateDeletedStructs(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
|
||||||
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
|
keepItem(item, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clear () {
|
||||||
|
this.doc.transact(transaction => {
|
||||||
|
/**
|
||||||
|
* @param {StackItem} stackItem
|
||||||
|
*/
|
||||||
|
const clearItem = stackItem => {
|
||||||
|
iterateDeletedStructs(transaction, stackItem.ds, item => {
|
||||||
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
|
keepItem(item, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.undoStack.forEach(clearItem)
|
||||||
|
this.redoStack.forEach(clearItem)
|
||||||
|
})
|
||||||
|
this.undoStack = []
|
||||||
|
this.redoStack = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UndoManager merges Undo-StackItem if they are created within time-gap
|
||||||
|
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
|
||||||
|
* StackItem won't be merged.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // without stopCapturing
|
||||||
|
* ytext.insert(0, 'a')
|
||||||
|
* ytext.insert(1, 'b')
|
||||||
|
* um.undo()
|
||||||
|
* ytext.toString() // => '' (note that 'ab' was removed)
|
||||||
|
* // with stopCapturing
|
||||||
|
* ytext.insert(0, 'a')
|
||||||
|
* um.stopCapturing()
|
||||||
|
* ytext.insert(0, 'b')
|
||||||
|
* um.undo()
|
||||||
|
* ytext.toString() // => 'a' (note that only 'b' was removed)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
stopCapturing () {
|
||||||
|
this.lastChange = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo last changes on type.
|
||||||
|
*
|
||||||
|
* @return {StackItem?} Returns StackItem if a change was applied
|
||||||
|
*/
|
||||||
|
undo () {
|
||||||
|
this.undoing = true
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = popStackItem(this, this.undoStack, 'undo')
|
||||||
|
} finally {
|
||||||
|
this.undoing = false
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redo last undo operation.
|
||||||
|
*
|
||||||
|
* @return {StackItem?} Returns StackItem if a change was applied
|
||||||
|
*/
|
||||||
|
redo () {
|
||||||
|
this.redoing = true
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = popStackItem(this, this.redoStack, 'redo')
|
||||||
|
} finally {
|
||||||
|
this.redoing = false
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/utils/YEvent.js
Normal file
226
src/utils/YEvent.js
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
isDeleted,
|
||||||
|
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as set from 'lib0/set.js'
|
||||||
|
import * as array from 'lib0/array.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YEvent describes the changes on a YType.
|
||||||
|
*/
|
||||||
|
export class YEvent {
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} target The changed type.
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*/
|
||||||
|
constructor (target, transaction) {
|
||||||
|
/**
|
||||||
|
* The type on which this event was created on.
|
||||||
|
* @type {AbstractType<any>}
|
||||||
|
*/
|
||||||
|
this.target = target
|
||||||
|
/**
|
||||||
|
* The current target on which the observe callback is called.
|
||||||
|
* @type {AbstractType<any>}
|
||||||
|
*/
|
||||||
|
this.currentTarget = target
|
||||||
|
/**
|
||||||
|
* The transaction that triggered this event.
|
||||||
|
* @type {Transaction}
|
||||||
|
*/
|
||||||
|
this.transaction = transaction
|
||||||
|
/**
|
||||||
|
* @type {Object|null}
|
||||||
|
*/
|
||||||
|
this._changes = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the path from `y` to the changed type.
|
||||||
|
*
|
||||||
|
* The following property holds:
|
||||||
|
* @example
|
||||||
|
* let type = y
|
||||||
|
* event.path.forEach(dir => {
|
||||||
|
* type = type.get(dir)
|
||||||
|
* })
|
||||||
|
* type === event.target // => true
|
||||||
|
*/
|
||||||
|
get path () {
|
||||||
|
// @ts-ignore _item is defined because target is integrated
|
||||||
|
return getPathTo(this.currentTarget, this.target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a struct is deleted by this event.
|
||||||
|
*
|
||||||
|
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
|
||||||
|
*
|
||||||
|
* @param {AbstractStruct} struct
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
deletes (struct) {
|
||||||
|
return isDeleted(this.transaction.deleteSet, struct.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a struct is added by this event.
|
||||||
|
*
|
||||||
|
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
|
||||||
|
*
|
||||||
|
* @param {AbstractStruct} struct
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
adds (struct) {
|
||||||
|
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {{added:Set<Item>,deleted:Set<Item>,delta:Array<{insert:Array<any>}|{delete:number}|{retain:number}>}}
|
||||||
|
*/
|
||||||
|
get changes () {
|
||||||
|
let changes = this._changes
|
||||||
|
if (changes === null) {
|
||||||
|
const target = this.target
|
||||||
|
const added = set.create()
|
||||||
|
const deleted = set.create()
|
||||||
|
/**
|
||||||
|
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
|
||||||
|
*/
|
||||||
|
const delta = []
|
||||||
|
/**
|
||||||
|
* @type {Map<string,{ action: 'add' | 'update' | 'delete', oldValue: any}>}
|
||||||
|
*/
|
||||||
|
const keys = new Map()
|
||||||
|
changes = {
|
||||||
|
added, deleted, delta, keys
|
||||||
|
}
|
||||||
|
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||||
|
if (changed.has(null)) {
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let lastOp = null
|
||||||
|
const packOp = () => {
|
||||||
|
if (lastOp) {
|
||||||
|
delta.push(lastOp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let item = target._start; item !== null; item = item.right) {
|
||||||
|
if (item.deleted) {
|
||||||
|
if (this.deletes(item) && !this.adds(item)) {
|
||||||
|
if (lastOp === null || lastOp.delete === undefined) {
|
||||||
|
packOp()
|
||||||
|
lastOp = { delete: 0 }
|
||||||
|
}
|
||||||
|
lastOp.delete += item.length
|
||||||
|
deleted.add(item)
|
||||||
|
} // else nop
|
||||||
|
} else {
|
||||||
|
if (this.adds(item)) {
|
||||||
|
if (lastOp === null || lastOp.insert === undefined) {
|
||||||
|
packOp()
|
||||||
|
lastOp = { insert: [] }
|
||||||
|
}
|
||||||
|
lastOp.insert = lastOp.insert.concat(item.content.getContent())
|
||||||
|
added.add(item)
|
||||||
|
} else {
|
||||||
|
if (lastOp === null || lastOp.retain === undefined) {
|
||||||
|
packOp()
|
||||||
|
lastOp = { retain: 0 }
|
||||||
|
}
|
||||||
|
lastOp.retain += item.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastOp !== null && lastOp.retain === undefined) {
|
||||||
|
packOp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed.forEach(key => {
|
||||||
|
if (key !== null) {
|
||||||
|
const item = /** @type {Item} */ (target._map.get(key))
|
||||||
|
/**
|
||||||
|
* @type {'delete' | 'add' | 'update'}
|
||||||
|
*/
|
||||||
|
let action
|
||||||
|
let oldValue
|
||||||
|
if (this.adds(item)) {
|
||||||
|
let prev = item.left
|
||||||
|
while (prev !== null && this.adds(prev)) {
|
||||||
|
prev = prev.left
|
||||||
|
}
|
||||||
|
if (this.deletes(item)) {
|
||||||
|
if (prev !== null && this.deletes(prev)) {
|
||||||
|
action = 'delete'
|
||||||
|
oldValue = array.last(prev.content.getContent())
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (prev !== null && this.deletes(prev)) {
|
||||||
|
action = 'update'
|
||||||
|
oldValue = array.last(prev.content.getContent())
|
||||||
|
} else {
|
||||||
|
action = 'add'
|
||||||
|
oldValue = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.deletes(item)) {
|
||||||
|
action = 'delete'
|
||||||
|
oldValue = array.last(/** @type {Item} */ item.content.getContent())
|
||||||
|
} else {
|
||||||
|
return // nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys.set(key, { action, oldValue })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this._changes = changes
|
||||||
|
}
|
||||||
|
return /** @type {any} */ (changes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the path from this type to the specified target.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // `child` should be accessible via `type.get(path[0]).get(path[1])..`
|
||||||
|
* const path = type.getPathTo(child)
|
||||||
|
* // assuming `type instanceof YArray`
|
||||||
|
* console.log(path) // might look like => [2, 'key1']
|
||||||
|
* child === type.get(path[0]).get(path[1])
|
||||||
|
*
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {AbstractType<any>} child target
|
||||||
|
* @return {Array<string|number>} Path to the target
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const getPathTo = (parent, child) => {
|
||||||
|
const path = []
|
||||||
|
while (child._item !== null && child !== parent) {
|
||||||
|
if (child._item.parentSub !== null) {
|
||||||
|
// parent is map-ish
|
||||||
|
path.unshift(child._item.parentSub)
|
||||||
|
} else {
|
||||||
|
// parent is array-ish
|
||||||
|
let i = 0
|
||||||
|
let c = child._item.parent._start
|
||||||
|
while (c !== child._item && c !== null) {
|
||||||
|
if (!c.deleted) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c = c.right
|
||||||
|
}
|
||||||
|
path.unshift(i)
|
||||||
|
}
|
||||||
|
child = child._item.parent
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
423
src/utils/encoding.js
Normal file
423
src/utils/encoding.js
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @module encoding
|
||||||
|
*
|
||||||
|
* We use the first five bits in the info flag for determining the type of the struct.
|
||||||
|
*
|
||||||
|
* 0: GC
|
||||||
|
* 1: Item with Deleted content
|
||||||
|
* 2: Item with JSON content
|
||||||
|
* 3: Item with Binary content
|
||||||
|
* 4: Item with String content
|
||||||
|
* 5: Item with Embed content (for richtext content)
|
||||||
|
* 6: Item with Format content (a formatting marker for richtext content)
|
||||||
|
* 7: Item with Type
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
findIndexSS,
|
||||||
|
GCRef,
|
||||||
|
ItemRef,
|
||||||
|
writeID,
|
||||||
|
createID,
|
||||||
|
readID,
|
||||||
|
getState,
|
||||||
|
getStateVector,
|
||||||
|
readAndApplyDeleteSet,
|
||||||
|
writeDeleteSet,
|
||||||
|
createDeleteSetFromStructStore,
|
||||||
|
transact,
|
||||||
|
Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
|
||||||
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as binary from 'lib0/binary.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {Array<AbstractStruct>} structs All structs by `client`
|
||||||
|
* @param {number} client
|
||||||
|
* @param {number} clock write structs starting with `ID(client,clock)`
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const writeStructs = (encoder, structs, client, clock) => {
|
||||||
|
// write first id
|
||||||
|
const startNewStructs = findIndexSS(structs, clock)
|
||||||
|
// write # encoded structs
|
||||||
|
encoding.writeVarUint(encoder, structs.length - startNewStructs)
|
||||||
|
writeID(encoder, createID(client, clock))
|
||||||
|
const firstStruct = structs[startNewStructs]
|
||||||
|
// write first struct with an offset
|
||||||
|
firstStruct.write(encoder, clock - firstStruct.id.clock, 0)
|
||||||
|
for (let i = startNewStructs + 1; i < structs.length; i++) {
|
||||||
|
structs[i].write(encoder, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {number} numOfStructs
|
||||||
|
* @param {ID} nextID
|
||||||
|
* @return {Array<GCRef|ItemRef>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const readStructRefs = (decoder, numOfStructs, nextID) => {
|
||||||
|
/**
|
||||||
|
* @type {Array<GCRef|ItemRef>}
|
||||||
|
*/
|
||||||
|
const refs = []
|
||||||
|
for (let i = 0; i < numOfStructs; i++) {
|
||||||
|
const info = decoding.readUint8(decoder)
|
||||||
|
const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, nextID, info) : new ItemRef(decoder, nextID, info)
|
||||||
|
nextID = createID(nextID.client, nextID.clock + ref.length)
|
||||||
|
refs.push(ref)
|
||||||
|
}
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {Map<number,number>} _sm
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeClientsStructs = (encoder, store, _sm) => {
|
||||||
|
// we filter all valid _sm entries into sm
|
||||||
|
const sm = new Map()
|
||||||
|
_sm.forEach((clock, client) => {
|
||||||
|
// only write if new structs are available
|
||||||
|
if (getState(store, client) > clock) {
|
||||||
|
sm.set(client, clock)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
getStateVector(store).forEach((clock, client) => {
|
||||||
|
if (!_sm.has(client)) {
|
||||||
|
sm.set(client, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// write # states that were updated
|
||||||
|
encoding.writeVarUint(encoder, sm.size)
|
||||||
|
sm.forEach((clock, client) => {
|
||||||
|
// @ts-ignore
|
||||||
|
writeStructs(encoder, store.clients.get(client), client, clock)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||||
|
* @return {Map<number,Array<GCRef|ItemRef>>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readClientsStructRefs = decoder => {
|
||||||
|
/**
|
||||||
|
* @type {Map<number,Array<GCRef|ItemRef>>}
|
||||||
|
*/
|
||||||
|
const clientRefs = new Map()
|
||||||
|
const numOfStateUpdates = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < numOfStateUpdates; i++) {
|
||||||
|
const numberOfStructs = decoding.readVarUint(decoder)
|
||||||
|
const nextID = readID(decoder)
|
||||||
|
const refs = readStructRefs(decoder, numberOfStructs, nextID)
|
||||||
|
clientRefs.set(nextID.client, refs)
|
||||||
|
}
|
||||||
|
return clientRefs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume computing structs generated by struct readers.
|
||||||
|
*
|
||||||
|
* While there is something to do, we integrate structs in this order
|
||||||
|
* 1. top element on stack, if stack is not empty
|
||||||
|
* 2. next element from current struct reader (if empty, use next struct reader)
|
||||||
|
*
|
||||||
|
* If struct causally depends on another struct (ref.missing), we put next reader of
|
||||||
|
* `ref.id.client` on top of stack.
|
||||||
|
*
|
||||||
|
* At some point we find a struct that has no causal dependencies,
|
||||||
|
* then we start emptying the stack.
|
||||||
|
*
|
||||||
|
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
|
||||||
|
* depends on struct3 (from client1). Therefore the max stack size is eqaul to `structReaders.length`.
|
||||||
|
*
|
||||||
|
* This method is implemented in a way so that we can resume computation if this update
|
||||||
|
* causally depends on another update.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const resumeStructIntegration = (transaction, store) => {
|
||||||
|
const stack = store.pendingStack
|
||||||
|
const clientsStructRefs = store.pendingClientsStructRefs
|
||||||
|
// iterate over all struct readers until we are done
|
||||||
|
while (stack.length !== 0 || clientsStructRefs.size !== 0) {
|
||||||
|
if (stack.length === 0) {
|
||||||
|
// take any first struct from clientsStructRefs and put it on the stack
|
||||||
|
const [client, structRefs] = clientsStructRefs.entries().next().value
|
||||||
|
stack.push(structRefs.refs[structRefs.i++])
|
||||||
|
if (structRefs.refs.length === structRefs.i) {
|
||||||
|
clientsStructRefs.delete(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ref = stack[stack.length - 1]
|
||||||
|
const m = ref._missing
|
||||||
|
const client = ref.id.client
|
||||||
|
const localClock = getState(store, client)
|
||||||
|
const offset = ref.id.clock < localClock ? localClock - ref.id.clock : 0
|
||||||
|
if (ref.id.clock + offset !== localClock) {
|
||||||
|
// A previous message from this client is missing
|
||||||
|
// check if there is a pending structRef with a smaller clock and switch them
|
||||||
|
const structRefs = clientsStructRefs.get(client)
|
||||||
|
if (structRefs !== undefined) {
|
||||||
|
const r = structRefs.refs[structRefs.i]
|
||||||
|
if (r.id.clock < ref.id.clock) {
|
||||||
|
// put ref with smaller clock on stack instead and continue
|
||||||
|
structRefs.refs[structRefs.i] = ref
|
||||||
|
stack[stack.length - 1] = r
|
||||||
|
// sort the set because this approach might bring the list out of order
|
||||||
|
structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.clock - r2.id.clock)
|
||||||
|
structRefs.i = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// wait until missing struct is available
|
||||||
|
return
|
||||||
|
}
|
||||||
|
while (m.length > 0) {
|
||||||
|
const missing = m[m.length - 1]
|
||||||
|
if (getState(store, missing.client) <= missing.clock) {
|
||||||
|
const client = missing.client
|
||||||
|
// get the struct reader that has the missing struct
|
||||||
|
const structRefs = clientsStructRefs.get(client)
|
||||||
|
if (structRefs === undefined) {
|
||||||
|
// This update message causally depends on another update message.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stack.push(structRefs.refs[structRefs.i++])
|
||||||
|
if (structRefs.i === structRefs.refs.length) {
|
||||||
|
clientsStructRefs.delete(client)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ref._missing.pop()
|
||||||
|
}
|
||||||
|
if (m.length === 0) {
|
||||||
|
if (offset < ref.length) {
|
||||||
|
ref.toStruct(transaction, store, offset).integrate(transaction)
|
||||||
|
}
|
||||||
|
stack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const tryResumePendingDeleteReaders = (transaction, store) => {
|
||||||
|
const pendingReaders = store.pendingDeleteReaders
|
||||||
|
store.pendingDeleteReaders = []
|
||||||
|
for (let i = 0; i < pendingReaders.length; i++) {
|
||||||
|
readAndApplyDeleteSet(pendingReaders[i], transaction, store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} store
|
||||||
|
* @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
const mergeReadStructsIntoPendingReads = (store, clientsStructsRefs) => {
|
||||||
|
const pendingClientsStructRefs = store.pendingClientsStructRefs
|
||||||
|
for (const [client, structRefs] of clientsStructsRefs) {
|
||||||
|
const pendingStructRefs = pendingClientsStructRefs.get(client)
|
||||||
|
if (pendingStructRefs === undefined) {
|
||||||
|
pendingClientsStructRefs.set(client, { refs: structRefs, i: 0 })
|
||||||
|
} else {
|
||||||
|
// merge into existing structRefs
|
||||||
|
const merged = pendingStructRefs.i > 0 ? pendingStructRefs.refs.slice(pendingStructRefs.i) : pendingStructRefs.refs
|
||||||
|
for (let i = 0; i < structRefs.length; i++) {
|
||||||
|
merged.push(structRefs[i])
|
||||||
|
}
|
||||||
|
pendingStructRefs.i = 0
|
||||||
|
pendingStructRefs.refs = merged.sort((r1, r2) => r1.id.clock - r2.id.clock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
|
*
|
||||||
|
* This is called when data is received from a remote peer.
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder The decoder object to read data from.
|
||||||
|
* @param {Transaction} transaction
|
||||||
|
* @param {StructStore} store
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readStructs = (decoder, transaction, store) => {
|
||||||
|
const clientsStructRefs = readClientsStructRefs(decoder)
|
||||||
|
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
|
||||||
|
resumeStructIntegration(transaction, store)
|
||||||
|
tryResumePendingDeleteReaders(transaction, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and apply a document update.
|
||||||
|
*
|
||||||
|
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @param {Doc} ydoc
|
||||||
|
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readUpdate = (decoder, ydoc, transactionOrigin) =>
|
||||||
|
transact(ydoc, transaction => {
|
||||||
|
readStructs(decoder, transaction, ydoc.store)
|
||||||
|
readAndApplyDeleteSet(decoder, transaction, ydoc.store)
|
||||||
|
}, transactionOrigin, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
|
||||||
|
*
|
||||||
|
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
|
||||||
|
*
|
||||||
|
* @param {Doc} ydoc
|
||||||
|
* @param {Uint8Array} update
|
||||||
|
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const applyUpdate = (ydoc, update, transactionOrigin) =>
|
||||||
|
readUpdate(decoding.createDecoder(update), ydoc, transactionOrigin)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
|
||||||
|
* only write the operations that are missing.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
|
||||||
|
writeClientsStructs(encoder, doc.store, targetStateVector)
|
||||||
|
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
|
||||||
|
* only write the operations that are missing.
|
||||||
|
*
|
||||||
|
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
|
||||||
|
*
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
|
||||||
|
writeStateAsUpdate(encoder, doc, targetStateVector)
|
||||||
|
return encoding.toUint8Array(encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read state vector from Decoder and return as Map
|
||||||
|
*
|
||||||
|
* @param {decoding.Decoder} decoder
|
||||||
|
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const readStateVector = decoder => {
|
||||||
|
const ss = new Map()
|
||||||
|
const ssLength = decoding.readVarUint(decoder)
|
||||||
|
for (let i = 0; i < ssLength; i++) {
|
||||||
|
const client = decoding.readVarUint(decoder)
|
||||||
|
const clock = decoding.readVarUint(decoder)
|
||||||
|
ss.set(client, clock)
|
||||||
|
}
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read decodedState and return State as Map.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} decodedState
|
||||||
|
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const decodeStateVector = decodedState => readStateVector(decoding.createDecoder(decodedState))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write State Vector to `lib0/encoding.js#Encoder`.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {Map<number,number>} sv
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeStateVector = (encoder, sv) => {
|
||||||
|
encoding.writeVarUint(encoder, sv.size)
|
||||||
|
sv.forEach((clock, client) => {
|
||||||
|
encoding.writeVarUint(encoder, client)
|
||||||
|
encoding.writeVarUint(encoder, clock)
|
||||||
|
})
|
||||||
|
return encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write State Vector to `lib0/encoding.js#Encoder`.
|
||||||
|
*
|
||||||
|
* @param {encoding.Encoder} encoder
|
||||||
|
* @param {Doc} doc
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode State as Uint8Array.
|
||||||
|
*
|
||||||
|
* @param {Doc} doc
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const encodeStateVector = doc => {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
writeDocumentStateVector(encoder, doc)
|
||||||
|
return encoding.toUint8Array(encoder)
|
||||||
|
}
|
||||||
22
src/utils/isParentOf.js
Normal file
22
src/utils/isParentOf.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if `parent` is a parent of `child`.
|
||||||
|
*
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Item|null} child
|
||||||
|
* @return {Boolean} Whether `parent` is a parent of `child`.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const isParentOf = (parent, child) => {
|
||||||
|
while (child !== null) {
|
||||||
|
if (child.parent === parent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
child = child.parent._item
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
241
src/y.js
241
src/y.js
@@ -1,241 +0,0 @@
|
|||||||
import debug from 'debug'
|
|
||||||
import extendConnector from './Connector.js'
|
|
||||||
import extendDatabase from './Database.js'
|
|
||||||
import extendTransaction from './Transaction.js'
|
|
||||||
import extendStruct from './Struct.js'
|
|
||||||
import extendUtils from './Utils.js'
|
|
||||||
|
|
||||||
extendConnector(Y)
|
|
||||||
extendDatabase(Y)
|
|
||||||
extendTransaction(Y)
|
|
||||||
extendStruct(Y)
|
|
||||||
extendUtils(Y)
|
|
||||||
|
|
||||||
Y.debug = debug
|
|
||||||
|
|
||||||
var requiringModules = {}
|
|
||||||
|
|
||||||
Y.requiringModules = requiringModules
|
|
||||||
|
|
||||||
Y.extend = function (name, value) {
|
|
||||||
if (arguments.length === 2 && typeof name === 'string') {
|
|
||||||
if (value instanceof Y.utils.CustomTypeDefinition) {
|
|
||||||
Y[name] = value.parseArguments
|
|
||||||
} else {
|
|
||||||
Y[name] = value
|
|
||||||
}
|
|
||||||
if (requiringModules[name] != null) {
|
|
||||||
requiringModules[name].resolve()
|
|
||||||
delete requiringModules[name]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (var i = 0; i < arguments.length; i++) {
|
|
||||||
var f = arguments[i]
|
|
||||||
if (typeof f === 'function') {
|
|
||||||
f(Y)
|
|
||||||
} else {
|
|
||||||
throw new Error('Expected function!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Y.requestModules = requestModules
|
|
||||||
function requestModules (modules) {
|
|
||||||
var sourceDir
|
|
||||||
if (Y.sourceDir === null) {
|
|
||||||
sourceDir = null
|
|
||||||
} else {
|
|
||||||
sourceDir = Y.sourceDir || '/bower_components'
|
|
||||||
}
|
|
||||||
// determine if this module was compiled for es5 or es6 (y.js vs. y.es6)
|
|
||||||
// if Insert.execute is a Function, then it isnt a generator..
|
|
||||||
// then load the es5(.js) files..
|
|
||||||
var extention = typeof regeneratorRuntime !== 'undefined' ? '.js' : '.es6'
|
|
||||||
var promises = []
|
|
||||||
for (var i = 0; i < modules.length; i++) {
|
|
||||||
var module = modules[i].split('(')[0]
|
|
||||||
var modulename = 'y-' + module.toLowerCase()
|
|
||||||
if (Y[module] == null) {
|
|
||||||
if (requiringModules[module] == null) {
|
|
||||||
// module does not exist
|
|
||||||
if (typeof window !== 'undefined' && window.Y !== 'undefined') {
|
|
||||||
if (sourceDir != null) {
|
|
||||||
var imported = document.createElement('script')
|
|
||||||
imported.src = sourceDir + '/' + modulename + '/' + modulename + extention
|
|
||||||
document.head.appendChild(imported)
|
|
||||||
}
|
|
||||||
let requireModule = {}
|
|
||||||
requiringModules[module] = requireModule
|
|
||||||
requireModule.promise = new Promise(function (resolve) {
|
|
||||||
requireModule.resolve = resolve
|
|
||||||
})
|
|
||||||
promises.push(requireModule.promise)
|
|
||||||
} else {
|
|
||||||
console.info('YJS: Please do not depend on automatic requiring of modules anymore! Extend modules as follows `require(\'y-modulename\')(Y)`')
|
|
||||||
require(modulename)(Y)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
promises.push(requiringModules[modules[i]].promise)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.all(promises)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ::
|
|
||||||
type MemoryOptions = {
|
|
||||||
name: 'memory'
|
|
||||||
}
|
|
||||||
type IndexedDBOptions = {
|
|
||||||
name: 'indexeddb',
|
|
||||||
namespace: string
|
|
||||||
}
|
|
||||||
type DbOptions = MemoryOptions | IndexedDBOptions
|
|
||||||
|
|
||||||
type WebRTCOptions = {
|
|
||||||
name: 'webrtc',
|
|
||||||
room: string
|
|
||||||
}
|
|
||||||
type WebsocketsClientOptions = {
|
|
||||||
name: 'websockets-client',
|
|
||||||
room: string
|
|
||||||
}
|
|
||||||
type ConnectionOptions = WebRTCOptions | WebsocketsClientOptions
|
|
||||||
|
|
||||||
type YOptions = {
|
|
||||||
connector: ConnectionOptions,
|
|
||||||
db: DbOptions,
|
|
||||||
types: Array<TypeName>,
|
|
||||||
sourceDir: string,
|
|
||||||
share: {[key: string]: TypeName}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
|
||||||
if (opts.hasOwnProperty('sourceDir')) {
|
|
||||||
Y.sourceDir = opts.sourceDir
|
|
||||||
}
|
|
||||||
opts.types = opts.types != null ? opts.types : []
|
|
||||||
var modules = [opts.db.name, opts.connector.name].concat(opts.types)
|
|
||||||
for (var name in opts.share) {
|
|
||||||
modules.push(opts.share[name])
|
|
||||||
}
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
if (opts == null) reject(new Error('An options object is expected!'))
|
|
||||||
else if (opts.connector == null) reject(new Error('You must specify a connector! (missing connector property)'))
|
|
||||||
else if (opts.connector.name == null) reject(new Error('You must specify connector name! (missing connector.name property)'))
|
|
||||||
else if (opts.db == null) reject(new Error('You must specify a database! (missing db property)'))
|
|
||||||
else if (opts.connector.name == null) reject(new Error('You must specify db name! (missing db.name property)'))
|
|
||||||
else {
|
|
||||||
opts = Y.utils.copyObject(opts)
|
|
||||||
opts.connector = Y.utils.copyObject(opts.connector)
|
|
||||||
opts.db = Y.utils.copyObject(opts.db)
|
|
||||||
opts.share = Y.utils.copyObject(opts.share)
|
|
||||||
Y.requestModules(modules).then(function () {
|
|
||||||
var yconfig = new YConfig(opts)
|
|
||||||
yconfig.db.whenUserIdSet(function () {
|
|
||||||
yconfig.init(function () {
|
|
||||||
resolve(yconfig)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}).catch(reject)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
class YConfig extends Y.utils.NamedEventHandler {
|
|
||||||
/* ::
|
|
||||||
db: Y.AbstractDatabase;
|
|
||||||
connector: Y.AbstractConnector;
|
|
||||||
share: {[key: string]: any};
|
|
||||||
options: Object;
|
|
||||||
*/
|
|
||||||
constructor (opts, callback) {
|
|
||||||
super()
|
|
||||||
this.options = opts
|
|
||||||
this.db = new Y[opts.db.name](this, opts.db)
|
|
||||||
this.connector = new Y[opts.connector.name](this, opts.connector)
|
|
||||||
this.connected = true
|
|
||||||
}
|
|
||||||
init (callback) {
|
|
||||||
var opts = this.options
|
|
||||||
var share = {}
|
|
||||||
this.share = share
|
|
||||||
this.db.requestTransaction(function * requestTransaction () {
|
|
||||||
// create shared object
|
|
||||||
for (var propertyname in opts.share) {
|
|
||||||
var typeConstructor = opts.share[propertyname].split('(')
|
|
||||||
var typeName = typeConstructor.splice(0, 1)
|
|
||||||
var type = Y[typeName]
|
|
||||||
var typedef = type.typeDefinition
|
|
||||||
var id = ['_', typedef.struct + '_' + typeName + '_' + propertyname + '_' + typeConstructor]
|
|
||||||
var args = []
|
|
||||||
if (typeConstructor.length === 1) {
|
|
||||||
try {
|
|
||||||
args = JSON.parse('[' + typeConstructor[0].split(')')[0] + ']')
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Was not able to parse type definition! (share.' + propertyname + ')')
|
|
||||||
}
|
|
||||||
if (type.typeDefinition.parseArguments == null) {
|
|
||||||
throw new Error(typeName + ' does not expect arguments!')
|
|
||||||
} else {
|
|
||||||
args = typedef.parseArguments(args[0])[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
share[propertyname] = yield * this.store.initType.call(this, id, args)
|
|
||||||
}
|
|
||||||
this.store.whenTransactionsFinished()
|
|
||||||
.then(callback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
isConnected () {
|
|
||||||
return this.connector.isSynced
|
|
||||||
}
|
|
||||||
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 () {
|
|
||||||
var self = this
|
|
||||||
return this.close().then(function () {
|
|
||||||
if (self.db.deleteDB != null) {
|
|
||||||
return self.db.deleteDB()
|
|
||||||
} else {
|
|
||||||
return Promise.resolve()
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
// remove existing event listener
|
|
||||||
super.destroy()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
close () {
|
|
||||||
var self = this
|
|
||||||
this.share = null
|
|
||||||
if (this.connector.destroy != null) {
|
|
||||||
this.connector.destroy()
|
|
||||||
} else {
|
|
||||||
this.connector.disconnect()
|
|
||||||
}
|
|
||||||
return this.db.whenTransactionsFinished().then(function () {
|
|
||||||
self.db.destroyTypes()
|
|
||||||
// make sure to wait for all transactions before destroying the db
|
|
||||||
self.db.requestTransaction(function * () {
|
|
||||||
yield * self.db.destroy()
|
|
||||||
})
|
|
||||||
return self.db.whenTransactionsFinished()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
test.html
Normal file
9
test.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing Yjs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" src="./dist/tests.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
|
|
||||||
import _Y from '../../yjs/src/y.js'
|
|
||||||
|
|
||||||
import yMemory from '../../y-memory/src/y-memory.js'
|
|
||||||
import yArray from '../../y-array/src/y-array.js'
|
|
||||||
import yMap from '../../y-map/src/Map.js'
|
|
||||||
import yTest from './test-connector.js'
|
|
||||||
|
|
||||||
import Chance from 'chance'
|
|
||||||
|
|
||||||
export let Y = _Y
|
|
||||||
|
|
||||||
Y.extend(yMemory, yArray, yMap, yTest)
|
|
||||||
|
|
||||||
export async function garbageCollectUsers (t, users) {
|
|
||||||
await flushAll(t, users)
|
|
||||||
await Promise.all(users.map(u => u.db.emptyGarbageCollector()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. reconnect and flush all
|
|
||||||
* 2. user 0 gc
|
|
||||||
* 3. get type content
|
|
||||||
* 4. disconnect & reconnect all (so gc is propagated)
|
|
||||||
* 5. compare os, ds, ss
|
|
||||||
*/
|
|
||||||
export async function compareUsers (t, users) {
|
|
||||||
await Promise.all(users.map(u => u.reconnect()))
|
|
||||||
if (users[0].connector.testRoom == null) {
|
|
||||||
await wait(100)
|
|
||||||
}
|
|
||||||
await flushAll(t, users)
|
|
||||||
await wait()
|
|
||||||
await flushAll(t, users)
|
|
||||||
|
|
||||||
var userTypeContents = users.map(u => u.share.array._content.map(c => c.val || JSON.stringify(c.type)))
|
|
||||||
|
|
||||||
await users[0].db.garbageCollect()
|
|
||||||
await users[0].db.garbageCollect()
|
|
||||||
|
|
||||||
// disconnect all except user 0
|
|
||||||
await Promise.all(users.slice(1).map(async u =>
|
|
||||||
u.disconnect()
|
|
||||||
))
|
|
||||||
if (users[0].connector.testRoom == null) {
|
|
||||||
await wait(100)
|
|
||||||
}
|
|
||||||
// reconnect all
|
|
||||||
await Promise.all(users.map(u => u.reconnect()))
|
|
||||||
if (users[0].connector.testRoom == null) {
|
|
||||||
await wait(100)
|
|
||||||
}
|
|
||||||
await users[0].connector.testRoom.flushAll(users)
|
|
||||||
await Promise.all(users.map(u =>
|
|
||||||
new Promise(function (resolve) {
|
|
||||||
u.connector.whenSynced(resolve)
|
|
||||||
})
|
|
||||||
))
|
|
||||||
let filterDeletedOps = users.every(u => u.db.gc === false)
|
|
||||||
var data = await Promise.all(users.map(async (u) => {
|
|
||||||
var data = {}
|
|
||||||
u.db.requestTransaction(function * () {
|
|
||||||
var os = yield * this.getOperationsUntransformed()
|
|
||||||
data.os = {}
|
|
||||||
for (let i = 0; i < os.untransformed.length; i++) {
|
|
||||||
let op = os.untransformed[i]
|
|
||||||
op = Y.Struct[op.struct].encode(op)
|
|
||||||
delete op.origin
|
|
||||||
/*
|
|
||||||
If gc = false, it is necessary to filter deleted ops
|
|
||||||
as they might have been split up differently..
|
|
||||||
*/
|
|
||||||
if (filterDeletedOps) {
|
|
||||||
let opIsDeleted = yield * this.isDeleted(op.id)
|
|
||||||
if (!opIsDeleted) {
|
|
||||||
data.os[JSON.stringify(op.id)] = op
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data.os[JSON.stringify(op.id)] = op
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.ds = yield * this.getDeleteSet()
|
|
||||||
data.ss = yield * this.getStateSet()
|
|
||||||
})
|
|
||||||
await u.db.whenTransactionsFinished()
|
|
||||||
return data
|
|
||||||
}))
|
|
||||||
for (var i = 0; i < data.length - 1; i++) {
|
|
||||||
await t.asyncGroup(async () => {
|
|
||||||
t.compare(userTypeContents[i], userTypeContents[i + 1], 'types')
|
|
||||||
t.compare(data[i].os, data[i + 1].os, 'os')
|
|
||||||
t.compare(data[i].ds, data[i + 1].ds, 'ds')
|
|
||||||
t.compare(data[i].ss, data[i + 1].ss, 'ss')
|
|
||||||
}, `Compare user${i} with user${i + 1}`)
|
|
||||||
}
|
|
||||||
await Promise.all(users.map(async (u) => {
|
|
||||||
await u.close()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initArrays (t, opts) {
|
|
||||||
var result = {
|
|
||||||
users: []
|
|
||||||
}
|
|
||||||
var share = Object.assign({ flushHelper: 'Map', array: 'Array' }, opts.share)
|
|
||||||
var chance = opts.chance || new Chance(t.getSeed() * 1000000000)
|
|
||||||
var connector = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, opts.connector)
|
|
||||||
for (let i = 0; i < opts.users; i++) {
|
|
||||||
let dbOpts
|
|
||||||
let connOpts
|
|
||||||
if (i === 0) {
|
|
||||||
// Only one instance can gc!
|
|
||||||
dbOpts = Object.assign({ gc: true }, opts.db)
|
|
||||||
connOpts = Object.assign({ role: 'master' }, connector)
|
|
||||||
} else {
|
|
||||||
dbOpts = Object.assign({ gc: false }, opts.db)
|
|
||||||
connOpts = Object.assign({ role: 'slave' }, connector)
|
|
||||||
}
|
|
||||||
let y = await Y({
|
|
||||||
connector: connOpts,
|
|
||||||
db: dbOpts,
|
|
||||||
share: share
|
|
||||||
})
|
|
||||||
result.users.push(y)
|
|
||||||
for (let name in share) {
|
|
||||||
result[name + i] = y.share[name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.array0.delete(0, result.array0.length)
|
|
||||||
if (result.users[0].connector.testRoom != null) {
|
|
||||||
// flush for sync if test-connector
|
|
||||||
await result.users[0].connector.testRoom.flushAll(result.users)
|
|
||||||
}
|
|
||||||
await Promise.all(result.users.map(u => {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
u.connector.whenSynced(resolve)
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
await flushAll(t, result.users)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function flushAll (t, users) {
|
|
||||||
// users = users.filter(u => u.connector.isSynced)
|
|
||||||
if (users.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await wait(0)
|
|
||||||
if (users[0].connector.testRoom != null) {
|
|
||||||
// use flushAll method specified in Test Connector
|
|
||||||
await users[0].connector.testRoom.flushAll(users)
|
|
||||||
} else {
|
|
||||||
// flush for any connector
|
|
||||||
await Promise.all(users.map(u => { return u.db.whenTransactionsFinished() }))
|
|
||||||
|
|
||||||
var flushCounter = users[0].share.flushHelper.get('0') || 0
|
|
||||||
flushCounter++
|
|
||||||
await Promise.all(users.map(async (u, i) => {
|
|
||||||
// wait for all users to set the flush counter to the same value
|
|
||||||
await new Promise(resolve => {
|
|
||||||
function observer () {
|
|
||||||
var allUsersReceivedUpdate = true
|
|
||||||
for (var i = 0; i < users.length; i++) {
|
|
||||||
if (u.share.flushHelper.get(i + '') !== flushCounter) {
|
|
||||||
allUsersReceivedUpdate = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (allUsersReceivedUpdate) {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u.share.flushHelper.observe(observer)
|
|
||||||
u.share.flushHelper.set(i + '', flushCounter)
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function flushSome (t, users) {
|
|
||||||
if (users[0].connector.testRoom == null) {
|
|
||||||
// if not test-connector, wait for some time for operations to arrive
|
|
||||||
await wait(100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wait (t) {
|
|
||||||
return new Promise(function (resolve) {
|
|
||||||
setTimeout(resolve, t != null ? t : 100)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
/* global Y */
|
|
||||||
import { wait } from './helper.js'
|
|
||||||
|
|
||||||
var rooms = {}
|
|
||||||
|
|
||||||
export class TestRoom {
|
|
||||||
constructor (roomname) {
|
|
||||||
this.room = roomname
|
|
||||||
this.users = {}
|
|
||||||
this.nextUserId = 0
|
|
||||||
}
|
|
||||||
join (connector) {
|
|
||||||
if (connector.userId == null) {
|
|
||||||
connector.setUserId('' + (this.nextUserId++))
|
|
||||||
}
|
|
||||||
Object.keys(this.users).forEach(uid => {
|
|
||||||
let user = this.users[uid]
|
|
||||||
if (user.role === 'master' || connector.role === 'master') {
|
|
||||||
this.users[uid].userJoined(connector.userId, connector.role)
|
|
||||||
connector.userJoined(uid, this.users[uid].role)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.users[connector.userId] = connector
|
|
||||||
}
|
|
||||||
leave (connector) {
|
|
||||||
delete this.users[connector.userId]
|
|
||||||
Object.keys(this.users).forEach(uid => {
|
|
||||||
this.users[uid].userLeft(connector.userId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
send (sender, receiver, m) {
|
|
||||||
m = JSON.parse(JSON.stringify(m))
|
|
||||||
var user = this.users[receiver]
|
|
||||||
if (user != null) {
|
|
||||||
user.receiveMessage(sender, m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
broadcast (sender, m) {
|
|
||||||
Object.keys(this.users).forEach(receiver => {
|
|
||||||
this.send(sender, receiver, m)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async flushAll (users) {
|
|
||||||
let flushing = true
|
|
||||||
let allUserIds = Object.keys(this.users)
|
|
||||||
if (users == null) {
|
|
||||||
users = allUserIds.map(id => this.users[id].y)
|
|
||||||
}
|
|
||||||
while (flushing) {
|
|
||||||
await wait(10)
|
|
||||||
let res = await Promise.all(allUserIds.map(id => this.users[id]._flushAll(users)))
|
|
||||||
flushing = res.some(status => status === 'flushing')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTestRoom (roomname) {
|
|
||||||
if (rooms[roomname] == null) {
|
|
||||||
rooms[roomname] = new TestRoom(roomname)
|
|
||||||
}
|
|
||||||
return rooms[roomname]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function extendTestConnector (Y) {
|
|
||||||
class TestConnector extends Y.AbstractConnector {
|
|
||||||
constructor (y, options) {
|
|
||||||
if (options === undefined) {
|
|
||||||
throw new Error('Options must not be undefined!')
|
|
||||||
}
|
|
||||||
if (options.room == null) {
|
|
||||||
throw new Error('You must define a room name!')
|
|
||||||
}
|
|
||||||
options.forwardAppliedOperations = options.role === 'master'
|
|
||||||
super(y, options)
|
|
||||||
this.options = options
|
|
||||||
this.room = options.room
|
|
||||||
this.chance = options.chance
|
|
||||||
this.testRoom = getTestRoom(this.room)
|
|
||||||
this.testRoom.join(this)
|
|
||||||
}
|
|
||||||
disconnect () {
|
|
||||||
this.testRoom.leave(this)
|
|
||||||
return super.disconnect()
|
|
||||||
}
|
|
||||||
reconnect () {
|
|
||||||
this.testRoom.join(this)
|
|
||||||
return super.reconnect()
|
|
||||||
}
|
|
||||||
send (uid, message) {
|
|
||||||
this.testRoom.send(this.userId, uid, message)
|
|
||||||
}
|
|
||||||
broadcast (message) {
|
|
||||||
this.testRoom.broadcast(this.userId, message)
|
|
||||||
}
|
|
||||||
async whenSynced (f) {
|
|
||||||
var synced = false
|
|
||||||
var periodicFlushTillSync = () => {
|
|
||||||
if (synced) {
|
|
||||||
f()
|
|
||||||
} else {
|
|
||||||
this.testRoom.flushAll([this.y]).then(function () {
|
|
||||||
setTimeout(periodicFlushTillSync, 10)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
periodicFlushTillSync()
|
|
||||||
return super.whenSynced(function () {
|
|
||||||
synced = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
receiveMessage (sender, m) {
|
|
||||||
if (this.userId !== sender && this.connections[sender] != null) {
|
|
||||||
var buffer = this.connections[sender].buffer
|
|
||||||
if (buffer == null) {
|
|
||||||
buffer = this.connections[sender].buffer = []
|
|
||||||
}
|
|
||||||
buffer.push(m)
|
|
||||||
if (this.chance.bool({likelihood: 30})) {
|
|
||||||
// flush 1/2 with 30% chance
|
|
||||||
var flushLength = Math.round(buffer.length / 2)
|
|
||||||
buffer.splice(0, flushLength).forEach(m => {
|
|
||||||
super.receiveMessage(sender, m)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async _flushAll (flushUsers) {
|
|
||||||
if (flushUsers.some(u => u.connector.userId === this.userId)) {
|
|
||||||
// this one needs to sync with every other user
|
|
||||||
flushUsers = Object.keys(this.connections).map(id => this.testRoom.users[id].y)
|
|
||||||
}
|
|
||||||
var finished = []
|
|
||||||
for (let i = 0; i < flushUsers.length; i++) {
|
|
||||||
let userId = flushUsers[i].connector.userId
|
|
||||||
if (userId !== this.userId && this.connections[userId] != null) {
|
|
||||||
let buffer = this.connections[userId].buffer
|
|
||||||
if (buffer != null) {
|
|
||||||
var messages = buffer.splice(0)
|
|
||||||
for (let j = 0; j < messages.length; j++) {
|
|
||||||
let p = super.receiveMessage(userId, messages[j])
|
|
||||||
finished.push(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(finished)
|
|
||||||
await this.y.db.whenTransactionsFinished()
|
|
||||||
return finished.length > 0 ? 'flushing' : 'done'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Y.extend('test', TestConnector)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof Y !== 'undefined') {
|
|
||||||
extendTestConnector(Y)
|
|
||||||
}
|
|
||||||
19
tests/consistency.tests.js
Normal file
19
tests/consistency.tests.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client id should be changed when an instance receives updates from another client using the same client id.
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testClientIdDuplicateChange = tc => {
|
||||||
|
const doc1 = new Y.Doc()
|
||||||
|
doc1.clientID = 0
|
||||||
|
const doc2 = new Y.Doc()
|
||||||
|
doc2.clientID = 0
|
||||||
|
t.assert(doc2.clientID === doc1.clientID)
|
||||||
|
doc1.getArray('a').insert(0, [1, 2])
|
||||||
|
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
|
||||||
|
t.assert(doc2.clientID !== doc1.clientID)
|
||||||
|
}
|
||||||
28
tests/encoding.tests.js
Normal file
28
tests/encoding.tests.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
contentRefs,
|
||||||
|
readContentBinary,
|
||||||
|
readContentDeleted,
|
||||||
|
readContentString,
|
||||||
|
readContentJSON,
|
||||||
|
readContentEmbed,
|
||||||
|
readContentType,
|
||||||
|
readContentFormat,
|
||||||
|
readContentAny
|
||||||
|
} from '../src/internals.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testStructReferences = tc => {
|
||||||
|
t.assert(contentRefs.length === 9)
|
||||||
|
t.assert(contentRefs[1] === readContentDeleted)
|
||||||
|
t.assert(contentRefs[2] === readContentJSON) // TODO: deprecate content json?
|
||||||
|
t.assert(contentRefs[3] === readContentBinary)
|
||||||
|
t.assert(contentRefs[4] === readContentString)
|
||||||
|
t.assert(contentRefs[5] === readContentEmbed)
|
||||||
|
t.assert(contentRefs[6] === readContentFormat)
|
||||||
|
t.assert(contentRefs[7] === readContentType)
|
||||||
|
t.assert(contentRefs[8] === readContentAny)
|
||||||
|
}
|
||||||
24
tests/index.js
Normal file
24
tests/index.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
import * as array from './y-array.tests.js'
|
||||||
|
import * as map from './y-map.tests.js'
|
||||||
|
import * as text from './y-text.tests.js'
|
||||||
|
import * as xml from './y-xml.tests.js'
|
||||||
|
import * as encoding from './encoding.tests.js'
|
||||||
|
import * as undoredo from './undo-redo.tests.js'
|
||||||
|
import * as consistency from './consistency.tests.js'
|
||||||
|
|
||||||
|
import { runTests } from 'lib0/testing.js'
|
||||||
|
import { isBrowser, isNode } from 'lib0/environment.js'
|
||||||
|
import * as log from 'lib0/logging.js'
|
||||||
|
|
||||||
|
if (isBrowser) {
|
||||||
|
log.createVConsole(document.body)
|
||||||
|
}
|
||||||
|
runTests({
|
||||||
|
map, array, text, xml, consistency, encoding, undoredo
|
||||||
|
}).then(success => {
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (isNode) {
|
||||||
|
process.exit(success ? 0 : 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
416
tests/testHelper.js
Normal file
416
tests/testHelper.js
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import * as Y from '../src/index.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDeleteSetFromStructStore,
|
||||||
|
getStateVector,
|
||||||
|
Item,
|
||||||
|
DeleteItem, DeleteSet, StructStore, Doc // eslint-disable-line
|
||||||
|
} from '../src/internals.js'
|
||||||
|
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
import * as prng from 'lib0/prng.js'
|
||||||
|
import * as encoding from 'lib0/encoding.js'
|
||||||
|
import * as decoding from 'lib0/decoding.js'
|
||||||
|
import * as syncProtocol from 'y-protocols/sync.js'
|
||||||
|
import * as object from 'lib0/object.js'
|
||||||
|
export * from '../src/internals.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TestYInstance} y // publish message created by `y` to all other online clients
|
||||||
|
* @param {Uint8Array} m
|
||||||
|
*/
|
||||||
|
const broadcastMessage = (y, m) => {
|
||||||
|
if (y.tc.onlineConns.has(y)) {
|
||||||
|
y.tc.onlineConns.forEach(remoteYInstance => {
|
||||||
|
if (remoteYInstance !== y) {
|
||||||
|
remoteYInstance._receive(m, y)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestYInstance extends Doc {
|
||||||
|
/**
|
||||||
|
* @param {TestConnector} testConnector
|
||||||
|
* @param {number} clientID
|
||||||
|
*/
|
||||||
|
constructor (testConnector, clientID) {
|
||||||
|
super()
|
||||||
|
this.userID = clientID // overwriting clientID
|
||||||
|
/**
|
||||||
|
* @type {TestConnector}
|
||||||
|
*/
|
||||||
|
this.tc = testConnector
|
||||||
|
/**
|
||||||
|
* @type {Map<TestYInstance, Array<Uint8Array>>}
|
||||||
|
*/
|
||||||
|
this.receiving = new Map()
|
||||||
|
testConnector.allConns.add(this)
|
||||||
|
// set up observe on local model
|
||||||
|
this.on('update', /** @param {Uint8Array} update @param {any} origin */ (update, origin) => {
|
||||||
|
if (origin !== testConnector) {
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
syncProtocol.writeUpdate(encoder, update)
|
||||||
|
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from TestConnector.
|
||||||
|
*/
|
||||||
|
disconnect () {
|
||||||
|
this.receiving = new Map()
|
||||||
|
this.tc.onlineConns.delete(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append yourself to the list of known Y instances in testconnector.
|
||||||
|
* Also initiate sync with all clients.
|
||||||
|
*/
|
||||||
|
connect () {
|
||||||
|
if (!this.tc.onlineConns.has(this)) {
|
||||||
|
this.tc.onlineConns.add(this)
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
syncProtocol.writeSyncStep1(encoder, this)
|
||||||
|
// publish SyncStep1
|
||||||
|
broadcastMessage(this, encoding.toUint8Array(encoder))
|
||||||
|
this.tc.onlineConns.forEach(remoteYInstance => {
|
||||||
|
if (remoteYInstance !== this) {
|
||||||
|
// remote instance sends instance to this instance
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
syncProtocol.writeSyncStep1(encoder, remoteYInstance)
|
||||||
|
this._receive(encoding.toUint8Array(encoder), remoteYInstance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive a message from another client. This message is only appended to the list of receiving messages.
|
||||||
|
* TestConnector decides when this client actually reads this message.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} message
|
||||||
|
* @param {TestYInstance} remoteClient
|
||||||
|
*/
|
||||||
|
_receive (message, remoteClient) {
|
||||||
|
let messages = this.receiving.get(remoteClient)
|
||||||
|
if (messages === undefined) {
|
||||||
|
messages = []
|
||||||
|
this.receiving.set(remoteClient, messages)
|
||||||
|
}
|
||||||
|
messages.push(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of TestYInstances.
|
||||||
|
*
|
||||||
|
* The TestYInstances add/remove themselves from the list of connections maiained in this object.
|
||||||
|
* I think it makes sense. Deal with it.
|
||||||
|
*/
|
||||||
|
export class TestConnector {
|
||||||
|
/**
|
||||||
|
* @param {prng.PRNG} gen
|
||||||
|
*/
|
||||||
|
constructor (gen) {
|
||||||
|
/**
|
||||||
|
* @type {Set<TestYInstance>}
|
||||||
|
*/
|
||||||
|
this.allConns = new Set()
|
||||||
|
/**
|
||||||
|
* @type {Set<TestYInstance>}
|
||||||
|
*/
|
||||||
|
this.onlineConns = new Set()
|
||||||
|
/**
|
||||||
|
* @type {prng.PRNG}
|
||||||
|
*/
|
||||||
|
this.prng = gen
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Y instance and add it to the list of connections
|
||||||
|
* @param {number} clientID
|
||||||
|
*/
|
||||||
|
createY (clientID) {
|
||||||
|
return new TestYInstance(this, clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose random connection and flush a random message from a random sender.
|
||||||
|
*
|
||||||
|
* If this function was unable to flush a message, because there are no more messages to flush, it returns false. true otherwise.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
flushRandomMessage () {
|
||||||
|
const gen = this.prng
|
||||||
|
const conns = Array.from(this.onlineConns).filter(conn => conn.receiving.size > 0)
|
||||||
|
if (conns.length > 0) {
|
||||||
|
const receiver = prng.oneOf(gen, conns)
|
||||||
|
const [sender, messages] = prng.oneOf(gen, Array.from(receiver.receiving))
|
||||||
|
const m = messages.shift()
|
||||||
|
if (messages.length === 0) {
|
||||||
|
receiver.receiving.delete(sender)
|
||||||
|
}
|
||||||
|
if (m === undefined) {
|
||||||
|
return this.flushRandomMessage()
|
||||||
|
}
|
||||||
|
const encoder = encoding.createEncoder()
|
||||||
|
// console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
|
||||||
|
// do not publish data created when this function is executed (could be ss2 or update message)
|
||||||
|
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
|
||||||
|
if (encoding.length(encoder) > 0) {
|
||||||
|
// send reply message
|
||||||
|
sender._receive(encoding.toUint8Array(encoder), receiver)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean} True iff this function actually flushed something
|
||||||
|
*/
|
||||||
|
flushAllMessages () {
|
||||||
|
let didSomething = false
|
||||||
|
while (this.flushRandomMessage()) {
|
||||||
|
didSomething = true
|
||||||
|
}
|
||||||
|
return didSomething
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectAll () {
|
||||||
|
this.allConns.forEach(conn => conn.connect())
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectAll () {
|
||||||
|
this.allConns.forEach(conn => conn.disconnect())
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAll () {
|
||||||
|
this.reconnectAll()
|
||||||
|
this.flushAllMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean} Whether it was possible to disconnect a randon connection.
|
||||||
|
*/
|
||||||
|
disconnectRandom () {
|
||||||
|
if (this.onlineConns.size === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
prng.oneOf(this.prng, Array.from(this.onlineConns)).disconnect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean} Whether it was possible to reconnect a random connection.
|
||||||
|
*/
|
||||||
|
reconnectRandom () {
|
||||||
|
/**
|
||||||
|
* @type {Array<TestYInstance>}
|
||||||
|
*/
|
||||||
|
const reconnectable = []
|
||||||
|
this.allConns.forEach(conn => {
|
||||||
|
if (!this.onlineConns.has(conn)) {
|
||||||
|
reconnectable.push(conn)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (reconnectable.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
prng.oneOf(this.prng, reconnectable).connect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
* @param {{users?:number}} conf
|
||||||
|
* @param {InitTestObjectCallback<T>} [initTestObject]
|
||||||
|
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
|
||||||
|
*/
|
||||||
|
export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const result = {
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
const gen = tc.prng
|
||||||
|
const testConnector = new TestConnector(gen)
|
||||||
|
result.testConnector = testConnector
|
||||||
|
for (let i = 0; i < users; i++) {
|
||||||
|
const y = testConnector.createY(i)
|
||||||
|
y.clientID = i
|
||||||
|
result.users.push(y)
|
||||||
|
result['array' + i] = y.get('array', Y.Array)
|
||||||
|
result['map' + i] = y.get('map', Y.Map)
|
||||||
|
result['xml' + i] = y.get('xml', Y.XmlElement)
|
||||||
|
result['text' + i] = y.get('text', Y.Text)
|
||||||
|
}
|
||||||
|
testConnector.syncAll()
|
||||||
|
result.testObjects = result.users.map(initTestObject || (() => null))
|
||||||
|
return /** @type {any} */ (result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. reconnect and flush all
|
||||||
|
* 2. user 0 gc
|
||||||
|
* 3. get type content
|
||||||
|
* 4. disconnect & reconnect all (so gc is propagated)
|
||||||
|
* 5. compare os, ds, ss
|
||||||
|
*
|
||||||
|
* @param {Array<TestYInstance>} users
|
||||||
|
*/
|
||||||
|
export const compare = users => {
|
||||||
|
users.forEach(u => u.connect())
|
||||||
|
while (users[0].tc.flushAllMessages()) {}
|
||||||
|
const userArrayValues = users.map(u => u.getArray('array').toJSON())
|
||||||
|
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
||||||
|
const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
|
||||||
|
const userTextValues = users.map(u => u.getText('text').toDelta())
|
||||||
|
for (const u of users) {
|
||||||
|
t.assert(u.store.pendingDeleteReaders.length === 0)
|
||||||
|
t.assert(u.store.pendingStack.length === 0)
|
||||||
|
t.assert(u.store.pendingClientsStructRefs.size === 0)
|
||||||
|
}
|
||||||
|
// Test Array iterator
|
||||||
|
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
|
||||||
|
// Test Map iterator
|
||||||
|
const ymapkeys = Array.from(users[0].getMap('map').keys())
|
||||||
|
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
|
||||||
|
ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const mapRes = {}
|
||||||
|
for (const [k, v] of users[0].getMap('map')) {
|
||||||
|
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
|
||||||
|
}
|
||||||
|
t.compare(userMapValues[0], mapRes)
|
||||||
|
// Compare all users
|
||||||
|
for (let i = 0; i < users.length - 1; i++) {
|
||||||
|
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
|
||||||
|
t.compare(userArrayValues[i], userArrayValues[i + 1])
|
||||||
|
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||||
|
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||||
|
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
|
||||||
|
t.compare(userTextValues[i], userTextValues[i + 1])
|
||||||
|
t.compare(getStateVector(users[i].store), getStateVector(users[i + 1].store))
|
||||||
|
compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store))
|
||||||
|
compareStructStores(users[i].store, users[i + 1].store)
|
||||||
|
}
|
||||||
|
users.map(u => u.destroy())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Item?} a
|
||||||
|
* @param {Item?} b
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {StructStore} ss1
|
||||||
|
* @param {StructStore} ss2
|
||||||
|
*/
|
||||||
|
export const compareStructStores = (ss1, ss2) => {
|
||||||
|
t.assert(ss1.clients.size === ss2.clients.size)
|
||||||
|
for (const [client, structs1] of ss1.clients) {
|
||||||
|
const structs2 = /** @type {Array<Y.AbstractStruct>} */ (ss2.clients.get(client))
|
||||||
|
t.assert(structs2 !== undefined && structs1.length === structs2.length)
|
||||||
|
for (let i = 0; i < structs1.length; i++) {
|
||||||
|
const s1 = structs1[i]
|
||||||
|
const s2 = structs2[i]
|
||||||
|
// checks for abstract struct
|
||||||
|
if (
|
||||||
|
s1.constructor !== s2.constructor ||
|
||||||
|
!Y.compareIDs(s1.id, s2.id) ||
|
||||||
|
s1.deleted !== s2.deleted ||
|
||||||
|
s1.length !== s2.length
|
||||||
|
) {
|
||||||
|
t.fail('Structs dont match')
|
||||||
|
}
|
||||||
|
if (s1 instanceof Item) {
|
||||||
|
if (
|
||||||
|
!(s2 instanceof Item) ||
|
||||||
|
!((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) ||
|
||||||
|
!compareItemIDs(s1.right, s2.right) ||
|
||||||
|
!Y.compareIDs(s1.origin, s2.origin) ||
|
||||||
|
!Y.compareIDs(s1.rightOrigin, s2.rightOrigin) ||
|
||||||
|
s1.parentSub !== s2.parentSub
|
||||||
|
) {
|
||||||
|
return t.fail('Items dont match')
|
||||||
|
}
|
||||||
|
// make sure that items are connected correctly
|
||||||
|
t.assert(s1.left === null || s1.left.right === s1)
|
||||||
|
t.assert(s1.right === null || s1.right.left === s1)
|
||||||
|
t.assert(s2.left === null || s2.left.right === s2)
|
||||||
|
t.assert(s2.right === null || s2.right.left === s2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DeleteSet} ds1
|
||||||
|
* @param {DeleteSet} ds2
|
||||||
|
*/
|
||||||
|
export const compareDS = (ds1, ds2) => {
|
||||||
|
t.assert(ds1.clients.size === ds2.clients.size)
|
||||||
|
for (const [client, deleteItems1] of ds1.clients) {
|
||||||
|
const deleteItems2 = /** @type {Array<DeleteItem>} */ (ds2.clients.get(client))
|
||||||
|
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
|
||||||
|
for (let i = 0; i < deleteItems1.length; i++) {
|
||||||
|
const di1 = deleteItems1[i]
|
||||||
|
const di2 = deleteItems2[i]
|
||||||
|
if (di1.clock !== di2.clock || di1.len !== di2.len) {
|
||||||
|
t.fail('DeleteSets dont match')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @callback InitTestObjectCallback
|
||||||
|
* @param {TestYInstance} y
|
||||||
|
* @return {T}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
* @param {Array<function(Y.Doc,prng.PRNG,T):void>} mods
|
||||||
|
* @param {number} iterations
|
||||||
|
* @param {InitTestObjectCallback<T>} [initTestObject]
|
||||||
|
*/
|
||||||
|
export const applyRandomTests = (tc, mods, iterations, initTestObject) => {
|
||||||
|
const gen = tc.prng
|
||||||
|
const result = init(tc, { users: 5 }, initTestObject)
|
||||||
|
const { testConnector, users } = result
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
if (prng.int31(gen, 0, 100) <= 2) {
|
||||||
|
// 2% chance to disconnect/reconnect a random user
|
||||||
|
if (prng.bool(gen)) {
|
||||||
|
testConnector.disconnectRandom()
|
||||||
|
} else {
|
||||||
|
testConnector.reconnectRandom()
|
||||||
|
}
|
||||||
|
} else if (prng.int31(gen, 0, 100) <= 1) {
|
||||||
|
// 1% chance to flush all
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
} else if (prng.int31(gen, 0, 100) <= 50) {
|
||||||
|
// 50% chance to flush a random message
|
||||||
|
testConnector.flushRandomMessage()
|
||||||
|
}
|
||||||
|
const user = prng.int31(gen, 0, users.length - 1)
|
||||||
|
const test = prng.oneOf(gen, mods)
|
||||||
|
test(users[user], gen, result.testObjects[user])
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
return result
|
||||||
|
}
|
||||||
248
tests/undo-redo.tests.js
Normal file
248
tests/undo-redo.tests.js
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||||
|
|
||||||
|
import {
|
||||||
|
UndoManager
|
||||||
|
} from '../src/internals.js'
|
||||||
|
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoText = tc => {
|
||||||
|
const { testConnector, text0, text1 } = init(tc, { users: 3 })
|
||||||
|
const undoManager = new UndoManager(text0)
|
||||||
|
|
||||||
|
// items that are added & deleted in the same transaction won't be undo
|
||||||
|
text0.insert(0, 'test')
|
||||||
|
text0.delete(0, 4)
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text0.toString() === '')
|
||||||
|
|
||||||
|
// follow redone items
|
||||||
|
text0.insert(0, 'a')
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
text0.delete(0, 1)
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text0.toString() === 'a')
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text0.toString() === '')
|
||||||
|
|
||||||
|
text0.insert(0, 'abc')
|
||||||
|
text1.insert(0, 'xyz')
|
||||||
|
testConnector.syncAll()
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text0.toString() === 'xyz')
|
||||||
|
undoManager.redo()
|
||||||
|
t.assert(text0.toString() === 'abcxyz')
|
||||||
|
testConnector.syncAll()
|
||||||
|
text1.delete(0, 1)
|
||||||
|
testConnector.syncAll()
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text0.toString() === 'xyz')
|
||||||
|
undoManager.redo()
|
||||||
|
t.assert(text0.toString() === 'bcxyz')
|
||||||
|
// test marks
|
||||||
|
text0.format(1, 3, { bold: true })
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'bcxyz' }])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoMap = tc => {
|
||||||
|
const { testConnector, map0, map1 } = init(tc, { users: 2 })
|
||||||
|
map0.set('a', 0)
|
||||||
|
const undoManager = new UndoManager(map0)
|
||||||
|
map0.set('a', 1)
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(map0.get('a') === 0)
|
||||||
|
undoManager.redo()
|
||||||
|
t.assert(map0.get('a') === 1)
|
||||||
|
// testing sub-types and if it can restore a whole type
|
||||||
|
const subType = new Y.Map()
|
||||||
|
map0.set('a', subType)
|
||||||
|
subType.set('x', 42)
|
||||||
|
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(map0.get('a') === 1)
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(map0.toJSON(), /** @type {any} */ ({ a: { x: 42 } }))
|
||||||
|
testConnector.syncAll()
|
||||||
|
// if content is overwritten by another user, undo operations should be skipped
|
||||||
|
map1.set('a', 44)
|
||||||
|
testConnector.syncAll()
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(map0.get('a') === 44)
|
||||||
|
undoManager.redo()
|
||||||
|
t.assert(map0.get('a') === 44)
|
||||||
|
|
||||||
|
// test setting value multiple times
|
||||||
|
map0.set('b', 'initial')
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
map0.set('b', 'val1')
|
||||||
|
map0.set('b', 'val2')
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(map0.get('b') === 'initial')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoArray = tc => {
|
||||||
|
const { testConnector, array0, array1 } = init(tc, { users: 3 })
|
||||||
|
const undoManager = new UndoManager(array0)
|
||||||
|
array0.insert(0, [1, 2, 3])
|
||||||
|
array1.insert(0, [4, 5, 6])
|
||||||
|
testConnector.syncAll()
|
||||||
|
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(array0.toArray(), [4, 5, 6])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
|
||||||
|
testConnector.syncAll()
|
||||||
|
array1.delete(0, 1) // user1 deletes [1]
|
||||||
|
testConnector.syncAll()
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(array0.toArray(), [4, 5, 6])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(array0.toArray(), [2, 3, 4, 5, 6])
|
||||||
|
array0.delete(0, 5)
|
||||||
|
// test nested structure
|
||||||
|
const ymap = new Y.Map()
|
||||||
|
array0.insert(0, [ymap])
|
||||||
|
t.compare(array0.toJSON(), [{}])
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
ymap.set('a', 1)
|
||||||
|
t.compare(array0.toJSON(), [{ a: 1 }])
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(array0.toJSON(), [{}])
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(array0.toJSON(), [{}])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(array0.toJSON(), [{ a: 1 }])
|
||||||
|
testConnector.syncAll()
|
||||||
|
array1.get(0).set('b', 2)
|
||||||
|
testConnector.syncAll()
|
||||||
|
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(array0.toJSON(), [{ b: 2 }])
|
||||||
|
undoManager.undo()
|
||||||
|
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(array0.toJSON(), [{ b: 2 }])
|
||||||
|
undoManager.redo()
|
||||||
|
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoXml = tc => {
|
||||||
|
const { xml0 } = init(tc, { users: 3 })
|
||||||
|
const undoManager = new UndoManager(xml0)
|
||||||
|
const child = new Y.XmlElement('p')
|
||||||
|
xml0.insert(0, [child])
|
||||||
|
const textchild = new Y.XmlText('content')
|
||||||
|
child.insert(0, [textchild])
|
||||||
|
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
|
||||||
|
// format textchild and revert that change
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
textchild.format(3, 4, { bold: {} })
|
||||||
|
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
|
||||||
|
undoManager.redo()
|
||||||
|
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
|
||||||
|
xml0.delete(0, 1)
|
||||||
|
t.assert(xml0.toString() === '<undefined></undefined>')
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoEvents = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 3 })
|
||||||
|
const undoManager = new UndoManager(text0)
|
||||||
|
let counter = 0
|
||||||
|
let receivedMetadata = -1
|
||||||
|
undoManager.on('stack-item-added', /** @param {any} event */ event => {
|
||||||
|
t.assert(event.type != null)
|
||||||
|
event.stackItem.meta.set('test', counter++)
|
||||||
|
})
|
||||||
|
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
|
||||||
|
t.assert(event.type != null)
|
||||||
|
receivedMetadata = event.stackItem.meta.get('test')
|
||||||
|
})
|
||||||
|
text0.insert(0, 'abc')
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(receivedMetadata === 0)
|
||||||
|
undoManager.redo()
|
||||||
|
t.assert(receivedMetadata === 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testTrackClass = tc => {
|
||||||
|
const { users, text0 } = init(tc, { users: 3 })
|
||||||
|
// only track origins that are numbers
|
||||||
|
const undoManager = new UndoManager(text0, { trackedOrigins: new Set([Number]) })
|
||||||
|
users[0].transact(() => {
|
||||||
|
text0.insert(0, 'abc')
|
||||||
|
}, 42)
|
||||||
|
t.assert(text0.toString() === 'abc')
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text0.toString() === '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testTypeScope = tc => {
|
||||||
|
const { array0 } = init(tc, { users: 3 })
|
||||||
|
// only track origins that are numbers
|
||||||
|
const text0 = new Y.Text()
|
||||||
|
const text1 = new Y.Text()
|
||||||
|
array0.insert(0, [text0, text1])
|
||||||
|
const undoManager = new UndoManager(text0)
|
||||||
|
const undoManagerBoth = new UndoManager([text0, text1])
|
||||||
|
text1.insert(0, 'abc')
|
||||||
|
t.assert(undoManager.undoStack.length === 0)
|
||||||
|
t.assert(undoManagerBoth.undoStack.length === 1)
|
||||||
|
t.assert(text1.toString() === 'abc')
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(text1.toString() === 'abc')
|
||||||
|
undoManagerBoth.undo()
|
||||||
|
t.assert(text1.toString() === '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoDeleteFilter = tc => {
|
||||||
|
/**
|
||||||
|
* @type {Array<Y.Map<any>>}
|
||||||
|
*/
|
||||||
|
const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0)
|
||||||
|
const undoManager = new UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) })
|
||||||
|
const map0 = new Y.Map()
|
||||||
|
map0.set('hi', 1)
|
||||||
|
const map1 = new Y.Map()
|
||||||
|
array0.insert(0, [map0, map1])
|
||||||
|
undoManager.undo()
|
||||||
|
t.assert(array0.length === 1)
|
||||||
|
array0.get(0)
|
||||||
|
t.assert(Array.from(array0.get(0).keys()).length === 1)
|
||||||
|
}
|
||||||
494
tests/y-array.tests.js
Normal file
494
tests/y-array.tests.js
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||||
|
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
import * as prng from 'lib0/prng.js'
|
||||||
|
import * as math from 'lib0/math.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testDeleteInsert = tc => {
|
||||||
|
const { users, array0 } = init(tc, { users: 2 })
|
||||||
|
array0.delete(0, 0)
|
||||||
|
t.describe('Does not throw when deleting zero elements with position 0')
|
||||||
|
t.fails(() => {
|
||||||
|
array0.delete(1, 1)
|
||||||
|
})
|
||||||
|
array0.insert(0, ['A'])
|
||||||
|
array0.delete(1, 0)
|
||||||
|
t.describe('Does not throw when deleting zero elements with valid position 1')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertThreeElementsTryRegetProperty = tc => {
|
||||||
|
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
|
||||||
|
array0.insert(0, [1, true, false])
|
||||||
|
t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testConcurrentInsertWithThreeConflicts = tc => {
|
||||||
|
var { users, array0, array1, array2 } = init(tc, { users: 3 })
|
||||||
|
array0.insert(0, [0])
|
||||||
|
array1.insert(0, [1])
|
||||||
|
array2.insert(0, [2])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testConcurrentInsertDeleteWithThreeConflicts = tc => {
|
||||||
|
const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 })
|
||||||
|
array0.insert(0, ['x', 'y', 'z'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
array0.insert(1, [0])
|
||||||
|
array1.delete(0)
|
||||||
|
array1.delete(1, 1)
|
||||||
|
array2.insert(1, [2])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertionsInLateSync = tc => {
|
||||||
|
const { testConnector, users, array0, array1, array2 } = init(tc, { users: 3 })
|
||||||
|
array0.insert(0, ['x', 'y'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[1].disconnect()
|
||||||
|
users[2].disconnect()
|
||||||
|
array0.insert(1, ['user0'])
|
||||||
|
array1.insert(1, ['user1'])
|
||||||
|
array2.insert(1, ['user2'])
|
||||||
|
users[1].connect()
|
||||||
|
users[2].connect()
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testDisconnectReallyPreventsSendingMessages = tc => {
|
||||||
|
var { testConnector, users, array0, array1 } = init(tc, { users: 3 })
|
||||||
|
array0.insert(0, ['x', 'y'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[1].disconnect()
|
||||||
|
users[2].disconnect()
|
||||||
|
array0.insert(1, ['user0'])
|
||||||
|
array1.insert(1, ['user1'])
|
||||||
|
t.compare(array0.toJSON(), ['x', 'user0', 'y'])
|
||||||
|
t.compare(array1.toJSON(), ['x', 'user1', 'y'])
|
||||||
|
users[1].connect()
|
||||||
|
users[2].connect()
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testDeletionsInLateSync = tc => {
|
||||||
|
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
|
||||||
|
array0.insert(0, ['x', 'y'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[1].disconnect()
|
||||||
|
array1.delete(1, 1)
|
||||||
|
array0.delete(0, 2)
|
||||||
|
users[1].connect()
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertThenMergeDeleteOnSync = tc => {
|
||||||
|
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
|
||||||
|
array0.insert(0, ['x', 'y', 'z'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[0].disconnect()
|
||||||
|
array1.delete(0, 3)
|
||||||
|
users[0].connect()
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertAndDeleteEvents = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>?}
|
||||||
|
*/
|
||||||
|
let event = null
|
||||||
|
array0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
array0.insert(0, [0, 1, 2])
|
||||||
|
t.assert(event !== null)
|
||||||
|
event = null
|
||||||
|
array0.delete(0)
|
||||||
|
t.assert(event !== null)
|
||||||
|
event = null
|
||||||
|
array0.delete(0, 2)
|
||||||
|
t.assert(event !== null)
|
||||||
|
event = null
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testNestedObserverEvents = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<number>}
|
||||||
|
*/
|
||||||
|
const vals = []
|
||||||
|
array0.observe(e => {
|
||||||
|
if (array0.length === 1) {
|
||||||
|
// inserting, will call this observer again
|
||||||
|
// we expect that this observer is called after this event handler finishedn
|
||||||
|
array0.insert(1, [1])
|
||||||
|
vals.push(0)
|
||||||
|
} else {
|
||||||
|
// this should be called the second time an element is inserted (above case)
|
||||||
|
vals.push(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
array0.insert(0, [0])
|
||||||
|
t.compareArrays(vals, [0, 1])
|
||||||
|
t.compareArrays(array0.toArray(), [0, 1])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertAndDeleteEventsForTypes = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>|null}
|
||||||
|
*/
|
||||||
|
let event = null
|
||||||
|
array0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
array0.insert(0, [new Y.Array()])
|
||||||
|
t.assert(event !== null)
|
||||||
|
event = null
|
||||||
|
array0.delete(0)
|
||||||
|
t.assert(event !== null)
|
||||||
|
event = null
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testChangeEvent = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let changes = null
|
||||||
|
array0.observe(e => {
|
||||||
|
changes = e.changes
|
||||||
|
})
|
||||||
|
const newArr = new Y.Array()
|
||||||
|
array0.insert(0, [newArr, 4, 'dtrn'])
|
||||||
|
t.assert(changes !== null && changes.added.size === 2 && changes.deleted.size === 0)
|
||||||
|
t.compare(changes.delta, [{ insert: [newArr, 4, 'dtrn'] }])
|
||||||
|
changes = null
|
||||||
|
array0.delete(0, 2)
|
||||||
|
t.assert(changes !== null && changes.added.size === 0 && changes.deleted.size === 2)
|
||||||
|
t.compare(changes.delta, [{ delete: 2 }])
|
||||||
|
changes = null
|
||||||
|
array0.insert(1, [0.1])
|
||||||
|
t.assert(changes !== null && changes.added.size === 1 && changes.deleted.size === 0)
|
||||||
|
t.compare(changes.delta, [{ retain: 1 }, { insert: [0.1] }])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertAndDeleteEventsForTypes2 = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<Object<string,any>>}
|
||||||
|
*/
|
||||||
|
const events = []
|
||||||
|
array0.observe(e => {
|
||||||
|
events.push(e)
|
||||||
|
})
|
||||||
|
array0.insert(0, ['hi', new Y.Map()])
|
||||||
|
t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements')
|
||||||
|
array0.delete(1)
|
||||||
|
t.assert(events.length === 2, 'Event is triggered exactly once for deletion')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This issue has been reported here https://github.com/yjs/yjs/issues/155
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testNewChildDoesNotEmitEventInTransaction = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
let fired = false
|
||||||
|
users[0].transact(() => {
|
||||||
|
const newMap = new Y.Map()
|
||||||
|
newMap.observe(() => {
|
||||||
|
fired = true
|
||||||
|
})
|
||||||
|
array0.insert(0, [newMap])
|
||||||
|
newMap.set('tst', 42)
|
||||||
|
})
|
||||||
|
t.assert(!fired, 'Event does not trigger')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGarbageCollector = tc => {
|
||||||
|
const { testConnector, users, array0 } = init(tc, { users: 3 })
|
||||||
|
array0.insert(0, ['x', 'y', 'z'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
users[0].disconnect()
|
||||||
|
array0.delete(0, 3)
|
||||||
|
users[0].connect()
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testEventTargetIsSetCorrectlyOnLocal = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 3 })
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let event
|
||||||
|
array0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
array0.insert(0, ['stuff'])
|
||||||
|
t.assert(event.target === array0, '"target" property is set correctly')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testEventTargetIsSetCorrectlyOnRemote = tc => {
|
||||||
|
const { testConnector, array0, array1, users } = init(tc, { users: 3 })
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let event
|
||||||
|
array0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
array1.insert(0, ['stuff'])
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(event.target === array0, '"target" property is set correctly')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testIteratingArrayContainingTypes = tc => {
|
||||||
|
const y = new Y.Doc()
|
||||||
|
const arr = y.getArray('arr')
|
||||||
|
const numItems = 10
|
||||||
|
for (let i = 0; i < numItems; i++) {
|
||||||
|
const map = new Y.Map()
|
||||||
|
map.set('value', i)
|
||||||
|
arr.push([map])
|
||||||
|
}
|
||||||
|
let cnt = 0
|
||||||
|
for (const item of arr) {
|
||||||
|
t.assert(item.get('value') === cnt++, 'value is correct')
|
||||||
|
}
|
||||||
|
y.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
let _uniqueNumber = 0
|
||||||
|
const getUniqueNumber = () => _uniqueNumber++
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<function(Doc,prng.PRNG,any):void>}
|
||||||
|
*/
|
||||||
|
const arrayTransactions = [
|
||||||
|
function insert (user, gen) {
|
||||||
|
const yarray = user.getArray('array')
|
||||||
|
var uniqueNumber = getUniqueNumber()
|
||||||
|
var content = []
|
||||||
|
var len = prng.int31(gen, 1, 4)
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
content.push(uniqueNumber)
|
||||||
|
}
|
||||||
|
var pos = prng.int31(gen, 0, yarray.length)
|
||||||
|
yarray.insert(pos, content)
|
||||||
|
},
|
||||||
|
function insertTypeArray (user, gen) {
|
||||||
|
const yarray = user.getArray('array')
|
||||||
|
var pos = prng.int31(gen, 0, yarray.length)
|
||||||
|
yarray.insert(pos, [new Y.Array()])
|
||||||
|
var array2 = yarray.get(pos)
|
||||||
|
array2.insert(0, [1, 2, 3, 4])
|
||||||
|
},
|
||||||
|
function insertTypeMap (user, gen) {
|
||||||
|
const yarray = user.getArray('array')
|
||||||
|
var pos = prng.int31(gen, 0, yarray.length)
|
||||||
|
yarray.insert(pos, [new Y.Map()])
|
||||||
|
var map = yarray.get(pos)
|
||||||
|
map.set('someprop', 42)
|
||||||
|
map.set('someprop', 43)
|
||||||
|
map.set('someprop', 44)
|
||||||
|
},
|
||||||
|
function _delete (user, gen) {
|
||||||
|
const yarray = user.getArray('array')
|
||||||
|
var length = yarray.length
|
||||||
|
if (length > 0) {
|
||||||
|
var somePos = prng.int31(gen, 0, length - 1)
|
||||||
|
var delLength = prng.int31(gen, 1, math.min(2, length - somePos))
|
||||||
|
if (prng.bool(gen)) {
|
||||||
|
var type = yarray.get(somePos)
|
||||||
|
if (type.length > 0) {
|
||||||
|
somePos = prng.int31(gen, 0, type.length - 1)
|
||||||
|
delLength = prng.int31(gen, 0, math.min(2, type.length - somePos))
|
||||||
|
type.delete(somePos, delLength)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
yarray.delete(somePos, delLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests4 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests40 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests42 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests43 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 43)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests44 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 44)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests45 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 45)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests46 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 46)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests300 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests400 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests500 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests600 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests1000 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests1800 = tc => {
|
||||||
|
applyRandomTests(tc, arrayTransactions, 1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests3000 = tc => {
|
||||||
|
t.skip(!t.production)
|
||||||
|
applyRandomTests(tc, arrayTransactions, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests5000 = tc => {
|
||||||
|
t.skip(!t.production)
|
||||||
|
applyRandomTests(tc, arrayTransactions, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYarrayTests30000 = tc => {
|
||||||
|
t.skip(!t.production)
|
||||||
|
applyRandomTests(tc, arrayTransactions, 30000)
|
||||||
|
}
|
||||||
566
tests/y-map.tests.js
Normal file
566
tests/y-map.tests.js
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||||
|
|
||||||
|
import {
|
||||||
|
compareIDs
|
||||||
|
} from '../src/internals.js'
|
||||||
|
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
import * as prng from 'lib0/prng.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testBasicMapTests = tc => {
|
||||||
|
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
|
||||||
|
users[2].disconnect()
|
||||||
|
|
||||||
|
map0.set('number', 1)
|
||||||
|
map0.set('string', 'hello Y')
|
||||||
|
map0.set('object', { key: { key2: 'value' } })
|
||||||
|
map0.set('y-map', new Y.Map())
|
||||||
|
map0.set('boolean1', true)
|
||||||
|
map0.set('boolean0', false)
|
||||||
|
const map = map0.get('y-map')
|
||||||
|
map.set('y-array', new Y.Array())
|
||||||
|
const array = map.get('y-array')
|
||||||
|
array.insert(0, [0])
|
||||||
|
array.insert(0, [-1])
|
||||||
|
|
||||||
|
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
|
||||||
|
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
|
||||||
|
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
|
||||||
|
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
|
||||||
|
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
||||||
|
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
||||||
|
|
||||||
|
users[2].connect()
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
|
||||||
|
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
|
||||||
|
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
|
||||||
|
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
|
||||||
|
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
|
||||||
|
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
||||||
|
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
||||||
|
|
||||||
|
// compare disconnected user
|
||||||
|
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
|
||||||
|
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
|
||||||
|
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
|
||||||
|
t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)')
|
||||||
|
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
|
||||||
|
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetAndSetOfMapProperty = tc => {
|
||||||
|
const { testConnector, users, map0 } = init(tc, { users: 2 })
|
||||||
|
map0.set('stuff', 'stuffy')
|
||||||
|
map0.set('undefined', undefined)
|
||||||
|
map0.set('null', null)
|
||||||
|
t.compare(map0.get('stuff'), 'stuffy')
|
||||||
|
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
const u = user.getMap('map')
|
||||||
|
t.compare(u.get('stuff'), 'stuffy')
|
||||||
|
t.assert(u.get('undefined') === undefined, 'undefined')
|
||||||
|
t.compare(u.get('null'), null, 'null')
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testYmapSetsYmap = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
const map = map0.set('Map', new Y.Map())
|
||||||
|
t.assert(map0.get('Map') === map)
|
||||||
|
map.set('one', 1)
|
||||||
|
t.compare(map.get('one'), 1)
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testYmapSetsYarray = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
const array = map0.set('Array', new Y.Array())
|
||||||
|
t.assert(array === map0.get('Array'))
|
||||||
|
array.insert(0, [1, 2, 3])
|
||||||
|
// @ts-ignore
|
||||||
|
t.compare(map0.toJSON(), { Array: [1, 2, 3] })
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetAndSetOfMapPropertySyncs = tc => {
|
||||||
|
const { testConnector, users, map0 } = init(tc, { users: 2 })
|
||||||
|
map0.set('stuff', 'stuffy')
|
||||||
|
t.compare(map0.get('stuff'), 'stuffy')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
var u = user.getMap('map')
|
||||||
|
t.compare(u.get('stuff'), 'stuffy')
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
||||||
|
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map1.set('stuff', 'c1')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
var u = user.getMap('map')
|
||||||
|
t.compare(u.get('stuff'), 'c1')
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
||||||
|
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map1.set('stuff', 'c1')
|
||||||
|
map1.delete('stuff')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
var u = user.getMap('map')
|
||||||
|
t.assert(u.get('stuff') === undefined)
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
|
||||||
|
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map1.set('stuff', 'c1')
|
||||||
|
map1.set('stuff', 'c2')
|
||||||
|
map2.set('stuff', 'c3')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
var u = user.getMap('map')
|
||||||
|
t.compare(u.get('stuff'), 'c3')
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
|
||||||
|
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
|
||||||
|
map0.set('stuff', 'c0')
|
||||||
|
map1.set('stuff', 'c1')
|
||||||
|
map1.set('stuff', 'c2')
|
||||||
|
map2.set('stuff', 'c3')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
map0.set('stuff', 'deleteme')
|
||||||
|
map1.set('stuff', 'c1')
|
||||||
|
map2.set('stuff', 'c2')
|
||||||
|
map3.set('stuff', 'c3')
|
||||||
|
map3.delete('stuff')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
for (const user of users) {
|
||||||
|
var u = user.getMap('map')
|
||||||
|
t.assert(u.get('stuff') === undefined)
|
||||||
|
}
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testObserveDeepProperties = tc => {
|
||||||
|
const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 })
|
||||||
|
const _map1 = map1.set('map', new Y.Map())
|
||||||
|
let calls = 0
|
||||||
|
let dmapid
|
||||||
|
map1.observeDeep(events => {
|
||||||
|
events.forEach(event => {
|
||||||
|
calls++
|
||||||
|
// @ts-ignore
|
||||||
|
t.assert(event.keysChanged.has('deepmap'))
|
||||||
|
t.assert(event.path.length === 1)
|
||||||
|
t.assert(event.path[0] === 'map')
|
||||||
|
// @ts-ignore
|
||||||
|
dmapid = event.target.get('deepmap')._item.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
const _map3 = map3.get('map')
|
||||||
|
_map3.set('deepmap', new Y.Map())
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
const _map2 = map2.get('map')
|
||||||
|
_map2.set('deepmap', new Y.Map())
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
const dmap1 = _map1.get('deepmap')
|
||||||
|
const dmap2 = _map2.get('deepmap')
|
||||||
|
const dmap3 = _map3.get('deepmap')
|
||||||
|
t.assert(calls > 0)
|
||||||
|
t.assert(compareIDs(dmap1._item.id, dmap2._item.id))
|
||||||
|
t.assert(compareIDs(dmap1._item.id, dmap3._item.id))
|
||||||
|
// @ts-ignore we want the possibility of dmapid being undefined
|
||||||
|
t.assert(compareIDs(dmap1._item.id, dmapid))
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testObserversUsingObservedeep = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<Array<string|number>>}
|
||||||
|
*/
|
||||||
|
const pathes = []
|
||||||
|
let calls = 0
|
||||||
|
map0.observeDeep(events => {
|
||||||
|
events.forEach(event => {
|
||||||
|
pathes.push(event.path)
|
||||||
|
})
|
||||||
|
calls++
|
||||||
|
})
|
||||||
|
map0.set('map', new Y.Map())
|
||||||
|
map0.get('map').set('array', new Y.Array())
|
||||||
|
map0.get('map').get('array').insert(0, ['content'])
|
||||||
|
t.assert(calls === 3)
|
||||||
|
t.compare(pathes, [[], ['map'], ['map', 'array']])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Test events in Y.Map
|
||||||
|
/**
|
||||||
|
* @param {Object<string,any>} is
|
||||||
|
* @param {Object<string,any>} should
|
||||||
|
*/
|
||||||
|
const compareEvent = (is, should) => {
|
||||||
|
for (var key in should) {
|
||||||
|
t.compare(should[key], is[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
let event = {}
|
||||||
|
map0.observe(e => {
|
||||||
|
event = e // just put it on event, should be thrown synchronously anyway
|
||||||
|
})
|
||||||
|
map0.set('stuff', 4)
|
||||||
|
compareEvent(event, {
|
||||||
|
target: map0,
|
||||||
|
keysChanged: new Set(['stuff'])
|
||||||
|
})
|
||||||
|
// update, oldValue is in contents
|
||||||
|
map0.set('stuff', new Y.Array())
|
||||||
|
compareEvent(event, {
|
||||||
|
target: map0,
|
||||||
|
keysChanged: new Set(['stuff'])
|
||||||
|
})
|
||||||
|
// update, oldValue is in opContents
|
||||||
|
map0.set('stuff', 5)
|
||||||
|
// delete
|
||||||
|
map0.delete('stuff')
|
||||||
|
compareEvent(event, {
|
||||||
|
keysChanged: new Set(['stuff']),
|
||||||
|
target: map0
|
||||||
|
})
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testChangeEvent = tc => {
|
||||||
|
const { map0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let changes = null
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let keyChange = null
|
||||||
|
map0.observe(e => {
|
||||||
|
changes = e.changes
|
||||||
|
})
|
||||||
|
map0.set('a', 1)
|
||||||
|
keyChange = changes.keys.get('a')
|
||||||
|
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
|
||||||
|
map0.set('a', 2)
|
||||||
|
keyChange = changes.keys.get('a')
|
||||||
|
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 1)
|
||||||
|
users[0].transact(() => {
|
||||||
|
map0.set('a', 3)
|
||||||
|
map0.set('a', 4)
|
||||||
|
})
|
||||||
|
keyChange = changes.keys.get('a')
|
||||||
|
t.assert(changes !== null && keyChange.action === 'update' && keyChange.oldValue === 2)
|
||||||
|
users[0].transact(() => {
|
||||||
|
map0.set('b', 1)
|
||||||
|
map0.set('b', 2)
|
||||||
|
})
|
||||||
|
keyChange = changes.keys.get('b')
|
||||||
|
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
|
||||||
|
users[0].transact(() => {
|
||||||
|
map0.set('c', 1)
|
||||||
|
map0.delete('c')
|
||||||
|
})
|
||||||
|
t.assert(changes !== null && changes.keys.size === 0)
|
||||||
|
users[0].transact(() => {
|
||||||
|
map0.set('d', 1)
|
||||||
|
map0.set('d', 2)
|
||||||
|
})
|
||||||
|
keyChange = changes.keys.get('d')
|
||||||
|
t.assert(changes !== null && keyChange.action === 'add' && keyChange.oldValue === undefined)
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testYmapEventExceptionsShouldCompleteTransaction = tc => {
|
||||||
|
const doc = new Y.Doc()
|
||||||
|
const map = doc.getMap('map')
|
||||||
|
|
||||||
|
let updateCalled = false
|
||||||
|
let throwingObserverCalled = false
|
||||||
|
let throwingDeepObserverCalled = false
|
||||||
|
doc.on('update', () => {
|
||||||
|
updateCalled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const throwingObserver = () => {
|
||||||
|
throwingObserverCalled = true
|
||||||
|
throw new Error('Failure')
|
||||||
|
}
|
||||||
|
|
||||||
|
const throwingDeepObserver = () => {
|
||||||
|
throwingDeepObserverCalled = true
|
||||||
|
throw new Error('Failure')
|
||||||
|
}
|
||||||
|
|
||||||
|
map.observe(throwingObserver)
|
||||||
|
map.observeDeep(throwingDeepObserver)
|
||||||
|
|
||||||
|
t.fails(() => {
|
||||||
|
map.set('y', '2')
|
||||||
|
})
|
||||||
|
|
||||||
|
t.assert(updateCalled)
|
||||||
|
t.assert(throwingObserverCalled)
|
||||||
|
t.assert(throwingDeepObserverCalled)
|
||||||
|
|
||||||
|
// check if it works again
|
||||||
|
updateCalled = false
|
||||||
|
throwingObserverCalled = false
|
||||||
|
throwingDeepObserverCalled = false
|
||||||
|
t.fails(() => {
|
||||||
|
map.set('z', '3')
|
||||||
|
})
|
||||||
|
|
||||||
|
t.assert(updateCalled)
|
||||||
|
t.assert(throwingObserverCalled)
|
||||||
|
t.assert(throwingDeepObserverCalled)
|
||||||
|
|
||||||
|
t.assert(map.get('z') === '3')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => {
|
||||||
|
const { users, map0 } = init(tc, { users: 3 })
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
let event = {}
|
||||||
|
map0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
map0.set('stuff', 2)
|
||||||
|
t.compare(event.value, event.target.get(event.name))
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc => {
|
||||||
|
const { users, map0, map1, testConnector } = init(tc, { users: 3 })
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
let event = {}
|
||||||
|
map0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
map1.set('stuff', 2)
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.compare(event.value, event.target.get(event.name))
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<function(Doc,prng.PRNG):void>}
|
||||||
|
*/
|
||||||
|
const mapTransactions = [
|
||||||
|
function set (user, gen) {
|
||||||
|
const key = prng.oneOf(gen, ['one', 'two'])
|
||||||
|
var value = prng.utf16String(gen)
|
||||||
|
user.getMap('map').set(key, value)
|
||||||
|
},
|
||||||
|
function setType (user, gen) {
|
||||||
|
const key = prng.oneOf(gen, ['one', 'two'])
|
||||||
|
var type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
|
||||||
|
user.getMap('map').set(key, type)
|
||||||
|
if (type instanceof Y.Array) {
|
||||||
|
type.insert(0, [1, 2, 3, 4])
|
||||||
|
} else {
|
||||||
|
type.set('deepkey', 'deepvalue')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function _delete (user, gen) {
|
||||||
|
const key = prng.oneOf(gen, ['one', 'two'])
|
||||||
|
user.getMap('map').delete(key)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests10 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests40 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests42 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests43 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 43)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests44 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 44)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests45 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 45)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests46 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 46)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests300 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests400 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests500 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests600 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 600)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests1000 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests1800 = tc => {
|
||||||
|
applyRandomTests(tc, mapTransactions, 1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests5000 = tc => {
|
||||||
|
t.skip(!t.production)
|
||||||
|
applyRandomTests(tc, mapTransactions, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests10000 = tc => {
|
||||||
|
t.skip(!t.production)
|
||||||
|
applyRandomTests(tc, mapTransactions, 10000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testRepeatGeneratingYmapTests100000 = tc => {
|
||||||
|
t.skip(!t.production)
|
||||||
|
applyRandomTests(tc, mapTransactions, 100000)
|
||||||
|
}
|
||||||
182
tests/y-text.tests.js
Normal file
182
tests/y-text.tests.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import * as Y from './testHelper.js'
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
const { init, compare } = Y
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testBasicInsertAndDelete = tc => {
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
let delta
|
||||||
|
|
||||||
|
text0.observe(event => {
|
||||||
|
delta = event.delta
|
||||||
|
})
|
||||||
|
|
||||||
|
text0.delete(0, 0)
|
||||||
|
t.assert(true, 'Does not throw when deleting zero elements with position 0')
|
||||||
|
|
||||||
|
text0.insert(0, 'abc')
|
||||||
|
t.assert(text0.toString() === 'abc', 'Basic insert works')
|
||||||
|
t.compare(delta, [{ insert: 'abc' }])
|
||||||
|
|
||||||
|
text0.delete(0, 1)
|
||||||
|
t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)')
|
||||||
|
t.compare(delta, [{ delete: 1 }])
|
||||||
|
|
||||||
|
text0.delete(1, 1)
|
||||||
|
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
|
||||||
|
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
|
||||||
|
|
||||||
|
users[0].transact(() => {
|
||||||
|
text0.insert(0, '1')
|
||||||
|
text0.delete(0, 1)
|
||||||
|
})
|
||||||
|
t.compare(delta, [])
|
||||||
|
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testBasicFormat = tc => {
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
let delta
|
||||||
|
text0.observe(event => {
|
||||||
|
delta = event.delta
|
||||||
|
})
|
||||||
|
text0.insert(0, 'abc', { bold: true })
|
||||||
|
t.assert(text0.toString() === 'abc', 'Basic insert with attributes works')
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }])
|
||||||
|
t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }])
|
||||||
|
text0.delete(0, 1)
|
||||||
|
t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)')
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }])
|
||||||
|
t.compare(delta, [{ delete: 1 }])
|
||||||
|
text0.delete(1, 1)
|
||||||
|
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }])
|
||||||
|
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
|
||||||
|
text0.insert(0, 'z', { bold: true })
|
||||||
|
t.assert(text0.toString() === 'zb')
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
|
||||||
|
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
|
||||||
|
// @ts-ignore
|
||||||
|
t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker')
|
||||||
|
text0.insert(0, 'y')
|
||||||
|
t.assert(text0.toString() === 'yzb')
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
|
||||||
|
t.compare(delta, [{ insert: 'y' }])
|
||||||
|
text0.format(0, 2, { bold: null })
|
||||||
|
t.assert(text0.toString() === 'yzb')
|
||||||
|
t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }])
|
||||||
|
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testGetDeltaWithEmbeds = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 1 })
|
||||||
|
text0.applyDelta([{
|
||||||
|
insert: { linebreak: 's' }
|
||||||
|
}])
|
||||||
|
t.compare(text0.toDelta(), [{
|
||||||
|
insert: { linebreak: 's' }
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSnapshot = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 1 })
|
||||||
|
const doc0 = /** @type {Y.Doc} */ (text0.doc)
|
||||||
|
doc0.gc = false
|
||||||
|
text0.applyDelta([{
|
||||||
|
insert: 'abcd'
|
||||||
|
}])
|
||||||
|
const snapshot1 = Y.snapshot(doc0)
|
||||||
|
text0.applyDelta([{
|
||||||
|
retain: 1
|
||||||
|
}, {
|
||||||
|
insert: 'x'
|
||||||
|
}, {
|
||||||
|
delete: 1
|
||||||
|
}])
|
||||||
|
const snapshot2 = Y.snapshot(doc0)
|
||||||
|
text0.applyDelta([{
|
||||||
|
retain: 2
|
||||||
|
}, {
|
||||||
|
delete: 3
|
||||||
|
}, {
|
||||||
|
insert: 'x'
|
||||||
|
}, {
|
||||||
|
delete: 1
|
||||||
|
}])
|
||||||
|
const state1 = text0.toDelta(snapshot1)
|
||||||
|
t.compare(state1, [{ insert: 'abcd' }])
|
||||||
|
const state2 = text0.toDelta(snapshot2)
|
||||||
|
t.compare(state2, [{ insert: 'axcd' }])
|
||||||
|
const state2Diff = text0.toDelta(snapshot2, snapshot1)
|
||||||
|
// @ts-ignore Remove userid info
|
||||||
|
state2Diff.forEach(v => {
|
||||||
|
if (v.attributes && v.attributes.ychange) {
|
||||||
|
delete v.attributes.ychange.user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSnapshotDeleteAfter = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 1 })
|
||||||
|
const doc0 = /** @type {Y.Doc} */ (text0.doc)
|
||||||
|
doc0.gc = false
|
||||||
|
text0.applyDelta([{
|
||||||
|
insert: 'abcd'
|
||||||
|
}])
|
||||||
|
const snapshot1 = Y.snapshot(doc0)
|
||||||
|
text0.applyDelta([{
|
||||||
|
retain: 4
|
||||||
|
}, {
|
||||||
|
insert: 'e'
|
||||||
|
}])
|
||||||
|
const state1 = text0.toDelta(snapshot1)
|
||||||
|
t.compare(state1, [{ insert: 'abcd' }])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testToJson = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 1 })
|
||||||
|
text0.insert(0, 'abc', { bold: true })
|
||||||
|
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testToDeltaEmbedAttributes = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 1 })
|
||||||
|
text0.insert(0, 'ab', { bold: true })
|
||||||
|
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
|
||||||
|
const delta0 = text0.toDelta()
|
||||||
|
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testToDeltaEmbedNoAttributes = tc => {
|
||||||
|
const { text0 } = init(tc, { users: 1 })
|
||||||
|
text0.insert(0, 'ab', { bold: true })
|
||||||
|
text0.insertEmbed(1, { image: 'imageSrc.png' })
|
||||||
|
const delta0 = text0.toDelta()
|
||||||
|
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present')
|
||||||
|
}
|
||||||
75
tests/y-xml.tests.js
Normal file
75
tests/y-xml.tests.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { init, compare } from './testHelper.js'
|
||||||
|
import * as Y from '../src/index.js'
|
||||||
|
|
||||||
|
import * as t from 'lib0/testing.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSetProperty = tc => {
|
||||||
|
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
|
||||||
|
xml0.setAttribute('height', '10')
|
||||||
|
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testEvents = tc => {
|
||||||
|
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let event
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
let remoteEvent
|
||||||
|
xml0.observe(e => {
|
||||||
|
event = e
|
||||||
|
})
|
||||||
|
xml1.observe(e => {
|
||||||
|
remoteEvent = e
|
||||||
|
})
|
||||||
|
xml0.setAttribute('key', 'value')
|
||||||
|
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)')
|
||||||
|
// check attributeRemoved
|
||||||
|
xml0.removeAttribute('key')
|
||||||
|
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)')
|
||||||
|
xml0.insert(0, [new Y.XmlText('some text')])
|
||||||
|
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)')
|
||||||
|
// test childRemoved
|
||||||
|
xml0.delete(0)
|
||||||
|
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testTreewalker = tc => {
|
||||||
|
const { users, xml0 } = init(tc, { users: 3 })
|
||||||
|
const paragraph1 = new Y.XmlElement('p')
|
||||||
|
const paragraph2 = new Y.XmlElement('p')
|
||||||
|
const text1 = new Y.XmlText('init')
|
||||||
|
const text2 = new Y.XmlText('text')
|
||||||
|
paragraph1.insert(0, [text1, text2])
|
||||||
|
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
|
||||||
|
const allParagraphs = xml0.querySelectorAll('p')
|
||||||
|
t.assert(allParagraphs.length === 2, 'found exactly two paragraphs')
|
||||||
|
t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1')
|
||||||
|
t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2')
|
||||||
|
t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
64
tsconfig.json
Normal file
64
tsconfig.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Basic Options */
|
||||||
|
"target": "es2018",
|
||||||
|
"lib": ["es2018", "dom"], /* Specify library files to be included in the compilation. */
|
||||||
|
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
"checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./dist/yjs.js", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
"paths": {
|
||||||
|
"yjs": ["./src/index.js"]
|
||||||
|
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
// "maxNodeModuleJsDepth": 0,
|
||||||
|
// "types": ["./src/utils/typedefs.js"]
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*.js", "./tests/**/*.js"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user