Compare commits

...

659 Commits

Author SHA1 Message Date
vorotamoroz
2e999b9426 docs: align package boundary proof with published artefact 2026-07-17 18:20:29 +00:00
vorotamoroz
7b7801b029 test: verify the packaged Commonlib boundary 2026-07-17 17:50:56 +00:00
vorotamoroz
f80137e364 docs: record package boundary verification 2026-07-17 16:08:49 +00:00
vorotamoroz
97964fddf1 refactor: inject CLI I/O and diagnostics 2026-07-17 16:01:48 +00:00
vorotamoroz
1b4a0d76dd fix: remove remaining Svelte state warnings 2026-07-17 15:37:20 +00:00
vorotamoroz
f6b03dc811 refactor: make Svelte prop lifetimes explicit 2026-07-17 15:04:01 +00:00
vorotamoroz
6e00b8195a fix: remove ineffective trash setting toggle 2026-07-17 14:55:30 +00:00
vorotamoroz
326bf77183 refactor: align platform APIs with community review 2026-07-17 14:47:27 +00:00
vorotamoroz
298738fc67 fix: keep packaged dialogs inside mobile safe areas 2026-07-17 13:28:58 +00:00
vorotamoroz
d5754f1f54 docs: keep package proof evidence durable 2026-07-17 11:58:08 +00:00
vorotamoroz
14da32cbab chore: refresh Commonlib package proof 2026-07-17 11:32:41 +00:00
vorotamoroz
a721d3b602 refactor: consume Commonlib as a package 2026-07-17 11:13:16 +00:00
vorotamoroz
6475d32769 docs: define common library package boundary 2026-07-17 02:36:21 +00:00
vorotamoroz
e114f66fb2 Harden release workflow validation and retries 2026-07-17 09:50:27 +09:00
vorotamoroz
e31e761c5a Merge pull request #1028 from vrtmrz/0_25_83
Releasing 0.25.83
2026-07-16 22:41:26 +09:00
vorotamoroz
977d50566d docs: finalise 0.25.83 release notes 2026-07-16 13:01:32 +00:00
github-actions[bot]
85fc62d61c Releasing 0.25.83 2026-07-16 12:28:26 +00:00
Ouyang Xingyuan
8e0d5581f7 test: cover the non-bulk newest resolution entry point
Reason:
- The non-bulk notice regression test should exercise the real resolve-by-newest call path.

Changes:
- Resolve a mocked conflict through `_anyResolveConflictByNewest` and assert that its success notice remains visible.
2026-07-16 21:18:16 +09:00
Ouyang Xingyuan
32e992db93 fix: scope merge notice suppression to bulk resolution
Reason:
- Bulk newest resolution should not display one success notice per file, while non-bulk resolution must retain its existing notice.

Changes:
- Pass an explicit notice flag only through the bulk resolution path.
- Cover bulk suppression, non-bulk notices, and ten-file progress updates.
2026-07-16 21:18:16 +09:00
Ouyang Xingyuan
3103dc4f52 fix: suppress bulk conflict resolution notice spam
Bulk newest-revision conflict resolution created one notice per resolved file
and updated progress for nine out of every ten files.

Keep per-file bulk resolution success messages in the log without displaying
notices, update notice-level progress once every ten files, and cover both
behaviours with unit tests.
2026-07-16 21:18:16 +09:00
vorotamoroz
943b2a3bd4 Point at merged commonlib rename guard 2026-07-16 20:48:12 +09:00
vorotamoroz
69f4879589 test: cover case-only file renames in Obsidian 2026-07-16 20:48:12 +09:00
vorotamoroz
918d3b5547 fix: guard case-only rename recovery 2026-07-16 20:48:12 +09:00
metrovoc
869a893c19 fix: synchronise case-only file renames safely 2026-07-16 20:48:12 +09:00
vorotamoroz
ba7ea27d0c feat: distinguish remote operation and request activity 2026-07-16 20:13:10 +09:00
vorotamoroz
9f9758bc12 fix: align chunk reads with remote delivery
Consume commonlib's delivery lifecycle, classify finite replication entry points, and document why the five-minute inactivity fuse is only a leak safety valve. Record the remote-activity counter correction and preserve continuous replication's unbounded live channel.
2026-07-16 20:13:10 +09:00
vorotamoroz
f54d162ef9 Merge pull request #1024 from vrtmrz/0_25_82
Releasing 0.25.82
2026-07-16 00:07:44 +09:00
vorotamoroz
9d70ac8064 Synchronise workspace lock dependencies 2026-07-15 14:13:43 +00:00
vorotamoroz
f44dc76087 Polish 0.25.82 release notes 2026-07-15 14:03:15 +00:00
github-actions[bot]
962589a40f Releasing 0.25.82 2026-07-15 14:00:29 +00:00
vorotamoroz
d8bbab6bed Fix Deno setup for release preparation 2026-07-15 22:55:19 +09:00
vorotamoroz
b475ea64e2 ci: publish CLI images for AMD64 and ARM64 2026-07-15 22:12:07 +09:00
vorotamoroz
908acc4a84 test: add CLI-to-Obsidian compatibility E2E 2026-07-15 22:12:07 +09:00
vorotamoroz
31e9186930 Protect bounded remote activity across app lifecycle 2026-07-15 20:53:27 +09:00
vorotamoroz
4cbccf85b4 Merge pull request #1019 from apple-ouyang/codex/refresh-security-seed-before-sync
Fix stale Security Seed after remote rebuild
2026-07-15 20:35:23 +09:00
Ouyang Xingyuan
45657ba39c fix: refresh Security Seed before replication
Reason:
- A client that remains open during a remote database rebuild can retain the previous Security Seed and upload documents encrypted with the wrong key.

Changes:
- Fetch the remote Security Seed at every replication preflight instead of reusing the process cache.
- Add a regression test and an unreleased change note for issue #1018.
2026-07-14 18:15:40 +08:00
vorotamoroz
056d89aa4d Merge pull request #1013 from vrtmrz/hold-release-merge-for-brat-validation
Hold release merges until BRAT validation
2026-07-14 08:52:41 +09:00
vorotamoroz
804103693f Hold release PR merge until BRAT validation 2026-07-14 08:45:23 +09:00
vorotamoroz
eb3abc71b4 Merge pull request #1012 from vrtmrz/0_25_81
Releasing 0.25.81
2026-07-14 08:21:47 +09:00
vorotamoroz
77eacade10 Generate fallback types during releases 2026-07-14 03:15:42 +09:00
vorotamoroz
d404f2ae54 Add the 0.25.81 release note 2026-07-14 03:04:10 +09:00
github-actions[bot]
a7c488f377 Releasing 0.25.81 2026-07-13 18:02:25 +00:00
vorotamoroz
0f75cd92c0 Automate and validate the release flow (#998)
* improve release flow

* limit permission

* Test and harden the release workflow

* Keep workspace lockfile versions aligned

* Run release regression tests in CI
2026-07-14 03:01:22 +09:00
vorotamoroz
a41c829c10 fix: preserve U+FEFF at chunk boundaries (#1010) 2026-07-14 02:15:57 +09:00
vorotamoroz
ff12778301 Fix strfry tmpfs permissions in CI (#1011) 2026-07-14 02:12:54 +09:00
vorotamoroz
e38084b901 Use cached ignore matchers from commonlib (#1008) 2026-07-14 00:29:11 +09:00
vorotamoroz
9265a74578 Merge pull request #1007 from apple-ouyang/codex/cache-cli-ignore-matchers
Compile CLI ignore patterns once
2026-07-13 13:26:21 +09:00
Ouyang Xingyuan
f28d886603 perf(cli): compile ignore patterns once
Long-running CLI processes rebuilt the same minimatch ASTs for every checked path.

Compile patterns while loading ignore rules and reuse them until the rules are reloaded.

Refs #1006
2026-07-13 10:47:35 +08:00
vorotamoroz
9c375bd6fd Deploy QR aggregator with GitHub Pages 2026-07-12 21:06:56 +09:00
vorotamoroz
42a333a571 Record P2P validation infrastructure 2026-07-12 20:40:38 +09:00
vorotamoroz
a6a5f7af53 Use merged storage capability contracts 2026-07-12 20:40:38 +09:00
vorotamoroz
4df87cc96a Explain portable storage path policy 2026-07-12 20:40:38 +09:00
vorotamoroz
d16e66c67a Track unreleased storage contract changes 2026-07-12 20:40:38 +09:00
vorotamoroz
57e26f1079 Enforce rooted storage path contracts 2026-07-12 20:40:38 +09:00
vorotamoroz
dbb8d2be22 test: establish storage adapter contracts 2026-07-12 20:40:38 +09:00
vorotamoroz
a60932d9e4 Strengthen CLI benchmark verification 2026-07-12 20:02:29 +09:00
vorotamoroz
fd3e8416b7 Move CLI E2E runner into CLI tests 2026-07-12 20:02:29 +09:00
vorotamoroz
b9a27ffef9 Run CLI P2P E2E in Compose CI 2026-07-12 20:02:29 +09:00
vorotamoroz
af72cac4e3 Add scoped CLI benchmark metadata 2026-07-12 20:02:29 +09:00
vorotamoroz
7d26f0ae35 Add P2P signalling-only netem benchmark 2026-07-12 20:02:29 +09:00
vorotamoroz
008a5ace06 Add P2P split-container netem stress fixture 2026-07-12 20:02:29 +09:00
vorotamoroz
b1955703b9 Record P2P selected candidate paths 2026-07-12 20:02:29 +09:00
vorotamoroz
39065e15e3 Add CouchDB netem shim benchmark 2026-07-12 20:02:29 +09:00
vorotamoroz
7b480d4c1d Add netem smoke fixture for benchmark simulation 2026-07-12 20:02:29 +09:00
vorotamoroz
33cd6a3b51 Document CLI benchmark comparison model 2026-07-12 20:02:29 +09:00
vorotamoroz
870ac93cac Lower benchmark relay file descriptor limit 2026-07-12 20:02:29 +09:00
vorotamoroz
2fcdbbc4da Stabilise CLI P2P smoke readiness 2026-07-12 20:02:29 +09:00
vorotamoroz
18e8b239bf Run CLI network smoke on pull requests 2026-07-12 20:02:29 +09:00
vorotamoroz
ef655a2976 Add Compose-based CLI network benchmarks 2026-07-12 20:02:29 +09:00
vorotamoroz
78eb6ec3bc build: use stable Obsidian test session 2026-07-12 15:10:49 +09:00
vorotamoroz
c0780d19ba build: update Obsidian test session preview 2026-07-12 15:10:49 +09:00
vorotamoroz
5b61ea5009 test: share Obsidian E2E session infrastructure 2026-07-12 15:10:49 +09:00
vorotamoroz
6a9918677f Merge pull request #996 from vrtmrz/0_25_80
releasing 0.25.80
2026-07-07 21:21:34 +09:00
vorotamoroz
2d42b92a89 bump 2026-07-07 12:15:58 +00:00
vorotamoroz
a2b794c520 Merge pull request #995 from vrtmrz/fix_conflict_issues
Fix conflict issues
2026-07-07 21:13:00 +09:00
vorotamoroz
6f9446f447 Merge remote-tracking branch 'origin/main' into fix_conflict_issues 2026-07-07 12:11:38 +00:00
vorotamoroz
3f9cd67b1c Merge pull request #992 from vrtmrz/fix_989
fix: enable hidden file sync before overwrite setup
2026-07-07 21:10:09 +09:00
vorotamoroz
dbcbf2c5ca fix: document safer conflict preservation 2026-07-07 12:05:40 +00:00
vorotamoroz
eed0fca8d3 fix: preserve ambiguous conflict candidates 2026-07-07 12:00:24 +00:00
vorotamoroz
05e031b90b update submodule pointer 2026-07-07 11:31:29 +00:00
vorotamoroz
bec767c13f add notes 2026-07-07 11:30:36 +00:00
vorotamoroz
8046a777af fix: refine conflict merge policy 2026-07-07 11:29:51 +00:00
vorotamoroz
0c58b0c513 test: reproduce conflict issue reports 2026-07-07 11:07:09 +00:00
vorotamoroz
b66e227a02 update doc
update submodule pointer
2026-07-07 10:54:49 +00:00
vorotamoroz
af6df84b5d fix: enable hidden file sync before overwrite setup 2026-07-03 05:09:55 +00:00
vorotamoroz
06bba49aad Merge pull request #985 from vrtmrz/test/obsidian-real-e2e
Add real obsidian e2e
2026-07-01 19:18:58 +09:00
vorotamoroz
f97888572b (build): use deno command for type generation 2026-06-30 09:37:54 +00:00
vorotamoroz
9d9b512f3f Merge remote-tracking branch 'origin/main' into test/obsidian-real-e2e 2026-06-30 09:18:08 +00:00
vorotamoroz
54e6a761e5 (test): add local Obsidian E2E suite 2026-06-30 08:36:29 +00:00
vorotamoroz
d37ed51660 Merge pull request #984 from vrtmrz/0_25_79
bump and releasing 0.25.79
2026-06-30 01:06:43 +09:00
vorotamoroz
9fbdee47a6 Merge branch 'main' into 0_25_79 2026-06-29 12:30:28 +01:00
vorotamoroz
b568c4b003 bump and releasing 0.25.79 2026-06-29 12:19:56 +01:00
vorotamoroz
505e3c5e8c Merge pull request #983 from vrtmrz/stream-inbox-patch
(refactor): genReplication to use StreamInbox and improve event handling
2026-06-29 20:02:59 +09:00
vorotamoroz
75cb9ece58 update the pointer 2026-06-29 12:02:33 +01:00
vorotamoroz
05877834dc update the pointer 2026-06-29 12:01:08 +01:00
vorotamoroz
dae565129a Merge pull request #978 from apple-ouyang/codex/fast-fetch-interruptions
Remember Simple Fetch choices during interrupted initialisation
2026-06-29 19:56:17 +09:00
vorotamoroz
701520fe15 Merge pull request #981 from apple-ouyang/codex/maintenance-prereq-dialog
Ask before applying maintenance prerequisite settings
2026-06-29 19:52:56 +09:00
vorotamoroz
e662e99641 Merge pull request #976 from kiraventom/main
Add nginx into table of contents
2026-06-29 19:42:24 +09:00
vorotamoroz
96396f77f5 (refactor): genReplication to use StreamInbox and improve event handling 2026-06-29 09:11:20 +00:00
vorotamoroz
e916683b8d (test): customisation sync 2 2026-06-26 12:16:39 +00:00
vorotamoroz
7028a0857e (test): customisation sync 2026-06-26 12:02:29 +00:00
vorotamoroz
150f0700b0 (test): hidden file sync roundtrip and misc 2026-06-26 11:39:02 +00:00
vorotamoroz
36590ee762 (test): improve e2e CouchDB 2026-06-26 10:55:06 +00:00
vorotamoroz
4a33236c8f (test): improve e2e session helper 2026-06-26 10:46:45 +00:00
vorotamoroz
be23fa51a1 (test): the E2E test on the real-Obsidian 2026-06-26 10:33:51 +00:00
Ouyang Xingyuan
7e59010824 Ask before applying maintenance prerequisite settings
Local database maintenance actions require local chunk revisions and fully replicated chunks, but previously failed with a notice when those settings were not already configured.

This adds a small prerequisite confirmation helper for the maintenance commands. When required settings are missing, the user can apply them and continue from the action they already started, or cancel without changing settings.

Covers both required maintenance settings:
- Compute revisions for chunks enabled
- Fetch chunks on demand disabled

Fixes #980
2026-06-25 14:41:14 +08:00
vorotamoroz
8e24739b96 Tag tidying 2026-06-24 05:55:05 +01:00
Ouyang Xingyuan
b2ea810dde Update commonlib for Fast Fetch interruption retries
The plug-in needs the commonlib change that retries transient Fast Fetch failures from the latest checkpoint.

This updates the src/lib submodule pointer to the commonlib commit with retry and AbortError handling.
2026-06-24 10:22:03 +08:00
Ouyang Xingyuan
bf8da52348 Remember pending Simple Fetch choices
Simple Fetch asked the same mode and deletion choices again after a failed or interrupted pending Fetch All run.

This stores the selected quick-flow choices in local small config until the operation completes, is cancelled, or is finalised.
2026-06-24 10:12:51 +08:00
Temirkanov Alikhan
31d01f039c Add nginx into table of contents 2026-06-23 17:59:34 +03:00
vorotamoroz
2d5b5da904 Merge pull request #974 from vrtmrz/0_25_78
Releasing v0.25.78
2026-06-23 20:19:31 +09:00
vorotamoroz
6d4ae6dccb bump 2026-06-23 12:10:05 +01:00
vorotamoroz
37ccbf249c Merge pull request #970 from vrtmrz/refactor_journalsync
Overhaul ObjectStorage Replicator
2026-06-23 20:02:02 +09:00
vorotamoroz
6e14f0210a Track the subrepo 2026-06-23 12:01:27 +01:00
vorotamoroz
c626fb1783 Update submodule pointer,
Update release note
2026-06-23 11:58:57 +01:00
vorotamoroz
23af915604 Merge pull request #973 from apple-ouyang/codex/fast-fetch-resume
Update commonlib for resumable Fast Fetch
2026-06-23 19:18:43 +09:00
Ouyang Xingyuan
2257ab7647 Update commonlib for resumable Fast Fetch
Point src/lib at the commonlib commit that makes Fast Fetch resume from a persisted CouchDB sequence.
2026-06-23 17:50:57 +08:00
vorotamoroz
08bfec204f Merge pull request #971 from kiraventom/main
Add a section on reverse-proxying via Nginx
2026-06-23 18:46:46 +09:00
vorotamoroz
916e24e21e Fix for CI 2026-06-23 10:41:20 +01:00
vorotamoroz
3ee2c401d4 Correcting sloppy wording on the design doc.
Creating and revising tests
Amending the vagueness in/arises from the draft
2026-06-23 10:27:28 +01:00
vorotamoroz
78d9d66438 fix scripts to make sure building types 2026-06-23 09:38:48 +01:00
vorotamoroz
7298aca2e9 prettified 2026-06-23 09:36:40 +01:00
vorotamoroz
431a845f18 fixed
- Journal upload issue
- Types and Setting Tab issue
2026-06-23 09:36:32 +01:00
vorotamoroz
877d1b09f4 refine 2026-06-23 01:17:09 +09:00
Temirkanov Alikhan
34caf19e20 Add a section on reverse-proxying via Nginx 2026-06-22 12:29:24 +03:00
vorotamoroz
9004c194b3 Overhaul ObjectStorage Replicator 2026-06-22 13:38:31 +09:00
vorotamoroz
c57b8a5f4e Write the minimal CoC. Welcome your feedback! 2026-06-19 12:13:23 +01:00
vorotamoroz
f90a83ef1e Update docs 2026-06-19 11:33:38 +01:00
vorotamoroz
62f44e38c0 Releasing 0.25.77 (#968)
Squash commits
2026-06-19 17:45:37 +09:00
vorotamoroz
c6c4044f3c Merge pull request #965 from vrtmrz/refactor_structure
Refactor structure (2)
2026-06-18 10:44:41 +09:00
vorotamoroz
2bd7832d71 Merge pull request #964 from mugenrei/main
Merge upstream
2026-06-18 10:43:08 +09:00
vorotamoroz
72033472f3 add dependency explicitly 2026-06-17 10:36:38 +01:00
vorotamoroz
93dc03e86f remove unused dependencies, update some dependencies 2026-06-17 10:05:45 +01:00
vorotamoroz
dae8443fe8 update more deps 2026-06-17 06:35:14 +01:00
vorotamoroz
88a8bcbd5a barrel node specific modules to summarise the warnings 2026-06-17 06:15:25 +01:00
vorotamoroz
4a5283543d for automatic review 2026-06-17 06:10:30 +01:00
vorotamoroz
7895336189 revert deno test 2026-06-17 06:08:40 +01:00
vorotamoroz
2d5cdccf7d for automatic review 2026-06-17 05:51:01 +01:00
mugenrei
28cdf40fcd Merge branch 'main' into main 2026-06-17 01:44:26 -03:00
vorotamoroz
497fd04081 fix global references 2026-06-17 05:29:45 +01:00
vorotamoroz
38142aaf1c Merge pull request #963 from vrtmrz/refactor_structure
Refactor structure
2026-06-17 13:06:30 +09:00
vorotamoroz
ae9c46f8f0 Fix import paths 2026-06-17 04:39:39 +01:00
vorotamoroz
dcd10cd690 Fix dependency management 2026-06-17 04:24:55 +01:00
vorotamoroz
5a35b71339 define workspace 2026-06-17 04:01:45 +01:00
vorotamoroz
18a59219f7 add ignore paths 2026-06-15 12:25:50 +01:00
vorotamoroz
c9095738e6 (chore): limit tags 2026-06-15 12:21:58 +01:00
vorotamoroz
bed415fc6b (chore): add missing release date 2026-06-15 12:21:11 +01:00
vorotamoroz
4eeb93b9e4 Merge pull request #961 from vrtmrz/0_25_76
Releasing 0.25.76
2026-06-15 20:18:37 +09:00
vorotamoroz
0fc233686b prettify and bump 2026-06-15 11:54:41 +01:00
vorotamoroz
def1d297f5 Merge pull request #958 from vrtmrz/fix_875
Fix S3 client with custom headers and strict server
2026-06-15 19:41:31 +09:00
vorotamoroz
e7cf2a6fba Merge branch 'main' into fix_875 2026-06-15 19:41:18 +09:00
vorotamoroz
2da2fd7671 Merge pull request #960 from vrtmrz/fix_956
Fix configuration serialisation
2026-06-15 19:40:37 +09:00
vorotamoroz
d5175969e7 Update notes. 2026-06-15 11:39:38 +01:00
vorotamoroz
46f9630999 track merged submodule 2026-06-15 11:39:13 +01:00
vorotamoroz
75adcf0ff0 track merged submodule 2026-06-15 11:38:50 +01:00
vorotamoroz
866dd49ba3 ### Fixed
- No longer connection information of the P2P synchronisation is broken on the specific platform (#956).
2026-06-15 11:34:25 +01:00
vorotamoroz
d0a84d07aa (chore) typo 2026-06-15 10:43:27 +01:00
vorotamoroz
089a4c0f8b ### Fixed
- Now S3 connection with custom headers works properly.
2026-06-15 10:41:26 +01:00
vorotamoroz
1a1f816872 Merge pull request #957 from vrtmrz/v0_25_75
Releasing v0.25.75
2026-06-13 12:05:16 +09:00
vorotamoroz
9d86c2828b Update dependency, and bump 2026-06-13 11:58:40 +09:00
vorotamoroz
3b6d3beaa7 Merge pull request #955 from vrtmrz/fix_953
Fix an issue where using fast synchronisation caused a TypeError in some environment
2026-06-13 11:21:19 +09:00
vorotamoroz
bb75b6ead8 Merge remote-tracking branch 'origin/main' into fix_953 2026-06-13 11:19:22 +09:00
vorotamoroz
fccb2304f6 update submodule pointer 2026-06-13 11:18:03 +09:00
vorotamoroz
f00ef5eaae update submodule pointer 2026-06-13 11:15:03 +09:00
vorotamoroz
4e7ee760de Merge pull request #949 from AutoraLabs/feat/keep-livesync-active-in-background
feat: opt-in desktop setting to keep replication active in the background
2026-06-13 11:14:04 +09:00
vorotamoroz
c4faade30c Update submodule pointer 2026-06-13 11:13:35 +09:00
vorotamoroz
295dc1392a ### Fixed
- Fixed an issue where using fast synchronisation caused a TypeError in some environments (#953).
2026-06-12 11:33:07 +01:00
Miguel Ferreira
445a8c747c chore(submodule): bump src/lib to commonlib main (#51 merged as e98d929)
vrtmrz/livesync-commonlib#51 is merged into commonlib main as e98d929. Repoint
the gitlink from the PR branch commit to that merge commit so this PR builds
against upstream main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:45:40 +01:00
Miguel Ferreira
292a6b9e1e refactor: detect platform via APIService.isMobile() instead of Platform.isDesktopApp
Address the maintainer review on #949: determine the platform through the
plugin's own service layer (services.API.isMobile()) rather than Obsidian's
Platform API directly, matching the existing call in ObsidianLiveSyncSettingTab.
Applies to both PR-introduced sites: the runtime guard (ModuleObsidianEvents)
and the settings-pane toggle (PaneSyncSettings).

The TFile import becomes type-only so deps.ts is no longer pulled at runtime;
the unit test drives the platform through the services.API.isMobile() mock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:45:40 +01:00
vorotamoroz
0e04e7d31d Merge pull request #951 from vrtmrz/feat_docker_ci_build
feat: Docker CI workflow to enhance image tagging and push logic base…
2026-06-09 18:41:26 +09:00
vorotamoroz
4cf4acf7e9 feat: Docker CI workflow to enhance image tagging and push logic based on branch and event type 2026-06-09 09:00:43 +00:00
vorotamoroz
0856693aac Add instruction 2026-06-09 02:26:07 +01:00
vorotamoroz
39d78a04ac Enhance remote database management and add --vault option
Added new commands for remote database management and introduced --vault option for daemon and mirror commands.
2026-06-08 21:12:42 +09:00
vorotamoroz
0b8d73ccd8 Merge pull request #948 from vrtmrz/adjust_overwrite_prevention
Adjust overwrite prevention
2026-06-08 19:57:20 +09:00
vorotamoroz
5921a71227 Add CI 2026-06-08 10:52:00 +00:00
vorotamoroz
a40929c9e4 fixed: enhance conflict handling by adding settings check for document writes 2026-06-08 10:47:34 +00:00
vorotamoroz
2d8a285201 Port new tests 2026-06-08 10:38:54 +00:00
vorotamoroz
d9903bfe9e Merge pull request #947 from vrtmrz/0_25_74
Release: 0.25.74
2026-06-08 19:29:33 +09:00
vorotamoroz
a6e7dddf7f Merge branch '0_25_74' into cli_test_deno 2026-06-08 11:29:06 +01:00
vorotamoroz
9a51c78011 tweak specificity 2026-06-08 11:18:35 +01:00
vorotamoroz
7b9c0b011f bump 2026-06-08 11:15:13 +01:00
vorotamoroz
26a050e6f6 Merge pull request #946 from vrtmrz/cli_vaultpath
feat:  decouple the database and vault directories
2026-06-08 19:12:11 +09:00
vorotamoroz
34162f747c fix corrupted test, update submodule 2026-06-08 11:10:37 +01:00
vorotamoroz
9e87ee4da1 Merge branch 'main' into cli_vaultpath 2026-06-08 19:07:02 +09:00
vorotamoroz
ba5d4c434b Merge pull request #945 from vrtmrz/fix_941
fixed: ignore hidden files completely if hidden file sync disabled
2026-06-08 18:57:09 +09:00
vorotamoroz
8f44f4c61d Merge remote-tracking branch 'origin/main' into fix_941 2026-06-08 10:55:47 +01:00
vorotamoroz
23d8a1ecc3 update submodule 2026-06-08 10:51:08 +01:00
vorotamoroz
a46abcad52 Merge branch 'main' into fix_941 2026-06-08 10:50:34 +01:00
vorotamoroz
347e1fcee5 Merge pull request #944 from vrtmrz/fix_911
fixed: fix automatic merging behaviour
2026-06-08 18:47:29 +09:00
vorotamoroz
d85d8bc5e4 Merge branch 'main' into fix_911 2026-06-08 18:45:35 +09:00
vorotamoroz
c493fb6f66 Merge pull request #942 from vrtmrz/feat_cli_database_commands
feat: Added new remote management commands
2026-06-08 18:44:32 +09:00
vorotamoroz
cf173caf88 feat: decouple the database and vault directories 2026-06-08 10:43:10 +01:00
vorotamoroz
a41c7b2f70 Merge pull request #928 from starskyzheng/feat/cli-daemon-vault-option
feat(cli): add --vault option for daemon and mirror commands
2026-06-08 18:03:25 +09:00
vorotamoroz
3acaa7f269 update readme 2026-06-08 09:26:43 +01:00
vorotamoroz
85787eddb3 fixed: ignore hidden files completely if hidden file sync disabled 2026-06-08 09:24:59 +01:00
vorotamoroz
8cbb233df7 fixed: fix automatic merging behaviour 2026-06-08 09:04:12 +01:00
vorotamoroz
5922186a0e feat: Added new remote management commands 2026-06-08 04:23:22 +00:00
vorotamoroz
60f21eb9d2 detect loopback and coturn option 2026-06-05 09:44:17 +01:00
vorotamoroz
6b7816d334 add coturn for test 2026-06-05 09:39:39 +01:00
vorotamoroz
369e62ee8d Improved: we can set empty for turnServer explicitly. 2026-06-05 09:27:19 +01:00
vorotamoroz
37593bbee6 Update CI to use deno 2026-06-05 09:07:38 +01:00
vorotamoroz
baa51a66a7 Merge branch 'main' into cli_test_deno 2026-06-05 08:34:31 +01:00
郑泽宇
be979a3bf1 fix(cli): respect mirror positional arg before --vault flag
Per vrtmrz's review feedback, restore the mirror [vault-path] positional
argument support with correct priority order:
  mirror positional arg > --vault flag > databasePath

Also update --vault help text and CLI README with the new option.
2026-06-05 11:50:23 +08:00
Miguel Ferreira
c78e583399 feat: opt-in desktop setting to keep replication active in the background
Replication is suspended when the Obsidian window becomes hidden (document.hidden),
so LiveSync and Periodic stop syncing while minimised until the window is focused.

Add keepReplicationActiveInBackground (default off, desktop only). When enabled, the
window-visibility handler no longer suspends on hide, so replication keeps running while
minimised. Becoming visible forces a teardown before reopen (LiveSync only) so a stalled,
half-open channel is always replaced.

Includes the setting definition (src/lib submodule), a desktop-only toggle in the Sync
pane shown for LiveSync and Periodic, a docs/settings.md entry, and unit tests for the
visibility handler.
2026-06-05 00:06:26 +01:00
vorotamoroz
31050c9cb8 update docs 2026-06-05 03:05:13 +09:00
vorotamoroz
1b44ab9f2b docs: update documentation 2026-06-05 01:57:03 +09:00
vorotamoroz
3a78c70539 (chore) update terms 2026-06-05 01:24:46 +09:00
vorotamoroz
0f08ad435f (chore): improve English 2026-06-05 01:08:54 +09:00
vorotamoroz
96abd930b3 Merge pull request #938 from vrtmrz/0_25_73
Release 0.25.73
2026-06-04 18:50:11 +09:00
vorotamoroz
4581db45c4 bump 2026-06-04 10:44:56 +01:00
vorotamoroz
05cab8ec66 Merge pull request #937 from vrtmrz/fix_926
fix: adjust CouchDB's database name checking to its specification
2026-06-04 18:39:38 +09:00
vorotamoroz
b005625ef3 Merge branch 'main' into fix_926 2026-06-04 18:38:26 +09:00
vorotamoroz
e5408b4dd7 Merge pull request #936 from vrtmrz/fix_path_handling_on_windows_cli
Fix: No longer path corruption on windows environment
2026-06-04 18:37:47 +09:00
vorotamoroz
95a9b1b41c Merge pull request #935 from vrtmrz/fix_first_fetch2
fixed: `Reset Syncronisation on This Device` for minio and P2P
2026-06-04 18:37:23 +09:00
vorotamoroz
2aa8bc1165 fix: adjust CouchDB's database name checking to its specification (#926). 2026-06-04 10:35:15 +01:00
vorotamoroz
f8998d5441 (fixed): No longer path corruption on windows environment (at least, pass the check) on CLI 2026-06-04 10:17:34 +01:00
vorotamoroz
26f5f54f24 fixed: Reset Syncronisation on This Device for minio and P2P is now working properly. 2026-06-04 09:40:27 +01:00
vorotamoroz
99f3aca024 Merge pull request #933 from vrtmrz/0_25_71
0 25 72
2026-06-03 20:22:29 +09:00
vorotamoroz
194397dd94 bump again 2026-06-03 12:12:21 +01:00
vorotamoroz
afa79d78dc bump 2026-06-03 11:18:25 +01:00
vorotamoroz
9a9440a768 Merge branch 'improve_first_fetch' into 0_25_71 2026-06-03 11:03:46 +01:00
vorotamoroz
e375860af8 fix importing issue and unit test issue 2026-06-03 06:58:19 +01:00
vorotamoroz
b57f34c15b Revise devs note 2026-06-03 01:13:50 +00:00
vorotamoroz
b82fd9f04b Revise redflag.md option descriptions
Updated descriptions for redflag options in troubleshooting documentation.
2026-06-02 21:14:42 +09:00
vorotamoroz
bb77426b7b update dependencies and bump 2026-06-02 12:50:46 +01:00
vorotamoroz
1bef5fbef3 Merge branch 'fix_warns' into improve_first_fetch 2026-06-02 12:36:32 +01:00
vorotamoroz
7d2ba1b0b9 ### Improved
- Database fetching (a.k.a. Reset Synchronisation on This Device) on the initialisation now supports streaming and is faster (CouchDB only)
- The database fetching process has been streamlined, and database operations are now suspended until it has been completed
- The initial synchronisation process has been simplified, making it easier to synchronise files with the remote server
- We can select the remote database to fetch from during the initialisation, when there are multiple remote databases configured (e.g. multiple CouchDBs or S3 remotes)
2026-06-02 12:34:46 +01:00
vorotamoroz
ac6b9a4dad fix bad English 2026-06-01 12:13:01 +01:00
vorotamoroz
225e2c5096 bump 2026-06-01 12:10:56 +01:00
vorotamoroz
674d68b7d9 Fixed:
-  No longer the status element breaks other plugins' interaction (#930).
2026-06-01 12:05:31 +01:00
vorotamoroz
0e6dd300ef Merge branch 'fix_warns' into improve_first_fetch 2026-06-01 11:22:01 +01:00
vorotamoroz
8171db353a bump 2026-06-01 11:19:42 +01:00
vorotamoroz
6ab1556880 prettify 2026-06-01 11:19:32 +01:00
vorotamoroz
cd2bff5fc7 Refactor types in svelte components. 2026-06-01 11:18:23 +01:00
vorotamoroz
5a280c7919 (feat): Bulk database fetching is now work in progress. This feature is expected to speed up rebuilds and setups. (WIP) 2026-06-01 10:41:48 +01:00
vorotamoroz
c6697327d5 Fixed typings
Fixed wrong typing for serviceHub, svelte dialog
2026-06-01 06:20:33 +01:00
vorotamoroz
7c203a522a Aligned to eslint rules and fixed following things:
This commit may contains behavioural changes.

- Fix for the issue with corrupted log displays
- Wrap the activeDocument
- Reduced potential type errors and strengthened certain checks
- Made error handling more robust (by rewriting the error class)
2026-06-01 05:28:03 +01:00
vorotamoroz
c80c294d93 satisfies
Address following rules
-
- @typescript-eslint/unbound-method
- obsidianmd/prefer-active-doc
- obsidianmd/prefer-window-timer

Improve typing
2026-06-01 04:20:47 +01:00
vorotamoroz
3e65ae932d Address following rules
- @typescript-eslint/no-redundant-type-constituents
2026-06-01 03:50:51 +01:00
vorotamoroz
f710f03380 Address following rules
- @typescript-eslint/no-redundant-type-constituents
- @typescript-eslint/no-unnecessary-type-assertion
2026-06-01 03:37:55 +01:00
vorotamoroz
b887269fc1 change eslint for more progressive 2026-06-01 02:37:07 +01:00
vorotamoroz
56a234e6d7 chore: wrap timer functions 2026-05-31 19:03:43 +09:00
vorotamoroz
39014b2294 use coreEnvVars for some vars 2026-05-30 23:59:36 +09:00
vorotamoroz
24e6c110a3 Remove unused imports 2026-05-30 23:53:19 +09:00
vorotamoroz
f24d110552 Change type assertion 2026-05-30 23:49:32 +09:00
vorotamoroz
7189c1c05a Update dependency
set no-deprecated to warn
2026-05-30 23:42:32 +09:00
vorotamoroz
547afe9a86 remove unused eslint comment
use _fetch instead of fetch
add ignore list
2026-05-30 23:32:46 +09:00
vorotamoroz
7b5876037d Add ignores 2026-05-30 23:22:17 +09:00
vorotamoroz
b714c00644 (chore): Improve type assertion, remove unused imports 2026-05-29 04:19:58 +01:00
郑泽宇
fff6df535f feat(cli): add --vault option for daemon and mirror commands
Allow specifying a separate vault directory for .md files, decoupled from
the database directory (where PouchDB data lives).

- Add --vault/-V flag to CLI options parser
- Use vaultPath (or fallback to databasePath) for file system operations
- Works with both daemon and mirror commands
- Log vault path alongside database path at startup
2026-05-28 16:44:34 +08:00
vorotamoroz
e14e771bfb (chore): tidied tsconfig and eslint, and some incorrect imports 2026-05-28 04:42:03 +01:00
vorotamoroz
1130bbcee8 remove unmaintained tests 2026-05-26 11:33:14 +01:00
vorotamoroz
8841ef4619 fix css duplicated props 2026-05-26 11:27:31 +01:00
vorotamoroz
45fe0b3682 chore: lint: enable cache and concurrency 2026-05-26 11:24:41 +01:00
vorotamoroz
8d3825abc9 Merge pull request #925 from vrtmrz/v0_25_70
Releasing v0.25.70
2026-05-25 18:38:04 +09:00
vorotamoroz
c5f9841b85 chore: fix grammatical error 2026-05-25 10:14:18 +01:00
vorotamoroz
d36d176d99 bump 2026-05-25 10:10:46 +01:00
vorotamoroz
38b2cf73ed Update lib (forgot to include #942) 2026-05-25 09:55:11 +01:00
vorotamoroz
40b15a6950 Merge pull request #924 from vrtmrz/p2p_add_fix_and_diag
Improvements: More diagnostic information for P2P connections
2026-05-25 17:52:37 +09:00
vorotamoroz
e312bb7640 Merge pull request #891 from SeleiXi/feat/conflict-diff-jump
feat: add diff navigation to conflict resolver
2026-05-25 17:51:49 +09:00
Ching Wing Kwok
852c0e6c13 Merge branch 'main' into feat/conflict-diff-jump 2026-05-25 15:38:21 +08:00
vorotamoroz
7c1bcf9e9b Merge pull request #889 from SeleiXi/diff-only-button
feat: add diff-only view button to document history
2026-05-25 14:00:11 +09:00
vorotamoroz
2b79bed085 Merge branch 'main' into pr/SeleiXi/889 2026-05-25 05:43:36 +01:00
vorotamoroz
6b1e0c4aa8 Merge pull request #890 from SeleiXi/feat/history-search
feat: Add document history search
2026-05-25 12:55:29 +09:00
vorotamoroz
3c3645eba4 Improved: More diagnostic information for P2P connections is now shown, including why a connection failure occurred and the current connection status. 2026-05-25 04:31:22 +01:00
SeleiXi
009cc3c87a Merge remote-tracking branch 'origin/main' into feat/conflict-diff-jump
# Conflicts:
#	src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts
2026-05-23 02:06:52 +08:00
SeleiXi
fc5fd4be94 Merge remote-tracking branch 'origin/main' into feat/history-search
# Conflicts:
#	src/lib
#	src/modules/features/DocumentHistory/DocumentHistoryModal.ts
2026-05-23 02:05:33 +08:00
SeleiXi
8ed1acf79d Merge remote-tracking branch 'origin/main' into diff-only-button
# Conflicts:
#	src/modules/features/DocumentHistory/DocumentHistoryModal.ts
2026-05-23 02:04:48 +08:00
vorotamoroz
c518223d21 Merge pull request #923 from vrtmrz/p2p_add_fix_and_diag
0.25.69: Fix P2P, add diag feature
2026-05-23 00:47:36 +09:00
vorotamoroz
caaff618e9 fix grammar 2026-05-22 16:40:40 +01:00
vorotamoroz
148aa8505e bump 2026-05-22 16:38:44 +01:00
vorotamoroz
f9a626a858 ### Fixed
- No longer the P2P passphrase mismatch causes a server shutdown.
- Settings related to P2P synchronisation are now correctly applied on start-up and no longer reverted.

### New features
- Diagnostic P2P connection stats are now available.
  - These stats indicate the number of connection trials, successes, and, failures.
2026-05-22 16:37:05 +01:00
vorotamoroz
2f10121d6c Merge remote-tracking branch 'origin/main' into cli_test_deno 2026-05-22 10:19:30 +00:00
vorotamoroz
1b8747115c Merge pull request #922 from vrtmrz/update_trystero
Releasing v0.25.68 (dependency and P2P update)
2026-05-22 19:17:22 +09:00
vorotamoroz
e739302fb9 bump 2026-05-22 11:11:18 +01:00
vorotamoroz
8f20d53f55 Update trystero, typed well. fixed some potentially problems
Weaken terser for libraries
2026-05-22 11:01:51 +01:00
vorotamoroz
3ab80190d6 test fix ci (Redundant test) 2026-05-22 03:48:41 +00:00
vorotamoroz
8948bf2803 test cli:p2p use nonce for peername 2026-05-22 03:48:02 +00:00
vorotamoroz
486fd15c60 fix resouce handling 2026-05-22 03:46:56 +00:00
vorotamoroz
5fd85c71ca test: chore: prettify 2026-05-22 03:20:28 +00:00
vorotamoroz
c1f41910c4 test: add actions / caching 2026-05-22 03:20:11 +00:00
vorotamoroz
3693d6a6b6 test: add port ready, container cleanup 2026-05-22 03:19:48 +00:00
vorotamoroz
cc3c992b1d cli: add large-file-test and benchmark between couchdb and p2p 2026-05-22 03:05:44 +00:00
vorotamoroz
df390ac456 test: fix deno test helpers 2026-05-22 03:02:11 +00:00
vorotamoroz
fd84b0377b Update actions 2026-05-21 10:02:46 +01:00
vorotamoroz
340d416b76 Merge pull request #920 from vrtmrz/v0_25_66
Release v0.25.67
2026-05-20 19:44:41 +09:00
vorotamoroz
3034af8d69 bump again 2026-05-20 11:37:51 +01:00
vorotamoroz
da3020bd45 fixed: fix auto-correction mismatch 2026-05-20 11:34:28 +01:00
vorotamoroz
ce232c1002 bump 2026-05-20 11:14:41 +01:00
vorotamoroz
0e13926400 fixed: update lib
I should do this with #919
2026-05-20 11:10:43 +01:00
vorotamoroz
fab7ec996a Merge pull request #919 from vrtmrz:feat_cli_remote_select
feat: add CLI commands to handle multiple remote configuration
2026-05-20 19:05:49 +09:00
vorotamoroz
88e22f99c5 Merge pull request #917 from vrtmrz:feat_tweak_auto_adjust
feat: implement auto-accept compatible tweak
2026-05-20 19:02:58 +09:00
vorotamoroz
83cbabf06f Merge branch 'main' into feat_tweak_auto_adjust 2026-05-20 11:01:51 +01:00
vorotamoroz
5e8d3b8f02 Update lib 2026-05-20 11:00:38 +01:00
vorotamoroz
1167b41340 feat: add CLI commands to handle multiple remote configuration 2026-05-20 05:10:42 +01:00
vorotamoroz
67da3964e5 Merge pull request #918 from vrtmrz/v0_25_65
V0.25.65
2026-05-19 20:27:34 +09:00
vorotamoroz
45ebc7eb6b Update the doc and issue report, and bump 2026-05-19 12:17:26 +01:00
vorotamoroz
cc5ead68bc minor layout fix 2026-05-19 11:59:54 +01:00
vorotamoroz
9b9e4f22f3 Merge pull request #916 from vrtmrz:fix_866
fixed: Now Chunk Splitter: `V3: Fine Deduplication`
2026-05-19 19:46:08 +09:00
vorotamoroz
7823f46053 Merge branch 'main' into fix_866 2026-05-19 11:45:41 +01:00
vorotamoroz
d6d8e548b3 Merge pull request #915 from vrtmrz/improve_log_feature
Improve log features
2026-05-19 19:40:28 +09:00
vorotamoroz
44b1ed7610 Merge branch 'main' into improve_log_feature 2026-05-19 11:40:14 +01:00
vorotamoroz
5786da5534 merged 2026-05-19 11:38:05 +01:00
vorotamoroz
042a80dd44 Merge pull request #914 from vrtmrz/fix_ios_resume
fix: Fix an issue about resuming from background on iOS (#888).
2026-05-19 19:34:49 +09:00
vorotamoroz
ee30f6cd6d fixed: Now Chunk Splitter: V3: Fine Deduplication is working fine again (#866). 2026-05-19 11:26:35 +01:00
vorotamoroz
977a300808 fix grammatical error 2026-05-19 04:10:39 +01:00
vorotamoroz
a392ccab6a - Improved an error verbosity on concurrent processing on start-up process.
- Now the `report` includes recent logs (of verbosity `verbose` even settings is not set to `verbose`).
- Updating logs is now debounced to avoid excessive updates during rapid log generation.
- Added a `Generate full report for opening the issue with debug info` command to the command palette, which generates a report without opening the settings dialogue.
2026-05-19 04:09:04 +01:00
vorotamoroz
dfdfa5383b grammatical fix 2026-05-19 01:55:10 +01:00
vorotamoroz
a08294ab16 fix: Fix an issue about resuming from background on iOS (#888). 2026-05-18 12:30:03 +01:00
vorotamoroz
e8c33a0d6a feat: implement auto-accept compatible tweak setting and enhance mismatch resolution logic 2026-05-18 11:21:53 +01:00
vorotamoroz
d6bf453a6d Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2026-05-18 11:07:43 +01:00
vorotamoroz
e80cdc2dae remove unused files 2026-05-18 08:47:03 +01:00
vorotamoroz
60780678fd Merge pull request #903 from Joysimple/cli-docker
bugfix: Add package-lock.json into docker build
2026-05-18 12:38:12 +09:00
vorotamoroz
273e7a2b63 Merge pull request #909 from vrtmrz/p2p_config_selectable
P2p config selectable (a.k.a. v0.25.64)
2026-05-17 13:24:40 +09:00
vorotamoroz
2572c54744 Track main 2026-05-17 13:17:09 +09:00
vorotamoroz
6a7c987985 bump 2026-05-17 13:13:21 +09:00
vorotamoroz
6ef866a77c P2P: Enhance status pane and card with active remote selection and replication features
- Added active P2P remote selector and creation option in the status pane.
- Introduced immediate replication action for accepted peers.
- Updated status control icons for clarity.
- Display stable Room ID suffix above Peer ID in the status card.
- Implemented dedicated active remote configuration for P2P features.
- Added migration support for P2P active remote selection.
- Improved unit test coverage for P2P settings.
2026-05-17 13:08:51 +09:00
vorotamoroz
eea26dee74 Update about P2P 2026-05-17 02:35:20 +09:00
vorotamoroz
ee24fe8c24 Merge pull request #904 from vrtmrz/enhance_fix_p2p
Fixed: fixed P2P bugs and and implement new UI
2026-05-17 02:32:50 +09:00
vorotamoroz
9d9364af36 Fix updates.md 2026-05-17 02:24:58 +09:00
vorotamoroz
83228e2077 Fix P2P replicator creation and enhance error handling in synchronization functions 2026-05-17 02:23:58 +09:00
vorotamoroz
a379b5bd78 bump 2026-05-17 01:40:50 +09:00
vorotamoroz
4ed1749652 Enhance P2P synchronization features and UI improvements 2026-05-17 01:36:09 +09:00
vorotamoroz
9a90256a8a Enhance P2P synchronization features and UI improvements 2026-05-16 23:50:08 +09:00
vorotamoroz
f0628a0d2c Improve UI 2026-05-16 23:09:11 +09:00
vorotamoroz
d5e2f57781 Fixed: fixed P2P bugs and and implement new UI 2026-05-15 10:18:53 +01:00
Nikolay Sokolov
02673a1631 bugfix: Add package-lock.json into docker build 2026-05-14 23:17:37 -07:00
vorotamoroz
91c9746886 Merge pull request #900 from vrtmrz/v0_25_62
Releasing v0.25.62
2026-05-14 20:15:02 +09:00
vorotamoroz
75b44b1636 bump 2026-05-14 09:40:12 +00:00
vorotamoroz
f1fe48c1ee add version-bump 2026-05-14 09:35:53 +00:00
vorotamoroz
437e7c0d9c Fixed an issue where a connection could not be established when attempting to connect to a brand-new remote database without going through the set-up wizard or configuration checking 2026-05-14 09:32:34 +00:00
vorotamoroz
5ffa7ec7ee Merge pull request #897 from vrtmrz/v0_25_61
Releaseing v0.25.61
2026-05-13 23:06:57 +09:00
vorotamoroz
a1859f5d2e bump 2026-05-13 14:44:38 +01:00
vorotamoroz
785af8cb8f Merge pull request #896 from vrtmrz/address_community_review
Address community review
2026-05-13 22:29:37 +09:00
vorotamoroz
06e1f4aa4a Update subrepo pointer 2026-05-13 14:25:49 +01:00
vorotamoroz
767f22ce9c Merge branch 'address_community_review' of https://github.com/vrtmrz/obsidian-livesync into address_community_review 2026-05-13 14:16:51 +01:00
vorotamoroz
6a9bba702c chore: ran prettier 2026-05-13 14:10:56 +01:00
vorotamoroz
de2397dc3f Adding a rough DI 2026-05-13 14:10:55 +01:00
vorotamoroz
daaad9212e Fix package-lock 2026-05-13 14:10:54 +01:00
vorotamoroz
a6891374a1 chore: Package modernise, update linter 2026-05-13 14:10:01 +01:00
vorotamoroz
b1cadf0549 prettify 2026-05-13 14:07:58 +01:00
vorotamoroz
95f40cc954 (chore): removing DOM Operation 2026-05-13 14:07:58 +01:00
vorotamoroz
8deaf123d6 Update eslint config to ignore file,
fix some type error on LiveSyncBaseCore
2026-05-13 14:06:51 +01:00
vorotamoroz
053813bffb Update for review once 2026-05-13 14:06:51 +01:00
vorotamoroz
cc7af03618 chore: Package modernise, update linter 2026-05-13 14:06:51 +01:00
vorotamoroz
a130e3700e prettify 2026-05-13 14:06:50 +01:00
vorotamoroz
0549e901b2 (chore): removing DOM Operation 2026-05-13 14:06:49 +01:00
vorotamoroz
e9afe06968 Merge pull request #895 from vrtmrz/update_lib
Update lib to fix P2P problems and merging contributions
2026-05-13 22:01:17 +09:00
vorotamoroz
c45aca4794 fixed: fixed subrepo pointer
I thought I’d rebased it, but it turns out everything had been merged.
2026-05-13 13:45:09 +01:00
vorotamoroz
e2c54aaf43 Update lib to fix P2P problems 2026-05-13 13:31:11 +01:00
vorotamoroz
37715d4c9f chore: ran prettier 2026-05-13 11:12:40 +00:00
vorotamoroz
106367fa41 Adding a rough DI 2026-05-13 11:09:04 +00:00
vorotamoroz
538130aa91 Fix package-lock 2026-05-13 11:36:01 +01:00
vorotamoroz
c9d0357fec Merge branch 'address_community_review' of https://github.com/vrtmrz/obsidian-livesync into address_community_review 2026-05-13 11:35:01 +01:00
vorotamoroz
d05c76da36 Update eslint config to ignore file,
fix some type error on LiveSyncBaseCore
2026-05-13 11:33:46 +01:00
vorotamoroz
d2eb6ecbaf Update for review once 2026-05-13 11:33:46 +01:00
vorotamoroz
25a6fde212 chore: Package modernise, update linter 2026-05-13 11:33:45 +01:00
vorotamoroz
e8f8b680ef prettify 2026-05-13 11:33:03 +01:00
vorotamoroz
6c30f2b863 (chore): removing DOM Operation 2026-05-13 11:33:03 +01:00
vorotamoroz
8dda24a689 Merge pull request #882 from vrtmrz/p2p-rpc
feat: use new p2p-rpc wrapper
2026-05-13 19:24:26 +09:00
vorotamoroz
fbbb63906a Merge branch 'main' into p2p-rpc 2026-05-13 19:22:03 +09:00
vorotamoroz
1e66a7f144 Merge pull request #894 from vrtmrz/fix_unexpected_error_on_startup
fixed: fixed unexpected error during startup
2026-05-13 19:16:18 +09:00
vorotamoroz
df79d81475 fixed: fixed unexpected error during startup 2026-05-13 10:14:47 +00:00
vorotamoroz
ad71355859 Merge pull request #893 from brian-spackman/fix-fractional-mtime-on-linux
fix: truncate sub-millisecond CLI mtimes to prevent mobile crash
2026-05-13 19:12:56 +09:00
vorotamoroz
95dc079fad Merge pull request #843 from andrewleech/daemon-sync
cli: implement continuous sync daemon mode
2026-05-13 18:53:59 +09:00
vorotamoroz
770d4af4a0 Update eslint config to ignore file,
fix some type error on LiveSyncBaseCore
2026-05-13 10:15:45 +01:00
vorotamoroz
3b311248cb Update for review once 2026-05-13 08:02:50 +01:00
Andrew Leech
67996f6d0a cli: fix stale stat.size in NodeVaultAdapter causing corrupted file errors
chokidar stats are captured at poll time and may not reflect the file's
final byte length by the time vault.read() is called. The downstream
integrity check compares stat.size to content length; a mismatch causes
other LiveSync clients to reject the file as corrupted.

Fix by updating file.stat.size from the actual content in read() and
readBinary().

Co-authored-by: Joysimple <Joysimple@users.noreply.github.com>
2026-05-13 16:56:08 +10:00
vorotamoroz
5772811a45 chore: Package modernise, update linter 2026-05-13 04:40:32 +01:00
vorotamoroz
55529cd71e prettify 2026-05-13 03:58:08 +01:00
vorotamoroz
2e9b8b7b62 (chore): removing DOM Operation 2026-05-13 03:55:11 +01:00
Andrew Leech
4ab2e41d18 cli daemon: set disableCheckingConfigMismatch for headless operation
The config mismatch dialog's defaultAction is "Dismiss" which blocks
replication. Since the daemon cannot resolve mismatches interactively,
skip the check entirely and accept the remote configuration as-is.
2026-05-13 11:21:06 +10:00
Andrew Leech
c0ad8ee15a cli: add configurable ignore rules and deployment artifacts
IgnoreRules (src/apps/cli/serviceModules/IgnoreRules.ts):
- Reads .livesync/ignore for user-defined glob patterns
- Applies gitignore matchBase semantics: patterns without / get **/ prefix,
  patterns ending with / get ** appended for directory contents
- Supports `import: .gitignore` directive to merge gitignore patterns
- Rejects negation patterns with a warning (not fully supportable)
- Integrated into both daemon and mirror commands via isTargetFile handler

Wiring:
- IgnoreRules loaded before LiveSyncBaseCore construction so beginWatch()
  receives rules when it fires during onLoad/onFirstInitialise
- Passed through initialiseServiceModulesCLI -> StorageEventManagerCLI ->
  CLIStorageEventManagerAdapter -> CLIWatchAdapter

Deployment:
- src/apps/cli/deploy/livesync-cli.service - systemd unit template
- src/apps/cli/deploy/install.sh - user/system install script

Testing:
- src/apps/cli/test/test-daemon-linux.sh - e2e tests for ignore rules
- src/apps/cli/serviceModules/IgnoreRules.unit.spec.ts - 15 unit tests
- src/apps/cli/commands/daemonCommand.unit.spec.ts - 7 unit tests
2026-05-13 11:21:06 +10:00
Andrew Leech
e6ae516493 cli: implement local→CouchDB file watching via chokidar
- Add chokidar ^4.0.0 as dependency (root package.json, runtime-package.json)
- Mark chokidar as external in vite.config.ts (not bundled, loaded at runtime)
- Implement CLIWatchAdapter.beginWatch() with chokidar:
  - ignoreInitial: true (startup files handled by mirror scan)
  - awaitWriteFinish to prevent partial-write events
  - Excludes dotfiles and .livesync/ directory at watcher level
  - Maps add/change/unlink/addDir/unlinkDir to IStorageEventWatchHandlers
  - Fatal error handler: logs clearly and releases watcher resources
- Add close() to CLIWatchAdapter, StorageEventManagerCLI for clean shutdown
- Register onUnload hook in CLIServiceModules to close watcher on shutdown
2026-05-13 11:21:06 +10:00
Andrew Leech
a4d5ef4620 cli: implement daemon startup sequence and CouchDB→local sync
- Add daemon command to help text and --interval/-i flag for polling mode
- Capture original sync settings before suspendAllSync() clobbers them
- Implement daemon startup: mirror scan → restore settings → applySettings()
  which triggers the full suspend/resume lifecycle and starts the _changes feed
- Guard processSynchroniseResult no-op to non-daemon commands so default
  handler writes incoming CouchDB changes to the local filesystem
- Polling mode: restore settings + clearInterval-safe try/catch error handling
- Warn when both liveSync and syncOnStart are false after restore (no-op config)
- Fix: only block indefinitely if daemon startup succeeded
2026-05-13 11:21:06 +10:00
Brian Spackman
3f7bb047ac fix: floor sub-millisecond CLI mtimes to prevent mobile crash
On Linux, fs.Stats.mtimeMs and ctimeMs return floats with sub-millisecond
precision derived from the kernel's nanosecond filesystem mtime. Stored
raw, this produces document timestamps like 1778511180024.462 in CouchDB
rather than integer milliseconds.

Mobile clients running LiveSync 0.25.60 have been observed to crash when
processing change-feed updates carrying non-integer millisecond timestamps
from CLI-written documents. Desktop and mobile GUI plugins write integer
milliseconds, so the crash only manifests when the headless CLI on Linux
is the source. Whether the issue was introduced in 0.25.60 or had been
latent in earlier versions hasn't been investigated; 0.25.60 is the
version where the crash was confirmed and the fix verified.

Floor the values at every stat-read site (six across three adapters and
one command) so CLI-written documents carry integer-millisecond
timestamps consistent with the rest of the mesh.
2026-05-12 18:00:25 -06:00
SeleiXi
5454e1106f feat: add diff navigation to conflict resolver 2026-05-13 00:19:56 +08:00
SeleiXi
0d9397c8b9 fix: resolve UI alignment issue for diff navigation buttons 2026-05-12 00:52:20 +08:00
SeleiXi
429a3ff1fd feat: add diff-only view button to document history 2026-05-11 23:53:07 +08:00
SeleiXi
bfff6ea7b8 feat: add document history search support 2026-05-11 22:45:42 +08:00
vorotamoroz
b6b153c0de Merge pull request #887 from vrtmrz/add_ignore_to_eslint
chore: Change eslint config to ignore _tools
2026-05-11 21:01:47 +09:00
vorotamoroz
eca6a6e0ba chore: Change eslint config to ignore _tools 2026-05-11 13:00:32 +01:00
vorotamoroz
ca43d96c46 Merge pull request #886 from vrtmrz/fix_prettier
Fix prettier config
2026-05-11 20:34:22 +09:00
vorotamoroz
112e3c8b1d Fix prettier config 2026-05-11 12:33:32 +01:00
vorotamoroz
d1eb105801 Merge pull request #872 from OriBoharon/make-cli-onboarding-easier
added documentaion and a hook build script to make onbaording easier when trying to build the cli app
2026-05-11 18:43:00 +09:00
vorotamoroz
d5b93e89cd Change the default issue report label from 'bug' to 'uncategorised' 2026-05-11 17:55:31 +09:00
vorotamoroz
e96fe7cde1 Merge pull request #885 from vrtmrz/tidy_file
(chore) remove obsoleted file
2026-05-11 17:52:22 +09:00
vorotamoroz
68e0610f1d (chore) remove obsoleted file 2026-05-11 09:49:32 +01:00
vorotamoroz
a6be20695a feat: use new p2p-rpc wrapper 2026-05-11 03:49:35 +01:00
vorotamoroz
772b6ecf26 Merge pull request #871 from SeleiXi/feat/diff-navigation-buttons
feat: Add diff navigation buttons for Document History
2026-05-09 22:51:04 +09:00
SeleiXi
81dc7f604b feat: auto navigation to diff 2026-05-09 14:07:08 +08:00
vorotamoroz
a9c87fa52e - Add default test environment
- Fixed to use environment by APIs
- Make test parallel
2026-05-08 03:04:14 +00:00
vorotamoroz
e81f023943 Add default test env 2026-05-08 03:01:22 +00:00
vorotamoroz
2afe12ad2d fix pattern 2026-05-07 11:28:01 +01:00
vorotamoroz
4a9d6c1349 Add ci 2026-05-07 11:23:51 +01:00
vorotamoroz
279fc8876e feat(tests): enhance push/pull test with Docker integration and improved environment variable handling
style(test): format comment
2026-05-07 11:22:56 +01:00
vorotamoroz
cc3d30dbcf feat(tests): add Deno-based tests for checking CLI functionality in the same-codebase between platforms. 2026-05-07 11:06:12 +01:00
vorotamoroz
39e82cc8a1 Fixed: Fix timing issue during test 2026-05-06 21:56:13 +09:00
bori
7a4b76a550 added documentaion and a hook build script to make onbaording easier 2026-05-02 18:51:07 +03:00
SeleiXi
f9294446ba feat: add diff block navigation to Document History modal
Add prev/next buttons to jump between diff blocks in the
Document History view. Includes position indicator and
auto-scroll with visual focus highlighting.
2026-05-02 22:18:43 +08:00
vorotamoroz
fa7ef62302 Fix: adjusting help
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 18:42:54 +09:00
vorotamoroz
81d8224330 bump
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 18:39:48 +09:00
vorotamoroz
cc466a4b3c ### Fixed
- Now larger settings can be exported and imported via QR code without issues. (#595)

- Fixed some errors during serialisation and deserialisation of the settings, which caused issues in some cases when importing/exporting settings via QR code.

Co-authored-by: Copilot <copilot@github.com>
2026-04-29 18:37:44 +09:00
vorotamoroz
ceebca7de9 Merge pull request #862 from fabiomanz/main
chore: remove obsolete `version` attribute from docker-compose.yml
2026-04-29 17:30:35 +09:00
Fabio
c2f696d0a4 chore: attribute version is obsolete 2026-04-29 07:07:45 +00:00
vorotamoroz
1aa7c45794 Fix the readme
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:55:34 +09:00
vorotamoroz
faefa80cbd Fix again 2026-04-29 12:40:40 +09:00
vorotamoroz
3737eacffd Fix readme
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:39:42 +09:00
vorotamoroz
4c0af0b608 Fixed(cli):
- `ls` and `mirror` commands now provide informative feedback when no documents are found or filters skip all files, resolving the issue where they would exit silently (#860).
- The command-line argument `vault` has been renamed to a more appropriate name, `databaseDir`.
- The `mirror` command now accepts a `vault` directory, which specifies the location where the actual files are stored. For compatibility reasons, the previous behaviour is still supported.

Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:22:00 +09:00
vorotamoroz
bb69eb13e7 bump 2026-04-27 11:15:07 +09:00
vorotamoroz
7c9db6376f Fixed:
- No longer Setup-wizard drops username and password silently. (#865)
- Setup URI is now correctly imported (#859).
- now French translation is added.
2026-04-27 11:14:06 +09:00
vorotamoroz
4c04e4e676 Merge pull request #863 from koteitan/fix/859-strip-trailing-slash-from-uri
fix: strip trailing slash from couchDB_URI to avoid double-slash 401
2026-04-27 11:08:11 +09:00
koteitan
14ec35b257 fix: strip trailing slash from couchDB_URI to avoid double-slash 401
When couchDB_URI ends with a trailing slash (e.g. https://host/), the
database name concatenation produces a double-slash path
(https://host//obsidiannotes), which causes CouchDB to reject requests
with 401 "Name or password is incorrect".

Strip trailing slashes from couchDB_URI / baseUri at the path
concatenation sites in:
- src/common/utils.ts (_requestToCouchDBFetch, _requestToCouchDB)
- src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts

The companion fix for the replication path is in the livesync-commonlib
submodule.

Ref: #859
2026-04-27 00:12:57 +09:00
vorotamoroz
b609e4973c Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-25 20:37:08 +09:00
vorotamoroz
354f0be9a3 bump
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 20:16:50 +09:00
vorotamoroz
16804ed34c Merge pull request #842 from kdavh/patch-1
Update README.md, fix webpeer link
2026-04-25 19:07:56 +09:00
vorotamoroz
31bd270869 Fixed: Hidden file JSON conflicts no longer keep re-opening and dismissing the merge dialogue before we can act, which fixes persistent unresolvable data.json conflicts in plug-in settings sync (related: #850).
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 17:22:25 +09:00
vorotamoroz
b5d054f259 Fixed: Issue report generation now redacts remoteConfigurations connection strings and keeps only the scheme (e.g. sls+https://), so credentials are not exposed in reports.
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 17:09:43 +09:00
vorotamoroz
1ef2955d00 - Fixed a worker-side recursion issue that could raise Maximum call stack size exceeded during chunk splitting (related: #855).
- Improved background worker crash cleanup so pending split/encryption tasks are released cleanly instead of being left in a waiting state (related: #855).
- On start-up, the selected remote configuration is now applied to runtime connection fields as well, reducing intermittent authentication failures caused by stale runtime settings (related: #855).

Co-authored-by: Copilot <copilot@github.com>
2026-04-25 16:51:37 +09:00
vorotamoroz
6ef56063b3 Fixed: No longer credentials are broken during object storage configuration (related: #852).
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 15:03:38 +09:00
vorotamoroz
a912585800 Improve issue template
Co-authored-by: Copilot <copilot@github.com>
2026-04-25 14:01:18 +09:00
vorotamoroz
7a863625bc bump 2026-04-09 04:32:34 +01:00
vorotamoroz
99b4037820 Fixed
- Packing a batch during the journal sync now continues even if the batch contains no items to upload.
2026-04-06 12:51:09 +01:00
vorotamoroz
d59b5dc2f9 Fixed
- Remote configuration URIs are now correctly encrypted when saved after editing in the settings dialogue.
- Fixed an issue where devices could no longer upload after another device performed 'Fresh Start Wipe' and 'Overwrite remote' in Object Storage mode (#848).
2026-04-06 11:47:19 +01:00
vorotamoroz
4d0203e4ca Update: beta tagging 2026-04-06 11:46:51 +01:00
vorotamoroz
3e4db571cd Fixed
- Remote configuration URIs are now correctly encrypted when saved after editing in the settings dialogue.
- Fixed an issue where devices could no longer upload after another device performed 'Fresh Start Wipe' and 'Overwrite remote' in Object Storage mode (#848).
2026-04-06 11:45:26 +01:00
vorotamoroz
b0a9bd84d6 bump 2026-04-05 18:21:38 +09:00
vorotamoroz
8c4e62e7c1 ### Fixed
- Now surely remote configurations are editable in the settings dialogue.
- We can fetch remote settings from the remote and apply them to the local settings for each remote configuration entry.
- No longer layout breaking occurs when the description of a remote configuration entry is too long.
2026-04-05 18:20:56 +09:00
vorotamoroz
3e03d1dbd5 bump 2026-04-05 17:48:00 +09:00
vorotamoroz
0dbf4cface bump 2026-04-05 17:47:45 +09:00
vorotamoroz
bc22d61a3a Fixed: Now error messages are kept hidden if the show status inside the editor is disabled. 2026-04-05 17:43:29 +09:00
vorotamoroz
d709bcc1d0 Add encryption for connection management 2026-04-05 16:24:34 +09:00
vorotamoroz
d7088be8af Improved: remote management 2026-04-05 16:00:57 +09:00
vorotamoroz
f17f1ecd93 ### Fixed
- No unexpected error (about a replicator) during early stage of initialisation.

### New features

- Now we can configure multiple Remote Databases of the same type, e.g, multiple CouchDBs or S3 remotes.
- We can switch between multiple Remote Databases in the settings dialogue.
2026-04-03 13:47:56 +01:00
vorotamoroz
bf556bd9f4 Merge pull request #838 from chinhkrb113/contribai/docs/undocumented-test-environment-variables
📝 Docs: Undocumented test environment variables
2026-04-03 13:05:05 +09:00
vorotamoroz
8b40969fa3 Add ru locale 2026-04-02 10:28:58 +00:00
vorotamoroz
6cce931a88 Add test for CLI 2026-04-02 09:58:25 +00:00
vorotamoroz
216861f2c3 Prettified 2026-04-02 10:33:36 +01:00
vorotamoroz
6ce724afb4 Add dockerfiles to webapp and webpeer 2026-04-02 10:33:13 +01:00
vorotamoroz
2e3e106fb2 Fix dockerfile 2026-04-02 10:31:17 +01:00
vorotamoroz
00f2606a2f Added a bit for development on Windows. 2026-04-02 10:31:03 +01:00
vorotamoroz
3c94a44285 Fixed: Replication progress is now correctly saved and restored in the CLI. 2026-04-02 10:30:14 +01:00
vorotamoroz
4c0908acde Add CI Build for cli-docker image 2026-04-02 07:47:10 +01:00
vorotamoroz
cda27fb7f8 - Update trystero to v0.23.0
- Add dockerfile for CLI
- Change relay image for testing on arm64
2026-03-31 07:17:51 +00:00
vorotamoroz
837a828cec Fix: fix update note... 2026-03-30 09:20:01 +01:00
vorotamoroz
4c8e13ccb9 bump 2026-03-30 09:14:49 +01:00
vorotamoroz
1ae4eaab02 Merge pull request #805 from L4z3x/patch-1
[docs] added changing docker compose data and etc folder ownership to user 5984.
2026-03-30 17:04:12 +09:00
chinhkrb113
b1efbf74c7 docs: clarify P2P_TEST_RELAY as Nostr relay 2026-03-30 02:18:25 +07:00
kdavh
12f04f6cf7 Update README.md, fix webpeer link 2026-03-28 12:47:28 -04:00
vorotamoroz
a937feed3f Merge pull request #833 from rewse/fix/cli-sync-locked-error-message
fix(cli): show actionable error when sync fails due to locked remote DB
2026-03-28 23:58:34 +09:00
ChinhLee
2de9899a99 docs: undocumented test environment variables
The P2P test suite relies on several specific environment variables (e.g., `P2P_TEST_ROOM_ID`, `P2P_TEST_PASSPHRASE`, `P2P_TEST_RELAY`) loaded from `.env` or `.test.env`. Because these are not documented anywhere in the repository, new contributors will be unable to configure their local environment to run the P2P tests successfully.

Affected files: vitest.config.p2p.ts

Signed-off-by: ChinhLee <76194645+chinhkrb113@users.noreply.github.com>
2026-03-27 22:26:00 +07:00
vorotamoroz
a0af6201a5 - No longer Peer-to-Peer Sync is not enabled. We cannot open a new connection. error occurs when we have not enabled P2P sync and are not expected to use it (#830). 2026-03-26 13:13:27 +01:00
vorotamoroz
9c7c6c8859 Merge pull request #831 from rewse/fix/cli-entrypoint-polyfill-default
fix(cli): handle incomplete localStorage in Node.js v25+
2026-03-26 20:36:46 +09:00
vorotamoroz
38d7cae1bc update some dependencies and ran npm-update. 2026-03-26 12:15:38 +01:00
vorotamoroz
fee34f0dcb Update dependency: deduplicate 2026-03-26 11:55:06 +01:00
Shibata, Tats
e01f7f4d92 test(cli): add TODO comment and locked-remote-DB test script
- Add inline TODO comment in runCommand.ts about standardising
  replication failure cause identification logic.
- Add test-sync-locked-remote-linux.sh that verifies:
  1. sync succeeds when the remote milestone is not locked.
  2. sync fails with an actionable error when the remote milestone
     has locked=true and accepted_nodes is empty.
2026-03-26 00:58:51 +09:00
Shibata, Tats
985004bc0e fix(cli): show actionable error when sync fails due to locked remote DB
When the remote database is locked and the CLI device is not in the
accepted_nodes list, openReplication returns false with no CLI-specific
guidance. The existing log message ('Fetch rebuilt DB, explicit
unlocking or chunk clean-up is required') is aimed at the Obsidian
plugin UI.

Check the replicator's remoteLockedAndDeviceNotAccepted flag after
sync failure and print a clear message directing the user to unlock
from the Obsidian plugin.

Ref: #832
2026-03-22 12:37:17 +09:00
Shibata, Tats
967a78d657 fix(cli): handle incomplete localStorage in Node.js v25+
Node.js v25 provides a built-in localStorage on globalThis, but without
`--localstorage-file` it is an empty object lacking getItem/setItem.
The existing check `!("localStorage" in globalThis)` passes, so the
polyfill is skipped and the CLI crashes with:

  TypeError: localStorage.getItem is not a function

Check for getItem as well so the polyfill is applied when the native
implementation is incomplete.
2026-03-22 11:57:47 +09:00
vorotamoroz
2ff60dd5ac Add missed files 2026-03-18 12:20:52 +01:00
vorotamoroz
c3341da242 Fix english 2026-03-18 12:05:15 +01:00
vorotamoroz
c2bfaeb5a9 Fixed: wrong import 2026-03-18 12:03:51 +01:00
vorotamoroz
c454616e1c bump 2026-03-18 12:01:57 +01:00
vorotamoroz
c88e73b7d3 Add note 2026-03-18 11:55:50 +01:00
vorotamoroz
3a29818612 - Delete items which are no longer used that might cause potential problems
- Fix Some Imports
- Fix floating promises on tests
2026-03-18 11:54:22 +01:00
vorotamoroz
ee69085830 Fixed: Some buttons on the setting dialogue now respond correctly again (#827). 2026-03-18 11:51:52 +01:00
vorotamoroz
3963f7c971 Refactored: P2P replicator has been refactored to be a little roust and easier to understand. 2026-03-18 11:49:41 +01:00
vorotamoroz
602fcef949 - Fixed the issue where the detail level was not being applied in the log pane.
- Pop-ups are now shown.
- Add coverage for test.
- Pop-ups are now shown in the web app as well.
2026-03-18 11:48:31 +01:00
vorotamoroz
075d260fdd Fixed:
- Fixed the corrupted display of the help message.
- Remove some unnecessary codes.
2026-03-18 11:46:52 +01:00
vorotamoroz
0717093d81 update for npm ci 2026-03-17 20:09:28 +09:00
vorotamoroz
1f87a9fd3d port setupManager, setupProtocol to serviceFeature
remove styles on webapp UI, and add stylesheet
2026-03-17 19:58:12 +09:00
vorotamoroz
fdd3a3aecb Add: vaultSelector (webapp) 2026-03-17 19:51:04 +09:00
vorotamoroz
d8281390c4 bump 2026-03-17 10:34:29 +01:00
vorotamoroz
08b1712f39 bump 2026-03-16 00:49:54 +09:00
vorotamoroz
6c69547cef ### Fixed
- Fixed flaky timing issues in P2P synchronisation.
- Fixed more binary file handling issues in CLI.

### Tests

- Rewrite P2P end-to-end tests to use the CLI as host.
2026-03-16 00:48:22 +09:00
vorotamoroz
89bf0488c3 Refactor: More refactor P2P Replicator 2026-03-15 04:07:47 +09:00
vorotamoroz
653cf8dfbe Refactor: Refactor P2P Replicator 2026-03-15 03:33:03 +09:00
vorotamoroz
33338506cf bump 2026-03-14 17:02:16 +09:00
vorotamoroz
9dd479e597 Fix for an issue where conflicts cannot be resolved in Journal Sync
Remove unnecessary test calling in CLI
2026-03-14 16:51:30 +09:00
vorotamoroz
8cad4cdf80 Add workaround for my mac 2026-03-14 16:50:43 +09:00
vorotamoroz
beced219c7 Fix: exit code 2026-03-14 16:13:14 +09:00
vorotamoroz
dfe13b1abd Fixed:
- No longer unexpected `Unhandled Rejections` during P2P operations (waiting acceptance).
CLI new features
- P2P sync has been implemented.
2026-03-14 15:08:31 +09:00
vorotamoroz
bf93bddbdd Fix: prevent transfer twice. 2026-03-13 23:34:38 +09:00
vorotamoroz
44890a34e8 remove conflicting option. 2026-03-13 23:08:05 +09:00
vorotamoroz
a14aa201a8 Merge branch 'beta' of https://github.com/vrtmrz/obsidian-livesync into beta 2026-03-13 18:13:12 +09:00
vorotamoroz
338a9ba9fa Add: mirror command
Tidy: test
2026-03-13 18:01:38 +09:00
vorotamoroz
0c65b5add9 Add: mirror command 2026-03-13 12:55:46 +09:00
vorotamoroz
29ce9a5df4 remove todo 2026-03-12 12:45:39 +01:00
vorotamoroz
10f5cb8b42 add paths 2026-03-12 12:31:53 +01:00
vorotamoroz
8aad3716d4 fix grammatical errors 2026-03-12 12:29:43 +01:00
vorotamoroz
d45f41500a Fix: no longer duplicated addLog setHandler 2026-03-12 12:27:47 +01:00
vorotamoroz
4cc0a11d86 Add ci cli-e2e 2026-03-12 12:23:13 +01:00
vorotamoroz
ad0a6b458f bump 2026-03-12 12:16:00 +01:00
vorotamoroz
6ae1d5d6a5 update readme 2026-03-12 12:07:05 +01:00
vorotamoroz
84110aee97 update readme 2026-03-12 19:46:52 +09:00
vorotamoroz
4646577f35 Update 2026-03-12 19:42:10 +09:00
vorotamoroz
822d957976 Refactor: separate entrypoint and main,
Fix: readlng binary file
2026-03-12 19:41:10 +09:00
vorotamoroz
d4aedf59f3 A- Add more tests.
- Object Storage support has also been confirmed (and fixed) in CLI.
2026-03-12 18:20:55 +09:00
vorotamoroz
5d80258a77 Add e2e-test (not passed yet)
Refine readme
2026-03-12 12:20:39 +09:00
vorotamoroz
fa14531599 Add note 2026-03-12 03:03:03 +09:00
vorotamoroz
7992b3c2b9 Wrote the test (but untested) 2026-03-12 03:01:46 +09:00
vorotamoroz
5872cad1e5 Implement commands 2026-03-12 02:56:30 +09:00
vorotamoroz
16c0dfef4c Remove the grandiloquence from the note written in work in progress. 2026-03-11 23:51:35 +09:00
vorotamoroz
70c7624c7a Add note 2026-03-11 14:58:06 +01:00
vorotamoroz
4a0d5e99d0 Fix import path and add readme 2026-03-11 14:57:36 +01:00
vorotamoroz
0742773e1e Add self-hosted-livesync-cli to src/apps/cli as a headless, and a dedicated version. 2026-03-11 14:51:01 +01:00
vorotamoroz
2f8bc4fef2 - Now useOfflineScanner, useCheckRemoteSize, and useRedFlagFeatures are set from main.ts, instead of LiveSyncBaseCore. 2026-03-11 14:44:37 +01:00
vorotamoroz
0dfd42259d 11th March, 2026
Now, Self-hosted LiveSync has finally begun to be split into the Self-hosted LiveSync plugin for Obsidian, and a properly abstracted version of it.
This may not offer much benefit to Obsidian plugin users, or might even cause a slight inconvenience, but I believe it will certainly help improve testability and make the ecosystem better.
However, I do not see the point in putting something with little benefit into beta, so I am handling this on the alpha branch. I would actually preferred to create an R&D branch, but I was not keen on the ampersand, and I feel it will eventually become a proper beta anyway.

### Refactored

- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`.
- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`.
- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`.

### Internal API changes

- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported.

### R&D

- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing.
- We can see the code in `src/apps/webapp` for the browser version.
2026-03-11 05:47:00 +01:00
vorotamoroz
9cf630320c Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-09 10:38:17 +09:00
vorotamoroz
584adc9296 bump 2026-03-09 10:25:19 +09:00
vorotamoroz
f7dba6854f ### Fixed
- No longer unexpected deletion-propagation occurs when the parent directory is not empty (#813).

### Revert reversions
- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature.
2026-03-09 10:24:49 +09:00
vorotamoroz
1d83e0ee31 manifest.json を更新 2026-03-08 17:24:54 +09:00
vorotamoroz
d0244bd6d0 bump 2026-03-07 18:36:54 +09:00
vorotamoroz
79bb5e1c77 ### Reverted
- Reverted to ModuleRedFlag and ModuleInitializerFile to the previous version because of some unexpected issues. (#813)
2026-03-07 18:36:10 +09:00
vorotamoroz
3403712e24 Downgrade version from 0.25.50 to 0.25.48 for #813 2026-03-07 13:13:37 +09:00
vorotamoroz
8faa19629b bump 2026-03-03 13:39:13 +00:00
vorotamoroz
7ff9c666ce Fix: No more credentials logged 2026-03-03 13:34:18 +00:00
vorotamoroz
d8bc2806e0 bump 2026-03-03 13:27:23 +00:00
vorotamoroz
62f78b4028 Modify unit-ci to upload coverage 2026-03-03 13:22:17 +00:00
vorotamoroz
cf9d2720ce ### Fixed
- No longer deleted files are not clickable in the Global History pane.
- Diff view now uses more specific classes (#803).
- A message of configuration mismatching slightly added for better understanding.
    - Now it says `When replication is initiated manually via the command palette or ribbon, a dialogue box will open to address this.` to make it clear that the user can fix the issue by themselves.

### Refactored

- `ModuleRedFlag` has been refactored to `serviceFeatures/redFlag` and also tested.
- `ModuleInitializerFile` has been refactored to `lib/serviceFeatures/offlineScanner` and also tested.
2026-03-03 13:19:22 +00:00
vorotamoroz
09115dfe15 Fixed: Styles on the diff-dialogue has been qualified. 2026-03-02 10:52:14 +00:00
vorotamoroz
4cbb833e9d bump 2026-03-02 09:36:43 +00:00
vorotamoroz
7419d0d2a1 Add unit-ci hook to push 2026-03-02 09:15:29 +00:00
vorotamoroz
f3e83d4045 Refactored: changed the implementation from using overrides to injecting an adapter. 2026-03-02 09:06:23 +00:00
vorotamoroz
28e06a21e4 bump 2026-02-27 11:15:23 +00:00
vorotamoroz
e08fbbd223 ### Fixed and refactored
- Fixed the inexplicable behaviour when retrieving chunks from the network.
    - Chunk manager has been layered to responsible its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`,and `ArrivalWaitLayer`.
        - All layers have test now!
        - `LayeredChunkManager` has been implemented to manage these layers. Also tested.
    - `EntryManager` has been mostly rewritten, and also tested.

- Now we can configure `Never warn` for remote storage size notification, again.
2026-02-27 11:00:30 +00:00
vorotamoroz
a1e331d452 bump 2026-02-26 11:34:14 +00:00
vorotamoroz
646f8af680 ### Fixed
- Unexpected errors no longer occurred when the plug-in was unloaded.
- Hidden File Sync now respects selectors.
- Registering protocol-handlers now works safely without causing unexpected errors.

### Refactored
- LiveSyncManagers has now explicit dependencies.
- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies.
    - This is to avoid circular dependencies and clarify the ownership of the managers.
- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly.
- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage.
2026-02-26 11:30:57 +00:00
vorotamoroz
392f76fd36 ### Refactored
- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and also tests have been added.
- Some unnecessary things have been removed.
2026-02-26 08:59:54 +00:00
vorotamoroz
f61a3eb85b bump 2026-02-25 09:39:55 +00:00
vorotamoroz
19c03ec8d8 ### Refactored
- `ModuleTargetFilter`, which was responsible for checking if a file is a target file, has been ported to a serviceFeature.
  - And also tests have been added. The middleware-style-power.
- `ModuleObsidianAPI` has been removed and implemented in `APIService` and `RemoteService`.
- Now `APIService` is responsible for the network-online-status, not `databaseService.managers.networkManager`.
2026-02-25 09:38:31 +00:00
vorotamoroz
be1642f1c1 Fix grammatical errors. 2026-02-24 11:20:59 +00:00
vorotamoroz
c9a71e2076 fix: align dependency versions to main 2026-02-24 11:14:14 +00:00
vorotamoroz
2199c1ebd3 change: test:docker-all:xxx to be parallel. 2026-02-24 11:13:58 +00:00
vorotamoroz
278935f85d Fix mock for testing 2026-02-24 11:08:08 +00:00
vorotamoroz
010631f553 bump dependencies 2026-02-24 10:38:35 +00:00
vorotamoroz
8c0c65307a bump 2026-02-24 08:23:08 +00:00
vorotamoroz
988cb34d7c Update 2026-02-24 07:56:23 +00:00
vorotamoroz
6eec8117f5 Refactor: Constantising log-mark 2026-02-24 07:51:21 +00:00
vorotamoroz
9f6a909143 Merge remote-tracking branch 'origin/main' into beta to port #802 2026-02-24 07:44:18 +00:00
vorotamoroz
09f283721a Merge remote-tracking branch 'origin/main' into beta 2026-02-24 04:13:34 +00:00
vorotamoroz
235c702223 Merge pull request #804 from A-wry/reduce-fetch-error-visibility
Add connection warning style logic and settings UI dropdown
2026-02-24 13:07:29 +09:00
A-wry
b923b43b6b update submodule pointer to latest commonlib changes 2026-02-23 22:29:46 -05:00
A-wry
fdcf3be0f9 Tagged downstream network errors to respect networkWarningStyle setting 2026-02-23 22:10:25 -05:00
vorotamoroz
25dd907591 port PR #802 2026-02-24 01:57:08 +00:00
vorotamoroz
80c049d276 Merge pull request #802 from waspeer/fix/write-file-auto-md-file-already-exists
fix: handle "File already exists" for .md files in writeFileAuto
2026-02-24 10:50:37 +09:00
L4z3x
310496d0b8 added changing docker compose data and etc folder, to prevent permissions errors
while trying to follow the docker compose guide i created the data folders using the root user, and had this error when i run the stack:
`touch: cannot touch '/opt/couchdb/etc/local.d/docker.ini': Permission denied`

the problem was solved by changing the ownership of the folder to the user 5984, then one in the docker compose file.
2026-02-23 22:21:18 +01:00
vorotamoroz
e961f01187 fix typos. 2026-02-21 14:17:28 +09:00
vorotamoroz
14b4c3cd50 prettified 2026-02-21 14:12:05 +09:00
vorotamoroz
4f987e7c2b ### Fixed
- Hidden file synchronisation now works!
- Now Hidden file synchronisation respects `.ignore` files.
- Replicator initialisation during rebuilding now works correctly.

### Refactored

- Some methods naming have been changed for better clarity, i.e., `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`.
2026-02-21 14:05:32 +09:00
A-wry
f4d8c0a8db Add connection warning style logic and settings UI dropdown 2026-02-20 21:51:30 -05:00
vorotamoroz
556ce471f8 ## 0.25.43-patched-8 2026-02-20 14:28:28 +00:00
vorotamoroz
32b6717114 keep a note 2026-02-19 10:38:42 +00:00
vorotamoroz
e0e72fae72 Fixed: saving device name 2026-02-19 10:37:19 +00:00
vorotamoroz
203dd17421 for 0.25.43-patched-7, please refer to the updates.md 2026-02-19 10:23:45 +00:00
vorotamoroz
1bde2b2ff1 Fixed an issue where the StorageEventManager
Build by Vite is now testing
2026-02-19 04:18:18 +00:00
mugenrei
ebbac96b16 docs: add comprehensive Cloudflare Tunnel setup guide
- Step-by-step tunnel creation in Zero Trust dashboard
- Detailed public hostname configuration (couchdb:5984 vs localhost)
- Troubleshooting section for 404, 502, and 524 errors
- Obsidian plugin configuration instructions
- Emphasis on token-based tunnels ignoring local config file
2026-02-18 10:57:18 -03:00
mugenrei
ed0e67e853 fix: CRLF line endings in couchdb-init.sh, add .gitattributes
Shell scripts with CRLF line endings crash inside Linux containers with
'set: illegal option -' and 'command not found' errors.

- Strip CRLF -> LF in couchdb-init.sh
- Add .gitattributes to enforce LF on .sh, .yml, .ini, .md etc.
  so this never regresses on Windows checkouts
2026-02-18 10:52:51 -03:00
mugenrei
f99d7dc00f Add initial Docker setup and configuration for self-hosted LiveSync with CouchDB 2026-02-18 09:46:49 -03:00
vorotamoroz
2bf1c775ee ## 0.25.43-patched-6
### Fixed

- Unlocking the remote database after rebuilding has been fixed.

### Refactored
- Now `StorageEventManagerBase` is separated from `StorageEventManagerObsidian` following their concerns.
- Now `FileAccessBase` is separated from `FileAccessObsidian` following their concerns.
2026-02-18 12:13:05 +00:00
vorotamoroz
4658e3735d Fix Shim 2026-02-17 10:56:05 +00:00
vorotamoroz
627edc96bf bump for beta 2026-02-17 10:14:13 +00:00
vorotamoroz
0a1917e83c Refactor for 0.25.43-patched-5 (very long, please refer the updates.md) 2026-02-17 10:14:04 +00:00
Wannes Salomé
48b0d22da6 fix: handle "File already exists" for .md files in writeFileAuto
During concurrent initialisation (UPDATE STORAGE runs up to 10 ops in
parallel), getAbstractFileByPath can return null for .md files whose
vault index entry hasn't been populated yet, even though the file
already exists on disk. This causes vault.create() to throw "File
already exists."

The same root cause (stale in-memory index) was already identified for
non-.md files (see comment above) and handled via adapterWrite. Extend
that workaround to .md files by catching the "File already exists"
error and falling back to adapterWrite, consistent with the existing
approach.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 00:24:30 +01:00
vorotamoroz
3201399bdf bump 2026-02-16 11:51:09 +00:00
vorotamoroz
2ae70e8f07 Refactor: DatabaseService and Replicator 2026-02-16 11:51:03 +00:00
vorotamoroz
2b9bb1ed06 beta bump 2026-02-16 06:51:52 +00:00
vorotamoroz
e63e3e6725 ### Refactor
- Module dependency refined. (For details, please refer to updates.md)
2026-02-16 06:50:31 +00:00
vorotamoroz
6e9ac6a9f9 - Application LifeCycle has now started in Main, not ServiceHub. 2026-02-14 15:21:00 +09:00
vorotamoroz
fb59c4a723 Refactored, please refer updates.md 2026-02-13 12:02:31 +00:00
vorotamoroz
1b5ca9e52c Refactor (write notes later) 2026-02-12 08:56:30 +00:00
vorotamoroz
787627a156 Refactor: Move some functions from modules to services 2026-02-12 06:27:29 +00:00
vorotamoroz
b1bba7685e Add note. 2026-02-12 03:39:56 +00:00
vorotamoroz
cdfc0ccead wow, auditing... bump. 2026-02-05 12:15:35 +00:00
vorotamoroz
0635cad350 bake i18n 2026-02-05 12:13:12 +00:00
vorotamoroz
6fd1fa6313 Fix typos and make P2P sync not experimental 2026-02-05 12:07:29 +00:00
vorotamoroz
12b1f881dc ### Fixed
- Encryption/decryption issues when using Object Storage as remote have been fixed.
2026-02-05 11:47:01 +00:00
vorotamoroz
bf3efab1af Merge pull request #733 from dayne/patch-2
Update regions setup-flyio-on-the-fly-v2.ipynb
2026-02-02 13:51:48 +09:00
vorotamoroz
da72fda221 Merge pull request #777 from oenhu/patch-1
Update README_cn.md
2026-02-02 13:42:14 +09:00
vorotamoroz
665501f485 Merge pull request #778 from oenhu/patch-2
Create tech_info_cn.md
2026-02-02 13:40:17 +09:00
vorotamoroz
6ee332fff8 Merge pull request #779 from oenhu/main
refactor: replace hardcoded strings with i18n keys
2026-02-02 13:38:01 +09:00
vorotamoroz
f66447cb59 Fix task 2026-02-02 13:00:28 +09:00
vorotamoroz
eb3120a8fd Fix CI 2026-02-02 12:57:49 +09:00
vorotamoroz
5fa39b3c6e Fix typo 2026-02-02 12:28:26 +09:00
vorotamoroz
91c35a88dd separate CIs 2026-02-02 12:27:37 +09:00
vorotamoroz
49f4d79f4f Update dependencies (Mostly translations) 2026-02-02 12:27:15 +09:00
vorotamoroz
abfd010467 Add the detail 2026-02-02 11:39:28 +09:00
vorotamoroz
cde1013359 Reducing ambiguity 2026-02-02 11:32:52 +09:00
vorotamoroz
9c7f9e4316 Tidy 2026-02-02 11:31:40 +09:00
vorotamoroz
aceda16c64 Add beta policy note 2026-02-02 11:31:06 +09:00
vorotamoroz
3656e5c725 Merge branch 'beta' 2026-02-02 11:00:12 +09:00
vorotamoroz
6915b160a2 Bump to stable 2026-02-02 10:57:15 +09:00
vorotamoroz
f464623bf6 revert for release script 2026-01-30 09:36:39 +00:00
vorotamoroz
b1f518071c add note 2026-01-30 09:27:31 +00:00
vorotamoroz
0e903c3520 Change: enabling PATH_TEST_INSTALL in production build 2026-01-30 09:25:12 +00:00
vorotamoroz
edcdfd97c4 Reduce dynamic function binding from APIService from 2026-01-30 09:23:54 +00:00
vorotamoroz
c2b7081215 Add some notes 2026-01-30 07:39:03 +00:00
vorotamoroz
cf1954b10e Bump for beta 2026-01-29 07:15:22 +00:00
vorotamoroz
46546e121f Refactor: Move webpeer from lib to main repository. 2026-01-26 09:17:01 +00:00
vorotamoroz
28146eec2c Refactor: Migrate the outdated, unstable platform abstraction layer to Services 2026-01-26 09:13:40 +00:00
vorotamoroz
3cd9b9e06d bump 2026-01-24 16:48:51 +09:00
vorotamoroz
7c43c61b85 ### Fixed
- No longer `No available splitter for settings!!` errors occur after fetching old remote settings while rebuilding local database.

### Improved

- Boot sequence warning is now kept in the in-editor notification area. (#748)

### New feature

- We can now set the maximum modified time for reflect events in the settings. (for #754)

### Refactored
- Module to service refactoring has been started for better maintainability:
  - UI module has been moved to UI service.

### Behaviour change
- Default chunk splitter version has been changed to `Rabin-Karp` for new installations.
2026-01-24 16:25:04 +09:00
vorotamoroz
465af4f3aa Urg: 0.25.40 for fix wrong eventType 2026-01-23 11:55:30 +00:00
vorotamoroz
0a1e3dcd51 bump 2026-01-23 06:27:06 +00:00
vorotamoroz
b97756d0cf add unittest 2026-01-23 05:45:53 +00:00
vorotamoroz
acf4bc3737 prettified 2026-01-23 05:45:19 +00:00
vorotamoroz
88838872e7 refactor_serivces
- Rewrite the service's binding/handler assignment systems
- Removed loopholes that allowed traversal between services to clarify dependencies.
- Consolidated the hidden state-related state, the handler, and the addition of bindings to the handler into a single object.
  - Currently, functions that can have handlers added implement either addHandler or setHandler directly on the function itself.
I understand there are differing opinions on this, but for now, this is how it stands.
- Services now possess a Context. Please ensure each platform has a class that inherits from ServiceContext.
- To permit services to be dynamically bound, the services themselves are now defined by interfaces.
2026-01-23 05:44:14 +00:00
vorotamoroz
7d3827d335 bump 2026-01-17 14:36:47 +09:00
vorotamoroz
92d3a0cfa2 ### Fixed
- Fixed an issue where indexedDB would not close correctly on some environments, causing unexpected errors during database operations.
2026-01-17 14:32:44 +09:00
vorotamoroz
bba26624ad bump 2026-01-15 04:12:19 +00:00
vorotamoroz
b82f497cab Add development guide link 2026-01-15 04:04:49 +00:00
vorotamoroz
37f4d13e75 Merge branch 'main' of https://github.com/vrtmrz/obsidian-livesync 2026-01-15 03:59:53 +00:00
vorotamoroz
7965f5342c Fix type 2026-01-15 12:47:26 +09:00
vorotamoroz
9cdc14dda8 Fix: CI 2026-01-15 12:45:29 +09:00
vorotamoroz
4f46276ebf Fix for CI 2026-01-15 12:45:13 +09:00
vorotamoroz
931d360fb1 Add summary note 2026-01-14 09:41:16 +00:00
vorotamoroz
f68c1855da Remove obsoleted file. 2026-01-14 08:07:40 +00:00
vorotamoroz
dff654b6e5 Fixed:
- Fixed mishandling of removing  duplicated queue on processing
- Dependency importing
2026-01-14 08:07:22 +00:00
vorotamoroz
7e85bcbf08 harness-ci.yml mod ci 2026-01-10 12:20:17 +09:00
vorotamoroz
38a695ea12 Fix actions 2026-01-09 12:18:38 +00:00
vorotamoroz
a502b0cd0c Split the tests 2026-01-09 12:15:57 +00:00
vorotamoroz
934f708753 Improve timing issue 2026-01-09 12:05:40 +00:00
vorotamoroz
0e574c6cb1 Flag executable 2026-01-09 11:50:23 +00:00
vorotamoroz
7375a85b07 Tests:
- More tests have been added.
2026-01-09 11:46:37 +00:00
vorotamoroz
4c3393d8b2 Fixed:
- Databases now correctly closed after rebuilding.
2026-01-09 11:45:00 +00:00
oenhu
a9f1bbff9f refactor: replace hardcoded strings with i18n keys 2026-01-09 00:29:40 +08:00
oenhu
f86815e420 Create tech_info_cn.md
Create Chinese version
2026-01-08 15:07:06 +08:00
oenhu
fd16b166ef Update README_cn.md
Update the translation content based on the latest English REDME.md.
2026-01-08 01:41:07 +08:00
vorotamoroz
02aa9319c3 Remove chromiumSandbox for Actions 2026-01-07 09:15:31 +00:00
vorotamoroz
1a72e46d53 Fix excluded 2026-01-07 09:12:21 +00:00
vorotamoroz
d755579968 Add excluded 2026-01-07 09:08:14 +00:00
vorotamoroz
b74ee9df77 Fix to pulling submodules 2026-01-07 09:03:18 +00:00
vorotamoroz
daa04bcea8 Add actions 2026-01-07 09:01:05 +00:00
vorotamoroz
b96b2f24a6 Add some script and run npm install 2026-01-07 09:00:55 +00:00
vorotamoroz
5569ab62df Change Port 2026-01-07 09:00:16 +00:00
vorotamoroz
d84b6c4f15 Flag executable 2026-01-07 08:41:43 +00:00
vorotamoroz
336f2c8a4d Add Test 2026-01-07 08:38:33 +00:00
vorotamoroz
b52ceec36a Update submodule 2026-01-07 08:33:06 +00:00
vorotamoroz
1e6400cf79 Tidied:
- Add some note for corrupting notice
- Array function standardised.

Fixed:
- Remove obsoleted accessing to Obsidian's global.
- Avoiding errors in exceptional circumstances.
- Removal of several outdated and inefficient QueueProcessor implementations
  - Log output is now a bit efficient.
2026-01-07 04:45:19 +00:00
vorotamoroz
1ff1ac951b - Remove old comments
- Fix Importing paths
2026-01-07 04:37:30 +00:00
vorotamoroz
aa6d771d17 bump 2025-12-25 10:30:04 +00:00
vorotamoroz
512c238415 ### Improved
- Now the garbage collector (V3) has been implemented. (Beta)
- Now the plug-in and device information is stored in the remote database.
2025-12-25 10:29:19 +00:00
vorotamoroz
55ffeeda10 bump 2025-12-24 03:38:40 +00:00
vorotamoroz
65f18b4160 ### Fixed
- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764).
2025-12-24 03:37:41 +00:00
vorotamoroz
b0c1d6a1bf bump 2025-12-10 09:48:25 +00:00
vorotamoroz
ca19f2f2ed Modify build script and prettier config type (.prettierrc to .prettierrc.mjs 2025-12-10 09:47:31 +00:00
vorotamoroz
ac0378ca4b ### Behaviour change
- The plug-in automatically fetches the missing chunks even if `Fetch chunks on demand` is disabled.
    - This change is to avoid loss of data when receiving a bulk of revisions.
    - This can be prevented by enabling `Use Only Local Chunks` in the settings.
- Storage application now saved during each event and restored on startup.
- Synchronisation result application is also now saved during each event and restored on startup.
    - These may avoid some unexpected loss of data when the editor crashes.

### Fixed

- Now the plug-in waits for the application of pended batch changes before the synchronisation starts.
    - This may avoid some unexpected loss or unexpected conflicts.
      Plug-in sends custom headers correctly when RequestAPI is used.
- No longer causing unexpected chunk creation during `Reset synchronisation on This Device` with bucket sync.

### Refactored

- Synchronisation result application process has been refactored.
- Storage application process has been refactored.
    - Please report if you find any unexpected behaviour after this update. A bit of large refactoring.
2025-12-10 09:45:57 +00:00
vorotamoroz
f06f8d1eb6 bump 2025-12-05 11:08:48 +00:00
vorotamoroz
77074cb92f ### New feature
- We can analyse the local database with the `Analyse database usage` command.
- We can reset the notification threshold and check the remote usage at once with the `Reset notification threshold and check the remote database usage` command.

### Fixed

- Now the plug-in resets the remote size notification threshold after rebuild.
2025-12-05 11:07:59 +00:00
vorotamoroz
1274b6f683 Grammatical Fix 2025-12-02 11:28:56 +00:00
vorotamoroz
c54ae58c0f bump 2025-12-02 11:24:31 +00:00
vorotamoroz
3f54921e90 ### Improved
- Now the plugin warns when we are on the several file-related situations that may cause unexpected behaviour (#300).
2025-12-02 11:20:24 +00:00
vorotamoroz
1b070c2dd4 bump 2025-11-18 08:54:41 +00:00
vorotamoroz
0e5846b670 add BUILD_MODE switching by environment variables. 2025-11-18 08:52:25 +00:00
vorotamoroz
f81e71802b update dependency 2025-11-18 08:51:51 +00:00
vorotamoroz
4c761eebff ### Fixed
- Now fetching configuration from server can handle the empty remote correctly (reported on #756) by common-lib.
- No longer asking to switch adapters during rebuilding.
2025-11-18 08:51:32 +00:00
vorotamoroz
bf754d6e07 Merge branch 'dev' 2025-11-17 23:37:39 +09:00
vorotamoroz
1c49acd5b5 Merge pull request #755 from vrtmrz/dev
v0.25.29
2025-11-17 13:32:32 +09:00
Dayne Broderson
c76187c6d2 Update setup-flyio-on-the-fly-v2.ipynb
remove extranious newline
2025-10-29 17:07:34 -08:00
Dayne Broderson
b3b3ad843c Update regions setup-flyio-on-the-fly-v2.ipynb
Updating the regions list pulled from recent `fly platform regions`
2025-10-22 09:26:13 -08:00
574 changed files with 66382 additions and 23666 deletions

33
.dockerignore Normal file
View File

@@ -0,0 +1,33 @@
# 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/
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

View File

@@ -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
View 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

View File

@@ -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
View 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-sync
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

134
.github/workflows/cli-docker.yml vendored Normal file
View File

@@ -0,0 +1,134 @@
# 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
#
# The image is also tagged 'latest' for convenience.
# 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"
- ".github/workflows/harness-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
# Stable release builds
TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${MAJOR_MINOR}-cli,${IMAGE}:latest,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli"
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
View 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

View 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
View 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

100
.github/workflows/finalise-release.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
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
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 }}
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
node utils/release-notes.mjs validate "${VERSION}"
- name: Ensure and push release tags
env:
VERSION: ${{ inputs.version }}
EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }}
run: |
set -euo pipefail
git fetch --tags --force
node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}"
git push --atomic origin "refs/tags/${VERSION}" "refs/tags/${VERSION}-cli"
- name: Dispatch release workflows
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
gh workflow run release.yml \
--ref "${VERSION}" \
--field tag="${VERSION}" \
--field draft=true \
--field prerelease=false
gh workflow run cli-docker.yml \
--ref "${VERSION}-cli" \
--field dry_run=false \
--field force=false
- name: Summarise next steps
env:
VERSION: ${{ inputs.version }}
run: |
{
echo "Ensured tags \`${VERSION}\` and \`${VERSION}-cli\` point to the reviewed release commit."
echo ""
echo "Dispatched the plug-in release workflow for \`${VERSION}\`. After approval for the release environment, it creates a draft GitHub Release."
echo "Dispatched the CLI Docker workflow for \`${VERSION}-cli\`. It publishes the version, major-minor, latest, and SHA-qualified image tags."
echo ""
echo "Keep the release pull request in draft after publishing the GitHub Release as the latest stable release. Mark it ready and merge it only after BRAT validation succeeds."
} >> "$GITHUB_STEP_SUMMARY"

66
.github/workflows/harness-ci.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
# Run tests by Harnessed CI
name: harness-ci
on:
workflow_dispatch:
inputs:
testsuite:
description: 'Run specific test suite (leave empty to run all)'
type: choice
options:
- ''
- 'suite/'
- 'suitep2p/'
default: ''
permissions:
contents: read
jobs:
test:
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: Install test dependencies (Playwright Chromium)
run: npm run test:install-dependencies
- name: Start test services (CouchDB)
run: npm run test:docker-couchdb:start
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }}
- name: Start test services (MinIO)
run: npm run test:docker-s3:start
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }}
- name: Start test services (Nostr Relay + WebPeer)
run: npm run test:docker-p2p:start
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }}
- name: Run tests suite
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }}
env:
CI: true
run: npm run test suite/
- name: Run P2P tests suite
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }}
env:
CI: true
run: npm run test:p2p
- name: Stop test services (CouchDB)
run: npm run test:docker-couchdb:stop
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }}
- name: Stop test services (MinIO)
run: npm run test:docker-s3:stop
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }}
- name: Stop test services (Nostr Relay + WebPeer)
run: npm run test:docker-p2p:stop
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }}

114
.github/workflows/prepare-release.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
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
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: |
cat > /tmp/release-pr-body.md <<EOF
> [!IMPORTANT]
> **Merge intentionally on hold**
>
> Publishing the GitHub Release does not unblock this pull request. Keep this pull request in draft, and leave \`main\` on the previous release, until the published build has passed BRAT validation.
## Release checklist
- [ ] Review and polish \`updates.md\`
- [ ] Confirm the release date
- [ ] Confirm \`manifest.json\`, \`versions.json\`, workspace package versions, and the locked Commonlib package version
- [ ] Confirm CI has passed
- [ ] Run the finalise release workflow with this PR's fixed head SHA
- [ ] Confirm the draft GitHub Release assets and the published CLI image
- [ ] Publish the GitHub Release as the latest stable release while keeping this pull request in draft
- [ ] Validate the published release with BRAT
- [ ] Mark this pull request ready and merge it with a merge commit
EOF
gh pr create \
--base "${BASE_BRANCH}" \
--head "${RELEASE_BRANCH}" \
--draft \
--title "Releasing ${VERSION}" \
--body-file /tmp/release-pr-body.md

View File

@@ -1,96 +1,71 @@
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
# Attest
- name: Attest Plugin Artifacts
uses: actions/attest-build-provenance@v4
with:
subject-path: |
main.js
manifest.json
styles.css
# 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
- name: Create Release and Upload Assets
uses: softprops/action-gh-release@v2
with:
@@ -99,6 +74,7 @@ jobs:
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 }}

133
.github/workflows/unit-ci.yml vendored Normal file
View File

@@ -0,0 +1,133 @@
# 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'
- '.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'
- '.github/workflows/prepare-release.yml'
- '.github/workflows/finalise-release.yml'
- '.github/workflows/release.yml'
- '.github/workflows/unit-ci.yml'
permissions:
contents: read
jobs:
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: 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: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Create environment configuration files
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
run: npm run test:docker-couchdb:start
- name: Start MinIO container
run: npm run test:docker-s3:start
- name: Run integration tests
run: npm run test:integration
- name: Stop containers
if: always()
run: |
npm run test:docker-couchdb:stop || true
npm run test:docker-s3:stop || true

13
.gitignore vendored
View File

@@ -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
View File

@@ -1,3 +0,0 @@
[submodule "src/lib"]
path = src/lib
url = https://github.com/vrtmrz/livesync-commonlib

View File

@@ -1,2 +1,4 @@
pouchdb-browser.js
main_org.js
main.js
_types/**

View File

@@ -1,7 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"printWidth": 120,
"semi": true,
"endOfLine": "lf"
}

20
.prettierrc.mjs Normal file
View 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
View 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

68
AGENTS.md Normal file
View File

@@ -0,0 +1,68 @@
# 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` or `npm run test:full` for full testing suites (including dockerised services).
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
View 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
View 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.

View File

@@ -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).
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
@@ -24,18 +24,18 @@ Additionally, it supports peer-to-peer synchronisation using WebRTC now (experim
- 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,65 @@ This plug-in may be particularly useful for researchers, engineers, and develope
[![LiveSync Setup onto Fly.io SpeedRun 2024 using Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](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
### Manual Setup
1. Setup the server
1. [Setup CouchDB on fly.io](docs/setup_flyio.md)
2. [Setup your CouchDB](docs/setup_own_server.md)
1. Set up the server
1. [Set up CouchDB on fly.io](docs/setup_flyio.md)
2. [Set up 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).
- 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.

View File

@@ -1,9 +1,12 @@
# Self-hosted LiveSync
Self-hosted LiveSync (自搭建在线同步) 是一个社区实现的在线同步插件。
使用一个自搭建的或者购买的 CouchDB 作为中转服务器。兼容所有支持 Obsidian 的平台。
它利用诸如CouchDB或对象存储系统例如MinIO、S3、R2等等强大的服务器解决方案以确保数据同步的可靠性。。兼容所有支持 Obsidian 的平台。
注意: 本插件与官方的 "Obsidian Sync" 服务不兼容
此外它现在支持使用WebRTC进行点对点同步实验性功能使您无需依赖服务器即可直接在设备之间同步笔记
>[!IMPORTANT]
>本插件与官方的 "Obsidian Sync" 服务不兼容。
![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif)
@@ -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)。
适用于出于安全原因需要将笔记完全自托管的研究人员、工程师或开发人员,以及任何喜欢笔记完全私密所带来的安全感的人。
此插件适用于出于安全原因需要将笔记完全自托管的研究人员、工程师或开发人员,以及任何喜欢笔记完全私密所带来的安全感的人。
## 重要提醒
- 请勿与其同步解决方案(包括 iCloudObsidian Sync一起使用。在启用此插件之前,请确保禁用所有其他同步方法以避免内容损坏或重复。如果要同步到多个服务,请一一进行,切勿同时启用两种同步方法
这包括不能将您的保管库放在云同步文件夹中(例如 iCloud 文件夹或 Dropbox 文件夹)
- 这是一个同步插件,不是备份解决方案。不要依赖它进行备份。
- 如果设备的存储空间耗尽,可能会发生数据库损坏。
- 隐藏文件或任何其他不可见文件不会保存在数据库中,因此不会被同步。(**并且可能会被删除**
>[!IMPORTANT]
> - 在安装或升级此插件之前,请务必备份您的保险库。
> - 请勿同时启用此插件与其同步方案包括iCloudObsidian 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)
**推荐初学者第一次使用此方法**
[![LiveSync Setup onto Fly.io SpeedRun 2024 using Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](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 许可协议授权。

View File

@@ -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

92
aggregator.html Normal file
View 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
View 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:unit # Run unit tests only (no Docker services required)
npm test # Run Harness based vitest tests (requires Docker services), not recommended, unstable.
```
### Tips
Use CLI E2E tests or real Obsidian E2E scripts instead of `npm test` when the behaviour can be verified outside the browser harness.
### 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.
- **Browser Harness Tests** (`vitest.config.ts`): Transitional browser-based harness tests using Playwright/Chromium. Executed via `npm run test`. This layer is no longer the preferred destination for new broad E2E coverage because `test/harness` can drift from real Obsidian behaviour.
- **P2P Tests** (`vitest.config.p2p.ts`): Browser-based Peer-to-Peer replication tests. Executed via `npm run test:p2p`.
- **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.
- **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**: Tests require CouchDB, MinIO (S3), and P2P services:
```bash
npm run test:docker-all:start # Start all test services
npm run test:full # Run tests with coverage
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
- `test/suite/` - Transitional browser harness tests for sync operations
- `test/unit/` - Unit tests (via vitest, as harness is browser-based)
- `test/harness/` - Transitional mock implementations (e.g., `obsidian-mock.ts`). Avoid adding broad new E2E coverage here unless it is explicitly a compatibility bridge.
### 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
### 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.
- 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 `src/common/messagesYAML/` in the `livesync-commonlib` repository
2. Run `npm run i18n:bake` in that repository 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`, `ja`, `ko`, `ru`, `zh`, `zh-tw`
### 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
## Beta Policy
- Beta versions are denoted by appending `-patchedN` to the base version number.
- `The base version` mostly corresponds to the stable release version.
- e.g., v0.25.41-patched1 is equivalent to v0.25.42-beta1.
- This notation is due to SemVer incompatibility of Obsidian's plugin system.
- Hence, this release is `0.25.41-patched1`.
- Each beta version may include larger changes, but bug fixes will often not be included.
- I think that in most cases, bug fixes will cause the stable releases.
- They will not be released per branch or backported; they will simply be released.
- Bug fixes for previous versions will be applied to the latest beta version.
This means, if xx.yy.02-patched1 exists and there is a defect in xx.yy.01, a fix is applied to xx.yy.02-patched1 and yields xx.yy.02-patched2.
If the fix is required immediately, it is released as xx.yy.02 (with xx.yy.01-patched1).
- This procedure remains unchanged from the current one.
- At the very least, I am using the latest beta.
- However, I will not be using a beta continuously for a week after it has been released. It is probably closer to an RC in nature.
In short, the situation remains unchanged for me, but it means you all become a little safer. Thank you for your understanding!
## 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. 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.
- 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 both the plug-in tag (for example, `0.25.81`) and the CLI tag (for example, `0.25.81-cli`) point to that commit, and explicitly dispatches the plug-in and CLI publishing workflows. The workflow can be retried when existing tags already point to the reviewed commit, but stops if either 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. Confirm that the CLI workflow has published the fixed version tag, the major-minor moving tag (for example, `0.25-cli`), `latest`, and the SHA-qualified tag.
- Publish the draft GitHub Release as the latest stable release while keeping the release PR in draft and leaving `main` unchanged. Record the state in the PR with: 'Release `<version>` has been published as the latest stable release. This pull request intentionally remains in draft, and `main` has not yet been updated. Merge is on hold until BRAT validation is complete.'
- 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 with a merge commit. This keeps the tagged release commit in the `main` history.
- If BRAT validation fails, keep the release PR in draft. Do not move published tags; prepare and publish a new patch release instead.
- If a pre-release is needed, run the `Release Obsidian Plugin` workflow manually with the target tag, `draft=false`, and `prerelease=true`.
### 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`.
- `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.
5. Approve the `Release Obsidian Plugin` workflow for the `release` environment, then check the generated draft GitHub Release.
6. Confirm that the explicitly dispatched CLI Docker workflow has published the expected image tags.
7. Publish the draft GitHub Release as the latest stable release, but keep the release PR in draft and leave `main` unchanged.
8. Update the PR state message to say that the release is now the latest stable release and that merging is intentionally 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 `main` with a merge commit.
11. If validation fails, leave the PR in draft and prepare a new patch release without moving the published tags.
12. If the release should be a pre-release instead of a draft release, run `Release Obsidian Plugin` manually with the target `tag`, `draft=false`, and `prerelease=true`.
## 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).

View File

@@ -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
View 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
View 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
View 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
}
}
}

View 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

View 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

View 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
View 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

View 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}"

View File

@@ -2,9 +2,9 @@
## Getting ready
1. Clone this repository recursively.
1. Clone the Commonlib repository, which owns the translation catalogue.
```sh
git clone --recursive https://github.com/vrtmrz/obsidian-livesync
git clone https://github.com/vrtmrz/livesync-commonlib
```
2. Make `ls-debug` folder under your vault's `.obsidian` folder (as like `.../dev/.obsidian/ls-debug`).
@@ -12,15 +12,15 @@ git clone --recursive https://github.com/vrtmrz/obsidian-livesync
1. Install dependencies, and build the plug-in as dev. build.
```sh
cd obsidian-livesync
npm i -D
npm run buildDev
cd livesync-commonlib
npm ci
npm run i18n:bake
```
2. Copy the `main.js` to `.obsidian/plugins/obsidian-livesync` folder of your vault, and run Obsidian-Self-hosted LiveSync.
2. Install the resulting packed Commonlib artefact in a Self-hosted LiveSync development checkout, build the plug-in, copy `main.js` to `.obsidian/plugins/obsidian-livesync` in your test Vault, and run 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`.
5. Add the translations to the Commonlib catalogue, run `npm run i18n:bake`, and make the pull request to `https://github.com/vrtmrz/livesync-commonlib`.
## Make messages to be translated
@@ -31,4 +31,4 @@ npm run buildDev
+ 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.
4. Follow the steps of "Add translations for already defined terms" to add the translations.

View File

@@ -0,0 +1,251 @@
# Architectural Decision Record: Real Obsidian End-to-End Test Runner
## Status
Proposed / Spike Implemented
## Release
Not yet. Planned after the serviceFeature refactoring branch is reviewed.
## Context
The current end-to-end tests run through Vitest browser mode and a mocked Obsidian environment in `test/harness`. This has been useful for exercising synchronisation flows without launching Obsidian, but it is 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.
The existing `test/harness` should be demoted to a transitional compatibility layer. It may remain temporarily while the real Obsidian runner reaches parity for critical flows, but new high-level E2E coverage should target 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 experimental Markdown conflict automatic merge check is available with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true` but is not 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 and MinIO have been started.
- `npm run test:e2e:obsidian:local-suite:services` stops leftover CouchDB and MinIO 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, CouchDB, and MinIO. The run completed discovery, smoke, vault reflection, CouchDB upload, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export. The build step still emits existing Svelte warnings.
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.
- Cross-platform support is still discovery-level. The working path has been validated on Linux ARM64. macOS and Windows should be validated in their own environments as follow-up work.
- 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. Markdown conflict automatic merging remains an optional check because it needs a dedicated, less timing-sensitive fixture.
- 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:
- `test/harness` is now documented as a transitional compatibility layer.
- New broad E2E work should target `test/e2e-obsidian/` when real Obsidian behaviour is the risk being tested.
- The next high-value scenarios are RedFlag variants and Fast Setup (Simple Fetch) variants, not a line-by-line migration of `test/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 and MinIO fixtures have been started.
- Provide `test:e2e:obsidian:local-suite:services` for a broader local pass that manages the CouchDB and MinIO 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. Mark `test/harness` as transitional and block new broad E2E work from targeting it.
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 can be deleted once the new runner covers the critical workflows.
- The project will gain slower but higher-confidence tests for the behaviours most likely to differ between mocks and Obsidian itself.

View File

@@ -0,0 +1,122 @@
# 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.
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 local storage reflection in this boundary. It is re-entrant and offline-capable, and needs a separate decision if activity reporting or power policy is added later.
## Verification
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.
- Users can now distinguish the lifetime of a finite remote operation from approximate request activity within it.

View 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.

View File

@@ -0,0 +1,322 @@
# Architectural Decision Record: Package the Common Library Behind Explicit Host Boundaries
## Status
Proposed — the implementation proof is complete, and `@vrtmrz/livesync-commonlib@0.1.0-rc.1` has been published and verified as the exact downstream dependency; the community-scanner preview remains pending.
## 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 generated catalogue. Core services receive a narrow translator capability, or a service-context equivalent, instead of importing a process-global `$msg` implementation which statically owns every language.
The first migration keeps language resources in the common-library repository and exposes them temporarily through the focused compatibility subpath `@vrtmrz/livesync-commonlib/compat/common/i18n`. Importing the root or `context` entry does not load the catalogue. Self-hosted LiveSync imports the catalogue and installs the translator at its composition root. A dedicated language subpath remains the preferred stable replacement once its contract has been narrowed.
This is initially one versioned npm package rather than two independently versioned packages. Message identifiers and their generated catalogue change together, so an immediate package split would add version coordination before the dependency direction is proven. A later `@vrtmrz/livesync-language` package is permitted when independent use or release cadence justifies it. Its dependency must point towards message contracts or core, never from core towards the catalogue.
### 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 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 common-library i18n tooling out of Self-hosted LiveSync release preparation.
- 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`, `browser`, `node`, and `rpc` entries, plus the explicit compatibility exports required by the current downstream migration. It publishes neither raw TypeScript nor Svelte source. The immutable `@vrtmrz/livesync-commonlib@0.1.0-rc.1` registry artefact 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. Its registry version and checksum are recorded by release validation.
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. The old `src/lib` Git submodule, generated `_types` fallback, type-generation scripts, and source aliases are removed by the proof branch.
The Commonlib contract suite passes 21 tests covering Context results, both platform storage implementations, and standard I/O; its complete suite passes 1,092 tests across 57 files. Self-hosted LiveSync's three host-composition contract tests and complete 340-test unit suite pass against the registry artefact. The plug-in, CLI, Webapp, and WebPeer production builds also pass. The CLI contract command completes its nine-step Deno workflow against the built Node artefact.
The Community review lint completes with no errors. Advisory warnings remain in pre-existing UI sentence casing, deprecated Obsidian interfaces, unchecked dynamic setting data, and application-specific console output, especially in the Webapp. They are separate follow-up work and do not justify widening the package boundary or exposing additional compatibility paths.
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, and setting Markdown export. These checks establish observable results and host composition rather than relying on declaration compatibility alone.
The current browser Harness has a pre-existing settings-migration initialisation timeout which reproduces on the untouched baseline. The Webapp Playwright workflow also currently reuses an unrelated process on its configured port and reaches a `Not Found` page. Neither failure is evidence for or against the package boundary. The Webapp production build and direct composition contract pass, while Webapp browser E2E remains supplementary until its runner is repaired. Current acceptance therefore relies primarily on Commonlib owner and packed-consumer tests, LiveSync unit and integration tests, Deno CLI E2E, application production builds, and the focused local real-Obsidian suite. The unrelated Harness and Playwright defects should be tracked independently.
## Scanner and Repository Consequences
Consuming the npm package is expected to remove the common-library source and generated `_types` from the plug-in repository's community scan, because package dependencies are treated as dependencies rather than maintained plug-in source. The proof removes the old integration, but this scanner expectation must still be verified before the decision is accepted and released.
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 migration is complete 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;
- two clients with different database, encryption, language, and lifecycle settings can coexist without sharing mutable client state;
- Self-hosted LiveSync verifies its local Svelte adapter and workflow policy through injected tests and a focused composition smoke test;
- if the framework-neutral Modal host is later promoted to Fancy Kit, Kit-owned lifecycle, viewport, safe-area, and touch-target guarantees replace duplicated device tests in LiveSync;
- Self-hosted LiveSync no longer has `src/lib`, `_types`, or `@lib/*` source aliases;
- the CLI, Webapp, WebPeer, plug-in build, unit tests, integration tests, 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 scanner preview confirms the expected change in source findings; and
- an external consumer can replace its submodule or custom loader with the documented package API.
## 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 scanning can distinguish plug-in source from a reviewed dependency, subject to preview verification.
- 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.
- Translation, Svelte, and platform dependencies become optional composition concerns rather than root-package side effects.
- Fancy Kit may gain a generally useful typed Obsidian Modal host without acquiring LiveSync policy or a mandatory Svelte dependency.
## Open Questions Before Acceptance
- Define the focused stable language entry point which will replace the temporary compatibility subpath.
- Define the minimum conflict and conditional-write guarantees required by the first public high-level client.
- Decide which current deep imports can migrate directly to public subpaths and which require temporary compatibility exports.
- 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.

168
docs/datastructure.md Normal file
View File

@@ -0,0 +1,168 @@
# 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.
In that case, if there are major changes, synchronisation may be stopped.
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.

View 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.

View File

@@ -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.

View 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.

View 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.

View 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.

View File

@@ -2,7 +2,7 @@
[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.
The plug-in 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.
![](../images/quick_setup_1.png)
@@ -10,7 +10,7 @@ There are three methods to set up Self-hosted LiveSync.
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)
3. [Fully manual setup and enabling on this dialogue](#3-manually-setup)
## At the first device
@@ -24,7 +24,7 @@ There are three methods to set up Self-hosted LiveSync.
In this procedure, [this video](https://youtu.be/7sa_I1832Xc?t=146) may help us.
1. Click `Use` button (Or launch `Use the copied setup URI` from Command palette).
1. Click the `Use` button (or launch the `Use the copied setup URI (Formerly Open setup URI)` command from the 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?`.
@@ -107,23 +107,27 @@ Note: If you are going to use Object Storage, you cannot select `LiveSync`.
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 dialogue of `Copy current settings as a new setup URI` will open automatically. Please input a passphrase to encrypt the new `Setup URI`. (This passphrase is to encrypt the setup URI, not the vault).
![](../images/quick_setup_10.png)
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`.
We can copy this at any time by running the "Copy settings as a new setup URI" command from the command palette (or clicking the "Copy the current settings to a Setup URI" button in the settings UI).
### 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.
And, please copy the setup URI by running the "Copy settings as a new setup URI" command (or using the "Copy the current settings to a Setup URI" button) 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.
> [!TIP]
> **Fast Setup (Simple Fetch)**
> In recent versions, when you import a Setup URI or trigger a Fetch All, the plug-in boots in scheduled fetch mode and runs a simplified **Fast Setup** process. This allows you to choose your sync strategy with a single dialogue and performs initial synchronisation in one step. Refer to the [Fast Setup Guide](./tips/fast-setup.md) for more details.

View File

@@ -1,6 +1,6 @@
# Quick setup
このプラグインには、いろいろな状況に対応するための非常に多くの設定オプションがあります。しかし、実際に使用する設定項目はそれほど多くはありません。そこで、初期設定を簡略化するために、「セットアップウィザード」を実装しています。
※なお、次のデバイスからは、`Copy setup URI``Open setup URI`を使ってセットアップしてください。
※なお、次のデバイスからは、`現在の設定をセットアップURIにコピー``セットアップURIで接続`を使ってセットアップしてください。
## Wizardの使い方
@@ -71,7 +71,8 @@ Fixボタンがなくなり、すべてチェックマークになれば完了
![](../images/quick_setup_9_1.png)
Presetsから、いずれかの同期方法を選び`Apply`を行うと、必要に応じてローカル・リモートのデータベースを初期化・構築します。
All done! と表示されれば完了です。自動的に、`Copy setup URI`が開き、`Setup URI`を暗号化するパスフレーズを聞かれます。
All done!」(日本語環境では「完了!」)と表示されれば完了です。自動的に、「現在の設定をセットアップURIにコピー」のダイアログが開き、Setup URIを暗号化するためのパスフレーズを求められますこのパスフレーズはSetup URIを暗号化するためのもので、Vault自体の暗号化キーではありません
パスフレーズを入力すると、クリップボードにSetup URIが保存されますので、これを2台目以降のデバイスに何らかの方法で転送してください。
![](../images/quick_setup_10.png)
@@ -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) をご参照ください。

View File

@@ -12,7 +12,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) |
@@ -68,7 +68,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 +120,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 +150,19 @@ 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.
- ** 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
@@ -172,6 +192,14 @@ 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.
#### 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 (Experimental)
Setting key: useDynamicIterationCount
@@ -192,30 +220,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 +283,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 +369,100 @@ 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.
#### 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 +488,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,15 +514,15 @@ 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)
@@ -360,10 +559,11 @@ Setting key: notifyAllSettingSyncFile
### 7. Hidden Files (Advanced)
#### Hidden file synchronization
#### 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 +573,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
@@ -406,42 +612,42 @@ Comma separated `.gitignore, .dockerignore`
#### Add default patterns
## 6. Customization sync (Advanced)
## 6. Customisation sync (Advanced)
### 1. Customization Sync
### 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 +662,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 +696,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 +717,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,6 +748,12 @@ Setting key: concurrencyOfReadChunksOnline
Setting key: minimumIntervalOfReadChunksOnline
#### Maximum size of chunks to send in one request
Setting key: sendChunksBulkMaxSize
Limit the maximum size of chunks to send in a single bulk request (MB).
## 9. Power users (Power User)
### 1. Remote Database Tweak
@@ -637,9 +859,11 @@ If this enabled, all chunks will be stored with the revision made from its conte
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.
### 4. Compatibility (Internal API Usage)
#### Scan changes on customization sync
#### Scan changes on customisation sync
Setting key: watchInternalFileChanges
Do not use internal API
@@ -664,7 +888,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,17 +914,25 @@ 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
@@ -712,17 +950,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.
@@ -752,7 +986,7 @@ Delete all data on the remote server.
#### Run database cleanup
Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Rebuild everything' under Total Overhaul.
Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Overwrite Server Data with This Device's Files' under Reset Synchronisation information.
### 7. Reset

View File

@@ -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を使用し、リモートと[このプロトコ
![CorruptedData](../images/lock_pattern1.png)
データベースがロックされていて、端末が「解決済み」とマークされていない場合、警告が表示されます。
他のデバイスで、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
![CorruptedData](../images/lock_pattern2.png)
@@ -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
![CorruptedData](../images/corrupted_data.png)

View File

@@ -13,7 +13,7 @@ In these instructions, create IBM Cloudant Instance for trial.
1. You can choose "Lite plan" for free.
![step 3](../instruction_images/cloudant_3.png)
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.
![step 4](../instruction_images/cloudant_4.png)
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.
![step 8](../instruction_images/cloudant_8.png)
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.
![step 9](../instruction_images/cloudant_9.png)
## 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.
![step 1](../instruction_images/couchdb_1.png)
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._
![step 2](../instruction_images/couchdb_2.png)
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.
![step 1](../instruction_images/credentials_1.png)
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.
![step 2](../instruction_images/credentials_2.png)
_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.
![step 3](../instruction_images/credentials_3.png)
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
![Setting](../images/remote_db_setting.png)
The Setting should be as below:
The settings should be as follows:
| Items | Value | example |
| ------------- | ----- | ----------------------------------------------------------------- |

View File

@@ -23,6 +23,7 @@
- [Setting up your domain](#setting-up-your-domain)
- [Reverse Proxies](#reverse-proxies)
- [Traefik](#traefik)
- [Nginx](#nginx)
---
## 1. Prepare CouchDB
@@ -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
@@ -226,7 +231,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 +292,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
```

View File

@@ -71,7 +71,6 @@ obsidian-livesync
可以参照以下内容编辑 `docker-compose.yml`:
```yaml
version: "2.1"
services:
couchdb:
image: couchdb

View File

@@ -1,8 +1,8 @@
# Designed architecture
## How does this plugin synchronize.
## How does this plug-in synchronise.
![Synchronization](../images/1.png)
![Synchronisation](../images/1.png)
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
View File

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

View File

@@ -2,7 +2,7 @@
## 同期
![Synchronization](../images/1.png)
![Synchronisation](../images/1.png)
1. ートが更新された際、Obsidianがイベントを発報します。Obsidian-LiveSyncはそれをハンドリングして、ローカルのPouchDBに変更を反映します。
2. PouchDBは、リモートのCouchDBに差分をレプリケーションします。

View File

@@ -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
View 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.

View 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. 処理が完了すると、プラグインは通常の動作状態へ復帰します。

145
docs/to_issue_reporting.md Normal file
View 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 1012)
- 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.

View File

@@ -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. ![Screenshot](../images/hatch.png)
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,71 @@ 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.
#### 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 +442,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)

View File

@@ -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,
],
});

View File

@@ -0,0 +1,81 @@
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/no-static-styles-assignment": "off",
"obsidianmd/prefer-create-el": "off",
"obsidianmd/prefer-active-doc": "off",
},
}
);

142
eslint.config.common.mjs Normal file
View 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",
};

View File

@@ -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",
},
},
]);

View File

@@ -1,10 +0,0 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.24.beta3",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",
"authorUrl": "https://github.com/vrtmrz",
"isDesktopOnly": false
}

View File

@@ -1,8 +1,8 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.30",
"minAppVersion": "0.9.12",
"version": "0.25.83",
"minAppVersion": "1.7.2",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",
"authorUrl": "https://github.com/vrtmrz",

21821
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,101 @@
{
"name": "obsidian-livesync",
"version": "0.25.30",
"version": "0.25.83",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
"scripts": {
"bakei18n": "npx tsx ./src/lib/_tools/bakei18n.ts",
"i18n:bakejson": "npx tsx ./src/lib/_tools/bakei18n.ts",
"i18n:yaml2json": "npx tsx ./src/lib/_tools/yaml2json.ts",
"i18n:json2yaml": "npx tsx ./src/lib/_tools/json2yaml.ts",
"prettyjson": "prettier --config ./.prettierrc ./src/lib/src/common/messagesJson/*.json --write --log-level error",
"postbakei18n": "prettier --config ./.prettierrc ./src/lib/src/common/messages/*.ts --write --log-level error",
"posti18n:yaml2json": "npm run prettyjson",
"predev": "npm run bakei18n",
"dev": "node --env-file=.env esbuild.config.mjs",
"prebuild": "npm run bakei18n",
"build": "node esbuild.config.mjs production",
"buildVite": "npx dotenv-cli -e .env -- vite build --mode production",
"buildViteOriginal": "npx dotenv-cli -e .env -- vite build --mode original",
"buildDev": "node esbuild.config.mjs dev",
"lint": "eslint src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
"lint": "eslint --cache --concurrency auto src",
"lint:community": "eslint --config eslint.community.config.mjs --concurrency auto src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
"tsc-check": "tsc --noEmit",
"tsc-check:apps": "tsc --noEmit -p src/apps/browser/tsconfig.json && tsc --noEmit -p src/apps/cli/tsconfig.json && tsc --noEmit -p src/apps/webapp/tsconfig.json && tsc --noEmit -p src/apps/webpeer/tsconfig.app.json && tsc --noEmit -p src/apps/webpeer/tsconfig.node.json",
"pretty:importpath": "cd utilsdeno && deno run -A ./normalise-imports.ts",
"pretty:json": "prettier --config ./.prettierrc.mjs \"**/*.json\" --write --log-level error",
"pretty": "npm run prettyNoWrite -- --write --log-level error",
"prettyCheck": "npm run prettyNoWrite -- --check",
"prettyNoWrite": "prettier --config ./.prettierrc \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
"check": "npm run lint && npm run svelte-check",
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/"
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
"check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15",
"check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility",
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/",
"test": "vitest run",
"test:unit": "vitest run --config vitest.config.unit.ts",
"test:contract:contexts": "vitest run --config vitest.config.unit.ts src/modules/services/ObsidianServiceContext.unit.spec.ts src/apps/cli/services/NodeServiceContext.unit.spec.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts",
"test:contract:context:webapp": "vitest run --config vitest.config.unit.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts",
"test:contract:context:cli": "npm run build --workspace self-hosted-livesync-cli && deno task --cwd src/apps/cli/testdeno test:setup-put-cat",
"test:contract:context:obsidian": "npm run build && npm run test:e2e:obsidian:smoke",
"test:e2e:cli": "npm run test:e2e:ci --workspace self-hosted-livesync-cli",
"test:e2e:cli:p2p": "npm run test:e2e:p2p --workspace self-hosted-livesync-cli",
"test:e2e:cli:all": "npm run test:e2e:all --workspace self-hosted-livesync-cli",
"test:integration": "npx dotenv-cli -e .env -e .test.env -- vitest run --config vitest.config.integration.ts",
"test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage",
"test:install-playwright": "npx playwright install chromium",
"test:install-dependencies": "npm run test:install-playwright",
"test:e2e:obsidian:install-appimage": "tsx test/e2e-obsidian/scripts/install-appimage.ts",
"test:e2e:obsidian:discover": "tsx test/e2e-obsidian/scripts/discover.ts",
"test:e2e:obsidian:cli-help": "tsx test/e2e-obsidian/scripts/cli-help.ts",
"test:e2e:obsidian:debug-ui": "tsx test/e2e-obsidian/scripts/debug-ui.ts",
"test:e2e:obsidian:smoke": "tsx test/e2e-obsidian/scripts/smoke.ts",
"test:e2e:obsidian:dialog-mounts": "tsx test/e2e-obsidian/scripts/dialog-mounts.ts",
"test:e2e:obsidian:settings-ui": "tsx test/e2e-obsidian/scripts/settings-ui.ts",
"test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts",
"test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts",
"test:e2e:obsidian:cli-to-obsidian-sync": "tsx test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts",
"test:e2e:obsidian:minio-upload": "tsx test/e2e-obsidian/scripts/minio-upload.ts",
"test:e2e:obsidian:startup-scan": "tsx test/e2e-obsidian/scripts/startup-scan.ts",
"test:e2e:obsidian:two-vault-sync": "tsx test/e2e-obsidian/scripts/two-vault-sync.ts",
"test:e2e:obsidian:hidden-file-snippet-sync": "tsx test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts",
"test:e2e:obsidian:customisation-sync": "tsx test/e2e-obsidian/scripts/customisation-sync.ts",
"test:e2e:obsidian:setting-markdown-export": "tsx test/e2e-obsidian/scripts/setting-markdown-export.ts",
"test:e2e:obsidian:local-suite": "tsx test/e2e-obsidian/scripts/local-suite.ts",
"test:e2e:obsidian:local-suite:services": "tsx test/e2e-obsidian/scripts/local-suite.ts --manage-services",
"test:coverage": "vitest run --coverage",
"test:docker-couchdb:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-start.sh",
"test:docker-couchdb:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-init.sh",
"test:docker-couchdb:start": "npm run test:docker-couchdb:up && sleep 5 && npm run test:docker-couchdb:init",
"test:docker-couchdb:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-stop.sh",
"test:docker-couchdb:stop": "npm run test:docker-couchdb:down",
"test:docker-s3:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-start.sh",
"test:docker-s3:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-init.sh",
"test:docker-s3:start": "npm run test:docker-s3:up && sleep 3 && npm run test:docker-s3:init",
"test:docker-s3:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-stop.sh",
"test:docker-s3:stop": "npm run test:docker-s3:down",
"test:docker-p2p:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-start.sh",
"test:docker-p2p:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-init.sh",
"test:docker-p2p:start": "npm run test:docker-p2p:up && sleep 3 && npm run test:docker-p2p:init",
"test:docker-p2p:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-stop.sh",
"test:docker-p2p:stop": "npm run test:docker-p2p:down",
"test:docker-all:up": "npm run test:docker-couchdb:up ; npm run test:docker-s3:up ; npm run test:docker-p2p:up",
"test:docker-all:init": "npm run test:docker-couchdb:init ; npm run test:docker-s3:init ; npm run test:docker-p2p:init",
"test:docker-all:down": "npm run test:docker-couchdb:down ; npm run test:docker-s3:down ; npm run test:docker-p2p:down",
"test:docker-all:start": "npm run test:docker-all:up && sleep 5 && npm run test:docker-all:init",
"test:docker-all:stop": "npm run test:docker-all:down",
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop",
"test:p2p": "bash test/suitep2p/run-p2p-tests.sh",
"update-workspaces": "node update-workspaces.mjs",
"version": "node version-bump.mjs && node update-workspaces.mjs && npm run pretty:json && git add manifest.json versions.json src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json"
},
"keywords": [],
"author": "vorotamoroz",
"license": "MIT",
"devDependencies": {
"@chialab/esbuild-plugin-worker": "^0.18.1",
"@eslint/compat": "^1.2.7",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5",
"@types/deno": "^2.3.0",
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1",
"@eslint/js": "^9.39.3",
"@playwright/test": "^1.58.2",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tsconfig/svelte": "^5.0.8",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
"@types/node": "^22.13.8",
"@types/markdown-it": "^14.1.2",
"@types/micromatch": "^4.0.10",
"@types/node": "^24.10.13",
"@types/pouchdb": "^6.4.2",
"@types/pouchdb-adapter-http": "^6.1.6",
"@types/pouchdb-adapter-idb": "^6.1.7",
@@ -47,20 +104,22 @@
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"builtin-modules": "5.0.0",
"esbuild": "0.25.0",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
"@vitest/coverage-v8": "^4.1.8",
"@vrtmrz/obsidian-test-session": "0.2.0",
"dotenv-cli": "^11.0.0",
"esbuild": "0.28.1",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.3",
"eslint": "^9.38.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-svelte": "^3.12.4",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.4.1",
"eslint-plugin-svelte": "^3.19.0",
"events": "^3.3.0",
"glob": "^11.0.3",
"obsidian": "^1.8.7",
"postcss": "^8.5.3",
"postcss-load-config": "^6.0.1",
"globals": "^14.0.0",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-idb": "^9.0.0",
"pouchdb-adapter-indexeddb": "^9.0.0",
@@ -72,31 +131,47 @@
"pouchdb-merge": "^9.0.0",
"pouchdb-replication": "^9.0.0",
"pouchdb-utils": "^9.0.0",
"prettier": "3.5.2",
"svelte": "5.41.1",
"svelte-check": "^4.3.3",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.56.3",
"svelte-check": "^4.6.0",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
"terser": "^5.39.0",
"tinyglobby": "^0.2.15",
"transform-pouch": "^2.0.0",
"tslib": "^2.8.1",
"tsx": "^4.20.6",
"tsx": "^4.21.0",
"typescript": "5.9.3",
"yaml": "^2.8.0"
"typescript-eslint": "^8.61.0",
"vite": "^8.0.16",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
"@smithy/fetch-http-handler": "^5.0.2",
"@smithy/md5-js": "^4.0.2",
"@smithy/middleware-apply-body-checksum": "^4.1.0",
"@smithy/protocol-http": "^5.1.0",
"@smithy/querystring-builder": "^4.0.2",
"@smithy/fetch-http-handler": "^5.3.10",
"@smithy/md5-js": "^4.2.9",
"@smithy/middleware-apply-body-checksum": "^4.3.9",
"@smithy/protocol-http": "^5.3.9",
"@smithy/querystring-builder": "^4.2.9",
"@smithy/types": "^4.14.3",
"@smithy/util-retry": "^4.4.5",
"@trystero-p2p/nostr": "^0.24.0",
"@vrtmrz/livesync-commonlib": "0.1.0-rc.1",
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"minimatch": "^10.0.2",
"octagonal-wheels": "^0.1.44",
"markdown-it": "^14.2.0",
"minimatch": "^10.2.5",
"obsidian": "^1.13.1",
"octagonal-wheels": "^0.1.51",
"qrcode-generator": "^1.4.4",
"trystero": "^0.22.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
}
},
"workspaces": [
"src/apps/cli",
"src/apps/webpeer",
"src/apps/webapp"
]
}

View File

@@ -66,7 +66,7 @@
"outputs": [],
"source": [
"# see https://fly.io/docs/reference/regions/\n",
"region = \"nrt/Tokyo, Japan\" #@param [\"ams/Amsterdam, Netherlands\",\"arn/Stockholm, Sweden\",\"atl/Atlanta, Georgia (US)\",\"bog/Bogotá, Colombia\",\"bos/Boston, Massachusetts (US)\",\"cdg/Paris, France\",\"den/Denver, Colorado (US)\",\"dfw/Dallas, Texas (US)\",\"ewr/Secaucus, NJ (US)\",\"eze/Ezeiza, Argentina\",\"gdl/Guadalajara, Mexico\",\"gig/Rio de Janeiro, Brazil\",\"gru/Sao Paulo, Brazil\",\"hkg/Hong Kong, Hong Kong\",\"iad/Ashburn, Virginia (US)\",\"jnb/Johannesburg, South Africa\",\"lax/Los Angeles, California (US)\",\"lhr/London, United Kingdom\",\"mad/Madrid, Spain\",\"mia/Miami, Florida (US)\",\"nrt/Tokyo, Japan\",\"ord/Chicago, Illinois (US)\",\"otp/Bucharest, Romania\",\"phx/Phoenix, Arizona (US)\",\"qro/Querétaro, Mexico\",\"scl/Santiago, Chile\",\"sea/Seattle, Washington (US)\",\"sin/Singapore, Singapore\",\"sjc/San Jose, California (US)\",\"syd/Sydney, Australia\",\"waw/Warsaw, Poland\",\"yul/Montreal, Canada\",\"yyz/Toronto, Canada\" ] {allow-input: true}\n",
"region = \"nrt/Tokyo, Japan\" #@param [\"jnb/Johannesburg, South Africa\",\"bom/Mumbai, India\",\"sin/Singapore, Singapore\",\"syd/Sydney, Australia\",\"nrt/Tokyo, Japan\",\"ams/Amsterdam, Netherlands\",\"fra/Frankfurt, Germany\",\"lhr/London, United Kingdom\",\"cdg/Paris, France\",\"arn/Stockholm, Sweden\",\"iad/Ashburn, Virginia (US)\",\"ord/Chicago, Illinois (US)\",\"dfw/Dallas, Texas (US)\",\"lax/Los Angeles, California (US)\",\"sjc/San Jose, California (US)\",\"ewr/Secaucus, NJ (US)\",\"yyz/Toronto, Canada\",\"gru/Sao Paulo, Brazil\"] {allow-input: true}\n",
"%env region={region.split(\"/\")[0]}\n",
"#%env appame=\n",
"#%env username=\n",

289
src/LiveSyncBaseCore.ts Normal file
View File

@@ -0,0 +1,289 @@
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
import type PouchDB from "pouchdb-core";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { __$checkInstanceBinding } from "@vrtmrz/livesync-commonlib/compat/dev/checks";
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
import type { Rebuilder } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseRebuilder";
import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler";
import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess";
import type { LiveSyncLocalDBEnv } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB";
import type { LiveSyncCouchDBReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
import type { CheckPointInfo } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncTypes";
import type { LiveSyncJournalReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicatorEnv";
import type { LiveSyncReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator";
import { useTargetFilters } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/targetFilter";
import { useRemoteConfigurationMigration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
import { AbstractModule } from "./modules/AbstractModule";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB";
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker";
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain";
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
import { usePrepareDatabaseForUse } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/prepareDatabaseForUse";
import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type";
export class LiveSyncBaseCore<
T extends ServiceContext = ServiceContext,
TCommands extends IMinimumLiveSyncCommands = IMinimumLiveSyncCommands,
>
implements
LiveSyncLocalDBEnv,
LiveSyncReplicatorEnv,
LiveSyncJournalReplicatorEnv,
LiveSyncCouchDBReplicatorEnv,
HasSettings<ObsidianLiveSyncSettings>
{
addOns = [] as TCommands[];
/**
* register an add-onn to the plug-in.
* Add-ons are features that are not essential to the core functionality of the plugin,
* @param addOn
*/
private _registerAddOn(addOn: TCommands) {
this.addOns.push(addOn);
this.services.appLifecycle.onUnload.addHandler(() => Promise.resolve(addOn.onunload()).then(() => true));
}
/**
* Get an add-on by its class name. Returns undefined if not found.
* @param cls
* @returns
*/
getAddOn<T extends TCommands>(cls: string) {
for (const addon of this.addOns) {
if (addon.constructor.name == cls) return addon as T;
}
return undefined;
}
constructor(
serviceHub: InjectableServiceHub<T>,
serviceModuleInitialiser: (
core: LiveSyncBaseCore<T, TCommands>,
serviceHub: InjectableServiceHub<T>
) => ServiceModules,
extraModuleInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => AbstractModule[],
addOnsInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => TCommands[],
featuresInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => void
) {
this._services = serviceHub;
this._serviceModules = serviceModuleInitialiser(this, serviceHub);
const extraModules = extraModuleInitialiser(this);
this.registerModules(extraModules);
this.initialiseServiceFeatures();
featuresInitialiser(this);
const addOns = addOnsInitialiser(this);
for (const addOn of addOns) {
this._registerAddOn(addOn);
}
this.bindModuleFunctions();
}
/**
* The service hub for managing all services.
*/
_services: InjectableServiceHub<T> | undefined = undefined;
get services() {
if (!this._services) {
throw new Error("Services not initialised yet");
}
return this._services;
}
/**
* Service Modules
*/
protected _serviceModules: ServiceModules;
get serviceModules() {
return this._serviceModules;
}
/**
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
*/
private modules = [
// Move to registerModules
] as AbstractModule[];
/**
* Get a module by its class. Throws an error if not found.
* Mostly used for getting SetupManager.
* @param constructor
* @returns
*/
getModule<T extends AbstractModule>(constructor: Constructor<T>): T {
for (const module of this.modules) {
if (module.constructor === constructor) return module as T;
}
throw new Error(`Module ${constructor.name} not found or not loaded.`);
}
/**
* Register a module to the plug-in.
* @param module The module to register.
*/
private _registerModule(module: AbstractModule) {
this.modules.push(module);
}
public registerModules(extraModules: AbstractModule[] = []) {
this._registerModule(new ModuleLiveSyncMain(this));
this._registerModule(new ModuleConflictChecker(this));
this._registerModule(new ModuleReplicatorMinIO(this));
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleConflictResolver(this));
this._registerModule(new ModulePeriodicProcess(this));
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
this._registerModule(new ModuleBasicMenu(this));
for (const module of extraModules) {
this._registerModule(module);
}
// Test and Dev Modules
}
/**
* Bind module functions to services.
*/
public bindModuleFunctions() {
for (const module of this.modules) {
if (module instanceof AbstractModule) {
module.onBindFunction(this, this.services);
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
} else {
// module should not be never.
const moduleName = (module as unknown)?.constructor?.name ?? "unknown";
this.services.API.addLog(
`Module ${moduleName} does not have onBindFunction, skipping binding.`,
LOG_LEVEL_INFO
);
}
}
}
/**
* @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user.
*/
get confirm(): Confirm {
return this.services.UI.confirm;
}
/**
* @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in.
*/
get settings() {
return this.services.setting.settings;
}
/**
* @obsolete Use services.setting.settings instead. Set the settings of the plug-in.
*/
set settings(value: ObsidianLiveSyncSettings) {
this.services.setting.settings = value;
}
/**
* @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in.
* @returns The current settings of the plug-in.
*/
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
/**
* @obsolete Use services.database.localDatabase instead. The local database instance.
*/
get localDatabase() {
return this.services.database.localDatabase;
}
/**
* @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database.
* @returns The PouchDB database instance.
*/
getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase;
}
/**
* @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc.
*/
get simpleStore() {
return this.services.keyValueDB.simpleStore as SimpleStore<CheckPointInfo>;
}
/**
* @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time.
*/
get replicator() {
return this.services.replicator.getActiveReplicator()!;
}
/**
* @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc.
*/
get kvDB() {
return this.services.keyValueDB.kvDB;
}
/// Modules which were relied on services
/**
* Storage Accessor for handling file operations.
* @obsolete Use serviceModules.storageAccess instead.
*/
get storageAccess(): StorageAccess {
return this.serviceModules.storageAccess;
}
/**
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
* @obsolete Use serviceModules.databaseFileAccess instead.
*/
get databaseFileAccess(): DatabaseFileAccess {
return this.serviceModules.databaseFileAccess;
}
/**
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
* @obsolete Use serviceModules.fileHandler instead.
*/
get fileHandler(): IFileHandler {
return this.serviceModules.fileHandler;
}
/**
* Rebuilder for handling database rebuilding operations.
* @obsolete Use serviceModules.rebuilder instead.
*/
get rebuilder(): Rebuilder {
return this.serviceModules.rebuilder;
}
// private initialiseServices<T extends ServiceContext>(serviceHub: InjectableServiceHub<T>) {
// this._services = serviceHub;
// }
/**
* Initialise ServiceFeatures.
* (Please refer `serviceFeatures` for more details)
*/
initialiseServiceFeatures() {
useTargetFilters(this);
// enable target filter feature.
usePrepareDatabaseForUse(this);
// Migration to multiple remote configurations
useRemoteConfigurationMigration(this);
}
}
export interface IMinimumLiveSyncCommands {
onunload(): void;
onload(): void | Promise<void>;
constructor: { name: string };
}

View File

@@ -0,0 +1,108 @@
import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
/** One platform-neutral storage adapter contract case. */
export interface StorageAdapterContractCase {
readonly name: string;
run(adapter: IStorageAdapter): Promise<void>;
}
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertEqual(actual: unknown, expected: unknown, message: string): void {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`${message}\nactual=${JSON.stringify(actual)}\nexpected=${JSON.stringify(expected)}`);
}
}
async function assertRejects(operation: () => Promise<unknown>, message: string): Promise<void> {
try {
await operation();
} catch {
return;
}
throw new Error(message);
}
/** Passing baseline shared by Node, FSAPI, and future storage adapters. */
export const storageAdapterContractCases: readonly StorageAdapterContractCase[] = [
{
name: "reports missing paths consistently",
async run(adapter) {
assertEqual(await adapter.exists("missing.txt"), false, "missing path should not exist");
assertEqual(await adapter.stat("missing.txt"), null, "missing stat should be null");
assertEqual(await adapter.trystat("missing.txt"), null, "missing trystat should be null");
},
},
{
name: "creates parent directories for nested text writes",
async run(adapter) {
await adapter.write("notes/nested/note.md", "hello");
assertEqual(await adapter.read("notes/nested/note.md"), "hello", "text should round-trip");
assert(await adapter.exists("notes/nested/note.md"), "written text path should exist");
assertEqual((await adapter.stat("notes/nested/note.md"))?.type, "file", "written path should be a file");
},
},
{
name: "round-trips exact binary bytes",
async run(adapter) {
const expected = Uint8Array.from([0x00, 0x7f, 0x80, 0xff, 0x42]);
await adapter.writeBinary("binary/blob.bin", expected.buffer.slice(0));
const result = await adapter.readBinary("binary/blob.bin");
assertEqual([...new Uint8Array(result)], [...expected], "binary data should round-trip exactly");
assertEqual(result.byteLength, expected.byteLength, "binary result should have the exact visible length");
},
},
{
name: "creates and extends text through append",
async run(adapter) {
await adapter.append("logs/events.log", "first");
await adapter.append("logs/events.log", ":second");
assertEqual(await adapter.read("logs/events.log"), "first:second", "append should create then extend text");
},
},
{
name: "lists direct files and folders",
async run(adapter) {
await adapter.mkdir("listing/folder");
await adapter.write("listing/file.txt", "content");
const listed = await adapter.list("listing");
assertEqual([...listed.files].sort(), ["listing/file.txt"], "list should contain the direct file");
assertEqual([...listed.folders].sort(), ["listing/folder"], "list should contain the direct folder");
},
},
{
name: "removes files and directory trees",
async run(adapter) {
await adapter.write("remove/file.txt", "content");
await adapter.write("remove/folder/nested.txt", "content");
await adapter.remove("remove/file.txt");
assertEqual(await adapter.exists("remove/file.txt"), false, "file should be removed");
await adapter.remove("remove/folder");
assertEqual(await adapter.exists("remove/folder"), false, "directory tree should be removed");
},
},
{
name: "keeps operations inside the configured root",
async run(adapter) {
await assertRejects(() => adapter.exists("../outside"), "parent traversal should be rejected");
await assertRejects(() => adapter.write("nested/../outside", "content"), "nested traversal should be rejected");
await assertRejects(() => adapter.read("/absolute"), "absolute paths should be rejected");
await assertRejects(() => adapter.read("C:\\absolute"), "drive-qualified paths should be rejected");
await assertRejects(() => adapter.read("nested\\outside"), "backslash-separated paths should be rejected");
await assertRejects(() => adapter.remove(""), "removing the configured root should be rejected");
},
},
{
name: "uses the empty path only for root-safe operations",
async run(adapter) {
await adapter.mkdir("");
assertEqual(await adapter.exists(""), true, "the configured root should exist");
assertEqual((await adapter.stat(""))?.type, "folder", "the configured root should be a folder");
assertEqual(await adapter.list(""), { files: [], folders: [] }, "the configured root should be listable");
await assertRejects(() => adapter.write("", "content"), "writing over the configured root should be rejected");
await assertRejects(() => adapter.append("", "content"), "appending to the configured root should be rejected");
},
},
];

View File

@@ -0,0 +1,107 @@
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
import MessageBox from "./ui/MessageBox.svelte";
import TextInputBox from "./ui/TextInputBox.svelte";
import { mount } from "svelte";
import { promiseWithResolvers } from "octagonal-wheels/promises";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
function displayMessageBox<T, U extends string[]>(
message: string,
buttons: U,
title: string,
commit: (ret: U[number]) => T
): Promise<T> {
const el = _activeDocument.createElement("div");
const p = promiseWithResolvers<T>();
mount(MessageBox, {
target: el,
props: {
message,
buttons: buttons as string[],
title: title,
commit: (action: U[number]) => {
const ret = commit(action);
p.resolve(ret);
},
},
});
_activeDocument.body.appendChild(el);
void p.promise.finally(() => {
el.remove();
});
return p.promise;
}
function promptForInput(
title: string,
key: string,
placeholder: string,
isPassword?: boolean
): Promise<string | false> {
const el = _activeDocument.createElement("div");
const p = promiseWithResolvers<string | false>();
mount(TextInputBox, {
target: el,
props: {
title,
message: key,
placeholder,
isPassword,
commit: (text: string | false) => {
p.resolve(text);
},
},
});
_activeDocument.body.appendChild(el);
void p.promise.finally(() => {
el.remove();
});
return p.promise;
}
export class BrowserConfirm<T extends ServiceContext> implements Confirm {
_context: T;
constructor(context: T) {
this._context = context;
}
askYesNo(message: string): Promise<"yes" | "no"> {
return displayMessageBox(message, ["Yes", "No"] as const, "Confirm", (action) =>
action == "Yes" ? "yes" : "no"
);
}
askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false> {
return promptForInput(title, key, placeholder, isPassword);
}
askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number }
): Promise<"yes" | "no"> {
return displayMessageBox(message, ["Yes", "No"] as const, opt.title ?? "Confirm", (action) =>
action == "Yes" ? "yes" : "no"
);
}
askSelectString(message: string, items: string[]): Promise<string> {
return displayMessageBox(message, [...items] as const, "Confirm", (action) => action);
}
askSelectStringDialogue<T extends readonly string[]>(
message: string,
buttons: T,
opt: { title?: string; defaultAction: T[number]; timeout?: number }
): Promise<T[number] | false> {
return displayMessageBox(message, [...buttons] as const, opt.title ?? "Confirm", (action) => action);
}
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void {
throw new Error("Method not implemented.");
}
confirmWithMessage(
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
return displayMessageBox(contentMd, [...buttons] as const, title ?? "Confirm", (action) => action);
}
}

View File

@@ -0,0 +1,71 @@
import { promiseWithResolver, type PromiseWithResolvers } from "octagonal-wheels/promises";
import { mount } from "svelte";
import MenuView from "./ui/MenuView.svelte";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
export class MenuItem {
type = "item";
title = "";
handler?: () => void | Promise<void>;
icon: string = "";
setTitle(title: string) {
this.title = title;
return this;
}
onClick(callback: () => void | Promise<void>) {
this.handler = callback;
return this;
}
setIcon(icon: string | null) {
this.icon = icon || "";
return this;
}
}
export class MenuSeparator {
type = "separator";
}
export class Menu {
type = "menu";
items: (MenuItem | MenuSeparator)[] = [];
constructor() {}
addItem(callback: (item: MenuItem) => void) {
const item = new MenuItem();
callback(item);
this.items.push(item);
return this;
}
addSeparator() {
this.items.push(new MenuSeparator());
return this;
}
waitingForClose?: PromiseWithResolvers<void>;
showAtPosition(pos: { x: number; y: number }) {
const el = _activeDocument.createElement("div");
if (this.waitingForClose) {
this.waitingForClose.resolve();
}
this.waitingForClose = promiseWithResolver<void>();
mount(MenuView, {
target: el,
props: {
items: this.items,
closeMenu: () => {
this.waitingForClose?.resolve();
this.waitingForClose = undefined;
},
x: pos.x,
y: pos.y,
},
});
_activeDocument.body.appendChild(el);
void this.waitingForClose.promise.finally(() => {
el.remove();
});
return this.waitingForClose.promise;
}
hide() {
this.waitingForClose?.resolve();
this.waitingForClose = undefined;
}
}

View File

@@ -0,0 +1,79 @@
import {
type ComponentHasResult,
SvelteDialogManagerBase,
SvelteDialogMixIn,
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import type { SvelteDialogManagerDependencies } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte";
export class ShimModal {
contentEl: HTMLElement;
titleEl: HTMLElement;
modalEl: HTMLElement;
isOpen: boolean = false;
baseEl: HTMLElement;
constructor() {
const baseEl = _activeDocument.createElement("popup");
this.baseEl = baseEl;
this.contentEl = _activeDocument.createElement("div");
this.contentEl.className = "modal-content";
this.titleEl = _activeDocument.createElement("div");
this.titleEl.className = "modal-title";
this.modalEl = _activeDocument.createElement("div");
this.modalEl.className = "modal";
this.modalEl.style.display = "none";
this.modalEl.appendChild(this.titleEl);
this.modalEl.appendChild(this.contentEl);
this.baseEl.appendChild(this.modalEl);
}
open() {
this.isOpen = true;
this.modalEl.style.display = "block";
if (!this.baseEl.parentElement) {
_activeDocument.body.appendChild(this.baseEl);
}
this.onOpen();
}
close() {
this.isOpen = false;
this.baseEl.style.display = "none";
this.baseEl.remove();
this.onClose();
}
onOpen() {}
onClose() {}
setPlaceholder(p: string) {}
setTitle(t: string) {
this.titleEl.textContent = t;
}
}
const BrowserSvelteDialogBase = SvelteDialogMixIn(ShimModal, DialogHost);
export class LiveSyncBrowserDialog<T, U, C extends ServiceContext = ServiceContext> extends BrowserSvelteDialogBase<
T,
U,
C
> {
constructor(
context: C,
dependents: SvelteDialogManagerDependencies<C>,
component: ComponentHasResult<T, U>,
initialData?: U
) {
super();
this.initDialog(context, dependents, component, initialData);
}
}
export class BrowserSvelteDialogManager<T extends ServiceContext> extends SvelteDialogManagerBase<T> {
override async openSvelteDialog<TT, TU>(
component: ComponentHasResult<TT, TU>,
initialData?: TU
): Promise<TT | undefined> {
const dialog = new LiveSyncBrowserDialog<TT, TU, T>(this.context, this.dependents, component, initialData);
dialog.open();
return await dialog.waitForClose();
}
}

View File

@@ -0,0 +1,25 @@
import type { BrowserServiceHostDependencies } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService";
import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte";
import { BrowserSvelteDialogManager } from "./BrowserSvelteDialogManager";
export class LiveSyncBrowserUIService<T extends ServiceContext> extends UIService<T> {
override get dialogToCopy() {
return DialogToCopy;
}
constructor(context: T, dependents: BrowserServiceHostDependencies<T>) {
const browserConfirm = dependents.API.confirm;
const obsidianSvelteDialogManager = new BrowserSvelteDialogManager<T>(context, {
appLifecycle: dependents.appLifecycle,
config: dependents.config,
replicator: dependents.replicator,
confirm: browserConfirm,
control: dependents.control,
});
super(context, {
dialogManager: obsidianSvelteDialogManager,
APIService: dependents.API,
});
}
}

View File

@@ -0,0 +1,33 @@
import { BrowserServiceHub, type BrowserServiceHost } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices";
import type { KeyValueDatabaseFactory } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
import { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService";
import { BrowserConfirm } from "./BrowserConfirm";
import { LiveSyncBrowserUIService } from "./LiveSyncBrowserUIService";
export type LiveSyncBrowserServiceHubOptions<T extends ServiceContext> = {
context?: T;
openKeyValueDatabase?: KeyValueDatabaseFactory;
};
function createLiveSyncBrowserHost<T extends ServiceContext>(): BrowserServiceHost<T> {
return {
createAPI(context) {
return new BrowserAPIService(context, {
confirm: new BrowserConfirm(context),
});
},
createUI(context, dependencies) {
return new LiveSyncBrowserUIService(context, dependencies);
},
};
}
export function createLiveSyncBrowserServiceHub<T extends ServiceContext>(
options: LiveSyncBrowserServiceHubOptions<T> = {}
): BrowserServiceHub<T> {
return new BrowserServiceHub<T>({
...options,
host: createLiveSyncBrowserHost<T>(),
});
}

View File

@@ -0,0 +1,28 @@
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
import { describe, expect, it } from "vitest";
import {
observeServiceComposition,
observeServiceContext,
SERVICE_CONTEXT_MEMBERS,
} from "../../../test/contracts/serviceContext";
import { createLiveSyncBrowserServiceHub } from "./createLiveSyncBrowserServiceHub";
describe("LiveSync browser service context contract", () => {
it("preserves one injected context and its API results throughout the Webapp composition", () => {
const context = createServiceContext({
translate: (key) => `webapp:${key}`,
});
const hub = createLiveSyncBrowserServiceHub({ context });
expect(observeServiceContext(context, "message")).toEqual({
translation: "webapp:message",
receivedEvents: ["context-contract-event"],
});
const composition = observeServiceComposition(hub, context);
expect(composition.hubUsesExpectedContext).toBe(true);
expect(SERVICE_CONTEXT_MEMBERS.filter((member) => !composition.servicesUsingExpectedContext[member])).toEqual(
[]
);
});
});

View File

@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"noEmit": true,
"paths": {
"@/*": ["../../*"]
}
},
"include": ["**/*.ts", "**/*.svelte"],
"exclude": ["**/*.unit.spec.ts"]
}

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import type { MenuItem } from "@/apps/browser/BrowserMenu";
type Props = {
item: MenuItem;
closeMenu: () => void;
};
const { item = $bindable(), closeMenu }: Props = $props();
function handleCommit(event: MouseEvent | KeyboardEvent) {
event.preventDefault();
try {
if (item.handler) {
item.handler?.();
}
} catch (ex) {
console.error(ex);
}
closeMenu();
}
const icons = {
checkmark: "✓",
} as { [key: string]: string };
function renderIcon(item: MenuItem) {
if (item.icon && item.icon in icons) {
return icons[item.icon] ?? item.icon;
} else if (item.icon !== undefined) {
return "";
}
return "";
}
</script>
<li>
<span class="icon">{renderIcon(item)}</span>
<label for=""
><!-- svelte-ignore a11y_invalid_attribute -->
<a onclick={handleCommit} onkeydown={handleCommit} role="button" tabindex="0" href="#">{item.title}</a></label
>
</li>
<style>
span.icon {
display: inline-block;
min-width: 1.5em;
text-align: center;
}
li {
list-style: none;
padding: 0.5em 1em;
}
</style>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import type { MenuSeparator } from "@/apps/browser/BrowserMenu";
type Props = {
item: MenuSeparator;
};
const { item = $bindable() }: Props = $props();
</script>
<hr />

View File

@@ -0,0 +1,89 @@
<script lang="ts">
import type { Menu, MenuItem, MenuSeparator } from "@/apps/browser/BrowserMenu";
import MenuItemView from "./MenuItemView.svelte";
import MenuSeparatorView from "./MenuSeparatorView.svelte";
type Props = {
items: (MenuItem | MenuSeparator)[];
closeMenu: () => void;
};
const { items = $bindable(), closeMenu }: Props = $props();
function handleKey(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
closeMenu();
}
}
</script>
<popup>
<nav>
{#each items as item}
{#if item.type === "separator"}
<MenuSeparatorView {item} />
{:else if item.type === "item"}
<MenuItemView item={item as MenuItem} {closeMenu} />
{/if}
{/each}
</nav>
</popup>
<div class="background" onclick={() => closeMenu()} onkeydown={handleKey} role="none"></div>
<style>
popup {
z-index: 1000;
position: fixed;
background: rgba(255, 255, 255, 0.8);
max-width: 70vw;
max-height: 80vh;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid var(--background-primary-alt);
justify-content: space-between;
width: fit-content;
height: fit-content;
}
/* popup header {
background: var(--background-primary);
color: var(--text-normal);
padding: 1em;
border-bottom: 1px solid var(--background-primary-alt);
font: size 1.4em;
}
popup article {
padding: 1em;
display: flex;
justify-content: center;
overflow-y: auto;
}
popup article .msg {
white-space: pre-wrap;
}
popup .buttons {
border-top: 1px solid var(--background-primary-alt);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
popup .buttons button {
margin: 0 0.5em;
background-color: var(--background-primary-alt);
} */
popup ~ .background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.125);
}
</style>

View File

@@ -0,0 +1,127 @@
<script lang="ts">
import { renderMessageMarkdown } from "./renderMessageMarkdown";
type Props = {
title: string;
message: string;
buttons: string[];
commit: (button: string) => void;
};
let { title, message, buttons, commit }: Props = $props();
const renderedMessage = $derived(renderMessageMarkdown(message));
function handleEsc(event: KeyboardEvent) {
if (event.key === "Escape") {
commit("");
}
}
</script>
<popup>
<header>{title}</header>
<article><div class="msg">{@html renderedMessage}</div></article>
<div class="buttons">
{#each buttons as button}
<button onclick={() => commit(button)}>{button}</button>
{/each}
</div>
</popup>
<div class="background" onclick={() => commit("")} onkeydown={handleEsc} role="none"></div>
<style>
popup {
z-index: 1000;
position: fixed;
background: rgba(255, 255, 255, 0.8);
max-width: 70vw;
max-height: 80vh;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-width: 50vw;
min-height: 50vh;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid var(--background-primary-alt);
justify-content: space-between;
}
popup header {
background: var(--background-primary);
color: var(--text-normal);
padding: 1em;
border-bottom: 1px solid var(--background-primary-alt);
font: size 1.4em;
}
popup article {
padding: 1em;
display: flex;
justify-content: center;
overflow-y: auto;
}
popup article .msg {
width: 100%;
line-height: 1.5;
overflow-wrap: anywhere;
}
popup article .msg :global(:first-child) {
margin-top: 0;
}
popup article .msg :global(:last-child) {
margin-bottom: 0;
}
popup article .msg :global(pre) {
overflow-x: auto;
padding: 0.75em;
border-radius: 4px;
background: var(--background-secondary);
}
popup article .msg :global(code) {
font-family: var(--font-monospace);
}
popup article .msg :global(blockquote) {
margin: 0;
padding-left: 1em;
border-left: 3px solid var(--background-modifier-border);
color: var(--text-muted);
}
popup article .msg :global(ul),
popup article .msg :global(ol) {
padding-left: 1.5em;
}
popup article .msg :global(table) {
width: 100%;
border-collapse: collapse;
}
popup article .msg :global(th),
popup article .msg :global(td) {
padding: 0.4em 0.6em;
border: 1px solid var(--background-modifier-border);
}
popup article .msg :global(a) {
color: var(--text-accent);
}
popup .buttons {
border-top: 1px solid var(--background-primary-alt);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
popup .buttons button {
margin: 0 0.5em;
background-color: var(--background-primary-alt);
}
popup ~ .background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.125);
}
</style>

View File

@@ -0,0 +1,126 @@
<script lang="ts">
type Props = {
title: string;
message: string;
initialText?: string;
placeholder?: string;
isPassword?: boolean;
commit: (text: string | false) => void;
};
const { title, message, commit, initialText, placeholder, isPassword }: Props = $props();
function initialTextSeed(): string {
return initialText ?? "";
}
let text = $state(initialTextSeed());
const type = $derived(isPassword ? "password" : "text");
function cancel() {
commit(false);
}
function handleKey(event: KeyboardEvent) {
if (event.key === "Escape") {
handleCancel(event);
} else if (event.key === "Enter") {
handleCommit(event);
}
}
function handleCancel(event: KeyboardEvent | MouseEvent) {
cancel();
event.preventDefault();
}
function handleCommit(event: KeyboardEvent | MouseEvent) {
commit(text);
event.preventDefault();
}
let textEl: HTMLInputElement;
$effect(() => {
textEl.focus();
});
</script>
<popup>
<header>{title}</header>
<article>
<div class="msg">{message}</div>
<div class="input">
<input
bind:this={textEl}
{type}
bind:value={text}
{placeholder}
onkeydown={handleKey}
onkeyup={handleKey}
/>
</div>
</article>
<div class="buttons">
<button onclick={handleCommit}>OK</button>
<button onclick={handleCancel}>Cancel</button>
</div>
</popup>
<div class="background" onclick={handleCancel} onkeydown={handleKey} role="none"></div>
<style>
popup {
z-index: 1000;
position: fixed;
background: rgba(255, 255, 255, 0.8);
max-width: 70vw;
max-height: 80vh;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-width: 50vw;
min-height: 50vh;
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
border: 1px solid var(--background-primary-alt);
justify-content: space-between;
}
popup header {
background: var(--background-primary);
color: var(--text-normal);
padding: 1em;
border-bottom: 1px solid var(--background-primary-alt);
font: size 1.4em;
}
popup article {
align-items: center;
padding: 1em;
display: flex;
justify-content: center;
flex-direction: column;
}
popup article .msg {
overflow-y: auto;
white-space: pre-wrap;
}
popup .buttons {
border-top: 1px solid var(--background-primary-alt);
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
}
popup .buttons button {
margin: 0 0.5em;
background-color: var(--background-primary-alt);
}
popup ~ .background {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.125);
}
</style>

View File

@@ -0,0 +1,21 @@
import MarkdownIt from "markdown-it";
const markdownRenderer = new MarkdownIt({
html: false,
breaks: true,
linkify: true,
});
const defaultLinkOpenRenderer =
markdownRenderer.renderer.rules.link_open ??
((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
markdownRenderer.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet("target", "_blank");
tokens[idx].attrSet("rel", "noopener noreferrer");
return defaultLinkOpenRenderer(tokens, idx, options, env, self);
};
export function renderMessageMarkdown(message: string): string {
return markdownRenderer.render(message);
}

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { renderMessageMarkdown } from "./renderMessageMarkdown";
describe("renderMessageMarkdown", () => {
it("renders basic markdown features used by browser dialogues", () => {
const html = renderMessageMarkdown("# Title\n\n| left | right |\n| --- | --- |\n| a | b |\n");
expect(html).toContain("<h1>Title</h1>");
expect(html).toContain("<table>");
expect(html).toContain("<td>a</td>");
});
it("escapes inline HTML instead of rendering it", () => {
const html = renderMessageMarkdown("Before<script>alert('xss')</script>After");
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;alert('xss')&lt;/script&gt;");
});
it("opens Markdown links safely in a new tab", () => {
const html = renderMessageMarkdown("[docs](https://example.com)");
expect(html).toContain('target="_blank"');
expect(html).toContain('rel="noopener noreferrer"');
});
});

9
src/apps/cli/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.livesync
test/*
!test/*.sh
test/test-init.local.sh
node_modules
.*.json
*.env
!.test.env
bench-results

8
src/apps/cli/.test.env Normal file
View File

@@ -0,0 +1,8 @@
hostname=http://127.0.0.1:5989/
dbname=livesync-test-db2
minioEndpoint=http://127.0.0.1:9000
username=admin
password=testpassword
accessKey=minioadmin
secretKey=minioadmin
bucketName=livesync-test-bucket

111
src/apps/cli/Dockerfile Normal file
View File

@@ -0,0 +1,111 @@
# syntax=docker/dockerfile:1
#
# Self-hosted LiveSync CLI — Docker image
#
# Build (from the repository root):
# docker build -f src/apps/cli/Dockerfile -t livesync-cli .
#
# Run:
# docker run --rm -v /path/to/your/vault:/data livesync-cli sync
# docker run --rm -v /path/to/your/vault:/data livesync-cli ls
# docker run --rm -v /path/to/your/vault:/data livesync-cli init-settings
# docker run --rm -v /path/to/your/vault:/data livesync-cli --help
#
# The first positional argument (database-path) is automatically set to /data.
# Mount your vault at /data, or override with: -e LIVESYNC_DB_PATH=/other/path
#
# P2P (WebRTC) networking — important notes
# -----------------------------------------
# The P2P replicator (p2p-host / p2p-sync / p2p-peers) uses WebRTC, which
# generates ICE candidates of three kinds:
#
# host — the container's bridge IP (172.17.x.x). Unreachable from outside
# the Docker bridge, so LAN peers cannot connect via this candidate.
# srflx — the host's public IP, obtained via STUN reflection. Works fine
# over the internet even with the default bridge network.
# relay — traffic relayed through a TURN server. Always reachable regardless
# of network mode.
#
# Recommended network modes per use-case:
#
# LAN P2P (Linux only)
# docker run --network host ...
# This exposes the real host IP as the 'host' candidate so LAN peers can
# connect directly. --network host is not available on Docker Desktop for
# macOS or Windows.
#
# LAN P2P (macOS / Windows Docker Desktop)
# Configure a TURN server in settings (P2P_turnServers / P2P_turnUsername /
# P2P_turnCredential). All data is then relayed through the TURN server,
# bypassing the bridge-network limitation.
#
# Internet P2P
# Default bridge network is sufficient; the srflx candidate carries the
# host's public IP and peers can connect normally.
#
# CouchDB sync only (no P2P)
# Default bridge network. No special configuration required.
# ─────────────────────────────────────────────────────────────────────────────
# Stage 1 — builder
# Full Node.js environment to compile native modules and bundle the CLI.
# ─────────────────────────────────────────────────────────────────────────────
FROM node:22-slim AS builder
# Build tools required by native Node.js addons (mainly leveldown)
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Install workspace dependencies first (layer-cache friendly)
COPY package.json package-lock.json ./
RUN npm install
# Copy the full source tree and build the CLI bundle
COPY . .
RUN cd src/apps/cli && npm run build
# ─────────────────────────────────────────────────────────────────────────────
# Stage 2 — runtime-deps
# Install only the external (unbundled) packages that the CLI requires at
# runtime. Native addons are compiled here against the same base image that
# the final runtime stage uses.
# ─────────────────────────────────────────────────────────────────────────────
FROM node:22-slim AS runtime-deps
# Build tools required to compile native addons
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /deps
# package.json lists only the packages that the CLI requires
COPY src/apps/cli/package.json ./package.json
RUN npm install --omit=dev
# ─────────────────────────────────────────────────────────────────────────────
# Stage 3 — runtime
# Minimal image: pre-compiled native modules + CLI bundle only.
# No build tools are included, keeping the image small.
# ─────────────────────────────────────────────────────────────────────────────
FROM node:22-slim
WORKDIR /app
# Copy pre-compiled external node_modules from runtime-deps stage
COPY --from=runtime-deps /deps/node_modules ./node_modules
# Copy the built CLI bundle from builder stage
COPY --from=builder /build/src/apps/cli/dist ./dist
# Install entrypoint wrapper
COPY src/apps/cli/docker-entrypoint.sh /usr/local/bin/livesync-cli
RUN chmod +x /usr/local/bin/livesync-cli
# Mount your vault / local database directory here
VOLUME ["/data"]
ENTRYPOINT ["livesync-cli"]

634
src/apps/cli/README.md Normal file
View File

@@ -0,0 +1,634 @@
# Self-hosted LiveSync CLI
Command-line version of Self-hosted LiveSync plug-in for syncing vaults without Obsidian.
## Features
- ✅ Sync Obsidian vaults using CouchDB without running Obsidian
- ✅ Compatible with Self-hosted LiveSync plug-in settings
- ✅ Supports all core sync features (encryption, conflict resolution, etc.)
- ✅ Lightweight and headless operation
- ✅ Cross-platform (Windows, macOS, Linux)
## Architecture
This CLI version is built using the same core as the Obsidian plug-in:
```
CLI Main
└─ NodeServiceContext (events, translation, database root, and injected standard I/O)
└─ LiveSyncBaseCore<NodeServiceContext, IMinimumLiveSyncCommands>
├─ NodeServiceHub (All services without Obsidian dependencies)
└─ ServiceModules (wired by initialiseServiceModulesCLI)
├─ FileAccessCLI (Node.js FileSystemAdapter)
├─ StorageEventManagerCLI
├─ ServiceFileAccessCLI
├─ ServiceDatabaseFileAccessCLI
├─ ServiceFileHandler
└─ ServiceRebuilder
```
### Key Components
1. **Node.js FileSystem Adapter** (`adapters/`)
- Platform-agnostic file operations using Node.js `fs/promises`
- Implements same interface as Obsidian's file system
2. **Service Modules** (`serviceModules/`)
- Initialised by `initialiseServiceModulesCLI`
- All core sync functionality preserved
3. **Service Hub and Settings Services** (`services/`)
- `NodeServiceContext` owns the host-selected database root and standard input/output implementation
- `NodeServiceHub` receives that exact Context instead of constructing platform capabilities implicitly
- Internal adapter diagnostics use injected callbacks wired to the service logging API
- Node-specific settings and key-value services are provided without Obsidian dependencies
4. **Main Entry Point** (`main.ts`)
- Command-line interface
- Settings management (JSON file)
- Graceful shutdown handling
## Usage
The CLI operates on a **database directory** which contains PouchDB data and settings.
> [!NOTE]
> `livesync-cli` is the alias for the CLI executable. Please replace with the actual command of your installation (e.g. `npm run --silent cli --` or `docker run ...`).
```bash
livesync-cli [database-path] [command] [args...]
```
### Arguments
- `database-path`: Path to the directory where `.livesync` folder and `settings.json` are (or will be) located.
- Note: In previous versions, this was referred to as the "vault" path. Now it is clearly distinguished from the actual vault (the directory containing your `.md` files).
- `--vault <path>` / `-V <path>`: (daemon/mirror only) Path to the vault directory containing `.md` files.
- Allows the PouchDB database directory and the actual vault directory to be different locations.
- For `mirror` command, the positional `[vault-path]` argument takes precedence over `--vault`.
### Commands
- `sync`: Run one replication cycle with the remote CouchDB.
- `mirror [vault-path]`: Bidirectional sync between the local database and a local directory (**the actual vault**).
- If `vault-path` is provided, the CLI will synchronise the database with files in the vault directory.
- If `vault-path` is omitted, it defaults to `database-path` (compatibility mode).
- Use this command to keep your local `.md` files in sync with the database.
- `ls [prefix]`: List files currently stored in the local database.
- `push <src> <dst>`: Push a local file `<src>` into the database at path `<dst>`.
- `pull <src> <dst>`: Pull a file `<src>` from the database into local file `<dst>`.
- `cat <src>`: Read a file from the database and write to stdout.
- `put <dst>`: Read from stdin and write to the database path `<dst>`.
- `remote-add <name> <connstr>`: Add a remote configuration from a connection string.
- `remote-rm <remote-id>`: Remove a remote configuration by ID.
- `remote-ls`: List remote configurations (`id`, `name`, `active/inactive`, redacted URI).
- `remote-export <remote-id>`: Export the stored connection string by remote ID.
- `remote-set <remote-id> <connstr>`: Replace the stored connection string by remote ID.
- `remote-activate <remote-id>`: Activate a remote configuration by ID.
- `mark-resolved [remote-id]`: Resolve remote synchronisation status.
- `unlock-remote [remote-id]`: Unlock the remote database.
- `lock-remote [remote-id]`: Lock the remote database.
- `remote-status [remote-id]`: Show remote database status.
- `init-settings [file]`: Create a default settings file.
### Examples
```bash
# Basic sync with remote
livesync-cli ./my-db sync
# Mirroring to your actual Obsidian vault
livesync-cli ./my-db mirror /path/to/obsidian-vault
# Manual file operations
livesync-cli ./my-db push ./note.md folder/note.md
livesync-cli ./my-db pull folder/note.md ./note.md
```
## Installation
### Build from source
```bash
# Clone the repository
git clone <repository-url>
cd obsidian-livesync
# Install dependencies from the repository root
npm install
# Build the CLI from the repository root
npm run build -w self-hosted-livesync-cli
# Or from the package directory
cd src/apps/cli
npm run build
```
The shared core is installed as the exact `@vrtmrz/livesync-commonlib` package artefact recorded in the root lockfile.
Run the CLI:
```bash
# Run with npm workspace script (from repository root)
npm run cli -w self-hosted-livesync-cli -- [database-path] [command] [args...]
# Or from the package directory
cd src/apps/cli
npm run cli -- [database-path] [command] [args...]
# Run the built executable directly
node src/apps/cli/dist/index.cjs [database-path] [command] [args...]
```
### Docker
A Docker image is provided for headless / server deployments. Build from the repository root:
```bash
docker build -f src/apps/cli/Dockerfile -t livesync-cli .
```
Run:
```bash
# Sync with CouchDB
docker run --rm -v /path/to/your/db:/data livesync-cli sync
# Mirror to a specific vault directory
docker run --rm -v /path/to/your/db:/data -v /path/to/your/vault:/vault livesync-cli mirror /vault
# List files in the local database
docker run --rm -v /path/to/your/db:/data livesync-cli ls
```
The database directory is mounted at `/data` by default. Override with `-e LIVESYNC_DB_PATH=/other/path`.
#### P2P (WebRTC) and Docker networking
The P2P replicator (`p2p-host`, `p2p-sync`, `p2p-peers`) uses WebRTC and generates
three kinds of ICE candidates. The default Docker bridge network affects which
candidates are usable:
| Candidate type | Description | Bridge network |
| -------------- | ---------------------------------- | -------------------------- |
| `host` | Container bridge IP (`172.17.x.x`) | Unreachable from LAN peers |
| `srflx` | Host public IP via STUN reflection | Works over the internet |
| `relay` | Traffic relayed via TURN server | Always reachable |
**LAN P2P on Linux** — use `--network host` so that the real host IP is
advertised as the `host` candidate:
```bash
docker run --rm --network host -v /path/to/your/vault:/data livesync-cli p2p-host
```
Note: also fix the alias to include `--network host` if you want to use `livesync-cli` for P2P commands.
> `--network host` is not available on Docker Desktop for macOS or Windows.
**LAN P2P on macOS / Windows Docker Desktop** — configure a TURN server in the
settings file (`P2P_turnServers`, `P2P_turnUsername`, `P2P_turnCredential`).
All P2P traffic will then be relayed through the TURN server, bypassing the
bridge-network limitation.
**Internet P2P** — the default bridge network is sufficient. The `srflx`
candidate carries the host's public IP and peers can connect normally.
**CouchDB sync only (no P2P)** — no special network configuration is required.
### Adding `livesync-cli` alias
To use the `livesync-cli` command globally, you can add an alias to your shell configuration file (e.g., `.zshrc` or `.bashrc`).
If you are using `npm run`, add the following line:
```bash
alias livesync-cli='npm run --silent --prefix /path/to/repository/src/apps/cli cli --'
# or
alias livesync-cli="npm run --silent --prefix $PWD cli --"
```
Alternatively, if you want to use the built executable directly:
```bash
alias livesync-cli='node /path/to/repository/src/apps/cli/dist/index.cjs'
or
alias livesync-cli="node $PWD/dist/index.cjs"
```
If you prefer using Docker:
```bash
alias livesync-cli='docker run --rm -v /path/to/your/db:/data livesync-cli'
```
After adding the alias, restart your shell or run `source ~/.zshrc` (or `.bashrc`).
## Usage
### Basic Usage
As you know, the CLI is designed to be used in a headless environment. Hence all operations are performed against a local vault directory and a settings file. Here are some example commands:
```bash
# Sync local database with CouchDB (no files will be changed).
livesync-cli /path/to/your-local-database --settings /path/to/settings.json sync
# Push files to local database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json push /your/storage/file.md /vault/path/file.md
# Pull files from local database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json pull /vault/path/file.md /your/storage/file.md
# Verbose logging
livesync-cli /path/to/your-local-database --settings /path/to/settings.json --verbose
# Apply setup URI to settings file (settings only; does not run synchronisation)
livesync-cli /path/to/your-local-database --settings /path/to/settings.json setup "obsidian://setuplivesync?settings=..."
# Put text from stdin into local database
echo "Hello from stdin" | livesync-cli /path/to/your-local-database --settings /path/to/settings.json put /vault/path/file.md
# Output a file from local database to stdout
livesync-cli /path/to/your-local-database --settings /path/to/settings.json cat /vault/path/file.md
# Output a specific revision of a file from local database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json cat-rev /vault/path/file.md 3-abcdef
# Pull a specific revision of a file from local database to local storage
livesync-cli /path/to/your-local-database --settings /path/to/settings.json pull-rev /vault/path/file.md /your/storage/file.old.md 3-abcdef
# List files in local database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json ls /vault/path/
# Show metadata for a file in local database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json info /vault/path/file.md
# Mark a file as deleted in local database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json rm /vault/path/file.md
# Resolve conflict by keeping a specific revision
livesync-cli /path/to/your-local-database --settings /path/to/settings.json resolve /vault/path/file.md 3-abcdef
# Add, list, activate, and remove remote configurations
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-add main "sls+https://user:pass@example.com/db"
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-ls
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-export remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-set remote-abc123 "sls+p2p://room-abc?passphrase=secret"
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-activate remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-rm remote-abc123
# Lock, unlock, resolve, and view status of remote database
livesync-cli /path/to/your-local-database --settings /path/to/settings.json remote-status remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json lock-remote remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json mark-resolved remote-abc123
livesync-cli /path/to/your-local-database --settings /path/to/settings.json unlock-remote remote-abc123
```
### Configuration
The CLI uses the same settings format as the Obsidian plug-in. Create a `.livesync/settings.json` file in your vault directory:
```json
{
"couchDB_URI": "http://localhost:5984",
"couchDB_USER": "admin",
"couchDB_PASSWORD": "password",
"couchDB_DBNAME": "obsidian-livesync",
"liveSync": true,
"syncOnSave": true,
"syncOnStart": true,
"encrypt": true,
"passphrase": "your-encryption-passphrase",
"usePluginSync": false,
"isConfigured": true
}
```
**Minimum required settings:**
- `couchDB_URI`: CouchDB server URL
- `couchDB_USER`: CouchDB username
- `couchDB_PASSWORD`: CouchDB password
- `couchDB_DBNAME`: Database name
- `isConfigured`: Set to `true` after configuration
### Command-line Reference
```
Usage:
livesync-cli <database-path> [options] <command> [command-args]
livesync-cli init-settings [path]
Arguments:
database-path Path to the local database directory (required except for init-settings)
Options:
--settings, -s <path> Path to settings file (default: .livesync/settings.json in local database directory)
--force, -f Overwrite existing file on init-settings
--verbose, -v Enable verbose logging
--debug, -d Enable debug logging (includes verbose)
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
--vault <path>, -V <path> (daemon/mirror) Path to vault directory, decoupled from database-path
--help, -h Show this help message
Commands:
daemon (default) Run mirror scan then continuously sync CouchDB <-> local filesystem
init-settings [path] Create settings JSON from DEFAULT_SETTINGS
sync Run one replication cycle and exit
p2p-peers <timeout> Show discovered peers as [peer]<TAB><peer-id><TAB><peer-name>
p2p-sync <peer> <timeout> Synchronise with specified peer-id or peer-name
p2p-host Start P2P host mode and wait until interrupted (Ctrl+C)
push <src> <dst> Push local file <src> into local database path <dst>
pull <src> <dst> Pull file <src> from local database into local file <dst>
pull-rev <src> <dst> <rev> Pull specific revision <rev> into local file <dst>
setup <setupURI> Apply setup URI to settings file
put <dst> Read text from standard input and write to local database path <dst>
cat <src> Write latest file content from local database to standard output
cat-rev <src> <rev> Write specific revision <rev> content from local database to standard output
ls [prefix] List files as path<TAB>size<TAB>mtime<TAB>revision[*]
info <path> Show file metadata including current and past revisions, conflicts, and chunk list
rm <path> Mark file as deleted in local database
resolve <path> <rev> Resolve conflict by keeping the specified revision
mirror [vaultPath] Mirror database contents to the local file system
(vaultPath positional arg > --vault flag > database-path)
```
Run via npm script:
```bash
npm run --silent cli -- [database-path] [options] [command] [command-args]
```
#### Detailed Command Descriptions
##### ls
`ls` lists files in the local database with optional prefix filtering. Output format is:
```vault/path/file.md<TAB>size<TAB>mtime<TAB>revision[*]
```
Note: `*` indicates if the file has conflicts.
##### p2p-peers
`p2p-peers <timeout>` waits for the specified number of seconds, then prints each discovered peer on a separate line:
```text
[peer]<TAB><peer-id><TAB><peer-name>
```
Use this command to select a target for `p2p-sync`.
##### p2p-sync
`p2p-sync <peer> <timeout>` discovers peers up to the specified timeout and synchronises with the selected peer.
- `<peer>` accepts either `peer-id` or `peer-name` from `p2p-peers` output.
- On success, the command prints a completion message to standard error and exits with status code `0`.
- On failure, the command prints an error message and exits non-zero.
##### p2p-host
`p2p-host` starts the local P2P host and keeps running until interrupted.
- Other peers can discover and synchronise with this host while it is running.
- Stop the host with `Ctrl+C`.
- In CLI mode, behaviour is non-interactive and acceptance follows settings.
##### info
`info` output fields:
- `id`: Document ID
- `revision`: Current revision
- `conflicts`: Conflicted revisions, or `N/A`
- `filename`: Basename of path
- `path`: Vault-relative path
- `size`: Size in bytes
- `revisions`: Available non-current revisions
- `chunks`: Number of chunk IDs
- `children`: Chunk ID list
##### mirror
`mirror` is a command that synchronises your storage with your local vault. It is essentially a process that runs upon startup in Obsidian.
In other words, it performs the following actions:
1. **Precondition checks** — Aborts early if any of the following conditions are not met:
- Settings must be configured (`isConfigured: true`).
- File watching must not be suspended (`suspendFileWatching: false`).
- Remediation mode must be inactive (`maxMTimeForReflectEvents: 0`).
2. **State restoration** — On subsequent runs (after the first successful scan), restores the previous storage state before proceeding.
3. **Expired deletion cleanup** — If `automaticallyDeleteMetadataOfDeletedFiles` is set to a positive number of days, any document that is marked deleted and whose `mtime` is older than the retention period is permanently removed from the local database.
4. **File collection** — Enumerates files from two sources:
- **Storage**: all files under the vault path that pass `isTargetFile`.
- **Local database**: all normal documents (fetched with conflict information) whose paths are valid and pass `isTargetFile`.
- Both collections build case-insensitive ↔ case-sensitive path maps, controlled by `handleFilenameCaseSensitive`.
5. **Categorisation and synchronisation** — The union of both file sets is split into three groups and processed concurrently (up to 10 files at a time):
| Group | Condition | Action |
| ----------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **UPDATE DATABASE** | File exists in storage only | Store the file into the local database. |
| **UPDATE STORAGE** | File exists in database only | If the entry is active (not deleted) and not conflicted, restore the file from the database to storage. Deleted entries and conflicted entries are skipped. |
| **SYNC DATABASE AND STORAGE** | File exists in both | Compare `mtime` freshness. If storage is newer → write to database (`STORAGE → DB`). If database is newer → restore to storage (`STORAGE ← DB`). If equal → do nothing. Conflicted documents and files exceeding the size limit are always skipped. |
6. **Initialisation flag** — On the very first successful run, writes `initialized = true` to the key-value database so that subsequent runs can restore state in step 2.
Note: `mirror` does not respect file deletions. If a file is deleted in storage, it will be restored on the next `mirror` run. To delete a file, use the `rm` command instead. This is a little inconvenient, but it is intentional behaviour (if we handle this automatically in `mirror`, we should be against a ton of edge cases).
##### daemon
`daemon` is the default command when no command is specified. It runs an initial mirror scan and then continuously syncs changes in both directions:
- **CouchDB → local filesystem**: via the `_changes` feed (LiveSync mode, default) or periodic polling (`--interval N`).
- **local filesystem → CouchDB**: via chokidar file watching. Any file created, modified, or deleted in the vault directory is pushed to CouchDB.
In **LiveSync mode** the `_changes` feed delivers remote changes as they arrive, with sub-second latency. In **polling mode** (`--interval N`) the CLI polls CouchDB every N seconds. Use polling mode if your CouchDB instance does not support long-lived HTTP connections, or if you need predictable network usage.
The daemon exits cleanly on `SIGINT` or `SIGTERM`.
```bash
# LiveSync mode (default — _changes feed, near-real-time)
livesync-cli /path/to/vault
# Polling mode — poll every 60 seconds
livesync-cli /path/to/vault --interval 60
```
### .livesync/ignore
Place a `.livesync/ignore` file in your vault root to exclude files from sync in both directions (local → CouchDB and CouchDB → local).
**Format:**
- Lines beginning with `#` are comments.
- Blank lines are ignored.
- All other lines are [minimatch](https://github.com/isaacs/minimatch) glob patterns, relative to the vault root.
- The directive `import: .gitignore` (exactly this string) reads `.gitignore` from the vault root and merges its non-comment, non-blank lines into the ignore rules.
- Negation patterns (lines starting with `!`) are not supported and will cause an error on load.
**Example `.livesync/ignore`:**
```
# Ignore temporary files
*.tmp
*.swp
# Ignore build output
build/
dist/
# Merge patterns from .gitignore
import: .gitignore
```
Patterns apply in both directions: the chokidar watcher will not emit events for matched files, and the `isTargetFile` filter will exclude them from CouchDB → local sync.
Changes to this file require a daemon restart to take effect.
### Systemd Installation
The `deploy/` directory contains a systemd unit template and an install script.
**Automated install (user service, recommended):**
```bash
bash src/apps/cli/deploy/install.sh --vault /path/to/vault
```
**With polling interval:**
```bash
bash src/apps/cli/deploy/install.sh --vault /path/to/vault --interval 60
```
**System-wide install** (requires root / sudo for `/etc/systemd/system/`):
```bash
bash src/apps/cli/deploy/install.sh --system --vault /path/to/vault
```
The script:
1. Builds the CLI (`npm install` + `npm run build`).
2. Installs the binary to `~/.local/bin/livesync-cli` (user) or `/usr/local/bin/livesync-cli` (system).
3. Writes the unit file to `~/.config/systemd/user/livesync-cli.service` (user) or `/etc/systemd/system/livesync-cli.service` (system).
4. Runs `systemctl [--user] daemon-reload && systemctl [--user] enable --now livesync-cli`.
**Manual setup** — if you prefer to manage the unit yourself, copy `deploy/livesync-cli.service`, replace `LIVESYNC_BIN` and `LIVESYNC_VAULT_PATH` with the actual binary path and vault path, then install to the appropriate systemd directory.
### Planned options:
- `--immediate`: Perform sync after the command (e.g. `push`, `pull`, `put`, `rm`).
- `serve`: Start CLI in server mode, exposing REST APIs for remote, and batch operations.
- `cause-conflicted <vaultPath>`: Mark a file as conflicted without changing its content, to trigger conflict resolution in Obsidian.
## Use Cases
### 1. Bootstrap a new headless vault
Create default settings, apply a setup URI, then run one sync cycle.
```bash
livesync-cli -- init-settings /data/livesync-settings.json
printf '%s\n' "$SETUP_PASSPHRASE" | livesync-cli -- /data/vault --settings /data/livesync-settings.json setup "$SETUP_URI"
livesync-cli -- /data/vault --settings /data/livesync-settings.json sync
```
### 2. Scripted import and export
Push local files into the database from automation, and pull them back for export or backup.
```bash
livesync-cli -- /data/vault --settings /data/livesync-settings.json push ./note.md notes/note.md
livesync-cli -- /data/vault --settings /data/livesync-settings.json pull notes/note.md ./exports/note.md
```
### 3. Revision inspection and restore
List metadata, find an older revision, then restore it by content (`cat-rev`) or file output (`pull-rev`).
```bash
livesync-cli -- /data/vault --settings /data/livesync-settings.json info notes/note.md
livesync-cli -- /data/vault --settings /data/livesync-settings.json cat-rev notes/note.md 3-abcdef
livesync-cli -- /data/vault --settings /data/livesync-settings.json pull-rev notes/note.md ./restore/note.old.md 3-abcdef
```
### 4. Conflict and cleanup workflow
Inspect conflicted revisions, resolve by keeping one revision, then delete obsolete files.
```bash
livesync-cli -- /data/vault --settings /data/livesync-settings.json info notes/note.md
livesync-cli -- /data/vault --settings /data/livesync-settings.json resolve notes/note.md 3-abcdef
livesync-cli -- /data/vault --settings /data/livesync-settings.json rm notes/obsolete.md
```
### 5. CI smoke test for content round-trip
Validate that `put`/`cat` is behaving as expected in a pipeline.
```bash
echo "hello-ci" | livesync-cli -- /data/vault --settings /data/livesync-settings.json put ci/test.md
livesync-cli -- /data/vault --settings /data/livesync-settings.json cat ci/test.md
```
## Development
### Project Structure
```
src/apps/cli/
├── commands/ # Command dispatcher and command utilities
│ ├── runCommand.ts
│ ├── runCommand.unit.spec.ts
│ ├── types.ts
│ ├── utils.ts
│ └── utils.unit.spec.ts
├── adapters/ # Node.js FileSystem Adapter
│ ├── NodeConversionAdapter.ts
│ ├── NodeFileSystemAdapter.ts
│ ├── NodePathAdapter.ts
│ ├── NodeStorageAdapter.ts
│ ├── NodeStorageAdapter.unit.spec.ts
│ ├── NodeTypeGuardAdapter.ts
│ ├── NodeTypes.ts
│ └── NodeVaultAdapter.ts
├── lib/
│ └── pouchdb-node.ts
├── managers/ # CLI-specific managers
│ ├── CLIStorageEventManagerAdapter.ts
│ └── StorageEventManagerCLI.ts
├── serviceModules/ # Service modules (ported from main.ts)
│ ├── CLIServiceModules.ts
│ ├── DatabaseFileAccess.ts
│ ├── FileAccessCLI.ts
│ └── ServiceFileAccessImpl.ts
├── services/
│ ├── NodeKeyValueDBService.ts
│ ├── NodeServiceHub.ts
│ └── NodeSettingService.ts
├── test/
│ ├── test-e2e-two-vaults-common.sh
│ ├── test-e2e-two-vaults-matrix.sh
│ ├── test-e2e-two-vaults-with-docker-linux.sh
│ ├── test-push-pull-linux.sh
│ ├── test-setup-put-cat-linux.sh
│ └── test-sync-two-local-databases-linux.sh
├── .gitignore
├── entrypoint.ts # CLI executable entry point (shebang)
├── main.ts # CLI entry point
├── main.unit.spec.ts
├── package.json
├── README.md # This file
├── tsconfig.json
├── util/ # Test and local utility scripts
└── vite.config.ts
```

View File

@@ -0,0 +1,28 @@
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { path } from "@vrtmrz/livesync-commonlib/node";
/**
* Conversion adapter implementation for Node.js
*/
export class NodeConversionAdapter implements IConversionAdapter<NodeFile, NodeFolder> {
nativeFileToUXFileInfoStub(file: NodeFile): UXFileInfoStub {
return {
name: path.basename(file.path),
path: file.path,
stat: file.stat,
isFolder: false,
};
}
nativeFolderToUXFolder(folder: NodeFolder): UXFolderInfo {
return {
name: path.basename(folder.path),
path: folder.path,
isFolder: true,
children: [],
parent: path.dirname(folder.path) as FilePath,
};
}
}

View File

@@ -0,0 +1,179 @@
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import { NodePathAdapter } from "./NodePathAdapter";
import { NodeTypeGuardAdapter } from "./NodeTypeGuardAdapter";
import { NodeConversionAdapter } from "./NodeConversionAdapter";
import { NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput";
/**
* Complete file system adapter implementation for Node.js
*/
export class NodeFileSystemAdapter implements IFileSystemAdapter<NodeFile, NodeFile, NodeFolder, NodeStat> {
readonly path: NodePathAdapter;
readonly typeGuard: NodeTypeGuardAdapter;
readonly conversion: NodeConversionAdapter;
readonly storage: NodeStorageAdapter;
readonly vault: NodeVaultAdapter;
private fileCache = new Map<string, NodeFile>();
constructor(
private basePath: string,
private reportDiagnostic: CliDiagnosticReporter = () => undefined
) {
this.path = new NodePathAdapter();
this.typeGuard = new NodeTypeGuardAdapter();
this.conversion = new NodeConversionAdapter();
this.storage = new NodeStorageAdapter(basePath);
this.vault = new NodeVaultAdapter(basePath);
}
private resolvePath(p: FilePath | string): string {
return path.join(this.basePath, p);
}
private normalisePath(p: FilePath | string): string {
return this.path.normalisePath(p);
}
private async hasExactPathCase(pathStr: string): Promise<boolean> {
try {
const segments = pathStr.split("/").filter((segment) => segment !== "");
let currentPath = this.basePath;
for (const segment of segments) {
const entries = await fs.readdir(currentPath);
if (!entries.includes(segment)) return false;
currentPath = path.join(currentPath, segment);
}
return segments.length > 0;
} catch {
return false;
}
}
async getAbstractFileByPath(p: FilePath | string): Promise<NodeFile | null> {
const pathStr = this.normalisePath(p);
if (!this.fileCache.has(pathStr) && !(await this.hasExactPathCase(pathStr))) {
this.fileCache.delete(pathStr);
return null;
}
return await this.refreshFile(pathStr);
}
async getAbstractFileByPathInsensitive(p: FilePath | string): Promise<NodeFile | null> {
const pathStr = this.normalisePath(p);
const exact = await this.getAbstractFileByPath(pathStr);
if (exact) {
return exact;
}
const lowerPath = pathStr.toLowerCase();
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
if (cachedPath.toLowerCase() === lowerPath) {
return cachedFile;
}
}
await this.scanDirectory();
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
if (cachedPath.toLowerCase() === lowerPath) {
return cachedFile;
}
}
return null;
}
async getFiles(): Promise<NodeFile[]> {
if (this.fileCache.size === 0) {
await this.scanDirectory();
}
return Array.from(this.fileCache.values());
}
async renameFile(file: NodeFile, newPath: string): Promise<NodeFile> {
const oldPath = file.path;
await this.vault.rename(file, newPath);
this.fileCache.delete(oldPath);
const renamedFile = await this.refreshFile(newPath);
if (!renamedFile) throw new Error(`Could not find renamed file: ${newPath}`);
return renamedFile;
}
async statFromNative(file: NodeFile): Promise<UXStat> {
return file.stat;
}
async reconcileInternalFile(p: string): Promise<void> {
// No-op in Node.js version
// This is used by Obsidian to sync internal file metadata
}
async refreshFile(p: string): Promise<NodeFile | null> {
const pathStr = this.normalisePath(p);
try {
const fullPath = this.resolvePath(pathStr);
const stat = await fs.stat(fullPath);
if (!stat.isFile()) {
this.fileCache.delete(pathStr);
return null;
}
const file: NodeFile = {
path: pathStr as FilePath,
stat: {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
ctime: Math.floor(stat.ctimeMs),
type: "file",
},
};
this.fileCache.set(pathStr, file);
return file;
} catch {
// Evict so a deleted file is not returned by subsequent cache scans.
this.fileCache.delete(pathStr);
return null;
}
}
/**
* Helper method to recursively scan directory and populate file cache
*/
async scanDirectory(relativePath: string = ""): Promise<void> {
const fullPath = this.resolvePath(relativePath);
try {
const entries = await fs.readdir(fullPath, { withFileTypes: true });
for (const entry of entries) {
const entryRelativePath = path.join(relativePath, entry.name).replace(/\\/g, "/");
if (entry.isDirectory()) {
await this.scanDirectory(entryRelativePath);
} else if (entry.isFile()) {
const entryFullPath = this.resolvePath(entryRelativePath);
const stat = await fs.stat(entryFullPath);
const file: NodeFile = {
path: entryRelativePath as FilePath,
stat: {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
ctime: Math.floor(stat.ctimeMs),
type: "file",
},
};
this.fileCache.set(entryRelativePath, file);
}
}
} catch (error) {
// Directory doesn't exist or is not readable
this.reportDiagnostic(`Error scanning directory ${fullPath}:`, error);
}
}
}

View File

@@ -0,0 +1,18 @@
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile } from "./NodeTypes";
import { path } from "@vrtmrz/livesync-commonlib/node";
/**
* Path adapter implementation for Node.js
*/
export class NodePathAdapter implements IPathAdapter<NodeFile> {
getPath(file: string | NodeFile): FilePath {
return (typeof file === "string" ? file : file.path) as FilePath;
}
normalisePath(p: string): string {
// Normalize path separators to forward slashes (like Obsidian)
return path.normalize(p).replace(/\\/g, "/");
}
}

View File

@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it } from "vitest";
import { storageAdapterContractCases } from "@/apps/_test/storageAdapterContract";
import { fsPromises as fs, os, path, NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node";
describe("NodeStorageAdapter", () => {
const tempDirs: string[] = [];
async function createAdapter() {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "livesync-cli-node-storage-"));
tempDirs.push(tempDir);
return new NodeStorageAdapter(tempDir);
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
for (const contractCase of storageAdapterContractCases) {
it(contractCase.name, async () => {
await contractCase.run(await createAdapter());
});
}
it("writes and reads binary data without corruption", async () => {
const adapter = await createAdapter();
const expected = Uint8Array.from([0x00, 0x7f, 0x80, 0xff, 0x42]);
await adapter.writeBinary("files/blob.bin", expected.buffer.slice(0));
const result = await adapter.readBinary("files/blob.bin");
expect(Array.from(new Uint8Array(result))).toEqual(Array.from(expected));
});
it("returns an ArrayBuffer with the exact file length", async () => {
const adapter = await createAdapter();
const expected = Uint8Array.from([0x10, 0x20, 0x30]);
await adapter.writeBinary("files/small.bin", expected.buffer.slice(0));
const result = await adapter.readBinary("files/small.bin");
expect(result.byteLength).toBe(expected.byteLength);
expect(Array.from(new Uint8Array(result))).toEqual([0x10, 0x20, 0x30]);
});
});

View File

@@ -0,0 +1,26 @@
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
/**
* Type guard adapter implementation for Node.js
*/
export class NodeTypeGuardAdapter implements ITypeGuardAdapter<NodeFile, NodeFolder> {
isFile(file: unknown): file is NodeFile {
return !!(
file &&
typeof file === "object" &&
"path" in file &&
"stat" in file &&
!(file as { isFolder?: boolean }).isFolder
);
}
isFolder(item: unknown): item is NodeFolder {
return !!(
item &&
typeof item === "object" &&
"path" in item &&
(item as { isFolder?: boolean }).isFolder === true
);
}
}

View File

@@ -0,0 +1,22 @@
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
/**
* Node.js file representation
*/
export type NodeFile = {
path: FilePath;
stat: UXStat;
};
/**
* Node.js folder representation
*/
export type NodeFolder = {
path: FilePath;
isFolder: true;
};
/**
* Node.js stat type (compatible with UXStat)
*/
export type NodeStat = UXStat;

View File

@@ -0,0 +1,131 @@
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
/**
* Vault adapter implementation for Node.js
*/
export class NodeVaultAdapter implements IVaultAdapter<NodeFile> {
constructor(private basePath: string) {}
private resolvePath(p: string): string {
return path.join(this.basePath, p);
}
async read(file: NodeFile): Promise<string> {
const content = await fs.readFile(this.resolvePath(file.path), "utf-8");
// Correct stale stat.size — chokidar stats may be from a poll before the final write.
// The downstream document integrity check compares stat.size to content length, so
// they must agree or other clients reject the file as corrupted.
file.stat.size = Buffer.byteLength(content, "utf-8");
return content;
}
async cachedRead(file: NodeFile): Promise<string> {
// No caching in CLI version, just read directly
return await this.read(file);
}
async readBinary(file: NodeFile): Promise<ArrayBuffer> {
const buffer = await fs.readFile(this.resolvePath(file.path));
// Same correction as read() — ensure stat.size matches actual byte length.
file.stat.size = buffer.length;
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
}
async modify(file: NodeFile, data: string, options?: UXDataWriteOptions): Promise<void> {
const fullPath = this.resolvePath(file.path);
await fs.writeFile(fullPath, data, "utf-8");
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
}
async modifyBinary(file: NodeFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
const fullPath = this.resolvePath(file.path);
await fs.writeFile(fullPath, new Uint8Array(data));
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
}
async create(p: string, data: string, options?: UXDataWriteOptions): Promise<NodeFile> {
const fullPath = this.resolvePath(p);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, data, "utf-8");
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
const stat = await fs.stat(fullPath);
return {
path: p as FilePath,
stat: {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
ctime: Math.floor(stat.ctimeMs),
type: "file",
},
};
}
async createBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<NodeFile> {
const fullPath = this.resolvePath(p);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, new Uint8Array(data));
if (options?.mtime || options?.ctime) {
const atime = options.mtime ? new Date(options.mtime) : new Date();
const mtime = options.mtime ? new Date(options.mtime) : new Date();
await fs.utimes(fullPath, atime, mtime);
}
const stat = await fs.stat(fullPath);
return {
path: p as FilePath,
stat: {
size: stat.size,
mtime: Math.floor(stat.mtimeMs),
ctime: Math.floor(stat.ctimeMs),
type: "file",
},
};
}
async rename(file: NodeFile, newPath: string): Promise<void> {
const targetPath = this.resolvePath(newPath);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.rename(this.resolvePath(file.path), targetPath);
file.path = newPath as FilePath;
}
async delete(file: NodeFile | NodeFolder, force = false): Promise<void> {
const fullPath = this.resolvePath(file.path);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await fs.rm(fullPath, { recursive: true, force });
} else {
await fs.unlink(fullPath);
}
}
async trash(file: NodeFile | NodeFolder, force = false): Promise<void> {
// In CLI, trash is the same as delete (no recycle bin)
await this.delete(file, force);
}
trigger(name: string, ...data: unknown[]): void {
// No-op in CLI version (no event system)
return undefined;
}
}

View File

@@ -0,0 +1,67 @@
import { fsPromises, os, path } from "@vrtmrz/livesync-commonlib/node";
import { describe, expect, it, vi } from "vitest";
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
import { NodeFileSystemAdapter } from "./NodeFileSystemAdapter";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
describe("NodeVaultAdapter.rename", () => {
it("changes the directory entry case without changing the content", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-case-rename-"));
try {
await fsPromises.writeFile(path.join(directory, "Calculus.md"), "content", "utf8");
const adapter = new NodeVaultAdapter(directory);
const file = {
path: "Calculus.md" as FilePath,
stat: { ctime: 1, mtime: 2, size: 7, type: "file" as const },
};
await adapter.rename(file, "calculus.md");
expect(await fsPromises.readdir(directory)).toEqual(["calculus.md"]);
expect(await fsPromises.readFile(path.join(directory, "calculus.md"), "utf8")).toBe("content");
expect(file.path).toBe("calculus.md");
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
});
describe("NodeFileSystemAdapter path case", () => {
it("finds the stored case and refreshes the cache after a case-only rename", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-case-cache-"));
try {
await fsPromises.writeFile(path.join(directory, "Calculus.md"), "content", "utf8");
const adapter = new NodeFileSystemAdapter(directory);
await expect(adapter.getAbstractFileByPath("calculus.md")).resolves.toBeNull();
const existingFile = await adapter.getAbstractFileByPathInsensitive("calculus.md");
expect(existingFile?.path).toBe("Calculus.md");
if (!existingFile) throw new Error("Expected to find Calculus.md case-insensitively");
const renamedFile = await adapter.renameFile(existingFile, "calculus.md");
expect(renamedFile.path).toBe("calculus.md");
expect((await adapter.getFiles()).map((file) => file.path)).toEqual(["calculus.md"]);
expect(await fsPromises.readdir(directory)).toEqual(["calculus.md"]);
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
it("reports directory scan failures through the injected diagnostic callback", async () => {
const directory = await fsPromises.mkdtemp(path.join(os.tmpdir(), "livesync-scan-diagnostic-"));
const missingDirectory = path.join(directory, "missing");
const reportDiagnostic = vi.fn();
try {
const adapter = new NodeFileSystemAdapter(missingDirectory, reportDiagnostic);
await adapter.scanDirectory();
expect(reportDiagnostic).toHaveBeenCalledWith(
`Error scanning directory ${missingDirectory}:`,
expect.any(Error)
);
} finally {
await fsPromises.rm(directory, { recursive: true, force: true });
}
});
});

Some files were not shown because too many files have changed in this diff Show More