Compare commits
725 Commits
dev
...
1_0_0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f176521e0e | ||
|
|
cd35858e01 | ||
|
|
371d6b161a | ||
|
|
2977287438 | ||
|
|
f8f11f358a | ||
|
|
d784799969 | ||
|
|
8d82a3a00c | ||
|
|
eaaf5fcdd9 | ||
|
|
defef55a6e | ||
|
|
dcbf26f874 | ||
|
|
f24c543dd3 | ||
|
|
59aa8ad9a9 | ||
|
|
a69c7b2b6c | ||
|
|
f7577db1d2 | ||
|
|
6b634cd9f2 | ||
|
|
4ed49c3c36 | ||
|
|
87152244d7 | ||
|
|
aad0e56f09 | ||
|
|
c669f64933 | ||
|
|
00f49a13ab | ||
|
|
f27aa5be7b | ||
|
|
2b8b3aa5fd | ||
|
|
65547515a1 | ||
|
|
fc74638642 | ||
|
|
1ef8c88139 | ||
|
|
e005f9eb13 | ||
|
|
8bf1ae81d7 | ||
|
|
04b5734089 | ||
|
|
b44a7fa50f | ||
|
|
f195b9be0c | ||
|
|
462b769e34 | ||
|
|
3eda3316c5 | ||
|
|
ed7471fba5 | ||
|
|
104eeadb4b | ||
|
|
2cf535332b | ||
|
|
86492ca2cc | ||
|
|
499f9e93e5 | ||
|
|
44784d4865 | ||
|
|
c49536acc9 | ||
|
|
ca90e561ab | ||
|
|
6484a43e2f | ||
|
|
893c08ad2a | ||
|
|
40a4987b0b | ||
|
|
7faf51a6c4 | ||
|
|
4cbe5679e1 | ||
|
|
80d1416a81 | ||
|
|
52e7bd25da | ||
|
|
f70779c352 | ||
|
|
71f1b63b9a | ||
|
|
0a31d30c6c | ||
|
|
eb486ed7a4 | ||
|
|
e79686d275 | ||
|
|
0c3c5db6c1 | ||
|
|
3cb043abd0 | ||
|
|
3f6907d77d | ||
|
|
fa336574e6 | ||
|
|
429427a556 | ||
|
|
f0ec046cf4 | ||
|
|
d28282edad | ||
|
|
ed3f81e9f9 | ||
|
|
52138bf7a5 | ||
|
|
ff01ca551a | ||
|
|
81d27a932f | ||
|
|
3e22c2eda8 | ||
|
|
6cc06bf20f | ||
|
|
d0be2968c2 | ||
|
|
2e999b9426 | ||
|
|
7b7801b029 | ||
|
|
f80137e364 | ||
|
|
97964fddf1 | ||
|
|
1b4a0d76dd | ||
|
|
f6b03dc811 | ||
|
|
6e00b8195a | ||
|
|
326bf77183 | ||
|
|
298738fc67 | ||
|
|
d5754f1f54 | ||
|
|
14da32cbab | ||
|
|
a721d3b602 | ||
|
|
6475d32769 | ||
|
|
e114f66fb2 | ||
|
|
e31e761c5a | ||
|
|
977d50566d | ||
|
|
85fc62d61c | ||
|
|
8e0d5581f7 | ||
|
|
32e992db93 | ||
|
|
3103dc4f52 | ||
|
|
943b2a3bd4 | ||
|
|
69f4879589 | ||
|
|
918d3b5547 | ||
|
|
869a893c19 | ||
|
|
ba7ea27d0c | ||
|
|
9f9758bc12 | ||
|
|
f54d162ef9 | ||
|
|
9d70ac8064 | ||
|
|
f44dc76087 | ||
|
|
962589a40f | ||
|
|
d8bbab6bed | ||
|
|
b475ea64e2 | ||
|
|
908acc4a84 | ||
|
|
31e9186930 | ||
|
|
4cbccf85b4 | ||
|
|
45657ba39c | ||
|
|
056d89aa4d | ||
|
|
804103693f | ||
|
|
eb3abc71b4 | ||
|
|
77eacade10 | ||
|
|
d404f2ae54 | ||
|
|
a7c488f377 | ||
|
|
0f75cd92c0 | ||
|
|
a41c829c10 | ||
|
|
ff12778301 | ||
|
|
e38084b901 | ||
|
|
9265a74578 | ||
|
|
f28d886603 | ||
|
|
9c375bd6fd | ||
|
|
42a333a571 | ||
|
|
a6a5f7af53 | ||
|
|
4df87cc96a | ||
|
|
d16e66c67a | ||
|
|
57e26f1079 | ||
|
|
dbb8d2be22 | ||
|
|
a60932d9e4 | ||
|
|
fd3e8416b7 | ||
|
|
b9a27ffef9 | ||
|
|
af72cac4e3 | ||
|
|
7d26f0ae35 | ||
|
|
008a5ace06 | ||
|
|
b1955703b9 | ||
|
|
39065e15e3 | ||
|
|
7b480d4c1d | ||
|
|
33cd6a3b51 | ||
|
|
870ac93cac | ||
|
|
2fcdbbc4da | ||
|
|
18e8b239bf | ||
|
|
ef655a2976 | ||
|
|
78eb6ec3bc | ||
|
|
c0780d19ba | ||
|
|
5b61ea5009 | ||
|
|
6a9918677f | ||
|
|
2d42b92a89 | ||
|
|
a2b794c520 | ||
|
|
6f9446f447 | ||
|
|
3f9cd67b1c | ||
|
|
dbcbf2c5ca | ||
|
|
eed0fca8d3 | ||
|
|
05e031b90b | ||
|
|
bec767c13f | ||
|
|
8046a777af | ||
|
|
0c58b0c513 | ||
|
|
b66e227a02 | ||
|
|
af6df84b5d | ||
|
|
06bba49aad | ||
|
|
f97888572b | ||
|
|
9d9b512f3f | ||
|
|
54e6a761e5 | ||
|
|
d37ed51660 | ||
|
|
9fbdee47a6 | ||
|
|
b568c4b003 | ||
|
|
505e3c5e8c | ||
|
|
75cb9ece58 | ||
|
|
05877834dc | ||
|
|
dae565129a | ||
|
|
701520fe15 | ||
|
|
e662e99641 | ||
|
|
96396f77f5 | ||
|
|
e916683b8d | ||
|
|
7028a0857e | ||
|
|
150f0700b0 | ||
|
|
36590ee762 | ||
|
|
4a33236c8f | ||
|
|
be23fa51a1 | ||
|
|
7e59010824 | ||
|
|
8e24739b96 | ||
|
|
b2ea810dde | ||
|
|
bf8da52348 | ||
|
|
31d01f039c | ||
|
|
2d5b5da904 | ||
|
|
6d4ae6dccb | ||
|
|
37ccbf249c | ||
|
|
6e14f0210a | ||
|
|
c626fb1783 | ||
|
|
23af915604 | ||
|
|
2257ab7647 | ||
|
|
08bfec204f | ||
|
|
916e24e21e | ||
|
|
3ee2c401d4 | ||
|
|
78d9d66438 | ||
|
|
7298aca2e9 | ||
|
|
431a845f18 | ||
|
|
877d1b09f4 | ||
|
|
34caf19e20 | ||
|
|
9004c194b3 | ||
|
|
c57b8a5f4e | ||
|
|
f90a83ef1e | ||
|
|
62f44e38c0 | ||
|
|
c6c4044f3c | ||
|
|
2bd7832d71 | ||
|
|
72033472f3 | ||
|
|
93dc03e86f | ||
|
|
dae8443fe8 | ||
|
|
88a8bcbd5a | ||
|
|
4a5283543d | ||
|
|
7895336189 | ||
|
|
2d5cdccf7d | ||
|
|
28cdf40fcd | ||
|
|
497fd04081 | ||
|
|
38142aaf1c | ||
|
|
ae9c46f8f0 | ||
|
|
dcd10cd690 | ||
|
|
5a35b71339 | ||
|
|
18a59219f7 | ||
|
|
c9095738e6 | ||
|
|
bed415fc6b | ||
|
|
4eeb93b9e4 | ||
|
|
0fc233686b | ||
|
|
def1d297f5 | ||
|
|
e7cf2a6fba | ||
|
|
2da2fd7671 | ||
|
|
d5175969e7 | ||
|
|
46f9630999 | ||
|
|
75adcf0ff0 | ||
|
|
866dd49ba3 | ||
|
|
d0a84d07aa | ||
|
|
089a4c0f8b | ||
|
|
1a1f816872 | ||
|
|
9d86c2828b | ||
|
|
3b6d3beaa7 | ||
|
|
bb75b6ead8 | ||
|
|
fccb2304f6 | ||
|
|
f00ef5eaae | ||
|
|
4e7ee760de | ||
|
|
c4faade30c | ||
|
|
295dc1392a | ||
|
|
445a8c747c | ||
|
|
292a6b9e1e | ||
|
|
0e04e7d31d | ||
|
|
4cf4acf7e9 | ||
|
|
0856693aac | ||
|
|
39d78a04ac | ||
|
|
0b8d73ccd8 | ||
|
|
5921a71227 | ||
|
|
a40929c9e4 | ||
|
|
2d8a285201 | ||
|
|
d9903bfe9e | ||
|
|
a6e7dddf7f | ||
|
|
9a51c78011 | ||
|
|
7b9c0b011f | ||
|
|
26a050e6f6 | ||
|
|
34162f747c | ||
|
|
9e87ee4da1 | ||
|
|
ba5d4c434b | ||
|
|
8f44f4c61d | ||
|
|
23d8a1ecc3 | ||
|
|
a46abcad52 | ||
|
|
347e1fcee5 | ||
|
|
d85d8bc5e4 | ||
|
|
c493fb6f66 | ||
|
|
cf173caf88 | ||
|
|
a41c7b2f70 | ||
|
|
3acaa7f269 | ||
|
|
85787eddb3 | ||
|
|
8cbb233df7 | ||
|
|
5922186a0e | ||
|
|
60f21eb9d2 | ||
|
|
6b7816d334 | ||
|
|
369e62ee8d | ||
|
|
37593bbee6 | ||
|
|
baa51a66a7 | ||
|
|
be979a3bf1 | ||
|
|
c78e583399 | ||
|
|
31050c9cb8 | ||
|
|
1b44ab9f2b | ||
|
|
3a78c70539 | ||
|
|
0f08ad435f | ||
|
|
96abd930b3 | ||
|
|
4581db45c4 | ||
|
|
05cab8ec66 | ||
|
|
b005625ef3 | ||
|
|
e5408b4dd7 | ||
|
|
95a9b1b41c | ||
|
|
2aa8bc1165 | ||
|
|
f8998d5441 | ||
|
|
26f5f54f24 | ||
|
|
99f3aca024 | ||
|
|
194397dd94 | ||
|
|
afa79d78dc | ||
|
|
9a9440a768 | ||
|
|
e375860af8 | ||
|
|
b57f34c15b | ||
|
|
b82fd9f04b | ||
|
|
bb77426b7b | ||
|
|
1bef5fbef3 | ||
|
|
7d2ba1b0b9 | ||
|
|
ac6b9a4dad | ||
|
|
225e2c5096 | ||
|
|
674d68b7d9 | ||
|
|
0e6dd300ef | ||
|
|
8171db353a | ||
|
|
6ab1556880 | ||
|
|
cd2bff5fc7 | ||
|
|
5a280c7919 | ||
|
|
c6697327d5 | ||
|
|
7c203a522a | ||
|
|
c80c294d93 | ||
|
|
3e65ae932d | ||
|
|
f710f03380 | ||
|
|
b887269fc1 | ||
|
|
56a234e6d7 | ||
|
|
39014b2294 | ||
|
|
24e6c110a3 | ||
|
|
f24d110552 | ||
|
|
7189c1c05a | ||
|
|
547afe9a86 | ||
|
|
7b5876037d | ||
|
|
b714c00644 | ||
|
|
fff6df535f | ||
|
|
e14e771bfb | ||
|
|
1130bbcee8 | ||
|
|
8841ef4619 | ||
|
|
45fe0b3682 | ||
|
|
8d3825abc9 | ||
|
|
c5f9841b85 | ||
|
|
d36d176d99 | ||
|
|
38b2cf73ed | ||
|
|
40b15a6950 | ||
|
|
e312bb7640 | ||
|
|
852c0e6c13 | ||
|
|
7c1bcf9e9b | ||
|
|
2b79bed085 | ||
|
|
6b1e0c4aa8 | ||
|
|
3c3645eba4 | ||
|
|
009cc3c87a | ||
|
|
fc5fd4be94 | ||
|
|
8ed1acf79d | ||
|
|
c518223d21 | ||
|
|
caaff618e9 | ||
|
|
148aa8505e | ||
|
|
f9a626a858 | ||
|
|
2f10121d6c | ||
|
|
1b8747115c | ||
|
|
e739302fb9 | ||
|
|
8f20d53f55 | ||
|
|
3ab80190d6 | ||
|
|
8948bf2803 | ||
|
|
486fd15c60 | ||
|
|
5fd85c71ca | ||
|
|
c1f41910c4 | ||
|
|
3693d6a6b6 | ||
|
|
cc3c992b1d | ||
|
|
df390ac456 | ||
|
|
fd84b0377b | ||
|
|
340d416b76 | ||
|
|
3034af8d69 | ||
|
|
da3020bd45 | ||
|
|
ce232c1002 | ||
|
|
0e13926400 | ||
|
|
fab7ec996a | ||
|
|
88e22f99c5 | ||
|
|
83cbabf06f | ||
|
|
5e8d3b8f02 | ||
|
|
1167b41340 | ||
|
|
67da3964e5 | ||
|
|
45ebc7eb6b | ||
|
|
cc5ead68bc | ||
|
|
9b9e4f22f3 | ||
|
|
7823f46053 | ||
|
|
d6d8e548b3 | ||
|
|
44b1ed7610 | ||
|
|
5786da5534 | ||
|
|
042a80dd44 | ||
|
|
ee30f6cd6d | ||
|
|
977a300808 | ||
|
|
a392ccab6a | ||
|
|
dfdfa5383b | ||
|
|
a08294ab16 | ||
|
|
e8c33a0d6a | ||
|
|
d6bf453a6d | ||
|
|
e80cdc2dae | ||
|
|
60780678fd | ||
|
|
273e7a2b63 | ||
|
|
2572c54744 | ||
|
|
6a7c987985 | ||
|
|
6ef866a77c | ||
|
|
eea26dee74 | ||
|
|
ee24fe8c24 | ||
|
|
9d9364af36 | ||
|
|
83228e2077 | ||
|
|
a379b5bd78 | ||
|
|
4ed1749652 | ||
|
|
9a90256a8a | ||
|
|
f0628a0d2c | ||
|
|
d5e2f57781 | ||
|
|
02673a1631 | ||
|
|
91c9746886 | ||
|
|
75b44b1636 | ||
|
|
f1fe48c1ee | ||
|
|
437e7c0d9c | ||
|
|
5ffa7ec7ee | ||
|
|
a1859f5d2e | ||
|
|
785af8cb8f | ||
|
|
06e1f4aa4a | ||
|
|
767f22ce9c | ||
|
|
6a9bba702c | ||
|
|
de2397dc3f | ||
|
|
daaad9212e | ||
|
|
a6891374a1 | ||
|
|
b1cadf0549 | ||
|
|
95f40cc954 | ||
|
|
8deaf123d6 | ||
|
|
053813bffb | ||
|
|
cc7af03618 | ||
|
|
a130e3700e | ||
|
|
0549e901b2 | ||
|
|
e9afe06968 | ||
|
|
c45aca4794 | ||
|
|
e2c54aaf43 | ||
|
|
37715d4c9f | ||
|
|
106367fa41 | ||
|
|
538130aa91 | ||
|
|
c9d0357fec | ||
|
|
d05c76da36 | ||
|
|
d2eb6ecbaf | ||
|
|
25a6fde212 | ||
|
|
e8f8b680ef | ||
|
|
6c30f2b863 | ||
|
|
8dda24a689 | ||
|
|
fbbb63906a | ||
|
|
1e66a7f144 | ||
|
|
df79d81475 | ||
|
|
ad71355859 | ||
|
|
95dc079fad | ||
|
|
770d4af4a0 | ||
|
|
3b311248cb | ||
|
|
67996f6d0a | ||
|
|
5772811a45 | ||
|
|
55529cd71e | ||
|
|
2e9b8b7b62 | ||
|
|
4ab2e41d18 | ||
|
|
c0ad8ee15a | ||
|
|
e6ae516493 | ||
|
|
a4d5ef4620 | ||
|
|
3f7bb047ac | ||
|
|
5454e1106f | ||
|
|
0d9397c8b9 | ||
|
|
429a3ff1fd | ||
|
|
bfff6ea7b8 | ||
|
|
b6b153c0de | ||
|
|
eca6a6e0ba | ||
|
|
ca43d96c46 | ||
|
|
112e3c8b1d | ||
|
|
d1eb105801 | ||
|
|
d5b93e89cd | ||
|
|
e96fe7cde1 | ||
|
|
68e0610f1d | ||
|
|
a6be20695a | ||
|
|
772b6ecf26 | ||
|
|
81dc7f604b | ||
|
|
a9c87fa52e | ||
|
|
e81f023943 | ||
|
|
2afe12ad2d | ||
|
|
4a9d6c1349 | ||
|
|
279fc8876e | ||
|
|
cc3d30dbcf | ||
|
|
39e82cc8a1 | ||
|
|
7a4b76a550 | ||
|
|
f9294446ba | ||
|
|
fa7ef62302 | ||
|
|
81d8224330 | ||
|
|
cc466a4b3c | ||
|
|
ceebca7de9 | ||
|
|
c2f696d0a4 | ||
|
|
1aa7c45794 | ||
|
|
faefa80cbd | ||
|
|
3737eacffd | ||
|
|
4c0af0b608 | ||
|
|
bb69eb13e7 | ||
|
|
7c9db6376f | ||
|
|
4c04e4e676 | ||
|
|
14ec35b257 | ||
|
|
b609e4973c | ||
|
|
354f0be9a3 | ||
|
|
16804ed34c | ||
|
|
31bd270869 | ||
|
|
b5d054f259 | ||
|
|
1ef2955d00 | ||
|
|
6ef56063b3 | ||
|
|
a912585800 | ||
|
|
7a863625bc | ||
|
|
99b4037820 | ||
|
|
d59b5dc2f9 | ||
|
|
4d0203e4ca | ||
|
|
3e4db571cd | ||
|
|
b0a9bd84d6 | ||
|
|
8c4e62e7c1 | ||
|
|
3e03d1dbd5 | ||
|
|
0dbf4cface | ||
|
|
bc22d61a3a | ||
|
|
d709bcc1d0 | ||
|
|
d7088be8af | ||
|
|
f17f1ecd93 | ||
|
|
bf556bd9f4 | ||
|
|
8b40969fa3 | ||
|
|
6cce931a88 | ||
|
|
216861f2c3 | ||
|
|
6ce724afb4 | ||
|
|
2e3e106fb2 | ||
|
|
00f2606a2f | ||
|
|
3c94a44285 | ||
|
|
4c0908acde | ||
|
|
cda27fb7f8 | ||
|
|
837a828cec | ||
|
|
4c8e13ccb9 | ||
|
|
1ae4eaab02 | ||
|
|
b1efbf74c7 | ||
|
|
12f04f6cf7 | ||
|
|
a937feed3f | ||
|
|
2de9899a99 | ||
|
|
a0af6201a5 | ||
|
|
9c7c6c8859 | ||
|
|
38d7cae1bc | ||
|
|
fee34f0dcb | ||
|
|
e01f7f4d92 | ||
|
|
985004bc0e | ||
|
|
967a78d657 | ||
|
|
2ff60dd5ac | ||
|
|
c3341da242 | ||
|
|
c2bfaeb5a9 | ||
|
|
c454616e1c | ||
|
|
c88e73b7d3 | ||
|
|
3a29818612 | ||
|
|
ee69085830 | ||
|
|
3963f7c971 | ||
|
|
602fcef949 | ||
|
|
075d260fdd | ||
|
|
0717093d81 | ||
|
|
1f87a9fd3d | ||
|
|
fdd3a3aecb | ||
|
|
d8281390c4 | ||
|
|
08b1712f39 | ||
|
|
6c69547cef | ||
|
|
89bf0488c3 | ||
|
|
653cf8dfbe | ||
|
|
33338506cf | ||
|
|
9dd479e597 | ||
|
|
8cad4cdf80 | ||
|
|
beced219c7 | ||
|
|
dfe13b1abd | ||
|
|
bf93bddbdd | ||
|
|
44890a34e8 | ||
|
|
a14aa201a8 | ||
|
|
338a9ba9fa | ||
|
|
0c65b5add9 | ||
|
|
29ce9a5df4 | ||
|
|
10f5cb8b42 | ||
|
|
8aad3716d4 | ||
|
|
d45f41500a | ||
|
|
4cc0a11d86 | ||
|
|
ad0a6b458f | ||
|
|
6ae1d5d6a5 | ||
|
|
84110aee97 | ||
|
|
4646577f35 | ||
|
|
822d957976 | ||
|
|
d4aedf59f3 | ||
|
|
5d80258a77 | ||
|
|
fa14531599 | ||
|
|
7992b3c2b9 | ||
|
|
5872cad1e5 | ||
|
|
16c0dfef4c | ||
|
|
70c7624c7a | ||
|
|
4a0d5e99d0 | ||
|
|
0742773e1e | ||
|
|
2f8bc4fef2 | ||
|
|
0dfd42259d | ||
|
|
9cf630320c | ||
|
|
584adc9296 | ||
|
|
f7dba6854f | ||
|
|
1d83e0ee31 | ||
|
|
d0244bd6d0 | ||
|
|
79bb5e1c77 | ||
|
|
3403712e24 | ||
|
|
8faa19629b | ||
|
|
7ff9c666ce | ||
|
|
d8bc2806e0 | ||
|
|
62f78b4028 | ||
|
|
cf9d2720ce | ||
|
|
09115dfe15 | ||
|
|
4cbb833e9d | ||
|
|
7419d0d2a1 | ||
|
|
f3e83d4045 | ||
|
|
28e06a21e4 | ||
|
|
e08fbbd223 | ||
|
|
a1e331d452 | ||
|
|
646f8af680 | ||
|
|
392f76fd36 | ||
|
|
f61a3eb85b | ||
|
|
19c03ec8d8 | ||
|
|
be1642f1c1 | ||
|
|
c9a71e2076 | ||
|
|
2199c1ebd3 | ||
|
|
278935f85d | ||
|
|
010631f553 | ||
|
|
8c0c65307a | ||
|
|
988cb34d7c | ||
|
|
6eec8117f5 | ||
|
|
9f6a909143 | ||
|
|
09f283721a | ||
|
|
235c702223 | ||
|
|
b923b43b6b | ||
|
|
fdcf3be0f9 | ||
|
|
25dd907591 | ||
|
|
80c049d276 | ||
|
|
310496d0b8 | ||
|
|
e961f01187 | ||
|
|
14b4c3cd50 | ||
|
|
4f987e7c2b | ||
|
|
f4d8c0a8db | ||
|
|
556ce471f8 | ||
|
|
32b6717114 | ||
|
|
e0e72fae72 | ||
|
|
203dd17421 | ||
|
|
1bde2b2ff1 | ||
|
|
ebbac96b16 | ||
|
|
ed0e67e853 | ||
|
|
f99d7dc00f | ||
|
|
2bf1c775ee | ||
|
|
4658e3735d | ||
|
|
627edc96bf | ||
|
|
0a1917e83c | ||
|
|
48b0d22da6 | ||
|
|
3201399bdf | ||
|
|
2ae70e8f07 | ||
|
|
2b9bb1ed06 | ||
|
|
e63e3e6725 | ||
|
|
6e9ac6a9f9 | ||
|
|
fb59c4a723 | ||
|
|
1b5ca9e52c | ||
|
|
787627a156 | ||
|
|
b1bba7685e | ||
|
|
cdfc0ccead | ||
|
|
0635cad350 | ||
|
|
6fd1fa6313 | ||
|
|
12b1f881dc | ||
|
|
bf3efab1af | ||
|
|
da72fda221 | ||
|
|
665501f485 | ||
|
|
6ee332fff8 | ||
|
|
f66447cb59 | ||
|
|
eb3120a8fd | ||
|
|
5fa39b3c6e | ||
|
|
91c35a88dd | ||
|
|
49f4d79f4f | ||
|
|
abfd010467 | ||
|
|
cde1013359 | ||
|
|
9c7f9e4316 | ||
|
|
aceda16c64 | ||
|
|
3656e5c725 | ||
|
|
6915b160a2 | ||
|
|
f464623bf6 | ||
|
|
b1f518071c | ||
|
|
0e903c3520 | ||
|
|
edcdfd97c4 | ||
|
|
c2b7081215 | ||
|
|
cf1954b10e | ||
|
|
46546e121f | ||
|
|
28146eec2c | ||
|
|
3cd9b9e06d | ||
|
|
7c43c61b85 | ||
|
|
465af4f3aa | ||
|
|
0a1e3dcd51 | ||
|
|
b97756d0cf | ||
|
|
acf4bc3737 | ||
|
|
88838872e7 | ||
|
|
7d3827d335 | ||
|
|
92d3a0cfa2 | ||
|
|
bba26624ad | ||
|
|
b82f497cab | ||
|
|
37f4d13e75 | ||
|
|
7965f5342c | ||
|
|
9cdc14dda8 | ||
|
|
4f46276ebf | ||
|
|
931d360fb1 | ||
|
|
f68c1855da | ||
|
|
dff654b6e5 | ||
|
|
7e85bcbf08 | ||
|
|
38a695ea12 | ||
|
|
a502b0cd0c | ||
|
|
934f708753 | ||
|
|
0e574c6cb1 | ||
|
|
7375a85b07 | ||
|
|
4c3393d8b2 | ||
|
|
a9f1bbff9f | ||
|
|
f86815e420 | ||
|
|
fd16b166ef | ||
|
|
02aa9319c3 | ||
|
|
1a72e46d53 | ||
|
|
d755579968 | ||
|
|
b74ee9df77 | ||
|
|
daa04bcea8 | ||
|
|
b96b2f24a6 | ||
|
|
5569ab62df | ||
|
|
d84b6c4f15 | ||
|
|
336f2c8a4d | ||
|
|
b52ceec36a | ||
|
|
1e6400cf79 | ||
|
|
1ff1ac951b | ||
|
|
aa6d771d17 | ||
|
|
512c238415 | ||
|
|
55ffeeda10 | ||
|
|
65f18b4160 | ||
|
|
b0c1d6a1bf | ||
|
|
ca19f2f2ed | ||
|
|
ac0378ca4b | ||
|
|
f06f8d1eb6 | ||
|
|
77074cb92f | ||
|
|
1274b6f683 | ||
|
|
c54ae58c0f | ||
|
|
3f54921e90 | ||
|
|
1b070c2dd4 | ||
|
|
0e5846b670 | ||
|
|
f81e71802b | ||
|
|
4c761eebff | ||
|
|
bf754d6e07 | ||
|
|
1c49acd5b5 | ||
|
|
c76187c6d2 | ||
|
|
b3b3ad843c |
34
.dockerignore
Normal file
@@ -0,0 +1,34 @@
|
||||
# Git history
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Dependencies — re-installed inside Docker
|
||||
node_modules/
|
||||
src/apps/cli/node_modules/
|
||||
|
||||
# Pre-built CLI output — rebuilt inside Docker
|
||||
src/apps/cli/dist/
|
||||
|
||||
# Obsidian plugin build outputs
|
||||
main.js
|
||||
main_org.js
|
||||
pouchdb-browser.js
|
||||
production/
|
||||
|
||||
# Test coverage and reports
|
||||
coverage/
|
||||
_testdata/
|
||||
test/bench-network/bench-results/
|
||||
src/apps/cli/testdeno/bench-results/
|
||||
|
||||
# Local environment / secrets
|
||||
.env
|
||||
*.env
|
||||
.test.env
|
||||
|
||||
# local config files
|
||||
*.local
|
||||
|
||||
# OS artefacts
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -20,12 +20,11 @@
|
||||
"ignorePatterns": [
|
||||
"**/node_modules/*",
|
||||
"**/jest.config.js",
|
||||
"src/lib/coverage",
|
||||
"src/lib/browsertest",
|
||||
"**/test.ts",
|
||||
"**/tests.ts",
|
||||
"**/**test.ts",
|
||||
"**/**.test.ts",
|
||||
"src/apps/**",
|
||||
"esbuild.*.mjs",
|
||||
"terser.*.mjs"
|
||||
],
|
||||
@@ -55,4 +54,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
.gitattributes
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Always checkout shell scripts with LF line endings (never CRLF)
|
||||
*.sh text eol=lf
|
||||
|
||||
# Standard text files — auto normalize on checkout
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.ini text eol=lf
|
||||
*.env text eol=lf
|
||||
*.json text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.css text eol=lf
|
||||
|
||||
# Binary files — no line ending conversion
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.woff2 binary
|
||||
*.woff binary
|
||||
*.sh text eol=lf
|
||||
88
.github/ISSUE_TEMPLATE/issue-report.md
vendored
@@ -2,77 +2,83 @@
|
||||
name: Issue report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'uncategorised'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Thank you for taking the time to report this issue!
|
||||
To improve the process, I would like to ask you to let me know the information in advance.
|
||||
Before filling in this form, please read: [How to report an issue](../docs/to_issue_reporting.md).
|
||||
|
||||
All instructions and examples, and empty entries can be deleted.
|
||||
Just for your information, a [filled example](https://docs.vrtmrz.net/LiveSync/hintandtrivia/Issue+example) is also written.
|
||||
Issues with sufficient information will be prioritised.
|
||||
|
||||
## Abstract
|
||||
The synchronisation hung up immediately after connecting.
|
||||
---
|
||||
|
||||
## Expected behaviour
|
||||
- Synchronisation ends with the message `Replication completed`
|
||||
- Everything synchronised
|
||||
## Required
|
||||
|
||||
## Actually happened
|
||||
- Synchronisation has been cancelled with the message `TypeError ... ` (captured in the attached log, around LL.10-LL.12)
|
||||
- No files synchronised
|
||||
### Abstract
|
||||
<!-- Briefly describe the problem in one or two sentences. -->
|
||||
|
||||
## Reproducing procedure
|
||||
### Expected behaviour
|
||||
<!-- What did you expect to happen? -->
|
||||
|
||||
1. Configure LiveSync as in the attached material.
|
||||
2. Click the replication button on the ribbon.
|
||||
3. Synchronising has begun.
|
||||
4. About two or three seconds later, we got the error `TypeError ... `.
|
||||
5. Replication has been stopped. No files synchronised.
|
||||
### Actually happened
|
||||
<!-- What actually happened? Include any error messages. -->
|
||||
|
||||
Note: If you do not catch the reproducing procedure, please let me know the frequency and signs.
|
||||
|
||||
## Report materials
|
||||
If the information is not available, do not hesitate to report it as it is. You can also of course omit it if you think this is indeed unnecessary. If it is necessary, I will ask you.
|
||||
|
||||
### Report from the LiveSync
|
||||
For more information, please refer to [Making the report](https://docs.vrtmrz.net/LiveSync/hintandtrivia/Making+the+report).
|
||||
<details>
|
||||
<summary>Report from hatch</summary>
|
||||
|
||||
```
|
||||
<!-- paste here -->
|
||||
```
|
||||
</details>
|
||||
### Reproducing procedure
|
||||
<!-- Step-by-step instructions to reproduce the issue. If you cannot reproduce it reliably, please describe the frequency and any signs you noticed. -->
|
||||
|
||||
### Obsidian debug info
|
||||
Please provide debug info for **each device involved**. The primary device (where the issue occurred) is required; others are strongly recommended. If your issue involves synchronisation between devices, debug info from relevant devices is very helpful.
|
||||
To get it: open the command palette → "Show debug info".
|
||||
|
||||
<details>
|
||||
<summary>Debug info</summary>
|
||||
<summary>Device 1 (primary)</summary>
|
||||
|
||||
```
|
||||
<!-- paste here -->
|
||||
```
|
||||
</details>
|
||||
|
||||
### Plug-in log
|
||||
We can see the log by tapping the Document box icon. If you noticed something suspicious, please let me know.
|
||||
Note: **Please enable `Verbose Log`**. For detail, refer to [Logging](https://docs.vrtmrz.net/LiveSync/hintandtrivia/Logging), please.
|
||||
|
||||
<details>
|
||||
<summary>Plug-in log</summary>
|
||||
<summary>Device 2 (if applicable)</summary>
|
||||
|
||||
```
|
||||
<!-- paste here -->
|
||||
```
|
||||
</details>
|
||||
|
||||
### Network log
|
||||
Network logs displayed in DevTools will possibly help with connection-related issues. To capture that, please refer to [DevTools](https://docs.vrtmrz.net/LiveSync/hintandtrivia/DevTools).
|
||||
### LiveSync version
|
||||
The hatch report (below) includes version information. If you cannot provide the report, please fill in the version here.
|
||||
|
||||
- Self-hosted LiveSync version: <!-- e.g. 0.23.0 — find it in Obsidian Settings → Community Plugins -->
|
||||
|
||||
### Report and Logs from LiveSync
|
||||
Perform a `Generate full report for opening the issue with debug info` command and provide the generated report. This contains detailed information and recent 1000 log lines, which is very helpful for debugging. **PLEASE AMEND THE REPORT TO REMOVE ANY SENSITIVE INFORMATION BEFORE PASTING.**
|
||||
If too large to paste here, upload to [Gist](https://gist.github.com/) and share the link.
|
||||
|
||||
<details>
|
||||
<summary>Report and Logs (primary)</summary>
|
||||
|
||||
```
|
||||
<!-- paste here or link to Gist -->
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Report and Logs (if applicable)</summary>
|
||||
|
||||
```
|
||||
<!-- paste here or link to Gist -->
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Optional
|
||||
|
||||
### Screenshots
|
||||
If applicable, please add screenshots to help explain your problem.
|
||||
|
||||
### Other information, insights and intuition.
|
||||
### Other information, insights and intuition
|
||||
Please provide any additional context or information about the problem.
|
||||
|
||||
182
.github/workflows/cli-deno-tests.yml
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
name: cli-deno-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
paths:
|
||||
- '.github/workflows/cli-deno-tests.yml'
|
||||
- 'src/apps/cli/**'
|
||||
- 'test/bench-network/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/cli-deno-tests.yml'
|
||||
- 'src/apps/cli/**'
|
||||
- 'test/bench-network/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_task:
|
||||
description: 'Deno test task to run'
|
||||
type: choice
|
||||
options:
|
||||
- test:ci
|
||||
- test:local
|
||||
- test:e2e-matrix
|
||||
default: test:ci
|
||||
enable_debug:
|
||||
description: 'Enable verbose and debug logging'
|
||||
type: boolean
|
||||
default: false
|
||||
use_coturn:
|
||||
description: 'Enable local coturn container for P2P tests'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
task_matrix: ${{ steps.select.outputs.task_matrix }}
|
||||
steps:
|
||||
- name: Select task matrix
|
||||
id: select
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SELECTED_TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test:ci' }}"
|
||||
echo "[INFO] Selected task set: $SELECTED_TASK"
|
||||
|
||||
case "$SELECTED_TASK" in
|
||||
test:ci)
|
||||
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]'
|
||||
;;
|
||||
test:local)
|
||||
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon"]'
|
||||
;;
|
||||
test:e2e-matrix)
|
||||
TASK_MATRIX='["test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]'
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unknown task set: $SELECTED_TASK" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "task_matrix=$TASK_MATRIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
test:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
DENO_DIR: ~/.cache/deno
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: ${{ fromJson(needs.prepare.outputs.task_matrix) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Cache Deno dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/deno
|
||||
key: ${{ runner.os }}-deno-${{ hashFiles('src/apps/cli/testdeno/deno.lock', 'src/apps/cli/testdeno/deno.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-deno-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build CLI
|
||||
working-directory: src/apps/cli
|
||||
run: npm run build
|
||||
|
||||
- name: Create .test.env
|
||||
working-directory: src/apps/cli
|
||||
run: |
|
||||
cat <<EOF > .test.env
|
||||
hostname=http://127.0.0.1:5989/
|
||||
dbname=livesync-test-db-ci
|
||||
username=admin
|
||||
password=testpassword
|
||||
minioEndpoint=http://127.0.0.1:9000
|
||||
accessKey=minioadmin
|
||||
secretKey=minioadmin
|
||||
bucketName=livesync-test-bucket-ci
|
||||
EOF
|
||||
|
||||
- name: Run Deno tests
|
||||
working-directory: src/apps/cli/testdeno
|
||||
env:
|
||||
LIVESYNC_DOCKER_MODE: native
|
||||
LIVESYNC_CLI_RETRY: 3
|
||||
LIVESYNC_CLI_DEBUG: ${{ inputs.enable_debug == true && '1' || '0' }}
|
||||
LIVESYNC_CLI_VERBOSE: ${{ inputs.enable_debug == true && '1' || '0' }}
|
||||
LIVESYNC_USE_COTURN: ${{ inputs.use_coturn == true && '1' || '0' }}
|
||||
run: |
|
||||
TASK="${{ matrix.task }}"
|
||||
echo "[INFO] Running Deno task: $TASK"
|
||||
deno task "$TASK"
|
||||
|
||||
- name: Stop leftover containers
|
||||
if: always()
|
||||
run: |
|
||||
docker stop couchdb-test minio-test relay-test coturn-test >/dev/null 2>&1 || true
|
||||
docker rm couchdb-test minio-test relay-test coturn-test >/dev/null 2>&1 || true
|
||||
|
||||
compose-p2p-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Show Docker versions
|
||||
run: |
|
||||
docker --version
|
||||
docker compose version
|
||||
|
||||
- name: Run Compose CLI P2P E2E
|
||||
env:
|
||||
CLI_E2E_TASK: test:p2p:ci
|
||||
RELAY: ws://nostr-relay:7777/
|
||||
PEERS_TIMEOUT: '20'
|
||||
SYNC_TIMEOUT: '60'
|
||||
LIVESYNC_USE_COTURN: '0'
|
||||
TURN_SERVERS: none
|
||||
LIVESYNC_P2P_PEERS_RETRY: '1'
|
||||
LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: '60000'
|
||||
BENCH_LIVESYNC_TEST_TEE: '0'
|
||||
run: docker compose -f test/bench-network/compose.yml run --build --rm bench-runner run-livesync-cli-e2e
|
||||
|
||||
- name: Show Compose diagnostics
|
||||
if: failure()
|
||||
run: |
|
||||
docker compose -f test/bench-network/compose.yml ps
|
||||
docker compose -f test/bench-network/compose.yml logs --no-color couchdb nostr-relay || true
|
||||
|
||||
- name: Stop Compose services
|
||||
if: always()
|
||||
run: docker compose -f test/bench-network/compose.yml down -v --remove-orphans
|
||||
139
.github/workflows/cli-docker.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
# Build and push the CLI Docker image to GitHub Container Registry (GHCR).#
|
||||
# Image tag format: <manifest-version>-<unix-epoch>-cli
|
||||
# Example: 0.25.56-1743500000-cli
|
||||
#
|
||||
# Stable releases are also tagged with their major-minor version and 'latest'.
|
||||
# Pre-releases receive immutable version and SHA-qualified tags only.
|
||||
# Image name: ghcr.io/<owner>/livesync-cli
|
||||
name: Build and Push CLI Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "*.*.*-cli"
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "*.md"
|
||||
- "images/**"
|
||||
- "assets/**"
|
||||
- "instruction_images/**"
|
||||
- "src/apps/webapp/**"
|
||||
- "src/apps/webpeer/**"
|
||||
- ".github/workflows/release.yml"
|
||||
- ".github/workflows/unit-ci.yml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Build only (do not push image to GHCR)
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
force:
|
||||
description: Continue to build/push even if CLI E2E fails (workflow_dispatch only)
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Derive image tag
|
||||
id: meta
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' manifest.json)
|
||||
MAJOR_MINOR=$(echo "${VERSION}" | cut -d. -f1,2)
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
|
||||
|
||||
# Build tag list based on the event and git ref
|
||||
TAGS=""
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
if [[ "${VERSION}" == *-* ]]; then
|
||||
# Pre-release builds must not advance stable moving tags.
|
||||
TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli"
|
||||
else
|
||||
# Stable release builds
|
||||
TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${MAJOR_MINOR}-cli,${IMAGE}:latest,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli"
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" == refs/heads/main ]]; then
|
||||
# Bleeding-edge / nightly builds
|
||||
TAGS="${IMAGE}:edge"
|
||||
else
|
||||
# Other branches / manual run fallback
|
||||
TAGS="${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli"
|
||||
fi
|
||||
|
||||
# Determine if the image should be pushed
|
||||
PUSH="true"
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
if [[ "${{ inputs.dry_run }}" == "true" ]]; then
|
||||
PUSH="false"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||
echo "push=${PUSH}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run CLI E2E (docker)
|
||||
id: e2e
|
||||
continue-on-error: ${{ github.event_name == 'workflow_dispatch' && inputs.force }}
|
||||
working-directory: src/apps/cli
|
||||
env:
|
||||
CI: true
|
||||
run: npm run test:e2e:docker:all
|
||||
|
||||
- name: Stop test containers (safety net)
|
||||
if: always()
|
||||
working-directory: src/apps/cli
|
||||
run: |
|
||||
# Keep this as a safety net for future suites/steps that may leave containers running.
|
||||
bash ./util/couchdb-stop.sh >/dev/null 2>&1 || true
|
||||
bash ./util/minio-stop.sh >/dev/null 2>&1 || true
|
||||
bash ./util/p2p-stop.sh >/dev/null 2>&1 || true
|
||||
|
||||
- name: Build and push
|
||||
if: ${{ steps.e2e.outcome == 'success' || (github.event_name == 'workflow_dispatch' && inputs.force) }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: src/apps/cli/Dockerfile
|
||||
push: ${{ steps.meta.outputs.push }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
65
.github/workflows/cli-e2e.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# Run CLI E2E tests
|
||||
name: cli-e2e
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
suite:
|
||||
description: 'CLI E2E suite to run'
|
||||
type: choice
|
||||
options:
|
||||
- two-vaults-matrix
|
||||
- two-vaults-couchdb
|
||||
- two-vaults-minio
|
||||
default: two-vaults-matrix
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run CLI E2E suite
|
||||
working-directory: src/apps/cli
|
||||
env:
|
||||
CI: true
|
||||
TEST_SUITE: ${{ github.event_name == 'workflow_dispatch' && inputs.suite || 'two-vaults-matrix' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "[INFO] Running CLI E2E suite: $TEST_SUITE"
|
||||
case "$TEST_SUITE" in
|
||||
two-vaults-matrix)
|
||||
npm run test:e2e:two-vaults:matrix
|
||||
;;
|
||||
two-vaults-couchdb)
|
||||
REMOTE_TYPE=COUCHDB ENCRYPT=0 npm run test:e2e:two-vaults
|
||||
;;
|
||||
two-vaults-minio)
|
||||
REMOTE_TYPE=MINIO ENCRYPT=0 npm run test:e2e:two-vaults
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unknown suite: $TEST_SUITE" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Stop test containers
|
||||
if: always()
|
||||
working-directory: src/apps/cli
|
||||
run: |
|
||||
bash ./util/couchdb-stop.sh >/dev/null 2>&1 || true
|
||||
bash ./util/minio-stop.sh >/dev/null 2>&1 || true
|
||||
94
.github/workflows/cli-p2p-compose-smoke.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
# Run the Compose-packaged CLI P2P smoke benchmark.
|
||||
#
|
||||
# This workflow is intentionally manual-only. It exercises the local Compose
|
||||
# package for CouchDB + Nostr relay + CLI runner, and uploads the benchmark JSON
|
||||
# results for inspection without adding benchmark work to pull-request CI.
|
||||
name: cli-p2p-compose-smoke
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cases:
|
||||
description: 'Comma-separated benchmark cases'
|
||||
required: false
|
||||
default: 'couchdb-baseline,p2p-direct-local'
|
||||
signalling_cases:
|
||||
description: 'Comma-separated signalling-shim P2P benchmark cases'
|
||||
required: false
|
||||
default: 'p2p-signalling-netem-home-wifi'
|
||||
md_files:
|
||||
description: 'Markdown file count'
|
||||
required: false
|
||||
default: '2'
|
||||
bin_files:
|
||||
description: 'Binary file count'
|
||||
required: false
|
||||
default: '1'
|
||||
couchdb_rtt_ms:
|
||||
description: 'Requested CouchDB RTT in milliseconds'
|
||||
required: false
|
||||
default: '20'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Show Docker versions
|
||||
run: |
|
||||
docker --version
|
||||
docker compose version
|
||||
|
||||
- name: Run Compose P2P smoke benchmark
|
||||
env:
|
||||
BENCH_CASES: ${{ inputs.cases || 'couchdb-baseline,p2p-direct-local' }}
|
||||
BENCH_MD_FILE_COUNT: ${{ inputs.md_files || '2' }}
|
||||
BENCH_MD_MIN_SIZE_BYTES: '128'
|
||||
BENCH_MD_MAX_SIZE_BYTES: '256'
|
||||
BENCH_BIN_FILE_COUNT: ${{ inputs.bin_files || '1' }}
|
||||
BENCH_BIN_SIZE_BYTES: '512'
|
||||
BENCH_COUCHDB_RTT_MS: ${{ inputs.couchdb_rtt_ms || '20' }}
|
||||
BENCH_SYNC_TIMEOUT: '180'
|
||||
BENCH_PEERS_TIMEOUT: '20'
|
||||
LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: '60000'
|
||||
BENCH_LIVESYNC_TEST_TEE: '0'
|
||||
run: docker compose -f test/bench-network/compose.yml run --build --rm bench-runner
|
||||
|
||||
- name: Run Compose P2P signalling-shim smoke benchmark
|
||||
env:
|
||||
BENCH_CASES: ${{ inputs.signalling_cases || 'p2p-signalling-netem-home-wifi' }}
|
||||
BENCH_MD_FILE_COUNT: ${{ inputs.md_files || '2' }}
|
||||
BENCH_MD_MIN_SIZE_BYTES: '128'
|
||||
BENCH_MD_MAX_SIZE_BYTES: '256'
|
||||
BENCH_BIN_FILE_COUNT: ${{ inputs.bin_files || '1' }}
|
||||
BENCH_BIN_SIZE_BYTES: '512'
|
||||
BENCH_SYNC_TIMEOUT: '180'
|
||||
BENCH_PEERS_TIMEOUT: '60'
|
||||
LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: '60000'
|
||||
BENCH_LIVESYNC_TEST_TEE: '0'
|
||||
NETEM_PROFILE: 'home-wifi'
|
||||
run: docker compose -f test/bench-network/compose.yml --profile signalling-shim run --build --rm bench-runner-signalling-shim
|
||||
|
||||
- name: Show Compose diagnostics
|
||||
if: failure()
|
||||
run: |
|
||||
docker compose -f test/bench-network/compose.yml ps
|
||||
docker compose -f test/bench-network/compose.yml --profile signalling-shim logs --no-color couchdb nostr-relay p2p-signalling-shim || true
|
||||
|
||||
- name: Upload benchmark results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-p2p-compose-smoke-results
|
||||
path: test/bench-network/bench-results/**
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Stop Compose services
|
||||
if: always()
|
||||
run: docker compose -f test/bench-network/compose.yml down -v --remove-orphans
|
||||
67
.github/workflows/deploy-pages.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'aggregator.html'
|
||||
- '.github/workflows/deploy-pages.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'aggregator.html'
|
||||
- '.github/workflows/deploy-pages.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Validate and package Pages site
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate aggregator
|
||||
run: |
|
||||
test -s aggregator.html
|
||||
grep -Fq '<!DOCTYPE html>' aggregator.html
|
||||
grep -Fq 'obsidian://setuplivesync?settingsQR=' aggregator.html
|
||||
sed -n '/<script>/,/<\/script>/p' aggregator.html | sed '1d;$d' > aggregator.js
|
||||
node --check aggregator.js
|
||||
rm aggregator.js
|
||||
|
||||
- name: Prepare Pages site
|
||||
run: |
|
||||
mkdir -p _site
|
||||
cp aggregator.html _site/aggregator.html
|
||||
touch _site/.nojekyll
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload GitHub Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: _site
|
||||
|
||||
deploy:
|
||||
name: Deploy GitHub Pages
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
128
.github/workflows/finalise-release.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
name: Finalise Release Tags
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Release version, for example 0.25.81
|
||||
required: true
|
||||
type: string
|
||||
release_branch:
|
||||
description: Release PR branch. Defaults to the version with dots replaced by underscores.
|
||||
required: false
|
||||
type: string
|
||||
expected_head_sha:
|
||||
description: Full head commit SHA reviewed in the release PR
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: Mark the GitHub Release as a pre-release
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
publish_cli:
|
||||
description: Create the CLI tag and publish its container image
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
finalise:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Resolve release branch
|
||||
id: branch
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
RELEASE_BRANCH_INPUT: ${{ inputs.release_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BRANCH="${RELEASE_BRANCH_INPUT}"
|
||||
if [[ -z "${BRANCH}" ]]; then
|
||||
BRANCH="${VERSION//./_}"
|
||||
fi
|
||||
echo "name=${BRANCH}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.branch.outputs.name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
|
||||
- name: Validate release head
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }}
|
||||
PRERELEASE: ${{ inputs.prerelease }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ACTUAL_HEAD_SHA="$(git rev-parse HEAD)"
|
||||
if [[ "${ACTUAL_HEAD_SHA}" != "${EXPECTED_HEAD_SHA}" ]]; then
|
||||
echo "Release branch head is ${ACTUAL_HEAD_SHA}, expected ${EXPECTED_HEAD_SHA}." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${VERSION}" == *-* && "${PRERELEASE}" != "true" ]]; then
|
||||
echo "Version ${VERSION} is a pre-release version, but prerelease was not enabled." >&2
|
||||
exit 1
|
||||
fi
|
||||
node utils/release-notes.mjs validate "${VERSION}"
|
||||
|
||||
- name: Ensure and push release tags
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }}
|
||||
PUBLISH_CLI: ${{ inputs.publish_cli }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --tags --force
|
||||
if [[ "${PUBLISH_CLI}" == "true" ]]; then
|
||||
node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}"
|
||||
git push --atomic origin "refs/tags/${VERSION}" "refs/tags/${VERSION}-cli"
|
||||
else
|
||||
node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}" --plugin-only
|
||||
git push origin "refs/tags/${VERSION}"
|
||||
fi
|
||||
|
||||
- name: Dispatch release workflows
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
PRERELEASE: ${{ inputs.prerelease }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh workflow run release.yml \
|
||||
--ref "${VERSION}" \
|
||||
--field tag="${VERSION}" \
|
||||
--field draft=true \
|
||||
--field prerelease="${PRERELEASE}"
|
||||
|
||||
- name: Summarise next steps
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
PRERELEASE: ${{ inputs.prerelease }}
|
||||
PUBLISH_CLI: ${{ inputs.publish_cli }}
|
||||
run: |
|
||||
{
|
||||
echo "Ensured the plug-in tag \`${VERSION}\` points to the reviewed release commit."
|
||||
if [[ "${PUBLISH_CLI}" == "true" ]]; then
|
||||
echo "The CLI tag \`${VERSION}-cli\` was also created; its tag event starts the container workflow."
|
||||
else
|
||||
echo "CLI publication was omitted."
|
||||
fi
|
||||
echo ""
|
||||
echo "Dispatched the plug-in release workflow for \`${VERSION}\`. After approval for the release environment, it creates a draft GitHub Release."
|
||||
echo ""
|
||||
if [[ "${PRERELEASE}" == "true" ]]; then
|
||||
echo "Publish the draft as a pre-release, keep the release pull request in draft, and merge only after BRAT validation succeeds."
|
||||
else
|
||||
echo "Publish the draft as the latest stable release, keep the release pull request in draft, and merge only after BRAT validation succeeds."
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
97
.github/workflows/prepare-release.yml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: Prepare Release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Release version, for example 0.25.81
|
||||
required: true
|
||||
type: string
|
||||
base_branch:
|
||||
description: Base branch for the release PR
|
||||
required: false
|
||||
type: string
|
||||
default: main
|
||||
release_branch:
|
||||
description: Release branch name. Defaults to the version with dots replaced by underscores.
|
||||
required: false
|
||||
type: string
|
||||
release_date:
|
||||
description: Release date in ordinal format. Defaults to the current UTC date.
|
||||
required: false
|
||||
type: string
|
||||
allow_empty_updates:
|
||||
description: Allow an empty Unreleased section
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.base_branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24.x"
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Prepare release changes
|
||||
id: prepare
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
RELEASE_BRANCH_INPUT: ${{ inputs.release_branch }}
|
||||
RELEASE_DATE: ${{ inputs.release_date }}
|
||||
ALLOW_EMPTY_UPDATES: ${{ inputs.allow_empty_updates }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
BRANCH="${RELEASE_BRANCH_INPUT}"
|
||||
if [[ -z "${BRANCH}" ]]; then
|
||||
BRANCH="${VERSION//./_}"
|
||||
fi
|
||||
|
||||
if git ls-remote --exit-code --heads origin "${BRANCH}" >/dev/null 2>&1; then
|
||||
echo "Release branch already exists: ${BRANCH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git switch -c "${BRANCH}"
|
||||
npm version "${VERSION}" --no-git-tag-version --allow-same-version
|
||||
node utils/release-notes.mjs prepare "${VERSION}"
|
||||
|
||||
git add package.json package-lock.json manifest.json versions.json updates.md src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json
|
||||
git diff --cached --check
|
||||
git commit -m "Releasing ${VERSION}"
|
||||
git push --set-upstream origin "${BRANCH}"
|
||||
|
||||
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create draft release PR
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||
RELEASE_BRANCH: ${{ steps.prepare.outputs.branch }}
|
||||
run: |
|
||||
node utils/release-pr-body.mjs "${VERSION}" "${BASE_BRANCH}" > /tmp/release-pr-body.md
|
||||
|
||||
gh pr create \
|
||||
--base "${BASE_BRANCH}" \
|
||||
--head "${RELEASE_BRANCH}" \
|
||||
--draft \
|
||||
--title "Releasing ${VERSION}" \
|
||||
--body-file /tmp/release-pr-body.md
|
||||
123
.github/workflows/release.yml
vendored
@@ -1,104 +1,73 @@
|
||||
name: Release Obsidian Plugin
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to build
|
||||
required: true
|
||||
type: string
|
||||
draft:
|
||||
description: Create the GitHub Release as a draft
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
prerelease:
|
||||
description: Mark the GitHub Release as a pre-release
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.tag }}
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x' # You might need to adjust this value to your own version
|
||||
# Get the version number and put it in a variable
|
||||
- name: Get Version
|
||||
id: version
|
||||
node-version: '24.x'
|
||||
- name: Validate release
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
echo "tag=$(git describe --abbrev=0 --tags)" >> $GITHUB_OUTPUT
|
||||
set -euo pipefail
|
||||
node utils/release-notes.mjs validate "${TAG}"
|
||||
HEAD_SHA="$(git rev-parse HEAD)"
|
||||
TAG_SHA="$(git rev-parse "refs/tags/${TAG}^{commit}")"
|
||||
if [[ "${HEAD_SHA}" != "${TAG_SHA}" ]]; then
|
||||
echo "Checked-out commit is ${HEAD_SHA}, but tag ${TAG} points to ${TAG_SHA}." >&2
|
||||
exit 1
|
||||
fi
|
||||
# Build the plugin
|
||||
- name: Build
|
||||
id: build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build --if-present
|
||||
# Package the required files into a zip
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir ${{ github.event.repository.name }}
|
||||
cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }}
|
||||
zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }}
|
||||
# Create the release on github
|
||||
# - name: Create Release
|
||||
# id: create_release
|
||||
# uses: actions/create-release@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# VERSION: ${{ steps.version.outputs.tag }}
|
||||
# with:
|
||||
# tag_name: ${{ steps.version.outputs.tag }}
|
||||
# release_name: ${{ steps.version.outputs.tag }}
|
||||
# draft: true
|
||||
# prerelease: false
|
||||
# # Upload the packaged release file
|
||||
# - name: Upload zip file
|
||||
# id: upload-zip
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: ./${{ github.event.repository.name }}.zip
|
||||
# asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.tag }}.zip
|
||||
# asset_content_type: application/zip
|
||||
# # Upload the main.js
|
||||
# - name: Upload main.js
|
||||
# id: upload-main
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: ./main.js
|
||||
# asset_name: main.js
|
||||
# asset_content_type: text/javascript
|
||||
# # Upload the manifest.json
|
||||
# - name: Upload manifest.json
|
||||
# id: upload-manifest
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: ./manifest.json
|
||||
# asset_name: manifest.json
|
||||
# asset_content_type: application/json
|
||||
# # Upload the style.css
|
||||
# - name: Upload styles.css
|
||||
# id: upload-css
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
# asset_path: ./styles.css
|
||||
# asset_name: styles.css
|
||||
# asset_content_type: text/css
|
||||
# Attest
|
||||
- name: Attest Plugin Artifacts
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
- name: Create Release and Upload Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
${{ github.event.repository.name }}.zip
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
name: ${{ steps.version.outputs.tag }}
|
||||
tag_name: ${{ steps.version.outputs.tag }}
|
||||
draft: true
|
||||
name: ${{ inputs.tag }}
|
||||
tag_name: ${{ inputs.tag }}
|
||||
draft: ${{ inputs.draft }}
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
|
||||
211
.github/workflows/unit-ci.yml
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
# Run Unit test without Harnesses
|
||||
name: unit-ci
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'test/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'vite.config.ts'
|
||||
- 'vitest.config*.ts'
|
||||
- 'esbuild.config.mjs'
|
||||
- 'eslint.config.mjs'
|
||||
- 'eslint.config.common.mjs'
|
||||
- 'eslint.community.config.mjs'
|
||||
- 'update-workspaces.mjs'
|
||||
- 'version-bump.mjs'
|
||||
- 'utils/release-*.mjs'
|
||||
- 'utils/release-*.unit.spec.ts'
|
||||
- 'utils/couchdb/**'
|
||||
- 'utils/flyio/**'
|
||||
- 'utils/setup/**'
|
||||
- '.github/workflows/prepare-release.yml'
|
||||
- '.github/workflows/finalise-release.yml'
|
||||
- '.github/workflows/release.yml'
|
||||
- '.github/workflows/unit-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'test/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'vite.config.ts'
|
||||
- 'vitest.config*.ts'
|
||||
- 'esbuild.config.mjs'
|
||||
- 'eslint.config.mjs'
|
||||
- 'eslint.config.common.mjs'
|
||||
- 'eslint.community.config.mjs'
|
||||
- 'update-workspaces.mjs'
|
||||
- 'version-bump.mjs'
|
||||
- 'utils/release-*.mjs'
|
||||
- 'utils/release-*.unit.spec.ts'
|
||||
- 'utils/couchdb/**'
|
||||
- 'utils/flyio/**'
|
||||
- 'utils/setup/**'
|
||||
- '.github/workflows/prepare-release.yml'
|
||||
- '.github/workflows/finalise-release.yml'
|
||||
- '.github/workflows/release.yml'
|
||||
- '.github/workflows/unit-ci.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
setup-tools:
|
||||
name: Self-hosted setup tools
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Setup Deno
|
||||
uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run setup-tool contract tests
|
||||
run: npm run test:setup-tools
|
||||
|
||||
- name: Create CouchDB test configuration
|
||||
run: |
|
||||
cat <<EOF > .test.env
|
||||
hostname=http://127.0.0.1:5989/
|
||||
dbname=livesync-test-db
|
||||
username=admin
|
||||
password=testpassword
|
||||
EOF
|
||||
|
||||
- name: Start CouchDB
|
||||
run: npm run test:docker-couchdb:start
|
||||
|
||||
- name: Provision a versioned LiveSync database
|
||||
run: npx dotenv-cli -e .test.env -- env database=setup-tools-ci retry_count=1 retry_delay_ms=0 ./utils/couchdb/couchdb-init.sh
|
||||
|
||||
- name: Verify the Commonlib database version
|
||||
run: |
|
||||
npx dotenv-cli -e .test.env -- bash -lc 'curl --fail --silent --show-error --user "${username}:${password}" "${hostname}/setup-tools-ci/obsydian_livesync_version" | node --input-type=module -e "import { VER } from \"@vrtmrz/livesync-commonlib/compat/common/types\";let input=\"\";process.stdin.on(\"data\",chunk=>input+=chunk).on(\"end\",()=>{const document=JSON.parse(input);if(document.type!==\"versioninfo\"||document.version!==VER)throw new Error(\"Unexpected LiveSync database version\");})"'
|
||||
|
||||
- name: Stop CouchDB
|
||||
if: always()
|
||||
run: npm run test:docker-couchdb:stop || true
|
||||
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run source checks
|
||||
run: npm run check
|
||||
|
||||
- name: Run unit tests suite with coverage
|
||||
run: npm run test:unit:coverage
|
||||
|
||||
- name: Run real-Obsidian runner contract tests
|
||||
run: npm run test:e2e:obsidian:runner
|
||||
|
||||
- name: Upload coverage report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unit-coverage-report
|
||||
path: coverage/**
|
||||
|
||||
integration-test:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect LiveSync-owned integration tests
|
||||
id: integration_tests
|
||||
shell: bash
|
||||
run: |
|
||||
git ls-files -- ':(glob)**/*.integration.spec.ts' ':(glob)**/*.integration.test.ts' > "$RUNNER_TEMP/livesync-integration-tests.txt"
|
||||
if [[ -s "$RUNNER_TEMP/livesync-integration-tests.txt" ]]; then
|
||||
echo 'present=true' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'present=false' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Record delegated integration coverage
|
||||
if: ${{ steps.integration_tests.outputs.present != 'true' }}
|
||||
run: echo 'No LiveSync-owned integration tests are present. Commonlib integration tests run in the Commonlib package CI.' >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: ${{ steps.integration_tests.outputs.present == 'true' }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ steps.integration_tests.outputs.present == 'true' }}
|
||||
run: npm ci
|
||||
|
||||
- name: Create environment configuration files
|
||||
if: ${{ steps.integration_tests.outputs.present == 'true' }}
|
||||
run: |
|
||||
cat <<EOF > .env
|
||||
BUILD_MODE=dev
|
||||
PATHS_TEST_INSTALL=
|
||||
EOF
|
||||
cat <<EOF > .test.env
|
||||
hostname=http://127.0.0.1:5989/
|
||||
dbname=livesync-test-db2
|
||||
username=admin
|
||||
password=testpassword
|
||||
minioEndpoint=http://127.0.0.1:9000
|
||||
accessKey=minioadmin
|
||||
secretKey=minioadmin
|
||||
bucketName=livesync-test-bucket
|
||||
EOF
|
||||
|
||||
- name: Start CouchDB container
|
||||
if: ${{ steps.integration_tests.outputs.present == 'true' }}
|
||||
run: npm run test:docker-couchdb:start
|
||||
|
||||
- name: Start MinIO container
|
||||
if: ${{ steps.integration_tests.outputs.present == 'true' }}
|
||||
run: npm run test:docker-s3:start
|
||||
|
||||
- name: Run integration tests
|
||||
if: ${{ steps.integration_tests.outputs.present == 'true' }}
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Stop containers
|
||||
if: ${{ always() && steps.integration_tests.outputs.present == 'true' }}
|
||||
run: |
|
||||
npm run test:docker-couchdb:stop || true
|
||||
npm run test:docker-s3:stop || true
|
||||
13
.gitignore
vendored
@@ -21,3 +21,16 @@ data.json
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
|
||||
# local config files
|
||||
*.local
|
||||
|
||||
cov_profile/**
|
||||
|
||||
coverage
|
||||
src/apps/cli/dist/*
|
||||
src/apps/webapp/playwright-report/
|
||||
src/apps/webapp/test-results/
|
||||
_testdata/**
|
||||
utils/bench/splitResults.csv
|
||||
.eslintcache
|
||||
|
||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "src/lib"]
|
||||
path = src/lib
|
||||
url = https://github.com/vrtmrz/livesync-commonlib
|
||||
@@ -1,2 +1,4 @@
|
||||
pouchdb-browser.js
|
||||
main_org.js
|
||||
main.js
|
||||
_types/**
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
20
.prettierrc.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import { readFileSync } from "fs";
|
||||
let localPrettierConfig = {};
|
||||
|
||||
try {
|
||||
const localConfig = readFileSync(".prettierrc.local", "utf-8");
|
||||
localPrettierConfig = JSON.parse(localConfig);
|
||||
console.log("Using local Prettier config from .prettierrc.local");
|
||||
} catch (e) {
|
||||
// no local config
|
||||
}
|
||||
const prettierConfig = {
|
||||
trailingComma: "es5",
|
||||
tabWidth: 4,
|
||||
printWidth: 120,
|
||||
semi: true,
|
||||
endOfLine: "lf",
|
||||
...localPrettierConfig,
|
||||
};
|
||||
|
||||
export default prettierConfig;
|
||||
11
.test.env
Normal file
@@ -0,0 +1,11 @@
|
||||
hostname=http://localhost:5989/
|
||||
dbname=livesync-test-db2
|
||||
minioEndpoint=http://127.0.0.1:9000
|
||||
username=admin
|
||||
password=testpassword
|
||||
accessKey=minioadmin
|
||||
secretKey=minioadmin
|
||||
bucketName=livesync-test-bucket
|
||||
# ENABLE_DEBUGGER=true
|
||||
# PRINT_LIVESYNC_LOGS=true
|
||||
# ENABLE_UI=true
|
||||
69
AGENTS.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# AI Coding Assistant Instructions (AGENTS.md)
|
||||
|
||||
When working on this repository (writing code, comments, documentation, or commits), you MUST follow these guidelines to maintain consistency.
|
||||
|
||||
## Required Reference Files
|
||||
|
||||
Before making changes to documentation, user-facing text, or settings:
|
||||
1. Read [docs/terms.md](docs/terms.md) for terminology, vocabulary conventions, and technical definitions.
|
||||
2. Read [docs/settings.md](docs/settings.md) (and [docs/settings_ja.md](docs/settings_ja.md)) for UI settings and setting key mappings.
|
||||
3. Read [docs/troubleshooting.md](docs/troubleshooting.md) for troubleshooting guidelines and common recovery steps (such as flag files and SCRAM state).
|
||||
4. Read [devs.md](devs.md) for development workflows, module architecture, and testing infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Documentation and User-Facing Text Rules
|
||||
|
||||
Always adhere to the following stylistic and spelling rules:
|
||||
|
||||
1. **British English Spelling**:
|
||||
- Write all documentation and user-facing messages in British English. If in doubt, the BBC News Styleguide may be useful as a reference.
|
||||
- **Traditional Spelling (Trad-spelling)**: Use `-ise` and `-isation` suffixes instead of `-ize` and `-ization` (for example: 'initialisation', 'synchronisation', and 'organisation').
|
||||
- **Oxford Comma**: Use the serial (Oxford) comma to separate items in lists of three or more (for example: 'settings, snippets, and themes' instead of 'settings, snippets and themes').
|
||||
- **Logical Punctuation**: Place punctuation marks (such as commas and full stops) outside quotation marks unless they are part of the quoted text itself (for example: write 'dialogue', not 'dialogue,').
|
||||
|
||||
2. **No Contractions**:
|
||||
- Do not use contractions in general text or documentation (for example: write "do not" instead of "don't", "cannot" instead of "can't", and "is not" instead of "isn't").
|
||||
|
||||
3. **Quotation Style**:
|
||||
- Prefer single quotation marks (`'`) over double quotation marks (`"`) in general documentation text, unless the context requires double quotes (for example, inside JSON code blocks).
|
||||
|
||||
4. **Specific Terminology and Spelling**:
|
||||
- Use **'dialogue'** in documentation, user-facing messages, and general text. Use **'dialog'** only inside source code (e.g. class names, methods).
|
||||
- Use the hyphenated form **'plug-in'** in user-facing text. Use **'plugin'** only in codebase files, configuration settings, or technical contexts.
|
||||
|
||||
5. **User Communication Language**:
|
||||
- Always reply to the user in the language in which they asked the question.
|
||||
|
||||
---
|
||||
|
||||
## Technical & Architecture Rules
|
||||
|
||||
1. **Database Structure**:
|
||||
- Remember that Self-hosted LiveSync splits files into **Metadata** (file properties, size, paths) and **Chunks** (actual content). Do not store raw content in the metadata document directly.
|
||||
2. **Setup and Recovery**:
|
||||
- **Fast Setup (Simple Fetch)** is the preferred flow for initial replication on secondary devices. It utilises stream-based replication for high speed and delays local file reflection to suppress temporary synchronisation warnings.
|
||||
- **Flag files** (such as `redflag.md`, `redflag2.md`, and `redflag3.md`) at the root of the vault control the boot-up sequence and trigger automated fetch/rebuild tasks.
|
||||
3. **Subrepositories**:
|
||||
- Treat `@vrtmrz/livesync-commonlib` as an external, authoritative package. Make Commonlib changes in its repository, validate the packed artefact and downstream LiveSync consumer, and update the exact dependency here. Do not recreate a `src/lib` source mirror or generated `_types` fallback.
|
||||
4. **Application Directories**:
|
||||
- The directory [src/apps](src/apps) contains independent application modules:
|
||||
- `cli`: A Command Line Interface application. Tests specifically for the CLI (both unit and End-to-End tests) are located and executed within [src/apps/cli](src/apps/cli) using its local `package.json` scripts.
|
||||
- `webapp`: A Web-based application.
|
||||
- `webpeer`: A Web-based peer utility.
|
||||
|
||||
---
|
||||
|
||||
## Development & Verification Commands
|
||||
|
||||
Before submitting code, you should run verification scripts locally to ensure correct syntax and function.
|
||||
|
||||
1. **Lint and Type Checking**:
|
||||
- Run `npm run check` to perform code verification. This runs type-checking (`tsc-check`), ESLint (`lint`), and Svelte checks (`svelte-check`).
|
||||
2. **Unit Tests**:
|
||||
- Run `npm run test:unit` to execute fast local unit tests.
|
||||
- Run `npm run test:unit:coverage` when unit-test coverage is required.
|
||||
- Run focused integration, CLI E2E, or real Obsidian E2E commands for the boundary being changed. Start only the Docker services required by that command.
|
||||
3. **Build**:
|
||||
- Run `npm run build` to compile the production bundle (`main.js`).
|
||||
- Run `npm run dev` for the development watch/build task.
|
||||
27
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Code of Conduct
|
||||
|
||||
We wish to maintain an open, welcoming, and collaborative environment for all contributors.
|
||||
|
||||
## Our Standard
|
||||
|
||||
Our core principle is mutual respect. We encourage open discussion, diverse perspectives, and constructive feedback.
|
||||
|
||||
## The Limit of Tolerance
|
||||
|
||||
To preserve a tolerant and open community, we do not tolerate intolerance. Actions that aim to harass, exclude, or silence others are not welcome. Specifically, we do not accept personal attacks, breaches of privacy, or sustained disruption of discussions. We prioritise protecting the community's capacity for open, peaceful collaboration.
|
||||
|
||||
## Resolution
|
||||
|
||||
If any issue arises, the project maintainers will resolve it in a fair, minimal, and constructive manner, aiming to restore a cooperative environment. Depending on the nature of the behaviour, actions may range from a simple warning to temporary or permanent suspension of repository access.
|
||||
|
||||
## Contact
|
||||
|
||||
You can contact the project maintainer via email at `vrtmrz@proton.me` or via Nostr at `npub1azzj0dzw8evwtgyjeucyfz5cs8k0eg7rd0x4qvggcg3s7lx0dmaqv9sfka`.
|
||||
|
||||
## Criticism of the Maintainer
|
||||
|
||||
To ensure open and transparent governance, criticism of the maintainer will not be deleted as long as it is clearly framed as a constructive objection. However, spamming duplicate issues on the same topic or resorting to personal attacks will result in closure or removal.
|
||||
|
||||
## Revisions
|
||||
|
||||
This Code of Conduct is maintained by the project maintainers and may be updated to address new challenges. While the final decision rests with the maintainers, we welcome constructive suggestions and feedback through issues or pull requests.
|
||||
67
CONTRIBUTING.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Contributing to Self-hosted LiveSync
|
||||
|
||||
Thank you for your interest in contributing to Self-hosted LiveSync! We welcome all contributions, including bug reports, feature requests, documentation improvements, translations, and pull requests.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To set up the development environment, please follow these steps:
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/vrtmrz/obsidian-livesync
|
||||
```
|
||||
|
||||
2. Install the package dependencies:
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
3. Build the plug-in:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
For a more comprehensive guide on development workflows, testing configurations, and the Commonlib dependency, please refer to [devs.md](devs.md).
|
||||
|
||||
## Guidelines for Contributions
|
||||
|
||||
### 1. Code Style and Verification
|
||||
|
||||
Before submitting a pull request, you must run verification scripts locally to ensure that there are no syntax, type, or linting errors:
|
||||
|
||||
- Run type checking and linting:
|
||||
```bash
|
||||
npm run check
|
||||
```
|
||||
This also type-checks the maintained CLI and browser applications, and applies the Community directory blocker rules. Run `npm run lint:community` separately to inspect its non-blocking recommendations.
|
||||
- Run unit tests:
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
If you have the capability and a suitable environment (such as Linux and Docker), running the CLI End-to-End (E2E) tests is also highly appreciated. Instructions are detailed in [devs.md](devs.md). If you cannot run E2E tests locally, please explicitly ask to run the tests on the CI by stating 'Please run CI tests' in your pull request description.
|
||||
|
||||
### 2. Documentation and UI Text Style
|
||||
|
||||
To maintain consistency across the project, we ask that you follow the established writing style and conventions of the codebase when contributing documentation or user-facing messages:
|
||||
|
||||
- **Spelling**: Prioritise region-independent, neutral spelling if a suitable word exists. If there is no such word, please use British English spelling to align with the codebase's style (for example: preferring '-ise' and '-isation' suffixes over '-ize' and '-ization'). However, we do not treat alternative spellings as errors.
|
||||
- **Oxford Comma**: Use the serial (Oxford) comma to separate items in lists of three or more (for example: 'settings, snippets, and themes').
|
||||
- **Logical Punctuation**: Place punctuation marks outside quotation marks unless they are part of the quoted text itself (for example: write 'dialogue', not 'dialogue,').
|
||||
- **No Contractions**: Avoid using contractions in general text or documentation (for example: write "do not" instead of "don't", and "cannot" instead of "can't").
|
||||
- **Affirmative Phrasing**: Avoid asking questions using negative forms in user-facing dialogue. Use affirmative questions to prevent translation and interpretation discrepancies.
|
||||
- **Specific Words**: Use 'dialogue' for documentation and user-facing messages (use 'dialog' only inside source code). Use the hyphenated form 'plug-in' in user-facing text (use 'plugin' only in configuration settings or technical contexts).
|
||||
|
||||
For a detailed list of vocabulary conventions and terms, please refer to [docs/terms.md](docs/terms.md).
|
||||
|
||||
### 3. Translations
|
||||
|
||||
To add or update translations, please refer to [docs/adding_translations.md](docs/adding_translations.md) for detailed instructions.
|
||||
|
||||
### 4. Commonlib changes
|
||||
|
||||
Shared synchronisation behaviour is provided by the `@vrtmrz/livesync-commonlib` package. If you wish to change that library, submit a separate pull request to the [livesync-commonlib repository](https://github.com/vrtmrz/livesync-commonlib), validate its packed artefact, then update the locked dependency in this repository. Do not add a source mirror or generated fallback declarations to this repository.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
73
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
Self-hosted LiveSync is a community-developed synchronisation plug-in available on all Obsidian-compatible platforms. It leverages robust server solutions such as CouchDB or object storage systems (e.g., MinIO, S3, R2, etc.) to ensure reliable data synchronisation.
|
||||
|
||||
Additionally, it supports peer-to-peer synchronisation using WebRTC now (experimental), enabling you to synchronise your notes directly between devices without relying on a server.
|
||||
Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling you to synchronise your notes directly between devices without relying on a server. Documentation is available for [Peer-to-Peer Synchronisation](./docs/p2p_sync_updates_2026.md).
|
||||
|
||||

|
||||
|
||||
@@ -18,24 +18,24 @@ Additionally, it supports peer-to-peer synchronisation using WebRTC now (experim
|
||||
- Use open-source solutions for the server.
|
||||
- Compatible solutions are supported.
|
||||
- Support end-to-end encryption.
|
||||
- Synchronise settings, snippets, themes, and plug-ins via [Customisation Sync (Beta)](docs/settings.md#6-customization-sync-advanced) or [Hidden File Sync](docs/settings.md#7-hidden-files-advanced).
|
||||
- Synchronise settings, snippets, themes, and plug-ins via [Customisation Sync (Beta)](docs/settings.md#6-customisation-sync-advanced) or [Hidden File Sync](docs/tips/hidden-file-sync.md).
|
||||
- Enable WebRTC peer-to-peer synchronisation without requiring a `host` (Experimental).
|
||||
- This feature is still in the experimental stage. Please exercise caution when using it.
|
||||
- WebRTC is a peer-to-peer synchronisation method, so **at least one device must be online to synchronise**.
|
||||
- Instead of keeping your device online as a stable peer, you can use two pseudo-peers:
|
||||
- [livesync-serverpeer](https://github.com/vrtmrz/livesync-serverpeer): A pseudo-client running on the server for receiving and sending data between devices.
|
||||
- [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer): A pseudo-client for receiving and sending data between devices.
|
||||
- A pre-built instance is available at [fancy-syncing.vrtmrz.net/webpeer](https://fancy-syncing.vrtmrz.net/webpeer/) (hosted on the vrtmrz blog site). This is also peer-to-peer. Feel free to use it.
|
||||
- [webpeer](https://github.com/vrtmrz/obsidian-livesync/tree/main/src/apps/webpeer): A pseudo-client for receiving and sending data between devices.
|
||||
- A pre-built instance is available at [fancy-syncing.vrtmrz.net/webpeer](https://fancy-syncing.vrtmrz.net/webpeer/) (hosted on the vrtmrz's blog site). This is also peer-to-peer. Feel free to use it.
|
||||
- For more information, refer to the [English explanatory article](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync-en.html) or the [Japanese explanatory article](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync).
|
||||
|
||||
This plug-in may be particularly useful for researchers, engineers, and developers who need to keep their notes fully self-hosted for security reasons. It is also suitable for anyone seeking the peace of mind that comes with knowing their notes remain entirely private.
|
||||
|
||||
>[!IMPORTANT]
|
||||
> - Before installing or upgrading this plug-in, please back up your vault.
|
||||
> - Do not enable this plug-in alongside another synchronisation solution at the same time (including iCloud and Obsidian Sync).
|
||||
> - Do not enable this plug-in alongside another synchronisation solution (including iCloud and Obsidian Sync).
|
||||
> - For backups, we also provide a plug-in called [Differential ZIP Backup](https://github.com/vrtmrz/diffzip).
|
||||
|
||||
## How to use
|
||||
## How to Use
|
||||
|
||||
### 3-minute setup - CouchDB on fly.io
|
||||
|
||||
@@ -43,60 +43,79 @@ This plug-in may be particularly useful for researchers, engineers, and develope
|
||||
|
||||
[](https://www.youtube.com/watch?v=7sa_I1832Xc)
|
||||
|
||||
1. [Setup CouchDB on fly.io](docs/setup_flyio.md)
|
||||
1. [Set up CouchDB on fly.io](docs/setup_flyio.md)
|
||||
2. Configure plug-in in [Quick Setup](docs/quick_setup.md)
|
||||
|
||||
### Manually Setup
|
||||
### Setup workflows
|
||||
|
||||
Choose a synchronisation method, prepare its server where required, then follow the corresponding client setup:
|
||||
|
||||
1. CouchDB
|
||||
1. Prepare the server:
|
||||
- [Set up your own CouchDB server](docs/setup_own_server.md).
|
||||
- [Set up CouchDB on fly.io](docs/setup_flyio.md).
|
||||
2. Configure the clients by following [CouchDB Quick Setup](docs/quick_setup.md).
|
||||
2. Object Storage
|
||||
1. Prepare the server. A maintained MinIO server installation guide is not currently available here, so set up an S3-compatible service or server of your choice.
|
||||
2. Configure the clients by following [Object Storage Setup](docs/setup_object_storage.md).
|
||||
3. Peer-to-Peer
|
||||
1. No server setup is required.
|
||||
2. Configure the clients by following [Peer-to-Peer Setup](docs/setup_p2p.md).
|
||||
|
||||
Each workflow establishes ordinary note synchronisation on the first device, generates the additional-device Setup URI from that working device, and verifies synchronisation in both directions.
|
||||
|
||||
1. Setup the server
|
||||
1. [Setup CouchDB on fly.io](docs/setup_flyio.md)
|
||||
2. [Setup your CouchDB](docs/setup_own_server.md)
|
||||
2. Configure plug-in in [Quick Setup](docs/quick_setup.md)
|
||||
> [!TIP]
|
||||
> Fly.io is no longer free. Fortunately, despite some issues, we can still use IBM Cloudant. Refer to [Setup IBM Cloudant](docs/setup_cloudant.md).
|
||||
> And also, we can use peer-to-peer synchronisation without a server. Or very cheap Object Storage -- Cloudflare R2 can be used for free.
|
||||
> HOWEVER, most importantly, we can use the server that we trust. Therefore, please set up your own server.
|
||||
> CouchDB can be run on a Raspberry Pi. (But please be careful about the security of your server).
|
||||
> Fly.io is no longer free. Fortunately, we can still use IBM Cloudant despite some limitations. Refer to [Set up IBM Cloudant](docs/setup_cloudant.md).
|
||||
> We can also use peer-to-peer synchronisation without a server. Alternatively, cheap object storage like Cloudflare R2 can be used for free.
|
||||
> However, most importantly, we can use a server that we trust. Therefore, please set up your own server.
|
||||
> CouchDB can also be run on a Raspberry Pi (please be mindful of your server's security).
|
||||
|
||||
|
||||
## Information in StatusBar
|
||||
## Information in the Status Bar
|
||||
|
||||
Synchronization status is shown in the status bar with the following icons.
|
||||
Synchronisation status is shown in the status bar with the following icons.
|
||||
|
||||
- Activity Indicator
|
||||
- 📲 Network request
|
||||
- 📲 A finite remote operation is in progress
|
||||
- 🌐N Approximate remote requests currently in progress
|
||||
- Status
|
||||
- ⏹️ Stopped
|
||||
- 💤 LiveSync enabled. Waiting for changes
|
||||
- ⚡️ Synchronization in progress
|
||||
- ⚡️ Synchronisation in progress
|
||||
- ⚠ An error occurred
|
||||
- Statistical indicator
|
||||
- Statistical Indicators
|
||||
- ↑ Uploaded chunks and metadata
|
||||
- ↓ Downloaded chunks and metadata
|
||||
- Progress indicator
|
||||
- Progress Indicators
|
||||
- 📥 Unprocessed transferred items
|
||||
- 📄 Working database operation
|
||||
- 💾 Working write storage processes
|
||||
- ⏳ Working read storage processes
|
||||
- 🛫 Pending read storage processes
|
||||
- 📬 Batched read storage processes
|
||||
- ⚙️ Working or pending storage processes of hidden files
|
||||
- ⚙️ Working or pending storage processes for hidden files
|
||||
- 🧩 Waiting chunks
|
||||
- 🔌 Working Customisation items (Configuration, snippets, and plug-ins)
|
||||
- 🔌 Working customisation items (configuration, snippets, and plug-ins)
|
||||
|
||||
To prevent file and database corruption, please wait to stop Obsidian until all progress indicators have disappeared as possible (The plugin will also try to resume, though). Especially in case of if you have deleted or renamed files.
|
||||
To prevent file and database corruption, please avoid closing Obsidian until all progress indicators have disappeared as much as possible (although the plug-in will attempt to resume if interrupted). This is especially important if you have deleted or renamed files.
|
||||
|
||||
## Tips and Troubleshooting
|
||||
If you are having problems getting the plugin working see: [Tips and Troubleshooting](docs/troubleshooting.md).
|
||||
- If you want a faster and simpler initial replication when setting up subsequent devices, see the [Fast Setup Guide](docs/tips/fast-setup.md).
|
||||
- Configure [Hidden File Sync](docs/tips/hidden-file-sync.md) only after ordinary note synchronisation works.
|
||||
- Self-hosted LiveSync 1.0 requires Obsidian 1.7.2 or later. If you need to use 1.0 on an earlier Obsidian version, please [open an issue](https://github.com/vrtmrz/obsidian-livesync/issues/new?template=issue-report.md) with your version, platform, and reason for remaining on it so that we can assess whether extending support is practical. The standard Community Plugins installer otherwise selects an older compatible plug-in release.
|
||||
- If you are having problems getting the plug-in working, see [Tips and Troubleshooting](docs/troubleshooting.md).
|
||||
|
||||
## Acknowledgements
|
||||
The project has been in continual progress and harmony thanks to:
|
||||
The project has been in continual progress and harmony thanks to the following:
|
||||
- Many [Contributors](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors).
|
||||
- Many [GitHub Sponsors](https://github.com/sponsors/vrtmrz#sponsors).
|
||||
- JetBrains Community Programs / Support for Open-Source Projects. <img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo" height="24">
|
||||
|
||||
May those who have contributed be honoured and remembered for their kindness and generosity.
|
||||
|
||||
## Development Guide
|
||||
Please refer to the [Development Guide](devs.md) for development setup, testing infrastructure, code conventions, and more.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the MIT License.
|
||||
|
||||
172
README_cn.md
@@ -1,9 +1,12 @@
|
||||
# Self-hosted LiveSync
|
||||
|
||||
Self-hosted LiveSync (自搭建在线同步) 是一个社区实现的在线同步插件。
|
||||
使用一个自搭建的或者购买的 CouchDB 作为中转服务器。兼容所有支持 Obsidian 的平台。
|
||||
它利用诸如CouchDB或对象存储系统(例如MinIO、S3、R2等)等强大的服务器解决方案,以确保数据同步的可靠性。。兼容所有支持 Obsidian 的平台。
|
||||
|
||||
注意: 本插件与官方的 "Obsidian Sync" 服务不兼容。
|
||||
此外,它现在支持使用WebRTC进行点对点同步(实验性功能),使您无需依赖服务器即可直接在设备之间同步笔记。
|
||||
|
||||
>[!IMPORTANT]
|
||||
>本插件与官方的 "Obsidian Sync" 服务不兼容。
|
||||
|
||||

|
||||
|
||||
@@ -11,119 +14,94 @@ Self-hosted LiveSync (自搭建在线同步) 是一个社区实现的在线同
|
||||
|
||||
## 功能
|
||||
|
||||
- 可视化的冲突解决器
|
||||
- 接近实时的多设备双向同步
|
||||
- 可使用 CouchDB 以及兼容的服务,如 IBM Cloudant
|
||||
- 支持端到端加密
|
||||
- 插件同步 (Beta)
|
||||
- 从 [obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) 接收 WebClip (本功能不适用端到端加密)
|
||||
- 以最少流量高效同步vault
|
||||
- 有效处理冲突的修改。
|
||||
- 自动合并简单冲突。
|
||||
- 服务端使用开源的解决方案
|
||||
- 支持兼容的解决方案。
|
||||
- 支持端到端加密。
|
||||
- 同步设置、代码片段、主题和插件,通过 [Customisation Sync (Beta)](docs/settings.md#6-customization-sync-advanced) 或者 [Hidden File Sync](docs/settings.md#7-hidden-files-advanced).
|
||||
- 启用 WebRTC 点对点同步,无需指定 `host`(实验性)。
|
||||
- 此功能仍处于试验阶段。请在使用时务必谨慎。
|
||||
- WebRTC 是一种点对点同步方法,因此**至少有一台设备必须在线才能进行同步**。
|
||||
- 与其让您的设备作为稳定的对等节点保持在线,您可以使用两个 pseudo-peers:
|
||||
- [livesync-serverpeer](https://github.com/vrtmrz/livesync-serverpeer): 在服务器上运行的 pseudo-client 用于在设备之间接收和发送数据。
|
||||
- [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer): 用于在设备之间接收和发送数据的pseudo-client。
|
||||
- 一个预构建的实例现已上线,地址为 [fancy-syncing.vrtmrz.net/webpeer](https://fancy-syncing.vrtmrz.net/webpeer/) (托管于vrtmrz博客网站). 这也是一个点对点的实例。可自由使用。
|
||||
- 欲了解更多信息,请参阅[英文说明文章](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync-en.html)或[日文说明文章](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync)。
|
||||
|
||||
适用于出于安全原因需要将笔记完全自托管的研究人员、工程师或开发人员,以及任何喜欢笔记完全私密所带来的安全感的人。
|
||||
此插件适用于出于安全原因需要将笔记完全自托管的研究人员、工程师或开发人员,以及任何喜欢笔记完全私密所带来的安全感的人。
|
||||
|
||||
## 重要提醒
|
||||
|
||||
- 请勿与其他同步解决方案(包括 iCloud、Obsidian Sync)一起使用。在启用此插件之前,请确保禁用所有其他同步方法以避免内容损坏或重复。如果要同步到多个服务,请一一进行,切勿同时启用两种同步方法。
|
||||
这包括不能将您的保管库放在云同步文件夹中(例如 iCloud 文件夹或 Dropbox 文件夹)
|
||||
- 这是一个同步插件,不是备份解决方案。不要依赖它进行备份。
|
||||
- 如果设备的存储空间耗尽,可能会发生数据库损坏。
|
||||
- 隐藏文件或任何其他不可见文件不会保存在数据库中,因此不会被同步。(**并且可能会被删除**)
|
||||
>[!IMPORTANT]
|
||||
> - 在安装或升级此插件之前,请务必备份您的保险库。
|
||||
> - 请勿同时启用此插件与其它同步方案(包括iCloud和Obsidian Sync)。
|
||||
> - 对于备份,我们还提供了一款名为[Differential ZIP Backup](https://github.com/vrtmrz/diffzip)的插件。
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 准备好你的数据库
|
||||
### 3分钟搞定——在fly.io上部署CouchDB
|
||||
|
||||
首先,准备好你的数据库。IBM Cloudant 是用于测试的首选。或者,您也可以在自己的服务器上安装 CouchDB。有关更多信息,请参阅以下内容:
|
||||
1. [Setup IBM Cloudant](docs/setup_cloudant.md)
|
||||
2. [Setup your CouchDB](docs/setup_own_server_cn.md)
|
||||
**推荐初学者第一次使用此方法**
|
||||
[](https://www.youtube.com/watch?v=7sa_I1832Xc)
|
||||
|
||||
Note: 正在征集更多搭建方法!目前在讨论的有 [使用 fly.io](https://github.com/vrtmrz/obsidian-livesync/discussions/85)。
|
||||
1. [Setup CouchDB on fly.io](docs/setup_flyio.md)
|
||||
2. 在 [Quick Setup](docs/quick_setup.md) 中配置插件。
|
||||
|
||||
### 第一个设备
|
||||
### 手动设置
|
||||
|
||||
1. 在您的设备上安装插件。
|
||||
2. 配置远程数据库信息。
|
||||
1. 将您的服务器信息填写到 `Remote Database configuration`(远程数据库配置)设置页中。
|
||||
2. 建议启用 `End to End Encryption`(端到端加密)。输入密码后,单击“应用”。
|
||||
3. 点击 `Test Database Connection` 并确保插件显示 `Connected to (你的数据库名称)`。
|
||||
4. 单击 `Check database configuration`(检查数据库配置)并确保所有测试均已通过。
|
||||
3. 在 `Sync Settings`(同步设置)选项卡中配置何时进行同步。(您也可以稍后再设置)
|
||||
1. 如果要实时同步,请启用 `LiveSync`。
|
||||
2. 或者,根据您的需要设置同步方式。默认情况下,不会启用任何自动同步,这意味着您需要手动触发同步过程。
|
||||
3. 其他配置也在这里。建议启用 `Use Trash for deleted files`(删除文件到回收站),但您也可以保持所有配置不变。
|
||||
4. 配置杂项功能。
|
||||
1. 启用 `Show staus inside editor` 会在编辑器右上角显示状态。(推荐开启)
|
||||
5. 回到编辑器。等待初始扫描完成。
|
||||
6. 当状态不再变化并显示 ⏹️ 图标表示 COMPLETED(没有 ⏳ 和 🧩 图标)时,您就可以与服务器同步了。
|
||||
7. 按功能区上的复制图标或从命令面板运行 `Replicate now`(立刻复制)。这会将您的所有数据发送到服务器。
|
||||
8. 打开命令面板,运行 `Copy setup URI`(复制设置链接),并设置密码。这会将您的配置导出到剪贴板,作为您导入其他设备的链接。
|
||||
1. 配置服务器
|
||||
1. [在fly.io上快速搭建CouchDB](docs/setup_flyio.md)
|
||||
2. [自行搭建CouchDB](docs/setup_own_server.md)
|
||||
2. 在[快速设置](docs/quick_setup.md)中配置插件
|
||||
|
||||
> [!提示]
|
||||
> Fly.io现已不再免费。不过,尽管存在一些问题,我们仍可使用IBM Cloudant。请参考[搭建IBM Cloudant](docs/setup_cloudant.md)。
|
||||
> 此外,我们还可以采用点对点同步方式,无需搭建服务器;或者选用价格极低的对象存储——Cloudflare R2可免费使用。
|
||||
> 但最重要的是,我们可以选择自己信任的服务器。因此,建议您搭建自有服务器
|
||||
> CouchDB可在树莓派上运行。(但请务必注意服务器的安全性)。
|
||||
|
||||
**重要: 不要公开本链接,这个链接包含了你的所有认证信息!** (即使没有密码别人读不了)
|
||||
|
||||
### 后续设备
|
||||
|
||||
注意:如果要与非空的 vault 进行同步,文件的修改日期和时间必须互相匹配。否则,可能会发生额外的传输或文件可能会损坏。
|
||||
为简单起见,我们强烈建议同步到一个全空的 vault。
|
||||
## 状态栏中的信息
|
||||
|
||||
1. 安装插件。
|
||||
2. 打开您从第一台设备导出的链接。
|
||||
3. 插件会询问您是否确定应用配置。 回答 `Yes`,然后按照以下说明进行操作:
|
||||
1. 对 `Keep local DB?` 回答 `Yes`。
|
||||
*注意:如果您希望保留本地现有 vault,则必须对此问题回答 `No`,并对 `Rebuild the database?` 回答 `No`。*
|
||||
2. 对 `Keep remote DB?` 回答 `Yes`。
|
||||
3. 对 `Replicate once?` 回答 `Yes`。
|
||||
完成后,您的所有设置将会从第一台设备成功导入。
|
||||
4. 你的笔记应该很快就会同步。
|
||||
|
||||
## 文件看起来有损坏...
|
||||
|
||||
请再次打开配置链接并回答如下:
|
||||
- 如果您的本地数据库看起来已损坏(当你的本地 Obsidian 文件看起来很奇怪)
|
||||
- 对 `Keep local DB?` 回答 `No`
|
||||
- 如果您的远程数据库看起来已损坏(当复制时发生中断)
|
||||
- 对 `Keep remote DB?` 回答 `No`
|
||||
|
||||
如果您对两者都回答“否”,您的数据库将根据您设备上的内容重建。并且远程数据库将锁定其他设备,您必须再次同步所有设备。(此时,几乎所有文件都会与时间戳同步。因此您可以安全地使用现有的 vault)。
|
||||
|
||||
## 测试服务器
|
||||
|
||||
设置 Cloudant 或本地 CouchDB 实例有点复杂,所以我搭建了一个 [self-hosted-livesync 尝鲜服务器](https://olstaste.vrtmrz.net/)。欢迎免费尝试!
|
||||
注意:请仔细阅读“限制”条目。不要发送您的私人 vault。
|
||||
|
||||
## 状态栏信息
|
||||
|
||||
同步状态将显示在状态栏。
|
||||
同步状态显示在状态栏中,采用以下图标。
|
||||
|
||||
- 活动指示器
|
||||
- 📲 网络请求
|
||||
- 状态
|
||||
- ⏹️ 就绪
|
||||
- 💤 LiveSync 已启用,正在等待更改。
|
||||
- ⚡️ 同步中。
|
||||
- ⚠ 一个错误出现了。
|
||||
- ↑ 上传的 chunk 和元数据数量
|
||||
- ↓ 下载的 chunk 和元数据数量
|
||||
- ⏳ 等待的过程的数量
|
||||
- 🧩 正在等待 chunk 的文件数量
|
||||
如果你删除或更名了文件,请等待 ⏳ 图标消失。
|
||||
- ⏹️ 已停止
|
||||
- 💤 LiveSync已启用,正在等待更改
|
||||
- ⚡️ 同步中
|
||||
- ⚠ 发生了错误
|
||||
- 统计指标
|
||||
- ↑ 上传的分块与元数据
|
||||
- ↓ 下载的分块与元数据
|
||||
- 进度指示器
|
||||
- 📥 未处理的传输项
|
||||
- 📄 正在进行的数据库操作
|
||||
- 💾 正在进行的写入存储进程
|
||||
- ⏳ 正在进行的读取存储进程
|
||||
- 🛫 待处理的读取存储进程
|
||||
- 📬 批量处理的读取存储进程
|
||||
- ⚙️ 正在进行或待处理的隐藏文件存储进程
|
||||
- 🧩 等待中的分块
|
||||
- 🔌 正在进行的自定义项(配置、代码片段和插件)
|
||||
|
||||
为避免文件和数据库损坏,请等待所有进度指示器尽可能消失后再关闭 Obsidian(插件也会尝试恢复同步进度)。特别是在您已删除或重命名文件的情况下,请务必遵守此操作。
|
||||
|
||||
|
||||
## 提示
|
||||
|
||||
- 如果文件夹在复制后变为空,则默认情况下该文件夹会被删除。您可以关闭此行为。检查 [设置](docs/settings.md)。
|
||||
- LiveSync 模式在移动设备上可能导致耗电量增加。建议使用定期同步 + 条件自动同步。
|
||||
- 移动平台上的 Obsidian 无法连接到非安全 (HTTP) 或本地签名的服务器,即使设备上安装了根证书。
|
||||
- 没有类似“exclude_folders”的配置。
|
||||
- 同步时,文件按修改时间进行比较,较旧的将被较新的文件覆盖。然后插件检查冲突,如果需要合并,将打开一个对话框。
|
||||
- 数据库中的文件在罕见情况下可能会损坏。当接收到的文件看起来已损坏时,插件不会将其写入本地存储。如果您的设备上有文件的本地版本,则可以通过编辑本地文件并进行同步来覆盖损坏的版本。但是,如果您的任何设备上都不存在该文件,则无法挽救该文件。在这种情况下,您可以从设置对话框中删除这些损坏的文件。
|
||||
- 要阻止插件的启动流程(例如,为了修复数据库问题),您可以在 vault 的根目录创建一个 "redflag.md" 文件。
|
||||
- 问:数据库在增长,我该如何缩小它?
|
||||
答:每个文档都保存了过去 100 次修订,用于检测和解决冲突。想象一台设备已经离线一段时间,然后再次上线。设备必须将其笔记与远程保存的笔记进行比较。如果存在曾经相同的历史修订,则可以安全地直接更新这个文件(和 git 的快进原理一样)。即使文件不在修订历史中,我们也只需检查两个设备上该文件的公有修订版本之后的差异。这就像 git 的冲突解决方法。所以,如果想从根本上解决数据库太大的问题,我们像构建一个扩大版的 git repo 一样去重新设计数据库。
|
||||
- 更多技术信息在 [技术信息](docs/tech_info.md)
|
||||
- 如果你想在没有黑曜石的情况下同步文件,你可以使用[filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync)。
|
||||
- WebClipper 也可在 Chrome Web Store 上使用:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||
## 使用技巧与故障排除
|
||||
如果您在配置插件时遇到问题,请参阅:[Tips and Troubleshooting](docs/troubleshooting.md).
|
||||
|
||||
|
||||
仓库地址:[obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip) (文档施工中)
|
||||
## 致谢
|
||||
本项目得以持续顺利推进,离不开以下各方的贡献:
|
||||
- 众多[贡献者](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors)。
|
||||
- 许多[GitHub 赞助人](https://github.com/sponsors/vrtmrz#sponsors)。
|
||||
- JetBrains 社区计划/对开源项目的支持。<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo" height="24">
|
||||
|
||||
## License
|
||||
愿所有作出贡献的人士因其善良与慷慨而受到尊敬与铭记。
|
||||
|
||||
The source code is licensed under the MIT License.
|
||||
本源代码使用 MIT 协议授权。
|
||||
## 许可协议
|
||||
|
||||
本项目采用 MIT 许可协议授权。
|
||||
|
||||
@@ -78,7 +78,8 @@ NDAや類似の契約や義務、倫理を守る必要のある、研究者、
|
||||
|
||||
|
||||
## Tips and Troubleshooting
|
||||
何かこまったら、[Tips and Troubleshooting](docs/troubleshooting.md)をご参照ください。
|
||||
- 2台目以降のセットアップ時に、初期同期をより迅速かつ簡単に行うには、[ファストセットアップガイド](docs/tips/fast-setup_ja.md)をご参照ください。
|
||||
- 何かこまったら、[Tips and Troubleshooting](docs/troubleshooting.md)をご参照ください。
|
||||
|
||||
## License
|
||||
|
||||
|
||||
12
_tools/bakei18n.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { allMessages } from "../src/common/messages/combinedMessages.dev.ts";
|
||||
import path from "path";
|
||||
const __dirname = import.meta.dirname;
|
||||
const currentPath = __dirname;
|
||||
const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts");
|
||||
|
||||
console.log(`Writing to ${outDir}`);
|
||||
writeFileSync(
|
||||
outDir,
|
||||
`export const allMessages: Readonly<Record<string, Readonly<Record<string, string>>>> = ${JSON.stringify(allMessages, null, 4)};`
|
||||
);
|
||||
58
_tools/checkI18nCoverage.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { glob } from "tinyglobby";
|
||||
import { parse } from "yaml";
|
||||
import { objectToDotted } from "./messagelib.ts";
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/"));
|
||||
const files = (await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir })).sort();
|
||||
|
||||
function flattenMessages(src: Record<string, unknown>) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(objectToDotted(src))
|
||||
.map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value] as const)
|
||||
.filter(([, value]) => typeof value === "string")
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
) as Record<string, string>;
|
||||
}
|
||||
|
||||
const localeData = new Map<string, Record<string, string>>();
|
||||
for (const file of files) {
|
||||
const segments = file.split(/[/\\]/);
|
||||
const locale = segments[segments.length - 1]!.replace(/\.yaml$/, "");
|
||||
const content = await readFile(file, "utf-8");
|
||||
localeData.set(locale, flattenMessages(parse(content) ?? {}));
|
||||
}
|
||||
|
||||
const baseLocale = "en";
|
||||
const base = localeData.get(baseLocale);
|
||||
if (!base) {
|
||||
throw new Error("en.yaml not found");
|
||||
}
|
||||
|
||||
const baseKeys = Object.keys(base);
|
||||
const report = Object.fromEntries(
|
||||
[...localeData.entries()].map(([locale, data]) => {
|
||||
const keys = new Set(Object.keys(data));
|
||||
const missing = baseKeys.filter((key) => !keys.has(key));
|
||||
const identicalToEnglish = baseKeys.filter(
|
||||
(key) => keys.has(key) && locale !== baseLocale && data[key] === base[key]
|
||||
);
|
||||
const translated = baseKeys.length - missing.length;
|
||||
return [
|
||||
locale,
|
||||
{
|
||||
totalBaseKeys: baseKeys.length,
|
||||
translatedKeys: translated,
|
||||
missingKeys: missing.length,
|
||||
identicalToEnglishCount: identicalToEnglish.length,
|
||||
coverage: `${translated}/${baseKeys.length}`,
|
||||
missing,
|
||||
identicalToEnglish,
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
55
_tools/decompileRosetta.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../src/common/rosetta";
|
||||
import { allMessages } from "../src/common/messages/combinedMessages.dev.ts";
|
||||
|
||||
import path from "path";
|
||||
const thisFileDir = __dirname;
|
||||
const outDir = path.join(thisFileDir, "i18n");
|
||||
|
||||
const out = {} as Record<string, { [key: string]: string | undefined }>;
|
||||
|
||||
for (const [key, value] of Object.entries(allMessages)) {
|
||||
for (const lang of [...SUPPORTED_I18N_LANGS, "def"]) {
|
||||
if (!out[lang]) out[lang] = {};
|
||||
if (lang in value) {
|
||||
out[lang][key] = value[lang as I18N_LANGS];
|
||||
} else {
|
||||
if (lang === "def") {
|
||||
out[lang][key] = key;
|
||||
} else {
|
||||
out[lang][key] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [lang, value] of Object.entries(out)) {
|
||||
const filename = `${lang}.ts`;
|
||||
const escapeString = (prefix: string, key: string, str: string) => {
|
||||
if (str.indexOf("\n") !== -1) {
|
||||
const encoded = JSON.stringify(str);
|
||||
const lineWrapped = encoded.split("\\n").join("\\\n" + prefix);
|
||||
|
||||
return `${prefix}${JSON.stringify(key)}: ${lineWrapped},`;
|
||||
}
|
||||
return `${prefix}${JSON.stringify(key)}: ${JSON.stringify(str)},`;
|
||||
};
|
||||
// const z ="a" "b" "c";
|
||||
const _stringify = (value: Record<string, string | undefined>) => {
|
||||
let res = "{\n";
|
||||
for (const key of Object.keys(value)) {
|
||||
const v = value[key];
|
||||
if (v) {
|
||||
res += escapeString("", key, v) + "\n";
|
||||
} else {
|
||||
res += escapeString("// ", key, out["def"]?.[key] ?? "") + "\n";
|
||||
}
|
||||
}
|
||||
return res + "\n}";
|
||||
};
|
||||
void writeFileSync(
|
||||
path.join(outDir, filename),
|
||||
`export const PartialMessages ={\n "${lang}":${_stringify(value)}\n} as const;`
|
||||
);
|
||||
}
|
||||
30
_tools/decompileRosettaToJson.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
import { allMessages } from "../src/common/messages/combinedMessages.prod.ts";
|
||||
const __dirname = import.meta.dirname;
|
||||
import path from "path";
|
||||
const thisFileDir = __dirname;
|
||||
const outDir = path.resolve(thisFileDir, "../src/common/messagesJson");
|
||||
|
||||
const out = {} as Record<string, { [key: string]: string | undefined }>;
|
||||
|
||||
for (const [key, value] of Object.entries(allMessages)) {
|
||||
//@ts-ignore
|
||||
for (const [lang, langValue] of Object.entries(allMessages[key])) {
|
||||
if (!out[lang]) out[lang] = {};
|
||||
if (lang in value) {
|
||||
out[lang][key] = langValue as string;
|
||||
} else {
|
||||
if (lang === "def") {
|
||||
out[lang][key] = key;
|
||||
} else {
|
||||
out[lang][key] = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [lang, value] of Object.entries(out)) {
|
||||
const filename = `${lang}.json`;
|
||||
void writeFileSync(path.join(outDir, filename), JSON.stringify(value, null, 4));
|
||||
}
|
||||
27
_tools/json2yaml.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// Convert Application convenient Message Resources (JSON) to Human-Editable format (YAML)
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { stringify } from "yaml";
|
||||
import { glob } from "tinyglobby";
|
||||
import { dottedToObject } from "./messagelib";
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const targetDir = resolve(join(__dirname, "../src/common/messagesJson/"));
|
||||
console.log(`Target directory: ${targetDir}`);
|
||||
const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir });
|
||||
for (const file of files) {
|
||||
const filePath = resolve(file);
|
||||
console.log(`Processing file: ${filePath}`);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const jsonDataSrc = JSON.parse(content);
|
||||
const jsonDataD2 = Object.fromEntries(
|
||||
Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
||||
);
|
||||
const jsonData = dottedToObject(jsonDataD2);
|
||||
const yamlData = stringify(jsonData, { indent: 2 });
|
||||
const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML");
|
||||
await writeFile(yamlFilePath, yamlData, "utf-8");
|
||||
console.log(`Converted ${filePath} to ${yamlFilePath}`);
|
||||
}
|
||||
|
||||
// console.dir(files, { depth: 0 });
|
||||
38
_tools/messagelib.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export function objectToDotted(obj: any, prefix = ""): Record<string, any> {
|
||||
return Object.entries(obj).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
Object.assign(acc, objectToDotted(value, newKey));
|
||||
} else {
|
||||
acc[newKey] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
}
|
||||
export function dottedToObject(obj: Record<string, any>): Record<string, any> {
|
||||
return Object.entries(obj).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key.includes(" ")) {
|
||||
// Return as is.
|
||||
return { ...acc, [key]: value }; // Skip keys with spaces
|
||||
}
|
||||
const keys = key.split(".");
|
||||
keys.reduce((nestedAcc, currKey, index) => {
|
||||
if (currKey in nestedAcc && typeof nestedAcc[currKey] !== "object") {
|
||||
nestedAcc[currKey] = { _value: nestedAcc[currKey] }; // Convert to object if not already
|
||||
}
|
||||
if (index === keys.length - 1) {
|
||||
nestedAcc[currKey] = value;
|
||||
} else {
|
||||
nestedAcc[currKey] = nestedAcc[currKey] || {};
|
||||
}
|
||||
return nestedAcc[currKey];
|
||||
}, acc);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
}
|
||||
29
_tools/yaml2json.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Convert Human-Editable format (YAML) to Application convenient Message Resources (JSON)
|
||||
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { parse } from "yaml";
|
||||
import { glob } from "tinyglobby";
|
||||
import { objectToDotted } from "./messagelib";
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/"));
|
||||
console.log(`Target directory: ${targetDir}`);
|
||||
const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir });
|
||||
for (const file of files) {
|
||||
const filePath = resolve(file);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const jsonDataSrc = parse(content);
|
||||
const jsonDataD2 = objectToDotted(jsonDataSrc);
|
||||
const jsonData = Object.fromEntries(
|
||||
Object.entries(jsonDataD2)
|
||||
.map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value])
|
||||
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
||||
);
|
||||
const yamlData = JSON.stringify(jsonData, null, 4) + "\n";
|
||||
const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json");
|
||||
await writeFile(yamlFilePath, yamlData, "utf-8");
|
||||
console.log(`Converted ${filePath} to ${yamlFilePath}`);
|
||||
}
|
||||
|
||||
// console.dir(files, { depth: 0 });
|
||||
92
aggregator.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Self-hosted LiveSync Setup QR Aggregator</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #f4f4f9; color: #333; }
|
||||
.container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); text-align: center; max-width: 90%; }
|
||||
.progress { margin: 20px 0; font-size: 1.2rem; font-weight: bold; }
|
||||
.status { margin-bottom: 20px; color: #666; }
|
||||
.btn { display: inline-block; padding: 12px 24px; background-color: #7c4dff; color: white; text-decoration: none; border-radius: 4px; font-weight: bold; transition: background-color 0.2s; border: none; cursor: pointer; }
|
||||
.btn:hover { background-color: #651fff; }
|
||||
.btn:disabled { background-color: #ccc; cursor: not-allowed; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(40px, 1fr)); gap: 8px; margin: 20px 0; }
|
||||
.tile { width: 40px; height: 40px; border: 2px solid #ddd; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; }
|
||||
.tile.filled { background-color: #7c4dff; color: white; border-color: #7c4dff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>LiveSync Setup</h1>
|
||||
<div id="app">
|
||||
<p>Checking hash data...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateUI() {
|
||||
const hash = window.location.hash.substring(1);
|
||||
const params = new URLSearchParams(hash);
|
||||
|
||||
const id = params.get('id');
|
||||
const total = parseInt(params.get('n') || '0');
|
||||
const index = parseInt(params.get('i') || '-1');
|
||||
const data = params.get('d');
|
||||
|
||||
const app = document.getElementById('app');
|
||||
|
||||
if (!id || total <= 0 || index === -1 || !data) {
|
||||
app.innerHTML = '<p class="status">Invalid setup URL. Please scan the QR code correctly.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get session data
|
||||
const storageKey = 'ls_agg_' + id;
|
||||
let session = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||
|
||||
// Save current data
|
||||
session[index] = data;
|
||||
localStorage.setItem(storageKey, JSON.stringify(session));
|
||||
|
||||
const receivedIndexes = Object.keys(session).map(Number);
|
||||
const count = receivedIndexes.length;
|
||||
|
||||
let html = `
|
||||
<div class="status">Session ID: ${id}</div>
|
||||
<div class="progress">${count} / ${total} Loaded</div>
|
||||
<div class="grid">
|
||||
`;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const isFilled = session[i] !== undefined;
|
||||
html += `<div class="tile ${isFilled ? 'filled' : ''}">${i + 1}</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
if (count === total) {
|
||||
const sortedData = Array.from({length: total}, (_, i) => session[i]).join('');
|
||||
// Use the correct protocol for settings
|
||||
const obsidianUri = `obsidian://setuplivesync?settingsQR=${sortedData}`;
|
||||
|
||||
html += `
|
||||
<p>All parts have been collected!</p>
|
||||
<a href="${obsidianUri}" class="btn">Open Obsidian to complete setup</a>
|
||||
<p style="margin-top:20px; font-size:0.8rem; color: #999;">Note: If the button does not respond, please ensure you are opening this in a browser that can trigger Obsidian.</p>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<p class="status">Please scan the next QR code.</p>
|
||||
<button class="btn" disabled>Waiting...</button>
|
||||
`;
|
||||
}
|
||||
|
||||
app.innerHTML = html;
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', updateUI);
|
||||
updateUI();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
308
devs.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# Self-hosted LiveSync Development Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
Self-hosted LiveSync is an Obsidian plugin for synchronising vaults across devices using CouchDB, MinIO/S3, or peer-to-peer WebRTC. The codebase uses a modular architecture with TypeScript, Svelte, and PouchDB.
|
||||
|
||||
## Build & Development Workflow
|
||||
|
||||
### Environment Setup
|
||||
|
||||
#### First-time Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vrtmrz/obsidian-livesync
|
||||
cd obsidian-livesync
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### Branch switching
|
||||
|
||||
When switching branches, reinstall dependencies when the lockfile changes.
|
||||
|
||||
```bash
|
||||
git checkout 0.25.70-patch1 # tag or branch name
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
npm run test:unit # Run unit tests with vitest (or `npm run test:unit:coverage` for coverage)
|
||||
npm run check # TypeScript and svelte type checking
|
||||
npm run dev # Development build with auto-rebuild (uses .env for test vault paths)
|
||||
npm run build # Production build
|
||||
npm run buildDev # Development build (one-time)
|
||||
npm run test:integration # Run CouchDB-backed integration tests
|
||||
npm run test:setup-tools # Check provisioning and Setup URI package contracts
|
||||
npm run test:e2e:cli:p2p # Run canonical P2P validation in Compose
|
||||
npm run test:e2e:obsidian:local-suite # Run the real Obsidian local suite
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
Select the narrowest unit, integration, CLI E2E, or real Obsidian E2E command that owns the behaviour being changed. The obsolete mocked browser Harness has been retired.
|
||||
|
||||
### Unreleased change notes
|
||||
|
||||
Keep changes that may belong in a future release under `## Unreleased` at the top of `updates.md` when they do not justify an immediate release. Do not add a date to this virtual version. Move relevant entries under the real version and ordinal release date when preparing that release, then leave an empty `## Unreleased` section for subsequent work.
|
||||
|
||||
Use this section for durable release-note candidates, including compatibility-relevant internal maintenance, rather than tasks, local diagnostics, or implementation journals. Categorise user-visible behaviour separately from internal changes and testing.
|
||||
|
||||
### Auto-copy to test vaults
|
||||
|
||||
To facilitate development and testing, the build process can automatically copy the built plugin to specified test vault
|
||||
|
||||
- Create `.env` file with `PATHS_TEST_INSTALL` pointing to test vault plug-in directories (`:` separated on Unix, `;` on Windows)
|
||||
- Development builds auto-copy to these paths on build whilst `npm run dev` is running (watch mode)
|
||||
|
||||
### Testing Infrastructure
|
||||
|
||||
- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~
|
||||
- This is now obsolete, migrated to vitest.
|
||||
- **Vitest**:
|
||||
- **Unit Tests** (`vitest.config.unit.ts`): Unit tests run in Node.js (excluding harnesses and integration tests). Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`). Executed via `npm run test:unit`.
|
||||
- **Integration Tests** (`vitest.config.integration.ts`): Tests run in Node.js against a real CouchDB instance. Integration tests should be `*.integration.spec.ts` or `*.integration.test.ts` and placed alongside the implementation file (e.g., `StreamingFetch.integration.spec.ts`). Executed via `npm run test:integration`.
|
||||
- If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you strongly expected to write an integration test to verify the behaviour against a real CouchDB server.
|
||||
- **Commonlib Tests**: Commonlib owns unit and package tests for shared RPC, storage, replication, and platform contracts. LiveSync CI verifies the exact packed dependency as a downstream consumer.
|
||||
- **CLI E2E** (`src/apps/cli/testdeno/`): Host-independent consumer workflows. The canonical Compose P2P suite covers ordinary two-peer synchronisation, replacement of the current replicator followed by transfer with the same peer, and explicit relay disconnection followed by paused and resumed reconnection. Its lifecycle entry point is included only in the Docker test build and does not add a public CLI command. Run `npm run test:e2e:cli` for the ordinary suite or `npm run test:e2e:cli:p2p` for P2P validation.
|
||||
- **Self-hosted setup tools** (`utils/couchdb/`, `utils/setup/`, and `utils/flyio/`): Deno contract tests consume the exact locked Commonlib registry package, verify current CouchDB, Object Storage, and random-room P2P Setup URI defaults and remote profiles, and keep CouchDB administration separate from package-owned LiveSync database-version negotiation. `unit-ci` also provisions a real temporary CouchDB database and verifies its version document against the installed Commonlib package. Run `npm run test:setup-tools` for the local contract gate.
|
||||
- **Real Obsidian E2E** (`test/e2e-obsidian/`): Local-first scripts that launch real Obsidian with temporary vaults and the built Self-hosted LiveSync plug-in. Use these for boot-up sequence, vault reflection, RedFlag flows, Fast Setup (Simple Fetch), settings dialogues, restart-sensitive workflows, Object Storage regressions, and other behaviour that depends on Obsidian itself. Run focused scripts such as `npm run test:e2e:obsidian:two-vault-sync`, or use `npm run test:e2e:obsidian:local-suite:services` to run the broader local suite with CouchDB and MinIO fixtures managed by the wrapper.
|
||||
|
||||
- **Docker Services**: Service-backed tests use CouchDB and MinIO (S3). Canonical P2P validation owns its relay through the CLI Compose runner:
|
||||
|
||||
```bash
|
||||
npm run test:docker-all:start # Start all test services
|
||||
npm run test:integration # Run the relevant service-backed suite
|
||||
npm run test:docker-all:stop # Stop services
|
||||
```
|
||||
|
||||
If some services are not needed, start only required ones (e.g., `test:docker-couchdb:start`).
|
||||
Note that if services are already running, starting script will fail. Please stop them first.
|
||||
|
||||
- **Test Structure**:
|
||||
- `test/e2e-obsidian/` - Real Obsidian E2E scripts for local verification
|
||||
- co-located `*.unit.spec.ts` files - Node-based unit tests
|
||||
- co-located `*.integration.spec.ts` files - service-backed integration tests
|
||||
- `src/apps/webapp/obsidianMock.ts` - Webapp-only Obsidian compatibility adapter; it is not an E2E Harness
|
||||
|
||||
### Import Path Normalisation
|
||||
|
||||
The codebase uses the `@/` alias for source owned by this repository. Commonlib imports use explicit `@vrtmrz/livesync-commonlib` package subpaths. To normalise LiveSync-owned imports and exports, use the following utility script:
|
||||
|
||||
```bash
|
||||
npm run pretty:importpath
|
||||
```
|
||||
|
||||
Under the hood, this runs Deno with the script [utilsdeno/normalise-imports.ts](utilsdeno/normalise-imports.ts). You can pass additional flags to this script if required (by running it via Deno directly from the `utilsdeno` directory):
|
||||
|
||||
- `--run`: Applies the changes (the script runs in dry-run mode by default).
|
||||
- `--all-alias`: Normalises sibling/child relative imports starting with `./` to use aliases.
|
||||
|
||||
### Commonlib dependency
|
||||
|
||||
Shared synchronisation code is compiled and typed by the `@vrtmrz/livesync-commonlib` package. `npm ci` installs the exact artefact recorded by the lockfile; this repository does not compile Commonlib source or commit fallback declarations.
|
||||
|
||||
Changes spanning both repositories must first produce a packed Commonlib artefact which passes its standalone package checks. Install that exact artefact in LiveSync, then run the LiveSync type checks, unit tests, application builds, CLI E2E, and any focused real-Obsidian E2E required by the changed boundary. Replace the temporary artefact reference with the reviewed immutable package version before release.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Module System
|
||||
|
||||
The plugin uses a dynamic module system to reduce coupling and improve maintainability:
|
||||
|
||||
- **Service Hub**: Central registry for services using dependency injection
|
||||
- Services are registered, and accessed via `this.services` (in most modules)
|
||||
- **Module Loading**: All modules extend `AbstractModule` or `AbstractObsidianModule` (which extends `AbstractModule`). These modules are loaded in main.ts and some modules.
|
||||
- **Module Categories** (by directory):
|
||||
- `core/` - Platform-independent core functionality
|
||||
- `coreObsidian/` - Obsidian-specific core (e.g., `ModuleFileAccessObsidian`)
|
||||
- `essential/` - Required modules (e.g., `ModuleMigration`, `ModuleKeyValueDB`)
|
||||
- `features/` - Optional features (e.g., `ModuleLog`, `ModuleObsidianSettings`)
|
||||
- `extras/` - Development/testing tools (e.g., `ModuleDev`, ~~`ModuleIntegratedTest`~~)
|
||||
- **Services**: Core services (e.g., `database`, `replicator`, `storageAccess`) are registered in `ServiceHub` and accessed by modules. They provide an extension point for add new behaviour without modifying existing code.
|
||||
- For example, checks before the replication can be added to the `replication.onBeforeReplicate` handler, and the handlers can be return `false` to prevent replication-starting. `vault.isTargetFile` also can be used to prevent processing specific files.
|
||||
- **ServiceModule**: A new type of module that directly depends on services.
|
||||
|
||||
#### Note on Module vs Service
|
||||
|
||||
After v0.25.44 refactoring, the Service will henceforth, as a rule, cease to use setHandler, that is to say, simple lazy binding. - They will be implemented directly in the service. - However, not everything will be middlewarised. Modules that maintain state or make decisions based on the results of multiple handlers are permitted.
|
||||
|
||||
Hence, the new feature should be implemented as follows:
|
||||
|
||||
- If it is a simple extension point (e.g., adding a check before replication), it should be implemented as a handler in the service (e.g., `replication.onBeforeReplicate`).
|
||||
- If it requires maintaining state or making decisions based on multiple handlers, it should be implemented as a serviceModule dependent on the relevant services explicitly.
|
||||
- If you have to implement a new feature without much modification, you can extent existing modules, but it is recommended to implement a new module or serviceModule for better maintainability.
|
||||
- Refactoring existing modules to services is also always welcome!
|
||||
- Please write tests for new features, you will notice that the simple handler approach is quite testable.
|
||||
|
||||
### Key Architectural Components
|
||||
|
||||
- **LiveSyncLocalDB** (`@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB`): Local PouchDB database wrapper
|
||||
- **Replicators** (`@vrtmrz/livesync-commonlib/compat/replication/*`): CouchDB, Journal, and P2P synchronisation engines
|
||||
- **Service Hub** (`src/modules/services/`): Central service registry using dependency injection
|
||||
- **Common Library** (`@vrtmrz/livesync-commonlib`): Platform-independent synchronisation logic, shared with the CLI, Webapp, WebPeer, and external tools
|
||||
|
||||
Commonlib owns the P2P replicator and Trystero transport lifecycle. Host commands, event handlers, and views must retain the Commonlib service-feature result and resolve its current `replicator` at the point of use. They must not snapshot an instance which can be replaced when settings or the local database change, close Trystero-owned raw peers, or install another Trystero transport generation at the application root.
|
||||
|
||||
### Conflict Merge Policy
|
||||
|
||||
Markdown conflict auto-merge should behave like a conservative three-way merge. The guiding rule is to merge changes when they touch non-overlapping regions, and to keep a manual conflict when the edits overlap semantically.
|
||||
|
||||
When in doubt, prefer the safer outcome: preserve data, keep the conflict visible, and ask the user rather than silently discarding content or choosing one side.
|
||||
|
||||
The detailed contract is documented in [Conflict resolution and revision provenance](docs/specs_conflict_resolution.md). Determine the merge base by intersecting the exact `available` revision IDs from both leaf histories and selecting the nearest shared revision. Do not infer ancestry from revision generation numbers. When a remote resolution reaches a Vault which still contains the exact content of a deleted losing branch, treat that content as known synchronised history so the resolution can be reflected without recreating the conflict.
|
||||
|
||||
File operations made while a conflict is active must use the device-local file-reflection provenance injected into `ServiceFileHandlerBase`. Treat its exact revision as authoritative; use byte equality only to reconstruct a missing record when exactly one available revision matches. If branch identity remains unknown, preserve data and leave the conflict visible. Do not hide key-value database readiness behind an implicit wait: maintained hosts open it through the sequential settings lifecycle before file events or replication begin.
|
||||
|
||||
- If one side deletes a line and the other side leaves that same line unchanged, treat it as a safe deletion. The deleted line must not be reintroduced into the merged result.
|
||||
- If one side inserts new content in a different region while the other side deletes an unchanged old region, preserve the insertion and the deletion.
|
||||
- If one side deletes a line and the other side modifies that same line, keep the conflict for user resolution.
|
||||
- If both sides insert different content at the same position, keep both insertions in a deterministic order unless the surrounding deletion context indicates that they are competing replacements.
|
||||
- Avoid resolving conflicts by simply choosing the newest revision unless the user has explicitly selected that behaviour.
|
||||
|
||||
This policy is intentionally aligned with the conflict checkboxes and compatibility settings: automatic merge should remove avoidable prompts, but it must not silently choose between overlapping user intentions.
|
||||
|
||||
### File Structure Conventions
|
||||
|
||||
- **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild)
|
||||
- **Development code**: Use `.dev.ts` suffix (replaced with `.prod.ts` in production)
|
||||
- **Path aliases**: `@/*` maps to `src/*`; Commonlib uses package exports rather than a source alias
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Internationalisation (i18n)
|
||||
|
||||
- **Translation Workflow**:
|
||||
1. Edit the human-readable YAML files in this repository under `src/common/messagesYAML/`
|
||||
2. Run `npm run i18n:bake` to compile YAML → JSON → TypeScript constants
|
||||
3. Use `$t()`, `$msg()` functions for translations
|
||||
You can also use `$f` for formatted messages with Tagged Template Literals.
|
||||
- **Usage**:
|
||||
```typescript
|
||||
$msg("dialog.someKey"); // Typed key with autocomplete
|
||||
$t("Some message"); // Direct translation
|
||||
$f`Hello, ${userName}`; // Formatted message
|
||||
```
|
||||
- **Supported languages**: `def` (English), `de`, `es`, `fr`, `he`, `ja`, `ko`, `ru`, `zh`, `zh-tw`
|
||||
|
||||
Commonlib owns the typed English fallback for messages requested by its services. LiveSync owns the multilingual application catalogue and injects its translator into the Obsidian, CLI, and browser service compositions. Adding a Commonlib message therefore requires its canonical English definition in Commonlib; LiveSync may provide translations here, while an untranslated key falls back to Commonlib English. Importing a Commonlib language catalogue is not part of the boundary.
|
||||
|
||||
### File Path Handling
|
||||
|
||||
- Use tagged types from `types.ts`: `FilePath`, `FilePathWithPrefix`, `DocumentID`
|
||||
- Prefix constants: `CHeader` (chunks), `ICHeader`/`ICHeaderEnd` (internal data)
|
||||
- Path utilities are supplied by the focused Commonlib compatibility path `@vrtmrz/livesync-commonlib/compat/string_and_binary/path`
|
||||
|
||||
### Logging & Debugging
|
||||
|
||||
- Use `this._log(msg, LOG_LEVEL_INFO)` in modules (automatically prefixes with module name)
|
||||
- Log levels: `LOG_LEVEL_DEBUG`, `LOG_LEVEL_VERBOSE`, `LOG_LEVEL_INFO`, `LOG_LEVEL_NOTICE`, `LOG_LEVEL_URGENT`
|
||||
- LOG_LEVEL_NOTICE and above are reported to the user via Obsidian notices
|
||||
- LOG_LEVEL_DEBUG is for debug only and not shown in default builds
|
||||
- Dev mode creates `ls-debug/` folder in `.obsidian/` for debug outputs (e.g., missing translations)
|
||||
- This causes pretty significant performance overhead.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Module Implementation (Now not recommended for new features, use services instead)
|
||||
|
||||
```typescript
|
||||
export class ModuleExample extends AbstractObsidianModule {
|
||||
async _everyOnloadStart(): Promise<boolean> {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Management
|
||||
|
||||
- Settings are defined by Commonlib (`ObsidianLiveSyncSettings`)
|
||||
- Configuration metadata is supplied by the Commonlib settings exports
|
||||
- Use `this.services.setting.saveSettingData()` instead of using plugin methods directly
|
||||
|
||||
### Database Operations
|
||||
|
||||
- Local database operations through `LiveSyncLocalDB` (wraps PouchDB)
|
||||
- Document types: `EntryDoc` (files), `EntryLeaf` (chunks), `PluginDataEntry` (plugin sync)
|
||||
|
||||
## Important Files
|
||||
|
||||
- [main.ts](src/main.ts) - Plugin entry point, module registration
|
||||
- [esbuild.config.mjs](esbuild.config.mjs) - Build configuration with platform/dev file replacement
|
||||
- [package.json](package.json) - Scripts reference and dependencies
|
||||
|
||||
## Pre-release Policy
|
||||
|
||||
- Use SemVer beta identifiers such as `1.0.0-beta.0` for immutable integration previews. Increment the beta number when a published preview needs a correction. Reserve `1.0.0-rc.0` for the first feature- and contract-frozen release candidate. Historical `-patchedN` releases remain unchanged in the release history.
|
||||
- Publish a pre-release from an immutable reviewed tag, mark its GitHub Release as a pre-release, and do not replace the latest stable release.
|
||||
- A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag.
|
||||
- Keep the release pull request in draft until the exact published plug-in has passed BRAT validation. If validation fails, prepare the next pre-release version rather than moving the existing tag.
|
||||
|
||||
## Release Notes
|
||||
|
||||
- Keep the top section of `updates.md` as `## Unreleased` during normal development.
|
||||
- When opening a feature or fix PR, update `## Unreleased` in the same PR if the change is user-facing.
|
||||
- Add only user-facing changes that help users understand what they gain, what has changed, or what they may need to do after updating.
|
||||
- Avoid listing purely internal refactors, maintenance chores, generated-file changes, and dependency updates unless they affect users; group and label them when they are included.
|
||||
- When preparing a release, replace `## Unreleased` with the target version heading (for example, `## 0.25.81`) and add a fresh empty `## Unreleased` section above it for the next cycle.
|
||||
- Review and polish the released section in the release PR before tagging, because the content is embedded into the plug-in and may be reused as the GitHub Release notes.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
This workflow is for maintainers. Contributors should update `## Unreleased` for user-facing feature or fix PRs, but do not need to run the release workflows.
|
||||
The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `release` GitHub Environment. Configure Environment protection in the repository settings so tag creation and release publication require maintainer approval.
|
||||
|
||||
- Run the `Prepare Release PR` workflow with the target version and selected base branch. It creates the release branch, updates versions, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. The base branch may already select the target development version; the workflow still runs the version lifecycle so that release-only metadata such as `versions.json` is recorded in the release commit.
|
||||
- Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`.
|
||||
- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that the plug-in tag points to that commit, optionally creates the corresponding CLI tag, and dispatches the plug-in release workflow. A CLI tag starts its own container workflow. The finalisation workflow can be retried when existing tags already point to the reviewed commit, but stops if a selected tag points elsewhere.
|
||||
- The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. The CLI Docker workflow retains its documented branch, tag, and manual triggers.
|
||||
- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. For a selected CLI publication, confirm the image tags appropriate to a stable or pre-release version.
|
||||
- Publish a stable draft as the latest release, or publish a pre-release draft without replacing the latest stable release. In either case, keep the release PR in draft and leave its base branch unchanged until BRAT validation succeeds. Record that state in the PR.
|
||||
- Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release.
|
||||
- After BRAT validation succeeds, mark the release PR ready and merge it into the selected base branch with a merge commit. This keeps the tagged release commit in that branch's history.
|
||||
- If BRAT validation fails, keep the release PR in draft and do not move published tags. Before preparing the next version, add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`. Keep only changes made after that tag under `## Unreleased`. Compare the historical section with `git show <tag>:updates.md`; do not merge the failed release PR or describe it as validated. The next release PR can then rotate only the correction notes while preserving the immutable release history.
|
||||
- Prepare and publish the next patch or pre-release version from that reconciled base. Leave the failed release PR draft until it is deliberately closed as superseded under a separate maintainer action.
|
||||
- For a pre-release, set `prerelease=true` in `Finalise Release Tags`. A hyphenated version is rejected unless that input is enabled.
|
||||
|
||||
### Release Cheat Sheet
|
||||
|
||||
1. Before starting, add user-facing notes under `## Unreleased` in `updates.md`.
|
||||
2. Run `Prepare Release PR` from GitHub Actions.
|
||||
- `version`: the target version, for example `0.25.81`.
|
||||
- `base_branch`: normally `main`, or the reviewed integration branch for an integration preview.
|
||||
- `release_branch`: leave blank to use the default branch name, for example `0_25_81`.
|
||||
- `release_date`: use an ordinal date such as `14th July, 2026`, or leave blank to use the current UTC date.
|
||||
- `allow_empty_updates`: leave disabled unless the release intentionally has no user-facing notes.
|
||||
3. Review the generated draft PR.
|
||||
- Polish `updates.md`.
|
||||
- Confirm `package.json`, `manifest.json`, `versions.json`, workspace package versions, and the locked Commonlib package version.
|
||||
- Confirm that `manifest.json` has the intended `minAppVersion`.
|
||||
- Wait for the necessary CI checks.
|
||||
4. When the PR head is fixed, run `Finalise Release Tags`.
|
||||
- `version`: the same target version.
|
||||
- `release_branch`: leave blank unless the release branch used a custom name.
|
||||
- `expected_head_sha`: the full head commit SHA reviewed in the release PR.
|
||||
- `prerelease`: enable for a version such as `1.0.0-rc.0`.
|
||||
- `publish_cli`: disable when the reviewed release is plug-in-only.
|
||||
5. Approve the `Release Obsidian Plugin` workflow for the `release` environment, then check the generated draft GitHub Release.
|
||||
6. If CLI publication was selected, confirm that the CLI tag event published the expected image tags.
|
||||
7. Publish the draft as a stable release or pre-release as selected, but keep the release PR in draft and leave its base branch unchanged.
|
||||
8. Update the PR state message to describe the published release and state that merging remains on hold until BRAT validation is complete.
|
||||
9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario.
|
||||
10. After BRAT validation succeeds, mark the release PR ready and merge it into the selected base branch with a merge commit.
|
||||
11. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
- Follow existing code style and conventions
|
||||
- Write integration tests (`*.integration.spec.ts` or `*.integration.test.ts`) when adding or modifying features that interact with the remote database, and ensure that they pass in the CI workflow.
|
||||
- Please bump dependencies with care, check artifacts after updates, with diff-tools and only expected changes in the build output (to avoid unexpected vulnerabilities).
|
||||
- When adding new features, please consider it has an OSS implementation, and avoid using proprietary services or APIs that may limit usage.
|
||||
- For example, any functionality to connect to a new type of server is expected to either have an OSS implementation available for that server, or to be managed under some responsibilities and/or limitations without disrupting existing functionality, and scope for surveillance reduced by some means (e.g., by client-side encryption, auditing the server ourselves).
|
||||
@@ -1,7 +1,6 @@
|
||||
# For details and other explanations about this file refer to:
|
||||
# https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/setup_own_server.md#traefik
|
||||
|
||||
version: "2.1"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
|
||||
52
docker/.env.example
Normal file
@@ -0,0 +1,52 @@
|
||||
# Self-hosted LiveSync — Environment Variables
|
||||
# Copy this file to .env and fill in your values.
|
||||
# NEVER commit .env to version control.
|
||||
|
||||
# =============================================================================
|
||||
# REQUIRED — CouchDB credentials
|
||||
# =============================================================================
|
||||
|
||||
# Admin username for CouchDB
|
||||
COUCHDB_USER=admin
|
||||
|
||||
# Admin password — use a strong random password (min 16 chars recommended)
|
||||
COUCHDB_PASSWORD=change_me_use_a_strong_password
|
||||
|
||||
# Name of the database the Obsidian plugin will use
|
||||
COUCHDB_DATABASE=obsidiannotes
|
||||
|
||||
# Host port CouchDB is exposed on (default: 5984)
|
||||
# For tunnel-only deployments you can set this to 127.0.0.1:5984 to block external access
|
||||
COUCHDB_PORT=5984
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: caddy (--profile caddy)
|
||||
# =============================================================================
|
||||
|
||||
# Your public domain pointing to this server (A record)
|
||||
# Example: couchdb.yourdomain.com
|
||||
COUCHDB_DOMAIN=couchdb.yourdomain.com
|
||||
|
||||
# Email for Let's Encrypt TLS certificate notifications
|
||||
ACME_EMAIL=you@yourdomain.com
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: tailscale (--profile tailscale)
|
||||
# =============================================================================
|
||||
|
||||
# Tailscale OAuth key (not a regular auth key — must be OAuth for persistent use)
|
||||
# Generate at: https://login.tailscale.com/admin/settings/oauth
|
||||
# Scopes needed: devices:write
|
||||
TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Hostname this node will have on your tailnet (becomes <hostname>.<tailnet>.ts.net)
|
||||
TS_HOSTNAME=livesync
|
||||
|
||||
# =============================================================================
|
||||
# PROFILE: cloudflare (--profile cloudflare)
|
||||
# =============================================================================
|
||||
|
||||
# Tunnel token from Cloudflare Zero Trust dashboard
|
||||
# Create at: https://one.dash.cloudflare.com/ → Networks → Tunnels → Create tunnel
|
||||
# Copy the token from the "Install connector" step
|
||||
CF_TUNNEL_TOKEN=eyJhIjoixxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
349
docker/README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Self-hosted LiveSync — Docker Setup
|
||||
|
||||
A fully self-hosted CouchDB stack for the [obsidian-livesync](https://github.com/vrtmrz/obsidian-livesync) plugin.
|
||||
**No fly.io. No IBM Cloudant. No cloud accounts required for basic use.**
|
||||
|
||||
> ✅ **Tested on Docker Desktop for Windows (Docker 29.2, Compose v5, WSL2 backend)** — full init, CORS, auth, and idempotent restart verified.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Obsidian (desktop / iOS / Android)
|
||||
│ CouchDB Replication Protocol (HTTPS)
|
||||
▼
|
||||
[ Reverse Proxy / Tunnel ] ◄── Choose ONE profile below
|
||||
│
|
||||
▼
|
||||
[ CouchDB container ] ◄── The only required service
|
||||
│ initialized once by couchdb-init container
|
||||
▼
|
||||
[ Named Docker Volume ] ◄── All vault data stored here
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- [Docker Desktop](https://docs.docker.com/desktop/) (Windows/Mac/Linux) or Docker Engine + Compose plugin
|
||||
- A machine that Obsidian devices can reach over HTTPS (see profiles below)
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
cp .env.example .env
|
||||
# Edit .env — at minimum set COUCHDB_USER and a strong COUCHDB_PASSWORD
|
||||
```
|
||||
|
||||
### 3. Launch
|
||||
|
||||
```bash
|
||||
# Default: CouchDB only (LAN / localhost, no TLS)
|
||||
docker compose up -d
|
||||
|
||||
# With Caddy (public domain + auto Let's Encrypt)
|
||||
docker compose --profile caddy up -d
|
||||
|
||||
# With Tailscale (no domain needed, private mesh or public Funnel)
|
||||
docker compose --profile tailscale up -d
|
||||
|
||||
# With Cloudflare Tunnel (Cloudflare account required)
|
||||
docker compose --profile cloudflare up -d
|
||||
```
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
# Should return {"status":"ok"}
|
||||
curl -u admin:yourpassword http://localhost:5984/_up
|
||||
|
||||
# Check CORS headers
|
||||
curl -v -H "Origin: app://obsidian.md" \
|
||||
-u admin:yourpassword \
|
||||
http://localhost:5984/
|
||||
```
|
||||
|
||||
### 5. Connect Obsidian
|
||||
|
||||
In the Obsidian plugin settings (**Self-hosted LiveSync**):
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| URI | `https://your-domain-or-ts-hostname:5984` (or `http://localhost:5984` for LAN-only) |
|
||||
| Username | value of `COUCHDB_USER` |
|
||||
| Password | value of `COUCHDB_PASSWORD` |
|
||||
| Database name | value of `COUCHDB_DATABASE` (default: `obsidiannotes`) |
|
||||
| End-to-end passphrase | *your own chosen passphrase — never stored server-side* |
|
||||
|
||||
---
|
||||
|
||||
## Profile Details
|
||||
|
||||
### Default (no profile) — LAN / Localhost only
|
||||
|
||||
CouchDB is exposed on `http://localhost:5984` (or LAN IP).
|
||||
**Desktop Obsidian works over HTTP.** Mobile Obsidian requires HTTPS — use a tunnel profile.
|
||||
|
||||
### `--profile caddy` — Public Domain + Auto TLS
|
||||
|
||||
**Requires**:
|
||||
- A domain with an A record pointing to this server's public IP
|
||||
- Ports 80 and 443 open in your firewall/router
|
||||
|
||||
**Set in `.env`**:
|
||||
```
|
||||
COUCHDB_DOMAIN=couchdb.yourdomain.com
|
||||
ACME_EMAIL=you@example.com
|
||||
```
|
||||
|
||||
Caddy automatically issues a Let's Encrypt certificate. No manual cert management.
|
||||
|
||||
### `--profile tailscale` — No Domain Required ✅ Recommended for privacy
|
||||
|
||||
**Requires**:
|
||||
- Free [Tailscale account](https://login.tailscale.com/)
|
||||
- Install the Tailscale app on all your Obsidian devices
|
||||
- Generate an **OAuth key** at: https://login.tailscale.com/admin/settings/oauth
|
||||
(Scopes: `devices:write`)
|
||||
|
||||
**Set in `.env`**:
|
||||
```
|
||||
TS_AUTHKEY=tskey-auth-...
|
||||
TS_HOSTNAME=livesync
|
||||
```
|
||||
|
||||
**Two sub-modes**:
|
||||
- **VPN mode** (default): CouchDB accessible only to devices on your Tailnet at
|
||||
`https://livesync.<tailnet>.ts.net` — completely private
|
||||
- **Funnel mode**: public HTTPS at `https://livesync.<tailnet>.ts.net` — no domain purchase
|
||||
Enable in your [Tailscale ACL](https://login.tailscale.com/admin/acls):
|
||||
```json
|
||||
"nodeAttrs": [{"target": ["tag:container"], "attr": ["funnel"]}]
|
||||
```
|
||||
|
||||
> **Note on Windows Docker Desktop**: If `/dev/net/tun` is unavailable, add `TS_USERSPACE=true`
|
||||
> to the tailscale service environment in `docker-compose.yml`.
|
||||
|
||||
### `--profile cloudflare` — Cloudflare Tunnel
|
||||
|
||||
**Requires**:
|
||||
- Free [Cloudflare account](https://www.cloudflare.com/)
|
||||
- A domain managed by Cloudflare DNS (can transfer existing domain for free)
|
||||
- Cloudflare Zero Trust account (free)
|
||||
|
||||
#### Step 1: Create a Cloudflare Tunnel
|
||||
|
||||
1. Log in to [Cloudflare Zero Trust](https://one.dash.cloudflare.com/)
|
||||
2. Navigate to **Networks → Tunnels**
|
||||
3. Click **Create a tunnel**
|
||||
4. Choose **Cloudflared** as tunnel type
|
||||
5. Name your tunnel (e.g., `obsidian-livesync`)
|
||||
6. Click **Save tunnel**
|
||||
7. **Copy the tunnel token** — it looks like `eyJhIjoiZX...` (very long, ~400 characters)
|
||||
|
||||
#### Step 2: Configure Environment
|
||||
|
||||
Edit `docker/.env`:
|
||||
```env
|
||||
CF_TUNNEL_TOKEN=eyJhIjoiZX... # Paste the full token from Step 1
|
||||
COUCHDB_DOMAIN=sync.yourdomain.com # Must be a domain managed by Cloudflare
|
||||
```
|
||||
|
||||
#### Step 3: Add Public Hostname Route
|
||||
|
||||
🚨 **CRITICAL**: Token-based tunnels ignore the local `cloudflared.yml` config file. All routing is controlled from the dashboard.
|
||||
|
||||
Back in the Zero Trust dashboard, **in the same tunnel creation flow** (or edit your tunnel later):
|
||||
|
||||
1. Go to the **Public Hostname** tab
|
||||
2. Click **Add a public hostname**
|
||||
3. Configure:
|
||||
- **Subdomain**: `sync` (or your preferred subdomain)
|
||||
- **Domain**: Select your Cloudflare domain from dropdown
|
||||
- **Type**: `HTTP`
|
||||
- **URL**: `couchdb:5984` ← **Do NOT use `localhost`!**
|
||||
|
||||
**Why `couchdb:5984` not `localhost:5984`?**
|
||||
- The `cloudflared` container runs inside Docker on the same network as `couchdb`
|
||||
- Docker's internal DNS resolves `couchdb` to the correct container
|
||||
- Using `localhost` would look inside the `cloudflared` container (nothing there)
|
||||
|
||||
4. Under **Additional application settings** (expand):
|
||||
- **No TLS Verify**: Leave **OFF** (CouchDB uses plain HTTP internally, that's fine)
|
||||
- Leave other settings at defaults
|
||||
5. Click **Save hostname**
|
||||
|
||||
#### Step 4: Start the Stack
|
||||
|
||||
```bash
|
||||
cd docker/
|
||||
docker compose --profile cloudflare up -d
|
||||
```
|
||||
|
||||
Verify containers are running:
|
||||
```bash
|
||||
docker ps --filter "name=livesync"
|
||||
```
|
||||
|
||||
You should see:
|
||||
- `livesync-couchdb` — Status: Up (healthy)
|
||||
- `livesync-cloudflared` — Status: Up
|
||||
- `livesync-init` — Status: Exited (0)
|
||||
|
||||
#### Step 5: Test the Connection
|
||||
|
||||
```bash
|
||||
# Should return 401 Unauthorized (proves CouchDB auth is working)
|
||||
curl -I https://sync.yourdomain.com
|
||||
|
||||
# Should return {"couchdb":"Welcome",...}
|
||||
curl -u admin:yourpassword https://sync.yourdomain.com
|
||||
```
|
||||
|
||||
If you get **404**, see Troubleshooting below.
|
||||
|
||||
#### Step 6: Configure Obsidian Plugin
|
||||
|
||||
In Obsidian → Settings → **Self-hosted LiveSync**:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| URI | `https://sync.yourdomain.com` |
|
||||
| Username | value of `COUCHDB_USER` from `.env` |
|
||||
| Password | value of `COUCHDB_PASSWORD` from `.env` |
|
||||
| Database name | value of `COUCHDB_DATABASE` from `.env` (default: `obsidiannotes`) |
|
||||
| End-to-end passphrase | *Choose your own* — never stored server-side |
|
||||
|
||||
Under **Remote Database Configuration → Advanced**:
|
||||
- Enable: ✅ **Use Request API to avoid inevitable CORS problem**
|
||||
(See "Known Issue" below for why this is critical)
|
||||
|
||||
---
|
||||
|
||||
#### 🔧 Troubleshooting Cloudflare Tunnel
|
||||
|
||||
**Problem: 404 Error / Cloud flare Generic Error Page**
|
||||
|
||||
**Diagnosis**:
|
||||
```bash
|
||||
# Check if cloudflared is running
|
||||
docker logs livesync-cloudflared --tail 20
|
||||
|
||||
# Look for: "Registered tunnel connection"
|
||||
# If you see the connector ID, the tunnel is connected but routing is wrong
|
||||
```
|
||||
|
||||
**Fix**: The public hostname rule is missing or incorrect.
|
||||
|
||||
1. Go to Zero Trust → Networks → Tunnels → your tunnel → **Edit**
|
||||
2. Click **Public Hostname** tab
|
||||
3. Verify a hostname exists with:
|
||||
- Service Type: `HTTP`
|
||||
- URL: `couchdb:5984` (NOT `localhost:5984`)
|
||||
4. If no hostname exists, add it (see Step 3 above)
|
||||
5. Wait 30 seconds for changes to propagate, then test again
|
||||
|
||||
**Problem: Connection immediately closes / 502 Bad Gateway**
|
||||
|
||||
**Diagnosis**: CouchDB is not healthy or not on the same Docker network as cloudflared.
|
||||
|
||||
```bash
|
||||
docker ps --filter "name=livesync-couchdb"
|
||||
# Status should be: Up (healthy)
|
||||
|
||||
docker inspect livesync-couchdb -f '{{.NetworkSettings.Networks}}'
|
||||
# Should show: livesync-net
|
||||
|
||||
docker inspect livesync-cloudflared -f '{{.NetworkSettings.Networks}}'
|
||||
# Should also show: livesync-net
|
||||
```
|
||||
|
||||
**Fix**: If CouchDB is unhealthy, check logs:
|
||||
```bash
|
||||
docker logs livesync-couchdb --tail 50
|
||||
```
|
||||
|
||||
**Problem: 524 Timeout Errors During Sync**
|
||||
|
||||
**Root cause**: Cloudflare's proxy has a **100-second idle timeout**. CouchDB's replication protocol uses long-polling on the `_changes` feed, which can idle for longer during quiet periods.
|
||||
|
||||
**Fix**: Switch to short-polling mode in the Obsidian plugin:
|
||||
1. Obsidian → Settings → Self-hosted LiveSync
|
||||
2. **Remote Database Configuration → Advanced**
|
||||
3. Enable: ✅ **Use Request API to avoid inevitable CORS problem**
|
||||
4. Save and restart sync
|
||||
|
||||
This keeps all requests under 100 seconds.
|
||||
|
||||
**Alternative**: Use Tailscale or Caddy profiles instead — neither has aggressive timeouts.
|
||||
|
||||
---
|
||||
|
||||
## Data & Backup
|
||||
|
||||
All vault data lives in the `couchdb-data` Docker named volume.
|
||||
|
||||
```bash
|
||||
# Backup
|
||||
docker run --rm -v obsidian-livesync_couchdb-data:/data \
|
||||
-v $(pwd)/backup:/backup alpine \
|
||||
tar czf /backup/couchdb-backup-$(date +%Y%m%d).tar.gz -C /data .
|
||||
|
||||
# Restore
|
||||
docker run --rm -v obsidian-livesync_couchdb-data:/data \
|
||||
-v $(pwd)/backup:/backup alpine \
|
||||
tar xzf /backup/couchdb-backup-20260218.tar.gz -C /data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- CouchDB requires authentication for **all** requests (configured by `livesync.ini`)
|
||||
- Enable **End-to-End Encryption** passphrase in the Obsidian plugin — vault data is
|
||||
encrypted before it ever leaves your device
|
||||
- The init container runs once and exits — it has no persistent access
|
||||
- Never expose CouchDB's admin interface (`/_utils`) to the public internet;
|
||||
use a firewall rule or the path-based obfuscation trick from
|
||||
[self-hosted-livesync-server](https://github.com/vrtmrz/self-hosted-livesync-server)
|
||||
|
||||
---
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker compose logs -f couchdb
|
||||
docker compose logs couchdb-init
|
||||
|
||||
# Re-run init (e.g. after changing credentials)
|
||||
docker compose restart couchdb-init
|
||||
|
||||
# Stop without removing data
|
||||
docker compose down
|
||||
|
||||
# Stop AND remove all data volumes (DESTRUCTIVE)
|
||||
docker compose down -v
|
||||
|
||||
# Open CouchDB admin UI (Fauxton) in browser
|
||||
open http://localhost:5984/_utils
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---|---|
|
||||
| Init container keeps restarting | CouchDB not healthy yet — wait 30s, check `docker compose logs couchdb` |
|
||||
| `curl: (52) Empty reply` | CouchDB not fully started — the healthcheck should gate this |
|
||||
| Mobile can't connect | Needs HTTPS — use tailscale or caddy profile |
|
||||
| 524 errors with Cloudflare | Enable "Use Request API" toggle in Obsidian plugin |
|
||||
| `Permission denied` on volumes | Run `docker compose down -v` and retry — first-run volume ownership issue |
|
||||
| CORS errors in browser | Confirm CouchDB headers: `curl -v -H "Origin: app://obsidian.md" http://localhost:5984/` |
|
||||
| CouchDB exits immediately, zero logs (Windows) | **Do not add `:ro`** to the `livesync.ini` volume mount. CouchDB's entrypoint runs `chmod 0644` on all files in `/opt/couchdb/etc` — read-only bind mounts cause a silent EPERM crash on Docker Desktop for Windows (WSL2). The compose file is already correct; do not modify it. |
|
||||
| Settings in `livesync.ini` seem ignored | Settings requiring restart (e.g. bind_address) load at start. Runtime-only settings (require_valid_user, enable_cors) are set by the init container via REST API and take effect immediately without restart. |
|
||||
26
docker/config/Caddyfile
Normal file
@@ -0,0 +1,26 @@
|
||||
# Caddy config for Self-hosted LiveSync CouchDB
|
||||
# =============================================================================
|
||||
# IMPORTANT: CouchDB handles CORS itself.
|
||||
# Do NOT add CORS headers here — they will conflict with CouchDB's own headers.
|
||||
# Do NOT intercept OPTIONS requests.
|
||||
# =============================================================================
|
||||
{
|
||||
# Email used for Let's Encrypt certificate notifications
|
||||
email {$ACME_EMAIL}
|
||||
}
|
||||
|
||||
{$COUCHDB_DOMAIN} {
|
||||
# Forward all traffic to CouchDB, preserving Host and forwarded-for headers
|
||||
reverse_proxy couchdb:5984 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-For {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# Logging
|
||||
log {
|
||||
output stdout
|
||||
level WARN
|
||||
}
|
||||
}
|
||||
}
|
||||
31
docker/config/cloudflared.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# cloudflared tunnel configuration for Self-hosted LiveSync
|
||||
# =============================================================================
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Create a tunnel in Cloudflare Zero Trust → Networks → Tunnels
|
||||
# 2. Copy the tunnel token to CF_TUNNEL_TOKEN in your .env
|
||||
# 3. Add a public hostname in the tunnel config:
|
||||
# Hostname : couchdb.yourdomain.com (or whatever you set COUCHDB_DOMAIN to)
|
||||
# Service : http://couchdb:5984
|
||||
#
|
||||
# Known issue: Cloudflare's 100-second proxy timeout can interrupt CouchDB's
|
||||
# long-polling replication change feed, causing 524 errors.
|
||||
# MITIGATION: In the Obsidian plugin settings, enable:
|
||||
# "Use Request API to avoid inevitable CORS problem"
|
||||
# This switches from long-poll to short-poll mode.
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
tunnel: ${CF_TUNNEL_ID}
|
||||
credentials-file: /etc/cloudflared/credentials.json
|
||||
|
||||
ingress:
|
||||
- hostname: ${COUCHDB_DOMAIN}
|
||||
service: http://couchdb:5984
|
||||
originRequest:
|
||||
# Increase timeouts for CouchDB replication streams
|
||||
connectTimeout: 30s
|
||||
keepAliveTimeout: 90s
|
||||
keepAliveConnections: 100
|
||||
noTLSVerify: false
|
||||
- service: http_status:404
|
||||
30
docker/config/livesync.ini
Normal file
@@ -0,0 +1,30 @@
|
||||
; CouchDB local configuration for Self-hosted LiveSync
|
||||
; This file is volume-mounted into /opt/couchdb/etc/local.d/livesync.ini
|
||||
;
|
||||
; IMPORTANT: Do NOT set require_valid_user here.
|
||||
; CouchDB needs to start without auth to complete its first-run cluster setup
|
||||
; (_users, _replicator databases must be created first).
|
||||
; The couchdb-init service applies auth lockdown via REST API after first-run.
|
||||
|
||||
[couchdb]
|
||||
; Max size per document (50MB). Large enough for binary attachments.
|
||||
max_document_size = 50000000
|
||||
|
||||
[chttpd]
|
||||
; Bind on all interfaces.
|
||||
bind_address = 0.0.0.0
|
||||
port = 5984
|
||||
; 4 GB max request (handles very large vaults)
|
||||
max_http_request_size = 4294967296
|
||||
|
||||
[httpd]
|
||||
WWW-Authenticate = Basic realm="couchdb"
|
||||
|
||||
[cors]
|
||||
; These are the exact app origins Obsidian uses on desktop + mobile
|
||||
credentials = true
|
||||
origins = app://obsidian.md,capacitor://localhost,http://localhost
|
||||
|
||||
[log]
|
||||
; Reduce noise in Docker logs — set to "debug" if troubleshooting
|
||||
level = warning
|
||||
19
docker/config/ts-serve.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"TCP": {
|
||||
"443": {
|
||||
"HTTPS": true
|
||||
}
|
||||
},
|
||||
"Web": {
|
||||
"${TS_CERT_DOMAIN}:443": {
|
||||
"Handlers": {
|
||||
"/": {
|
||||
"Proxy": "http://127.0.0.1:5984"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowFunnel": {
|
||||
"${TS_CERT_DOMAIN}:443": true
|
||||
}
|
||||
}
|
||||
187
docker/docker-compose.yml
Normal file
@@ -0,0 +1,187 @@
|
||||
# Self-hosted LiveSync — Docker Compose
|
||||
# =============================================================================
|
||||
# PROFILES
|
||||
# --------
|
||||
# (default) CouchDB only — LAN/localhost access, no TLS
|
||||
# Suitable for desktop-only use or testing.
|
||||
#
|
||||
# --profile caddy CouchDB + Caddy reverse proxy
|
||||
# Auto TLS via Let's Encrypt. Needs public domain + ports 80/443.
|
||||
#
|
||||
# --profile tailscale CouchDB + Tailscale sidecar
|
||||
# No domain required. HTTPS via *.ts.net PKI.
|
||||
# Needs a Tailscale account (free tier works).
|
||||
#
|
||||
# --profile cloudflare CouchDB + cloudflared tunnel daemon
|
||||
# Free public HTTPS via Cloudflare. Needs a CF account + tunnel token.
|
||||
# NOTE: Enable "Use Request API" in the Obsidian plugin to avoid 524 timeouts.
|
||||
#
|
||||
# QUICK START (local test):
|
||||
# cp .env.example .env && edit .env
|
||||
# docker compose up -d
|
||||
# curl -u admin:yourpassword http://localhost:5984/_up
|
||||
# =============================================================================
|
||||
|
||||
name: obsidian-livesync
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CouchDB — the only required service
|
||||
# ---------------------------------------------------------------------------
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
container_name: livesync-couchdb
|
||||
restart: unless-stopped
|
||||
# NOTE: Do NOT set user: here — the CouchDB entrypoint starts as root to
|
||||
# write docker.ini (from env vars), then drops to uid 5984 automatically.
|
||||
environment:
|
||||
COUCHDB_USER: ${COUCHDB_USER:?Set COUCHDB_USER in .env}
|
||||
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD:?Set COUCHDB_PASSWORD in .env}
|
||||
volumes:
|
||||
- couchdb-data:/opt/couchdb/data
|
||||
# Mount to /opt/couchdb/etc/local.ini (NOT into local.d/).
|
||||
# Do NOT use :ro — the CouchDB entrypoint runs chmod on this file at startup
|
||||
# and will crash with EPERM if the file is read-only. The file is only read
|
||||
# at startup; runtime changes go via the REST API into local.d/docker.ini.
|
||||
- ./config/livesync.ini:/opt/couchdb/etc/local.ini
|
||||
ports:
|
||||
# Exposes CouchDB on the host for LAN/localhost access.
|
||||
# The tunnel profiles (caddy/tailscale/cloudflare) provide HTTPS on top.
|
||||
# You can remove this port mapping once a tunnel profile is in use.
|
||||
- "${COUCHDB_PORT:-5984}:5984"
|
||||
healthcheck:
|
||||
# Test with admin credentials — ensures both CouchDB is up AND auth is ready.
|
||||
# ${COUCHDB_USER} / ${COUCHDB_PASSWORD} are expanded by Docker Compose here.
|
||||
test:
|
||||
- "CMD-SHELL"
|
||||
- "curl -sf -u ${COUCHDB_USER}:${COUCHDB_PASSWORD} http://localhost:5984/_session | grep -q ok || exit 1"
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 24
|
||||
start_period: 20s
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# One-shot init container — runs couchdb-init.sh after CouchDB is healthy.
|
||||
# Sets single-node cluster, auth requirements, CORS, size limits, creates DB.
|
||||
# Restarts on failure (e.g. race at first boot) but won't re-run if already done.
|
||||
# ---------------------------------------------------------------------------
|
||||
couchdb-init:
|
||||
image: curlimages/curl:latest
|
||||
container_name: livesync-init
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COUCHDB_INTERNAL_URL: http://couchdb:5984
|
||||
COUCHDB_USER: ${COUCHDB_USER}
|
||||
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
|
||||
COUCHDB_DATABASE: ${COUCHDB_DATABASE:-obsidiannotes}
|
||||
volumes:
|
||||
- ./scripts/couchdb-init.sh:/couchdb-init.sh:ro
|
||||
entrypoint: ["sh", "/couchdb-init.sh"]
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: caddy — Caddy reverse proxy with automatic Let's Encrypt TLS
|
||||
# Requirements: public domain, ports 80 + 443 open to internet
|
||||
# Usage: docker compose --profile caddy up -d
|
||||
# ---------------------------------------------------------------------------
|
||||
caddy:
|
||||
image: caddy:latest
|
||||
container_name: livesync-caddy
|
||||
profiles: [caddy]
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
COUCHDB_DOMAIN: ${COUCHDB_DOMAIN:?Set COUCHDB_DOMAIN in .env for caddy profile}
|
||||
ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL in .env for caddy profile}
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: tailscale — Tailscale sidecar for mesh VPN + optional Funnel
|
||||
# Requirements: Tailscale account (free), OAuth key, Funnel enabled in ACL
|
||||
# Usage: docker compose --profile tailscale up -d
|
||||
# The CouchDB port mapping above can be removed for tailscale-only deployments.
|
||||
# ---------------------------------------------------------------------------
|
||||
tailscale:
|
||||
image: tailscale/tailscale:latest
|
||||
container_name: livesync-tailscale
|
||||
profiles: [tailscale]
|
||||
restart: unless-stopped
|
||||
hostname: ${TS_HOSTNAME:-livesync}
|
||||
environment:
|
||||
TS_AUTHKEY: ${TS_AUTHKEY:?Set TS_AUTHKEY in .env for tailscale profile}
|
||||
TS_STATE_DIR: /var/lib/tailscale
|
||||
TS_SERVE_CONFIG: /config/serve.json
|
||||
TS_USERSPACE: "false"
|
||||
TS_ACCEPT_DNS: "false"
|
||||
TS_EXTRA_ARGS: ""
|
||||
volumes:
|
||||
- tailscale-state:/var/lib/tailscale
|
||||
- ./config/ts-serve.json:/config/serve.json:ro
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- SYS_MODULE
|
||||
# Share CouchDB's network namespace so Tailscale can reach it on localhost
|
||||
network_mode: service:couchdb
|
||||
depends_on:
|
||||
- couchdb
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PROFILE: cloudflare — cloudflared tunnel daemon
|
||||
# Requirements: Cloudflare account, tunnel token from CF Zero Trust dashboard
|
||||
# Usage: docker compose --profile cloudflare up -d
|
||||
# NOTE: Enable "Use Request API" toggle in the Obsidian LiveSync plugin settings
|
||||
# to avoid Cloudflare's 100-second proxy timeout (524 errors).
|
||||
# ---------------------------------------------------------------------------
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
container_name: livesync-cloudflared
|
||||
profiles: [cloudflare]
|
||||
restart: unless-stopped
|
||||
command: tunnel --no-autoupdate run
|
||||
environment:
|
||||
TUNNEL_TOKEN: ${CF_TUNNEL_TOKEN:?Set CF_TUNNEL_TOKEN in .env for cloudflare profile}
|
||||
volumes:
|
||||
- ./config/cloudflared.yml:/etc/cloudflared/config.yml:ro
|
||||
depends_on:
|
||||
couchdb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- livesync-net
|
||||
|
||||
# =============================================================================
|
||||
# Volumes
|
||||
# =============================================================================
|
||||
volumes:
|
||||
couchdb-data:
|
||||
driver: local
|
||||
caddy-data:
|
||||
driver: local
|
||||
caddy-config:
|
||||
driver: local
|
||||
tailscale-state:
|
||||
driver: local
|
||||
|
||||
# =============================================================================
|
||||
# Networks
|
||||
# =============================================================================
|
||||
networks:
|
||||
livesync-net:
|
||||
driver: bridge
|
||||
79
docker/scripts/couchdb-init.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
# Self-hosted LiveSync — CouchDB Initialization Script
|
||||
# Runs once on first startup via the couchdb-init service.
|
||||
# Configures single-node cluster, auth, CORS, and size limits.
|
||||
|
||||
set -e
|
||||
|
||||
hostname="${COUCHDB_INTERNAL_URL:-http://couchdb:5984}"
|
||||
username="${COUCHDB_USER:?COUCHDB_USER is required}"
|
||||
password="${COUCHDB_PASSWORD:?COUCHDB_PASSWORD is required}"
|
||||
node="${COUCHDB_NODE:-_local}"
|
||||
|
||||
echo "==> Waiting for CouchDB at ${hostname} ..."
|
||||
# _up is publicly accessible (no auth required) — safe pre-auth wait
|
||||
until curl -sf "${hostname}/_up" 2>/dev/null | grep -q '"status":"ok"'; do
|
||||
printf '.'
|
||||
sleep 2
|
||||
done
|
||||
echo ""
|
||||
echo "==> CouchDB is up. Initializing..."
|
||||
|
||||
# 1. Enable single-node cluster
|
||||
curl -sf -X POST "${hostname}/_cluster_setup" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" \
|
||||
--user "${username}:${password}" && echo "[OK] cluster_setup"
|
||||
|
||||
# 2. Require valid user on both http interfaces
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/require_valid_user" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd/require_valid_user"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd_auth/require_valid_user" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd_auth/require_valid_user"
|
||||
|
||||
# 3. HTTP auth challenge header
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/httpd/WWW-Authenticate" \
|
||||
-H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}" && echo "[OK] httpd/WWW-Authenticate"
|
||||
|
||||
# 4. Enable CORS on both http listeners
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/httpd/enable_cors" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] httpd/enable_cors"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/enable_cors" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] chttpd/enable_cors"
|
||||
|
||||
# 5. Increase size limits for large vaults
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/chttpd/max_http_request_size" \
|
||||
-H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}" && echo "[OK] chttpd/max_http_request_size"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/couchdb/max_document_size" \
|
||||
-H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}" && echo "[OK] couchdb/max_document_size"
|
||||
|
||||
# 6. CORS configuration — allow Obsidian app origins
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/cors/credentials" \
|
||||
-H "Content-Type: application/json" -d '"true"' --user "${username}:${password}" && echo "[OK] cors/credentials"
|
||||
|
||||
curl -sf -X PUT "${hostname}/_node/${node}/_config/cors/origins" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '"app://obsidian.md,capacitor://localhost,http://localhost"' \
|
||||
--user "${username}:${password}" && echo "[OK] cors/origins"
|
||||
|
||||
# 7. Create the vault database if it doesn't exist
|
||||
db="${COUCHDB_DATABASE:-obsidiannotes}"
|
||||
set +e
|
||||
status=$(curl -sf -o /dev/null -w "%{http_code}" --user "${username}:${password}" "${hostname}/${db}" 2>/dev/null)
|
||||
curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "[OK] database '${db}' already exists"
|
||||
else
|
||||
curl -sf -X PUT "${hostname}/${db}" --user "${username}:${password}" && echo "[OK] database '${db}' created" || echo "[WARN] database creation returned non-200 — may already exist"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> CouchDB initialization complete!"
|
||||
echo " URL : ${hostname}"
|
||||
echo " Database : ${db}"
|
||||
echo " Username : ${username}"
|
||||
@@ -1,34 +1,49 @@
|
||||
# How to add translations
|
||||
|
||||
Self-hosted LiveSync owns its multilingual catalogue. Commonlib supplies only the typed English messages which its services request; the plug-in combines those keys with its application messages and injects the selected translator at each service composition root.
|
||||
|
||||
## Getting ready
|
||||
|
||||
1. Clone this repository recursively.
|
||||
```sh
|
||||
git clone --recursive https://github.com/vrtmrz/obsidian-livesync
|
||||
```
|
||||
2. Make `ls-debug` folder under your vault's `.obsidian` folder (as like `.../dev/.obsidian/ls-debug`).
|
||||
1. Clone this repository.
|
||||
|
||||
## Add translations for already defined terms
|
||||
```sh
|
||||
git clone https://github.com/vrtmrz/obsidian-livesync
|
||||
cd obsidian-livesync
|
||||
npm ci
|
||||
```
|
||||
|
||||
1. Install dependencies, and build the plug-in as dev. build.
|
||||
```sh
|
||||
cd obsidian-livesync
|
||||
npm i -D
|
||||
npm run buildDev
|
||||
```
|
||||
2. Create an `ls-debug` directory below the test Vault's `.obsidian` directory, for example `.obsidian/ls-debug`.
|
||||
|
||||
2. Copy the `main.js` to `.obsidian/plugins/obsidian-livesync` folder of your vault, and run Obsidian-Self-hosted LiveSync.
|
||||
3. You will get the `missing-translation-yyyy-mm-dd.jsonl`, please fill in new translations.
|
||||
4. Build the plug-in again, and confirm that displayed things were expected.
|
||||
5. Merge them into `rosetta.ts`, and make the PR to `https://github.com/vrtmrz/livesync-commonlib`.
|
||||
## Add translations for existing messages
|
||||
|
||||
## Make messages to be translated
|
||||
1. Edit the human-readable YAML files under `src/common/messagesYAML/`.
|
||||
2. Regenerate the JSON and TypeScript resources.
|
||||
|
||||
1. Find the message that you want to be translated.
|
||||
2. Change the literal to use `$tf`, like below.
|
||||
```diff
|
||||
- Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT);
|
||||
+ Logger($tf('someKeyForPassphraseError'), LOG_LEVEL_URGENT);
|
||||
```
|
||||
3. Make the PR to `https://github.com/vrtmrz/obsidian-livesync`.
|
||||
4. Follow the steps of "Add translations for already defined terms" to add the translations.
|
||||
```sh
|
||||
npm run i18n:bake
|
||||
```
|
||||
|
||||
3. Build the plug-in in development mode, install it in the test Vault, and run Self-hosted LiveSync.
|
||||
4. Review any `missing-translation-yyyy-mm-dd.jsonl` file written below `.obsidian/ls-debug`, and add the required translations to the YAML catalogue.
|
||||
5. Bake and build again, then confirm the displayed text and placeholder substitution in the relevant workflow.
|
||||
|
||||
Commit the edited YAML and all regenerated JSON and TypeScript resources together.
|
||||
|
||||
## Make a message translatable
|
||||
|
||||
LiveSync-owned messages may first be added to
|
||||
`src/common/messages/LiveSyncProvisionalMessages.ts` while their wording is being
|
||||
exercised. This keeps an application-only message out of Commonlib and provides a
|
||||
typed English fallback without requiring contributors to update every language.
|
||||
|
||||
When the wording is ready for translation:
|
||||
|
||||
1. Move its canonical English entry from
|
||||
`src/common/messages/LiveSyncProvisionalMessages.ts` to
|
||||
`src/common/messagesYAML/en.yaml`. Remove the provisional entry in the same
|
||||
change. Translations in the other LiveSync YAML files may follow as contributor
|
||||
updates.
|
||||
2. Replace the source literal with `$msg()` or another existing translation helper, using the English catalogue key as the typed contract.
|
||||
3. Run `npm run i18n:bake`, build the plug-in, and verify the affected workflow.
|
||||
|
||||
When a new message belongs to Commonlib rather than the application, add its canonical English definition and key type in Commonlib first. Add any available translations to LiveSync when consuming that package; untranslated languages use Commonlib's canonical English fallback.
|
||||
|
||||
252
docs/adr/2026_06_real_obsidian_e2e.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Architectural Decision Record: Real Obsidian End-to-End Test Runner
|
||||
|
||||
## Status
|
||||
|
||||
Accepted / Implemented
|
||||
|
||||
## Release
|
||||
|
||||
Accepted — implemented as the maintained local E2E layer for the 1.0 release preparation.
|
||||
|
||||
## Context
|
||||
|
||||
When this decision was proposed, the end-to-end tests ran through Vitest browser mode and a mocked Obsidian environment in `test/harness`. This was useful for exercising synchronisation flows without launching Obsidian, but it was no longer a reliable final signal for plug-in behaviour.
|
||||
|
||||
The main issues are:
|
||||
|
||||
- The harness reimplements a large part of the Obsidian API surface, including vault files, workspace events, settings, and lifecycle behaviour. This mock can drift from real Obsidian behaviour without failing.
|
||||
- The tests run inside a browser-style environment, while the desktop plug-in runs inside Obsidian's Electron environment with its own application lifecycle, storage paths, command registry, and event ordering.
|
||||
- Several high-value regressions are about integration boundaries: boot-up sequence timing, real vault file reflection, Obsidian command registration, settings persistence, restart prompts, and file watcher behaviour. These are precisely the areas where a mock harness gives weak confidence.
|
||||
- Maintaining the harness competes with maintaining the plug-in. Adding behaviour to the plug-in often requires teaching the mock another Obsidian detail before the actual regression can be tested.
|
||||
|
||||
The current harness should therefore stop being treated as the primary E2E layer.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a new E2E layer that launches real Obsidian with temporary vaults and the built Self-hosted LiveSync plug-in installed into those vaults.
|
||||
|
||||
The long-term test pyramid should be:
|
||||
|
||||
1. Unit tests for deterministic operations and serviceFeature boundaries.
|
||||
2. Integration tests for CouchDB, Object Storage, P2P services, database operations, and replication protocols.
|
||||
3. Real Obsidian E2E tests for boot-up sequence, vault reflection, command registration, settings dialogues, restart scheduling, and user-visible workflows.
|
||||
|
||||
Retain the existing browser Harness only as a transitional compatibility layer while the real Obsidian runner reaches parity for critical flows, then remove it. New high-level E2E coverage targets the real runner.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not replace unit or integration tests with slow UI tests.
|
||||
- Do not keep extending the Obsidian mock to cover new Obsidian APIs unless a short-term compatibility bridge is required.
|
||||
- Do not require real Obsidian E2E for every pull request initially. The first CI integration should be opt-in or nightly until stability is proven.
|
||||
- Do not test every setting dialogue through UI clicks if the behaviour is already covered by unit or integration tests. Use UI automation only for workflows whose risk is in real Obsidian integration.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
### Runner
|
||||
|
||||
Create a dedicated runner under `test/e2e-obsidian/`.
|
||||
|
||||
The runner should:
|
||||
|
||||
- Create one or more temporary vault directories.
|
||||
- Build the plug-in once with `npm run build` or a narrower production build command.
|
||||
- Install `main.js`, `manifest.json`, and `styles.css` when present into `.obsidian/plugins/obsidian-livesync/`.
|
||||
- Prepare `.obsidian/community-plugins.json` and `.obsidian/plugins/obsidian-livesync/data.json` as needed.
|
||||
- Launch Obsidian against the temporary vault.
|
||||
- Wait until the plug-in reports readiness through a deterministic probe.
|
||||
- Drive assertions through a narrow control channel rather than fragile visual selectors wherever possible.
|
||||
- Dispose of Obsidian and temporary vaults after each scenario.
|
||||
|
||||
### Obsidian Launch
|
||||
|
||||
The preferred desktop target is the installed Obsidian application. The launch mechanism should be platform-specific but hidden behind a small adapter:
|
||||
|
||||
- Linux: launch the Obsidian executable with a vault path or Obsidian URI, depending on what is most reliable. If an AppImage is used and FUSE is not available, extract it with `--appimage-extract` and launch the extracted `squashfs-root/obsidian` binary.
|
||||
- macOS: launch the app bundle through `open` or the executable inside the bundle.
|
||||
- Windows: launch the installed executable or the registered application protocol.
|
||||
|
||||
The first implementation can support Linux only if that is the local and CI target. Cross-platform support can be added after the runner contract is stable.
|
||||
|
||||
In headless Linux environments, launch through `xvfb-run`, pass Electron flags such as `--no-sandbox` and `--disable-gpu`, and isolate `HOME`, `XDG_CONFIG_HOME`, and `--user-data-dir` per temporary vault.
|
||||
|
||||
### Control Channel
|
||||
|
||||
The runner needs a stable way to observe readiness and issue test commands. Prefer a test-only plug-in bridge compiled only in test builds or enabled only by an environment variable.
|
||||
|
||||
Possible bridge options:
|
||||
|
||||
- The official Obsidian CLI, using the installed `obsidian-cli` helper to open vaults, reload the plug-in, run `eval`, and call developer commands.
|
||||
- A local HTTP/WebSocket bridge bound to `127.0.0.1` with a random port and token.
|
||||
- A file-based bridge in the vault, where Obsidian writes status files and consumes command files.
|
||||
- A DevTools protocol bridge if Obsidian exposes a stable debugging port in the test environment.
|
||||
|
||||
The first implementation uses Obsidian's CLI for orchestration and readiness checks. The CLI handles vault opening through `obsidian://open?path=...`, enables community plug-ins through `app.plugins.setEnable(true)`, reloads Self-hosted LiveSync through `plugin:reload id=obsidian-livesync`, and verifies that `app.plugins.plugins['obsidian-livesync']` is loaded.
|
||||
|
||||
This keeps E2E-only behaviour out of the production plug-in bundle. The runner should not require Self-hosted LiveSync to write marker files or expose a test server merely to prove that Obsidian loaded it.
|
||||
|
||||
The DevTools protocol remains useful for diagnostics. Obsidian's CLI exposes developer commands such as `dev:cdp`, `dev:errors`, and `dev:console`, so the runner should prefer the CLI path first and fall back to direct DevTools attachment only if the CLI cannot provide the required signal.
|
||||
|
||||
### Test Data and Services
|
||||
|
||||
Keep the existing Docker scripts for CouchDB, MinIO, and P2P services. The real Obsidian runner should reuse these service fixtures instead of creating another service orchestration stack.
|
||||
|
||||
Each test should use unique database names, bucket prefixes, vault names, and P2P room IDs. This prevents tests from depending on cleanup and makes interrupted runs less harmful.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 0: Discovery
|
||||
|
||||
- Confirm how Obsidian can be launched reliably on the local development environment.
|
||||
- Confirm whether Obsidian accepts a vault path directly, requires an Obsidian URI, or needs a pre-existing vault registry.
|
||||
- Identify where Obsidian stores per-user state in the test environment and decide how to isolate it.
|
||||
- Decide whether the first bridge is file-based or HTTP/WebSocket.
|
||||
|
||||
Initial discovery on Linux ARM64 found that:
|
||||
|
||||
- `Obsidian-1.12.7-arm64.AppImage` requires `libfuse.so.2` for direct AppImage execution.
|
||||
- Extracting the AppImage with `--appimage-extract` works without FUSE.
|
||||
- Launching the extracted `squashfs-root/obsidian` binary under `xvfb-run` with isolated user data stays alive for the smoke timeout.
|
||||
- No missing shared libraries were reported by `ldd` for the extracted binary in the tested environment.
|
||||
- Obsidian's CLI is disabled unless the global `obsidian.json` contains `cli: true`.
|
||||
- Passing only `.obsidian/community-plugins.json` is not enough to load community plug-ins on Obsidian 1.12. The runner also has to enable the global community plug-in switch through `app.plugins.setEnable(true)`.
|
||||
- The reliable launch sequence is: start Obsidian, send `obsidian://open?path=...` through `obsidian-cli`, wait until the vault-side CLI exposes the plug-in catalogue, enable community plug-ins, reload Self-hosted LiveSync, and verify plug-in readiness through `obsidian-cli eval`.
|
||||
|
||||
### Phase 1: Smoke Runner
|
||||
|
||||
- Add `test/e2e-obsidian/runner` utilities for temporary vault creation, plug-in installation, launch, readiness wait, and cleanup.
|
||||
- Add one smoke test:
|
||||
- launch Obsidian with an empty vault,
|
||||
- load Self-hosted LiveSync,
|
||||
- wait for the boot-up sequence to become ready,
|
||||
- read the plug-in version or status through the control channel,
|
||||
- close Obsidian cleanly.
|
||||
- Add an npm script such as `test:e2e:obsidian`.
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- Added `test/e2e-obsidian/runner` helpers for Obsidian discovery, CLI discovery, temporary vault creation, plug-in installation, process launch, CLI execution, and readiness polling.
|
||||
- Added `test:e2e:obsidian:discover`, `test:e2e:obsidian:cli-help`, `test:e2e:obsidian:smoke`, `test:e2e:obsidian:vault-reflection`, `test:e2e:obsidian:couchdb-upload`, `test:e2e:obsidian:minio-upload`, `test:e2e:obsidian:startup-scan`, `test:e2e:obsidian:two-vault-sync`, `test:e2e:obsidian:hidden-file-snippet-sync`, `test:e2e:obsidian:customisation-sync`, `test:e2e:obsidian:setting-markdown-export`, `test:e2e:obsidian:local-suite`, `test:e2e:obsidian:local-suite:services`, and `test:e2e:obsidian:install-appimage`.
|
||||
- Added `startObsidianLiveSyncSession()` so future workflows can reuse the launch, trusted temporary vault state, vault open, community plug-in reload, and readiness sequence without duplicating smoke runner code.
|
||||
- Added CouchDB runner utilities that reuse `.test.env`/process environment values, create unique temporary databases, query uploaded documents directly, and clean up the database unless `E2E_OBSIDIAN_KEEP_COUCHDB=true` is set.
|
||||
- Added a manual AppImage installer that downloads Obsidian `1.12.7` for `arm64` or `x86_64`, stores it under `_testdata/obsidian`, and extracts it for FUSE-free execution.
|
||||
- Confirmed the smoke runner on Linux ARM64 with the extracted Obsidian `1.12.7` AppImage, `xvfb-run`, and the built Self-hosted LiveSync bundle.
|
||||
- Confirmed the runner can enable the Obsidian CLI through isolated `obsidian.json` state, pre-seed the temporary Chromium local storage so the generated vault ID is trusted for community plug-ins, open the temporary vault through `obsidian-cli`, reload Self-hosted LiveSync, and verify readiness through `obsidian-cli eval`.
|
||||
- Removed the first test-only ready-marker bridge from the plug-in bundle. The current runner observes readiness from outside the plug-in through Obsidian's own CLI, so normal user vaults do not receive E2E marker files.
|
||||
|
||||
Current verification:
|
||||
|
||||
- `npm run tsc-check` passes.
|
||||
- `npm run build` passes with existing Svelte warnings.
|
||||
- `npm run test:e2e:obsidian:discover` finds `_testdata/obsidian/squashfs-root/obsidian` when the extracted AppImage is present.
|
||||
- `E2E_OBSIDIAN_SMOKE_TIMEOUT_MS=1000 npm run test:e2e:obsidian:smoke` passes locally.
|
||||
- `npm run test:e2e:obsidian:vault-reflection` creates a note through Obsidian's vault API, verifies the reflected file on disk, and reads it back through Obsidian.
|
||||
- `npm run test:e2e:obsidian:couchdb-upload` configures a unique CouchDB database, creates a note through Obsidian, commits it into the local database, runs one-shot synchronisation, and verifies that CouchDB contains the metadata document and all referenced chunk documents.
|
||||
- `npm run test:e2e:obsidian:minio-upload` configures a unique Object Storage bucket prefix, creates a note through Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written to the S3-compatible bucket.
|
||||
- `npm run test:e2e:obsidian:startup-scan` verifies that a file written while Obsidian is stopped is picked up during the next real Obsidian boot and uploaded to CouchDB after one-shot synchronisation.
|
||||
- `npm run test:e2e:obsidian:two-vault-sync` verifies two-vault note synchronisation: creation, update, rename, deletion, per-device target-filter differences, and a separate encrypted round-trip with Path Obfuscation enabled. The optional Markdown conflict check creates divergent branches in two real Vaults, conservatively merges them, edits the merged result again, and verifies that the Vault holding the deleted losing revision accepts the propagated result without recreating a conflict. Enable it with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`. A separate `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` scope keeps four conflicts active while a Vault edits, deletes, case-renames, and cross-path-renames files, then verifies exact parent revisions and replicated trees. Neither scope is part of the default local suite.
|
||||
- `npm run test:e2e:obsidian:hidden-file-snippet-sync` verifies hidden file synchronisation as a two-vault round-trip: creation, deletion, automatic JSON conflict merging with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target-pattern differences.
|
||||
- `npm run test:e2e:obsidian:customisation-sync` verifies a two-vault Customisation Sync workflow: scan a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronise them through CouchDB, apply them on the second vault, assert the resulting `.obsidian` files, propagate a snippet update, and verify deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy.
|
||||
- `npm run test:e2e:obsidian:setting-markdown-export` verifies that setting Markdown export creates a vault file and omits credentials when credential export is disabled.
|
||||
- `npm run test:e2e:obsidian:install-appimage` reuses the existing AppImage and extracted binary when they are already present.
|
||||
- `npm run test:e2e:obsidian:local-suite` runs the local verification sequence for the real Obsidian runner after CouchDB, MinIO, and the P2P relay have been started.
|
||||
- `npm run test:e2e:obsidian:local-suite:services` stops leftover CouchDB, MinIO, and P2P relay fixtures, starts fresh fixtures, runs the local suite, and stops the fixtures again.
|
||||
- `npm run test:e2e:obsidian:local-suite:services` has been verified locally with real Obsidian and all three fixtures. In addition to the core launch and synchronisation scenarios, the maintained suite covers CouchDB, Object Storage, and P2P Setup URI workflows in which the working first device generates the URI imported by the second device.
|
||||
|
||||
Known limits:
|
||||
|
||||
- The smoke runner currently proves only one-vault launch and plug-in load readiness. Broader workflows are covered by separate real Obsidian scripts, including CouchDB upload, startup scan, two-vault note synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export.
|
||||
- The working path has been validated on Linux ARM64 and on macOS with Obsidian 1.12.7. Windows remains unverified.
|
||||
- CI wiring is intentionally not implemented. The runner depends on a licensed desktop application and is treated as a local verification tool.
|
||||
|
||||
### Phase 2: First Real Workflow
|
||||
|
||||
- Add a one-vault local workflow:
|
||||
- configure a temporary CouchDB database,
|
||||
- create a note in the real vault,
|
||||
- wait for metadata and chunks to be stored,
|
||||
- restart Obsidian,
|
||||
- verify that the plug-in loads and the note remains consistent.
|
||||
|
||||
This validates real boot-up, settings persistence, vault file access, database writes, and restart-sensitive state.
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- Added a pre-CouchDB workflow that creates a note through Obsidian's vault API, confirms the note is reflected as a real vault file, and reads the same note back through Obsidian. This covers the vault reflection part of the Phase 2 path before remote database setup is introduced.
|
||||
- Added a first CouchDB-backed upload workflow, modelled after the CLI Deno tests: reuse the standard CouchDB environment variables, create a unique remote database, apply CouchDB settings through the plug-in's setting service, commit the note through the real Obsidian vault path, run one-shot synchronisation, and assert that remote metadata and chunks exist.
|
||||
- Added an Object Storage-backed upload workflow against MinIO to exercise Journal Sync and the AWS SDK path from real Obsidian.
|
||||
- Added Obsidian-specific workflows for boot-time vault scanning, two-vault note synchronisation, hidden `.obsidian/snippets` file round-tripping, hidden JSON conflict resolution, Customisation Sync application for snippets, config JSON files, and plug-in fixtures, per-device target-filter differences, and setting Markdown export. These scenarios assert against CouchDB documents, vault files, or real Obsidian UI outcomes instead of internal service state.
|
||||
|
||||
### Phase 3: Two-Vault Synchronisation
|
||||
|
||||
- Launch two Obsidian instances with two temporary vaults.
|
||||
- Configure both against the same temporary remote database.
|
||||
- Create, modify, rename, and delete notes in one vault.
|
||||
- Verify reflection in the other vault.
|
||||
- Cover encrypted and non-encrypted configurations separately.
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- `test:e2e:obsidian:two-vault-sync` covers creation, update, rename, deletion, and per-device target-filter behaviour for a non-encrypted CouchDB configuration. Its optional conflict fixtures cover real two-Vault divergence, conservative merge, post-resolution editing, propagation to a Vault whose file still contains the deleted losing revision, and edit, logical deletion, case-only rename, and cross-path rename while conflicts remain active.
|
||||
- The same script creates a separate temporary CouchDB database and temporary vault pair for an encrypted two-vault round-trip with Path Obfuscation enabled.
|
||||
|
||||
### Phase 4: Harness Retirement
|
||||
|
||||
- Mark `test/harness` as deprecated in documentation.
|
||||
- Stop adding new tests to `test/suite` unless they are explicitly transitional.
|
||||
- Do not mechanically port `test/suite` into real Obsidian E2E. Scenarios that can already be exercised and asserted through the CLI test layer should stay there or move to lower-level integration tests.
|
||||
- Prioritise real Obsidian coverage for behaviours that the CLI cannot prove well, especially RedFlag flag-file recovery flows, Fast Setup (Simple Fetch), boot-up sequencing, restart-sensitive initial synchronisation, and user-visible recovery dialogues.
|
||||
- Remove the harness only after the new runner covers the critical boot-up and synchronisation workflows.
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- The mocked Vitest browser suites, their P2P runner, their root-level relay helpers, and the manual `harness-ci` workflow have been removed after maintained suites covered the critical flows.
|
||||
- Headless CouchDB and Object Storage combinations, with and without encryption, remain owned by the CLI two-Vault matrix. P2P transport replacement and relay lifecycle remain owned by the CLI Compose E2E suite. Real Obsidian owns the visible CouchDB, Object Storage, and P2P Setup URI workflows, including first-device URI generation, second-device import, and two-way Vault synchronisation.
|
||||
- The Obsidian compatibility implementation still needed by the Webapp has moved to `src/apps/webapp/obsidianMock.ts`; it is not a retained browser E2E Harness.
|
||||
- Remaining high-value scenarios, including RedFlag and Fast Setup (Simple Fetch) variants, should be added according to their owning integration boundary rather than copied line by line from the retired suite.
|
||||
|
||||
## Local Verification Strategy
|
||||
|
||||
Real Obsidian E2E is a local verification layer. It should not be wired into the default CI gate.
|
||||
|
||||
- Keep the scripts individually runnable for focused local debugging.
|
||||
- Provide `test:e2e:obsidian:local-suite` for a broader local pass after the CouchDB, MinIO, and P2P relay fixtures have been started.
|
||||
- Provide `test:e2e:obsidian:local-suite:services` for a broader local pass that manages all three fixtures itself.
|
||||
- Use `OBSIDIAN_BINARY` when testing against an installed desktop application.
|
||||
- Use `test:e2e:obsidian:install-appimage` on Linux when a local AppImage copy is needed, and reuse the extracted `_testdata/obsidian/squashfs-root` directory between local runs.
|
||||
- Capture Obsidian logs, plug-in logs, vault snapshots, and service logs manually when investigating failures.
|
||||
- Fail fast on launch failures, readiness timeouts, and cleanup failures with clear diagnostics.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **Obsidian licensing and installation**: Keep the runner local-first and capable of using `OBSIDIAN_BINARY`.
|
||||
- **Flakiness from UI timing**: Prefer a control channel and service-level probes over visual selectors.
|
||||
- **Multiple instances**: Obsidian may not support multiple independent instances cleanly on all platforms. Start with one-instance smoke tests, then validate two-instance behaviour on Linux before expanding scope.
|
||||
- **State leakage**: Isolate vault directories, Obsidian user data, remote database names, and bridge tokens per test.
|
||||
- **Security of E2E controls**: Keep readiness and control outside the production plug-in bundle. Prefer Obsidian CLI probes over E2E-only plug-in code.
|
||||
- **Runtime cost**: Keep real Obsidian E2E out of the default PR gate. Use focused scripts or the local suite when a change touches real Obsidian integration.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Which launch mechanism is most reliable for Obsidian on each supported desktop platform?
|
||||
- Can two Obsidian instances run with isolated user data at the same time?
|
||||
- Do future scenarios need a richer control channel than Obsidian CLI, or can CLI `eval` and developer commands cover the required workflows?
|
||||
- Should any future E2E-only plug-in code live in a separate test build, or should the production bundle remain free of E2E controls?
|
||||
- Which RedFlag and Fast Setup (Simple Fetch) variants should be added first?
|
||||
|
||||
## Initial Implementation Checklist
|
||||
|
||||
1. Add an Obsidian launch discovery script that prints the detected executable, version, and launch mode.
|
||||
2. Add temporary vault and plug-in installation helpers.
|
||||
3. Add CLI-based plug-in readiness polling.
|
||||
4. Add `test:e2e:obsidian:smoke` for one-vault plug-in load.
|
||||
5. Document required local environment variables, especially `OBSIDIAN_BINARY`.
|
||||
6. Port one CouchDB-backed workflow after the smoke test is stable.
|
||||
7. Retire the browser Harness after critical workflows have replacement coverage.
|
||||
8. Add the local suite script for broader local verification.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Real Obsidian E2E becomes the source of truth for plug-in lifecycle and vault integration.
|
||||
- Unit and integration tests remain the primary fast feedback loops.
|
||||
- The old browser Harness has been deleted now that the replacement coverage owns its critical workflows.
|
||||
- The project will gain slower but higher-confidence tests for the behaviours most likely to differ between mocks and Obsidian itself.
|
||||
129
docs/adr/2026_07_bounded_remote_activity.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Architectural Decision Record: Bounded Remote Activity
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Self-hosted LiveSync performs remote work through more than one path:
|
||||
|
||||
- finite, or bounded, replication starts for manual, event-driven, periodic, and start-up synchronisation;
|
||||
- long-running rebuild uploads, standard fetches, and fast fetches;
|
||||
- continuous replication keeps a channel open until the application lifecycle stops it; and
|
||||
- remote chunk fetching can occur independently of replication, for example while reading history.
|
||||
|
||||
The existing API request and response counters do not cover every one of these paths. They therefore cannot, by themselves, provide an accurate remote-work indicator.
|
||||
|
||||
Long-running finite operations can also be interrupted by platform lifecycle behaviour. A mobile or desktop display may sleep while an operation is in progress. Changing Obsidian to a hidden or minimised state normally invokes the suspension lifecycle, which closes the active replicator. The existing desktop-only `keepReplicationActiveInBackground` setting deliberately provides a broader, opt-in policy for continuous and periodic replication, and should not be made a prerequisite for completing a finite operation.
|
||||
|
||||
Screen wake lock and background execution are related platform effects, but they are not equivalent. A screen wake lock prevents display sleep only while the platform and document visibility permit it. It does not guarantee background execution, prevent operating-system suspension, or override an explicit system sleep action.
|
||||
|
||||
## Decision
|
||||
|
||||
`ReplicatorService` owns a reactive `boundedRemoteActivityCount` and a `runBoundedRemoteActivity` closure boundary.
|
||||
|
||||
The boundary has the following contract:
|
||||
|
||||
- increment the count immediately before running a finite remote task;
|
||||
- run the task through an optional host-provided activity runner;
|
||||
- decrement the count in `finally`, including when the task rejects;
|
||||
- allow overlapping tasks, so transitions may be `0 → 1 → 2 → 1 → 0`; and
|
||||
- count logical operations, not physical connections, sockets, HTTP requests, queued work, or retry delays outside the bounded task.
|
||||
|
||||
`ReplicatorService` also owns a narrower `finiteReplicationActivityCount`. Callers enter it through the typed `runFiniteReplicationActivity` method; diagnostic labels do not determine behaviour. The narrower count describes operations which may still place replicated documents in the local database; it excludes rebuilds, chunk-fetch claims, and other bounded work which cannot satisfy an arbitrary missing-chunk read. It is a delivery-lifecycle capability, not a second Wake Lock policy or a connection counter.
|
||||
|
||||
The Obsidian host injects the screen wake-lock manager from the `octagonal-wheels` package in the Fancy Kit monorepo as the activity runner on both mobile and desktop. Unsupported or rejected wake-lock requests remain best effort and do not prevent the remote task from running. The manager is disposed when the plug-in unloads.
|
||||
|
||||
Finite replication enters both counts only after readiness checks have succeeded and leaves them after `openReplication(..., continuous: false, ...)` settles. A successful completion has reached the latest sequence in that operation's scope and is therefore an authoritative quiescence boundary for chunk retrieval. A failed operation does not prove latest state, but can no longer deliver documents from that attempt. Failure handling runs afterwards so a mismatch or recovery dialogue does not retain the activity. This includes the direct start-up synchronisation path as well as manual, event-driven, and periodic calls through `ReplicationService`. The unbounded continuous channel does not enter either boundary, but its finite initial pull-only catch-up does; the one-shot parameter fallback chain remains inside that boundary.
|
||||
|
||||
Delivery into the local database and application to the Obsidian Vault are deliberately separate lifetimes. A mobile device can have only a short opportunity to obtain remote data, while applying a large downloaded batch to the Vault is durable, offline-capable work which can continue or resume later. The replication-result queue and its recovery snapshot therefore remain outside `boundedRemoteActivityCount`. Finite remote activity ends when the transfer operation settles, even when the `📥` queue still contains documents awaiting Vault application.
|
||||
|
||||
This separation also keeps the activity indicators truthful: `📲` describes a finite remote operation and must not remain active solely because local Vault writes are pending. The existing replication-result count continues to describe that local queue. If a future feature offers screen-awake protection while applying downloaded documents, it must use a separately typed local-application activity or power-policy boundary, preserve the current behaviour by default, and avoid incrementing either remote-activity count.
|
||||
|
||||
Manual P2P commands which bypass `ReplicationService` enter the broad boundary. Direct P2P pull and push entry points are therefore both protected as finite remote work, covering the Obsidian panes, CLI, and Webapp. A pull or bidirectional synchronisation also enters the narrower finite-replication boundary because it can place documents in the local database. A push-only request remains broad-only: it cannot satisfy a local missing-chunk read and must not present itself as a delivery source. Automatic synchronisation on peer discovery, a pull requested by a remote peer, and a watched pull following a peer progress notification enter both boundaries because each can deliver local documents. A normal P2P peer-selection dialogue represents one broad finite session: it remains inside the boundary while waiting for a peer and while the person may perform repeated synchronisations, then settles when the dialogue closes and any in-flight synchronisation has finished. Closing without synchronising returns a failed result and releases the boundary. The 'Start Sync & Close' action completes its synchronisation before closing. This deliberately protects peer discovery and selection, because display sleep can interrupt discovery or connection establishment and require the person to start detection again. It may therefore retain a Wake Lock longer than the network transfer alone. A transfer performed inside that session temporarily adds a nested activity; the count remains a logical-operation count rather than a connection total.
|
||||
|
||||
`ChunkFetcher` enters the broad boundary synchronously when it accepts newly missing chunk identifiers, but it does not increment the finite-replication count. A typed per-identifier claim keeps the broad boundary active through queue waiting, interval throttling, `fetchRemoteChunks`, validation, local persistence, and terminal event delivery. Duplicate requests share the existing claim. Explicit absence, failure, cancellation, or a conservative five-minute period without fetcher-observable progress settles the affected claim. The five-minute value is only a last-resort leak fuse: it prevents a never-settling integration from retaining the per-identifier claim and waiter indefinitely and, once the activity runner has entered the claim task, lets the associated Wake Lock, lifecycle deferral, and indicator finish. It is not an arrival estimate, proof of remote absence, or a transport deadline. This scope is defined in detail by the chunk-arrival-quiescence ADR.
|
||||
|
||||
Rebuild operations use the same boundary at their destructive or remote phase:
|
||||
|
||||
- remote rebuild covers settings application, remote reset, and both upload passes, but releases before the completion dialogue;
|
||||
- rebuild everything covers local and remote reset and both upload passes, but releases before the completion dialogue;
|
||||
- standard fetch starts after any user confirmation and covers local reset, both download passes, and automatic reflection resumption; and
|
||||
- fast fetch starts after remote-type selection and covers reset, resumable download retries, reflection resumption when requested, and checkpoint removal. A non-CouchDB fallback enters only the standard-fetch boundary.
|
||||
|
||||
Rebuilder-owned confirmation before destructive work and completion dialogues remain outside the boundary so a person cannot hold a Wake Lock indefinitely merely by leaving a dialogue open. P2P peer discovery and selection during a rebuild deliberately remain inside the boundary. They occur after destructive work has begun, and releasing protection at that point would both leave a partial rebuild unprotected and allow display sleep to interrupt peer detection, forcing the person to repeat it. The protected selection period is therefore part of the rebuild operation rather than an incidental confirmation dialogue.
|
||||
|
||||
The deprecated database-clean-up workflow also places its connection, one-shot replication, balancing, and remote resolution inside one `database-cleanup` boundary after the user's choice. Its preliminary count and choice dialogue remain outside.
|
||||
|
||||
On every platform, a visibility change to hidden defers the normal suspension lifecycle while the bounded count is non-zero. When the last bounded operation ends, the deferred suspension runs if the document is still hidden and the existing desktop background-replication setting does not apply. This does not bypass mobile operating-system restrictions: a hidden document loses its Screen Wake Lock, and the operating system may still pause or terminate the application. LiveSync merely avoids aborting the operation itself while it may still be able to finish.
|
||||
|
||||
Fetch rebuilds temporarily suspend file watching. Their visibility event still records the observed hidden state and a pending lifecycle suspension while bounded activity is in progress, without committing or processing file events. A hidden application is suspended after the rebuild boundary ends and can therefore resume normally when it becomes visible. If it becomes visible before the boundary ends, the pending suspension is cancelled and no unmatched resume lifecycle is emitted.
|
||||
|
||||
If the desktop background setting applies, its existing continuous or periodic policy remains authoritative. When a Desktop LiveSync window becomes visible during bounded activity, the normal continuous-channel teardown and resume sequence is also deferred until that activity ends, so recovery does not abort the finite operation.
|
||||
|
||||
The status bar separates the two meanings. `📲` is shown while the bounded remote activity count is non-zero. It therefore reports a finite logical operation, including periods such as P2P peer selection or chunk-fetch queueing when no request is currently crossing the network. An adjacent `🌐N` reports the approximate number of tracked physical-request units currently in progress. The icon values and the physical indicator's 150 ms minimum display time are named constants so their presentation can be revised without changing the activity contract.
|
||||
|
||||
Each physical HTTP attempt owns one balanced counter pair. The request counter is incremented immediately before invoking the selected fetch implementation, and the response counter is incremented in `finally`, whether the attempt returns or rejects. A web-fetch failure followed by the native fallback is two physical attempts and therefore contributes two balanced pairs. Callers must not add another pair around `performFetch`, because duplicated or missing increments leave the status indicator permanently active.
|
||||
|
||||
Object Storage contributes one approximate unit for each AWS SDK command issued by its adapter, including upload, download, listing, deletion, availability, and usage requests. A download remains active until its response body has been consumed. This boundary is above the request-handler choice, so it covers both the standard SDK handler and Obsidian's internal request API without double counting. SDK-internal retries remain within one reported command. The displayed value is therefore intentionally approximate and is not an exact count of sockets, HTTP exchanges, or bytes transferred.
|
||||
|
||||
P2P does not yet contribute to the physical-request count because it does not have a request unit comparable with CouchDB HTTP attempts or Object Storage SDK commands. Its finite operations remain visible through `📲`. A future P2P transfer metric should be added only when it has a stable meaning, rather than being inferred from the broad logical-operation count.
|
||||
|
||||
## Ownership
|
||||
|
||||
`ReplicatorService` is the shared ownership point because both `ReplicationService` and `ChunkFetcher` already depend on it. It owns the broad activity count and classifies the semantic subset which represents finite replication. Placing either activity state in `ReplicationService` would make chunk fetching depend in the opposite direction and risk a service dependency cycle. Adding another Service Hub service would introduce a wider capability surface without a distinct lifecycle owner.
|
||||
|
||||
The platform activity runner remains injected. Common library and headless consumers can omit it while retaining the same bounded activity count and operation semantics.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not count continuous replication as a bounded activity.
|
||||
- Do not reinterpret the count as an exact number of network connections or HTTP requests.
|
||||
- Do not use either diagnostic count for replication completion, throttling, protocol correctness, or power-policy decisions.
|
||||
- Do not claim or implement privileged mobile background execution.
|
||||
- Do not guarantee protection against operating-system suspension, closing a laptop lid, forced termination, network loss, or a user-initiated sleep action.
|
||||
- Do not add a lifecycle timeout which would abort an unusually slow rebuild. A genuinely stalled operation may postpone LiveSync's visibility suspension until it settles, but the platform may still suspend or terminate background work.
|
||||
- Do not broaden `keepReplicationActiveInBackground`; it remains an opt-in desktop policy for continuous and periodic operation after finite work has ended.
|
||||
- Do not include offline scans, unrelated local storage reflection, or the durable replication-result queue in this boundary. They are offline-capable and require a separate decision if activity reporting or power policy is added later.
|
||||
|
||||
## Verification
|
||||
|
||||
Before changing the transfer/application separation, add a deterministic regression scenario which leaves downloaded documents queued after finite transfer settles, verifies that remote activity has ended, persists the queued state, and resumes Vault application after suspension or restart. An optional local-application Wake Lock feature requires its own enabled and disabled cases; existing remote-operation E2E is not evidence for that separate policy.
|
||||
|
||||
Unit tests cover:
|
||||
|
||||
- overlapping bounded activities and their reactive count transitions;
|
||||
- count cleanup after rejection;
|
||||
- entry into the boundary only after replication readiness succeeds;
|
||||
- replication failure handling occurring after the finite activity ends;
|
||||
- start-up one-shot replication and continuous start-up's finite pull-only catch-up entering the boundary, while the unbounded live channel does not;
|
||||
- start-up readiness failure avoiding the boundary;
|
||||
- direct P2P commands entering the boundary;
|
||||
- direct P2P pull and push entry points entering the broad boundary, while only pull and bidirectional operations enter the finite-delivery boundary;
|
||||
- automatic synchronisation on peer discovery, remote pull requests, and watched peer progress entering the boundary;
|
||||
- P2P peer-selection sessions settling on close, including cancellation, repeated synchronisation, and a close during in-flight work;
|
||||
- remote chunk fetching remaining inside the shared boundary from synchronous queue acceptance through local persistence and terminal notification;
|
||||
- missing-chunk waiters rechecking local storage when observed per-identifier claims and finite replication have settled;
|
||||
- the finite-replication count excluding other bounded work;
|
||||
- standard, fast, remote, and combined rebuild activity boundaries;
|
||||
- Rebuilder-owned confirmation and completion dialogues remaining outside rebuild activity;
|
||||
- fallback from fast fetch avoiding a nested activity boundary;
|
||||
- fast-fetch reflection resumption and checkpoint removal remaining inside the activity;
|
||||
- visibility suspension being deferred while bounded activity is in progress on desktop and mobile;
|
||||
- rebuild-time file-watching suspension preserving the deferred lifecycle action;
|
||||
- deferred suspension after the final activity ends while the document remains hidden;
|
||||
- hidden-to-visible transitions before the final activity avoiding an unmatched resume; and
|
||||
- continuous-channel recovery being deferred when a desktop window becomes visible during bounded activity.
|
||||
|
||||
Additional tests cover balanced physical-request counters after success and rejection, each Object Storage command boundary, response-body consumption for downloads, the split `📲` and `🌐N` status labels, and a deterministic real-Obsidian CouchDB request held while the physical indicator is observed. The Object Storage integration and real-Obsidian MinIO workflows verify that actual AWS SDK operations advance and rebalance the shared counters.
|
||||
|
||||
The exact Fancy Kit screen wake-lock behaviour is covered by its package and Harness tests. A real Obsidian smoke test remains appropriate when changing the platform adapter or lifecycle integration, but is not required for changes confined to the already-tested injected activity-runner contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One finite activity definition drives Wake Lock, lifecycle protection, and status UI without coupling common library code to Obsidian or browser globals.
|
||||
- Callers can observe accurate logical activity even in CLI and Webapp hosts which do not inject a Wake Lock implementation.
|
||||
- Rebuild operations now retain Wake Lock and lifecycle protection across their longest interruption-sensitive phases without retaining them for Rebuilder-owned pre-operation or completion dialogues. Post-reset P2P discovery and selection remain protected as an intentional part of completing the rebuild.
|
||||
- Downloaded documents may remain in the durable Vault-application queue after remote activity has ended, allowing transfer and offline application to follow different mobile lifetimes without presenting local writes as communication.
|
||||
- Users can now distinguish the lifetime of a finite remote operation from approximate request activity within it.
|
||||
144
docs/adr/2026_07_chunk_arrival_quiescence.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Architectural Decision Record: Chunk Arrival Quiescence
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
A file metadata document refers to one or more content-addressed chunk documents. LiveSync normally writes the chunks before writing the metadata, but those writes are separate database operations and are not replicated as one atomic unit. A local reader can consequently observe metadata before every referenced chunk is locally readable. This is expected when CouchDB on-demand chunk fetching is enabled, and can also occur around conflict resolution, replication event processing, and historical data.
|
||||
|
||||
Before this decision, a missing-chunk waiter used a fixed 5-second or 30-second timeout. Its budget included queueing, throttling, network transfer, validation, local persistence, and event delivery. A healthy operation could therefore time out immediately before it delivered the requested chunk. Conversely, when no operation was capable of delivering the chunk, the timer merely delayed the same unavailable result.
|
||||
|
||||
Finite replication provides a stronger boundary than elapsed wall-clock time. In the absence of an error, a finite replication does not complete until it has reached the latest sequence in its scope. Once it completes, that replication cannot deliver another chunk. An on-demand fetch is a separate finite delivery path and needs its own per-identifier boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
Wait only for a delivery lifecycle which is observable when the local miss is handled. Do not guess how long an unobserved producer might take.
|
||||
|
||||
Two lifecycle signals are relevant:
|
||||
|
||||
- `finiteReplicationActivityCount` records finite replication operations which can still place database documents in the local database; and
|
||||
- a `ChunkDeliveryCoordinator` claim records the complete lifetime of an accepted on-demand request for each missing chunk identifier.
|
||||
|
||||
The finite replication count is distinct from the broader `boundedRemoteActivityCount`. Rebuilds and other bounded remote work may need Wake Lock and lifecycle protection without being able to satisfy an arbitrary missing-chunk read. A P2P push-only operation is broad for the same reason: it sends documents away but cannot place one in the local database. P2P pull and bidirectional operations are finite-delivery sources. `ReplicatorService` updates the narrower count only through the typed `runFiniteReplicationActivity` boundary; the diagnostic activity label is not used as a behavioural discriminator.
|
||||
|
||||
The waiting layer follows these rules:
|
||||
|
||||
1. Register the waiter before requesting on-demand delivery.
|
||||
2. Dispatch `missingChunks` synchronously when direct fetch is permitted. `ChunkFetcher` must claim accepted identifiers before dispatch returns, closing the scheduling gap without a timer.
|
||||
3. If a matching claim or finite replication is active, wait for that observable producer.
|
||||
4. A valid chunk arrival resolves the waiter immediately.
|
||||
5. An explicit remote-missing result resolves it as missing immediately.
|
||||
6. Once every observed producer completes, read the requested identifier from the local database once more, bypassing the cache.
|
||||
7. Return the rechecked chunk, or return unavailable. Do not add a fixed grace period after the producer has stopped.
|
||||
8. If no producer is observable after synchronous dispatch, return unavailable immediately. There is no operation for a duration to represent.
|
||||
|
||||
If new relevant activity starts while the final database recheck is pending, that result is stale. The waiter remains active until the newer producer completes and a current recheck finishes.
|
||||
|
||||
A successful finite replication completion is the authoritative ‘latest’ boundary. A failed operation does not prove that the remote lacks the chunk, but it is still terminal for that attempt: it can no longer deliver data. The same local recheck preserves any documents received before the failure, after which normal replication error and retry handling remains responsible for recovery. Missing-chunk code must not misreport that case as an explicit remote absence.
|
||||
|
||||
### On-demand fetch boundary
|
||||
|
||||
`ChunkFetcher` claims newly requested identifiers synchronously while handling the `missingChunks` event. The claim remains active through:
|
||||
|
||||
- queueing and concurrency scheduling;
|
||||
- configured interval throttling;
|
||||
- entry into the injected bounded-activity runner;
|
||||
- `fetchRemoteChunks`;
|
||||
- response validation;
|
||||
- local database persistence; and
|
||||
- fetched or missing event delivery.
|
||||
|
||||
The claim settles on every terminal path, including explicit absence, no active replicator, rejection, invalid results, destruction, and cancellation. Its completion Promise is the task passed to the bounded `chunk-fetch` activity, keeping Wake Lock, application lifecycle deferral, the remote-work indicator, and missing-chunk delivery aligned.
|
||||
|
||||
### Five-minute leak fuse
|
||||
|
||||
An accepted on-demand claim has a separate five-minute inactivity fuse. This is a last-resort leak safety valve, not a chunk-arrival budget or a remote-request timeout.
|
||||
|
||||
Its purpose is to prevent a faulty integration, a never-settling Promise, or a stalled transport from retaining logical ownership indefinitely. When it fires, the coordinator releases the per-identifier claim and its waiter. If the bounded activity callback has been entered, resolving the claim also allows the associated Wake Lock, application-lifecycle deferral, and remote-work indicator to be released. `ChunkFetcher` refreshes the fuse only at observable progress points, such as entering the activity boundary, beginning and completing throttling or transfer, and completing persistence.
|
||||
|
||||
Five minutes is deliberately a conservative operational limit, not a value derived from a network protocol, a benchmark, or evidence that a missing chunk will arrive within that period. Firing the fuse neither proves remote absence nor makes the underlying request safe to abort. The current `fetchRemoteChunks` contract has no `AbortSignal`, so a physical request may still complete after its logical claim has been released. A future cancellable transport contract should add transport-specific deadlines and explicit cancellation without changing the lifecycle-based wait rule.
|
||||
|
||||
### Continuous replication
|
||||
|
||||
The unbounded live channel is not a quiescence gate because it has no natural end. Its initial pull-only catch-up is finite, however, and must enter `runFiniteReplicationActivity`. This includes the one-shot parameter fallback chain: every retry remains within the catch-up boundary until it succeeds or stops. If continuous replication later restarts with adjusted parameters, the new initial catch-up enters a new finite boundary.
|
||||
|
||||
Once the live channel has begun, a chunk delivered through it still resolves an existing waiter immediately, but the channel itself does not keep a new waiter open. CouchDB on-demand fetching supplies its own per-identifier claim. If a future defect demonstrates a delivery race inside a live batch, that batch lifecycle should be exposed explicitly rather than approximated with another elapsed delay.
|
||||
|
||||
## Ownership
|
||||
|
||||
`ReplicatorService` owns both the broad bounded-operation count and the narrower finite-replication count. It is the common lifecycle owner for CouchDB, sequential, and P2P replicators.
|
||||
|
||||
`LayeredChunkManager` owns one `ChunkDeliveryCoordinator` and supplies it to its arrival layer and `ChunkFetcher`. `ArrivalWaitLayer` owns waiter resolution and the final local database recheck. It depends on the narrow coordinator capability rather than on `ReplicatorService` itself.
|
||||
|
||||
`ChunkFetcher` owns per-identifier claims because it knows when each identifier enters its queue and reaches a terminal result.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Preserve immediate reads when `waitForDelivery` is false or the deprecated call-site `timeout` is zero or negative.
|
||||
- Treat a positive deprecated `timeout` only as source-compatible opt-in to lifecycle waiting. Its numeric value no longer represents an arrival duration.
|
||||
- Preserve `preventRemoteRequest`: no on-demand request is dispatched, although an already-active finite replication may satisfy the waiter.
|
||||
- Preserve Promise sharing for concurrent reads of the same chunk identifier.
|
||||
- Preserve immediate explicit remote-missing results.
|
||||
- Do not change which remote types support direct on-demand fetching.
|
||||
|
||||
## Historical Evidence and Scope
|
||||
|
||||
This decision addresses the lifecycle-race class rather than treating every ‘Load failed’ report as a timeout:
|
||||
|
||||
- [Issue #166](https://github.com/vrtmrz/obsidian-livesync/issues/166) contained logs where chunk collection failed shortly before related chunk writes appeared. It is evidence for the timing class, although that issue's hidden-file start-up path was repaired separately and is not claimed as a direct regression test here.
|
||||
- The 2021 timing fixes in [commit `39e2eab0`](https://github.com/vrtmrz/obsidian-livesync/commit/39e2eab0238d9c37e3653cdec884cbeed543fc23) and the extended leaf timeout in [commit `9facb577`](https://github.com/vrtmrz/obsidian-livesync/commit/9facb577601d8aceff7df547cd2a6f9357fdaa29) show that elapsed timeout values have historically been used to absorb the same ordering uncertainty. They do not provide a protocol basis for retaining 5-second or 30-second delays.
|
||||
- Replication pacing introduced by [commit `8d66c372`](https://github.com/vrtmrz/obsidian-livesync/commit/8d66c372e15c43a2de84a223c6385077b7724eec) and commonlib [commit `051b50c`](https://github.com/vrtmrz/livesync-commonlib/commit/051b50ca38ec4c05a11e8216ac259b4488b825f0) is a direct precedent for preventing replication progress from outrunning chunk collection. The present design expresses that dependency as an explicit lifecycle and completion recheck.
|
||||
- [Issue #505](https://github.com/vrtmrz/obsidian-livesync/issues/505) was traced to chunks which were genuinely absent after the former bulk-send option broke the chunks-before-metadata guarantee. Waiting cannot recreate missing data, so this decision does not claim to fix it.
|
||||
- [Issue #771](https://github.com/vrtmrz/obsidian-livesync/issues/771) and [Issue #986](https://github.com/vrtmrz/obsidian-livesync/issues/986) contain ambiguous or version-dependent `Load failed` reports. They remain unclaimed until the original writer and database state can be reproduced.
|
||||
|
||||
The detailed setting and replicator matrix is recorded in [Chunk Retrieval and Waiting](../design_docs/chunk_retrieval_and_waiting.md).
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Increase the fixed timeout constants
|
||||
|
||||
Any fixed total duration can still expire immediately before a queued or progressing operation reports its result. Larger values also make genuine failures slower without defining what the system is waiting for.
|
||||
|
||||
### Start another grace period after finite completion
|
||||
|
||||
A successful finite replication has already reached its latest sequence, and the completion recheck observes documents persisted without a waiter event. Waiting an additional 5 or 30 seconds has no identified producer to wait for and merely retains the historical approximation.
|
||||
|
||||
### Observe all bounded remote activity
|
||||
|
||||
The broad count includes operations which cannot provide the requested chunk. Using it as the delivery gate lets unrelated work delay a read and makes its completion semantically meaningless. A separate finite-replication count avoids that leak.
|
||||
|
||||
### Keep a fallback timer for unobserved delivery
|
||||
|
||||
An unobserved producer has no defined start, progress, or completion semantics. A timer would therefore be a guess rather than a safety property. Relevant delivery paths must claim their work synchronously or expose a finite replication boundary; otherwise the read returns unavailable.
|
||||
|
||||
### Remove every timer
|
||||
|
||||
The arrival wait has no elapsed timer, but an implementation fault can leave a delivery claim unresolved forever. The five-minute inactivity fuse bounds that leaked logical state without being used as a successful delivery condition.
|
||||
|
||||
## Verification
|
||||
|
||||
Unit tests use deterministic clocks and deferred Promises to cover:
|
||||
|
||||
- a finite replication which lasts well beyond the former arrival values;
|
||||
- successful finite completion causing a cache-bypassing local database recheck;
|
||||
- immediate unavailability when no producer is observable;
|
||||
- per-identifier claims covering queueing, throttling, remote fetch, validation, persistence, and event delivery;
|
||||
- explicit missing, no-replicator, rejection, invalid response, cancellation, runner rejection, and teardown paths;
|
||||
- overlapping claims and finite replications;
|
||||
- a runner which never enters the task and a request which never settles;
|
||||
- the five-minute fuse being refreshed by observable progress;
|
||||
- continuous replication's finite initial catch-up, including its parameter fallback path; and
|
||||
- the setting and replicator decision matrix.
|
||||
|
||||
Integration-style unit tests exercise `LayeredChunkManager`, `ChunkFetcher`, a memory-backed PouchDB database, and a deferred fake replicator together. A real Obsidian test is not required because the change remains behind the existing database, service, and event boundaries and does not alter platform UI or an adapter contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A healthy finite replication or on-demand request no longer loses a race against an unrelated wall-clock estimate.
|
||||
- Successful finite replication completion provides a precise latest boundary for missing-chunk reads.
|
||||
- The local recheck closes event-delivery and cache timing gaps without extending the wait after completion.
|
||||
- Reads no longer pause for 5 or 30 seconds when no observable operation can deliver the chunk.
|
||||
- Relevant producers must expose a lifecycle and must continue to prove cleanup on every exceptional path.
|
||||
- The five-minute fuse bounds leaked logical activity, but it neither establishes remote absence nor cancels a physical request.
|
||||
347
docs/adr/2026_07_common_library_package_boundary.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# Architectural Decision Record: Package the Common Library Behind Explicit Host Boundaries
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — the package boundary is implemented, published pre-release artefacts have been verified as exact downstream dependencies, and the Community directory scanner preview confirms the intended source boundary. Each later release candidate still requires immutable artefact and downstream validation.
|
||||
|
||||
## Context
|
||||
|
||||
Self-hosted LiveSync currently consumes `livesync-commonlib` as the `src/lib` Git submodule. TypeScript, Vite, Vitest, the CLI, Webapp, and WebPeer resolve `@lib/*` directly to the submodule's TypeScript source. The common-library repository has no package manifest, standalone build, export map, or self-contained test command. Its tests and dependency versions are consequently supplied by the Self-hosted LiveSync repository.
|
||||
|
||||
Source archives do not populate a Git submodule. Self-hosted LiveSync therefore also commits generated declarations under `_types` and resolves `@lib/*` to those declarations as a fallback. This makes one logical dependency appear in the plug-in repository twice: once as a submodule checkout and once as generated declarations. Release preparation must regenerate and commit the fallback, and repository scanners can report generated lint directives as if they were maintained plug-in source.
|
||||
|
||||
The Obsidian community scanner currently inspects repository source beyond the plug-in entry point. Reports include `src/apps/cli`, `src/apps/webapp`, `src/apps/webpeer`, and generated `_types`. Moving the common library from `src/lib` to another source directory or a workspace package inside the Self-hosted LiveSync repository would therefore preserve the scan surface. It could remove the submodule fallback, but it would not create the same dependency boundary as consuming a published npm package.
|
||||
|
||||
The common library is already a substantial shared domain layer. At the time of this decision it contains 270 non-test TypeScript or Svelte source files. At Self-hosted LiveSync revision `e114f66fb2b7c6f3fec2d53701f6638d2557e606`, source outside `src/lib` uses 180 distinct raw `@lib/*` specifiers across plug-in, application, and test source. Normalising optional TypeScript source suffixes leaves 140 migration paths. The plug-in contributes most of those imports, but the CLI, Webapp, and WebPeer also depend on the library. Treating every existing deep path as a permanent public API would make future refactoring impractical.
|
||||
|
||||
The intended external use is not new:
|
||||
|
||||
- [commonlib issue #1](https://github.com/vrtmrz/livesync-commonlib/issues/1) has requested an npm package and an established API since 2022;
|
||||
- [Self-hosted LiveSync issue #87](https://github.com/vrtmrz/obsidian-livesync/issues/87) asks for a client with list, get, submit, and delete operations;
|
||||
- [commonlib issue #10](https://github.com/vrtmrz/livesync-commonlib/issues/10) asks for an API which lets another application add Markdown files to the CouchDB data model; and
|
||||
- [commonlib issue #13](https://github.com/vrtmrz/livesync-commonlib/issues/13) records the integration cost of consuming a Git submodule and compiling its source.
|
||||
|
||||
Existing external consumers use custom TypeScript aliases, loaders, and host stubs to reach `DirectFileManipulator` and other deep modules. The present source shape therefore imposes real integration work without supplying a stable contract.
|
||||
|
||||
The library is close to being self-contained, but it crosses its boundary in a small number of important places:
|
||||
|
||||
- four files depend on Self-hosted LiveSync's event hub or event identifiers;
|
||||
- `KeyValueDBService` imports the host's concrete database-opening function;
|
||||
- `ObsidianServiceContext` imports the plug-in class and Obsidian types from the parent repository; and
|
||||
- `coreEnvFunctions` imports an Obsidian function type even though the module is intended to be host-neutral.
|
||||
|
||||
The library also mixes domain logic with host presentation:
|
||||
|
||||
- Svelte dialogue mounting depends on Svelte, LiveSync service context, application-lifecycle events, translation, and cancellation policy in one implementation;
|
||||
- Obsidian context and setup helpers remain below the common-library directory;
|
||||
- browser dialogue shims and LiveSync Svelte components are shipped alongside headless replication code; and
|
||||
- the translation implementation statically imports the complete generated catalogue. The generated message modules and JSON catalogues account for approximately 1.5 MB of source and generated data, even when a headless consumer does not require them.
|
||||
|
||||
The root `src/index.ts` currently exports only `DirectFileManipulator` and its options. `DirectFileManipulator` is useful evidence for a future SDK, but it is not yet a sufficient stable façade: initialisation starts from its constructor, enumeration remains unfinished, watch ownership and failure semantics are not documented, and conflict and concurrency semantics are not defined as a public guarantee.
|
||||
|
||||
The present process model also assumes one main LiveSync instance. Event dispatch, translation state, environment configuration, offline-scan state, worker pools, synchronisation-parameter handlers, diagnostic counters, and some compatibility caches are module-scoped. Some of these are safe process-wide facilities, while others can allow two client instances to influence each other. Publishing a package makes concurrent clients in one process a supported possibility, so this state must be classified rather than carried across accidentally.
|
||||
|
||||
## Decision
|
||||
|
||||
Maintain `livesync-commonlib` as the authoritative independent repository and publish its compiled output as the scoped, pre-1.0 npm package `@vrtmrz/livesync-commonlib`.
|
||||
|
||||
Self-hosted LiveSync, its CLI, Webapp, WebPeer, and external tools will consume the compiled package and declarations through package export maps. They must not compile the common library's source through a path alias. The package lock records the exact resolved artefact used to build a plug-in release.
|
||||
|
||||
The common-library repository may become a small workspace if independently useful artefacts emerge, but it does not move into the Fancy Kit repository or the Self-hosted LiveSync plug-in repository. This preserves its domain ownership, existing history, issues, and independent release cadence while making the plug-in repository a package consumer.
|
||||
|
||||
The first package release is an infrastructure and compatibility release, not a declaration that every internal service is stable. It exposes:
|
||||
|
||||
- a small documented root API;
|
||||
- named, task-oriented public subpaths;
|
||||
- explicitly marked compatibility subpaths required to migrate current Self-hosted LiveSync imports; and
|
||||
- no unrestricted source-directory wildcard as a permanent contract.
|
||||
|
||||
Compatibility subpaths may be exported during the migration, but they remain pre-1.0 and are documented as internal. New external integrations should use the high-level client façade rather than reproduce Self-hosted LiveSync's internal Service Hub composition.
|
||||
|
||||
### Domain ownership
|
||||
|
||||
The common library continues to own behaviour which defines the Self-hosted LiveSync data and synchronisation model:
|
||||
|
||||
- document, metadata, chunk, setting, and protocol types;
|
||||
- path and identifier encoding;
|
||||
- chunk splitting, hashing, compression, encryption, and content reconstruction;
|
||||
- PouchDB-facing data access and replication primitives;
|
||||
- CouchDB, Object Storage, and P2P replication domain logic;
|
||||
- conflict, chunk-delivery, storage-event, and replication managers;
|
||||
- platform-neutral storage and service contracts;
|
||||
- headless composition; and
|
||||
- the future high-level client façade.
|
||||
|
||||
The Self-hosted LiveSync repository owns plug-in and product integration:
|
||||
|
||||
- `ObsidianServiceContext` and every reference to the plug-in class, `App`, `Plugin`, or Obsidian lifecycle;
|
||||
- Setup Wizard components and LiveSync-specific presentation policy;
|
||||
- Obsidian menus, notices, settings panes, and dialogue composition;
|
||||
- plug-in event wiring which is not part of the replication protocol; and
|
||||
- the concrete initialisation of injected environment, translation, storage, key-value database, and UI capabilities.
|
||||
|
||||
Browser-only implementations may remain in the common-library repository only when they implement a documented host-neutral contract used by more than one browser consumer. LiveSync-specific Webapp and WebPeer composition remains application code even if it later moves out of the plug-in repository.
|
||||
|
||||
Neutral utilities which do not express LiveSync data, storage, replication, or UI policy may move to `octagonal-wheels` after more than one consumer proves the abstraction. Reusable Obsidian adapters may move to `@vrtmrz/obsidian-plugin-kit`. Neither package becomes an owner of LiveSync domain behaviour.
|
||||
|
||||
### Dependency inversion
|
||||
|
||||
The package must contain no `@/` import and no direct Obsidian import. The current reverse dependencies are removed as follows:
|
||||
|
||||
- common protocol and service events live with their common-library contracts;
|
||||
- host-only event reactions are registered by Self-hosted LiveSync at its composition root;
|
||||
- `KeyValueDBService` receives an `openKeyValueDatabase` factory through constructor or service dependencies;
|
||||
- `ObsidianServiceContext` moves to Self-hosted LiveSync;
|
||||
- the language getter uses a local function type rather than importing the Obsidian declaration; and
|
||||
- browser globals, fetch, crypto, timers, storage, and document access are obtained through explicit host capabilities where behaviour varies by runtime.
|
||||
|
||||
Temporary package-level configuration is acceptable where converting every call site in one change would be unsafe, but configuration must be instance-scoped wherever multiple clients can coexist. Importing the package must not patch `HTMLElement`, `SVGElement`, or another global prototype. Webapp compatibility patches belong in Webapp bootstrap code.
|
||||
|
||||
Every mutable module-level value is classified as one of:
|
||||
|
||||
- immutable or safely shared process infrastructure;
|
||||
- an explicitly keyed cache with bounded ownership and disposal; or
|
||||
- client state which moves behind an instance-owned service or dependency.
|
||||
|
||||
In particular, event subscriptions, translation selection, offline-scan maps and timers, synchronisation-parameter handlers, and database transformation policy must not leak between independent client instances. A high-level client owns an explicit asynchronous disposal path which removes subscriptions, cancels work it owns, and releases its instance state.
|
||||
|
||||
### Translation resources
|
||||
|
||||
Translation lookup is separated from the application catalogue. Commonlib owns a typed canonical English definition for the messages requested by its services and uses it when a host omits the optional translator capability. This ensures that another consumer receives meaningful English rather than symbolic keys.
|
||||
|
||||
Self-hosted LiveSync owns the complete multilingual catalogue, generation tools, selected-language state, and translator implementation. It injects that translator into the Obsidian, CLI, and browser composition roots. Commonlib does not publish `compat/common/i18n`, `compat/common/rosetta`, generated language modules, or JSON catalogues.
|
||||
|
||||
The canonical Commonlib key type and English fallback change with Commonlib. LiveSync may add translations for those keys without duplicating every English definition; its translator delegates absent keys to Commonlib's canonical English fallback. A separate language package remains possible only if independent consumers and release cadence later justify it; core must never depend on an application catalogue.
|
||||
|
||||
LiveSync-owned wording may remain in a typed, application-local provisional English map while it is being exercised. The LiveSync translator composes those keys with the generated application catalogue and the Commonlib key type. Moving a stable message into LiveSync's YAML catalogue makes it available for translation without changing Commonlib.
|
||||
|
||||
### Svelte dialogue hosting
|
||||
|
||||
The present Svelte dialogue implementation is split into three responsibilities:
|
||||
|
||||
1. an Obsidian `Modal` host which owns content, title, close, one-shot settlement, and disposal;
|
||||
2. a Svelte adapter which mounts and unmounts a component in that host; and
|
||||
3. Self-hosted LiveSync policy which supplies service context, reacts to plug-in unload, requires explicit cancellation in selected workflows, translates messages, and styles Setup Wizard content.
|
||||
|
||||
Fancy Kit is an appropriate owner for the first responsibility when the contract is useful beyond this migration. The Kit API should be framework-neutral: it accepts a mount callback, exposes typed `resolve`, `cancel`, `close`, and `setTitle` controls, and accepts a disposer returned by the callback. It returns a `Promise<TResult | null>` which settles once, and it owns safe-area, viewport, focus, and Obsidian Modal lifecycle guarantees.
|
||||
|
||||
Fancy Kit does not add Svelte to its core dependencies for this migration. Self-hosted LiveSync initially owns the small Svelte adapter which calls Svelte's `mount` and `unmount` through the framework-neutral host. The browser dialogue host also remains outside the Obsidian plug-in kit. If a second Obsidian plug-in needs the same Svelte adapter, it can be promoted to an optional Kit subpath with an optional Svelte peer dependency, or to a separate `@vrtmrz/obsidian-svelte-kit` package. That promotion must preserve the existing `sideEffects: false` and focused-import guarantees for consumers which do not use Svelte.
|
||||
|
||||
Self-hosted LiveSync's `openWithExplicitCancel` retry policy, application service context, and Setup Wizard components do not move to Fancy Kit.
|
||||
|
||||
Arbitrary component mounting is not added to the neutral `UiInteractions` contract. A component instance is a framework and host integration detail rather than a portable interaction request which a generic driver can serialise or answer. A LiveSync workflow which needs a hosted component defines and receives its own narrow, typed dialogue capability; Self-hosted LiveSync composes that capability from the Kit Modal host and its local Svelte adapter.
|
||||
|
||||
### Package artefacts
|
||||
|
||||
The common-library package publishes compiled ESM and generated declaration files. Consumers do not receive raw TypeScript as the runtime entry point. The package must provide:
|
||||
|
||||
- an explicit `exports` map;
|
||||
- declaration maps where they remain useful for debugging;
|
||||
- browser and Node entry points where implementations differ;
|
||||
- accurate `sideEffects` metadata;
|
||||
- explicit runtime, peer, and optional dependencies;
|
||||
- no unresolved `@lib/*`, `@/*`, Vite query, or source `.ts` import in published JavaScript or declarations;
|
||||
- an `npm pack` contents check; and
|
||||
- clean-install consumer fixtures for Node, a browser bundle, and Self-hosted LiveSync.
|
||||
|
||||
Svelte source, worker query imports, AWS SDK adapters, PouchDB adapters, and Node-only crypto must not leak through the root entry point. Optional feature subpaths may carry their own heavier dependency surface.
|
||||
|
||||
### Platform host entries
|
||||
|
||||
Platform-specific host access is explicit rather than inferred. `@vrtmrz/livesync-commonlib/node` supplies Node-only capabilities and `createNodeStorage({ rootPath })`. `@vrtmrz/livesync-commonlib/browser` supplies `createFileSystemAccessStorage({ rootHandle })` for the browser File System Access API. Browser bundles cannot resolve the Node implementation through the browser entry.
|
||||
|
||||
Both storage factories receive an existing root from their host. Commonlib does not choose a process directory, present a browser directory picker, request browser permission, or persist a `FileSystemDirectoryHandle`. The CLI owns path selection and configuration. The Webapp owns user activation, permission, handle persistence, and re-authorisation. The adapters own only path containment and storage operations below the injected root.
|
||||
|
||||
Here, the browser capability means the browser File System Access API, not Node's `fs` API. The package proof deliberately moves the rooted `IStorageAdapter` implementation first. The Webapp retains its LiveSync-specific `IFileSystemAdapter` composition, file-object cache, Vault semantics, picker flow, and storage-event policy. Those responsibilities can move later only with their own documented contracts; they are not implied by the low-level browser entry point.
|
||||
|
||||
A later composition may place the constructed storage contract on a host-specific `ServiceContext` subtype, so services depend only on the injected capability while each platform owns context initialisation. This proof does not add storage or raw platform objects to the base `ServiceContext`: doing so would make an optional application capability mandatory for Obsidian, CLI, Webapp, WebPeer, and test contexts at the same time. A future change should inject the narrow `IStorageAdapter` contract rather than expose `FileSystemDirectoryHandle`, Node `fs`, or environment detection through the shared context.
|
||||
|
||||
The paired adapters run against the same contract suite for metadata, text and binary access, append, listing, removal, missing paths, parent creation, empty-root handling, and traversal rejection. Timestamp fidelity remains platform-specific because the File System Access API does not provide the same creation-time and timestamp-setting facilities as Node.
|
||||
|
||||
The Node adapter rejects symbolic-link components which it observes in adapter paths before an operation and opens file entries without following the final link where the platform supports that flag. The CLI delegates its Vault reads, writes, discovery, deletion, and atomic rename to this rooted adapter rather than maintaining a second direct `fs` path. This rejects traversal paths and symbolic links which already exist when a remote-derived operation begins, while retaining case-only and cross-directory rename behaviour.
|
||||
|
||||
This adapter is not an operating-system filesystem sandbox. The CLI assumes that the host-selected Vault root and its local filesystem are trusted and are not concurrently rewritten by an untrusted local process. A privileged CLI must therefore not use a Vault root which is writable by a less-trusted local actor.
|
||||
|
||||
The Node entry also centralises direct Node built-in access needed by trusted headless application code. This is a package and scanner boundary, not an assertion that Node and browser APIs are interchangeable. Cross-platform behaviour belongs in a shared contract with separate implementations, as demonstrated by rooted storage.
|
||||
|
||||
### Standard input and output
|
||||
|
||||
Commonlib's `context` entry exposes a narrow `StandardIo` contract for command-line composition. It reads UTF-8 standard input, asks one line-oriented question, and writes text or binary chunks to standard output and standard error without adding delimiters. Commonlib's `node` entry supplies `createNodeStandardIo()`, which binds that contract to host-selected Node streams and defaults to the current process streams.
|
||||
|
||||
Self-hosted LiveSync constructs the Node implementation at the CLI composition root and places the exact instance on `NodeServiceContext`. `NodeServiceHub` must receive that Context rather than silently constructing one. Command handlers reach input, prompts, and protocol output through the Context, so unit tests can inject memory I/O without replacing process globals. Obsidian and browser Contexts do not acquire a fictitious terminal capability, and the base `ServiceContextContract` remains limited to capabilities required by every composition.
|
||||
|
||||
Standard I/O does not own command-line arguments, exit codes, signals, stream lifecycle, log levels, or log persistence. Diagnostic logging remains the responsibility of the existing API and Logger composition. CLI adapters receive a narrow diagnostic callback wired to the service logging API instead of calling `console` directly. A CLI host may render selected logs on standard error, but Logger is not added to `StandardIo` or made mandatory on the base Context.
|
||||
|
||||
Commonlib verifies memory injection, split UTF-8 decoding, object-mode rejection, text and binary output, and prompting against injected Node streams. The downstream CLI verifies Context identity and exact injected I/O in unit tests, then runs the built Node artefact through its Deno command E2E.
|
||||
|
||||
### Context and result compatibility
|
||||
|
||||
`ServiceContextContract` is the minimum host-neutral composition contract. It supplies an event channel and message translator selected by the host. Obsidian and CLI contexts extend the default implementation with their own capabilities; the Webapp currently uses the default implementation. Every Service Hub and every service in one composition must retain the exact Context object supplied by that host.
|
||||
|
||||
Compatibility is established through observable results, not only structural TypeScript compatibility. A shared probe verifies event delivery, translation results, and Context identity across the public Service Hub surface. Commonlib separately verifies that default contexts isolate their event channels and translators. Real Obsidian runs the same invariants against the loaded plug-in through `obsidian-cli eval`; the CLI runs its composition contract in unit tests and then exercises the built Node artefact through Deno E2E; and the Webapp composition runs the shared contract directly without depending on its currently stale Playwright workflow.
|
||||
|
||||
The same rule applies as more APIs move: define the shared result set first, run the same cases against each implementation, and document platform-specific behaviour outside that result set. Matching method names or return types alone does not establish behavioural compatibility. The contract runner remains test support during this proof rather than becoming a new public package API.
|
||||
|
||||
### Barrels and export surfaces
|
||||
|
||||
The migration does not prohibit every barrel. A small root entry point or task-oriented subpath entry point is an intentional package contract when it has one documented responsibility, uses explicit named exports, and does not load unrelated implementations or optional dependencies. The root client entry point, a focused RPC entry point, and type-only storage-adapter entry points are examples which may remain after review.
|
||||
|
||||
An existing barrel or forwarding façade is removed when the migration provides a clearer import and the barrel:
|
||||
|
||||
- aggregates unrelated domains or exposes implementation layout as API;
|
||||
- hides a platform, UI framework, worker, database adapter, or another optional dependency;
|
||||
- makes side effects or tree-shaking behaviour difficult to determine;
|
||||
- merely re-exports another package without adding a LiveSync-owned contract; or
|
||||
- exists only to preserve the present `@lib/*` source alias or Service Hub composition.
|
||||
|
||||
In particular, the broad `common/types` barrel is retained only as a temporary compatibility path while focused settings, model, protocol, and path contracts are extracted. The `InjectableServices` forwarding barrel is removed in favour of explicit compatibility imports. Forwarding exports for `octagonal-wheels` facilities are removed where consumers can import the owning package directly; mixed modules such as conversion and utility modules are split when this prevents neutral dependencies from being presented as LiveSync-owned API.
|
||||
|
||||
Every retained barrel must correspond to an explicit `exports` entry, list named exports rather than use an unrestricted wildcard, and have a packed-consumer or bundle test proving that unrelated optional code is not loaded. Removing a barrel is not sufficient reason to expose every underlying file as a public subpath.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 0: Record and enforce the boundary
|
||||
|
||||
- Add a package-boundary check which rejects `@/` and direct Obsidian imports in common-library production source.
|
||||
- Record the current Self-hosted LiveSync `@lib/*` import inventory and classify each path as public, compatibility-only, host-owned, or obsolete.
|
||||
- Inventory mutable module-level state and add a two-client isolation test before promising a public client API.
|
||||
- Add standalone test and type-check configuration to the common-library repository while retaining downstream Self-hosted LiveSync CI.
|
||||
- Add a packed-consumer fixture before changing Self-hosted LiveSync resolution.
|
||||
|
||||
### Phase 1: Remove host leaks
|
||||
|
||||
- Move `ObsidianServiceContext` and `setupObsidian` presentation code to Self-hosted LiveSync.
|
||||
- Inject the key-value database factory and host event reactions.
|
||||
- Replace the Obsidian language type import with a local contract.
|
||||
- Move global DOM compatibility mutation to the browser application bootstrap.
|
||||
- Introduce translator injection and detach core imports from the full catalogue.
|
||||
- Split the generic dialogue host, Svelte adapter, and LiveSync policy without changing visible behaviour.
|
||||
|
||||
### Phase 2: Build the package
|
||||
|
||||
- Add the package manifest, compiled ESM build, declarations, export map, and package documentation.
|
||||
- Keep the root export deliberately small.
|
||||
- Remove accidental and forwarding barrels where focused imports are clearer; retain only reviewed package entry points with explicit named exports.
|
||||
- Add temporary compatibility subpaths required by the classified 140-path migration inventory.
|
||||
- Test Node, browser, worker, PouchDB, Object Storage, P2P, and headless entry points independently.
|
||||
- Verify that installing a core entry does not pull Svelte UI or language catalogue code into a representative bundle.
|
||||
|
||||
### Phase 3: Publish and validate a pre-release
|
||||
|
||||
- Publish an immutable pre-1.0 version to a pre-release npm dist-tag.
|
||||
- Verify its package name, provenance, checksum, export map, and packed files.
|
||||
- Run common-library tests against the packed artefact rather than the source checkout.
|
||||
- Run downstream Self-hosted LiveSync type checks, unit tests, integration tests, CLI tests, Webapp tests, WebPeer tests, production builds, and focused real-Obsidian E2E against the exact package version.
|
||||
- Validate at least one external consumer which currently uses a submodule or custom loader.
|
||||
|
||||
### Phase 4: Convert Self-hosted LiveSync
|
||||
|
||||
- Replace `@lib/*` source aliases with package imports.
|
||||
- Remove the `src/lib` submodule, `_types`, `tsconfig.types.json`, `generate-types.mjs`, and release-workflow steps which regenerate fallback declarations.
|
||||
- Move the multilingual catalogue and its generation tooling into Self-hosted LiveSync, while retaining only Commonlib's typed English fallback in the package.
|
||||
- Keep application-owned source under `src/apps` until a separate decision moves an application.
|
||||
- Run the community scanner's branch or commit preview before releasing the converted plug-in.
|
||||
|
||||
### Phase 5: Stabilise the SDK
|
||||
|
||||
- Design a high-level asynchronous client around explicit `create`, `list`, `get`, `put`, `delete`, `watch`, and `close` lifecycles.
|
||||
- Specify path normalisation, encryption negotiation, conflict handling, conditional writes, deletion, history, and resource disposal before declaring the façade stable.
|
||||
- Adapt `DirectFileManipulator` behind that façade or deprecate it; do not treat its current constructor and deep dependencies as the final API.
|
||||
- Narrow or remove compatibility-only export paths as Self-hosted LiveSync imports migrate to documented package modules.
|
||||
|
||||
## Implementation Proof
|
||||
|
||||
The package proof builds Commonlib as one compiled ESM package with a small root, `context`, `settings`, `remote-configurations`, `browser`, `node`, and `rpc` entries, plus only the explicit compatibility exports required by the reviewed downstream inventory. It publishes neither raw TypeScript nor Svelte source, uses no unrestricted export wildcard, and can be installed into a clean consumer, imported in Node, type-checked from declarations, and bundled independently for browser context, browser storage, browser services, and workers. Release validation records and compares the immutable registry version and checksum.
|
||||
|
||||
The compatibility inventory is regenerated from the maintained Self-hosted LiveSync consumer. Focused entries replace compatibility imports when their contracts are proven; obsolete paths are removed from the next candidate rather than retained merely because an earlier immutable release contained them. The multilingual `common/i18n` and `common/rosetta` paths have been removed in this way after ownership moved to the host.
|
||||
|
||||
The proof found and fixed three boundary defects which source-alias consumption had hidden: compiled JSON imports required explicit output extensions, precompiled Svelte output could not safely be treated as source by the downstream Svelte pipeline, and Vite's default client conditions selected Commonlib's browser worker while building the Node CLI. Packed-consumer regressions cover the first two. The CLI now uses Vite's server conditions and treats every Node built-in reported by Commonlib's Node entry as external; the built CLI is exercised through Deno E2E. Importing root or context also no longer patches DOM prototypes, translator injection prevents the context entry from loading the complete language catalogue, and standard input and protocol output are supplied by the package-owned host contract rather than direct stream access in command handlers.
|
||||
|
||||
Self-hosted LiveSync, its CLI, Webapp, WebPeer, plug-in source, and tests compile against that exact registry artefact without `@lib/*`. Focused downstream storage tests pass against the package-owned Node and File System Access API implementations. Commonlib also owns the Trystero implementation and version; the host retains no direct Trystero dependency, preventing two transport generations from entering one application graph. The old `src/lib` Git submodule, generated `_types` fallback, type-generation scripts, and source aliases are removed by the proof branch.
|
||||
|
||||
Commonlib's contract and complete suites cover Context results, both platform storage implementations, standard I/O, replication, settings, and package consumption. Self-hosted LiveSync runs the same host-composition result contract for Obsidian, CLI, and browser compositions against the exact packed artefact, followed by its unit, integration, application-build, CLI E2E, and focused real-Obsidian gates.
|
||||
|
||||
The package-owned Trystero transport also completes the canonical Compose P2P synchronisation workflow with a local relay and two isolated CLI peers. In real Obsidian, the plug-in starts with one consistent `ObsidianServiceContext`, the representative server-selection and Setup URI Svelte dialogues mount and close through their normal controls, their mobile variants satisfy viewport, safe-area, and touch-target assertions, and the settings pane exposes only the effective deletion controls. These runtime checks complement the package tests without making Webapp maintenance the primary release gate.
|
||||
|
||||
The Community directory scanner preview completes with no source-code errors. The former findings attributed to generated `_types`, raw `src/lib` source, Node built-ins, forbidden rule suppressions, unsupported Obsidian APIs, and undescribed directive comments are absent. This confirms that the registry dependency is recognised as a package boundary. The preview identified behaviour-neutral redundant CLI candidate types and Webapp File System Access API assertions; these are corrected in the host source rather than carried as known warnings.
|
||||
|
||||
The remaining source warnings belong to application code. Browser dialogue visibility now uses DOM state instead of inline static styling, so the earlier styling warning is absent. Direct diagnostic output was resolved at its existing ownership boundaries: Webapp components use an injected log function backed by `BrowserAPIService`, WebPeer retains output in its Svelte log store, Obsidian modules use the established Logger path, and duplicate console emission was removed from `ModuleLog`. The later Webapp and WebPeer recomposition around maintained Context and serviceFeature APIs should preserve these explicit output paths.
|
||||
|
||||
The final Community lint inventory for this boundary has no errors and 126 warnings: 67 sentence-case findings, 58 deprecated-API findings, and one declarative setting-definition suggestion. The sentence-case strings and deprecated interfaces are retained deliberately to avoid an unrelated localisation and host-lifecycle change. Declarative definitions would migrate the complete Obsidian setting tab into the 1.13 settings-search model; that is a separate visible UI project after LiveSync 1.0, not a hidden package-boundary release gate. Revisit each category through focused UI and compatibility work rather than suppressing the rules or treating the warning count as zero.
|
||||
|
||||
WebPeer's production build still reports that Vite externalises the guarded Node `crypto` fallback reached through a compatibility path. Browser execution selects `globalThis.crypto`, and the focused root, `context`, and `browser` bundle checks do not include the Node fallback, so this is not a leak in the reviewed public browser entries. Removing the compatibility-build warning requires a focused crypto-capability contract or a platform-specific implementation split and remains part of compatibility-surface narrowing.
|
||||
|
||||
The dependency preview also reports `uuid`, but the installed and locked graph resolves PouchDB's UUID dependency to the patched `uuid@11.1.1` through the repository override, and `npm audit` does not report the UUID advisory. The scanner appears to infer the older declared PouchDB range rather than the resolved override, so this warning is treated as a scanner false positive unless a packed-artefact inspection shows otherwise.
|
||||
|
||||
The 1.0 dependency review found newly disclosed parser and denial-of-service advisories with compatible fixes. All locked `brace-expansion` generations now use their patched releases, including the production generation reached by Commonlib path matching and the CLI's user-configured ignore patterns. The development-only ESLint and Istanbul `js-yaml` generations likewise use patched releases. The production Markdown parser uses the patched `linkify-it` release to avoid quadratic processing of maliciously structured `mailto:` links, while the development-only JSON Schema toolchain uses the patched `fast-uri` release for unambiguous hostname parsing. A clean install and both complete and production-only `npm audit` checks no longer report these packages.
|
||||
|
||||
The remaining audit report is the existing `werift` and `werift-ice` dependency on `ip`, for which npm offers no patched version. The advisory concerns `ip.isPublic()` misclassifying unusual loopback representations. The locked werift implementation uses `ip` for address encoding, decoding, format detection, and loopback filtering, but does not call `isPublic()` or `isPrivate()`. LiveSync reaches werift only through the Node CLI's injected `RTCPeerConnection`; the Obsidian plug-in and browser applications use their platform WebRTC implementation, and the plug-in artefact does not contain werift. The package-level finding is therefore accepted for the 1.0 integration preview as a non-reachable advisory in the reviewed call path, not as a general waiver. Revisit it when werift or `ip` publishes a replacement, or before any change which delegates address trust, routing, or URL access decisions to that dependency.
|
||||
|
||||
The local real-Obsidian suite verifies the actual loaded `ObsidianServiceContext`, all 18 services, Vault reflection, CouchDB and Object Storage transfer, remote-activity accounting, CLI-to-Obsidian encrypted synchronisation, startup scanning, two-Vault create, update, delete, ordinary rename, case-only rename, target mismatch, Hidden File Sync, Customisation Sync, setting Markdown export, and two-device CouchDB, Object Storage, and P2P Setup URI workflows. These checks establish observable results and host composition rather than relying on declaration compatibility alone.
|
||||
|
||||
The obsolete mocked browser Harness and its `harness-ci` workflow have been removed. Webapp production builds and the direct browser composition contract remain part of the package-boundary proof; future Webapp browser automation is a Webapp-owned workflow rather than a substitute for real Obsidian E2E.
|
||||
|
||||
## Scanner and Repository Consequences
|
||||
|
||||
Consuming the npm package removes the common-library source and generated `_types` from the plug-in repository's Community directory scan, because package dependencies are treated as dependencies rather than maintained plug-in source. The branch preview confirms this expected boundary.
|
||||
|
||||
The change does not hide or resolve warnings in Self-hosted LiveSync-owned source. The scanner will continue to inspect the plug-in, CLI, Webapp, and WebPeer while those applications remain in the repository. Moving those applications to a LiveSync-family application repository may be considered after the package boundary is stable, but it is not part of this decision because the CLI and Webapp still import shared plug-in composition code.
|
||||
|
||||
Package extraction also does not resolve release-asset attestation verification, unsupported release assets, declarative settings migration, or other scanner findings which are independent of source ownership.
|
||||
|
||||
## Alternatives Rejected
|
||||
|
||||
### Move the common library into the Self-hosted LiveSync monorepo
|
||||
|
||||
This would permit atomic source changes and remove the Git submodule, but the community scanner already examines non-plug-in application source. A source workspace would remain in scope, and every library directive and platform dependency would be attributed to the plug-in repository. It would also make independent consumers depend on the plug-in repository's release cadence.
|
||||
|
||||
### Move the common library into Fancy Kit
|
||||
|
||||
Fancy Kit owns reusable framework-neutral interactions, Obsidian adapters, test infrastructure, and neutral utilities. Replication protocols, chunk and metadata formats, PouchDB composition, conflict rules, and LiveSync storage policy are a different domain. Moving them would obscure ownership and make general plug-in tooling carry Self-hosted LiveSync release concerns.
|
||||
|
||||
### Publish the current source tree without changing boundaries
|
||||
|
||||
This would expose accidental deep imports, global mutations, Svelte and browser code, Node-only modules, and parent-repository imports. Consumers would still need source aliases and bundler-specific behaviour, while maintainers would be unable to distinguish public API from implementation.
|
||||
|
||||
### Split every concern into a separate npm package immediately
|
||||
|
||||
Core, language, UI, browser, Node, P2P, Object Storage, and SDK packages could make dependency graphs precise, but the present source has not yet proven those release boundaries. Starting with one compiled package and explicit optional subpaths allows measurement without creating a coordinated release matrix. Additional packages remain possible after their contracts and consumers are demonstrated.
|
||||
|
||||
## Verification
|
||||
|
||||
The package-boundary conversion is ready for acceptance only when:
|
||||
|
||||
- common-library production source has no parent-repository or Obsidian dependency;
|
||||
- the package builds and tests from a standalone clean checkout;
|
||||
- `npm pack` contains only intended compiled artefacts and documentation;
|
||||
- packed Node and browser consumers resolve only exported paths;
|
||||
- a headless client does not bundle Svelte or the full language catalogue;
|
||||
- retained entry-point barrels do not load unrelated optional implementations, and no removed barrel is replaced by unrestricted deep exports;
|
||||
- Self-hosted LiveSync verifies its local Svelte adapter and workflow policy through injected tests and a focused composition smoke test;
|
||||
- Self-hosted LiveSync no longer has `src/lib`, `_types`, or `@lib/*` source aliases;
|
||||
- the Commonlib owner and packed-consumer suites, Self-hosted LiveSync unit and integration tests, Deno CLI E2E, plug-in and application production builds, and focused real-Obsidian E2E pass against the reviewed package artefact;
|
||||
- common-library downstream CI records and tests the exact Self-hosted LiveSync ref;
|
||||
- a Community directory scanner preview confirms the expected change in source findings; and
|
||||
- compatibility exports are explicitly enumerated from a reviewed downstream inventory rather than exposed through a wildcard.
|
||||
|
||||
The following are later SDK-stabilisation gates, not blockers for accepting the package boundary:
|
||||
|
||||
- two clients with different database, encryption, language, and lifecycle settings can coexist without sharing mutable client state;
|
||||
- an external consumer can replace its submodule or custom loader with the documented high-level package API; and
|
||||
- if the framework-neutral Modal host is promoted to Fancy Kit, Kit-owned lifecycle, viewport, safe-area, and touch-target guarantees replace duplicated device tests in LiveSync.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The common library gains an independently consumable and testable artefact while retaining its domain ownership and history.
|
||||
- Self-hosted LiveSync release archives no longer need generated fallback declarations for an absent submodule.
|
||||
- Community directory scanning distinguishes plug-in source from the reviewed dependency.
|
||||
- Changes which span the package and plug-in require coordinated package and downstream validation rather than one atomic source commit.
|
||||
- Temporary compatibility exports increase the first package surface, but their pre-1.0 status and explicit classification prevent them from becoming silent permanent contracts.
|
||||
- Application translation, Svelte, and platform dependencies become optional composition concerns rather than root-package side effects; Commonlib's small English fallback remains safe by default.
|
||||
- Fancy Kit may gain a generally useful typed Obsidian Modal host without acquiring LiveSync policy or a mandatory Svelte dependency.
|
||||
|
||||
## Later Open Questions
|
||||
|
||||
- Define the minimum conflict and conditional-write guarantees required by the first public high-level client.
|
||||
- Continue narrowing the broad model, service-composition, and replication compatibility paths as focused result contracts become available.
|
||||
- Confirm whether another Fancy Kit consumer needs the framework-neutral Modal host before it is added to the Kit, or whether LiveSync should pilot the contract first.
|
||||
49
docs/adr/2026_07_feature_maturity_for_1_0.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Feature maturity for 1.0
|
||||
|
||||
## Status
|
||||
|
||||
Proposed for the 1.0 integration branch.
|
||||
|
||||
## Context
|
||||
|
||||
Self-hosted LiveSync accumulated several labels such as 'experimental', 'Beta', 'obsolete', and 'sunset' over the 0.x line. Those labels did not consistently describe the current implementation. Some formerly experimental features now have maintained unit, Compose, and real-Obsidian coverage, while some old database-format options remain executable only because existing data must still be opened safely.
|
||||
|
||||
Version 1.0 needs to distinguish supported opt-in features from previews and from compatibility paths. Removing a label does not make a network environment reliable, and retaining a setting key does not make it a recommendation.
|
||||
|
||||
## Decision
|
||||
|
||||
### Supported opt-in features
|
||||
|
||||
- Peer-to-Peer Synchronisation is supported. Commonlib owns the transport lifecycle, the Compose suite covers transfer, replacement, disconnect, and reconnect behaviour, and the Obsidian host verifies its current pane boundary. Documentation must describe environmental WebRTC limitations without calling the feature experimental.
|
||||
- Hidden File Sync is supported as an advanced, separately initialised feature. It remains disabled during ordinary Setup URI initialisation and has its own two-Vault, conflict, filtering, and notification acceptance workflow.
|
||||
- Customisation Sync is supported as an advanced opt-in feature. Its maintained two-Vault real-Obsidian workflow covers snippets, configuration files, and plug-in files, including updates and deletion of source synchronisation data. It remains separate from Hidden File Sync and must not manage the same files concurrently.
|
||||
- Data Compression is maintained as an advanced opt-in storage and bandwidth trade-off. It remains disabled by default. The three-repeat CLI and CouchDB benchmark reduced stored chunk data and upload bodies by about 9% for the mixed fixture, but processing and worker-memory costs remained substantial. The [Data Compression specification](../specs_data_compression.md) records the contract, measurements, compatibility behaviour, execution model, and reproduction path for future default-setting decisions.
|
||||
- The real-Obsidian E2E runner is maintained release infrastructure rather than an experimental Harness.
|
||||
|
||||
### Retained previews
|
||||
|
||||
- JWT authentication remains experimental because it depends on specialised CouchDB server configuration and does not yet have a maintained server-backed authentication matrix. The current implementation, Setup URI transport, focused unit coverage, and reported ES512 use justify retaining it.
|
||||
- Ignore files remain Beta. They have focused target-filter tests, but nested rules, hidden-file expectations, and open user reports still require review.
|
||||
- Automatic newer-file conflict resolution remains Beta and disabled by default because it can deliberately overwrite one side of a conflict.
|
||||
- Garbage Collection V3 remains Beta and explicitly initiated. Its algorithm and safety protocol are outside this classification change and require a separate decision.
|
||||
|
||||
### Compatibility-only and sunset paths
|
||||
|
||||
- E2EE V1 and its dynamic iteration-count setting remain for existing encrypted databases. E2EE V2 is the new-Vault contract.
|
||||
- The old IndexedDB adapter remains only with its migration path back to IDB.
|
||||
- `xxhash64` is the current hash contract. Other hash algorithms remain available for existing databases and edge-case recovery, not as experimental alternatives for new Vaults.
|
||||
- Eden chunks remain accepted at runtime and in transported settings, but are not offered in the settings interface.
|
||||
- `doNotUseFixedRevisionForChunks` remains an inert compatibility input. Chunk revisions are always content-derived.
|
||||
- The deprecated cleaned-database reconciliation callback remains internal while an old IndexedDB client may still encounter that remote state. It is not a user-selectable maintenance action and is omitted from the settings reference.
|
||||
|
||||
### Already removed
|
||||
|
||||
The obsolete mocked browser Harness, automatic bulk chunk pre-send, legacy trash toggle, and fixed-revision control have no supported 1.0 UI path. Their compatibility data, where required, remains accepted independently of their removed controls.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Supported opt-in features retain focused release gates and user documentation.
|
||||
- Preview features remain off by default and keep explicit maturity labels.
|
||||
- Compatibility-only settings must not silently change existing data formats. New configuration should not expose retired formats merely because their decoders remain available.
|
||||
- Commonlib setting types and Setup URI decoding remain broad enough to read existing configurations. Removing those package contracts requires a separately versioned compatibility decision.
|
||||
- Deprecated host accessors and Community directory API warnings are a separate refactoring boundary. This decision does not authorise removing broadly used internal access paths.
|
||||
88
docs/adr/2026_07_multiple_remote_onboarding.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Architectural Decision Record: Make Onboarding Profile-Aware
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — implemented and verified against the locked Commonlib package.
|
||||
|
||||
## Context
|
||||
|
||||
Self-hosted LiveSync stores multiple remote connections in `remoteConfigurations` and selects the ordinary replication target with `activeConfigurationId`. P2P features have a separate `P2P_ActiveRemoteConfigurationId`. Existing replication implementations still consume the older flat CouchDB, Object Storage, and P2P fields, so selecting a profile projects its connection settings onto those compatibility fields.
|
||||
|
||||
The settings pane already creates and edits profiles directly. The Setup Wizard was inconsistent:
|
||||
|
||||
- modern Setup URI and QR payloads retained a supplied profile map;
|
||||
- legacy imports relied on `SettingService` to migrate flat connection fields into `legacy-*` profiles;
|
||||
- manual CouchDB and Object Storage setup wrote only the flat fields and relied on that same migration; and
|
||||
- manual P2P setup partly updated the profile map itself.
|
||||
|
||||
Legacy migration deliberately runs only when the profile map is empty. If a Vault already had one or more profiles, manually configuring another CouchDB or Object Storage connection changed the flat fields but did not add a profile. The selected stored profile could subsequently project its older values back onto those fields. Fresh P2P onboarding could also finish without a profile when no P2P profile ID existed beforehand.
|
||||
|
||||
## Decision
|
||||
|
||||
The profile map is authoritative persisted state for every newly configured remote. The compatibility fields remain the runtime projection of a selected profile and the input accepted from an older settings payload.
|
||||
|
||||
### Manual onboarding
|
||||
|
||||
A successful manual CouchDB, Object Storage, or P2P setup creates or deliberately updates a profile through Commonlib's focused `@vrtmrz/livesync-commonlib/remote-configurations` entry.
|
||||
|
||||
- Existing profiles are preserved.
|
||||
- A newly created profile receives an opaque generated ID.
|
||||
- CouchDB and Object Storage setup select the new profile as the main remote.
|
||||
- P2P setup selects the profile through `P2P_ActiveRemoteConfigurationId`.
|
||||
- P2P setup also selects it as the main remote when P2P is being configured as the main remote.
|
||||
- Configuring P2P alongside another main remote changes only the P2P selection.
|
||||
- A known profile ID is supplied only when an existing profile is deliberately being updated; omitting the ID creates another profile.
|
||||
|
||||
The onboarding dialogue does not add a naming step. Commonlib proposes a concise type-specific display name and adds a numeric suffix when necessary. The settings pane can rename it later.
|
||||
|
||||
### Identity and naming
|
||||
|
||||
Profile names are presentation only. They are neither unique identity nor a marker for the selected profile. No entry receives a special `default` ID or name. Opaque IDs establish identity, `activeConfigurationId` establishes the main selection, and `P2P_ActiveRemoteConfigurationId` establishes the P2P selection.
|
||||
|
||||
Generated names describe the connection without exposing credentials, for example `CouchDB couch.example`, `S3 notes`, or `P2P team-room`.
|
||||
|
||||
### Setup URI and QR import
|
||||
|
||||
A modern payload preserves its profile IDs, display names, profile URIs, main selection, and P2P selection. Setup does not rename or recreate those profiles.
|
||||
|
||||
A legacy payload containing only flat connection fields remains supported. `SettingService` migrates it into clearly labelled `legacy-couchdb`, `legacy-s3`, or `legacy-p2p` profiles only when no modern profile map exists. This is the only onboarding path which intentionally relies on the compatibility migration.
|
||||
|
||||
### Persistence and restart ordering
|
||||
|
||||
Profile construction happens before the settings are submitted to the onboarding completion boundary. Existing-device and new-device setup reserve Fetch or Rebuild respectively before enabling and saving the settings. Profile awareness does not change that initialisation ownership or restart ordering.
|
||||
|
||||
Commonlib produces an in-memory plaintext profile URI. The standard setting service applies its configured at-rest encryption during persistence.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
### Keep relying on legacy migration
|
||||
|
||||
This works only while the profile map is empty. It silently fails to register a newly configured connection once multiple-remote settings are already in use and leaves P2P with a separate implementation.
|
||||
|
||||
### Create a special `default` profile
|
||||
|
||||
The special meaning would duplicate `activeConfigurationId`, make a user-visible name carry identity, and become ambiguous as soon as the user selects another profile. Selection IDs already express the required state.
|
||||
|
||||
### Add profile naming and full list editing to onboarding
|
||||
|
||||
That would make the first-run path longer and duplicate the established Remote Databases interface. Automatic descriptive names and later renaming keep this change limited to data integrity and consistent selection.
|
||||
|
||||
### Replace the compatibility fields immediately
|
||||
|
||||
Replication, diagnostics, and older settings paths still consume the projected fields. Removing them belongs to a broader runtime migration and is not required to make onboarding correctly profile-aware.
|
||||
|
||||
## Verification
|
||||
|
||||
Commonlib unit tests cover preserving existing profiles, opaque-ID insertion, generated display names, duplicate-name suffixes, main activation, independent P2P selection, and URI serialisation. Its packed-consumer test imports the focused entry point from the generated package.
|
||||
|
||||
Self-hosted LiveSync unit tests cover preserving modern Setup URI profiles and their active selection, retaining legacy Setup URI and QR migration, adding CouchDB and Object Storage profiles beside an existing profile, independent P2P selection, fresh P2P selection as both main and P2P remote, and cancellation without mutation.
|
||||
|
||||
The real-Obsidian onboarding E2E owns the invitation, dialogue presentation, safe-area and touch-target checks, cancellation, and command reopening. It does not contact a remote or submit credentials. Remote connection correctness remains owned by the CouchDB, Object Storage, P2P, and two-Vault suites. The end-to-end Setup URI and provisioning acceptance workflow remains a separate release gate.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Manual onboarding and the Remote Databases pane share one Commonlib profile contract.
|
||||
- Existing profiles survive reconfiguration, and a newly configured connection becomes explicitly selectable.
|
||||
- Modern imports retain user-assigned profile identity and names.
|
||||
- Legacy Setup URIs continue to work through an isolated compatibility boundary.
|
||||
- Runtime consumers may keep using projected flat fields while the persisted model and new APIs use the 1.0 multiple-remote contract.
|
||||
91
docs/adr/2026_07_p2p_transport_lifecycle.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Architectural Decision Record: P2P Room and Transport Lifecycle
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — implemented and verified through Commonlib owner tests, the Compose transport suite, and the real-Obsidian setup workflow.
|
||||
|
||||
## Context
|
||||
|
||||
Self-hosted LiveSync uses Trystero's Nostr strategy for P2P discovery, signalling, and WebRTC transport. Three related resources have different owners and lifetimes:
|
||||
|
||||
- a LiveSync P2P service instance owns its commands, RPC sessions, advertisements, and room membership;
|
||||
- Trystero owns the underlying WebRTC peers and may share one physical peer across more than one room; and
|
||||
- Trystero's Nostr relay manager owns WebSocket clients shared by relay URL.
|
||||
|
||||
Closing every `RTCPeerConnection` returned by `room.getPeers()` bypasses Trystero's shared-peer manager. The manager may then retain a stale shared peer and prevent a replacement LiveSync replicator from discovering the same remote peer again.
|
||||
|
||||
Room departure and physical transport destruction are not equivalent. `room.leave()` sends the room-leave action, removes that room's actions and callbacks, and detaches its shared-peer binding. Trystero may retain a healthy physical WebRTC peer for later reuse after the last room binding has gone. The retained peer cannot carry actions for the room which has been left.
|
||||
|
||||
Relay WebSockets have a separate lifecycle. LiveSync's explicit disconnect operation must close them and prevent automatic reconnection. A later explicit connect must allow reconnection before joining the room again.
|
||||
|
||||
## Decision
|
||||
|
||||
Normal P2P shutdown delegates physical peer ownership to Trystero:
|
||||
|
||||
1. Stop LiveSync broadcast, replication, watch, client, and RPC state.
|
||||
2. Leave the active Trystero room through `room.leave()`.
|
||||
3. Remove LiveSync's room, advertisement, diagnostic-listener, and active-instance references.
|
||||
4. Pause Trystero relay reconnection and close the current relay WebSockets.
|
||||
|
||||
LiveSync does not call `close()` on the `RTCPeerConnection` values returned by `room.getPeers()` during normal shutdown or from a peer-leave callback. A peer-leave callback removes LiveSync-owned advertisement and client state only. Trystero remains responsible for deciding whether an underlying shared peer is reusable, stale, or ready for idle destruction.
|
||||
|
||||
The explicit disconnect operation therefore has the following contract:
|
||||
|
||||
| Resource | State after the operation |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| LiveSync P2P service and RPC room | Closed immediately. |
|
||||
| Trystero room membership | Left; room actions and advertisements are no longer available. |
|
||||
| Nostr relay WebSockets | Closed, with automatic reconnection paused. |
|
||||
| Underlying WebRTC peer | May remain idle under Trystero ownership for reuse, but cannot carry the departed room's traffic. |
|
||||
|
||||
This operation is a logical LiveSync disconnection and a physical signalling-server disconnection. It does not promise that every browser-owned WebRTC object has been destroyed synchronously.
|
||||
|
||||
An explicit connect resumes relay reconnection before opening a new room. Settings application and database lifecycle replacement close the current LiveSync replicator, discard it, construct a new instance from the current settings, and open that current instance when the configured policy requires it. Commands, event handlers, and panes resolve the current service-feature result at the point of use rather than retaining an obsolete replicator.
|
||||
|
||||
Lifecycle operations on one `LiveSyncTrysteroReplicator` are serialised. A close requested while an open is in progress must leave no orphan room serving, and repeated opens must not create parallel rooms. No fixed delay is inserted between close and open: readiness is determined by the actual lifecycle operation and peer discovery.
|
||||
|
||||
Relay sockets retain their Trystero-provided close handlers. LiveSync pauses relay reconnection, closes the sockets, and later resumes reconnection through Trystero's public functions. It does not replace `socket.onclose`, because Trystero uses that handler to retire and recreate shared relay clients correctly.
|
||||
|
||||
P2P setup follows the transport's actual ownership model. First-device initialisation resets and scans the local database, but does not attempt to lock, reset, or upload to a non-existent central remote database. Its confirmation dialogues therefore describe preparing this device and do not present the central-server overwrite warnings or remote-configuration option. An additional device performs one explicit peer-selection and finite Fetch pass, then resumes database and Vault reflection. The generic second convergence pass remains reserved for central remote types because repeating it for P2P would ask the user to select the same peer twice.
|
||||
|
||||
## Ownership
|
||||
|
||||
Commonlib owns the LiveSync-specific P2P service, RPC, command, and lifecycle composition. Trystero owns WebRTC peer creation, sharing, reuse, stale detection, and destruction, as well as relay-client reconstruction. The Self-hosted LiveSync host owns the current Commonlib service-feature result and supplies the platform services used by its current replicator.
|
||||
|
||||
Self-hosted LiveSync does not add a separate root Trystero dependency. Tests which must observe relay sockets resolve the exact Trystero generation owned by the locked Commonlib package, avoiding two independent transport singletons in one process.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
### Close every value returned by `room.getPeers()`
|
||||
|
||||
This bypasses Trystero's shared-peer manager and can prevent a replacement replicator from rediscovering the same peer.
|
||||
|
||||
### Add a fixed close-to-open delay
|
||||
|
||||
A timing guess does not repair stale ownership and would make ordinary settings application slower.
|
||||
|
||||
### Keep raw close behaviour behind a force command
|
||||
|
||||
Changing the command name does not make the lifecycle safe. A force command which cannot reconnect predictably has no reliable operational value.
|
||||
|
||||
### Override relay `onclose` to suppress reconnection
|
||||
|
||||
This interferes with Trystero's shared relay clients. The public pause and resume functions provide the intended control boundary.
|
||||
|
||||
## Verification
|
||||
|
||||
Commonlib unit tests prove that normal P2P host closure calls `room.leave()` without directly closing Trystero-owned peer connections. Additional package tests cover the action API, replaceable peer-event subscriptions, multiple RPC transport disposers, serialised open and close operations, local-only first-device initialisation, and one-pass additional-device Fetch.
|
||||
|
||||
Self-hosted LiveSync unit tests prove that settings and database replacement leave panes on the current replicator, and that an explicit P2P rebuild bypasses the policy intended for ordinary replication.
|
||||
|
||||
The canonical Compose P2P suite uses a real local Nostr relay and WebRTC implementation. It covers ordinary two-peer synchronisation, replacement of the active LiveSync replicator followed by discovery and transfer with the same peer, and explicit relay disconnection followed by paused and resumed reconnection. The lifecycle scenario is exposed only through a Docker test build and an injected CLI command runner; it is not part of the public CLI command surface.
|
||||
|
||||
The real-Obsidian P2P Setup URI workflow creates the first device, generates the second-device URI from it, accepts each peer visibly, and verifies a two-way note round-trip through a local relay. A separate focused pane test covers the principal connection control and teardown without requiring a remote peer. Transport replacement and relay-socket lifecycle remain owned by the package and Compose tests rather than being duplicated in Obsidian.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Replacing a P2P replicator no longer leaves host views or commands bound to an obsolete instance.
|
||||
- Explicit signalling-server disconnection has a testable socket-level meaning without claiming immediate destruction of idle WebRTC objects.
|
||||
- Settings which change the relay, room, passphrase, or TURN configuration can replace the whole LiveSync room safely.
|
||||
- Trystero may reuse healthy peers across room lifecycles, reducing unnecessary renegotiation.
|
||||
- Strict physical WebRTC teardown remains unavailable until Trystero exposes a safe ownership-aware operation.
|
||||
103
docs/adr/2026_07_release_notes_and_database_compatibility.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Release notes and database compatibility gates
|
||||
|
||||
## Status
|
||||
|
||||
Accepted for the 1.0 release line.
|
||||
|
||||
## Context
|
||||
|
||||
Self-hosted LiveSync historically used several unrelated kinds of version and settings state during start-up.
|
||||
|
||||
The plug-in SemVer was converted into a numeric major/minor value and stored in `lastReadUpdates`. The settings dialogue used that value to open the change log automatically, and offered a button which marked the release line as read. Patch versions were intentionally ignored. Pre-release identifiers containing an additional dot did not fit this numeric representation and could be interpreted as a much larger release line.
|
||||
|
||||
Separately, the internal database compatibility constant `VER` is recorded in device-local storage under a Vault-scoped key. Crossing this internal version used to set `versionUpFlash` and permanently change several automatic synchronisation settings to `false`. Replication services already reject work while `versionUpFlash` is non-empty, so changing the user's saved choices duplicated the runtime safety gate and required manual reconstruction after acknowledgement.
|
||||
|
||||
The remote `obsydian_livesync_version` document also carries the internal database version. It is a protocol and data-compatibility mechanism, not a copy of the plug-in SemVer.
|
||||
|
||||
Commonlib's `settingVersion` describes the stored settings shape, while `DEFAULT_SETTINGS` was historically used both to complete missing values in an existing document and to initialise a new Vault. Those operations require different defaults: an existing Vault needs conservative completion, while a genuinely new Vault can use current recommendations without changing an established configuration.
|
||||
|
||||
## Decision
|
||||
|
||||
### Release notes
|
||||
|
||||
- Keep the change-log pane and render the current release history whenever it is opened.
|
||||
- Remove automatic unread-version tracking, the acknowledgement button for ordinary release notes, and automatic navigation to the change-log pane.
|
||||
- Do not derive data-compatibility behaviour from the plug-in's major, minor, patch, or pre-release identifiers.
|
||||
- Retain the saved `lastReadUpdates` field in the settings schema for backwards compatibility, but do not use it in the plug-in. It can be removed through a separately reviewed settings-schema migration if retaining it later becomes burdensome.
|
||||
|
||||
### Settings schema and initial settings
|
||||
|
||||
- Keep the Commonlib settings schema version independent of the plug-in SemVer and the internal database version `VER`. Increment it only for an ordered change to the stored settings shape.
|
||||
- Use Commonlib's conservative schema defaults to complete an existing settings document. Explicit stored values take precedence, and an ordinary migration does not disable or replace the person's synchronisation choices.
|
||||
- Use `createNewVaultSettings()` only for a store which has never held Self-hosted LiveSync settings, explicit new-user onboarding, a factory reset, or CLI settings creation. Setup URI, QR, Markdown, and other existing-setting imports retain conservative completion semantics.
|
||||
- Apply remote-specific preferred values only when that remote is explicitly selected during setup. Do not infer and merge new recommendations into an existing configuration.
|
||||
- Keep settings saved by a future Commonlib schema fail-closed and do not persist an apparent downgrade migration.
|
||||
|
||||
The current new-Vault base selects a 50 MB maximum synchronised file size, Rabin–Karp chunk splitting, Plug-in Sync V2, case-insensitive file-name handling, and E2EE V2. It does not enable synchronisation, encryption, or a remote connection without user action. Chunk revisions are always content-derived; `doNotUseFixedRevisionForChunks` remains only as deprecated compatibility input and is not a recommendation or review setting.
|
||||
|
||||
Relative to the conservative existing-setting fallbacks, only Plug-in Sync V2 and the explicit case-insensitive value differ for a new Vault. The 50 MB limit, Rabin–Karp splitter, and E2EE V2 already match the legacy fallback values. Data Compression, Eden, V1 dynamic iteration, the legacy IndexedDB adapter, Hidden File Sync, and automatic synchronisation remain disabled when absent from an existing settings document.
|
||||
|
||||
An existing settings document without an explicit `handleFilenameCaseSensitive` choice is normalised to `false` and saved. This preserves the effective case-insensitive branch used by earlier releases when the value was absent, and does not require a review or rebuild. An explicit `true` or `false` choice remains unchanged.
|
||||
|
||||
Keep configured-state inference separate from new-Vault initialisation. If an existing legacy document has no `isConfigured` value, repeat the pre-1.0 comparison with the conservative defaults: a default-equivalent document remains unconfigured, while a non-default stored value is evidence that it was configured. Persist that inferred boolean so a migration cannot turn an unconfigured Vault into an irreversible configured state merely because its settings document was non-empty.
|
||||
|
||||
### Database compatibility
|
||||
|
||||
- Continue to use the internal database version `VER` for changes which require explicit compatibility review. Changing the plug-in SemVer alone does not increment `VER`.
|
||||
- Store the last acknowledged internal database version through Commonlib's device-local small-configuration contract under `database-compatibility-version`. Copy the legacy raw local-storage value into that contract once, then remove the legacy key after the copy has completed.
|
||||
- Initialise the marker to the current `VER` only when Commonlib identifies a genuinely new Vault with no pending review. A configured existing Vault with a missing or invalid marker requires review instead of being silently accepted.
|
||||
- Defer database compatibility evaluation for an existing unconfigured Vault. It cannot replicate, so do not persist a misleading pause or acknowledge its missing marker while onboarding is still pending. Keep the marker absent so a later configured start evaluates the same state before ordinary synchronisation.
|
||||
- Treat a missing marker on a configured Vault as an ambiguous device transition. Copying or restoring a Vault, or opening it with a new Obsidian profile, can preserve settings and database files without preserving device-local storage. Do not infer acknowledgement from an empty local database: a recovery operation, partial copy, or remote-first setup can also produce that state. Explain these cases and require an explicit decision in the compatibility dialogue.
|
||||
- Derive one structured pause from the acknowledged database version, Commonlib's settings-migration state, and any persisted legacy review message. Persist the generic `versionUpFlash` message without changing any automatic synchronisation setting, because Commonlib already treats that field as a replication gate.
|
||||
- Treat non-empty `versionUpFlash` as a runtime replication gate. Standard and one-shot replication must stop before remote work begins.
|
||||
- Apply the same ordinary replication policy to P2P pull, push, and peer-requested synchronisation. An explicitly confirmed Fetch or Rebuild may bypass the ordinary policy because it is the operation selected to construct or recover the local state.
|
||||
- Present the reason in a dedicated dialogue after the Obsidian layout is ready. The details view is explanatory only and returns to the summary before any decision can be made. The safe default and closing either dialogue keep synchronisation paused. A persistent Notice and a command allow the dialogue to be reopened without using the settings pane.
|
||||
- Let the person read focused compatibility details without presenting the whole release history as a safety instruction. The Change Log remains a manually opened release-history pane and contains no compatibility acknowledgement control.
|
||||
- Offer an explicit resume action only when every reason is recoverable in the running implementation. An upgrade, a missing or invalid marker on an existing Vault, and a reviewed migration from an older settings schema are resumable after all devices have been updated. A downgrade from a newer acknowledged `VER`, or settings saved by a future schema, cannot be acknowledged by the older installation.
|
||||
- On resume, clear `versionUpFlash` and persist that fail-closed change before recording the current `VER` as acknowledged. If saving fails, restore the gate. Reapply settings only after the marker has advanced so that the previously configured synchronisation behaviour can resume without reconstruction.
|
||||
- Preserve the original legacy review message as a structured reason when no more specific database or settings-schema reason is available. Escape it before including it in Markdown UI.
|
||||
- Continue to reject a remote version document which is newer than the running implementation. That receiver-side check is independent of the local upgrade review.
|
||||
|
||||
### Onboarding activation and initialisation
|
||||
|
||||
- Keep an unconfigured Vault outside database initialisation, offline scanning, and configured-only checks. Offer setup through the long-lived onboarding Notice and the permanent command instead of opening a competing dialogue automatically.
|
||||
- For new-device onboarding, reserve Rebuild before enabling and saving the accepted settings.
|
||||
- For an unconfigured existing device, reserve Fetch before enabling and saving imported or manually confirmed settings.
|
||||
- Suspend the current runtime after the flag has been written, apply the accepted settings through the scheduler's preparation callback, and request restart only after that callback succeeds.
|
||||
- If the flag cannot be written, do not enable the settings. If preparation fails, remove the flag, resume the current runtime, and leave the transition incomplete.
|
||||
- Applying compatible settings to an already configured device remains an ordinary edit and does not schedule Fetch automatically.
|
||||
|
||||
`isConfigured`, the Fetch and Rebuild flags, and in-memory suspension therefore retain separate meanings. `isConfigured` controls participation in ordinary processing, a flag selects a one-shot operation for the next start, and suspension prevents the old process from observing newly enabled settings before that selected restart.
|
||||
|
||||
### Flag-file recovery order
|
||||
|
||||
- For a configured Vault, evaluate and persist the compatibility gate after settings load, before Obsidian layout-ready recovery begins. This blocks ordinary and one-shot replication even while the review dialogue has not yet opened. An existing unconfigured Vault follows the deferred rule above instead.
|
||||
- Preserve the existing ordered flag-file recovery handlers: SCRAM at priority 5, fetch-all at priority 10, and rebuild-all at priority 20. These files express an explicit recovery instruction and may invoke their focused storage or rebuild service while ordinary replication remains gated.
|
||||
- Present the compatibility review at priority 30, after any selected recovery operation. A recovery handler which cancels start-up, keeps SCRAM active, or schedules a restart returns `false`, so the current process does not open a competing compatibility dialogue. If recovery completes and start-up continues, the dialogue opens before normal synchronisation is allowed to resume.
|
||||
- Keep database preparation independent of an unanswered compatibility dialogue, because the compatibility gate already blocks replication. Before Config Doctor begins its interactive checks, await the active initial review so that the two update dialogues cannot overlap.
|
||||
- Never mark compatibility as acknowledged merely because fetch, rebuild, or local database reset completed. The person must still use the explicit resume action. This keeps destructive recovery intent separate from protocol and settings compatibility acknowledgement.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Ordinary releases no longer force the settings dialogue to show release notes. Important operational instructions must be clear in the published release notes and any explicit migration notice.
|
||||
- SemVer pre-releases such as `1.0.0-rc.0` no longer require a special numeric encoding inside plug-in settings.
|
||||
- An internal compatibility change remains fail-closed for replication, but it no longer destroys the person's synchronisation preferences.
|
||||
- A new installation has no previous internal-version marker and therefore does not show an upgrade review. Its initial settings and onboarding remain responsible for keeping replication disabled until configuration is complete.
|
||||
- An existing unconfigured installation also remains on onboarding without a compatibility warning. Unlike a genuinely new Vault, it does not receive an acknowledgement marker; activation leaves the compatibility decision for its next configured start.
|
||||
- A copied or restored configured Vault can show a one-time compatibility review on its new device or profile. This is intentional even when its local database appears empty, because emptiness does not prove how the Vault was produced.
|
||||
- A genuinely new Vault receives current recommendations without applying them as fallbacks to an existing configuration. It remains inert until onboarding is accepted.
|
||||
- Accepted new-device and existing-device setup cannot enable ordinary processing before the selected Rebuild or Fetch has been reserved.
|
||||
- An older installation cannot dismiss evidence that a newer implementation or settings schema has already been used on the device.
|
||||
- The Obsidian-specific dialogue depends only on a host-neutral compatibility result and the injected confirmation capability. Commonlib remains responsible for settings migration, device-local storage, and the replication gate.
|
||||
- A future incompatible database change must increment `VER`, provide an actionable review message, verify the remote version negotiation, and test both the pending and acknowledged states. A major SemVer increase without those changes has no database-compatibility effect.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests verify new-Vault initialisation, upgrades, missing and invalid markers, downgrades, future settings schemas, legacy marker migration, acknowledgement ordering, and save-failure recovery while retaining automatic synchronisation choices.
|
||||
- Commonlib package tests verify conservative stored-setting completion, independently mutable new-Vault settings, legacy file-name case normalisation, future-schema protection, and the focused settings entry from a clean consumer.
|
||||
- Host unit tests verify new-Vault factory use, conservative import paths, the unconfigured start-up gate, deferred compatibility evaluation and later re-evaluation, flag-before-settings ordering, rollback when the flag cannot be reserved, ordinary configured edits, and compatibility acknowledgement persistence.
|
||||
- Unit tests verify that a pending review is honoured by the packaged Commonlib replication service before remote activity begins.
|
||||
- Unit and Compose tests verify that ordinary P2P replication observes the policy, explicit P2P rebuild uses the setup bypass, and replacement leaves host actions on the current replicator.
|
||||
- A real-Obsidian settings test verifies the dedicated summary and details dialogues, captures representative screenshots, confirms that the acknowledged internal version advances only after explicit resume, and confirms that the Change Log contains no acknowledgement control.
|
||||
- The real-Obsidian CouchDB workflow starts from configured plug-in data without a device-local marker, verifies the copied-or-restored Vault explanation, resumes through the actual dialogue, and then completes remote metadata, chunk, and activity checks. The two-Vault workflow performs the same review once per isolated Vault before reusing the acknowledged device state for later process launches.
|
||||
- Unit tests fix the layout-ready priority after the three flag-file recovery priorities, so a recovery which stops start-up cannot race the compatibility dialogue.
|
||||
169
docs/datastructure.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Data Structures of Self-Hosted LiveSync
|
||||
|
||||
## Overview
|
||||
|
||||
Self-hosted LiveSync uses the following types of documents:
|
||||
|
||||
- Metadata
|
||||
- Legacy Metadata
|
||||
- Binary Metadata
|
||||
- Plain Metadata
|
||||
- Chunk
|
||||
- Versioning
|
||||
- Synchronise Information
|
||||
- Synchronise Parameters
|
||||
- Milestone Information
|
||||
|
||||
## Description of Each Data Structure
|
||||
|
||||
All documents inherit from the `DatabaseEntry` interface. This is necessary for conflict resolution and deletion flags.
|
||||
|
||||
```ts
|
||||
export interface DatabaseEntry {
|
||||
_id: DocumentID;
|
||||
_rev?: string;
|
||||
_deleted?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Versioning Document
|
||||
|
||||
This document stores version information for Self-hosted LiveSync.
|
||||
The ID is fixed as `obsydian_livesync_version` [VERSIONING_DOCID]. Yes, the typo has become a curse.
|
||||
When Self-hosted LiveSync detects changes to this document via Replication, it reads the version information and checks compatibility.
|
||||
This internal database version is independent of the plug-in's SemVer version. The last version explicitly acknowledged on a device is stored through Commonlib's device-local configuration contract. When that version differs, or when a settings migration requires review, Self-hosted LiveSync presents a dedicated compatibility dialogue and blocks replication without changing the user's automatic synchronisation choices. A supported upgrade can resume only after explicit review. A downgrade from a newer acknowledged database version, or settings written by a future schema, remains blocked until a compatible plug-in is installed.
|
||||
Please refer to negotiation.ts.
|
||||
|
||||
### Synchronise Information Document
|
||||
|
||||
This document stores information that should be verified in synchronisation settings.
|
||||
The ID is fixed as `syncinfo` [SYNCINFO_ID].
|
||||
The information stored in this document is only the conditions necessary for synchronisation to succeed, and as of v0.25.43, only a random string is stored.
|
||||
This document is only used during rebuilds from the settings screen for CouchDB-based synchronisation, making it like an appendix. It may be removed in the future.
|
||||
|
||||
### Synchronise Parameters Document
|
||||
|
||||
This document stores synchronisation parameters.
|
||||
Synchronisation parameters include the protocol version and salt used for encryption, but do not include chunking settings.
|
||||
|
||||
The ID is fixed as `_local/obsidian_livesync_sync_parameters` [DOCID_SYNC_PARAMETERS] or `_obsidian_livesync_journal_sync_parameters.json` [DOCID_JOURNAL_SYNC_PARAMETERS].
|
||||
|
||||
This document exists only on the remote and not locally.
|
||||
This document stores the following information.
|
||||
It is read each time before connecting and is used to verify that E2EE settings match.
|
||||
This mismatch cannot be ignored and synchronisation will be stopped.
|
||||
|
||||
```ts
|
||||
export interface SyncParameters extends DatabaseEntry {
|
||||
_id: typeof DOCID_SYNC_PARAMETERS;
|
||||
type: (typeof EntryTypes)["SYNC_PARAMETERS"];
|
||||
protocolVersion: ProtocolVersion;
|
||||
pbkdf2salt: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### protocolVersion
|
||||
|
||||
This field indicates the protocol version used by the remote. Mostly, this value should be `2` (ProtocolVersions.ADVANCED_E2EE), which indicates safer E2EE support.
|
||||
|
||||
#### pbkdf2salt
|
||||
|
||||
This field stores the salt used for PBKDF2 key derivation on the remote. This salt and the passphrase provides E2EE encryption keys.
|
||||
|
||||
### Milestone Information Document
|
||||
|
||||
This document stores information about how the remote accepts and recognises clients.
|
||||
The ID is fixed as `_local/obsidian_livesync_milestone` [MILESTONE_DOCID].
|
||||
This document exists only on the remote and not locally.
|
||||
This document is used to indicate synchronisation progress and includes the version range of accepted chunks for each node and adjustment values for each node.
|
||||
Tweak Mismatched is determined based on the information in this document.
|
||||
|
||||
For details, please refer to LiveSyncReplicator.ts, LiveSyncJournalReplicator.ts, and LiveSyncDBFunctions.ts.
|
||||
|
||||
```ts
|
||||
export interface EntryMilestoneInfo extends DatabaseEntry {
|
||||
_id: typeof MILESTONE_DOCID;
|
||||
type: EntryTypes["MILESTONE_INFO"];
|
||||
created: number;
|
||||
accepted_nodes: string[];
|
||||
node_info: { [key: NodeKey]: NodeData };
|
||||
locked: boolean;
|
||||
cleaned?: boolean;
|
||||
node_chunk_info: { [key: NodeKey]: ChunkVersionRange };
|
||||
tweak_values: { [key: NodeKey]: TweakValues };
|
||||
}
|
||||
```
|
||||
|
||||
### locked
|
||||
|
||||
If the remote has been requested to lock out from any client, this is set to true.
|
||||
When set to true, clients will stop synchronisation unless they are included in accepted_nodes.
|
||||
|
||||
### cleaned
|
||||
|
||||
If the remote has been cleaned up from any client, this is set to true.
|
||||
In this case, clients will stop synchronisation as they need to rebuild again.
|
||||
|
||||
### Metadata Document
|
||||
|
||||
Metadata documents store metadata for Obsidian notes.
|
||||
|
||||
```ts
|
||||
export interface MetadataDocument extends DatabaseEntry {
|
||||
_id: DocumentID;
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
deleted?: boolean;
|
||||
eden: Record<string, EdenChunk>; // Obsolete
|
||||
path: FilePathWithPrefix;
|
||||
children: string[];
|
||||
type: EntryTypes["NOTE_LEGACY" | "NOTE_BINARY" | "NOTE_PLAIN"];
|
||||
}
|
||||
```
|
||||
|
||||
### type
|
||||
|
||||
This field indicates the type of Metadata document.
|
||||
By convention, Self-hosted LiveSync does not save the mime type of the file, but distinguishes them with this field. Please note this.
|
||||
Possible values are as follows:
|
||||
|
||||
- NOTE_LEGACY: Legacy metadata document
|
||||
- Please do not use
|
||||
- NOTE_BINARY: Binary metadata document (newnote)
|
||||
- NOTE_PLAIN: Plain metadata document (plain)
|
||||
|
||||
#### children
|
||||
|
||||
This field stores an array of Chunk Document IDs.
|
||||
|
||||
#### \_id, path
|
||||
|
||||
\_id is generated based on the path of the Obsidian note.
|
||||
|
||||
- If the path starts with `_`, it is converted to `/_` for convenience.
|
||||
- If Case Sensitive is disabled, it is converted to lowercase.
|
||||
|
||||
When Obfuscation is enabled, the path field contains `f:{obfuscated path}`.
|
||||
The path field stores the path as is. However, when Obfuscation is enabled, the obfuscated path is stored.
|
||||
|
||||
When Property Encryption is enabled, the path field stores all properties including children, mtime, ctime, and size in an encrypted state. Please refer to encryption.ts.
|
||||
|
||||
### Chunk Document
|
||||
|
||||
```ts
|
||||
export type EntryLeaf = DatabaseEntry & {
|
||||
_id: DocumentID;
|
||||
type: EntryTypes["CHUNK"];
|
||||
data: string;
|
||||
};
|
||||
```
|
||||
|
||||
Chunk documents store parts of note content.
|
||||
|
||||
- The type field is always `[CHUNK]`, `leaf`.
|
||||
- The data field stores the chunk content.
|
||||
- The \_id field is generated based on a hash of the content and the passphrase.
|
||||
|
||||
Hash functions used include xxHash and SHA-1, depending on settings.
|
||||
Chunking methods used include Contextual Chunking and Rabin-Karp Chunking, depending on settings.
|
||||
118
docs/design_docs/chunk_retrieval_and_waiting.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Chunk Retrieval and Waiting
|
||||
|
||||
## Purpose
|
||||
|
||||
This document records how LiveSync retrieves chunks after file metadata has been found, which operation provides each terminal condition, and what the remaining time value means. It is an implementation specification for developers; it is not a user configuration guide.
|
||||
|
||||
The architectural decision and historical rationale are in [Chunk Arrival Quiescence](../adr/2026_07_chunk_arrival_quiescence.md).
|
||||
|
||||
## Invariants and Sources of Apparent Reordering
|
||||
|
||||
A normal local save creates and persists the chunks before it writes the metadata document which refers to them. LiveSync must preserve this invariant: publishing metadata first can expose a reference which no client can satisfy.
|
||||
|
||||
This ordering is not an atomic transaction across documents. A reader may still see metadata before a referenced chunk for these reasons:
|
||||
|
||||
- CouchDB replication transfers individual documents and does not expose the chunk and metadata writes as one atomic unit.
|
||||
- With `readChunksOnline` enabled, CouchDB pull replication deliberately excludes chunk documents. Seeing metadata first is then the intended design, and the chunk is fetched by identifier.
|
||||
- A winning metadata conflict revision may refer to chunks created by another revision or client which have not yet reached the local database.
|
||||
- Replication persists documents before every downstream change callback and file-reflection task has necessarily observed them.
|
||||
- Historical versions and removed transfer modes may have produced data which did not preserve the normal ordering invariant.
|
||||
|
||||
Waiting may resolve temporary visibility and processing gaps only when a known operation can still deliver the chunk. It cannot repair a chunk which is absent from every available source.
|
||||
|
||||
## Retrieval Capabilities
|
||||
|
||||
Direct on-demand fetch is currently available only for CouchDB when `useOnlyLocalChunk` is false. This capability deliberately does not depend on `readChunksOnline`:
|
||||
|
||||
- when `readChunksOnline` is true, direct fetch is the normal way to obtain a chunk omitted from pull replication; and
|
||||
- when `readChunksOnline` is false, direct fetch remains a recovery path if a normally replicated chunk is locally absent.
|
||||
|
||||
MinIO's sequential replicator and P2P do not implement direct `fetchRemoteChunks` delivery through this path. Their chunks must arrive through a finite replication operation. A P2P pull or bidirectional synchronisation is such a producer. A push-only P2P request remains broad remote activity for Wake Lock and lifecycle reporting, but it is deliberately excluded from `finiteReplicationActivityCount` because it cannot deliver a local document.
|
||||
|
||||
`waitForReady` is a call-site policy, not a persisted user setting. `true` permits waiting for an already-observable producer. `false` normally requests an immediate local result, except that CouchDB on-demand delivery still waits for the claim which synchronous dispatch creates.
|
||||
|
||||
## Policy Matrix
|
||||
|
||||
The matrix selects whether lifecycle waiting is permitted and whether the waiter may dispatch a direct request. It does not assign elapsed arrival budgets.
|
||||
|
||||
| Remote | `waitForReady` | `useOnlyLocalChunk` | Direct fetch | Wait for observed producer | Intended behaviour |
|
||||
| ------- | -------------: | ------------------: | -----------: | -------------------------: | --------------------------------------------------------------------------------------- |
|
||||
| CouchDB | `false` | `false` | Yes | Yes | Dispatch on-demand fetch and finish at its per-identifier claim boundary. |
|
||||
| CouchDB | `true` | `false` | Yes | Yes | Accept an active finite replication or dispatch direct fetch. |
|
||||
| CouchDB | `false` | `true` | No | No | Return immediately after the local miss. |
|
||||
| CouchDB | `true` | `true` | No | Yes | Wait for an already-active finite replication; otherwise return unavailable. |
|
||||
| MinIO | `false` | Either | No | No | Return immediately after the local miss. |
|
||||
| MinIO | `true` | Either | No | Yes | Wait for an already-active finite sequential replication; otherwise return unavailable. |
|
||||
| P2P | `false` | Either | No | No | Return immediately after the local miss. |
|
||||
| P2P | `true` | Either | No | Yes | Wait for an already-active finite P2P replication; otherwise return unavailable. |
|
||||
|
||||
For CouchDB, `readChunksOnline` changes what normal replication includes, not the direct-fetch capability or this matrix:
|
||||
|
||||
| `readChunksOnline` | CouchDB pull contains chunks | Role of direct fetch |
|
||||
| -----------------: | ---------------------------: | ------------------------------------------------------------------------ |
|
||||
| `true` | No | Primary chunk delivery after metadata arrives. |
|
||||
| `false` | Yes | Recovery fallback for a chunk which is unexpectedly unavailable locally. |
|
||||
|
||||
`concurrencyOfReadChunksOnline` and `minimumIntervalOfReadChunksOnline` affect only the scheduling of CouchDB on-demand requests. They do not change whether a request may be dispatched or which lifecycle a reader observes. Accepted identifiers remain claimed while they wait for a concurrency slot and while the configured interval is applied. A minimum interval of five minutes or more is an exceptional value: the inactivity fuse may release the logical claim before that deliberate pause completes. This safety precedence does not abort the delayed physical request.
|
||||
|
||||
## Wait State Machine
|
||||
|
||||
1. Read the cache and local database.
|
||||
2. If every requested chunk is present, return it without entering a wait.
|
||||
3. Register one shared waiter per missing identifier.
|
||||
4. If policy permits direct fetch, emit `missingChunks`. `ChunkFetcher` synchronously creates the per-identifier claim before the event dispatch returns.
|
||||
5. Observe both the matching claim and `finiteReplicationActivityCount`.
|
||||
6. Resolve immediately if a valid chunk or explicit remote-missing event arrives.
|
||||
7. If an observed producer remains active, do not charge elapsed time against an arrival budget.
|
||||
8. When all observed producers end, bypass the cache and read the identifiers from the local database once.
|
||||
9. Return the rechecked chunk, or return unavailable. Do not add another fixed grace after the authoritative boundary.
|
||||
10. If no producer is observable after synchronous dispatch, return unavailable immediately.
|
||||
|
||||
If new relevant activity starts while the final database recheck is pending, that result becomes stale. The waiter remains active until the newer producer completes and a current recheck finishes.
|
||||
|
||||
## Meaning of Finite Replication Completion
|
||||
|
||||
Finite replication enters the typed `runFiniteReplicationActivity` boundary and is represented by the narrower `finiteReplicationActivityCount`. The optional `replication` label remains diagnostic and does not control this behaviour.
|
||||
|
||||
For a successful finite operation, completion means that its replicator has reached the latest sequence in the operation's scope and processed its replication change callbacks. No more database documents can arrive from that operation. This is the primary semantic cutoff.
|
||||
|
||||
If the operation fails, it has not proved remote absence or latest state. It has nevertheless stopped being a producer. The waiting layer rechecks documents which may have arrived before the failure and then returns unavailable; the replication error and retry workflow owns further recovery.
|
||||
|
||||
Overlapping finite replications keep the count above zero until the final operation settles. The local recheck therefore occurs only after every observed finite producer is quiescent.
|
||||
|
||||
The continuous live channel is intentionally excluded because it has no completion boundary and would otherwise make a chunk read unbounded. The pull-only catch-up run before opening that channel is finite and enters the same typed boundary. Its one-shot batch-size fallback remains inside that boundary. A live-channel fallback starts another continuous attempt and therefore another bounded initial catch-up.
|
||||
|
||||
## Meaning of an On-demand Claim
|
||||
|
||||
An accepted identifier remains claimed from synchronous queue acceptance through throttling, physical fetch, validation, local persistence, and terminal event delivery. The claim is identifier-scoped because a global remote-work count cannot say whether unrelated work can provide this chunk.
|
||||
|
||||
The claim finishes when the fetcher has recorded an outcome for the identifier. A transport error, missing active replicator, or invalid result releases the claim without emitting an explicit remote-missing result unless the remote actually supplied that information.
|
||||
|
||||
## Meaning of the Five-minute Value
|
||||
|
||||
The five-minute value is an inactivity leak fuse for an accepted on-demand claim. It is the only elapsed duration in this state machine, and it is not a normal terminal condition.
|
||||
|
||||
The fuse bounds retention if a faulty activity runner never enters its task, a Promise never settles, or a transport stops making observable progress. It prevents the per-identifier claim and waiter from remaining live forever. Once the bounded activity callback has entered, releasing the claim also allows Wake Lock, application-lifecycle deferral, and the remote-work indicator associated with that callback to finish. Observable progress rearms the fuse.
|
||||
|
||||
Five minutes is a conservative operational ceiling rather than a measured chunk-arrival expectation. It must not be used to infer that the remote lacks a chunk, and it does not abort the physical request. `fetchRemoteChunks` does not yet accept an `AbortSignal`, so the request may complete after the logical state has been released. Transport cancellation and transport-specific deadlines are separate future work.
|
||||
|
||||
The old 5-second and 30-second constants remain exported for source compatibility only. A positive deprecated `ChunkReadOptions.timeout` opts into lifecycle waiting, but its numeric value is ignored. Zero or a negative value still requests an immediate result. New code uses `waitForDelivery` explicitly.
|
||||
|
||||
## Test Obligations
|
||||
|
||||
Changes to this behaviour must keep automated coverage for:
|
||||
|
||||
- chunks-before-metadata save ordering;
|
||||
- every row in the retrieval policy matrix;
|
||||
- a finite replication which remains active well beyond the former 5-second and 30-second values;
|
||||
- successful completion with the chunk already persisted but no arrival event delivered;
|
||||
- immediate unavailability when no producer is observable;
|
||||
- overlapping finite operations and overlapping per-identifier claims;
|
||||
- activity restarting while a local recheck is pending;
|
||||
- direct fetch queueing, throttling, persistence, and terminal notification;
|
||||
- explicit remote absence versus transport or replicator failure;
|
||||
- runner rejection, cancellation, teardown, and an operation which never enters its task;
|
||||
- leak-fuse refresh at observable progress points; and
|
||||
- continuous replication's finite initial catch-up and parameter fallback.
|
||||
|
||||
The service, database, and event boundaries are testable with memory-backed PouchDB and injected activity sources. A real Obsidian test is required only when a change crosses into the platform adapter, application lifecycle, or visible UI rather than for this retrieval state machine alone.
|
||||
@@ -18,7 +18,7 @@ Original title: Synchronise without CouchDB
|
||||
|
||||
### Methods and implementations
|
||||
|
||||
Ordinarily, local pouchDB and the remote CouchDB are synchronised by sending each missing document through several conversations in their replication protocol. However, to achieve this plan, we cannot rely on CouchDB and its protocols. This limitation is so harsh. However, Overcoming this means gaining new possibilities. After some trials, It was concluded that synchronisation could be completed even if the actions that could be performed were limited to uploading, downloading and retrieving the list. This means we can use any old-fashioned WebDAV server, and Sophisticated “Object storages” such as Self-hosted MinIO, S3, and R2 or any we like. This is realised by sharing and complementing the differences of the journal by each client. Therefore, The focus is therefore on how to identify which are the differences and send them without dynamic communication.
|
||||
Ordinarily, local pouchDB and the remote CouchDB are synchronised by sending each missing document through several conversations in their replication protocol. However, to achieve this plan, we cannot rely on CouchDB and its protocols. This limitation is so harsh. However, Overcoming this means gaining new possibilities. After some trials, It was concluded that synchronisation could be completed even if the actions that could be performed were limited to uploading, downloading, and retrieving the list. This means we can use any old-fashioned WebDAV server, and sophisticated 'object storages' such as Self-hosted MinIO, S3, and R2, or any we like. This is realised by sharing and complementing the differences of the journal by each client. Therefore, The focus is therefore on how to identify which are the differences and send them without dynamic communication.
|
||||
|
||||
All clients manage their data in PouchDB. I know this is probably known information, but it has its own journal.
|
||||
|
||||
|
||||
94
docs/design_docs_of_journalsync_2nd.md
Normal file
@@ -0,0 +1,94 @@
|
||||
## The design document of the Journal Replicator 2nd Edition
|
||||
|
||||
### Goal
|
||||
- Build a robust and memory-efficient replication foundation that decouples the physical storage layer by leveraging the Web Streams API.
|
||||
- Maintain strict compliance with the data consistency and replication protocols of CouchDB/PouchDB.
|
||||
- Introduce the `IJournalStorage` abstraction to ensure easy extensibility. This allows the core to seamlessly interact with Object Storages (MinIO, S3, R2, etc.) while opening the door for entirely new Storage Engines and Mocks for testing.
|
||||
|
||||
### Motivation
|
||||
- The original Journal Replicator used a custom queue mechanism called `Trench` to manage backpressure, which had limitations regarding memory efficiency when dealing with a massive number of files.
|
||||
- The storage operation logic was tightly coupled with `JournalSyncAbstract`, making it difficult to swap out the physical storage layer (e.g., S3 and WebDAV).
|
||||
- The transfer of revision trees (`_revisions`) conforming to PouchDB's replication protocol was implicitly managed. There was a need for a stricter, more deterministic application of document histories.
|
||||
|
||||
### Differences from v1 (Original)
|
||||
|
||||
The overall architecture and mechanisms have been drastically modernised from the first version. Here are the key differences:
|
||||
|
||||
| Feature / Mechanism | v1 (Original) | v2 (2nd Edition) | Key Benefits in v2 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Backpressure / Queueing** | Custom `Trench` mechanism | Native **Web Streams API** | Prevents memory exhaustion during massive transfers; extremely stable sustained throughput. |
|
||||
| **Storage Architecture** | Tightly coupled in `JournalSyncAbstract` | Abstracted via **`IJournalStorage`** | Easy to plug in new Storage Engines (WebDAV, etc.) and testing Mocks without altering the core logic. |
|
||||
| **Document Application** | Sometimes evaluated as new local edits | Strict `bulkDocs` with **`new_edits: false`** | Drastically faster insertions; prevents redundant conflict branches and "echo" network traffic. |
|
||||
|
||||
#### Class Structure Changes (Diff from v1)
|
||||
|
||||
Looking at the Git diff from the `main` branch, the class structure has undergone a significant refactoring to achieve the aforementioned decoupling:
|
||||
|
||||
- **`JournalSyncAbstract.ts` -> `JournalSyncCore.ts`**: The core logic was renamed. Instead of being an abstract base class for specific storages, it is now a concrete core class that manages the Web Streams API pipelines.
|
||||
- **`JournalSyncMinio.ts` -> `MinioStorageAdapter.ts`**: The MinIO-specific implementation was decoupled from the core logic and converted into a dedicated storage adapter.
|
||||
- **`IJournalStorage` (New)**: Introduced in `JournalStorageAdapter.ts` to define the interface that all storage adapters must implement.
|
||||
|
||||
### Methods and implementations
|
||||
|
||||
#### Pipeline Construction using Web Streams API
|
||||
We replaced `Trench` with standard Web Streams APIs (`ReadableStream`, `TransformStream`, and `WritableStream`) to build the sending and receiving pipelines.
|
||||
- **Sending Pipeline**: Reads documents from the PouchDB changes stream, passes them through a compression `TransformStream`, and pipes them to an upload `WritableStream`. This enables automatic backpressure, keeping memory consumption stable even during large-scale synchronisation.
|
||||
- **Receiving Pipeline**: Processes storage file listing, downloading/decompression, and bulk application to PouchDB in a streamlined manner.
|
||||
|
||||
#### Decoupling the Physical Layer via IJournalStorage
|
||||
To detach the storage operations from the core synchronisation logic (`JournalSyncCore`), we introduced the `IJournalStorage` interface.
|
||||
This ensures extensibility not only to Object Storages (MinIO, S3, R2, etc., handled via `MinioStorageAdapter` and Connection Strings) but also to entirely new Storage Engines (e.g., WebDAV, Google Drive) and Mocks for testing. When adding a new backend, developers only need to add an Adapter that implements this interface, without modifying the core replicator.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class LiveSyncAbstractReplicator {
|
||||
<<abstract>>
|
||||
}
|
||||
class LiveSyncJournalReplicator {
|
||||
+setupJournalSyncClient()
|
||||
}
|
||||
class JournalSyncCore {
|
||||
-IJournalStorage storage
|
||||
+sendLocalJournal()
|
||||
+receiveRemoteJournal()
|
||||
}
|
||||
class IJournalStorage {
|
||||
<<interface>>
|
||||
+upload()
|
||||
+download()
|
||||
}
|
||||
class MinioStorageAdapter {
|
||||
+upload()
|
||||
+download()
|
||||
}
|
||||
|
||||
LiveSyncAbstractReplicator <|-- LiveSyncJournalReplicator : Extends
|
||||
LiveSyncJournalReplicator *-- JournalSyncCore : Instantiates & delegates
|
||||
LiveSyncJournalReplicator ..> MinioStorageAdapter : Creates
|
||||
JournalSyncCore --> IJournalStorage : Uses for backend I/O
|
||||
IJournalStorage <|.. MinioStorageAdapter : Implements
|
||||
```
|
||||
|
||||
#### Strict Application of PouchDB Replication Protocols
|
||||
To synchronise precisely according to the CouchDB/PouchDB protocol, the following steps were optimised:
|
||||
1. **Transferring History**: Using `bulkGet({ revs: true })`, the replicator transfers not only the latest revision of a document but its entire history tree (`_revisions`) alongside the deletion flag (`_deleted`).
|
||||
2. **Applying History**: On the receiving end, the replicator uses `revsDiff` to identify which incoming revisions are missing locally. It then applies them using `bulkDocs(saveDocs, { new_edits: false })`.
|
||||
By specifying `new_edits: false`, PouchDB integrates the received history exactly as it is without treating them as new local edits. This prevents unexpected conflicts and redundant branching of the revision tree.
|
||||
|
||||
|
||||
### Performance and Speed Characteristics
|
||||
|
||||
By migrating from the previous `Trench` architecture to the Web Streams API and strict PouchDB protocol compliance, the replication speed characteristics have changed in the following ways:
|
||||
|
||||
1. **On-Demand Generation and Consistent Throughput**:
|
||||
In the previous `Trench` architecture, the system would eagerly generate or download all 'Changes' in bulk before processing them. This batch processing became a significant bottleneck and caused massive memory spikes (Even though, some of them have been stored into the idb temporally). The Web Streams API fundamentally shifts this to **on-demand (lazy) generation**. Data is pulled and processed only as much as the next pipeline stage (Compress -> Upload/Write) can handle. While this on-demand approach might appear slightly slower in terms of peak burst speed compared to in-memory batching, it completely eliminates the 'create-everything-at-once' bottleneck. This makes the **sustained throughput far more stable** and prevents out-of-memory crashes on mobile devices.
|
||||
|
||||
2. **Faster Receive-Side Application (`new_edits: false`)**:
|
||||
In the previous version, incoming documents were sometimes evaluated as new local edits. By utilising PouchDB's `bulkDocs({ new_edits: false })` alongside the proper `_revisions` tree, we bypass unnecessary conflict generation and local revision hashing. This drastically **speeds up the document insertion process** on the receiving end.
|
||||
|
||||
3. **Optimised Network Traffic**:
|
||||
Because conflicts are resolved deterministically and revision trees are replicated exactly as they exist, the system avoids generating 'echoes' (redundant synchronisations triggered by a device misunderstanding a history tree). This reduces unnecessary background traffic significantly.
|
||||
|
||||
### Consideration and Conclusion
|
||||
The Journal Replicator 2nd Edition achieves robust and scalable storage synchronisation through enhanced memory efficiency (via Web Streams), decoupled extensibility (via IJournalStorage), and strict protocol compliance (via `new_edits: false`).
|
||||
Moving forward, this foundation will make it much easier to officially support a wider variety of backend storages.
|
||||
206
docs/design_docs_of_remote_configurations.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# The design document of remote configuration management
|
||||
|
||||
## Goal
|
||||
|
||||
- Allow us to manage multiple remote connections in a single vault.
|
||||
- Keep the existing synchronisation implementations working without requiring a large rewrite.
|
||||
- Provide a safe migration path from the previous single-remote configuration model.
|
||||
- Allow connections to be imported and exported in a compact and reusable format.
|
||||
|
||||
## Motivation
|
||||
|
||||
Historically, Self-hosted LiveSync stored one effective remote configuration directly in the main settings. This was simple, but it had several limitations.
|
||||
|
||||
- We could only keep one CouchDB, one bucket, or one Peer-to-Peer target as the effective configuration at a time.
|
||||
- Switching between same-type-remotes required manually rewriting the active settings.
|
||||
- Setup URI, QR code, CLI setup, and similar entry points all restored settings differently, which made migration logic easy to miss.
|
||||
- The internal settings shape had gradually become a mix of user-facing settings, transport-specific credentials, and compatibility-oriented values.
|
||||
|
||||
Once multiple remotes of the same type became desirable, the previous model no longer scaled well enough. We therefore needed a structure that could store many remotes, still expose one effective remote to the replication logic, and keep migration and import behaviour consistent.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- Existing synchronisation features must continue to read an effective remote configuration from the current settings.
|
||||
- Existing vaults must continue to work without requiring manual reconfiguration.
|
||||
- Setup URI, QR code, CLI setup, protocol handlers, and other imported settings must be normalised in the same way.
|
||||
- Import and export must be compact enough to be shared easily.
|
||||
- We must be explicit that exported connection strings may contain credentials or secrets.
|
||||
|
||||
## Outlined methods and implementation plans
|
||||
|
||||
### Abstract
|
||||
|
||||
The current settings now have two layers for remote configuration.
|
||||
|
||||
1. A stored collection of named remotes.
|
||||
2. One active remote projected into the legacy flat settings fields.
|
||||
|
||||
This means the replication and database layers can continue to read the effective remote from the existing settings fields, while the settings dialogue and migration logic can manage many stored remotes.
|
||||
|
||||
In short, the list is the source of truth for saved remotes, and the legacy fields remain the runtime compatibility layer.
|
||||
|
||||
### Data model
|
||||
|
||||
The main settings now contain the following properties.
|
||||
|
||||
```typescript
|
||||
type RemoteConfiguration = {
|
||||
id: string;
|
||||
name: string;
|
||||
uri: string;
|
||||
isEncrypted: boolean;
|
||||
};
|
||||
|
||||
type RemoteConfigurations = {
|
||||
remoteConfigurations: Record<string, RemoteConfiguration>;
|
||||
activeConfigurationId: string;
|
||||
};
|
||||
```
|
||||
|
||||
Each entry stores a connection string in `uri`.
|
||||
|
||||
- `sls+http://` or `sls+https://` for CouchDB-compatible remotes
|
||||
- `sls+s3://` for bucket-style remotes
|
||||
- `sls+p2p://` for Peer-to-Peer remotes
|
||||
|
||||
This structure allows multiple remotes of the same type to be stored without adding a large number of duplicated settings fields.
|
||||
|
||||
### Runtime compatibility
|
||||
|
||||
The replication logic still reads the effective remote from legacy flat settings such as the following.
|
||||
|
||||
- `remoteType`
|
||||
- `couchDB_URI`, `couchDB_USER`, `couchDB_PASSWORD`, `couchDB_DBNAME`
|
||||
- `endpoint`, `bucket`, `accessKey`, `secretKey`, and related bucket fields
|
||||
- `P2P_roomID`, `P2P_passphrase`, and related Peer-to-Peer fields
|
||||
|
||||
When a remote is activated, its connection string is parsed and projected into these legacy fields. Therefore, existing services do not need to know whether the remote came from an old vault, a Setup URI, or the new remote list.
|
||||
|
||||
This projection is intentionally one-way at runtime. The stored remote list is the persistent catalogue, while the flat fields describe the remote currently in use.
|
||||
|
||||
### Connection string format
|
||||
|
||||
The connection string is the transport-neutral storage format for a remote entry.
|
||||
|
||||
Benefits:
|
||||
|
||||
- It is compact enough for clipboard-based workflows.
|
||||
- It can be used for import and export in the settings dialogue.
|
||||
- It avoids introducing a separate serialisation format only for the remote list.
|
||||
- It can be parsed into the legacy settings shape whenever the active remote changes.
|
||||
|
||||
This is not equivalent to Setup URI.
|
||||
|
||||
- Setup URI represents a broader settings transfer workflow.
|
||||
- A remote connection string represents one remote only.
|
||||
|
||||
### Import and export
|
||||
|
||||
The settings dialogue now supports the following workflows.
|
||||
|
||||
- Add connection: create a new remote by using the remote setup dialogues.
|
||||
- Import connection: paste a connection string, validate it, and save it as a named remote.
|
||||
- Export: copy a stored remote connection string to the clipboard.
|
||||
|
||||
Import normalises the string by parsing and serialising it again before saving. This ensures that equivalent but differently formatted URIs are saved in a canonical form.
|
||||
|
||||
Export is intentionally simple. It copies the connection string itself, because this is the most direct representation of one remote entry.
|
||||
|
||||
### Security note
|
||||
|
||||
Connection strings may include credentials, secrets, JWT-related values, or Peer-to-Peer passphrases.
|
||||
|
||||
Therefore:
|
||||
|
||||
- Export is a deliberate clipboard operation.
|
||||
- Import trusts the supplied connection string as-is after parsing.
|
||||
- We should regard exported connection strings as sensitive information, much like Setup URI or a credentials-bearing configuration file.
|
||||
|
||||
The `isEncrypted` field is currently reserved for future expansion. At present, the connection string itself is stored plainly inside the settings data, in the same sense that the effective runtime configuration can contain usable remote credentials.
|
||||
|
||||
### Migration strategy
|
||||
|
||||
Older vaults store only one effective remote in the flat settings fields. The migration creates a first remote list from those values.
|
||||
|
||||
Rules:
|
||||
|
||||
- If no remote list exists and the legacy fields contain a CouchDB configuration, create `legacy-couchdb`.
|
||||
- If no remote list exists and the legacy fields contain a bucket configuration, create `legacy-s3`.
|
||||
- If no remote list exists and the legacy fields contain a Peer-to-Peer configuration, create `legacy-p2p`.
|
||||
- If more than one legacy remote is populated, create all possible entries and select the active one according to `remoteType`.
|
||||
|
||||
This migration is intentionally additive. It does not remove the flat fields because they remain necessary as the active runtime projection.
|
||||
|
||||
### Normalisation and application paths
|
||||
|
||||
One important design lesson from this work is that migration cannot rely only on loading `data.json`.
|
||||
|
||||
Settings may enter the system from several routes:
|
||||
|
||||
- normal settings load
|
||||
- Setup URI
|
||||
- QR code
|
||||
- protocol handler
|
||||
- CLI setup
|
||||
- Peer-to-Peer remote configuration retrieval
|
||||
- red flag based remote adjustment
|
||||
- settings markdown import
|
||||
|
||||
To keep behaviour consistent, normalisation is centralised in the settings service.
|
||||
|
||||
- `adjustSettings` is responsible for in-place normalisation and migration of a settings object.
|
||||
- `applyExternalSettings` is responsible for applying imported or externally supplied settings after passing them through the same normalisation flow.
|
||||
|
||||
This ensures that imported settings can migrate to the current remote list model even if they never passed through the ordinary `loadSettings` path.
|
||||
|
||||
### Why not store only the remote list
|
||||
|
||||
It would be possible to let all consumers parse the active remote every time and stop using the flat fields entirely. However, this would require broader changes across replication, diagnostics, and compatibility layers.
|
||||
|
||||
The current design keeps the change set limited.
|
||||
|
||||
- The remote list improves storage and UX.
|
||||
- The flat fields preserve compatibility and reduce migration risk.
|
||||
|
||||
This is a pragmatic transitional architecture, not an accidental duplication.
|
||||
|
||||
## Test strategy
|
||||
|
||||
The feature should be tested from four viewpoints.
|
||||
|
||||
1. Migration from old settings.
|
||||
- A vault with only legacy flat remote settings should gain a remote list automatically.
|
||||
- The correct active remote should be selected according to `remoteType`.
|
||||
|
||||
2. Runtime activation.
|
||||
- Activating a stored remote should correctly project its values into the effective flat settings.
|
||||
|
||||
3. External import paths.
|
||||
- Setup URI, QR code, CLI setup, Peer-to-Peer remote config, red flag adjustment, and settings markdown import should all pass through the same normalisation path.
|
||||
|
||||
4. Import and export.
|
||||
- Imported connection strings should be parsed, canonicalised, named, and stored correctly.
|
||||
- Export should copy the exact saved connection string.
|
||||
|
||||
## Documentation strategy
|
||||
|
||||
- This document explains the design and compatibility model of remote configuration management.
|
||||
- User-facing setup documents should explain only how to add, import, export, and activate remotes.
|
||||
- Release notes may refer to this document when changes in remote handling are significant.
|
||||
|
||||
## Outlook
|
||||
|
||||
Import/export configuration strings should also be encrypted in the future, but this is a separate feature that can be added on top of the current design.
|
||||
|
||||
## Consideration and conclusion
|
||||
|
||||
The remote configuration list solves the practical need to manage multiple remotes without forcing the whole codebase to abandon the previous effective-settings model at once.
|
||||
|
||||
Its core idea is modest but effective.
|
||||
|
||||
- Store named remotes as connection strings.
|
||||
- Select one active remote.
|
||||
- Project it into the legacy settings for runtime use.
|
||||
- Normalise every imported settings object through the same path.
|
||||
|
||||
This keeps the implementation understandable and migration-friendly. It also opens the door for future work, such as encrypted per-remote storage, richer remote metadata, or remote-scoped options, without forcing another large redesign of how remotes are represented.
|
||||
62
docs/p2p_sync_updates_2026.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# User Guide: Peer-to-Peer Synchronisation (2026 Edition)
|
||||
|
||||
Peer-to-Peer (P2P) synchronisation has evolved significantly. This guide covers the essential setup and the new features introduced in the 2026 updates.
|
||||
|
||||
## 1. Core Concept: Server-less Freedom
|
||||
P2P synchronisation allows your devices to talk directly to each other using WebRTC. A central server is not required for data storage, ensuring maximum privacy and "freedom."
|
||||
|
||||
## 2. Setting Up via P2P Status Pane
|
||||
You no longer need to navigate through complex menus. Simply open the **P2P Status** (via the ribbon icon or command palette) and click the **⚙ (Cog)** icon.
|
||||
|
||||
This opens the **P2P Setup** dialogue where you can configure the essentials:
|
||||
- **Room ID:** A unique identifier for your synchronisation group.
|
||||
- **Passphrase:** Your encryption key. Ensure all your devices use the exact same passphrase.
|
||||
- **Device Name:** A recognisable name for the current device (e.g., `iphone-16`).
|
||||
|
||||
Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network.
|
||||
|
||||
*Tip: You can also toggle **Auto Connect** in the setup dialogue to automatically join the network whenever Obsidian starts.*
|
||||
|
||||
## 3. Real-time Control
|
||||
The status pane in the right sidebar provides granular control over your synchronisation:
|
||||
|
||||
- **Active P2P Remote (new):** P2P now has its own active remote selection, separate from the normal active remote for database replication. Use the combo box next to the cog icon to choose which P2P remote configuration is active for P2P features.
|
||||
- **Create P2P Remote (new):** Use the **+** button to open the P2P setup dialogue and create a dedicated P2P remote configuration. This is recommended when no P2P active remote has been selected yet.
|
||||
- **Selection required (new):** If no P2P active remote is selected, the pane asks for selection before P2P target-related changes are saved.
|
||||
|
||||
- **Signalling Status:** Shows if you are connected to the relay (🟢 Online).
|
||||
- **Live-push (Broadcast):** Toggle "Broadcast changes" to notify other peers whenever you make an edit.
|
||||
- **Replicate now (🔄):** Start immediate bidirectional replication with a visible peer (Pull, then Push).
|
||||
- **Watch (🔔/🔕):** Enable "Watch" on specific peers to automatically pull changes when they broadcast. This creates a "LiveSync-like" experience.
|
||||
- **Sync target (🔗/⛓️💥):** Mark specific peers as **sync targets**. Peers marked here will be included when you run the **"P2P: Sync with targets"** command (see section 5). Click the button next to a peer to toggle it on (🔗, highlighted) or off (⛓️💥). This setting is persisted in your configuration.
|
||||
|
||||
## 4. Replication Dialogue
|
||||
If you want to synchronise with a specific peer manually, use the **Replication** command or button. This opens the **Replication Dialogue** listing available devices.
|
||||
|
||||
Inside the dialogue, the **Server Status** card at the top confirms you are still connected while performing the sync.
|
||||
The status card now shows a stable **Room ID suffix** above **Peer ID**. The Room ID suffix is better for identifying your P2P group, while Peer ID may change between connections.
|
||||
|
||||
Two actions are available per peer:
|
||||
|
||||
- **Sync** — Starts a bidirectional synchronisation (Pull then Push) and keeps the dialogue open so you can monitor progress or sync with additional peers.
|
||||
- **Start Sync & Close** — Runs the same bidirectional synchronisation, waits for it to settle, then closes the dialogue. After a successful synchronisation, it also closes the signalling connection.
|
||||
|
||||
On supported mobile and desktop devices, LiveSync keeps the screen awake while this peer-selection dialogue is open and while its synchronisations finish. This is intentional: display sleep can interrupt peer discovery or connection establishment and require detection to start again. Wake Lock support remains best effort, does not keep a hidden application running in the background, and does not override operating-system sleep or suspension.
|
||||
|
||||
During a P2P rebuild, peer discovery and selection remain protected after the rebuild has started. Keep Obsidian visible until a peer is selected and the transfer finishes, because mobile platform restrictions can still pause or terminate a hidden application.
|
||||
|
||||
## 5. Syncing with Registered Targets via Command Palette
|
||||
|
||||
You can now trigger a synchronisation with all your pre-registered target peers in one step, without opening any UI.
|
||||
|
||||
1. Open the **Command Palette** (`Ctrl/Cmd + P`).
|
||||
2. Run **"P2P: Sync with targets"**.
|
||||
|
||||
This command synchronises with every peer whose **SYNC** toggle is enabled in the **Detected Peers** list. If no targets are registered, or if the P2P server is not running, the command will notify you accordingly.
|
||||
|
||||
*Tip: Pair this command with a hotkey for a quick, keyboard-driven sync workflow.*
|
||||
|
||||
## 6. Technical Improvements in 2026
|
||||
- **Decoupled Architecture:** The UI is now strictly separated from the core logic, making the plug-in more stable across different platforms (Mobile, Desktop, and Web).
|
||||
- **Svelte 5 UI:** The interface has been rebuilt for better responsiveness and clearer status indicators.
|
||||
- **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes.
|
||||
@@ -2,128 +2,104 @@
|
||||
|
||||
[Japanese docs](./quick_setup_ja.md) - [Chinese docs](./quick_setup_cn.md).
|
||||
|
||||
The plugin has so many configuration options to deal with different circumstances. However, only a few settings are required in the normal cases. Therefore, `The Setup wizard` has been implemented to simplify the setup.
|
||||
This guide establishes ordinary note synchronisation on the first device and then adds another device. Optional features are configured only after this basic path works.
|
||||
|
||||

|
||||
Before starting:
|
||||
|
||||
There are three methods to set up Self-hosted LiveSync.
|
||||
- back up every Vault involved;
|
||||
- disable Obsidian Sync, iCloud synchronisation, and any other service which writes to the same Vault;
|
||||
- prepare the remote service and a Setup URI; and
|
||||
- keep the Setup URI and its passphrase separate from each other.
|
||||
|
||||
1. [Using setup URIs](#1-using-setup-uris) *(Recommended)*
|
||||
2. [Minimal setup](#2-minimal-setup)
|
||||
3. [Full manually setup the and Enable on this dialogue](#3-manually-setup)
|
||||
This walkthrough covers the recommended provisioned CouchDB path. Follow [Set up a CouchDB server](./setup_own_server.md) to prepare the server and Setup URI.
|
||||
|
||||
## At the first device
|
||||
## What a Setup URI contains
|
||||
|
||||
### 1. Using setup URIs
|
||||
A Setup URI starts with `obsidian://setuplivesync?settings=`. It contains encrypted connection settings, including credentials, and must be protected even though it is encrypted.
|
||||
|
||||
> [!TIP]
|
||||
> What is the setup URI? Why is it required?
|
||||
> The setup URI is the encrypted representation of Self-hosted LiveSync configuration as a URI. This starts `obsidian://setuplivesync?settings=`. This is encrypted with a passphrase, so that it can be shared relatively securely between devices. It is a bit long, but it is one line. This allows a series of settings to be set at once without any inconsistencies.
|
||||
>
|
||||
> If you have configured the remote database by [Automated setup on Fly.io](./setup_flyio.md#a-very-automated-setup) or [set up your server with the tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server), **you should have one of them**
|
||||
The Setup URI passphrase decrypts the URI. It is different from the Vault encryption passphrase which protects synchronised data. Store both securely, and do not send the Setup URI and its passphrase through the same channel.
|
||||
|
||||
In this procedure, [this video](https://youtu.be/7sa_I1832Xc?t=146) may help us.
|
||||
## Set up the first device
|
||||
|
||||
1. Click `Use` button (Or launch `Use the copied setup URI` from Command palette).
|
||||
2. Paste the Setup URI into the dialogue
|
||||
3. Type the passphrase of the Setup URI
|
||||
4. Answer `yes` for `Importing LiveSync's conf, OK?`.
|
||||
5. Answer `Set it up as secondary or subsequent device` for `How would you like to set it up?`.
|
||||
6. Initialisation will begin, please hold a while.
|
||||
7. You will asked about the hidden file synchronisation, answer as you like.
|
||||
1. If you are new to Self-hosted LiveSync, we can configure it later so leave it once.
|
||||
8. Synchronisation has been started! `Reload app without saving` is recommended after the indicators of Self-hosted LiveSync disappear.
|
||||
Use this path only when the remote database is new, or when this device is intentionally the source of truth for a full server rebuild.
|
||||
|
||||
OK, we can proceed the [next step](#).
|
||||
1. Install and enable Self-hosted LiveSync in the intended Vault.
|
||||
2. Select the `Welcome to Self-hosted LiveSync` Notice to open onboarding.
|
||||
3. Select `I am setting this up for the first time`, then confirm that you want to set up a new synchronisation.
|
||||
4. On `Connection Method`, select `Use a Setup URI (Recommended)`.
|
||||
5. Paste the Setup URI, enter its Setup URI passphrase, and select `Test Settings and Continue`.
|
||||
|
||||
### 2. Minimal setup
|
||||

|
||||
|
||||
If you do not have any setup URI, Press the `start` button. The setting dialogue turns into the wizard mode and will display only minimal items.
|
||||
6. Review `Setup Complete: Preparing to Initialise Server`, then select `Restart and Initialise Server`.
|
||||
|
||||
>[!TIP]
|
||||
> We can generate the setup URI with the tool in any time. Please use [this tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server).
|
||||

|
||||
|
||||

|
||||
7. Read the final overwrite warning carefully. Select `I Understand, Overwrite Server` only after checking that backups exist and that replacing the remote data is intended.
|
||||
|
||||

|
||||
|
||||
#### Select the remote type
|
||||
8. A newly provisioned database may show `Fetch Remote Configuration Failed` because it does not contain a saved preferred configuration yet. If this is a genuinely new setup, select `Skip and proceed`. Otherwise, stop and investigate before continuing.
|
||||
|
||||
1. Select the Remote Type from dropdown list.
|
||||
We now have a choice between CouchDB (and its compatibles) and object storage (MinIO, S3, R2). CouchDB is the first choice and is also recommended. And supporting Object Storage is an experimental feature.
|
||||

|
||||
|
||||
#### Remote configuration
|
||||
9. Acknowledge `All optional features are disabled`. Optional features remain off until the ordinary synchronisation path has been verified.
|
||||
10. Allow initialisation and any requested restart to finish. Keep Obsidian open until the LiveSync progress indicators have cleared.
|
||||
|
||||
##### CouchDB
|
||||
Create an ordinary test note and allow it to upload before adding another device.
|
||||
|
||||
Enter the information for the database we have set up.
|
||||
## Create a Setup URI for another device
|
||||
|
||||

|
||||
Generate the additional-device Setup URI from the working first device. This captures the settings which that device is actually using, rather than asking another device to reuse the bootstrap URI produced during server provisioning.
|
||||
|
||||
##### Object Storage
|
||||
1. Open the Obsidian command palette on the first device.
|
||||
2. Run `Self-hosted LiveSync: Copy settings as a new Setup URI`.
|
||||
3. Enter a new passphrase which will protect this Setup URI, then select `OK`.
|
||||
|
||||
1. Enter the information for the S3 API and bucket.
|
||||

|
||||
|
||||

|
||||
4. Copy the resulting Setup URI, then select `OK`.
|
||||
|
||||
Note 1: if you use S3, you can leave the Endpoint URL empty.
|
||||
Note 2: if your Object Storage cannot configure the CORS setting fully, you may able to connect to the server by enabling the `Use Custom HTTP Handler` toggle.
|
||||

|
||||
|
||||
2. Press `Test` of `Test Connection` once and ensure you can connect to the Object Storage.
|
||||
Store the new Setup URI and its passphrase separately. The URI is encrypted, but it contains credentials and Vault settings, so continue to protect it.
|
||||
|
||||
#### Only CouchDB: Test database connection and Check database configuration
|
||||
## Add another device
|
||||
|
||||
We can check the connectivity to the database, and the database settings.
|
||||
Start with a new or separately backed-up Vault. Do not use a production Vault containing unsynchronised notes unless you have reviewed the [Fast Setup choices](./tips/fast-setup.md).
|
||||
|
||||

|
||||
1. Install and enable Self-hosted LiveSync.
|
||||
2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice.
|
||||
3. Select `I am adding a device to an existing synchronisation setup`, then confirm that you want to add the device.
|
||||
4. On `Device Setup Method`, select `Use a Setup URI (Recommended)`.
|
||||
5. Paste the new Setup URI generated by the first device, enter its Setup URI passphrase, and select `Test Settings and Continue`.
|
||||
6. Review `Setup Complete: Preparing to Fetch Synchronisation Data`, then select `Restart and Fetch Data`.
|
||||
|
||||
#### Only CouchDB: Check and Fix database configuration
|
||||

|
||||
|
||||
Check the database settings and fix any problems on the spot.
|
||||
7. For a new or empty Vault, select `Overwrite all with remote files`. For a Vault with local work, stop and choose the appropriate strategy from the [Fast Setup guide](./tips/fast-setup.md).
|
||||
|
||||

|
||||

|
||||
|
||||
This item may vary depending on the connection. In the above case, press all three Fix buttons.
|
||||
If the Fix buttons disappear and all become check marks, we are done.
|
||||
8. When asked how to handle extra local files, the conservative choice is `Keep local files even if not on remote`. Select the delete option only when the local Vault is disposable and an exact remote copy is intended.
|
||||
|
||||
#### Confidentiality configuration (Optional but very preferred)
|
||||

|
||||
|
||||

|
||||
9. Allow retrieval, file reflection, and any requested restart to finish. Keep Obsidian open until the LiveSync progress indicators have cleared.
|
||||
|
||||
Enable End-to-end encryption and the contents of your notes will be encrypted at the moment it leaves the device. We strongly recommend enabling it. And `Path Obfuscation` also obfuscates filenames. Now stable and recommended.
|
||||
Confirm that the ordinary test note from the first device appears unchanged. Then edit or create a second ordinary note on the new device, and confirm that it reaches the first device.
|
||||
|
||||
These setting can be disabled if you are inside a closed network and it is clear that you will not be accessed by third parties.
|
||||

|
||||
|
||||
> [!TIP]
|
||||
> Encryption is based on 256-bit AES-GCM.
|
||||
## After ordinary synchronisation works
|
||||
|
||||
We should proceed to the Next step.
|
||||
Add optional features separately so that their ownership and initialisation direction are explicit:
|
||||
|
||||
#### Sync Settings
|
||||
Finally, finish the wizard by selecting a preset for synchronisation.
|
||||
- [Hidden File Sync](./tips/hidden-file-sync.md) for selected hidden files and folders; or
|
||||
- [Customisation Sync](./settings.md#6-customisation-sync-advanced) for managed Obsidian customisations.
|
||||
|
||||
Note: If you are going to use Object Storage, you cannot select `LiveSync`.
|
||||
Do not enable both features for the same files.
|
||||
|
||||

|
||||
## Manual configuration
|
||||
|
||||
Select any synchronisation methods we want to use and `Apply`. If database initialisation is required, it will be performed at this time. When `All done!` is displayed, we are ready to synchronise.
|
||||
|
||||
The dialogue of `Copy settings as a new setup URI` will be open automatically. Please input a passphrase to encrypt the new `Setup URI`. (This passphrase is to encrypt the setup URI, not the vault).
|
||||
|
||||

|
||||
|
||||
The Setup URI will be copied to the clipboard, please make a note(Not in Obsidian) of this.
|
||||
|
||||
>[!TIP]
|
||||
We can copy this in any time by `Copy current settings as a new setup URI`.
|
||||
|
||||
### 3. Manually setup
|
||||
|
||||
It is strongly recommended to perform a "minimal set-up" first and set up the other contents after making sure has been synchronised.
|
||||
|
||||
However, if you have some specific reasons to configure it manually, please click the `Enable` button of `Enable LiveSync on this device as the set-up was completed manually`.
|
||||
And, please copy the setup URI by `Copy current settings as a new setup URI` and make a note(Not in Obsidian) of this.
|
||||
|
||||
## At the subsequent device
|
||||
After installing Self-hosted LiveSync on the first device, we should have a setup URI. **The first choice is to use it**. Please share it with the device you want to setup.
|
||||
|
||||
It is completely same as [Using setup URIs on the first device](#1-using-setup-uris). Please refer it.
|
||||
If a Setup URI is unavailable, choose `Enter the server information manually` during onboarding. Manual configuration is an advanced path: verify the connection, encryption, remote profile, and synchronisation preset before initialising either side. After the first device works, use `Copy settings as a new Setup URI` from the command palette to add later devices through the recommended path.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Quick setup
|
||||
このプラグインには、いろいろな状況に対応するための非常に多くの設定オプションがあります。しかし、実際に使用する設定項目はそれほど多くはありません。そこで、初期設定を簡略化するために、「セットアップウィザード」を実装しています。
|
||||
※なお、次のデバイスからは、`Copy setup URI`と`Open setup URI`を使ってセットアップしてください。
|
||||
※なお、次のデバイスからは、`現在の設定をセットアップURIにコピー`と`セットアップURIで接続`を使ってセットアップしてください。
|
||||
|
||||
|
||||
## Wizardの使い方
|
||||
@@ -71,7 +71,8 @@ Fixボタンがなくなり、すべてチェックマークになれば完了
|
||||

|
||||
|
||||
Presetsから、いずれかの同期方法を選び`Apply`を行うと、必要に応じてローカル・リモートのデータベースを初期化・構築します。
|
||||
All done! と表示されれば完了です。自動的に、`Copy setup URI`が開き、`Setup URI`を暗号化するパスフレーズを聞かれます。
|
||||
「All done!」(日本語環境では「完了!」)と表示されれば完了です。自動的に、「現在の設定をセットアップURIにコピー」のダイアログが開き、Setup URIを暗号化するためのパスフレーズを求められます(このパスフレーズはSetup URIを暗号化するためのもので、Vault自体の暗号化キーではありません)。
|
||||
パスフレーズを入力すると、クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
|
||||
|
||||

|
||||
|
||||
@@ -79,10 +80,14 @@ All done! と表示されれば完了です。自動的に、`Copy setup URI`が
|
||||
クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
|
||||
|
||||
# 2台目以降の設定方法
|
||||
2台目の端末にSelf-hosted LiveSyncをインストールしたあと、コマンドパレットから`Open setup URI`を選択し、転送したsetup URIを入力します。その後、パスフレーズを入力するとセットアップ用のウィザードが開きます。
|
||||
2台目の端末にSelf-hosted LiveSyncをインストールしたあと、コマンドパレットから`Use the copied setup URI (Formerly Open setup URI)`を選択し、転送したsetup URIを入力します。その後、パスフレーズを入力するとセットアップ用のウィザードが開きます。
|
||||
下記のように答えてください。
|
||||
|
||||
- `Importing LiveSync's conf, OK?` に `Yes`
|
||||
- `How would you like to set it up?` に `Set it up as secondary or subsequent device`
|
||||
|
||||
これで設定が反映され、レプリケーションが開始されます。
|
||||
これで設定が反映され、レプリケーションが開始されます。
|
||||
|
||||
> [!TIP]
|
||||
> **ファストセットアップ (Fast Setup)**
|
||||
> 近年のバージョンでは、セットアップURIの読み込みやデータの全取得(Fetch All)を実行した際、より簡単に同期戦略を選択して即座に初期同期を完了できる **ファストセットアップ (Simple Fetch)** フローが利用できます。詳細は [ファストセットアップガイド](./tips/fast-setup_ja.md) をご参照ください。
|
||||
1826
docs/releases/0.25.md
Normal file
1794
docs/releases/legacy.md
Normal file
67
docs/review_harness.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Review Harness
|
||||
|
||||
The Review Harness is an opt-in, real-device review tool for Self-hosted LiveSync maintainers. It replaces the disabled legacy Test Pane with fixed, auditable scenarios, a device-local one-shot continuation, and a Markdown report which can be pasted into a pull request.
|
||||
|
||||
It supplies supporting evidence rather than a release gate by itself. Unit, integration, Compose, CLI E2E, and real-Obsidian E2E remain authoritative for the boundaries they own.
|
||||
|
||||
## Enabling the Harness
|
||||
|
||||
1. Use a dedicated test Vault.
|
||||
2. Enable **Power users → Enable Developers' Debug Tools**.
|
||||
3. Restart Obsidian.
|
||||
4. Run **Self-hosted LiveSync: Open review harness** from the command palette.
|
||||
|
||||
The command and view are registered only when developer tools are enabled. Disabling the setting and restarting removes the entry point from an ordinary user session.
|
||||
|
||||
## Scenarios and access
|
||||
|
||||
| Scenario | Mode | Access | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| Settings lifecycle | Automatic | Read-only | Exposes the seven synchronisation choices as typed observations and, for a genuinely new Vault, compares the selected recommendations with Commonlib's new-Vault contract. Existing-Vault setting preservation remains an automated migration and `settings-ui` E2E responsibility. |
|
||||
| Compatibility review boundary | Guided | Device-local state | Observes the current dedicated compatibility controller and opens its actual review action. The Harness does not restore Change Log acknowledgement, `lastReadUpdates`, or a separate manual Pass or Fail result. |
|
||||
| P2P composition | Automatic | Read-only | Confirms that the live P2P result resolves one replicator bound to the active Obsidian services. Peer discovery, replacement, reconnection, and transfer remain unit, CLI, and Compose E2E responsibilities. |
|
||||
| Vault fixture round trip | Automatic, explicit | Dedicated Vault fixtures | After explicit confirmation, creates, reads, modifies, renames, and removes one owned fixture tree. General Vault reflection remains covered by its separate real-Obsidian E2E workflow. |
|
||||
|
||||
**Automatic** runs only the two read-only local observations. **Full review** also starts the guided compatibility observation and asks before the Vault fixture scenario writes anything. A scenario can also be run individually.
|
||||
|
||||
These observations deliberately do not repeat stronger automated workflows. They exist to record which contracts the current real-device composition exposes while a maintainer reviews an immutable artefact.
|
||||
|
||||
### Vault fixture boundary
|
||||
|
||||
The Vault scenario owns only `__self-hosted-livesync-review-harness__`. It refuses to run if that path already exists, so it cannot assume ownership of a user's existing file or folder. Once it creates the root, it removes the complete owned tree from a `finally` block whether the round trip passes or fails.
|
||||
|
||||
The Harness accepts no path, command, code, remote configuration, or credential through plug-in data or its continuation state. New write scenarios must use a distinct fixed fixture root, describe their side effects in the interface, require confirmation, and clean up in `finally`.
|
||||
|
||||
## Restart continuation
|
||||
|
||||
The restart action writes a small device-local record under `review-harness-v1`, then asks Obsidian to reload. The record permits only:
|
||||
|
||||
- the fixed `compatibility-review` scenario;
|
||||
- the fixed `awaiting-restart` stage;
|
||||
- a canonical ISO request time; and
|
||||
- a request identifier derived exactly as `compatibility-review-<request time>`.
|
||||
|
||||
On the next settings load, the Harness deletes the record before parsing and acting on it. A valid record reopens the Harness after layout is ready and leaves the compatibility observation waiting for the reviewer. Invalid state is removed and reported as a failed continuation. The record is not stored in `data.json`, copied through a Setup URI, or synchronised to another device.
|
||||
|
||||
The compatibility controller does not require an Obsidian restart to acknowledge a pause. This continuation belongs to the review tool and proves its one-shot reload boundary; it is not a second compatibility lifecycle.
|
||||
|
||||
## Reports and privacy
|
||||
|
||||
**Copy Markdown report** includes:
|
||||
|
||||
- the plug-in and Obsidian versions;
|
||||
- the platform, user agent, and viewport;
|
||||
- each scenario's status and bounded summary; and
|
||||
- a bounded event transcript.
|
||||
|
||||
The formatter has no inputs for Vault identifiers, paths, file names, file contents, remote configuration, or secrets. The report is copied locally and is never transmitted by the plug-in. Review the environment information before posting it because a user agent and viewport may identify a device or operating system.
|
||||
|
||||
Unexpected runtime errors are written to the local LiveSync log, while copied reports retain only a generic failure summary. This prevents an adapter error from copying a local path or file name into a pull request by accident.
|
||||
|
||||
## Automated real-Obsidian coverage
|
||||
|
||||
External automation drives the stable `data-testid` attributes beginning with `review-harness-`. The dedicated workflow checks only Harness-owned behaviour: debug-only registration, consume-before-use continuation handling, fixed Vault fixture clean-up, report privacy, and mobile layout.
|
||||
|
||||
Compatibility dialogue behaviour and persistence belong to `test:e2e:obsidian:settings-ui`. Real P2P transport belongs to the Compose P2P suite. General Vault reflection belongs to `test:e2e:obsidian:vault-reflection`. Keeping those responsibilities separate prevents the review tool from becoming a second, weaker copy of the acceptance suite.
|
||||
|
||||
The mobile checks use `app.emulateMobile(true)`, a representative viewport, and safe-area and touch-target assertions. They do not claim to reproduce native operating-system overlays.
|
||||
383
docs/settings.md
@@ -4,6 +4,17 @@ NOTE: This document not completed. I'll improve this doc in a while. but your co
|
||||
|
||||
There are many settings in Self-hosted LiveSync. This document describes each setting in detail (not how-to). Configuration and settings are divided into several categories and indicated by icons. The icon is as follows:
|
||||
|
||||
## Feature maturity for 1.0
|
||||
|
||||
The following status applies to optional and compatibility features in the 1.0 line:
|
||||
|
||||
| Status | Features | 1.0 policy |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Supported, opt-in | Peer-to-Peer Synchronisation, Hidden File Sync, and Customisation Sync | Maintained and covered by focused real-runtime tests. Enable them only where their separate setup and operational constraints are acceptable. |
|
||||
| Maintained, advanced | Data Compression | Available as an explicit storage and bandwidth trade-off. It remains disabled by default because the measured processing and memory costs outweigh the mixed-dataset saving. |
|
||||
| Beta or experimental | JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 | Retained for explicit testing and specialised use. They remain disabled by default and are not part of the minimum supported setup. |
|
||||
| Compatibility only | V1 dynamic iteration counts, the old IndexedDB adapter, non-current hash algorithms, Eden chunks, and the stored `doNotUseFixedRevisionForChunks` key | Existing settings and data remain readable. New Vaults use the current defaults, and compatibility controls are shown only where a migration or recovery path still needs them. |
|
||||
|
||||
| Icon | Description |
|
||||
| :--: | ------------------------------------------------------------------ |
|
||||
| 💬 | [0. Change Log](#0-change-log) |
|
||||
@@ -12,7 +23,7 @@ There are many settings in Self-hosted LiveSync. This document describes each se
|
||||
| 🛰️ | [3. Remote Configuration](#3-remote-configuration) |
|
||||
| 🔄 | [4. Sync Settings](#4-sync-settings) |
|
||||
| 🚦 | [5. Selector (Advanced)](#5-selector-advanced) |
|
||||
| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) |
|
||||
| 🔌 | [6. Customisation sync (Advanced)](#6-customisation-sync-advanced) |
|
||||
| 🧰 | [7. Hatch](#7-hatch) |
|
||||
| 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) |
|
||||
| 💪 | [9. Power users (Power User)](#9-power-users-power-user) |
|
||||
@@ -21,12 +32,18 @@ There are many settings in Self-hosted LiveSync. This document describes each se
|
||||
|
||||
## 0. Change Log
|
||||
|
||||
This pane shows version up information. You can check what has been changed in recent versions.
|
||||
This pane always shows the current release history. It does not track whether a particular plug-in version has been read and does not open automatically after an ordinary update.
|
||||
|
||||
Internal database or settings compatibility reviews use a separate safety dialogue, not this pane. The dialogue explains why remote synchronisation has been paused and preserves the automatic synchronisation choices which were configured before the update. A configured Vault which was copied, restored, or opened in a new Obsidian profile can require this review because its device-local acknowledgement is not part of the Vault data. An empty local database is not accepted as evidence that it is safe to continue. An existing unconfigured Vault remains in onboarding without this synchronisation warning; its missing acknowledgement is not filled in automatically, so it is evaluated if the Vault is configured later. Closing the dialogue keeps synchronisation paused. When the detected state can be handled by the running version, the explicit resume action records the current internal database version and restores the configured behaviour. A persistent Notice and the `Review why synchronisation is paused` command reopen the review. An older installation cannot dismiss a pause caused by a newer database or settings version.
|
||||
|
||||
## 1. Setup
|
||||
|
||||
This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync.
|
||||
|
||||
An unconfigured installation does not open the onboarding dialogue automatically or scan the Vault into the local database. A long-lived Notice offers the onboarding action, and **Open onboarding wizard** remains available from the command palette after that Notice closes.
|
||||
|
||||
Choose the new-device path when this device owns the files which should initialise synchronisation. Choose the existing-device path when it should receive an established remote state. The wizard reserves Rebuild or Fetch respectively before enabling the settings and requesting a restart, so the selected initialisation runs before the ordinary start-up scan.
|
||||
|
||||
### 1. Quick Setup
|
||||
|
||||
Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks.
|
||||
@@ -35,10 +52,14 @@ Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted L
|
||||
|
||||
Setup the Self-hosted LiveSync with the `setup URI` which is [copied from another device](#copy-current-settings-as-a-new-setup-uri) or the setup script.
|
||||
|
||||
A current Setup URI retains its remote profiles, display names, and separate main and P2P selections. Older Setup URIs containing only flat connection settings remain supported and are migrated to a remote profile when applied.
|
||||
|
||||
#### Manual setup
|
||||
|
||||
Step-by-step setup for Self-hosted LiveSync. You can setup Self-hosted LiveSync manually with Minimal setting items.
|
||||
|
||||
Completing manual CouchDB, Object Storage, or P2P setup creates the corresponding remote profile without replacing profiles which are already saved. CouchDB and Object Storage setup select the new profile as the main remote. P2P setup selects it for P2P use and, when the wizard is enabling LiveSync, also selects it as the main remote. A descriptive display name is generated and can be changed later.
|
||||
|
||||
#### Enable LiveSync
|
||||
|
||||
This button only appears when the setup was not completed. If you have completed the setup manually, you can enable LiveSync on this device by this button.
|
||||
@@ -68,7 +89,7 @@ Following panes will be shown when you enable this setting.
|
||||
| Icon | Description |
|
||||
| :--: | ------------------------------------------------------------------ |
|
||||
| 🚦 | [5. Selector (Advanced)](#5-selector-advanced) |
|
||||
| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) |
|
||||
| 🔌 | [6. Customisation sync (Advanced)](#6-customisation-sync-advanced) |
|
||||
| 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) |
|
||||
|
||||
#### Enable poweruser features
|
||||
@@ -120,6 +141,18 @@ Setting key: showStatusOnStatusbar
|
||||
|
||||
We can show the status of synchronisation on the status bar. (Default: On)
|
||||
|
||||
#### Show status icon instead of file warnings banner
|
||||
|
||||
Setting key: hideFileWarningNotice
|
||||
|
||||
If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown.
|
||||
|
||||
#### Network warning style
|
||||
|
||||
Setting key: networkWarningStyle
|
||||
|
||||
How to display network errors when the sync server is unreachable.
|
||||
|
||||
### 2. Logging
|
||||
|
||||
#### Show only notifications
|
||||
@@ -138,11 +171,21 @@ Show verbose log. Please enable when you report the logs
|
||||
|
||||
### 1. Remote Server
|
||||
|
||||
Self-hosted LiveSync supports multiple remote connection profiles under **Remote Server** -> **Remote Databases**. This allows you to save and switch between multiple databases or bucket configurations in a single vault.
|
||||
|
||||
Each profile has an opaque identifier and a presentation name. The name does not need to be unique and is not used to select the profile. The main remote and the P2P remote are selected independently, so code and settings imports must preserve both selections rather than relying on a special identifier such as `default`.
|
||||
|
||||
- **➕ Add new connection**: Create a new connection profile by launching the setup dialogue.
|
||||
- **📥 Import connection**: Paste a connection string (e.g., `sls+https://...`, `sls+s3://...`, `sls+p2p://...`) to import a remote configuration profile.
|
||||
- **🔧 Configure**: Open the setup dialogue to edit settings for the selected connection profile.
|
||||
- **✅ Activate**: Select and activate this profile as the current active remote.
|
||||
- **🗑️ Delete**: Remove this connection profile from the list.
|
||||
|
||||
#### Remote Type
|
||||
|
||||
Setting key: remoteType
|
||||
|
||||
Remote server type
|
||||
The active remote server type. This is automatically projected to the legacy configuration when you activate a connection profile.
|
||||
|
||||
### 2. Notification
|
||||
|
||||
@@ -150,7 +193,7 @@ Remote server type
|
||||
|
||||
Setting key: notifyThresholdOfRemoteStorageSize
|
||||
|
||||
MB (0 to disable). We can get a notification when the estimated remote storage size exceeds this value.
|
||||
MB (0 to disable). At startup, Self-hosted LiveSync shows a long-lived, clickable notice when this value has not been configured or the estimated remote storage size exceeds it. Select **Review options** to open the detailed, untimed dialogue. Running the remote-size check explicitly opens that dialogue directly.
|
||||
|
||||
### 3. Privacy & Encryption
|
||||
|
||||
@@ -172,11 +215,20 @@ Setting key: usePathObfuscation
|
||||
|
||||
In default, the path of the file is not obfuscated to improve the performance. If you enable this, the path of the file will be obfuscated. This is useful when you want to hide the path of the file.
|
||||
|
||||
#### Use dynamic iteration count (Experimental)
|
||||
#### Encryption Algorithm
|
||||
|
||||
Setting key: E2EEAlgorithm
|
||||
|
||||
The encryption algorithm version used for end-to-end encryption.
|
||||
|
||||
- `v2` (V2: AES-256-GCM With HKDF): Recommended and default version.
|
||||
- `forceV1` or `""` (V1: Legacy): Older legacy encryption. Only use this if you have an existing vault encrypted in the legacy format.
|
||||
|
||||
#### Use dynamic iteration count (legacy V1 compatibility)
|
||||
|
||||
Setting key: useDynamicIterationCount
|
||||
|
||||
This is an experimental feature and not recommended. If you enable this, the iteration count of the encryption will be dynamically determined. This is useful when you want to improve the performance.
|
||||
This setting applies only to legacy V1 encryption data. Keep the saved value when opening an existing V1 database. New Vaults use E2EE V2 and do not use this setting.
|
||||
|
||||
---
|
||||
|
||||
@@ -192,30 +244,62 @@ Fetch necessary settings from already configured remote server.
|
||||
|
||||
### 5. Minio,S3,R2
|
||||
|
||||
These settings are configured within the S3/MinIO/R2 Setup dialogue when adding (`➕`) or editing (`🔧`) an Object Storage connection profile.
|
||||
|
||||
#### Endpoint URL
|
||||
|
||||
Setting key: endpoint
|
||||
|
||||
The URL of the remote storage endpoint.
|
||||
Note: Only Secure (HTTPS) connections can be used on Obsidian Mobile.
|
||||
|
||||
#### Access Key
|
||||
|
||||
Setting key: accessKey
|
||||
|
||||
The Access Key ID used for authentication.
|
||||
|
||||
#### Secret Key
|
||||
|
||||
Setting key: secretKey
|
||||
|
||||
The Secret Access Key used for authentication.
|
||||
|
||||
#### Region
|
||||
|
||||
Setting key: region
|
||||
|
||||
The storage region (e.g., `us-east-1`, or `auto` for Cloudflare R2).
|
||||
|
||||
#### Bucket Name
|
||||
|
||||
Setting key: bucket
|
||||
|
||||
The name of the bucket to store synchronised files.
|
||||
|
||||
#### Use Custom HTTP Handler
|
||||
|
||||
Setting key: useCustomRequestHandler
|
||||
Enable this if your Object Storage doesn't support CORS
|
||||
|
||||
This option is labeled **Use internal API** in the setup dialogue. Enable this if your Object Storage does not support CORS. It uses Obsidian's internal API to communicate with the S3 server, which is not compliant with web standards but can bypass CORS restrictions. Note that this might break in future Obsidian versions.
|
||||
|
||||
#### File prefix on the bucket
|
||||
|
||||
Setting key: bucketPrefix
|
||||
|
||||
This option is labeled **Folder Prefix** in the setup dialogue. Effectively a directory. Should end with `/`. e.g., `vault-name/`. Leave blank to store data at the root of the bucket.
|
||||
|
||||
#### Enable forcePathStyle
|
||||
|
||||
Setting key: forcePathStyle
|
||||
|
||||
This option is labeled **Use Path-Style Access** in the setup dialogue. If enabled, the forcePathStyle option will be used for bucket operations.
|
||||
|
||||
#### Custom Headers
|
||||
|
||||
Setting key: bucketCustomHeaders
|
||||
|
||||
Custom HTTP headers to include in every request sent to the Object Storage bucket. Specify them in the format `Header-Name: Value`, with each header on a new line.
|
||||
|
||||
#### Test Connection
|
||||
|
||||
@@ -223,24 +307,82 @@ Enable this if your Object Storage doesn't support CORS
|
||||
|
||||
### 6. CouchDB
|
||||
|
||||
These settings are configured within the CouchDB Setup dialogue when adding (`➕`) or editing (`🔧`) a CouchDB connection profile.
|
||||
|
||||
#### Server URI
|
||||
|
||||
Setting key: couchDB_URI
|
||||
|
||||
The URI of the CouchDB server.
|
||||
Note: Only Secure (HTTPS) connections can be used on Obsidian Mobile. The URI must not end with a trailing slash.
|
||||
|
||||
#### Username
|
||||
|
||||
Setting key: couchDB_USER
|
||||
username
|
||||
|
||||
The username used to authenticate with CouchDB.
|
||||
|
||||
#### Password
|
||||
|
||||
Setting key: couchDB_PASSWORD
|
||||
password
|
||||
|
||||
The password used to authenticate with CouchDB.
|
||||
|
||||
#### Database Name
|
||||
|
||||
Setting key: couchDB_DBNAME
|
||||
|
||||
The name of the database.
|
||||
Note: The database name cannot contain capital letters, spaces, or special characters other than `_$()+/-`, and cannot start with an underscore (`_`).
|
||||
|
||||
#### Use Request API to avoid inevitable CORS problem
|
||||
|
||||
Setting key: useRequestAPI
|
||||
|
||||
This option is labeled **Use Internal API** in the setup dialogue. If enabled, Obsidian's internal request API will be used to bypass CORS restrictions. This is a workaround that may not be compliant with web standards and is less secure. Note that this might break in future Obsidian versions.
|
||||
|
||||
#### Custom Headers
|
||||
|
||||
Setting key: couchDB_CustomHeaders
|
||||
|
||||
Custom HTTP headers to include in every request sent to the CouchDB server. Specify them in the format `Header-Name: Value`, with each header on a new line.
|
||||
|
||||
#### Use JWT Authentication
|
||||
|
||||
Setting key: useJWT
|
||||
|
||||
Enable JSON Web Token (JWT) authentication for CouchDB. This is an experimental feature and has not been thoroughly verified.
|
||||
|
||||
#### JWT Algorithm
|
||||
|
||||
Setting key: jwtAlgorithm
|
||||
|
||||
The algorithm used to sign the JWT. Supported algorithms: `HS256`, `HS512`, `ES256`, `ES512`.
|
||||
|
||||
#### JWT Expiration Duration (minutes)
|
||||
|
||||
Setting key: jwtExpDuration
|
||||
|
||||
Token expiration duration in minutes. Set to 0 to disable expiration.
|
||||
|
||||
#### JWT Key
|
||||
|
||||
Setting key: jwtKey
|
||||
|
||||
The secret key (for HS256/HS512) or the PKCS#8 PEM-formatted private key (for ES256/ES512) used to sign the JWT.
|
||||
|
||||
#### JWT Key ID (kid)
|
||||
|
||||
Setting key: jwtKid
|
||||
|
||||
The Key ID (`kid`) header parameter included in the JWT.
|
||||
|
||||
#### JWT Subject (sub)
|
||||
|
||||
Setting key: jwtSub
|
||||
|
||||
The subject (`sub`) claim of the JWT, which should match your CouchDB username.
|
||||
|
||||
#### Test Database Connection
|
||||
|
||||
Open database connection. If the remote database is not found and you have permission to create a database, the database will be created.
|
||||
@@ -251,26 +393,105 @@ Checks and fixes any potential issues with the database config.
|
||||
|
||||
#### Apply Settings
|
||||
|
||||
### 7. Peer-to-Peer (P2P) Synchronisation
|
||||
|
||||
#### Enable P2P Synchronisation
|
||||
|
||||
Setting key: P2P_Enabled
|
||||
|
||||
Enable direct peer-to-peer synchronisation via WebRTC.
|
||||
|
||||
#### Relay URL
|
||||
|
||||
Setting key: P2P_relays
|
||||
|
||||
The WebSocket relay server URL(s) used for coordinating P2P connections via WebRTC. Multiple URLs can be separated by commas.
|
||||
|
||||
#### Group ID
|
||||
|
||||
Setting key: P2P_roomID
|
||||
|
||||
The room ID or Group ID used to identify your group of synchronising devices. All devices you wish to synchronise must use the same Group ID. You can enter any custom string or generate a random Group ID.
|
||||
|
||||
#### Passphrase
|
||||
|
||||
Setting key: P2P_passphrase
|
||||
|
||||
The password or passphrase used to authenticate and encrypt P2P communication. All devices must use the same passphrase.
|
||||
|
||||
#### Device Peer ID
|
||||
|
||||
Setting key: P2P_DevicePeerName
|
||||
|
||||
The peer name or identifier of this device in the P2P network. This should be unique within your group of devices.
|
||||
|
||||
#### Automatically start P2P connection on launch
|
||||
|
||||
Setting key: P2P_AutoStart
|
||||
|
||||
This option is labeled **Auto Start P2P Connection** in the setup dialogue. If enabled, the P2P connection will start automatically when the plug-in launches.
|
||||
|
||||
#### Connect and disconnect
|
||||
|
||||
Closing a P2P connection leaves the LiveSync P2P room, stops its replication service, closes the signalling relay sockets, and pauses their automatic reconnection. An idle WebRTC connection may remain temporarily under the transport's ownership so that it can be reused, but it cannot carry traffic for the room which has been left. Connecting again resumes relay reconnection and joins a new LiveSync room.
|
||||
|
||||
#### Automatically broadcast changes to connected peers
|
||||
|
||||
Setting key: P2P_AutoBroadcast
|
||||
|
||||
This option is labeled **Auto Broadcast Changes** in the setup dialogue. If enabled, changes will be automatically broadcasted to connected peers, requesting them to fetch the changes.
|
||||
|
||||
#### TURN Server URLs (comma-separated)
|
||||
|
||||
Setting key: P2P_turnServers
|
||||
|
||||
A comma-separated list of TURN/STUN server URLs. Used to relay P2P connections when direct WebRTC connection fails due to strict NAT or firewalls. In most cases, these can be left blank.
|
||||
|
||||
#### TURN Username
|
||||
|
||||
Setting key: P2P_turnUsername
|
||||
|
||||
The username for authentication with the TURN server.
|
||||
|
||||
#### TURN Credential
|
||||
|
||||
Setting key: P2P_turnCredential
|
||||
|
||||
The password or credential for authentication with the TURN server.
|
||||
|
||||
## 4. Sync Settings
|
||||
|
||||
### 1. Synchronization Preset
|
||||
### 1. Synchronisation Preset
|
||||
|
||||
#### Presets
|
||||
|
||||
Setting key: preset
|
||||
Apply preset configuration
|
||||
|
||||
### 2. Synchronization Method
|
||||
### 2. Synchronisation Method
|
||||
|
||||
#### Sync Mode
|
||||
|
||||
Setting key: syncMode
|
||||
|
||||
The trigger mechanism for synchronisation.
|
||||
|
||||
- **LiveSync** (`LIVESYNC`): Real-time, continuous, bidirectional synchronisation.
|
||||
Note: This requires a CouchDB or WebRTC P2P remote server. It is not supported for S3-compatible Object Storage.
|
||||
- **Periodic Sync** (`PERIODIC`): Synchronisation is performed at regular intervals specified by the **Periodic Sync interval** setting.
|
||||
- **On Events** (`ONEVENTS`): Synchronisation is triggered by specific events (such as save, file open, or startup) configured via the toggles below.
|
||||
|
||||
#### Periodic Sync interval
|
||||
|
||||
Setting key: periodicReplicationInterval
|
||||
Interval (sec)
|
||||
|
||||
#### Minimum interval for syncing
|
||||
|
||||
Setting key: syncMinimumInterval
|
||||
|
||||
The minimum interval for automatic synchronisation on event.
|
||||
|
||||
#### Sync on Save
|
||||
|
||||
Setting key: syncOnSave
|
||||
@@ -296,6 +517,13 @@ Automatically Sync all files when opening Obsidian.
|
||||
Setting key: syncAfterMerge
|
||||
Sync automatically after merging files
|
||||
|
||||
#### Keep replication active in the background
|
||||
|
||||
Setting key: keepReplicationActiveInBackground
|
||||
Desktop only; uses more battery and network. This setting applies to continuous and periodic replication.
|
||||
|
||||
Finite remote operations, including one-shot replication, P2P peer discovery and selection, rebuilds, fetches, and remote chunk fetching, request best-effort screen-awake protection automatically and do not require this setting. That protection does not guarantee execution while Obsidian is hidden or while the operating system suspends the device.
|
||||
|
||||
### 3. Update thinning
|
||||
|
||||
#### Batch database update
|
||||
@@ -315,22 +543,24 @@ Saving will be performed forcefully after this number of seconds.
|
||||
|
||||
### 4. Deletion Propagation (Advanced)
|
||||
|
||||
#### Use the trash bin
|
||||
#### Legacy trash setting
|
||||
|
||||
Setting key: trashInsteadDelete
|
||||
Move remotely deleted files to the trash, instead of deleting.
|
||||
This key remains accepted for settings imports, Setup URIs, and compatibility with earlier versions, but it is no longer shown in the settings interface. Remote file deletion follows the user's Obsidian deletion preferences through the `FileManager.trashFile` API, regardless of this legacy value.
|
||||
|
||||
#### Keep empty folder
|
||||
|
||||
Setting key: doNotDeleteFolder
|
||||
Should we keep folders that don't have any files inside?
|
||||
Should we keep folders that do not have any files inside?
|
||||
|
||||
### 5. Conflict resolution (Advanced)
|
||||
|
||||
Conflict resolution preserves unknown local content and automatically merges only when the available revision history supplies a safe shared base. See [Conflict resolution and revision provenance](specs_conflict_resolution.md) for the revision-tree rules, stale and concurrent resolutions, binary-file limitation, and the device-local provenance used for operations while a conflict is live.
|
||||
|
||||
#### (BETA) Always overwrite with a newer file
|
||||
|
||||
Setting key: resolveConflictsByNewerFile
|
||||
Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.
|
||||
Testing only. Resolve file conflicts by selecting the copy with the newer modification time. This can overwrite modified files and cannot establish which revision reflects the user's intent.
|
||||
|
||||
#### Delay conflict resolution of inactive files
|
||||
|
||||
@@ -360,10 +590,13 @@ Setting key: notifyAllSettingSyncFile
|
||||
|
||||
### 7. Hidden Files (Advanced)
|
||||
|
||||
#### Hidden file synchronization
|
||||
See the [Hidden File Sync guide](./tips/hidden-file-sync.md) before enabling this feature. Rebuild and Fetch setup operations deliberately leave optional features disabled; establish ordinary note synchronisation first, then initialise Hidden File Sync independently on each device.
|
||||
|
||||
#### Enable Hidden files sync
|
||||
|
||||
Setting key: syncInternalFiles
|
||||
Enable the synchronisation of hidden files and folders (e.g. settings files, templates, snippets, and themes under `.obsidian`).
|
||||
|
||||
#### Scan for hidden files before replication
|
||||
|
||||
Setting key: syncInternalFilesBeforeReplication
|
||||
@@ -373,6 +606,12 @@ Setting key: syncInternalFilesBeforeReplication
|
||||
Setting key: syncInternalFilesInterval
|
||||
Seconds, 0 to disable
|
||||
|
||||
#### Suppress notification of hidden files change
|
||||
|
||||
Setting key: suppressNotifyHiddenFilesChange
|
||||
|
||||
If enabled, the notification of hidden files change will be suppressed.
|
||||
|
||||
## 5. Selector (Advanced)
|
||||
|
||||
### 1. Normal Files
|
||||
@@ -400,48 +639,52 @@ If this is set, changes to local files which are matched by the ignore files wil
|
||||
Setting key: ignoreFiles
|
||||
Comma separated `.gitignore, .dockerignore`
|
||||
|
||||
When a saved setting changes whether a normal file can be reflected, LiveSync re-evaluates the normal-file metadata already held in the local database. This covers selector expressions, ignore-file settings, maximum-size and modification-time limits, and file-name case handling. A remote revision which was received and checkpointed while excluded can therefore be reflected after the criteria are broadened, without rewinding the remote checkpoint. Narrowing the criteria does not delete files which have already been reflected.
|
||||
|
||||
### 2. Hidden Files (Advanced)
|
||||
|
||||
#### Ignore patterns
|
||||
|
||||
#### Add default patterns
|
||||
|
||||
## 6. Customization sync (Advanced)
|
||||
## 6. Customisation sync (Advanced)
|
||||
|
||||
### 1. Customization Sync
|
||||
Customisation Sync is a supported, advanced opt-in feature. Its current per-file implementation is covered by a two-Vault real-Obsidian workflow for snippets, configuration files, and plug-in files. Hidden File Sync is a separate feature with different setup, selection, and conflict behaviour; do not use both features to manage the same files.
|
||||
|
||||
### 1. Customisation Sync
|
||||
|
||||
#### Device name
|
||||
|
||||
Setting key: deviceAndVaultName
|
||||
Unique name between all synchronized devices. To edit this setting, please disable customization sync once.
|
||||
Unique name between all synchronised devices. To edit this setting, please disable customisation sync once.
|
||||
|
||||
#### Per-file-saved customization sync
|
||||
#### Per-file-saved customisation sync
|
||||
|
||||
Setting key: usePluginSyncV2
|
||||
If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.
|
||||
If enabled, per-file efficient customisation sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enable this, we lose compatibility with old versions.
|
||||
|
||||
#### Enable customization sync
|
||||
#### Enable customisation sync
|
||||
|
||||
Setting key: usePluginSync
|
||||
|
||||
#### Scan customization automatically
|
||||
#### Scan customisation automatically
|
||||
|
||||
Setting key: autoSweepPlugins
|
||||
Scan customization before replicating.
|
||||
Scan customisation before replicating.
|
||||
|
||||
#### Scan customization periodically
|
||||
#### Scan customisation periodically
|
||||
|
||||
Setting key: autoSweepPluginsPeriodic
|
||||
Scan customization every 1 minute.
|
||||
Scan customisation every 1 minute.
|
||||
|
||||
#### Notify customized
|
||||
#### Notify customised
|
||||
|
||||
Setting key: notifyPluginOrSettingUpdated
|
||||
Notify when other device has newly customized.
|
||||
Notify when another device has newly customised.
|
||||
|
||||
#### Open
|
||||
|
||||
Open the dialog
|
||||
Open the dialogue
|
||||
|
||||
## 7. Hatch
|
||||
|
||||
@@ -456,14 +699,18 @@ Warning! This will have a serious impact on performance. And the logs will not b
|
||||
|
||||
### 2. Scram Switches
|
||||
|
||||
Emergency controls to suspend synchronisation processes in order to prevent database corruption. If a critical mismatch or sync error occurs, the plug-in may automatically enter a Scram state and suspend operations.
|
||||
|
||||
#### Suspend file watching
|
||||
|
||||
Setting key: suspendFileWatching
|
||||
Stop watching for file changes.
|
||||
|
||||
Stop watching for local file changes.
|
||||
|
||||
#### Suspend database reflecting
|
||||
|
||||
Setting key: suspendParseReplicationResult
|
||||
|
||||
Stop reflecting database changes to storage files.
|
||||
|
||||
### 3. Recovery and Repair
|
||||
@@ -486,7 +733,7 @@ Compare the content of files between on local database and storage. If not match
|
||||
|
||||
#### Back to non-configured
|
||||
|
||||
#### Delete all customization sync data
|
||||
#### Delete all customisation sync data
|
||||
|
||||
## 8. Advanced (Advanced)
|
||||
|
||||
@@ -507,6 +754,12 @@ Setting key: hashCacheMaxAmount
|
||||
|
||||
Setting key: customChunkSize
|
||||
|
||||
#### Chunk Splitter
|
||||
|
||||
Setting key: chunkSplitterVersion
|
||||
|
||||
Select the chunk splitter version; V3 is the most efficient. If you experience issues, please choose Default or Legacy.
|
||||
|
||||
#### Use splitting-limit-capped chunk splitter
|
||||
|
||||
Setting key: enableChunkSplitterV2
|
||||
@@ -532,14 +785,20 @@ Setting key: concurrencyOfReadChunksOnline
|
||||
|
||||
Setting key: minimumIntervalOfReadChunksOnline
|
||||
|
||||
#### Maximum request size for manually resending chunks
|
||||
|
||||
Setting key: sendChunksBulkMaxSize
|
||||
|
||||
Limit the maximum size of chunks sent in one request by the explicit **Resend all chunks** maintenance operation (MB). Ordinary and initial replication do not use this setting.
|
||||
|
||||
## 9. Power users (Power User)
|
||||
|
||||
### 1. Remote Database Tweak
|
||||
|
||||
#### Incubate Chunks in Document (Beta)
|
||||
#### Incubate Chunks in Document (sunset compatibility)
|
||||
|
||||
Setting key: useEden
|
||||
If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.
|
||||
This setting is no longer offered for new configuration. Existing saved values remain accepted so that established databases can be opened and migrated without silently changing their structure.
|
||||
|
||||
#### Maximum Incubating Chunks
|
||||
|
||||
@@ -556,10 +815,14 @@ The maximum total size of chunks that can be incubated within the document. Chun
|
||||
Setting key: maxAgeInEden
|
||||
The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.
|
||||
|
||||
#### Data Compression (Experimental)
|
||||
#### Data Compression (advanced opt-in)
|
||||
|
||||
Setting key: enableCompression
|
||||
|
||||
Data Compression applies fflate level 8 to each chunk before E2EE. A chunk is left uncompressed when compression would not make it smaller, and readers continue to accept both representations. Changing the setting does not require a rebuild for compatibility; existing and new representations can coexist.
|
||||
|
||||
The setting remains disabled by default because the measured storage and transfer benefit comes with workload-dependent processing and memory costs. See the [Data Compression specification](specs_data_compression.md) for the contract, evidence, execution model, and reproduction command.
|
||||
|
||||
### 2. CouchDB Connection Tweak
|
||||
|
||||
#### Batch size
|
||||
@@ -627,19 +890,23 @@ Enable this option to automatically apply the most recent change to documents ev
|
||||
Setting key: useIndexedDBAdapter
|
||||
Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.
|
||||
|
||||
#### Compute revisions for chunks (Previous behaviour)
|
||||
#### Content-derived chunk revisions (obsolete setting)
|
||||
|
||||
Setting key: doNotUseFixedRevisionForChunks
|
||||
If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)
|
||||
Chunk revisions are always derived from their content. This key remains accepted in stored settings and Setup URIs for compatibility, but its value no longer changes behaviour and it is not a maintenance prerequisite.
|
||||
|
||||
#### Handle files as Case-Sensitive
|
||||
|
||||
Setting key: handleFilenameCaseSensitive
|
||||
If this enabled, All files are handled as case-Sensitive (Previous behaviour).
|
||||
|
||||
When this setting is disabled, changing only the letter case of a file name within the same directory is synchronised as a rename. Changing the letter case of a directory name is not supported by this handling.
|
||||
|
||||
New Vaults use case-insensitive handling for cross-platform compatibility. Existing settings with an explicit value preserve that choice. Earlier releases also followed the case-insensitive branch when this value was absent, so 1.0 saves a missing legacy value as `false` without requiring a compatibility review or database rebuild.
|
||||
|
||||
### 4. Compatibility (Internal API Usage)
|
||||
|
||||
#### Scan changes on customization sync
|
||||
#### Scan changes on customisation sync
|
||||
|
||||
Setting key: watchInternalFileChanges
|
||||
Do not use internal API
|
||||
@@ -651,10 +918,12 @@ Do not use internal API
|
||||
Setting key: additionalSuffixOfDatabaseName
|
||||
LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.
|
||||
|
||||
#### The Hash algorithm for chunk IDs (Experimental)
|
||||
#### The Hash algorithm for chunk IDs (compatibility)
|
||||
|
||||
Setting key: hashAlg
|
||||
|
||||
`xxhash64` is the supported current value. Older algorithms remain selectable only as an edge-case compatibility path for existing databases. Changing the algorithm can reduce chunk reuse between devices and requires the normal tweak review.
|
||||
|
||||
### 6. Edge case addressing (Behaviour)
|
||||
|
||||
#### Fetch database with previous behaviour
|
||||
@@ -664,7 +933,13 @@ Setting key: doNotSuspendOnFetching
|
||||
#### Keep empty folder
|
||||
|
||||
Setting key: doNotDeleteFolder
|
||||
Should we keep folders that don't have any files inside?
|
||||
Should we keep folders that do not have any files inside?
|
||||
|
||||
#### Process files even if seems to be corrupted
|
||||
|
||||
Setting key: processSizeMismatchedFiles
|
||||
|
||||
Enable this setting to process files with size mismatches, which can sometimes be created by certain external APIs or integrations.
|
||||
|
||||
### 7. Edge case addressing (Processing)
|
||||
|
||||
@@ -684,23 +959,31 @@ If enabled, the file under 1kb will be processed in the UI thread.
|
||||
|
||||
Setting key: disableCheckingConfigMismatch
|
||||
|
||||
### 9. Remediation
|
||||
|
||||
#### Maximum file modification time for reflected file events
|
||||
|
||||
Setting key: maxMTimeForReflectEvents
|
||||
|
||||
Files with modification times greater than this value (in seconds since the Unix epoch) will not have their events reflected. Set to 0 to disable this limit.
|
||||
|
||||
## 11. Maintenance
|
||||
|
||||
### 1. Scram!
|
||||
|
||||
#### Lock Server
|
||||
|
||||
Lock the remote server to prevent synchronization with other devices.
|
||||
Lock the remote server to prevent synchronisation with other devices.
|
||||
|
||||
#### Emergency restart
|
||||
|
||||
Disables all synchronization and restart.
|
||||
Disables all synchronisation and restart.
|
||||
|
||||
### 2. Syncing
|
||||
|
||||
#### Resend
|
||||
|
||||
Resend all chunks to the remote.
|
||||
Explicitly resend all locally available chunks to the remote. This is a recovery and maintenance operation; ordinary replication does not pre-send every chunk.
|
||||
|
||||
#### Reset journal received history
|
||||
|
||||
@@ -712,17 +995,13 @@ Initialise journal sent history. On the next sync, every item except this device
|
||||
|
||||
### 3. Rebuilding Operations (Local)
|
||||
|
||||
#### Fetch from remote
|
||||
#### Reset Synchronisation on This Device
|
||||
|
||||
Restore or reconstruct local database from remote.
|
||||
|
||||
#### Fetch rebuilt DB (Save local documents before)
|
||||
|
||||
Restore or reconstruct local database from remote database but use local chunks.
|
||||
|
||||
### 4. Total Overhaul
|
||||
|
||||
#### Rebuild everything
|
||||
#### Overwrite Server Data with This Device's Files
|
||||
|
||||
Rebuild local and remote database with local files.
|
||||
|
||||
@@ -748,12 +1027,6 @@ Purge all download/upload cache.
|
||||
|
||||
Delete all data on the remote server.
|
||||
|
||||
### 6. Deprecated
|
||||
|
||||
#### Run database cleanup
|
||||
|
||||
Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Rebuild everything' under Total Overhaul.
|
||||
|
||||
### 7. Reset
|
||||
|
||||
#### Delete local database to reset or uninstall Self-hosted LiveSync
|
||||
|
||||
@@ -3,23 +3,133 @@
|
||||
# このプラグインの設定項目
|
||||
|
||||
## Remote Database Configurations
|
||||
同期先のデータベース設定を行います。何らかの同期が有効になっている場合は編集できないため、同期を解除してから行ってください。
|
||||
同期先のデータベース設定(Remote Server)を行います。
|
||||
|
||||
### URI
|
||||
CouchDBのURIを入力します。Cloudantの場合は「External Endpoint(preferred)」になります。
|
||||
**スラッシュで終わってはいけません。**
|
||||
こちらにデータベース名を含めてもかまいません。
|
||||
現在のバージョンでは、複数のリモート接続設定(接続プロファイル)を登録・管理し、切り替えて使用することが可能です(「Remote Databases」リスト)。
|
||||
|
||||
### Username
|
||||
ユーザー名を入力します。このユーザーは管理者権限があることが望ましいです。
|
||||
- **➕ 新規接続を追加 (Add new connection)**: 新しい接続設定を作成し、各セットアップダイアログを起動します。
|
||||
- **📥 接続をインポート (Import connection)**: 接続文字列(`sls+https://...`、`sls+s3://...`、`sls+p2p://...`など)を貼り付けてインポートします。
|
||||
- **🔧 設定 (Configure)**: セットアップダイアログを開き、選択した接続プロファイルの設定を編集します。
|
||||
- **✅ 有効化 (Activate)**: 選択したプロファイルをアクティブな同期先として有効化します。
|
||||
- **🗑️ 削除 (Delete)**: 接続プロファイルを一覧から削除します。
|
||||
|
||||
### Password
|
||||
パスワードを入力します。
|
||||
これらの接続プロファイルを追加・編集する際、選択したデータベースの種類(CouchDB、S3互換オブジェクトストレージ、P2Pなど)に応じたセットアップダイアログが開きます。
|
||||
|
||||
### Database Name
|
||||
同期するデータベース名を入力します。
|
||||
⚠️存在しない場合は、テストや接続を行った際、自動的に作成されます[^1]。
|
||||
[^1]:権限がない場合は自動作成には失敗します。
|
||||
何らかの同期が有効になっている場合は編集できないため、同期を解除してから行ってください。
|
||||
|
||||
### CouchDB の設定
|
||||
CouchDBの各設定項目は、接続プロファイルを追加 (➕) または設定 (🔧) する際に開く **CouchDB セットアップダイアログ** 内で設定します。
|
||||
|
||||
#### URI
|
||||
設定キー: couchDB_URI
|
||||
|
||||
CouchDBの接続先URIです。ダイアログ内では **URL** と表記されます。Cloudantの場合は「External Endpoint (preferred)」になります。
|
||||
注意: Obsidian Mobileではセキュア接続 (HTTPS) のみが使用可能です。また、末尾にスラッシュ(`/`)を付けてはいけません。
|
||||
|
||||
#### Username
|
||||
設定キー: couchDB_USER
|
||||
|
||||
CouchDBのログインユーザー名です。ダイアログ内では **Username** と表記されます。このユーザーには管理者権限があることが望ましいです。
|
||||
|
||||
#### Password
|
||||
設定キー: couchDB_PASSWORD
|
||||
|
||||
CouchDBのログインパスワードです。ダイアログ内では **Password** と表記されます。
|
||||
|
||||
#### Database Name
|
||||
設定キー: couchDB_DBNAME
|
||||
|
||||
同期先のデータベース名です。ダイアログ内では **Database Name** と表記されます。
|
||||
注意: データベース名には大文字、スペース、および一部の特殊文字(`_$()+/-` 以外)は使用できません。また、アンダースコア(`_`)から始めることはできません。存在しない場合は、接続テスト時または設定適用時に自動作成されます(作成権限が必要です)。
|
||||
|
||||
#### CORS回避のためにRequest APIを使用する
|
||||
設定キー: useRequestAPI
|
||||
|
||||
この項目はセットアップダイアログ内では **Use Internal API** と表記されます。有効な場合、不可避なCORS問題を回避するためにObsidianの内部Request APIを使用します。これはWeb標準に準拠していない回避策であり、すべての環境での動作を保証するものではありません。安全性が低下する可能性がある点にご注意ください。将来のObsidianのアップデートによって動作しなくなる可能性があります。
|
||||
|
||||
#### カスタムヘッダー
|
||||
設定キー: couchDB_CustomHeaders
|
||||
|
||||
CouchDBサーバーに送信するすべてのリクエストに含めるカスタムHTTPヘッダーを設定します。ダイアログ内では **Custom Headers** と表記されます。`ヘッダー名: 値` の形式で、1行に1つずつ入力してください。
|
||||
|
||||
#### JWT認証の使用 (実験的機能)
|
||||
設定キー: useJWT
|
||||
|
||||
CouchDBでのJSON Web Token (JWT) 認証を有効にします。ダイアログ内では **Use JWT Authentication** と表記されます。十分に検証されていない実験的機能であるため、ご注意ください。
|
||||
|
||||
#### JWTアルゴリズム
|
||||
設定キー: jwtAlgorithm
|
||||
|
||||
JWTの署名に使用するアルゴリズムを選択します。ダイアログ内では **JWT Algorithm** と表記されます。対応アルゴリズム: `HS256`, `HS512`, `ES256`, `ES512`
|
||||
|
||||
#### JWT有効期限 (分)
|
||||
設定キー: jwtExpDuration
|
||||
|
||||
トークンの有効期限を分単位で指定します。ダイアログ内では **JWT Expiration Duration (minutes)** と表記されます。`0` を指定すると有効期限は無効になります。
|
||||
|
||||
#### JWTキー
|
||||
設定キー: jwtKey
|
||||
|
||||
JWTの署名に使用する秘密鍵またはプライベートキーを指定します。ダイアログ内では **JWT Key** と表記されます。`HS256/HS512` の場合は共通鍵を、`ES256/ES512` の場合は pkcs8 PEM形式の秘密鍵を入力してください。
|
||||
|
||||
#### JWTキーID (kid)
|
||||
設定キー: jwtKid
|
||||
|
||||
JWTヘッダーに含めるキーIDを指定します。ダイアログ内では **JWT Key ID (kid)** と表記されます。
|
||||
|
||||
#### JWTサブジェクト (sub)
|
||||
設定キー: jwtSub
|
||||
|
||||
JWTのサブジェクト (CouchDBユーザー名) を指定します。ダイアログ内では **JWT Subject (sub)** と表記されます。
|
||||
|
||||
### Object Storage (Minio, S3, R2) の設定
|
||||
Object Storageの各設定項目は、接続プロファイルを追加 (➕) または設定 (🔧) する際に開く **S3/MinIO/R2 セットアップダイアログ** 内で設定します。
|
||||
|
||||
#### エンドポイントURL
|
||||
設定キー: endpoint
|
||||
|
||||
S3互換ストレージのエンドポイントURLです。ダイアログ内では **Endpoint URL** と表記されます。
|
||||
注意: Obsidian Mobileではセキュア接続 (HTTPS) のみが使用可能です。
|
||||
|
||||
#### アクセスキー ID
|
||||
設定キー: accessKey
|
||||
|
||||
認証に使用するアクセスキーIDです。ダイアログ内では **Access Key ID** と表記されます。
|
||||
|
||||
#### シークレットアクセスキー
|
||||
設定キー: secretKey
|
||||
|
||||
認証に使用するシークレットアクセスキーです。ダイアログ内では **Secret Access Key** と表記されます。
|
||||
|
||||
#### リージョン
|
||||
設定キー: region
|
||||
|
||||
ストレージのリージョンを指定します(例: `us-east-1`、Cloudflare R2の場合は `auto`)。ダイアログ内では **Region** と表記されます。
|
||||
|
||||
#### バケット名
|
||||
設定キー: bucket
|
||||
|
||||
同期データを保存するバケット名です。ダイアログ内では **Bucket Name** と表記されます。
|
||||
|
||||
#### カスタムHTTPハンドラーを使用する
|
||||
設定キー: useCustomRequestHandler
|
||||
|
||||
この項目はセットアップダイアログ内では **Use internal API** と表記されます。オブジェクトストレージがCORSをサポートしていない場合に有効にします。Obsidianの内部APIを使用してS3サーバーと通信することでCORS制約を回避します。Web標準には準拠していないため、将来のObsidianのアップデートによって動作しなくなる可能性があります。
|
||||
|
||||
#### バケット内のファイルプレフィックス
|
||||
設定キー: bucketPrefix
|
||||
|
||||
この項目はセットアップダイアログ内では **Folder Prefix** と表記されます。実質的なディレクトリ指定です。末尾は `/` である必要があります(例:`vault-name/`)。バケットのルートに保存する場合は空欄のままにしてください。
|
||||
|
||||
#### forcePathStyleを有効にする
|
||||
設定キー: forcePathStyle
|
||||
|
||||
この項目はセットアップダイアログ内では **Use Path-Style Access** と表記されます。有効な場合、バケット操作でforcePathStyleオプションを使用します。
|
||||
|
||||
#### カスタムヘッダー
|
||||
設定キー: bucketCustomHeaders
|
||||
|
||||
オブジェクトストレージバケットに送信するすべてのリクエストに含めるカスタムHTTPヘッダーを設定します。ダイアログ内では **Custom Headers** と表記されます。`ヘッダー名: 値` の形式で、1行に1つずつ入力してください。
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +140,18 @@ CouchDBのURIを入力します。Cloudantの場合は「External Endpoint(prefe
|
||||
### Passphrase
|
||||
暗号化を行う際に使用するパスフレーズです。充分に長いものを使用してください。
|
||||
|
||||
### パスの難読化
|
||||
設定キー: usePathObfuscation
|
||||
|
||||
ダイアログ内では **Obfuscate Properties** と表記されます。有効な場合、リモートサーバー上でのファイルパスやフォルダ名を難読化(暗号化)します。これによりプライバシーが向上しますが、パフォーマンスがわずかに低下する可能性があります。
|
||||
|
||||
### 暗号化アルゴリズム
|
||||
設定キー: E2EEAlgorithm
|
||||
|
||||
ダイアログ内では **Encryption Algorithm** と表記されます。エンドツーエンド暗号化に使用する暗号化アルゴリズムのバージョンを選択します。
|
||||
- `v2` (V2: AES-256-GCM With HKDF): 推奨されるデフォルトのバージョンです。
|
||||
- `forceV1` または `""` (V1: Legacy): レガシーな暗号化バージョンです。古いバージョンで暗号化された既存の保管庫(Vault)を同期する場合にのみ使用してください。
|
||||
|
||||
### Apply
|
||||
End to End 暗号化を行うに当たって、異なるパスフレーズで暗号化された同一の内容を入手されることは避けるべきです。また、Self-hosted LiveSyncはコンテンツのcrc32を重複回避に使用しているため、その点でも攻撃が有効になってしまいます。
|
||||
|
||||
@@ -53,12 +175,66 @@ End to End 暗号化を行うに当たって、異なるパスフレーズで暗
|
||||
どちらのオペレーションも、実行するとすべての同期設定が無効化されます。
|
||||
|
||||
|
||||
|
||||
|
||||
### Test Database connection
|
||||
上記の設定でデータベースに接続できるか確認します。
|
||||
|
||||
### Check database configuration
|
||||
ここから直接CouchDBの設定を確認・変更できます。
|
||||
|
||||
### Peer-to-Peer (P2P) 同期の設定
|
||||
|
||||
#### P2P同期を有効にする
|
||||
設定キー: P2P_Enabled
|
||||
|
||||
WebRTCを介したデバイス間での直接的なP2P同期を有効にします。ダイアログ内では **Enabled** と表記されます。
|
||||
|
||||
#### リレーサーバーのURL
|
||||
設定キー: P2P_relays
|
||||
|
||||
WebRTCによるP2P接続を仲介・調整するためのWebSocketリレーサーバーのURLを指定します。ダイアログ内では **Relay URL** と表記されます。複数のURLを指定する場合はカンマで区切ります。ダイアログ内のボタンをクリックすると、デフォルトのリレーサーバーを設定できます。
|
||||
|
||||
#### グループID
|
||||
設定キー: P2P_roomID
|
||||
|
||||
同期するデバイス群を識別するためのルームIDまたはグループIDを指定します。ダイアログ内では **Group ID** と表記されます。同期させたいすべてのデバイスで同じグループIDを指定する必要があります。任意のカスタム文字列を入力するか、ランダム生成ボタンで生成できます。
|
||||
|
||||
#### パスフレーズ
|
||||
設定キー: P2P_passphrase
|
||||
|
||||
P2P通信の認証および暗号化に使用するパスワード(パスフレーズ)を指定します。ダイアログ内では **Passphrase** と表記されます。同期するすべてのデバイスで同じパスフレーズを指定する必要があります。
|
||||
|
||||
#### デバイス名
|
||||
設定キー: P2P_DevicePeerName
|
||||
|
||||
P2Pネットワーク上でこのデバイスを識別するための名前を指定します。ダイアログ内では **Device Peer ID** と表記されます。グループ内のデバイス間で重複しない一意の値を設定してください。
|
||||
|
||||
#### 起動時のP2P自動接続開始
|
||||
設定キー: P2P_AutoStart
|
||||
|
||||
有効な場合、プラグインの起動時に自動的にP2P接続を開始します。ダイアログ内では **Auto Start P2P Connection** と表記されます。
|
||||
|
||||
#### 接続済みピアへの変更の自動ブロードキャスト
|
||||
設定キー: P2P_AutoBroadcast
|
||||
|
||||
有効な場合、ローカルでの変更が接続済みのピアに自動的にブロードキャストされます。ダイアログ内では **Auto Broadcast Changes** と表記されます。通知されたピアは変更の取得を開始します。
|
||||
|
||||
#### TURNサーバーのURL (カンマ区切り)
|
||||
設定キー: P2P_turnServers
|
||||
|
||||
ダイアログ内では **TURN Server URLs (comma-separated)** と表記されます。厳しいNATやファイアウォールがある環境で、WebRTCの直接接続が確立できない場合にP2P接続を中継するためのTURN/STUNサーバーのURLをカンマ区切りで指定します。通常は空欄のままで問題ありません。
|
||||
|
||||
#### TURNユーザー名
|
||||
設定キー: P2P_turnUsername
|
||||
|
||||
TURNサーバーでの認証に使用するユーザー名を設定します。ダイアログ内では **TURN Username** と表記されます。
|
||||
|
||||
#### TURNパスワード
|
||||
設定キー: P2P_turnCredential
|
||||
|
||||
TURNサーバーでの認証に使用するパスワード(クレデンシャル)を設定します。ダイアログ内では **TURN Credential** と表記されます。
|
||||
|
||||
## Local Database Configurations
|
||||
端末内に作成されるデータベースの設定です。
|
||||
|
||||
@@ -71,7 +247,8 @@ End to End 暗号化を行うに当たって、異なるパスフレーズで暗
|
||||
このオプションはLiveSyncと同時には使用できません。
|
||||
|
||||
### minimum chunk size と LongLine threshold
|
||||
チャンクの分割についての設定です。
|
||||
チャンクの分割についての設定です。※現在これらの項目はUIから直接設定することはできません(デフォルト値で自動処理されます)。
|
||||
|
||||
Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk size文字確保した上で、できるだけ効率的に同期できるよう、ノートを分割してチャンクを作成します。
|
||||
これは、同期を行う際に、一定の文字数で分割した場合、先頭の方を編集すると、その後の分割位置がすべてずれ、結果としてほぼまるごとのファイルのファイル送受信を行うことになっていた問題を避けるために実装されました。
|
||||
具体的には、先頭から順に直近の下記の箇所を検索し、一番長く切れたものを一つのチャンクとします。
|
||||
@@ -88,6 +265,11 @@ Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk s
|
||||
改行文字と#を除き、すべて●に置換しても、アルゴリズムは有効に働きます。
|
||||
デフォルトは20文字と、250文字です。
|
||||
|
||||
### チャンクスプリッター
|
||||
設定キー: chunkSplitterVersion
|
||||
|
||||
チャンク分割アルゴリズムを選択します。V3が最も効率的です。問題が発生した場合はDefaultまたはLegacyに設定してください。
|
||||
|
||||
## General Settings
|
||||
一般的な設定です。
|
||||
|
||||
@@ -97,18 +279,35 @@ Self-hosted LiveSyncは一つのチャンクのサイズを最低minimum chunk s
|
||||
### Vervose log
|
||||
詳細なログをログに出力します。
|
||||
|
||||
### ファイル警告バナーの代わりにステータスアイコンを表示
|
||||
設定キー: hideFileWarningNotice
|
||||
|
||||
有効な場合、ファイル警告バナーの代わりにステータス表示内に ⛔ アイコンが表示されます(詳細情報は非表示になります)。
|
||||
|
||||
### ネットワーク警告のスタイル
|
||||
設定キー: networkWarningStyle
|
||||
|
||||
同期サーバーに接続できない場合のネットワークエラーの表示方法。
|
||||
|
||||
## Sync setting
|
||||
同期に関する設定です。
|
||||
|
||||
### LiveSync
|
||||
LiveSyncを行います。
|
||||
他の同期方法では、同期の順序が「バージョン確認を行い、ロックが行われていないか確認した後、リモートの変更を受信した後、デバイスの変更を送信する」という挙動になります。
|
||||
### 同期モード (Sync Mode)
|
||||
設定キー: syncMode
|
||||
|
||||
### Periodic Sync
|
||||
定期的に同期を行います。
|
||||
同期処理を実行するトリガーとなる条件を設定します。
|
||||
- **LiveSync** (`LIVESYNC`): リアルタイムかつ継続的な双方向同期を行います。
|
||||
注意: このモードには CouchDB または WebRTC P2P リモートサーバーが必要です。S3互換オブジェクトストレージではサポートされていません。
|
||||
- **Periodic Sync** (`PERIODIC`): **Periodic Sync Interval** で指定した一定の間隔ごとに同期処理を実行します。
|
||||
- **On Events** (`ONEVENTS`): ファイルの保存、ファイルを開く、起動時など、特定のイベントが発生した際に同期をトリガーします(詳細は下部の設定スイッチで制御します)。
|
||||
|
||||
### Periodic Sync Interval
|
||||
定期的に同期を行う場合の間隔です。
|
||||
定期的に同期を行う場合の間隔(秒単位)です。
|
||||
|
||||
### 同期の最小間隔
|
||||
設定キー: syncMinimumInterval
|
||||
|
||||
イベント時の自動同期の最小間隔(ミリ秒)。
|
||||
|
||||
### Sync on Save
|
||||
ファイルが保存されたときに同期を行います。
|
||||
@@ -146,6 +345,11 @@ Self-hosted LiveSyncは通常、フォルダ内のファイルがすべて削除
|
||||
- Scan hidden files periodicaly.
|
||||
このオプションを有効にすると、n秒おきに隠しファイルをスキャンします。
|
||||
|
||||
#### 非表示ファイルの変更通知を抑制
|
||||
設定キー: suppressNotifyHiddenFilesChange
|
||||
|
||||
有効な場合、非表示ファイルの変更に関する通知を抑制します。
|
||||
|
||||
隠しファイルは能動的に検出されないため、スキャンが必要です。
|
||||
スキャンでは、ファイルと共にファイルの変更時刻を保存します。もしファイルが消された場合は、その事実も保存します。このファイルを記録したエントリーがレプリケーションされた際、ストレージよりも新しい場合はストレージに反映されます。
|
||||
|
||||
@@ -176,6 +380,45 @@ Self-hosted LiveSyncはPouchDBを使用し、リモートと[このプロトコ
|
||||
### Batch limit
|
||||
一度に処理するBatchの数です。デフォルトは40です。
|
||||
|
||||
### 1回のリクエストで送信するチャンクの最大サイズ
|
||||
設定キー: sendChunksBulkMaxSize
|
||||
|
||||
メガバイト(MB)単位で指定します。
|
||||
|
||||
## Customisation Sync (カスタマイズ同期)
|
||||
プラグイン、ホットキー、テーマ、スニペットなどのObsidianのカスタマイズ設定を同期する機能です(以前は **Plugin Sync** と呼ばれていました)。
|
||||
|
||||
### デバイス名 (Device name)
|
||||
設定キー: deviceAndVaultName
|
||||
|
||||
同期するすべてのデバイス間で一意となるデバイス名です。この設定を編集するには、一度カスタマイズ同期を無効にする必要があります。
|
||||
|
||||
### ファイル保存ごとのカスタマイズ同期 (Per-file-saved customisation sync)
|
||||
設定キー: usePluginSyncV2
|
||||
|
||||
有効な場合、ファイルごとの効率的なカスタマイズ同期が使用されます。有効にする際には簡単な移行作業が必要であり、すべてのデバイスを v0.23.18 以降にアップデートする必要があります。この機能を有効にすると、古いバージョンとの互換性が失われます。
|
||||
|
||||
### カスタマイズ同期を有効にする (Enable customisation sync)
|
||||
設定キー: usePluginSync
|
||||
|
||||
テーマ、スニペット、ホットキー、プラグイン設定などの同期を有効にします。
|
||||
注意: 安全上の理由から、この機能を使用するにはエンドツーエンド暗号化(End-to-End Encryption)が有効になっている必要があります。
|
||||
|
||||
### カスタマイズの自動スキャン (Scan customisation automatically)
|
||||
設定キー: autoSweepPlugins
|
||||
|
||||
レプリケーション(同期処理)を実行する前に、カスタマイズ設定の変更をスキャンします。
|
||||
|
||||
### 定期的なカスタマイズのスキャン (Scan customisation periodically)
|
||||
設定キー: autoSweepPluginsPeriodic
|
||||
|
||||
1分ごとにカスタマイズ設定の変更を定期的にスキャンします。
|
||||
|
||||
### カスタマイズ更新の通知 (Notify customised)
|
||||
設定キー: notifyPluginOrSettingUpdated
|
||||
|
||||
他のデバイスで新しくカスタマイズ設定が更新されたときに通知を表示します。
|
||||
|
||||
## Miscellaneous
|
||||
その他の設定です
|
||||
### Show status inside editor
|
||||
@@ -195,8 +438,8 @@ Self-hosted LiveSyncはPouchDBを使用し、リモートと[このプロトコ
|
||||

|
||||
データベースがロックされていて、端末が「解決済み」とマークされていない場合、警告が表示されます。
|
||||
他のデバイスで、End to End暗号化を有効にしたか、Drop Historyを行った等、他の端末がそのまま同期を行ってはいない状態に陥った場合表示されます。
|
||||
暗号化を有効化した場合は、パスフレーズを設定してApply and recieve、Drop Historyを行った場合は、Drop and recieveを行うと自動的に解除されます。
|
||||
手動でこのロックを解除する場合は「mark this device as resolved」をクリックしてください。
|
||||
暗号化を有効化した場合は、パスフレーズを設定して「このデバイスの同期状態をリセット」、または「このデバイスのファイルでサーバーデータを上書き」を行うと自動的に解除されます。
|
||||
手動でこのロックを解除する場合は「I've made a backup, mark this device 'resolved'」をクリックしてください。
|
||||
|
||||
- パターン2
|
||||

|
||||
@@ -207,18 +450,52 @@ Self-hosted LiveSyncはPouchDBを使用し、リモートと[このプロトコ
|
||||
### Verify and repair all files
|
||||
Vault内のファイルを全て読み込み直し、もし差分があったり、データベースから正常に読み込めなかったものに関して、データベースに反映します。
|
||||
|
||||
- Drop and send
|
||||
デバイスとリモートのデータベースを破棄し、ロックしてからデバイスのファイルでデータベースを構築後、リモートに上書きします。
|
||||
- Drop and receive
|
||||
デバイスのデータベースを破棄した後、リモートから、操作しているデバイスに関してロックを解除し、データを受信して再構築します。
|
||||
- このデバイスの同期状態をリセット (Reset Synchronisation on This Device)
|
||||
ローカルのデータベースを破棄し、リモートのデータから再構築します。
|
||||
- このデバイスのファイルでサーバーデータを上書き (Overwrite Server Data with This Device's Files)
|
||||
ローカルおよびリモートのデータベースをこのデバイス上のファイルで再構築(上書き)します。
|
||||
|
||||
### Lock remote database
|
||||
リモートのデータベースをロックし、他の端末で同期を行おうとしてもエラーとともに同期がキャンセルされるように設定します。これは、データベースの再構築を行った場合、自動的に設定されるものと同じものです。
|
||||
|
||||
万が一同期に不具合が発生していて、使用しているデバイスのデータ+サーバーのデータを保護する場合などに、緊急避難的に使用してください。
|
||||
|
||||
### Suspend file watching
|
||||
ファイルの更新の監視を止めます。
|
||||
### Scram スイッチ (Scram Switches)
|
||||
データベースの破損や予期しないデータ喪失を防ぐために、同期処理を緊急停止するためのスイッチです。重大な設定不一致や同期エラーが発生した場合、プラグインは自動的に Scram 状態に移行し、同期動作を一時停止することがあります。
|
||||
|
||||
#### ファイルの更新監視を一時停止 (Suspend file watching)
|
||||
設定キー: suspendFileWatching
|
||||
|
||||
ローカルファイル変更の監視と検知を停止します。
|
||||
|
||||
#### データベース反映を一時停止 (Suspend database reflecting)
|
||||
設定キー: suspendParseReplicationResult
|
||||
|
||||
データベースでの変更をストレージファイル(Vault内のファイル)へ書き戻す処理を停止します。
|
||||
|
||||
### 互換性(メタデータ)(Compatibility (Metadata))
|
||||
|
||||
#### 削除済みファイルのメタデータを保持しない (Do not keep metadata of deleted files.)
|
||||
設定キー: deleteMetadataOfDeletedFiles
|
||||
|
||||
ファイルを削除した際に、そのファイルの同期履歴メタデータも即座にデータベースから削除し、保持しないようにします。
|
||||
|
||||
#### 削除済みデータのメタデータをクリーンナップする (Delete old metadata of deleted files on start-up)
|
||||
設定キー: automaticallyDeleteMetadataOfDeletedFiles
|
||||
|
||||
ファイルを削除した際のメタデータを保持する期間(日数)を設定します。指定した日数を経過した古い削除済みファイルのメタデータは、プラグイン起動時にデータベースから自動的に削除(クリーンナップ)されます。`0` を指定すると自動削除は無効になります。
|
||||
|
||||
### 破損している可能性があるファイルも処理する
|
||||
設定キー: processSizeMismatchedFiles
|
||||
|
||||
サイズ不一致のあるファイルを処理します。特定のAPIや外部連携によって作成されたファイルを同期する際に役立ちます。
|
||||
|
||||
### Remediation
|
||||
|
||||
#### イベント反映時の最大ファイル更新日時
|
||||
設定キー: maxMTimeForReflectEvents
|
||||
|
||||
この値(Unixエポックからの秒数)より新しい更新日時を持つファイルについては、イベントの反映を無視します。0を指定すると制限が無効になります。
|
||||
|
||||
### Corrupted data
|
||||

|
||||
|
||||
@@ -13,7 +13,7 @@ In these instructions, create IBM Cloudant Instance for trial.
|
||||
1. You can choose "Lite plan" for free.
|
||||

|
||||
|
||||
1. Select Multitenant(it's the default) and the region as you like.
|
||||
1. Select Multitenant (it is the default) and the region as you like.
|
||||

|
||||
|
||||
1. Be sure to select "IAM and Legacy credentials" for "Authentication Method".
|
||||
@@ -28,20 +28,20 @@ In these instructions, create IBM Cloudant Instance for trial.
|
||||
1. When all of the above steps have been done, open "Resource list" on the left pane. you can see the Cloudant instance in the "Service and software". Click it.
|
||||

|
||||
|
||||
1. In resource details, there's information to connect from Self-hosted LiveSync.
|
||||
Copy the "External Endpoint(preferred)" address. <sup>(\*1)</sup>. We use this address later, with the database name.
|
||||
1. In resource details, there is information to connect from Self-hosted LiveSync.
|
||||
Copy the "External Endpoint (preferred)" address. <sup>(\*1)</sup>. We use this address later, with the database name.
|
||||

|
||||
|
||||
## Database setup
|
||||
|
||||
1. Hit the "Launch Dashboard" button, Cloudant dashboard will be shown.
|
||||
Yes, it's almost CouchDB's fauxton.
|
||||
Yes, it is almost CouchDB's fauxton.
|
||||

|
||||
|
||||
1. First, you have to enable the CORS option.
|
||||
Hit the Account menu and open the "CORS" tab.
|
||||
Initially, "Origin Domains" is set to "Restrict to specific domains"., so set to "All domains(\*)"
|
||||
_NOTE: of course We want to set "app://obsidian.md" but it's not acceptable on Cloudant._
|
||||
_NOTE: of course We want to set "app://obsidian.md" but it is not acceptable on Cloudant._
|
||||

|
||||
|
||||
1. Next, Open the "Databases" tab and hit the "Create Database" button.
|
||||
@@ -55,10 +55,10 @@ In these instructions, create IBM Cloudant Instance for trial.
|
||||
|
||||
### Credentials Setup
|
||||
|
||||
1. Back into IBM Cloud, Open the "Service credentials". You'll get an empty list, hit the "New credential" button.
|
||||
1. Back into IBM Cloud, Open the "Service credentials". You will get an empty list, hit the "New credential" button.
|
||||

|
||||
|
||||
1. The dialog to create a credential will be shown.
|
||||
1. The dialogue to create a credential will be shown.
|
||||
type any name or leave it default, hit the "Add" button.
|
||||

|
||||
_NOTE: This "name" is not related to your username that uses in Self-hosted LiveSync._
|
||||
@@ -68,14 +68,14 @@ In these instructions, create IBM Cloudant Instance for trial.
|
||||

|
||||
The username and password pair is inside this JSON.
|
||||
"username" and "password" are so.
|
||||
follow the figure, it's
|
||||
follow the figure, it is
|
||||
"apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"<sup>(\*3)</sup> and "c2c11651d75497fa3d3c486e4c8bdf27"<sup>(\*4)</sup>
|
||||
|
||||
## Self-hosted LiveSync settings
|
||||
|
||||

|
||||
|
||||
The Setting should be as below:
|
||||
The settings should be as follows:
|
||||
|
||||
| Items | Value | example |
|
||||
| ------------- | ----- | ----------------------------------------------------------------- |
|
||||
|
||||
108
docs/setup_object_storage.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Set up Object Storage
|
||||
|
||||
This guide establishes Object Storage synchronisation on a first device, generates an additional-device Setup URI from that working device, and verifies synchronisation in both directions.
|
||||
|
||||
Object Storage uses the S3-compatible API. Prepare the following before starting:
|
||||
|
||||
- an HTTPS endpoint reachable by every device;
|
||||
- an access key and secret key with access to the selected bucket;
|
||||
- a bucket name and region;
|
||||
- a unique bucket prefix when the bucket is shared; and
|
||||
- separate passphrases for Vault encryption and Setup URI encryption.
|
||||
|
||||
Back up every Vault involved, and do not use Obsidian Sync, iCloud synchronisation, or another synchronisation service on the same Vault.
|
||||
|
||||
## Generate the bootstrap Setup URI
|
||||
|
||||
The public generator applies the Object Storage preset and records the connection as the selected remote profile. Run it from a trusted terminal:
|
||||
|
||||
```sh
|
||||
export remote_type=s3
|
||||
export endpoint=https://objects.example.com
|
||||
export access_key=<ACCESS KEY>
|
||||
export secret_key=<SECRET KEY>
|
||||
export bucket=vault-data
|
||||
export region=auto
|
||||
export bucket_prefix=my-vault
|
||||
export passphrase=<A STRONG VAULT ENCRYPTION PASSPHRASE>
|
||||
export uri_passphrase=<A SEPARATE SETUP URI PASSPHRASE>
|
||||
deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts
|
||||
```
|
||||
|
||||
For providers which require them, set `force_path_style`, `use_custom_request_handler`, or `bucket_custom_headers` as described in the [setup utility reference](../utils/readme.md#object-storage).
|
||||
|
||||
Store the generated Setup URI and Setup URI passphrase separately. The URI is encrypted, but it contains the Object Storage credentials.
|
||||
|
||||
## Set up the first device
|
||||
|
||||
Use a new bucket prefix, or a prefix whose contents you deliberately intend to replace.
|
||||
|
||||
1. Install and enable Self-hosted LiveSync in the intended Vault.
|
||||
2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice.
|
||||
3. Select `I am setting this up for the first time`, then choose the recommended Setup URI method.
|
||||
4. Paste the bootstrap Setup URI, enter its passphrase, and select `Test Settings and Continue`.
|
||||
|
||||

|
||||
|
||||
5. Select `Restart and Initialise Server`, then read and accept the final overwrite confirmation only when this Vault is the intended source of truth.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. A new prefix may show `Fetch Remote Configuration Failed` because it has no saved configuration. Select `Skip and proceed` only for a genuinely new prefix. Otherwise, stop and check the endpoint, credentials, bucket, and prefix.
|
||||
|
||||

|
||||
|
||||
7. Keep optional features disabled until ordinary note synchronisation works.
|
||||
8. Create an ordinary test note, and keep Obsidian open until the LiveSync progress indicators have cleared.
|
||||
|
||||
## Generate the second-device Setup URI
|
||||
|
||||
Generate a fresh Setup URI from the working first device:
|
||||
|
||||
1. Run `Self-hosted LiveSync: Copy settings as a new Setup URI` from the command palette.
|
||||
2. Enter a new Setup URI passphrase.
|
||||
|
||||

|
||||
|
||||
3. Copy the resulting URI.
|
||||
|
||||

|
||||
|
||||
Store this URI and its passphrase separately.
|
||||
|
||||
## Add another device
|
||||
|
||||
Start with a new or separately backed-up Vault.
|
||||
|
||||
1. Install and enable Self-hosted LiveSync.
|
||||
2. Open onboarding, select `I am adding a device to an existing synchronisation setup`, and choose the recommended Setup URI method.
|
||||
3. Enter the URI generated by the first device and its passphrase.
|
||||
|
||||

|
||||
|
||||
4. Select `Restart and Fetch Data`.
|
||||
|
||||

|
||||
|
||||
5. For a new or empty Vault, choose `Overwrite all with remote files`, then `Keep local files even if not on remote`. Review the [Fast Setup guide](./tips/fast-setup.md) before choosing a different policy for a Vault containing local work.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. Keep Obsidian open until retrieval and file reflection finish.
|
||||
|
||||
Confirm that the first device's test note appears unchanged. Create a second ordinary note on the new device, wait for its journal synchronisation to finish, and confirm that it reaches the first device. Configure optional features only after this two-way check passes.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Safety notes
|
||||
|
||||
- Treat the endpoint, bucket, prefix, access key, secret key, Vault passphrase, Setup URI, and Setup URI passphrase as sensitive.
|
||||
- Use a distinct prefix per synchronisation set unless shared data is explicitly intended.
|
||||
- Do not select first-device initialisation against an existing prefix unless replacing its contents is deliberate.
|
||||
- Object Storage is not a Vault backup. Keep independent backups and test restoration separately.
|
||||
@@ -23,6 +23,7 @@
|
||||
- [Setting up your domain](#setting-up-your-domain)
|
||||
- [Reverse Proxies](#reverse-proxies)
|
||||
- [Traefik](#traefik)
|
||||
- [Nginx](#nginx)
|
||||
---
|
||||
|
||||
## 1. Prepare CouchDB
|
||||
@@ -32,7 +33,7 @@
|
||||
```bash
|
||||
|
||||
# Adding environment variables.
|
||||
export hostname=localhost:5984
|
||||
export hostname=http://localhost:5984
|
||||
export username=goojdasjdas #Please change as you like.
|
||||
export password=kpkdasdosakpdsa #Please change as you like
|
||||
|
||||
@@ -64,6 +65,10 @@ Congrats, move on to [step 2](#2-run-couchdb-initsh-for-initialise)
|
||||
# Creating the save data & configuration directories.
|
||||
mkdir couchdb-data
|
||||
mkdir couchdb-etc
|
||||
|
||||
# Changing perms to user 5984.
|
||||
chown -R 5984:5984 ./couchdb-data
|
||||
chown -R 5984:5984 ./couchdb-etc
|
||||
```
|
||||
|
||||
#### 2. Create a `docker-compose.yml` file with the following added to it
|
||||
@@ -110,31 +115,27 @@ Congrats, move on to [step 2](#2-run-couchdb-initsh-for-initialise)
|
||||
Please refer to the [official document](https://docs.couchdb.org/en/stable/install/index.html). However, we do not have to configure it fully. Just the administrator needs to be configured.
|
||||
|
||||
## 2. Run couchdb-init.sh for initialise
|
||||
|
||||
Deno 2 is required. Export the CouchDB connection and database details, then run the provisioning wrapper:
|
||||
|
||||
```
|
||||
export hostname=http://localhost:5984
|
||||
export username=<INSERT USERNAME HERE>
|
||||
export password=<INSERT PASSWORD HERE>
|
||||
export database=obsidiannotes
|
||||
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash
|
||||
```
|
||||
|
||||
If it results like the following:
|
||||
```
|
||||
-- Configuring CouchDB by REST APIs... -->
|
||||
{"ok":true}
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
<-- Configuring CouchDB by REST APIs Done!
|
||||
CouchDB provisioning completed.
|
||||
```
|
||||
|
||||
Your CouchDB has been initialised successfully. If you want this manually, please read the script.
|
||||
The wrapper runs the exact registry-pinned Commonlib consumer. When `database` is supplied, it creates the database and initialises its LiveSync database-version document through Commonlib. Without `database`, it configures only the CouchDB server.
|
||||
|
||||
If you are using Docker Compose and the above command does not work or displays `ERROR: Hostname missing`, you can try running the following command, replacing the placeholders with your own values:
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://<YOUR SERVER IP>:5984 username=<INSERT USERNAME HERE> password=<INSERT PASSWORD HERE> bash
|
||||
curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://<YOUR SERVER IP>:5984 username=<INSERT USERNAME HERE> password=<INSERT PASSWORD HERE> database=obsidiannotes bash
|
||||
```
|
||||
|
||||
## 3. Expose CouchDB to the Internet
|
||||
@@ -170,39 +171,38 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv
|
||||
|
||||
### 1. Generate the setup URI on a desktop device or server
|
||||
```bash
|
||||
export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault
|
||||
export database=obsidiannotes #Please change as you like
|
||||
export passphrase=dfsapkdjaskdjasdas #Please change as you like
|
||||
export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com
|
||||
export database=obsidiannotes
|
||||
export username=johndoe
|
||||
export password=abc123
|
||||
deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts
|
||||
export password=<INSERT THE COUCHDB PASSWORD>
|
||||
export passphrase=<INSERT A STRONG VAULT ENCRYPTION PASSPHRASE>
|
||||
export uri_passphrase=<INSERT A SEPARATE SETUP URI PASSPHRASE> # Optional
|
||||
deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> What is the `passphrase`? Is it different from `uri_passphrase`?
|
||||
> Yes, the `passphrase` we have exported now is for an End-to-End Encryption passphrase.
|
||||
> And, `uri_passphrase` that used in the `generate_setupuri.ts` is a different one; for decrypting Set-up URI at using that.
|
||||
> Why: I (vorotamoroz) think that the passphrase of the Setup-URI should be different from the E2EE passphrase to prevent exposure caused by operational errors or the possibility of evil in our environment. On top of that, I believe that it is desirable for the Setup-URI to be random. Setup-URI is inevitably long, so it goes through the clipboard. I think that its passphrase should not go through the same path, so it should essentially be typed manually.
|
||||
> Hence, if we keep empty for uri_passphrase, generate_setupuri.ts generates an adjective-noun-randomnumber passphrase so that we can remember it without going through the clipboard.
|
||||
> `passphrase` protects the synchronised Vault data with end-to-end encryption. `uri_passphrase` protects only the Setup URI. Use different values, store both securely, and do not send the Setup URI and its passphrase through the same channel.
|
||||
>
|
||||
> If `uri_passphrase` is omitted, the generator creates a cryptographically random value and prints it once.
|
||||
|
||||
The generator consumes the exact registry-pinned Commonlib release used by the provisioning utility. It creates a configured CouchDB remote profile, applies the current new-Vault defaults, and encodes them with Commonlib's Setup URI contract.
|
||||
|
||||
You will then get the following output:
|
||||
|
||||
```bash
|
||||
Generated couchdb Setup URI.
|
||||
Your passphrase for the Setup URI is: H7vX...a-random-32-character-value
|
||||
This passphrase is never shown again, so store it safely.
|
||||
obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D
|
||||
|
||||
Your passphrase of Setup-URI is: patient-haze
|
||||
This passphrase is never shown again, so please note it in a safe place.
|
||||
```
|
||||
|
||||
Please keep your passphrase of Setup-URI.
|
||||
Store the Setup URI and its passphrase separately.
|
||||
|
||||
### 2. Setup Self-hosted LiveSync to Obsidian
|
||||
[This video](https://youtu.be/7sa_I1832Xc?t=146) may help us.
|
||||
1. Install Self-hosted LiveSync
|
||||
2. Choose `Use the copied setup URI` from the command palette and paste the setup URI. (obsidian://setuplivesync?settings=.....).
|
||||
3. Type the previously displayed passphrase (`patient-haze`) for setup-uri passphrase.
|
||||
4. Answer `yes` and `Set it up...`, and finish the first dialogue with `Keep them disabled`.
|
||||
5. `Reload app without save` once.
|
||||
|
||||
Follow [Quick setup](./quick_setup.md#set-up-the-first-device) for the first device. It covers the current onboarding Notice, Setup URI import, server initialisation, and the safety prompts shown for a newly provisioned database.
|
||||
|
||||
After ordinary note synchronisation works, [generate a new Setup URI on that first device](./quick_setup.md#create-a-setup-uri-for-another-device), then follow [Add another device](./quick_setup.md#add-another-device). Do not make the second device depend on retaining the provisioning-time bootstrap URI. Configure optional features only after the normal path is verified; [Hidden File Sync has its own guide](./tips/hidden-file-sync.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -226,7 +226,6 @@ And, be sure to check the server log and be careful of malicious access.
|
||||
If you are using Traefik, this [docker-compose.yml](https://github.com/vrtmrz/obsidian-livesync/blob/main/docker-compose.traefik.yml) file (also pasted below) has all the right CORS parameters set. It assumes you have an external network called `proxy`.
|
||||
|
||||
```yaml
|
||||
version: "2.1"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb:latest
|
||||
@@ -288,3 +287,103 @@ entryPoints:
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
When configuring nginx as a reverse-proxy for CouchDB, note the common mistakes:
|
||||
|
||||
1. If fast fetch progress stalls and seems to freeze indefinitely, make sure you disabled `proxy_buffering`:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5984;
|
||||
proxy_buffering off;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. If you get the "413 Entity too large" error, increase the `client_max_body_size`:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5984;
|
||||
client_max_body_size 50M; # Tweak the value to your needs
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
3. If you get the "404 Database not found" error, make sure you placed CouchDB at the root location (recommended):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name couchdb.domain.com
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5984;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It is possible to place CouchDB into the subdirectory, however, the config should be modified respectively:
|
||||
|
||||
```nginx
|
||||
|
||||
server {
|
||||
server_name domain.com
|
||||
|
||||
location /couchdb {
|
||||
rewrite ^ $request_uri;
|
||||
rewrite ^/couchdb/(.*) /$1 break;
|
||||
|
||||
proxy_pass http://127.0.0.1:5984$uri;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
...
|
||||
}
|
||||
location /_session {
|
||||
proxy_pass http://127.0.0.1:5984/_session;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. If you added custom HTTP headers in database connection advanced settings, make sure to update both nginx and CouchDB configurations:
|
||||
|
||||
Nginx:
|
||||
```nginx
|
||||
location / {
|
||||
set $pass 1;
|
||||
|
||||
# Example of handling custom HTTP header
|
||||
if ($http_x_custom_header != 'foo'){
|
||||
set $pass 0;
|
||||
}
|
||||
|
||||
# Important: OPTIONS requests don't carry headers, so they should always be proxied to the CouchDB
|
||||
if ($request_method = 'OPTIONS') {
|
||||
set $pass 1;
|
||||
}
|
||||
|
||||
if ($pass = 0) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
proxy_pass http://127.0.0.1:5984;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
couchdb-etc/docker.ini:
|
||||
```ini
|
||||
...
|
||||
[cors]
|
||||
credentials = true
|
||||
origins = app://obsidian.md,capacitor://localhost,http://localhost
|
||||
|
||||
;Make sure to add your custom header to the list so CORS won't break
|
||||
headers = accept, authorization, content-type, origin, referer, x-custom-header
|
||||
```
|
||||
|
||||
@@ -71,7 +71,6 @@ obsidian-livesync
|
||||
|
||||
可以参照以下内容编辑 `docker-compose.yml`:
|
||||
```yaml
|
||||
version: "2.1"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb
|
||||
|
||||
121
docs/setup_p2p.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Set up peer-to-peer synchronisation
|
||||
|
||||
This guide establishes a peer-to-peer synchronisation set, generates the second-device Setup URI on the working first device, and verifies synchronisation in both directions with explicit peer approval.
|
||||
|
||||
Peer-to-peer synchronisation has no central copy of the Vault. At least one device containing the required data must be online when another device fetches it. A Nostr-compatible signalling relay helps devices discover each other, while the Vault data travels through the peer connection.
|
||||
|
||||
Before starting:
|
||||
|
||||
- back up both Vaults;
|
||||
- prepare a signalling relay reachable by both devices;
|
||||
- ensure the networks permit a WebRTC connection, or review the [P2P troubleshooting guidance](./tips/p2p-sync-tips.md);
|
||||
- disable every other synchronisation service for these Vaults; and
|
||||
- keep both devices awake and Obsidian open during the initial transfer.
|
||||
|
||||
## Generate the bootstrap Setup URI
|
||||
|
||||
Run the public generator from a trusted terminal. Supply your own relay for a controlled self-hosted setup:
|
||||
|
||||
```sh
|
||||
export remote_type=p2p
|
||||
export p2p_relays=wss://relay.example.com
|
||||
export p2p_room_id=<A PRIVATE ROOM ID> # Optional; generated when omitted
|
||||
export p2p_passphrase=<A PRIVATE P2P PASSPHRASE> # Optional; generated when omitted
|
||||
export passphrase=<A STRONG VAULT ENCRYPTION PASSPHRASE>
|
||||
export uri_passphrase=<A SEPARATE SETUP URI PASSPHRASE>
|
||||
deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts
|
||||
```
|
||||
|
||||
The generated Setup URI contains the encrypted room, relay, and Vault settings. It deliberately omits the device-specific peer name. Store the URI and its passphrase separately.
|
||||
|
||||
## Set up the first device
|
||||
|
||||
1. Install and enable Self-hosted LiveSync in the intended Vault.
|
||||
2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice.
|
||||
3. Select `I am setting this up for the first time`, then choose the recommended Setup URI method.
|
||||
4. Paste the bootstrap Setup URI, enter its passphrase, and select `Test Settings and Continue`.
|
||||
|
||||

|
||||
|
||||
5. Complete the first-device initialisation and final confirmation. This initialises the local LiveSync database; P2P has no central remote database to erase.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. Keep optional features disabled until ordinary note synchronisation works.
|
||||
7. Open `Self-hosted LiveSync: Open P2P Replicator` from the command palette. Select `Open connection` if signalling is disconnected.
|
||||
|
||||

|
||||
|
||||
8. Create an ordinary test note and wait for the local LiveSync progress indicators to clear.
|
||||
|
||||
## Generate the second-device Setup URI
|
||||
|
||||
On the working first device:
|
||||
|
||||
1. Run `Self-hosted LiveSync: Copy settings as a new Setup URI` from the command palette.
|
||||
2. Enter a new Setup URI passphrase.
|
||||
|
||||

|
||||
|
||||
3. Copy the resulting URI.
|
||||
|
||||

|
||||
|
||||
Keep the first device online. Store the new URI and its passphrase separately.
|
||||
|
||||
## Add the second device
|
||||
|
||||
1. Install and enable Self-hosted LiveSync in a new or separately backed-up Vault.
|
||||
2. Open onboarding, select `I am adding a device to an existing synchronisation setup`, and choose the recommended Setup URI method.
|
||||
3. Enter the Setup URI generated by the first device and its passphrase.
|
||||
|
||||

|
||||
|
||||
4. Select `Restart and Fetch Data`.
|
||||
|
||||

|
||||
|
||||
5. For a new or empty Vault, choose `Overwrite all with remote files`, then `Keep local files even if not on remote`. Review the [Fast Setup guide](./tips/fast-setup.md) before using a Vault which contains local work.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. In `P2P Rebuild`, confirm that the expected first-device name is shown, then select `Sync`.
|
||||
|
||||

|
||||
|
||||
7. On the first device, verify the requesting device name and select `Accept`. Use `Accept Temporarily` instead when approval should last only for this Obsidian session.
|
||||
|
||||

|
||||
|
||||
8. Keep both devices open until the test note appears on the second device.
|
||||
|
||||

|
||||
|
||||
## Verify the return journey
|
||||
|
||||
Create a second ordinary note on the second device. With automatic broadcast disabled, start the next finite synchronisation explicitly:
|
||||
|
||||
1. Open the P2P Replicator pane on both devices.
|
||||
2. If the previous finite peer connection no longer appears, select `Disconnect` and then `Open connection` on the first device, followed by the second device. The device which joins last is advertised to devices already in the room.
|
||||
3. On the first device, select `Refresh`, verify the second-device name, then select `Replicate now`.
|
||||
4. On the second device, verify the requesting first-device name and select `Accept` or `Accept Temporarily`.
|
||||
|
||||

|
||||
|
||||
5. Confirm that the second-device note appears unchanged on the first device.
|
||||
|
||||

|
||||
|
||||
The two devices are now proven to share the same room, encryption settings, and data format in both directions. Configure automatic start, automatic broadcast, or optional features separately after this manual path works.
|
||||
|
||||
## If a peer does not appear
|
||||
|
||||
- Confirm that both panes show `Connected` and the same Room ID suffix.
|
||||
- Select `Refresh` after the other device joins.
|
||||
- Reconnect the device which should be discovered last.
|
||||
- Check that the Setup URI came from the working first device and that neither device copied a peer name manually.
|
||||
- Check relay reachability, WebRTC restrictions, VPNs, and TURN considerations in [Peer-to-Peer Synchronisation Tips](./tips/p2p-sync-tips.md).
|
||||
227
docs/specs_conflict_resolution.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Conflict resolution and revision provenance
|
||||
|
||||
This document describes the conflict-resolution and file-reflection guarantees used by Self-hosted LiveSync 1.0, together with the cases which still require user judgement. The underlying revision-tree operations and injectable provenance contract are owned by `@vrtmrz/livesync-commonlib`; LiveSync owns persistent device-local composition, Vault reflection, settings, and dialogue policy.
|
||||
|
||||
## Revision-tree model
|
||||
|
||||
PouchDB stores a document as a revision tree. It selects one live leaf as the deterministic winner and reports the other live leaves as conflicts. That winner is not proof that its content is newer, safer, or the version currently shown in the Vault.
|
||||
|
||||
For example:
|
||||
|
||||
```text
|
||||
A1
|
||||
├── B1 ── C1 ── D1
|
||||
└── B2 ── C2
|
||||
```
|
||||
|
||||
The two live leaves are `D1` and `C2`. Their nearest shared ancestor is `A1`; neither `B1` nor `B2` is shared. A conservative three-way merge therefore compares the changes from `A1` to each leaf. Matching generation numbers, or selecting the first older revision from one branch, does not prove shared ancestry.
|
||||
|
||||
Resolving a conflict writes the selected or merged result on one observed branch and deletes the other observed live leaf. A stale device may still have the deleted leaf's content in its Vault when it receives the resolution.
|
||||
|
||||
## Implemented 1.0 guarantees
|
||||
|
||||
- Automatic text and structured-data merge uses the nearest `available` revision ID which is present in both leaf histories.
|
||||
- Missing or compacted history stops conservative automatic merge instead of guessing a base.
|
||||
- A receiving Vault file which exactly matches any available revision in the document tree is treated as previously synchronised content. This includes an ancestor below a deleted losing leaf.
|
||||
- A receiving Vault file whose bytes do not match any available revision is preserved as an unsynchronised local change.
|
||||
- File bytes, rather than path, size, modification time, or revision generation, determine whether content is known.
|
||||
- Three or more live versions are reviewed one pair at a time in a deterministic order, with each completed pair committed before the next live pair is read.
|
||||
- Each device records the exact revision most recently reflected in each Vault file. An edit, deletion, or case-only rename made while a conflict is active extends that displayed branch rather than the deterministic database winner.
|
||||
- A cross-path rename stores the target before logically deleting only the displayed source branch.
|
||||
|
||||
The all-branch history check prevents a resolved conflict from being recreated merely because the receiving Vault still contains the known losing version. If the user has edited that version again, its bytes differ and the overwrite guard preserves it.
|
||||
|
||||
## Resolution patterns
|
||||
|
||||
| State | Safe action |
|
||||
| -------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Both leaves contain identical bytes | Collapse the duplicate leaf. |
|
||||
| Text or structured data has an available shared base and non-overlapping changes | Perform a conservative three-way merge. |
|
||||
| One side deletes content which the other leaves unchanged | Preserve the deletion. |
|
||||
| One side deletes content which the other modifies | Ask the user. |
|
||||
| A receiving file matches a revision available anywhere in the tree | Apply the propagated database result. |
|
||||
| A receiving file matches no available revision | Preserve it and ask the user. |
|
||||
| A required body or shared ancestor is missing or compacted | Ask the user. |
|
||||
| Binary contents differ | Prefer an explicit user selection; semantic merge is unavailable. |
|
||||
|
||||
The compatibility implementation currently selects the newer modification time for differing binary conflicts even when the general **Always overwrite with a newer file** option is disabled. This is existing behaviour, not a new 1.0 guarantee. Changing it to explicit selection only is a separate compatibility decision.
|
||||
|
||||
### Two devices independently create the same path
|
||||
|
||||
If two devices create the same full synchronised path before either device has
|
||||
received the other creation, the two generation-one leaves have no shared
|
||||
revision. Files with the same name in different directories remain separate
|
||||
paths and do not form this conflict.
|
||||
|
||||
When the independently created files contain identical bytes, LiveSync deletes
|
||||
one duplicate leaf without synthesising merged content. A device which still
|
||||
records the deleted duplicate as its displayed revision already has the same
|
||||
bytes as the surviving revision, so it does not recreate the conflict. It
|
||||
rebinds its device-local provenance to the surviving revision.
|
||||
|
||||
When the independently created files contain different bytes, conservative
|
||||
three-way merge has no valid base. LiveSync therefore leaves the two versions
|
||||
for manual selection; it does not guess an empty base or concatenate unrelated
|
||||
files. If both versions instead descend from a revision which the devices had
|
||||
previously synchronised, they are ordinary divergent branches: LiveSync may
|
||||
merge non-overlapping text or structured-data changes from that shared base,
|
||||
and otherwise asks the user.
|
||||
|
||||
## Stale and concurrent resolutions
|
||||
|
||||
A device can resolve only the leaves which it has observed. If another device has already extended a branch, later replication can reveal another live leaf and require another resolution. Two devices can also produce different resolutions concurrently, leaving multiple live leaves after their trees meet.
|
||||
|
||||
A higher revision generation or modification time does not make either result authoritative. The resolver must examine every current live leaf again until one result remains or user action is required. This is continued conflict processing, not a reset of the synchronisation checkpoint.
|
||||
|
||||
## More than two live versions
|
||||
|
||||
When three or more versions remain, LiveSync compares the current PouchDB winner with one conflict leaf at a time. Commonlib orders the remaining candidates by revision generation ascending, original leaf modification time ascending, then the complete revision ID in code-unit lexical order. A missing or non-finite modification time is ordered before a finite value. Modification time makes pair selection reproducible here; it does not decide which content wins.
|
||||
|
||||
For each pair, LiveSync first collapses identical content, then attempts a conservative sensible merge, and finally asks the user when neither automatic action is safe. A completed action is written to the ordinary revision tree and its losing observed leaf is deleted before LiveSync reads the remaining live leaves again. There is no separate persistent merge accumulator.
|
||||
|
||||
**Concat both** writes the concatenated result as a new child of the displayed PouchDB winner, then deletes only the other leaf shown in that dialogue. With two live versions, that action resolves the conflict. With three or more, the new child remains live against every untouched leaf and becomes part of the next pairwise review; it does not create an unrelated root or consume an unseen branch.
|
||||
|
||||
Consequently, choosing **Not now** or closing Obsidian cannot undo a completed pair. After restart, LiveSync reconstructs the next pair from the live tree. If replication changes either revision while a dialogue is open, LiveSync discards the stale selection, refreshes the live count, and rechecks the path rather than deleting a revision which was not the one shown.
|
||||
|
||||
## Device-local file provenance
|
||||
|
||||
LiveSync composes Commonlib's injected `FileReflectionProvenance` with its local key-value database. Each device stores:
|
||||
|
||||
```text
|
||||
path -> { revision, observedStorageMtime? }
|
||||
```
|
||||
|
||||
`revision` identifies the exact database revision which most recently produced the displayed Vault file. `observedStorageMtime` is the raw local modification time observed after reflection. It is not rounded, combined with another device's value, or used as proof of branch identity. No content hash is persisted.
|
||||
|
||||
The record changes only after a successful database-to-Vault reflection or Vault-to-database write. Reading a file does not change it. The recorded revision remains authoritative even if the user edits the file to bytes which equal another branch; otherwise content equality could silently move the edit to a branch which was not displayed.
|
||||
|
||||
LiveSync creates the namespaced store handle during service composition, before the key-value database is open. The sequential `onSettingLoaded` lifecycle opens that database before Vault scanning, watching, or replication starts. Store operations do not wait for implicit readiness: a lifecycle violation fails promptly, avoiding an indefinite or self-referential initialisation wait. Local database reset is a transient unavailable boundary, after which scanning reconstructs derived state.
|
||||
|
||||
When no record exists, LiveSync may reconstruct the displayed revision only if the current Vault bytes match exactly one available revision body. No match, or identical content in multiple revisions, cannot prove branch identity.
|
||||
|
||||
## Operations while a conflict exists
|
||||
|
||||
- Editing a file writes a child of its recorded or uniquely reconstructed displayed revision.
|
||||
- Deleting a file writes a logical-deletion child of that revision. It uses LiveSync's `deleted` marker, rather than a PouchDB `_deleted` tombstone, so the deletion remains a live branch which can replicate and be resolved against the other branch.
|
||||
- A case-only rename writes the new path as a child in the same document tree.
|
||||
- A cross-path rename stores the target document first, then writes a logical-deletion child on the displayed source branch.
|
||||
|
||||
If an edit's base cannot be proved, LiveSync keeps the bytes as another manual-resolution branch instead of attaching them silently to the database winner. If a deletion's displayed branch cannot be proved after the file body has gone, LiveSync preserves every branch and requests conflict review. For an unproven cross-path rename, the new target remains stored and every source branch is preserved for review. These fallbacks can leave a temporary duplicate or unresolved source, but they do not discard an unproven branch.
|
||||
|
||||
## Interactive dialogue policy
|
||||
|
||||
Choosing **Not now** postpones repeated merge dialogues for the same
|
||||
uninterrupted conflict episode in the current plug-in session. Ordinary file
|
||||
checks and replication do not reopen the dialogue while at least one conflict
|
||||
leaf remains. If the in-editor status display is enabled, the active file shows
|
||||
**This file has 3 unresolved versions. They will be reviewed one pair at a
|
||||
time.** for three or more live versions, using the current count, and **This
|
||||
file has unresolved conflicts.** for two. Postponement therefore does not make
|
||||
the conflict invisible.
|
||||
|
||||
The command **Resolve if conflicted.**, and selecting a file through **Pick a
|
||||
file to resolve conflict**, explicitly clear the postponement and request the
|
||||
dialogue again. Cancellation caused by another conflict dialogue does not count
|
||||
as **Not now**. Once the document has no remaining conflicts, the episode ends;
|
||||
a later conflict at the same path prompts normally. The postponement is not
|
||||
persisted across a plug-in reload. Completed pairwise resolutions are persisted
|
||||
in the ordinary revision tree, so a reload forgets only the postponement and
|
||||
does not repeat an already committed stage.
|
||||
|
||||
When synchronisation supplies a resolved document, the existing incoming-file
|
||||
processing event closes an open conflict dialogue for that path. The same event
|
||||
rechecks the local revision tree: if no conflict leaf remains, it ends any
|
||||
postponed episode and removes the active-file warning. If conflict leaves still
|
||||
exist, the stale dialogue closes and the warning changes to the current live
|
||||
version count. A postponed episode stays postponed; otherwise, subsequent
|
||||
conflict processing may open a fresh dialogue for the current revision tree.
|
||||
Each dialogue owns its completion result, so a prompt which is answered or
|
||||
closed immediately still completes the waiting conflict operation; the result
|
||||
does not depend on a later global listener being ready.
|
||||
The end of an automatic repeat is silent. An explicit **Pick a file to resolve
|
||||
conflict** request which starts with no conflicts may show one confirmation
|
||||
Notice.
|
||||
|
||||
## Example device scenarios
|
||||
|
||||
### A user edits the branch shown on one device
|
||||
|
||||
Mac and Android have produced two branches of `shared.md`. Mac's local database selects revision `C1` as its deterministic winner, but the file currently shown in the Android Vault came from revision `C2`:
|
||||
|
||||
```text
|
||||
A1
|
||||
├── B1 ── C1 database winner
|
||||
└── B2 ── C2 displayed on Android
|
||||
```
|
||||
|
||||
Android recorded `C2` when it wrote that revision into the Vault. If the user edits the file on Android, the new revision extends `C2`:
|
||||
|
||||
```text
|
||||
A1
|
||||
├── B1 ── C1
|
||||
└── B2 ── C2 ── D2 Android edit
|
||||
```
|
||||
|
||||
After synchronisation, both devices receive `C1` and `D2` as the live branches. The edit is not moved silently onto `C1`, and ordinary conflict resolution can compare the real descendants.
|
||||
|
||||
### A user deletes the branch shown on one device
|
||||
|
||||
If Android deletes the file while it still displays `C2`, LiveSync writes a logical-deletion revision below `C2`:
|
||||
|
||||
```text
|
||||
A1
|
||||
├── B1 ── C1
|
||||
└── B2 ── C2 ── D2 (deleted: true)
|
||||
```
|
||||
|
||||
The deletion remains one side of the live conflict. The user can still choose between the content at `C1` and deleting the file. LiveSync does not delete `C1` merely because PouchDB selected it as the winner.
|
||||
|
||||
### A user renames a conflicted file
|
||||
|
||||
If the user changes only the spelling case, such as `Note.md` to `note.md`, LiveSync keeps the rename in the same revision tree and extends the revision displayed on that device.
|
||||
|
||||
If the user renames `draft.md` to `published.md`, LiveSync stores `published.md` before it marks the displayed `draft.md` branch as logically deleted. If an interruption occurs between those operations, the recoverable result is a duplicate which can be reviewed, rather than loss of the only copy. Any other live branch of `draft.md` remains available for conflict resolution.
|
||||
|
||||
### A remote resolution reaches a device which still shows the losing content
|
||||
|
||||
Android may resolve a conflict and continue editing while Mac still shows the losing revision. When Mac receives the resolved tree, LiveSync searches every available branch and recognises Mac's unchanged bytes as content which was already synchronised below the deleted losing leaf. It can apply Android's resolution without asking Mac to resolve the same unchanged conflict again.
|
||||
|
||||
If the user edited the file on Mac before the resolution arrived, the bytes no longer match that historical revision. LiveSync preserves the Mac edit as an unsynchronised conflict instead of overwriting it.
|
||||
|
||||
### A three-version review is interrupted
|
||||
|
||||
Mac receives three live versions of `shared.md`. The active-file status reports three unresolved versions, and the first dialogue compares the deterministic winner with the first ordered conflict leaf. The user completes that pair, leaving two live versions, then chooses **Not now** on the next dialogue and closes Obsidian.
|
||||
|
||||
The first decision has already changed the ordinary revision tree. On restart, LiveSync reads the two surviving versions and presents only that remaining pair; it does not reconstruct the original three-version state. If another device resolves the remaining pair before or while the dialogue is open, the warning disappears and the stale dialogue closes.
|
||||
|
||||
### The device-local record is missing
|
||||
|
||||
A local-database reset removes revision provenance. On the next scan, if the Vault file matches exactly one available revision, LiveSync can reconstruct which branch was displayed and continue from it. If the bytes match multiple revisions, or no available revision, the branch remains unproved.
|
||||
|
||||
In that unproved state, an edit is retained as another manual-resolution branch. A deletion leaves all existing branches intact. A cross-path rename stores the target but leaves every source branch for review. The result can require an extra decision, but it does not discard data by guessing the winner.
|
||||
|
||||
### Start-up or reset overlaps a provenance operation
|
||||
|
||||
LiveSync creates the provenance handle during composition, then opens its backing store during the sequential settings lifecycle before starting scans, watchers, or replication. If the store cannot open, start-up stops rather than leaving file processing waiting indefinitely.
|
||||
|
||||
During reset, the store can be temporarily unavailable. A racing provenance lookup fails promptly and follows the same conservative missing-record behaviour. After reopen, scanning can reconstruct a record when one exact revision body matches the Vault file.
|
||||
|
||||
## Unsafe shortcuts
|
||||
|
||||
Do not:
|
||||
|
||||
- infer a common ancestor from generation numbers alone;
|
||||
- assume that the PouchDB winner is the version currently displayed in the Vault;
|
||||
- replace recorded displayed provenance merely because current bytes match another branch;
|
||||
- discard local content when revision-history lookup fails;
|
||||
- infer revision identity from path, size, modification time, or content hash without a revision ID;
|
||||
- select the newest modification time unless the user has explicitly chosen that destructive policy; or
|
||||
- merge overlapping text edits or unrelated binary contents automatically.
|
||||
|
||||
## Verification
|
||||
|
||||
Commonlib's real-PouchDB and injected-boundary unit tests cover unequal branch lengths, exact shared ancestry, deterministic ordering of multiple live leaves, a sensible stage followed by reconstruction of a manual pair, content below a deleted losing leaf, recorded and reconstructed branch identity, ambiguous matches, conflict-time editing, logical deletion, case-only rename, cross-path rename, and safe unproven fallbacks.
|
||||
|
||||
LiveSync's optional real-Obsidian two-Vault checks have two scopes. `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true` resolves and edits a Markdown conflict, propagates it to a Vault which still displays the deleted losing content, and requires one live result to remain. `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` edits, deletes, case-renames, and cross-path-renames files while conflicts remain active; it verifies the parent revision of each resulting branch, replicates those exact trees, and confirms that the other live branches remain intact.
|
||||
|
||||
The focused `test:e2e:obsidian:conflict-dialog-policy` scenario creates three live versions in one real Obsidian Vault. It verifies the count warning, commits a concatenated child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the isolated Obsidian profile, and confirms that only the live pair is reconstructed. It also verifies that an incoming resolution closes a stale dialogue, completes the waiting conflict operation, and clears the warning.
|
||||
116
docs/specs_data_compression.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Data Compression specification
|
||||
|
||||
## Status and decision for 1.0
|
||||
|
||||
Data Compression is a maintained, advanced opt-in feature for CouchDB-compatible remote databases. It remains disabled by default in the 1.0 line.
|
||||
|
||||
The feature provides a modest, measurable reduction in mixed-workload storage and transfer volume. Its value depends heavily on the chunk contents, while the current implementation adds substantial processing and worker-memory costs. Users with slow, metered, or storage-constrained connections may still find that trade-off worthwhile.
|
||||
|
||||
## Stored-data behaviour
|
||||
|
||||
The `enableCompression` setting affects chunk documents written through the CouchDB remote connection. It does not compress the local database, and it is separate from the journal format used by Object Storage synchronisation.
|
||||
|
||||
For each document containing string `data`, the writer:
|
||||
|
||||
1. detects canonical Base64 and decodes it to bytes, or encodes text as UTF-8;
|
||||
2. applies raw DEFLATE through fflate at level 8;
|
||||
3. encodes the result as Base64 and adds the LiveSync compressed-data marker; and
|
||||
4. stores the compressed representation only when its string representation is shorter than the original.
|
||||
|
||||
Already compressed data and data which does not become smaller are therefore retained unchanged. Readers always recognise and expand the compressed-data marker, including when their own `enableCompression` setting is off. Compressed and uncompressed chunks can coexist in the same remote database.
|
||||
|
||||
When E2EE is enabled, compression is applied before E2EE V2 encryption. The encrypted remote representation does not expose the compression marker.
|
||||
|
||||
Changing the setting does not require a rebuild for compatibility: new writes adopt the selected policy, while existing chunks remain readable. All devices are still asked to agree on the remote tweak value so that future writes use one consistent policy. A deliberate remote rebuild can normalise existing storage, but it is optional rather than a prerequisite for synchronisation.
|
||||
|
||||
## Execution model
|
||||
|
||||
The compression and decompression algorithms are not executed synchronously on Obsidian's UI thread. Browser builds call fflate's asynchronous `deflate` and `inflate` APIs, which create a Web Worker for an operation and terminate it after the callback. The CLI uses the corresponding Node worker-thread path.
|
||||
|
||||
The PouchDB transform hook, Base64 detection and conversion, UTF-8 conversion, worker creation, result conversion, and document mutation still run in the calling JavaScript context. The current implementation also creates a separate fflate worker for each attempted chunk rather than reusing LiveSync's persistent splitting and encryption worker pool. `transform-pouch` applies the incoming transform to a bulk batch with `Promise.all`, so a batch can start many of these workers concurrently. It can therefore consume significant total CPU and memory, and worker churn may still affect responsiveness or mobile process limits even though the DEFLATE calculation itself is off the UI thread.
|
||||
|
||||
The current benchmark measures the Node CLI process. It does not establish Obsidian UI event-loop latency, Electron renderer responsiveness, mobile WebView memory behaviour, battery use, thermal throttling, or platform watchdog thresholds. Those remain real-runtime validation gaps.
|
||||
|
||||
## Reproducible benchmark
|
||||
|
||||
The benchmark is implemented under `src/apps/cli/testdeno` and packaged by `test/bench-network`. It uses the real CLI mirror and synchronisation path, Commonlib 0.1.0-rc.4, CouchDB 3.5.0, the V3 Rabin–Karp splitter, optional Data Compression, and E2EE V2. It runs each of these conditions three times in rotating order:
|
||||
|
||||
- E2EE off, compression off;
|
||||
- E2EE off, compression on;
|
||||
- E2EE on, compression off; and
|
||||
- E2EE on, compression on.
|
||||
|
||||
The 623,553-byte deterministic fixture contains three Markdown files, two generated JPEG files, two repository PNG files, two JSON files, two TypeScript files, one gzip file, and one high-entropy binary file. Every run materialises and byte-compares all 13 files after synchronisation.
|
||||
|
||||
Run it from the repository root:
|
||||
|
||||
```bash
|
||||
BENCH_COMMAND=compression \
|
||||
BENCH_COMPRESSION_REPEAT_COUNT=3 \
|
||||
BENCH_COUCHDB_RTT_MS=1 \
|
||||
docker compose -f test/bench-network/compose.yml run --build --rm bench-runner
|
||||
```
|
||||
|
||||
The latest local three-repeat result was generated on 21st July, 2026. The percentages below compare medians with compression off and on.
|
||||
|
||||
| Measurement | E2EE off | E2EE on |
|
||||
| ------------------------------------------------ | -------: | ------: |
|
||||
| Stored chunk-data reduction | 9.12% | 9.01% |
|
||||
| CouchDB external-size reduction | 9.03% | 8.92% |
|
||||
| CouchDB file-size reduction | 2.57% | 7.62% |
|
||||
| Upload request-body reduction | 8.58% | 8.61% |
|
||||
| Complete materialisation response-body reduction | 6.56% | 4.84% |
|
||||
| Upload wall-time increase | 197.10% | 199.29% |
|
||||
| Upload CPU-time increase | 650.43% | 581.54% |
|
||||
| Complete materialisation wall-time increase | 19.58% | 19.75% |
|
||||
| Complete materialisation CPU-time increase | 45.11% | 43.98% |
|
||||
|
||||
With E2EE on, median upload wall time rose from 1.49 seconds to 4.45 seconds, CPU time rose from 1.30 seconds to 8.86 seconds, and upload maximum resident memory rose from about 160 MiB to 403 MiB. The full materialisation workflow starts a CLI process for each file and produced a much higher compressed-path peak of about 983 MiB; treat that figure as evidence about the current CLI workflow rather than a browser decompression lower bound.
|
||||
|
||||
The E2EE upload saved 99,034 decoded HTTP body bytes while adding about 2.96 seconds of local processing wall time. A simple serial-transfer estimate puts the wall-time break-even near 0.27 Mbit/s. This estimate excludes headers, contention, request overlap, radio energy, data charges, and server behaviour. The benchmark does not emulate a bandwidth-limited mobile link, so transfer-volume reduction may still be valuable where elapsed time is not the only cost.
|
||||
|
||||
## Results by file kind
|
||||
|
||||
The E2EE stored chunk-data reductions were:
|
||||
|
||||
| Fixture kind | Reduction |
|
||||
| ------------------- | --------: |
|
||||
| Markdown | 16.30% |
|
||||
| JPEG | 4.72% |
|
||||
| PNG | 6.16% |
|
||||
| JSON | 72.80% |
|
||||
| TypeScript | 74.11% |
|
||||
| gzip | 0% |
|
||||
| high-entropy binary | 0% |
|
||||
|
||||
These results describe payload and chunk characteristics, not a reliable file-extension policy. The JSON and TypeScript fixtures were repetitive and mapped to relatively large chunks. The Markdown files were split into 151 referenced chunks, so small-chunk overhead and limited repetition windows reduced their benefit. JPEG, PNG, and gzip inputs had already undergone format-level compression, while the deterministic binary input was intentionally difficult to compress.
|
||||
|
||||
The remote transform sees a content-addressed chunk, not a trustworthy original file type. A chunk can also be deduplicated across files with different extensions. Enabling compression only for selected extensions would therefore either require carrying new provenance into the chunk format or make the representation depend on whichever file first produced a shared chunk. Neither is suitable for the 1.0 format.
|
||||
|
||||
## Follow-up optimisation candidates
|
||||
|
||||
The first optimisation should remove unbounded per-chunk worker creation:
|
||||
|
||||
1. add compression and decompression tasks to a reusable worker pool;
|
||||
2. run synchronous fflate inside those workers so that it does not create a nested worker for each task;
|
||||
3. put a bounded scheduler or semaphore before dispatch, rather than submitting the complete `Promise.all` batch at once;
|
||||
4. transfer derived input and output `ArrayBuffer` objects explicitly in each `postMessage` transfer list; and
|
||||
5. decide that a result is not smaller inside the worker, so an unhelpful compressed buffer does not need to be returned.
|
||||
|
||||
LiveSync's current `bgWorker` is a useful starting point because it creates a fixed pool of approximately half the reported hardware concurrency and selects workers in round-robin order. It is not sufficient unchanged: its `processing` count does not control selection, and it does not limit the number of tasks posted to each worker. Its foreground modules also form a circular dependency: `bgWorker.ts` imports the splitting and encryption adapters, while those adapters import task dispatch and removal from `bgWorker.ts`. Compression must not add another branch to that cycle.
|
||||
|
||||
Before adding compression, the Worker code should be separated into a dependency-bottom pool, task registry, scheduler, and transfer transport, with splitting, encryption, and compression implemented as task adapters above it. The shared scheduler should own concurrency limits, cancellation, crash propagation, fairness, and task clean-up. Compression then needs either a separate bounded lane or scheduling which prevents a long level-8 task from starving splitting and encryption work.
|
||||
|
||||
The `TransformStream` currently used by `bgWorker.splitting` is local to the calling context; it is not transferred to the Worker. Moving a stream endpoint across contexts could provide real end-to-end back-pressure, but it is not the first memory optimisation for Data Compression. The [Streams Standard transfer algorithm](https://streams.spec.whatwg.org/#transferable-streams) transfers the stream endpoint and posts each chunk through an internal `MessagePort` with an empty transfer list. Binary chunks can therefore still be cloned. Explicitly transferring each `ArrayBuffer` is the clearer zero-copy boundary for independent chunk tasks.
|
||||
|
||||
Cross-context transferable streams also cannot yet be required by the supported mobile baseline. WebKit lists `ReadableStream`, `WritableStream`, and `TransformStream` transfer via `postMessage()` as a [Safari 27 beta addition](https://webkit.org/blog/17967/news-from-wwdc26-webkit-in-safari-27-beta/#readablestream-improvements). A future general `bgWorker` redesign may use transferable streams behind capability detection, particularly where back-pressure matters more than copying, but it needs a message-based fallback.
|
||||
|
||||
After bounding worker use, adaptive per-chunk compression can reduce unnecessary work without depending on the source filename:
|
||||
|
||||
1. skip very small chunks where the marker and worker start-up dominate;
|
||||
2. sample decoded bytes and estimate entropy or repetition before starting level-8 DEFLATE;
|
||||
3. require both a minimum byte saving and a minimum percentage saving;
|
||||
4. compare lower DEFLATE levels against level 8; and
|
||||
5. replace one-worker-per-chunk operation with a bounded, persistent compression worker pool and back-pressure.
|
||||
|
||||
Any optimisation must preserve the current wire contract: content may remain uncompressed, the compressed marker must stay readable, compression must precede E2EE, and mixed representations must interoperate. It should be evaluated with the same four-condition benchmark, a bandwidth-shaped case, UI event-loop latency, and at least one real mobile Obsidian run before reconsidering the default.
|
||||
@@ -1,8 +1,8 @@
|
||||
# Designed architecture
|
||||
|
||||
## How does this plugin synchronize.
|
||||
## How does this plug-in synchronise.
|
||||
|
||||

|
||||

|
||||
|
||||
1. When notes are created or modified, Obsidian raises some events. Self-hosted LiveSync catches these events and reflects changes into Local PouchDB.
|
||||
2. PouchDB automatically or manually replicates changes to remote CouchDB.
|
||||
|
||||
16
docs/tech_info_cn.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 架构设计
|
||||
|
||||
## 这个插件是怎么实现同步的.
|
||||
|
||||

|
||||
|
||||
1. 当笔记创建或修改时,Obsidian会触发事件。Self-hosted LiveSync捕获这些事件,并将变更同步至本地PouchDB
|
||||
2. PouchDB通过自动或手动方式将变更同步至远程CouchDB
|
||||
3. 其他设备监听远程CouchDB的变更,从而获取最新更新
|
||||
4. Self-hosted LiveSync 将同步的变更集反映到Obsidian存储库中。
|
||||
|
||||
注:图示为简化演示,仅展示两个设备间的单向同步。实际为多设备间同时进行的双向同步。
|
||||
|
||||
## 降低带宽消耗的技术方案。
|
||||
|
||||

|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 同期
|
||||
|
||||

|
||||

|
||||
|
||||
1. ノートが更新された際、Obsidianがイベントを発報します。Obsidian-LiveSyncはそれをハンドリングして、ローカルのPouchDBに変更を反映します。
|
||||
2. PouchDBは、リモートのCouchDBに差分をレプリケーションします。
|
||||
|
||||
@@ -2,23 +2,101 @@
|
||||
|
||||
## Spelling and Vocabulary conventions
|
||||
|
||||
1. Almost all of the english words are written in British English. For example, "organisation" instead of "organization", "synchronisation" instead of "synchronization", etc. This convention originated from the author's personal preference but is now maintained for consistency.
|
||||
All guidelines and conventions listed below are disclosed and maintained solely for the sake of documentation `consistency`.
|
||||
|
||||
2. Idiomatic terms, such as used in HTML, CSS, and JavaScript, are usually be aligned with the language used in the technology. For example, "color" instead of "colour", "program" instead of "programme", etc. Especially, terms which are used for attributes, properties, and methods are notable.
|
||||
1. Almost all of the English words are written in British English. This convention originated from the author's personal preference.
|
||||
- **Traditional Spelling (Trad-spelling)**: We prefer traditional British English spellings. In particular, we use `-ise` and `-isation` suffixes rather than the Oxford spelling `-ize` and `-ization` (for example, 'initialisation', 'synchronisation', and 'organisation').
|
||||
- **Oxford Comma**: We use the serial (Oxford) comma to separate items in lists of three or more (for example, 'settings, snippets, and themes' instead of 'settings, snippets and themes').
|
||||
- **Logical Punctuation**: We place punctuation marks (such as commas and full stops) outside quotation marks, unless the punctuation mark is part of the quoted text itself. For example, we write 'dialogue', not 'dialogue,'.
|
||||
- **BBC News Styleguide**: If in wonder, the BBC News Styleguide may be useful as a reference.
|
||||
|
||||
2. Idiomatic terms, such as those used in HTML, CSS, and JavaScript, are usually aligned with the language used in the technology. For example, "color" instead of "colour", "program" instead of "programme", etc. Especially, terms which are used for attributes, properties, and methods are notable.
|
||||
|
||||
3. We use `dialogue` in documentation for consistency. While `dialog` may appear in source code, particularly in class names, method names, and attributes (following technical conventions in No. 2), we consistently use `dialogue` for user-facing messages and general documentation text. This approach balances No. 1 with No. 2.
|
||||
|
||||
4. Contractions are not used. For example, "do not" instead of "don't", "cannot" instead of "can't", etc. especially `'d`.
|
||||
4. Contractions are not used. For example, "do not" instead of "don't", "cannot" instead of "can't", etc., especially `'d`.
|
||||
- We may encounter difficulties with tenses.
|
||||
|
||||
5. However, try using affirmative forms, `Discard` instead of `Do not keep`, `Continue` instead of `Do not stop`, etc.
|
||||
- Some languages, such as Japanese, have a different meaning for `yes` and `no` between affirmative and negative questions.
|
||||
|
||||
## Terminology
|
||||
6. Single quotation marks (`'`) are preferred over double quotation marks (`"`) in general documentation text, unless the context requires double quotes (for example, inside JSON code blocks).
|
||||
|
||||
- Self-hosted LiveSync
|
||||
- This plug-in name. `Self-hosted` is one word.
|
||||
### Terminology
|
||||
|
||||
- Boot-up sequence (boot-sequence)
|
||||
- The initialisation process of the plug-in when Obsidian starts. It starts with the loading of the plug-in, setting up core services, loading saved settings, and opening the local database. Once the layout is ready, the plug-in checks for the presence of flag files, runs configuration diagnostics, connects to the remote database, and begins file watching. The sequence finishes once the plug-in is fully ready and operational.
|
||||
- Broken files (Size mismatch)
|
||||
- A state where a file's metadata and the actual content stored in its chunks do not match, causing file retrieval or synchronisation failures. These mismatches can be detected and resolved by running validation tools such as `Verify and repair all files` on the Hatch pane.
|
||||
- Chunk / Chunks
|
||||
- Divided units of data stored in the database or object storage to facilitate efficient synchronisation.
|
||||
- Compaction
|
||||
- A database maintenance procedure that discards old historical document revisions to shrink the remote database size.
|
||||
- Custom HTTP Handler / Use Internal API (CORS Bypass Settings)
|
||||
- Settings used to bypass CORS restrictions by routing requests through Obsidian's native request APIs. There are two distinct settings under the hood depending on the remote server type:
|
||||
- **For S3-compatible Object Storage (useCustomRequestHandler)**: Labeled as **"Use Custom HTTP Handler"** in the standard settings tab, **"Use internal API"** in the Svelte-based Setup Wizard dialogue, and represented as `useProxy` in the Setup URI's query parameters due to an unfortunate misunderstanding during development.
|
||||
- **For CouchDB (useRequestAPI)**: Labeled as **"Use Request API to avoid `inevitable` CORS problem"** in the standard settings tab, **"Use Internal API"** in the Svelte-based Setup Wizard dialogue, and represented as `useRequestAPI` in the Setup URI's query parameters.
|
||||
- Customisation Sync
|
||||
- The feature that synchronises settings, snippets, themes, and plug-ins. Write with an "s" in documentation (`Customisation`), though technical configurations and links may use `customization`.
|
||||
- Database Adapter (IDB vs. IndexedDB)
|
||||
- The local database storage interface used by PouchDB. The `IDB` adapter is recommended since the older `IndexedDB` adapter is obsolete and known to cause memory leaks in `LiveSync` mode. Users can switch between these adapters without a full database rebuild, although a local data migration and an Obsidian restart are required.
|
||||
- Database Suffix (additionalSuffixOfDatabaseName)
|
||||
- A unique suffix appended to the database name to allow synchronising multiple vaults with the same name on the same remote server.
|
||||
- E2EE Algorithm
|
||||
- The cryptographic algorithm version used for end-to-end encryption. All devices in the synchronisation group must be configured with a compatible version (such as `V2` or `V1`).
|
||||
- Eden (Eden Chunks)
|
||||
- A performance optimisation where newly created chunks are held within the document until they stabilise, before graduating to independent chunks.
|
||||
- Fast Setup (Simple Fetch)
|
||||
- A simplified, automated initial synchronisation flow triggered when setting up subsequent devices or recovering a database. It bypasses the detailed step-by-step setup wizard dialogues, prompting the user with high-level data processing decisions and completing the initial download and local file scan in one continuous process.
|
||||
- Flag files (redflag.md, redflag2.md, redflag3.md)
|
||||
- Special Markdown files (or directories) placed at the root of the vault to stop the boot-up sequence or trigger recovery tasks. For instance, `redflag.md` suspends all processes, while `redflag2.md` (`flag_rebuild.md`) triggers a full database rebuild and `redflag3.md` (`flag_fetch.md`) discards the local database to fetch it again from the remote.
|
||||
- Garbage Collection (GC)
|
||||
- The process of identifying and purging unreferenced chunks (unused data) from local and remote databases to reclaim storage space.
|
||||
- Hatch (Hatch pane)
|
||||
- A dedicated troubleshooting and maintenance section in the plug-in settings, typically hidden behind a warning-labeled collapsible panel to prevent accidental misconfiguration. It contains diagnostic utilities, database reset controls, status reports, and advanced edge-case patches.
|
||||
- Hidden File Sync
|
||||
- The feature that synchronises files located in hidden directories (like `.obsidian`).
|
||||
- JWT Authentication
|
||||
- An experimental authentication option for CouchDB allowing secure token-based authentication instead of standard credentials. It requires a configured private key/secret, algorithm, expiration duration, subject, and key ID.
|
||||
- LiveSync
|
||||
- Very confusing term.
|
||||
- As shorten-form of `Self-hosted LiveSync`.
|
||||
- As a name of synchronisation mode. This should be changed to `Continuos`, in contrast to `Periodic`.
|
||||
- A very confusing term.
|
||||
- As a shortened form of `Self-hosted LiveSync`.
|
||||
- As the name of a synchronisation mode. This should be changed to `Continuous`, in contrast to `Periodic`.
|
||||
- livesync-serverpeer / webpeer
|
||||
- Pseudo-clients that assist in WebRTC peer-to-peer communication.
|
||||
- Metadata (File metadata)
|
||||
- A database document that stores properties of a file, including its filename, path, size, modification time, and references (hashes) of the chunks that comprise the file's content. Conflict state is carried by the surrounding PouchDB/CouchDB revision metadata rather than by a separate history field inside the file metadata document. In Self-hosted LiveSync, file metadata is stored separately from the actual file content to enable efficient synchronisation and versioning.
|
||||
- OneShot Sync
|
||||
- A single, immediate bidirectional synchronisation (pull then push) triggered on demand or on specific events, as opposed to continuous (live) replication.
|
||||
- Overwrite Server Data with This Device's Files
|
||||
- A maintenance operation (formerly known as `Rebuild everything`) that discards the remote database and reconstructs it by uploading all current local files as a fresh database, overwriting any remote changes.
|
||||
- Path Obfuscation
|
||||
- A privacy option that encrypts file paths and folder names on the remote server.
|
||||
- plug-in
|
||||
- We use the hyphenated form `plug-in` in user-facing messages and general documentation, while `plugin` may appear in codebase files, configuration settings, or technical contexts.
|
||||
- Relay Server (P2P relays)
|
||||
- A WebSocket-based coordination server used to establish direct WebRTC peer-to-peer connections. The default relay is provided by the plug-in author.
|
||||
- Remediation (maxMTimeForReflectEvents)
|
||||
- A recovery setting that restricts the propagation of changes from the database to local storage, ignoring any file events (such as accidental mass deletions) that occurred after a specified date and time.
|
||||
- Reset Synchronisation on This Device
|
||||
- A maintenance operation (formerly known as `Fetch everything`) that discards the local database and reconstructs it by downloading all data from the remote server.
|
||||
- Scram (Scram Switches)
|
||||
- Emergency controls in the settings that allow users to suspend file watching or database writes to prevent corruption.
|
||||
- Segmenter (Segmented-splitter)
|
||||
- A chunking method that divides files on semantic boundaries (such as paragraphs or sections) rather than arbitrary byte boundaries.
|
||||
- Self-hosted LiveSync
|
||||
- The name of this plug-in. `Self-hosted` is one word.
|
||||
- Setting Doctor (Config Doctor)
|
||||
- A diagnostic utility that checks for mismatches or suboptimal configurations, presenting users with ideal values and recommendation reasons to easily resolve issues during migration, configuration import, or general troubleshooting.
|
||||
- Setup URI
|
||||
- An encrypted representation of the plug-in's settings containing server configuration, which allows users to clone their configuration across devices securely using a passphrase.
|
||||
- Streaming replication (Stream-based replication)
|
||||
- A data transfer method that downloads database documents as a continuous stream of events. It is significantly faster than traditional chunk-by-chunk HTTP requests and is used during Fast Setup to retrieve remote metadata quickly.
|
||||
- Sync Mode
|
||||
- The replication trigger mechanism. Users can select from `On Events` (synchronising on local file changes), `Periodic and Events` (synchronising at fixed intervals as well as on events), or `LiveSync` (continuous, real-time synchronisation).
|
||||
- TURN Server (WebRTC P2P)
|
||||
- A server type (Traversal Using Relays around NAT) used as a fallback to relay traffic when direct WebRTC peer-to-peer connection is blocked by strict NAT or firewalls.
|
||||
- Update Thinning (Batch database update)
|
||||
- An optimisation that groups multiple local file edits together over a short delay before committing them to the local database, reducing the number of database write operations.
|
||||
- WebRTC P2P (Peer-to-Peer)
|
||||
- A synchronisation method enabling direct communication between devices without a central server database.
|
||||
|
||||
65
docs/tips/fast-setup.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Fast Setup (Simple Fetch)
|
||||
|
||||
Fast Setup is a streamlined, user-friendly data retrieval and initialisation flow designed to simplify setting up secondary devices or recovering databases.
|
||||
|
||||
Instead of guiding the user through the detailed multi-step setup wizard dialogues, Fast Setup prompts the user with high-level sync decisions and automates database download and local storage scanning in one continuous process.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
When you import a **Setup URI** on a secondary device, or when a **Fetch All** operation is triggered (such as by placing a `redflag3.md` / `flag_fetch.md` flag file at the root of the vault), the plug-in schedules remote data retrieval.
|
||||
|
||||
On the next startup, the plug-in boots in scheduled fetch mode and opens a simplified dialogue: **"Data retrieval scheduled"**.
|
||||
|
||||
---
|
||||
|
||||
## Technical Characteristics
|
||||
|
||||
Fast Setup leverages several backend optimisations to make the retrieval fast, safe, and clean:
|
||||
|
||||
1. **Stream-based Replication for Speed**
|
||||
- It fetches all remote metadata via stream reception, which is significantly faster than traditional chunk-by-chunk retrieval.
|
||||
2. **Delayed File Reflection to Prevent Corrupted Warnings**
|
||||
- By suspending file reflection during the download phase, it prevents the plug-in from raising temporary or false "corrupted data synchronisation" or "size mismatch" warnings that can occur during the chunk download process.
|
||||
3. **Time-Based Comparison is Generally Sufficient**
|
||||
- Since the vault is entering a fresh synchronisation or recovery state, comparing files based on their modification timestamps (newer-wins) is highly reliable and sufficient to reconcile files without needing complex manual conflict resolution.
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### Step 1: Choose Data Processing Method
|
||||
You will be prompted to choose how the retrieved remote data will interact with your existing local files:
|
||||
|
||||
1. **Compare time and take newer (newer-wins)**
|
||||
- Compares the modified time of files and accepts the newer version.
|
||||
- **Recommended if:** You have been using Self-hosted LiveSync and have made changes on multiple devices that you want to merge.
|
||||
2. **Overwrite all with remote files (remote-wins)**
|
||||
- Remote data is treated as the source of truth.
|
||||
- **Recommended if:** You are setting up a brand new device with an empty or clean vault.
|
||||
- *Warning: This will overwrite local files with remote files. Please ensure you have a backup of your local vault before proceeding.*
|
||||
3. **Use the detailed flow (legacy)**
|
||||
- Switches back to the detailed, traditional setup wizard dialogues.
|
||||
- **Recommended if:** You want full control over the step-by-step database setup options.
|
||||
|
||||
### Step 2: Configure Conflict & Deletion Rules
|
||||
Depending on your choice in Step 1, you will configure how to handle mismatches:
|
||||
|
||||
#### If you chose "Compare time and take newer":
|
||||
- **Delete local files if they were deleted on remote**
|
||||
- Keeps your local vault clean by removing files that have already been deleted on other devices.
|
||||
- **Recreate remote files even if they were deleted on remote**
|
||||
- Preserves local files and uploads them back to the remote database, even if they were deleted on other devices.
|
||||
|
||||
#### If you chose "Overwrite all with remote files":
|
||||
- **Delete local files if not on remote**
|
||||
- Removes local-only files so that your local vault matches the remote database exactly.
|
||||
- **Keep local files even if not on remote**
|
||||
- Retains all existing local-only files, although this may result in duplicates that you will need to clean up manually after synchronisation.
|
||||
|
||||
### Step 3: Automated Synchronisation
|
||||
Once you confirm your choices:
|
||||
1. The plug-in performs a fast download of the remote database (`fetchLocalDBFast`).
|
||||
2. It automatically runs a full scan (`synchroniseAllFilesBetweenDBandStorage`) in the foreground to reflect database changes in your local vault files immediately.
|
||||
3. The plug-in finalises the process and resumes normal operational status.
|
||||
66
docs/tips/fast-setup_ja.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# ファストセットアップ (Fast Setup / Simple Fetch)
|
||||
|
||||
ファストセットアップは、2台目以降のデバイスのセットアップやデータベース再構築時のデータ取得・初期化処理を、直感的かつ迅速に行うための簡略化された同期フローです。
|
||||
|
||||
従来のセットアップウィザードにおける複数の詳細なステップを踏むことなく、同期の基本方針を選択するだけで、データベースのダウンロードとローカルファイルのスキャン・反映を一連のプロセスとして自動的に実行します。
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||
2台目以降のデバイスで **セットアップURI** をインポートした場合や、手動でデータの再フェッチ(Vaultルートに `redflag3.md` / `flag_fetch.md` を配置する等)が予約された場合、プラグインはリモートデータベースからのデータ取得スケジュールを設定します。
|
||||
|
||||
その後の起動時、プラグインはデータフェッチ予約モードで立ち上がり、**「Data retrieval scheduled(データ取得のスケジュール)」** という簡略化されたダイアログを表示します。
|
||||
|
||||
---
|
||||
|
||||
## 技術的な特徴
|
||||
|
||||
ファストセットアップは、高速かつ安全でクリーンな処理を実現するために、以下の最適化を行っています。
|
||||
|
||||
1. **高速なストリーム受信**
|
||||
- 全データを取得しますが、ストリーム受信によるレプリケーションを使用するため、処理が非常に高速です。
|
||||
2. **ストレージ反映の遅延による不要な警告の抑制**
|
||||
- データのダウンロード中にローカルファイル(ストレージ)への書き出し(反映)処理をあえて遅延させることで、同期途中で発生しがちな「破損データ同期」や「サイズ不一致」などの一時的なエラー警告を抑え込みます。
|
||||
- すべてのデータダウンロードが完了した後に一括してストレージへ書き出すため、不必要な警告画面でユーザーを混乱させません。
|
||||
3. **時刻ベース比較の妥当性**
|
||||
- 初期セットアップやリカバリーの段階(この状態に移行した直後)においては、概ねファイル更新時刻(タイムスタンプ)ベースでの単純比較を行うことで、十分かつ妥当な同期結果を得ることができます。これにより複雑な競合解決の手間を省きます。
|
||||
|
||||
---
|
||||
|
||||
## 設定手順
|
||||
|
||||
### ステップ 1: データの反映方法の選択
|
||||
取得したリモートデータを、既存のローカルファイルとどのように統合するかを選択します。
|
||||
|
||||
1. **Compare time and take newer (newer-wins)**
|
||||
- ファイルの更新日時を比較し、より新しい方を採用します。
|
||||
- **推奨されるケース:** すでに Self-hosted LiveSync を使用しており、複数のデバイスで編集した変更内容をタイムスタンプに基づいて統合したい場合。
|
||||
2. **Overwrite all with remote files (remote-wins)**
|
||||
- リモートデータベースの内容を正(Source of Truth)として扱います。
|
||||
- **推奨されるケース:** まったく新しいデバイスをセットアップする場合(空のVaultなど)。
|
||||
- *警告: ローカルにあるすべてのファイルがリモートの内容で上書きされます。重要なデータがある場合は、事前にバックアップを取得してください。*
|
||||
3. **Use the detailed flow (legacy)**
|
||||
- 従来の詳細なセットアップウィザードダイアログに戻ります。
|
||||
- **推奨されるケース:** データベースの構成オプションをステップバイステップで細かく制御・確認したい場合。
|
||||
|
||||
### ステップ 2: 競合および削除ルールの構成
|
||||
ステップ 1 での選択内容に応じて、ローカルとリモートの不一致をどう処理するかを設定します。
|
||||
|
||||
#### 「Compare time and take newer」を選択した場合:
|
||||
- **Delete local files if they were deleted on remote**
|
||||
- 他のデバイスで削除済みのファイルをローカルからも削除し、Vaultを同期・クリーンな状態に保ちます。
|
||||
- **Recreate remote files even if they were deleted on remote**
|
||||
- 他のデバイスで削除されたファイルであっても、ローカルファイルを維持し、リモートデータベースに再度アップロードします。
|
||||
|
||||
#### 「Overwrite all with remote files」を選択した場合:
|
||||
- **Delete local files if not on remote**
|
||||
- リモートに存在しないローカル専用ファイルを削除し、ローカルのVaultをリモートデータベースと完全に一致させます。
|
||||
- **Keep local files even if not on remote**
|
||||
- リモートに存在しないローカルファイルをそのまま残します。ただし、同期後に重複ファイルが発生する可能性があるため、その場合は手動でクリーンアップを行ってください。
|
||||
|
||||
### ステップ 3: 自動同期の実行
|
||||
選択を確定すると、以下の処理が順次実行されます。
|
||||
1. リモートデータベースの高速ダウンロードを実行します (`fetchLocalDBFast`)。
|
||||
2. ローカルファイルへの変更反映のため、フォアグラウンドでフルスキャン (`synchroniseAllFilesBetweenDBandStorage`) を自動的に実行します。
|
||||
3. 処理が完了すると、プラグインは通常の動作状態へ復帰します。
|
||||
70
docs/tips/hidden-file-sync.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Hidden File Sync
|
||||
|
||||
Hidden File Sync is an advanced, optional feature for synchronising hidden files and folders, including selected content below `.obsidian`. It is separate from ordinary note synchronisation and is deliberately left disabled during Rebuild and Fetch setup operations.
|
||||
|
||||
Enable it only after ordinary notes synchronise correctly in both directions on every device.
|
||||
|
||||
> [!WARNING]
|
||||
> Hidden files can control active Obsidian settings, themes, snippets, and plug-ins. Back up every Vault before enabling this feature. Do not use Hidden File Sync and Customisation Sync to manage the same files.
|
||||
|
||||
## Choose the initial source
|
||||
|
||||
Enabling Hidden File Sync requires an initialisation direction:
|
||||
|
||||
- `Merge` compares the local and remote hidden-file state without declaring either side authoritative. Use this when both sides contain changes which must be retained, and review any conflicts.
|
||||
- `Fetch` treats the remote hidden-file state as authoritative and applies it locally. Use this on an additional device after the desired files have been uploaded.
|
||||
- `Overwrite` treats this device's hidden files as authoritative and writes them to the remote database. Use this first on the device whose hidden files you intend to distribute.
|
||||
|
||||
`Fetch` and `Overwrite` can replace files on one side. Check the direction and the backup before continuing.
|
||||
|
||||
## Review the file selection
|
||||
|
||||
1. Open Self-hosted LiveSync settings.
|
||||
2. Open `Setup`, find `Enable extra and advanced features`, and enable `Advanced features`.
|
||||
|
||||

|
||||
|
||||
3. Open `Selector`, then review the `Hidden Files` section.
|
||||
4. Use `Target patterns` to limit the feature to the hidden files you intend to synchronise. An empty target list includes every otherwise eligible hidden file.
|
||||
5. Review `Ignore patterns`. The default excludes `node_modules`, `.git`, and Self-hosted LiveSync's own plug-in data. `Add default patterns` offers a `Cross-platform` set which also excludes Obsidian workspace files.
|
||||
|
||||

|
||||
|
||||
Prefer a narrow target list. Device-specific workspace state and another plug-in's credentials are usually poor candidates for cross-device synchronisation.
|
||||
|
||||
Target patterns also control directory traversal. A pattern must therefore match each parent directory as well as the intended files. For example, the following pattern admits the `.obsidian` parent and only its `snippets` subtree:
|
||||
|
||||
```text
|
||||
^\.obsidian(?:$|/snippets(?:/|$))
|
||||
```
|
||||
|
||||
A pattern containing only `snippets` does not admit the `.obsidian` parent, so the scan cannot reach that directory.
|
||||
|
||||
## Enable the first device
|
||||
|
||||
1. Open `Sync Settings`, then find the advanced `Hidden Files` section.
|
||||
|
||||

|
||||
|
||||
2. Under `Enable Hidden File Sync`, select the initialisation direction chosen above.
|
||||
3. Keep Obsidian open while the initial scan and synchronisation finish.
|
||||
4. Restart Obsidian when the completion Notice recommends it.
|
||||
5. Confirm that the expected hidden files, and only those files, are present in the remote synchronisation state.
|
||||
|
||||
For the common source-of-truth rollout, select `Overwrite` on the authoritative device first.
|
||||
|
||||
## Enable each additional device
|
||||
|
||||
Hidden File Sync must be enabled independently after ordinary setup on each device.
|
||||
|
||||
1. Back up the additional device's Vault.
|
||||
2. Apply the same target and ignore patterns.
|
||||
3. Select `Fetch` if the remote hidden files are authoritative. Select `Merge` only when local hidden-file changes must also be retained.
|
||||
4. Wait for the initial scan and file application to finish, then restart Obsidian if requested.
|
||||
5. Verify representative files before enabling the feature on the next device.
|
||||
|
||||
If you later run Rebuild or Fetch as a recovery operation, expect optional features to be disabled again. Complete ordinary recovery first, then repeat this guide deliberately.
|
||||
|
||||
## Related settings
|
||||
|
||||
The [settings reference](../settings.md#7-hidden-files-advanced) describes periodic scanning, scanning before replication, notification suppression, selectors, and overwrite patterns. Change those controls only after the basic Hidden File Sync path is working.
|
||||
@@ -10,8 +10,10 @@ authors:
|
||||
|
||||
# Peer-to-Peer Synchronisation Tips
|
||||
|
||||
For the complete first-device, Setup URI, second-device, and two-way verification procedure, see [Set up peer-to-peer synchronisation](../setup_p2p.md).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Peer-to-peer synchronisation is still an experimental feature. Although we have made every effort to ensure its reliability, it may not function correctly in all environments.
|
||||
> Peer-to-peer synchronisation is a supported opt-in feature. WebRTC connectivity still depends on the networks, relays, and optional TURN servers available to every device, so a working connection cannot be guaranteed in every environment.
|
||||
|
||||
## Difficulties with Peer-to-Peer Synchronisation
|
||||
|
||||
|
||||
145
docs/to_issue_reporting.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# How to report an issue
|
||||
|
||||
Thank you for helping improve Self-hosted LiveSync!
|
||||
|
||||
This document explains how to collect the information needed for an issue report. Issues with sufficient information will be prioritised.
|
||||
|
||||
---
|
||||
|
||||
## Filled example
|
||||
|
||||
Here is an example of a well-filled report for reference.
|
||||
|
||||
### Abstract
|
||||
|
||||
The synchronisation hung up immediately after connecting.
|
||||
|
||||
### Expected behaviour
|
||||
|
||||
- Synchronisation ends with the message `Replication completed`
|
||||
- Everything synchronised
|
||||
|
||||
### Actually happened
|
||||
|
||||
- Synchronisation was cancelled with the message `TypeError: Failed to fetch` (visible in the plug-in log around lines 10–12)
|
||||
- No files synchronised
|
||||
|
||||
### Reproducing procedure
|
||||
|
||||
1. Configure LiveSync with the settings shown in the attached report.
|
||||
2. Click the sync button on the ribbon.
|
||||
3. Synchronisation begins.
|
||||
4. About two or three seconds later, the error `TypeError: Failed to fetch` appears.
|
||||
5. Replication stops. No files synchronised.
|
||||
|
||||
### Obsidian debug info (Device 1 — Windows desktop)
|
||||
|
||||
```
|
||||
SYSTEM INFO:
|
||||
Obsidian version: v1.2.8
|
||||
Installer version: v1.1.15
|
||||
Operating system: Windows 10 Pro 10.0.19044
|
||||
Login status: logged in
|
||||
Catalyst license: supporter
|
||||
Insider build toggle: off
|
||||
Community theme: Minimal v6.1.11
|
||||
Snippets enabled: 3
|
||||
Restricted mode: off
|
||||
Plugins installed: 35
|
||||
Plugins enabled: 11
|
||||
1: Self-hosted LiveSync v0.19.4
|
||||
...
|
||||
```
|
||||
|
||||
### Report from LiveSync
|
||||
|
||||
```
|
||||
----remote config----
|
||||
cors:
|
||||
credentials: "true"
|
||||
...
|
||||
---- Plug-in config ---
|
||||
couchDB_URI: self-hosted
|
||||
couchDB_USER: 𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷
|
||||
...
|
||||
```
|
||||
|
||||
### Plug-in log
|
||||
|
||||
```
|
||||
2023/5/24 10:50:33->HTTP:GET to:/ -> failed
|
||||
2023/5/24 10:50:33->TypeError:Failed to fetch
|
||||
2023/5/24 10:50:33->could not connect to https://example.com/ : your vault
|
||||
(TypeError:Failed to fetch)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to collect each piece of information
|
||||
|
||||
### Obsidian debug info
|
||||
|
||||
Open the command palette (`Ctrl/Cmd + P`) and run **"Show debug info"**. Copy the output and paste it into the issue.
|
||||
|
||||
If multiple devices are involved in the problem (e.g., sync between a phone and a desktop), please provide the debug info for each device. The device where the issue occurred is required; information from other devices is strongly recommended.
|
||||
|
||||
### Report from LiveSync (hatch report)
|
||||
|
||||
1. Open LiveSync settings.
|
||||
2. Go to the **Hatch** pane.
|
||||
3. Press the **Make report** button.
|
||||
|
||||
The report will be copied to your clipboard. It contains your LiveSync configuration and the remote server configuration, with credentials automatically redacted.
|
||||
|
||||
**Tip:** For large reports, consider uploading to [GitHub Gist](https://gist.github.com/) and sharing the link instead of pasting directly into the issue. This makes it easier to manage, and if you accidentally leave sensitive data in, a Gist can be deleted.
|
||||
|
||||
If you paste directly, wrap it in a `<details>` tag to keep the issue readable:
|
||||
|
||||
```
|
||||
<details>
|
||||
<summary>Report from hatch</summary>
|
||||
|
||||
```
|
||||
----remote config----
|
||||
:
|
||||
```
|
||||
</details>
|
||||
```
|
||||
|
||||
### Plug-in log
|
||||
|
||||
The plug-in log is volatile by default (not saved to disk) and shown only in the log dialogue, which can be opened by tapping the **document box icon** in the ribbon.
|
||||
|
||||
#### Enable verbose log
|
||||
|
||||
Before reproducing the issue, enable **Verbose Log** in LiveSync's **General Settings** pane. Without this, many diagnostic messages will be suppressed.
|
||||
|
||||
#### Persist the log to a file (optional)
|
||||
|
||||
If you need to capture a log across a restart, enable **"Write logs into the file"** in General Settings. Note that log files may contain sensitive information — use this option only for troubleshooting, and disable it afterwards.
|
||||
|
||||
As with the hatch report, consider uploading large logs to [GitHub Gist](https://gist.github.com/).
|
||||
|
||||
### Network log (for connection-related issues only)
|
||||
|
||||
If the issue is related to network connectivity (e.g., cannot connect to the server, authentication errors), a network log captured from browser DevTools can be very helpful. You do not need to include this for non-connection issues.
|
||||
|
||||
#### Opening DevTools
|
||||
|
||||
| Platform | Shortcut |
|
||||
|----------|----------|
|
||||
| Windows / Linux | `Ctrl + Shift + I` |
|
||||
| macOS | `Cmd + Shift + I` |
|
||||
| Android | Use [Chrome remote debugging](https://developer.chrome.com/docs/devtools/remote-debugging/) |
|
||||
| iOS | Use [Safari Web Inspector](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios) on a Mac |
|
||||
|
||||
#### What to capture
|
||||
|
||||
1. Open the **Network** pane in DevTools.
|
||||
2. Reproduce the issue.
|
||||
3. Look for requests marked in red.
|
||||
4. Capture screenshots of the **Headers**, **Payload**, and **Response** tabs for those requests.
|
||||
|
||||
**Important — redact before sharing:**
|
||||
- Headers: conceal the request URL path, Remote Address, `authority`, and `authorisation` values.
|
||||
- Payload / Response: the `_id` field contains your file paths — redact if needed.
|
||||
@@ -1,5 +1,4 @@
|
||||
# Tips and Troubleshooting
|
||||
|
||||
- [Tips and Troubleshooting](#tips-and-troubleshooting)
|
||||
- [Tips](#tips)
|
||||
- [CORS avoidance](#cors-avoidance)
|
||||
@@ -14,7 +13,12 @@
|
||||
- [Notable bugs and fixes](#notable-bugs-and-fixes)
|
||||
- [Binary files get bigger on iOS](#binary-files-get-bigger-on-ios)
|
||||
- [Some setting name has been changed](#some-setting-name-has-been-changed)
|
||||
- [FAQ](#faq)
|
||||
- [Questions and Answers](#questions-and-answers)
|
||||
- [How should I share the settings between multiple devices?](#how-should-i-share-the-settings-between-multiple-devices)
|
||||
- [What should I enter for the passphrase of Setup-URI?](#what-should-i-enter-for-the-passphrase-of-setup-uri)
|
||||
- [Why the settings of Self-hosted LiveSync itself is disabled in default?](#why-the-settings-of-self-hosted-livesync-itself-is-disabled-in-default)
|
||||
- [The plug-in says `something went wrong`.](#the-plug-in-says-something-went-wrong)
|
||||
- [A large number of files were deleted, and were synchronised!](#a-large-number-of-files-were-deleted-and-were-synchronised)
|
||||
- [Why `Use an old adapter for compatibility` is somehow enabled in my vault?](#why-use-an-old-adapter-for-compatibility-is-somehow-enabled-in-my-vault)
|
||||
- [ZIP (or any extensions) files were not synchronised. Why?](#zip-or-any-extensions-files-were-not-synchronised-why)
|
||||
- [I hope to report the issue, but you said you needs `Report`. How to make it?](#i-hope-to-report-the-issue-but-you-said-you-needs-report-how-to-make-it)
|
||||
@@ -32,6 +36,7 @@
|
||||
- [While using Cloudflare Tunnels, often Obsidian API fallback and `524` error occurs.](#while-using-cloudflare-tunnels-often-obsidian-api-fallback-and-524-error-occurs)
|
||||
- [On the mobile device, cannot synchronise on the local network!](#on-the-mobile-device-cannot-synchronise-on-the-local-network)
|
||||
- [I think that something bad happening on the vault...](#i-think-that-something-bad-happening-on-the-vault)
|
||||
- [Flag Files](#flag-files)
|
||||
- [Old tips](#old-tips)
|
||||
|
||||
<!-- - -->
|
||||
@@ -39,19 +44,21 @@
|
||||
## Tips
|
||||
|
||||
### CORS avoidance
|
||||
|
||||
If we are unable to configure CORS properly for any reason (for example, if we cannot configure non-administered network devices), we may choose to ignore CORS.
|
||||
To use the Obsidian API (also known as the Non-Native API) to bypass CORS, we can enable the toggle ``Use Request API to avoid `inevitable` CORS problem``.
|
||||
|
||||
<!-- Add **Long explanation of CORS** here for integrity -->
|
||||
|
||||
### CORS configuration with reverse proxy
|
||||
|
||||
- IMPORTANT: CouchDB handles CORS by itself. Do not process CORS on the reverse
|
||||
proxy.
|
||||
- Do not process `Option` requests on the reverse proxy!
|
||||
- Make sure `host` and `X-Forwarded-For` headers are forwarded to the CouchDB.
|
||||
- If you are using a subdirectory, make sure to handle it properly. More
|
||||
detailed information is in the
|
||||
[CouchDB documentation](https://docs.couchdb.org/en/stable/best-practices/reverse-proxies.html).
|
||||
- Do not process `Option` requests on the reverse proxy!
|
||||
- Make sure `host` and `X-Forwarded-For` headers are forwarded to the CouchDB.
|
||||
- If you are using a subdirectory, make sure to handle it properly. More
|
||||
detailed information is in the
|
||||
[CouchDB documentation](https://docs.couchdb.org/en/stable/best-practices/reverse-proxies.html).
|
||||
|
||||
Minimal configurations are as follows:
|
||||
|
||||
@@ -170,7 +177,56 @@ Probably, we can accept that.
|
||||
| Setup Wizard | Minimal Setup |
|
||||
| Check database configuration | Check and Fix database configuration |
|
||||
|
||||
## FAQ
|
||||
## Questions and Answers
|
||||
|
||||
### How should I share the settings between multiple devices?
|
||||
|
||||
- Device setup:
|
||||
- Using `Setup URI` is the most straightforward way.
|
||||
- Setting changes during use:
|
||||
- Use `Sync settings via Markdown files` on the `🔄️ Sync settings` pane.
|
||||
|
||||
### What should I enter for the passphrase of Setup-URI?
|
||||
|
||||
- Anything you like is OK. However, the recommendation is as follows:
|
||||
- Include the vault (group) information.
|
||||
- Include the date of operation.
|
||||
- Anything random for your security.
|
||||
- For example, `MyVault-20240901-r4nd0mStr1ng`.
|
||||
- Why?
|
||||
- The Setup-URI is encoded; that means it cannot indicate the actual settings. Hence, if you use the same passphrase for multiple vaults, you may accidentally mix up vaults.
|
||||
|
||||
### Why the settings of Self-hosted LiveSync itself is disabled in default?
|
||||
|
||||
Basically, if we configure all `additionalSuffixOfDatabaseName` the same, we can synchronise this file between multiple devices.
|
||||
(`additionalSuffixOfDatabaseName` should be unique in each device, not in the synchronised vaults).
|
||||
However, if we synchronise the settings of Self-hosted LiveSync itself, we may encounter some unexpected behaviours.
|
||||
For example, if a setting that 'let Self-hosted LiveSync setting be excluded' is synced, it is very unlikely that things will recover automatically after this, and there is little chance we will even notice this. Even if we change our minds and change the settings back on other devices. It could get even worse if incompatible changes are automatically reflected; everything will break.
|
||||
|
||||
### The plug-in says `something went wrong`.
|
||||
|
||||
There are many cases where this is really unclear. One possibility is that the chunk fetch did not go well.
|
||||
|
||||
1. Restarting Obsidian sometimes helps (fetch-order problem).
|
||||
2. If actually there are no chunks, please perform `Recreate missing chunks for all files` on the `🧰 Hatch` pane at the other devices. And synchronise again. (also restart Obsidian may effect).
|
||||
3. If the problem persists, please perform `Verify and repair all files` on the `🧰 Hatch` pane. If our local database and storage are not matched, we will be asked to apply which one.
|
||||
|
||||
### A large number of files were deleted, and were synchronised!
|
||||
|
||||
1. Backup everything important.
|
||||
- Your local vault.
|
||||
- Your CouchDB database (this can be done by replicating to another database).
|
||||
2. Prepare the empty vault
|
||||
3. Place `redflag.md` at the top of the vault.
|
||||
4. Apply the settings **BUT DO NOT PROCEED TO RESTORE YET**.
|
||||
- You can use `Setup URI`, QR Code, or manually apply the settings.
|
||||
5. Set `Maximum file modification time for reflected file events` in `Remediation` on the `🩹 Patches` pane.
|
||||
- If you know when the files were deleted, set the time a bit before that.
|
||||
- If not, bisecting may help us.
|
||||
6. Delete `redflag.md`.
|
||||
7. Perform `Reset Synchronisation on This Device` on the `🎛️ Maintenance` pane.
|
||||
|
||||
This mode is very fragile. Please be careful.
|
||||
|
||||
### Why `Use an old adapter for compatibility` is somehow enabled in my vault?
|
||||
|
||||
@@ -180,16 +236,16 @@ not been stable. The new adapter has better performance and has a new feature
|
||||
like purging. Therefore, we should use new adapters and current default is so.
|
||||
|
||||
However, when switching from an old adapter to a new adapter, some converting or
|
||||
local database rebuilding is required, and it takes a few time. It was a long
|
||||
local database rebuilding is required, and it takes some time. It was a long
|
||||
time ago now, but we once inconvenienced everyone in a hurry when we changed the
|
||||
format of our database. For these reasons, this toggle is automatically on if we
|
||||
have upgraded from vault which using an old adapter.
|
||||
|
||||
When you rebuild everything or fetch from the remote again, you will be asked to
|
||||
When you overwrite server data with this device's files or reset synchronisation on this device again, you will be asked to
|
||||
switch this.
|
||||
|
||||
Therefore, experienced users (especially those stable enough not to have to
|
||||
rebuild the database) may have this toggle enabled in their Vault. Please
|
||||
overwrite server data) may have this toggle enabled in their Vault. Please
|
||||
disable it when you have enough time.
|
||||
|
||||
### ZIP (or any extensions) files were not synchronised. Why?
|
||||
@@ -199,14 +255,20 @@ It depends on Obsidian detects. May toggling `Detect all extensions` of
|
||||
|
||||
### I hope to report the issue, but you said you needs `Report`. How to make it?
|
||||
|
||||
We can copy the report to the clipboard, by pressing the `Make report` button on
|
||||
the `Hatch` pane. 
|
||||
We can copy the report to the clipboard, by performing
|
||||
`Generate full report for opening the issue with debug info` command!
|
||||
|
||||
### Where can I check the log?
|
||||
|
||||
We can launch the log pane by `Show log` on the command palette. And if you have
|
||||
troubled something, please enable the `Verbose Log` on the `General Setting`
|
||||
pane.
|
||||
`Generate full report for opening the issue with debug info` command also contains
|
||||
the recent 1000 log lines, which is very helpful for debugging. Full-report is
|
||||
already set to the verbose level, so it contains all the logs without enabling the
|
||||
`Verbose Log` toggle.
|
||||
|
||||
Let me note that please be sure to remove any sensitive information before sharing the report.
|
||||
|
||||
However, the logs would not be kept so long and cleared when restarted. If you
|
||||
want to check the logs, please enable `Write logs into the file` temporarily.
|
||||
@@ -241,22 +303,24 @@ happened on other devices. This means that conflicts will happen in the past,
|
||||
after the time we have synchronised. Hence we cannot collect and delete the
|
||||
unused chunks even though if we are not currently referenced.
|
||||
|
||||
To shrink the database size, `Rebuild everything` only reliably and effectively.
|
||||
To shrink the database size, `Overwrite Server Data with This Device's Files` is the only reliable and effective way.
|
||||
But do not worry, if we have synchronised well. We have the actual and real
|
||||
files. Only it takes a bit of time and traffics.
|
||||
files. Only it takes a bit of time and traffic.
|
||||
|
||||
### How to launch the DevTools
|
||||
|
||||
#### On Desktop Devices
|
||||
|
||||
We can launch the DevTools by pressing `ctrl`+`shift`+`i` (`Command`+`shift`+`i` on Mac).
|
||||
|
||||
#### On Android
|
||||
|
||||
Please refer to [Remote debug Android devices](https://developer.chrome.com/docs/devtools/remote-debugging/).
|
||||
Once the DevTools have been launched, everything operates the same as on a PC.
|
||||
|
||||
#### On iOS, iPadOS devices
|
||||
If we have a Mac, we can inspect from Safari on the Mac. Please refer to [Inspecting iOS and iPadOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios).
|
||||
|
||||
If we have a Mac, we can inspect from Safari on the Mac. Please refer to [Inspecting iOS and iPadOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios).
|
||||
|
||||
### How can I use the DevTools?
|
||||
|
||||
@@ -302,47 +366,73 @@ self-signed certificate.
|
||||
|
||||
### I think that something bad happening on the vault...
|
||||
|
||||
Place `redflag.md` on top of the vault, and restart Obsidian. The most simple
|
||||
Place the [flag file](#flag-files) on top of the vault, and restart Obsidian. The most simple
|
||||
way is to create a new note and rename it to `redflag`. Of course, we can put it
|
||||
without Obsidian.
|
||||
|
||||
If there is `redflag.md`, Self-hosted LiveSync suspends all database and storage
|
||||
For example, if there is `redflag.md`, Self-hosted LiveSync suspends all database and storage
|
||||
processes.
|
||||
|
||||
There are some options to use `redflag.md`.
|
||||
### Scram State and Flag Files (SCRAM Warning Loop)
|
||||
|
||||
| Filename | Human-Friendly Name | Description |
|
||||
| ------------- | ------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `redflag.md` | - | Suspends all processes. |
|
||||
| `redflag2.md` | `flag_rebuild.md` | Suspends all processes, and rebuild both local and remote databases by local files. |
|
||||
| `redflag3.md` | `flag_fetch.md` | Suspends all processes, discard the local database, and fetch from the remote again. |
|
||||
The plug-in uses a **Scram state** (emergency suspension of all synchronisation processes) to prevent database corruption when severe errors or conflicts are detected. This state is often triggered or persisted by **flag files** placed at the root of the vault.
|
||||
|
||||
When fetching everything remotely or performing a rebuild, restarting Obsidian
|
||||
is performed once for safety reasons. At that time, Self-hosted LiveSync uses
|
||||
these files to determine whether the process should be carried out. (The use of
|
||||
normal markdown files is a trick to externally force cancellation in the event
|
||||
of faults in the rebuild or fetch function itself, especially on mobile
|
||||
devices). This mechanism is also used for set-up. And just for information,
|
||||
these files are also not subject to synchronisation.
|
||||
If you encounter a warning saying **"Scram detected, all sync operations are suspended per SCRAM"** or get caught in an infinite loop where the warning persists even after clicking "Resume", it is likely due to a flag file in your vault.
|
||||
|
||||
However, occasionally the deletion of files may fail. This should generally work
|
||||
normally after restarting Obsidian. (As far as I can observe).
|
||||
#### Flag Files
|
||||
|
||||
A flag file is a simple Markdown file located at the root of your vault. Its very existence is significant; it may be left blank or contain any text. These files are used so they can be easily placed or deleted from outside Obsidian (e.g., when Obsidian fails to launch).
|
||||
|
||||
| Filename | Human-Friendly Name | Description |
|
||||
| ------------- | ------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `redflag.md` | - | Suspends all processes (activates Scram). |
|
||||
| `redflag2.md` | `flag_rebuild.md` | Suspends all processes, and overwrites server data with this device's files. |
|
||||
| `redflag3.md` | `flag_fetch.md` | Suspends all processes, discards the local database, and resets synchronisation on this device. |
|
||||
|
||||
When resetting synchronisation on this device or overwriting server data, restarting Obsidian is performed once for safety reasons. At that time, Self-hosted LiveSync uses these files to determine whether the process should be carried out. (This mechanism is especially useful on mobile devices to force cancellation if the database rebuilding fails). These files are not subject to synchronisation.
|
||||
|
||||
Flag-file recovery is handled before the compatibility-review dialogue. `redflag.md` keeps start-up stopped, while a cancelled recovery or one which schedules a restart also prevents a second dialogue from competing with it in that process. When fetch-all or rebuild-all completes and start-up continues, Self-hosted LiveSync can still ask for compatibility review before ordinary synchronisation resumes. Completing a recovery does not automatically acknowledge a database or settings version; use the explicit resume action after reviewing the explanation.
|
||||
|
||||
#### How to Resolve the Scram Loop
|
||||
|
||||
If you cannot disable Scram, please follow these steps:
|
||||
1. Close Obsidian completely.
|
||||
2. Open your system's file manager and check the root directory of your vault.
|
||||
3. Locate and delete any flag files (such as `redflag.md`, `redflag2.md`, or `redflag3.md`).
|
||||
4. Launch Obsidian.
|
||||
5. Go to the settings dialogue -> **Hatch** -> **Scram Switches**, and manually toggle **Suspend file watching** and **Suspend database reflecting** to `false` (disabled) if they have not been reset automatically.
|
||||
|
||||
> [!TIP]
|
||||
> This is the reason why flag files are standard `.md` files: it allows you to manage them externally. On mobile devices, you can use system file manager applications (such as the native **Files** app on iOS/iPadOS or **Files by Google** on Android) to find and delete these files to resolve a lock, or conversely, create/place a new `redflag.md` file (or directory) at the root of your vault to force-suspend synchronisation and stop Obsidian's boot-up sequence if you need to fix a database issue.
|
||||
|
||||
### JWT Authentication Errors
|
||||
|
||||
#### DataError when configuring JWT authentication
|
||||
|
||||
If you encounter a `DataError:` with no additional information in the logs when configuring JWT authentication, this usually indicates a private key formatting issue.
|
||||
|
||||
Self-hosted LiveSync requires the private key (for ES256/ES512 algorithms) to be in the **PKCS#8 PEM** format. Standard SEC1 EC private keys (which begin with `-----BEGIN EC PRIVATE KEY-----`) will trigger this error.
|
||||
|
||||
To resolve this, convert your private key to PKCS#8 format using the following `openssl` command:
|
||||
```bash
|
||||
openssl pkcs8 -topk8 -inform PEM -nocrypt -in private.key -out pkcs8.key
|
||||
```
|
||||
Then paste the contents of `pkcs8.key` (which begins with `-----BEGIN PRIVATE KEY-----`) into the JWT Key field.
|
||||
|
||||
### Old tips
|
||||
|
||||
- Rarely, a file in the database could be corrupted. The plugin will not write
|
||||
- Rarely, a file in the database could be corrupted. The plug-in will not write
|
||||
to local storage when a file looks corrupted. If a local version of the file
|
||||
is on your device, the corruption could be fixed by editing the local file and
|
||||
synchronizing it. But if the file does not exist on any of your devices, then
|
||||
synchronising it. But if the file does not exist on any of your devices, then
|
||||
it can not be rescued. In this case, you can delete these items from the
|
||||
settings dialog.
|
||||
settings dialogue.
|
||||
- To stop the boot-up sequence (eg. for fixing problems on databases), you can
|
||||
put a `redflag.md` file (or directory) at the root of your vault. Tip for iOS:
|
||||
a redflag directory can be created at the root of the vault using the File
|
||||
application.
|
||||
- Also, with `redflag2.md` placed, we can automatically rebuild both the local
|
||||
and the remote databases during the boot-up sequence. With `redflag3.md`, we
|
||||
can discard only the local database and fetch from the remote again.
|
||||
- Also, with `redflag2.md` placed, we can automatically overwrite server data with this device's files during the boot-up sequence. With `redflag3.md`, we
|
||||
can discard only the local database and reset synchronisation on this device.
|
||||
- Q: The database is growing, how can I shrink it down? A: each of the docs is
|
||||
saved with their past 100 revisions for detecting and resolving conflicts.
|
||||
Picturing that one device has been offline for a while, and comes online
|
||||
@@ -354,7 +444,7 @@ normally after restarting Obsidian. (As far as I can observe).
|
||||
So, We have to make the database again like an enlarged git repo if you want
|
||||
to solve the root of the problem.
|
||||
- And more technical Information is in the [Technical Information](tech_info.md)
|
||||
- If you want to synchronize files without obsidian, you can use
|
||||
- If you want to synchronise files without obsidian, you can use
|
||||
[filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync).
|
||||
- WebClipper is also available on Chrome Web
|
||||
Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import sveltePlugin from "esbuild-svelte";
|
||||
import { sveltePreprocess } from "svelte-preprocess";
|
||||
import fs from "node:fs";
|
||||
@@ -20,15 +19,15 @@ const packageJson = JSON.parse(fs.readFileSync("./package.json") + "");
|
||||
const updateInfo = JSON.stringify(fs.readFileSync("./updates.md") + "");
|
||||
|
||||
const PATHS_TEST_INSTALL = process.env?.PATHS_TEST_INSTALL || "";
|
||||
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter).map(p => p.trim()).filter(p => p.length);
|
||||
if (!prod) {
|
||||
if (PATH_TEST_INSTALL) {
|
||||
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||
} else {
|
||||
console.log("Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows).");
|
||||
}
|
||||
const PATH_TEST_INSTALL = PATHS_TEST_INSTALL.split(path.delimiter)
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length);
|
||||
if (PATH_TEST_INSTALL) {
|
||||
console.log(`Built files will be copied to ${PATH_TEST_INSTALL}`);
|
||||
} else {
|
||||
console.log("Production build");
|
||||
console.log(
|
||||
"Development build: You can install the plug-in to Obsidian for testing by exporting the PATHS_TEST_INSTALL environment variable with the paths to your vault plugins directories separated by your system path delimiter (':' on Unix, ';' on Windows)."
|
||||
);
|
||||
}
|
||||
|
||||
const moduleAliasPlugin = {
|
||||
@@ -71,6 +70,38 @@ const moduleAliasPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
const removePragmaCommentsPlugin = {
|
||||
name: "remove-pragma-comments",
|
||||
setup(build) {
|
||||
const sourceRoot = `${path.resolve("src")}${path.sep}`;
|
||||
// Filter target extensions (e.g., JavaScript and TypeScript)
|
||||
build.onLoad({ filter: /\.[jt]s?$/ }, async (args) => {
|
||||
// Dependencies are already compiled and may contain comment-like text inside strings.
|
||||
// Only maintained plug-in source needs its local suppression directives removed.
|
||||
if (!args.path.startsWith(sourceRoot)) return;
|
||||
const source = await fs.promises.readFile(args.path, "utf8");
|
||||
|
||||
// Regex targeting both single-line and multi-line comments
|
||||
// This regex looks for:
|
||||
// - /* eslint ... */ (multi-line)
|
||||
// const esLintPragmaRegexBlock = /\/\*[\s\S]*?eslint[\s\S]*?\*\/|([^\\:]|^)\/\/.*eslint.*$/gm;
|
||||
// - // eslint-disable-next-line
|
||||
let cleanedSource = source;
|
||||
const tsIgnoreRegex = /\/\*\s*@ts-ignore\s*\*\/|([^\\:]|^)\/\/.*?@ts-ignore.*$/gm;
|
||||
const esLintPragmaRegexLine = /([^\\:]|^)\/\/.*?eslint-.*$/gm;
|
||||
const exps = [tsIgnoreRegex, esLintPragmaRegexLine];
|
||||
for (const exp of exps) {
|
||||
cleanedSource = cleanedSource.replace(exp, "$1");
|
||||
}
|
||||
|
||||
return {
|
||||
contents: cleanedSource,
|
||||
loader: args.path.endsWith("ts") ? "ts" : "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** @type esbuild.Plugin[] */
|
||||
const plugins = [
|
||||
{
|
||||
@@ -182,6 +213,7 @@ const context = await esbuild.context({
|
||||
preprocess: sveltePreprocess(),
|
||||
compilerOptions: { css: "injected", preserveComments: false },
|
||||
}),
|
||||
removePragmaCommentsPlugin,
|
||||
...plugins,
|
||||
],
|
||||
});
|
||||
|
||||
79
eslint.community.config.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import globals from "globals";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores([
|
||||
"node_modules",
|
||||
"**/dist",
|
||||
"build",
|
||||
"coverage",
|
||||
"main.js",
|
||||
"main_org.js",
|
||||
"pouchdb-browser.js",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"versions.json",
|
||||
// Svelte is covered by the project lint and svelte-check; the directory report currently analyses TypeScript.
|
||||
"**/*.svelte",
|
||||
"**/svelte.config.js",
|
||||
"**/*.unit.spec.ts",
|
||||
"**/test/**",
|
||||
"src/apps/_test/**",
|
||||
"src/apps/cli/testdeno/**",
|
||||
]),
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
MANIFEST_VERSION: "readonly",
|
||||
PACKAGE_VERSION: "readonly",
|
||||
UPDATE_INFO: "readonly",
|
||||
hostPlatform: "readonly",
|
||||
},
|
||||
parserOptions: {
|
||||
project: [
|
||||
"./tsconfig.json",
|
||||
"./src/apps/browser/tsconfig.json",
|
||||
"./src/apps/cli/tsconfig.json",
|
||||
"./src/apps/webapp/tsconfig.json",
|
||||
"./src/apps/webpeer/tsconfig.app.json",
|
||||
"./src/apps/webpeer/tsconfig.node.json",
|
||||
],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
// The directory review reports console usage as guidance rather than a release blocker.
|
||||
"obsidianmd/rule-custom-message": "off",
|
||||
"no-console": "warn",
|
||||
"obsidianmd/no-unsupported-api": "error",
|
||||
// Keep legacy type-safety debt visible while reserving errors for directory-review blockers.
|
||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-return": "warn",
|
||||
"@typescript-eslint/no-base-to-string": "warn",
|
||||
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/apps/**/*.{ts,js,mjs}"],
|
||||
rules: {
|
||||
// These applications are inspected by the directory review but accept external command and file data.
|
||||
"@typescript-eslint/restrict-template-expressions": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/apps/browser/**/*.ts", "src/apps/webapp/**/*.ts"],
|
||||
rules: {
|
||||
"obsidianmd/prefer-active-doc": "off",
|
||||
},
|
||||
}
|
||||
);
|
||||
142
eslint.config.common.mjs
Normal file
@@ -0,0 +1,142 @@
|
||||
const restrictedGlobalsOptions = [
|
||||
{
|
||||
name: "app",
|
||||
message: "Avoid using the global app object. Instead use the reference provided by your plugin instance.",
|
||||
},
|
||||
"warn",
|
||||
{
|
||||
name: "fetch",
|
||||
message: "Use the built-in `requestUrl` function instead of `fetch` for network requests in Obsidian.",
|
||||
},
|
||||
{
|
||||
name: "localStorage",
|
||||
message:
|
||||
"Prefer `App#saveLocalStorage` / `App#loadLocalStorage` functions to write / read localStorage data that's unique to a vault.",
|
||||
},
|
||||
];
|
||||
const restrictedImportsOptions = [
|
||||
{
|
||||
name: "axios",
|
||||
message: "Use the built-in `requestUrl` function instead of `axios`.",
|
||||
},
|
||||
{
|
||||
name: "superagent",
|
||||
message: "Use the built-in `requestUrl` function instead of `superagent`.",
|
||||
},
|
||||
{
|
||||
name: "got",
|
||||
message: "Use the built-in `requestUrl` function instead of `got`.",
|
||||
},
|
||||
{
|
||||
name: "ofetch",
|
||||
message: "Use the built-in `requestUrl` function instead of `ofetch`.",
|
||||
},
|
||||
{
|
||||
name: "ky",
|
||||
message: "Use the built-in `requestUrl` function instead of `ky`.",
|
||||
},
|
||||
{
|
||||
name: "node-fetch",
|
||||
message: "Use the built-in `requestUrl` function instead of `node-fetch`.",
|
||||
},
|
||||
{
|
||||
name: "moment",
|
||||
message: "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.",
|
||||
},
|
||||
];
|
||||
|
||||
const warnWhileDev = "off";
|
||||
|
||||
/**
|
||||
* @type {import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const baseRules = {
|
||||
// -- Base rules (turned off in favour of TS specific versions or explicitly disabled).
|
||||
"no-unused-vars": "off",
|
||||
"no-unused-labels": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"require-await": "off",
|
||||
// -- TypeScript specific rules (Gradual adoption of stricter rules, currently set to 'warn' for a while).
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-redundant-type-constituents": "warn",
|
||||
// -- TypeScript specific rules
|
||||
// @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any:
|
||||
// This project contains a lot of library-sh code where the use of `any` is often necessary and justified.
|
||||
// Rules is now set to 'off' for a while.
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
// -- Reasonable rules.
|
||||
"@typescript-eslint/no-deprecated": warnWhileDev,
|
||||
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
|
||||
// -- General rules
|
||||
"no-async-promise-executor": warnWhileDev,
|
||||
"no-constant-condition": ["error", { checkLoops: false }],
|
||||
// -- Disabled rules
|
||||
// no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time.
|
||||
"no-undef": "off",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const obsidianRules = {
|
||||
// -- Obsidian rules
|
||||
// Runtime fallbacks must use requireApiVersion guards recognised by the official rule.
|
||||
"obsidianmd/no-unsupported-api": "error",
|
||||
|
||||
// -- Plugin specific overrides
|
||||
"obsidianmd/rule-custom-message": "off",
|
||||
"obsidianmd/ui/sentence-case": "off",
|
||||
"obsidianmd/no-plugin-as-component": "warn",
|
||||
|
||||
// -- Temporary overrides for migration
|
||||
"obsidianmd/no-static-styles-assignment": "off",
|
||||
};
|
||||
/**
|
||||
* @type {(base:string) => import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const ImportAliasRules = (base) => ({
|
||||
"@dword-design/import-alias/prefer-alias": [
|
||||
"error",
|
||||
{
|
||||
aliasForSubpaths: true,
|
||||
alias: {
|
||||
"@": `${base}/src`,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
/**
|
||||
* @type {import("eslint").Linter.RulesRecord}
|
||||
*/
|
||||
export const CommunityReviewRecommendedRules = {
|
||||
"no-unused-vars": "off",
|
||||
"no-prototype-bultins": "off",
|
||||
"no-self-compare": "warn",
|
||||
"no-eval": "error",
|
||||
"no-implied-eval": "error",
|
||||
"prefer-const": "off",
|
||||
"no-implicit-globals": "error",
|
||||
"no-console": "off", // overridden by obsidianmd/rule-custom-message
|
||||
"no-restricted-globals": ["error", ...restrictedGlobalsOptions],
|
||||
"no-restricted-imports": ["error", ...restrictedImportsOptions],
|
||||
"no-alert": "error",
|
||||
"no-undef": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
|
||||
// "import/no-nodejs-modules": "off",
|
||||
// "import/no-extraneous-dependencies": "error",
|
||||
};
|
||||
@@ -1,99 +1,121 @@
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
import _import from "eslint-plugin-import";
|
||||
import { fixupPluginRules } from "@eslint/compat";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import globals from "globals";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import importAlias from "@dword-design/eslint-plugin-import-alias";
|
||||
import { baseRules, ImportAliasRules, obsidianRules } from "./eslint.config.common.mjs";
|
||||
const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled.
|
||||
const lintProjects = [
|
||||
"./tsconfig.json",
|
||||
"./src/apps/browser/tsconfig.json",
|
||||
"./src/apps/cli/tsconfig.json",
|
||||
"./src/apps/webapp/tsconfig.json",
|
||||
"./src/apps/webpeer/tsconfig.app.json",
|
||||
"./src/apps/webpeer/tsconfig.node.json",
|
||||
];
|
||||
export default defineConfig([
|
||||
globalIgnores([
|
||||
// Build outputs and legacy files
|
||||
"**/build",
|
||||
"**/dist/**",
|
||||
"coverage",
|
||||
"**/main.js",
|
||||
"main_org.js",
|
||||
"pouchdb-browser.js",
|
||||
"version-bump.mjs",
|
||||
"package.json",
|
||||
"**/*.json",
|
||||
"**/.eslintrc.js.bak",
|
||||
// Files from linked dependencies (those files should not exist for most people).
|
||||
"modules/octagonal-wheels/dist",
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
// Sub-project tooling with its own environment
|
||||
"utils",
|
||||
|
||||
export default [
|
||||
// Config files and build scripts
|
||||
"**/jest.config.js",
|
||||
"**/rollup.config.js",
|
||||
"**/esbuild.config.mjs",
|
||||
"**/terser.*.mjs",
|
||||
".prettierrc.*.mjs",
|
||||
".prettierrc.mjs",
|
||||
"*.config.mjs",
|
||||
"vite.*",
|
||||
"vitest.*",
|
||||
// Testing files (Simplified patterns)
|
||||
"test/**",
|
||||
"**/test/**",
|
||||
"src/apps/_test/**",
|
||||
"src/apps/cli/testdeno/**",
|
||||
"**/*.test.ts",
|
||||
"**/*.unit.spec.ts",
|
||||
"**/test.ts",
|
||||
"**/tests.ts",
|
||||
]),
|
||||
...sveltePlugin.configs["flat/base"],
|
||||
...obsidianmd.configs.recommended,
|
||||
importAlias.configs.recommended,
|
||||
{
|
||||
ignores: [
|
||||
"**/node_modules/*",
|
||||
"**/jest.config.js",
|
||||
"src/lib/coverage",
|
||||
"src/lib/browsertest",
|
||||
"**/test.ts",
|
||||
"**/tests.ts",
|
||||
"**/**test.ts",
|
||||
"**/**.test.ts",
|
||||
"**/esbuild.*.mjs",
|
||||
"**/terser.*.mjs",
|
||||
"**/node_modules",
|
||||
"**/build",
|
||||
"**/.eslintrc.js.bak",
|
||||
"src/lib/src/patches/pouchdb-utils",
|
||||
"**/esbuild.config.mjs",
|
||||
"**/rollup.config.js",
|
||||
"modules/octagonal-wheels/rollup.config.js",
|
||||
"modules/octagonal-wheels/dist/**/*",
|
||||
"src/lib/test",
|
||||
"src/lib/src/cli",
|
||||
"**/main.js",
|
||||
"src/lib/apps/webpeer/*"
|
||||
],
|
||||
},
|
||||
...compat.extends(
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
svelte,
|
||||
import: fixupPluginRules(_import),
|
||||
},
|
||||
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
project: ["tsconfig.json"],
|
||||
project: lintProjects,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: false,
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
args: "none",
|
||||
},
|
||||
],
|
||||
|
||||
"no-unused-labels": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"require-await": "error",
|
||||
"@typescript-eslint/require-await": "warn",
|
||||
"@typescript-eslint/no-misused-promises": "warn",
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"no-async-promise-executor": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
|
||||
"no-constant-condition": [
|
||||
"error",
|
||||
{
|
||||
checkLoops: false,
|
||||
},
|
||||
],
|
||||
...baseRules,
|
||||
...obsidianRules,
|
||||
// -- Project specific rules
|
||||
...ImportAliasRules("."),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
files: ["**/*.svelte"],
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, PouchDB: "readonly" },
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: lintProjects,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
extraFileExtensions: [".svelte"],
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// no-unused-vars:
|
||||
// Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time.
|
||||
// it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],]
|
||||
"no-unused-vars": "off",
|
||||
...obsidianRules,
|
||||
"obsidianmd/no-plugin-as-component": "off",
|
||||
...ImportAliasRules("."),
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/apps/**/*.ts"],
|
||||
rules: {
|
||||
// Platform adapters implement asynchronous contracts even when a local operation is synchronous.
|
||||
"@typescript-eslint/require-await": "off",
|
||||
// Keep existing application code visible without making gradual type tightening a release blocker.
|
||||
"@typescript-eslint/no-base-to-string": "warn",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
|
||||
"@typescript-eslint/restrict-template-expressions": "warn",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/apps/browser/**/*.{ts,svelte}", "src/apps/webapp/**/*.ts"],
|
||||
rules: {
|
||||
// Browser applications use the DOM rather than Obsidian's DOM extensions.
|
||||
"obsidianmd/prefer-create-el": "off",
|
||||
"obsidianmd/prefer-active-doc": "off",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
BIN
images/hidden-file-sync/guide-hidden-file-advanced-features.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
images/hidden-file-sync/guide-hidden-file-enable.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
images/hidden-file-sync/guide-hidden-file-selector.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 34 KiB |