mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-22 11:27:05 +00:00
Compare commits
119
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86ca8817c9 | ||
|
|
6ed7c83109 | ||
|
|
a4194a8978 | ||
|
|
1e9ab39adc | ||
|
|
3973290485 | ||
|
|
510db277cd | ||
|
|
b3bf717947 | ||
|
|
93f0f78494 | ||
|
|
11cb09d49a | ||
|
|
e0160f15f3 | ||
|
|
a3aa79ead9 | ||
|
|
db8dac2db4 | ||
|
|
c933674a0d | ||
|
|
eda78dee36 | ||
|
|
67c8424231 | ||
|
|
1735bc4d66 | ||
|
|
d11a92498d | ||
|
|
950623f5d6 | ||
|
|
bb72b6b317 | ||
|
|
769b7ff4b6 | ||
|
|
1bb50c1580 | ||
|
|
d17330f3c7 | ||
|
|
bd45924649 | ||
|
|
b1ad3e0653 | ||
|
|
6b94f0ce47 | ||
|
|
08b55b7677 | ||
|
|
21d904cfd6 | ||
|
|
00de35e5d4 | ||
|
|
f2976bc89a | ||
|
|
b65deede79 | ||
|
|
9203bdd40e | ||
|
|
fcd30d07be | ||
|
|
cfb75a05db | ||
|
|
5b19f4415d | ||
|
|
047429033f | ||
|
|
9765569bb6 | ||
|
|
c5835a9da6 | ||
|
|
8311060f77 | ||
|
|
13d9624407 | ||
|
|
76560e3bf2 | ||
|
|
1e190d042c | ||
|
|
40215032dd | ||
|
|
fd9a9175dd | ||
|
|
1dfdb72fbd | ||
|
|
23d9fa360d | ||
|
|
070b63c952 | ||
|
|
d37af53858 | ||
|
|
b05309ad9c | ||
|
|
92e01a17e7 | ||
|
|
b3030bba53 | ||
|
|
cf5181bb28 | ||
|
|
002cf57116 | ||
|
|
15abe344bb | ||
|
|
a9e64860d5 | ||
|
|
741551be3a | ||
|
|
5f69d301f0 | ||
|
|
b42df389fb | ||
|
|
6a46966957 | ||
|
|
18bf31a01b | ||
|
|
33250c6961 | ||
|
|
a36fe6c36e | ||
|
|
92a5e6e145 | ||
|
|
be6da86a3a | ||
|
|
8bb2d5038d | ||
|
|
9d25a00c1d | ||
|
|
277337af40 | ||
|
|
da3e2c983f | ||
|
|
185ec2c392 | ||
|
|
b1d7f3b622 | ||
|
|
697ebe8272 | ||
|
|
c27dbbd572 | ||
|
|
83087f159e | ||
|
|
d12d0f8ef7 | ||
|
|
e9b2477bf3 | ||
|
|
9c59cd15d1 | ||
|
|
d672ff04c3 | ||
|
|
cd5f5aa740 | ||
|
|
34d8fe5d31 | ||
|
|
96a69c85bd | ||
|
|
d15df7367f | ||
|
|
de190e2b23 | ||
|
|
36b2ea7096 | ||
|
|
34ae802796 | ||
|
|
19f0b3d145 | ||
|
|
3ba9feaac7 | ||
|
|
42f37753b8 | ||
|
|
ec1fc4a97e | ||
|
|
146e25907d | ||
|
|
c97346e262 | ||
|
|
48c398948a | ||
|
|
e32e3f545e | ||
|
|
0aed7f8556 | ||
|
|
50a51bf07e | ||
|
|
1570136703 | ||
|
|
5b6bbc8118 | ||
|
|
54f0262b3f | ||
|
|
7d33613017 | ||
|
|
b700c631ca | ||
|
|
00152523db | ||
|
|
fbfb77440d | ||
|
|
89d4667d95 | ||
|
|
21dd24682b | ||
|
|
5c52d69d66 | ||
|
|
a251648e48 | ||
|
|
57c9726ae5 | ||
|
|
a6977da9e8 | ||
|
|
397a9b69c8 | ||
|
|
ce573f18a6 | ||
|
|
0df1f57f3b | ||
|
|
0518fab62a | ||
|
|
e296708845 | ||
|
|
b6746be73e | ||
|
|
c385bd7ce7 | ||
|
|
17fe6c1518 | ||
|
|
93bbab4253 | ||
|
|
8b7d410327 | ||
|
|
a3a09df3c8 | ||
|
|
4393a49cba | ||
|
|
c4d6b768da |
@@ -8,7 +8,7 @@ assignees: ''
|
||||
---
|
||||
|
||||
Thank you for taking the time to report this issue!
|
||||
Before filling in this form, please read: [How to report an issue](../docs/to_issue_reporting.md).
|
||||
Before filling in this form, please read [How to report an issue](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/to_issue_reporting.md).
|
||||
|
||||
Issues with sufficient information will be prioritised.
|
||||
|
||||
@@ -49,13 +49,14 @@ To get it: open the command palette → "Show debug info".
|
||||
</details>
|
||||
|
||||
### LiveSync version
|
||||
The hatch report (below) includes version information. If you cannot provide the report, please fill in the version here.
|
||||
The full LiveSync 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 -->
|
||||
- Self-hosted LiveSync version: <!-- 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.
|
||||
Run `Generate full report for opening the issue with debug info` and provide the generated report. It contains detailed information and up to 1,000 recent log lines. Review the complete output, and remove credentials, private remote details, Vault names, file paths, file contents, and other private information before sharing it.
|
||||
|
||||
If the report is too large to paste here, upload the redacted report to [Gist](https://gist.github.com/) and share the link.
|
||||
|
||||
<details>
|
||||
<summary>Report and Logs (primary)</summary>
|
||||
|
||||
@@ -137,8 +137,9 @@ jobs:
|
||||
echo "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action."
|
||||
elif [[ "${PRERELEASE}" == "true" ]]; then
|
||||
echo "Publish the draft initially as a pre-release without replacing the latest stable release."
|
||||
echo "After BRAT validation, merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch."
|
||||
echo "Only after the default branch contains the exact release metadata, remove the pre-release designation and make this exact release the latest stable release."
|
||||
echo "After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before its manifest reaches the default branch."
|
||||
echo "Confirm the release is no longer a pre-release, then merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch."
|
||||
echo "This order prevents Community Directory review from inspecting a stable manifest version while the matching GitHub Release remains a pre-release."
|
||||
echo "Create the stable CLI tag and publish its latest and major-minor image tags through a separate maintainer gate."
|
||||
else
|
||||
echo "Publish the draft as the latest stable release, keep the release pull request in draft, and merge only after BRAT validation succeeds."
|
||||
|
||||
@@ -23,6 +23,7 @@ Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling de
|
||||
- No central data-storage server is required, but a signalling relay is still required for peer discovery.
|
||||
- At least one device containing the required data must be online while another device synchronises.
|
||||
- Follow the [Peer-to-Peer Setup](docs/setup_p2p.md) after reviewing the [P2P communication model](docs/p2p.md).
|
||||
- If you would like to see how P2P setup and connectivity behave on your devices, try the guided connection check in the experimental [WebPeer browser utility](https://vrtmrz.github.io/obsidian-livesync/webpeer/). It uses an empty Vault and does not test note synchronisation. See the [WebPeer documentation](src/apps/webpeer/README.md) for its scope and limitations.
|
||||
|
||||
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.
|
||||
|
||||
@@ -67,6 +68,13 @@ Each workflow establishes ordinary note synchronisation on the first device, gen
|
||||
> CouchDB can also be run on a Raspberry Pi (please be mindful of your server's security).
|
||||
|
||||
|
||||
### Third-party managed CouchDB hosting
|
||||
|
||||
> [!NOTE]
|
||||
> The following is a third-party hosting option proposed by Zenith Hosting. It is not an official Self-hosted LiveSync service, and it is neither endorsed nor recommended by this project.
|
||||
|
||||
If you would rather not set up and maintain a server yourself, Zenith Hosting offers a managed CouchDB server which this plug-in can be configured to connect to: [Zenith Hosting](https://zenith.hosting/host/obsidian-livesync). As with any hosted service, your data will reside on a server operated by a third party, so please consider whether that is acceptable for your vault before using it.
|
||||
|
||||
## Information in the Status Bar
|
||||
|
||||
Synchronisation status is shown in the status bar with the following icons.
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
const id = params.get('id');
|
||||
const total = parseInt(params.get('n') || '0');
|
||||
const index = parseInt(params.get('i') || '-1');
|
||||
const data = params.get('d');
|
||||
// Keep the chunk percent-encoded so URI delimiters remain part of the settings payload.
|
||||
const data = hash.match(/(?:^|&)d=([^&]*)/)?.[1];
|
||||
|
||||
const app = document.getElementById('app');
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ export class ModuleExample extends AbstractObsidianModule {
|
||||
- A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag.
|
||||
- Keep a hyphenated pre-release's release pull request in draft and unmerged after BRAT validation. Reconcile the published version's metadata into its base branch through a reviewed metadata-only commit, then close the release pull request only through a separate maintainer action.
|
||||
- Stage a stable version for BRAT by publishing its exact `x.y.z` tag initially as a GitHub pre-release with `prerelease=true` and `publish_cli=false`. The stable manifest version would otherwise make the CLI workflow advance `latest` and the major-minor image tag before validation.
|
||||
- After a staged stable version passes BRAT validation, merge its exact release commit into the reviewed base branch and integrate it through the reviewed branch chain into the repository's default branch. Promote the GitHub Release only after the default branch contains the exact stable metadata; publish stable CLI tags through a separate maintainer gate.
|
||||
- After a staged stable version passes BRAT validation, remove the GitHub pre-release designation and make the exact release the latest stable release first. Confirm that promotion, then merge its exact release commit into the reviewed base branch and integrate it through the reviewed branch chain into the repository's default branch. This order prevents Community Directory review from inspecting a stable manifest version while the matching GitHub Release remains a pre-release. Publish stable CLI tags through a separate maintainer gate.
|
||||
- If validation fails, leave every published tag unchanged and prepare the next pre-release or patch version.
|
||||
|
||||
## Release Notes
|
||||
@@ -276,7 +276,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
|
||||
- Publish the draft as a GitHub pre-release without replacing the latest stable release. Keep its release pull request in draft and leave its base branch unchanged throughout BRAT validation. Record that state in the pull request.
|
||||
- Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release.
|
||||
- After a hyphenated pre-release passes, keep its release pull request unmerged. Add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`, then close the release pull request only through a separate maintainer action.
|
||||
- After a stable version passes, mark its release pull request ready and merge the exact release commit into the selected base branch with a merge commit. Integrate that exact commit through the reviewed branch chain into the repository's default branch. Only after the default branch contains the matching stable metadata, remove the GitHub pre-release designation and make that exact release the latest stable release. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate.
|
||||
- After a stable version passes, remove the GitHub pre-release designation and make that exact release the latest stable release. Confirm that the release is no longer a pre-release, then mark its release pull request ready, merge the exact release commit into the selected base branch with a merge commit, and integrate that exact commit through the reviewed branch chain into the repository's default branch. Confirm that the default branch contains the matching stable metadata. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate.
|
||||
- If BRAT validation fails, keep the release PR in draft and do not move published tags. Before preparing the next version, add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`. Keep only changes made after that tag under `## Unreleased`. Compare the historical section with `git show <tag>:updates.md`; do not merge the failed release PR or describe it as validated. The next release PR can then rotate only the correction notes while preserving the immutable release history.
|
||||
- Prepare and publish the next patch or pre-release version from that reconciled base. Leave the failed release PR draft until it is deliberately closed as superseded under a separate maintainer action.
|
||||
- A hyphenated version is rejected unless `prerelease=true`. A stable version staged with `prerelease=true` is rejected unless `publish_cli=false`.
|
||||
@@ -307,7 +307,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
|
||||
8. Update the PR state message to describe the published pre-release and state that merging remains on hold.
|
||||
9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario.
|
||||
10. After a hyphenated pre-release passes, keep its release PR unmerged, reconcile its `versions.json` entry and exact release-note section into the selected base branch as metadata only, then close the PR through a separate maintainer action.
|
||||
11. After a stable version passes, mark the release PR ready and merge the exact release commit into the selected base branch. Integrate that commit through the reviewed branch chain into the repository's default branch. Once the default branch contains the matching stable metadata, remove the pre-release designation, make the exact release the latest stable release, and publish stable CLI tags through a separate maintainer gate if selected.
|
||||
11. After a stable version passes, remove the pre-release designation and make the exact release the latest stable release. Confirm that promotion, then mark the release PR ready, merge the exact release commit into the selected base branch, and integrate it through the reviewed branch chain into the repository's default branch. Confirm that the default branch contains the matching stable metadata, then publish stable CLI tags through a separate maintainer gate if selected.
|
||||
12. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
@@ -37,9 +37,9 @@ The Obsidian host injects the screen wake-lock manager from the `octagonal-wheel
|
||||
|
||||
Finite replication enters both counts only after readiness checks have succeeded and leaves them after `openReplication(..., continuous: false, ...)` settles. A successful completion has reached the latest sequence in that operation's scope and is therefore an authoritative quiescence boundary for chunk retrieval. A failed operation does not prove latest state, but can no longer deliver documents from that attempt. Failure handling runs afterwards so a mismatch or recovery dialogue does not retain the activity. This includes the direct start-up synchronisation path as well as manual, event-driven, and periodic calls through `ReplicationService`. The unbounded continuous channel does not enter either boundary, but its finite initial pull-only catch-up does; the one-shot parameter fallback chain remains inside that boundary.
|
||||
|
||||
Delivery into the local database and application to the Obsidian Vault are deliberately separate lifetimes. A mobile device can have only a short opportunity to obtain remote data, while applying a large downloaded batch to the Vault is durable, offline-capable work which can continue or resume later. The replication-result queue and its recovery snapshot therefore remain outside `boundedRemoteActivityCount`. Finite remote activity ends when the transfer operation settles, even when the `📥` queue still contains documents awaiting Vault application.
|
||||
Delivery into the local database and application to the Obsidian Vault are deliberately separate lifetimes. Finite remote activity ends when the transfer operation settles, even when the `📥` queue still contains documents awaiting Vault application. The Obsidian host tracks that queue through a separate `boundedLocalApplicationActivityCount`, so local Vault writes do not increment either remote-activity count or keep the `📲` indicator visible.
|
||||
|
||||
This separation also keeps the activity indicators truthful: `📲` describes a finite remote operation and must not remain active solely because local Vault writes are pending. The existing replication-result count continues to describe that local queue. If a future feature offers screen-awake protection while applying downloaded documents, it must use a separately typed local-application activity or power-policy boundary, preserve the current behaviour by default, and avoid incrementing either remote-activity count.
|
||||
Applying downloaded document changes enters one local-application boundary from the first queued document until the queue and its in-progress set are both empty. The final empty recovery snapshot is attempted before the boundary settles; snapshot failure is logged and still releases the boundary. Additional batches share the existing boundary. Suspending replication-result processing releases it, and resuming reacquires it while work remains. The same host activity runner supplies best-effort Wake Lock protection, while the visibility lifecycle waits for both remote and local bounded activity to finish.
|
||||
|
||||
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.
|
||||
|
||||
@@ -76,6 +76,8 @@ P2P does not yet contribute to the physical-request count because it does not ha
|
||||
|
||||
The platform activity runner remains injected. Common library and headless consumers can omit it while retaining the same bounded activity count and operation semantics.
|
||||
|
||||
The Obsidian-specific `ObsidianReplicatorService` owns `boundedLocalApplicationActivityCount` and reuses the injected activity runner. It is deliberately absent from the common service contract: CLI and Webapp processing continue directly, while the Obsidian host uses the count for Wake Lock and visibility-lifecycle policy without changing remote-operation reporting.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not count continuous replication as a bounded activity.
|
||||
@@ -85,11 +87,11 @@ The platform activity runner remains injected. Common library and headless consu
|
||||
- Do not guarantee protection against operating-system suspension, closing a laptop lid, forced termination, network loss, or a user-initiated sleep action.
|
||||
- Do not add a lifecycle timeout which would abort an unusually slow rebuild. A genuinely stalled operation may postpone LiveSync's visibility suspension until it settles, but the platform may still suspend or terminate background work.
|
||||
- Do not broaden `keepReplicationActiveInBackground`; it remains an opt-in desktop policy for continuous and periodic operation after finite work has ended.
|
||||
- Do not include offline scans, unrelated local storage reflection, or the durable replication-result queue in this boundary. They are offline-capable and require a separate decision if activity reporting or power policy is added later.
|
||||
- Do not include offline scans, unrelated local storage reflection, or the durable replication-result queue in either remote-activity count. The queue uses its separate local-application boundary.
|
||||
|
||||
## Verification
|
||||
|
||||
Before changing the transfer/application separation, add a deterministic regression scenario which leaves downloaded documents queued after finite transfer settles, verifies that remote activity has ended, persists the queued state, and resumes Vault application after suspension or restart. An optional local-application Wake Lock feature requires its own enabled and disabled cases; existing remote-operation E2E is not evidence for that separate policy.
|
||||
Before changing the transfer/application separation, keep deterministic coverage which leaves downloaded documents queued after finite transfer settles, verifies that remote activity has ended, and retains the separate local-application boundary until Vault application and the final recovery snapshot settle.
|
||||
|
||||
Unit tests cover:
|
||||
|
||||
@@ -106,6 +108,8 @@ Unit tests cover:
|
||||
- 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;
|
||||
- replicated document application sharing one local boundary, settling after the final recovery snapshot, and releasing around processing suspension;
|
||||
- local application activity leaving both remote-activity counts unchanged;
|
||||
- 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;
|
||||
@@ -125,5 +129,5 @@ The exact Fancy Kit screen wake-lock behaviour is covered by its package and Har
|
||||
- One finite activity definition drives Wake Lock, lifecycle protection, and status UI without coupling common library code to Obsidian or browser globals.
|
||||
- Callers can observe accurate logical activity even in CLI and Webapp hosts which do not inject a Wake Lock implementation.
|
||||
- Rebuild operations now retain Wake Lock and lifecycle protection across their longest interruption-sensitive phases without retaining them for Rebuilder-owned pre-operation or completion dialogues. Post-reset P2P discovery and selection remain protected as an intentional part of completing the rebuild.
|
||||
- Downloaded documents may remain in the durable Vault-application queue after remote activity has ended, allowing transfer and offline application to follow different mobile lifetimes without presenting local writes as communication.
|
||||
- Downloaded documents may remain in the durable Vault-application queue after remote activity has ended, while a separate local-application boundary retains best-effort Wake Lock and lifecycle protection without presenting local writes as communication.
|
||||
- Users can now distinguish the lifetime of a finite remote operation from approximate request activity within it.
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Architectural Decision Record: Browser-assisted P2P Connection Check
|
||||
|
||||
## Status
|
||||
|
||||
Accepted — WebPeer provides a disposable, browser-observed connection check with a clean one-device path and an optional same-room second-device attempt.
|
||||
|
||||
## Context
|
||||
|
||||
P2P availability depends on the browser, device, signalling relay, NAT, firewall, and current network. A Setup URI can make a trial easier, but a connection observed between a browser and one device does not prove that two other devices can connect to each other, and a WebRTC connection does not prove that LiveSync documents can be synchronised correctly.
|
||||
|
||||
Showing this experiment inside the ordinary LiveSync interface would mix a temporary connectivity check with the state of a real Vault. It would also make a browser-owned diagnostic result appear to be a plug-in-owned synchronisation result.
|
||||
|
||||
The Commonlib diagnostic counters are cumulative within one JavaScript realm. A negotiation may create more than one `RTCPeerConnection`, a failed attempt may later be followed by a successful retry, and closing a successful connection is not a failure. The counters therefore cannot be interpreted as peer counts or as mutually exclusive outcomes.
|
||||
|
||||
## Decision
|
||||
|
||||
WebPeer provides a separate page labelled 'P2P connection check'. It acts as a temporary reference peer and creates a fresh, random P2P configuration locally in the browser. The page displays an encrypted Setup URI, a QR code containing that URI, and the separate Setup URI passphrase.
|
||||
|
||||
The initial implementation is part of the WebPeer production build and is served over HTTPS or from `localhost`. A downloaded copy of `check.html` alone is not a supported standalone application because the build uses separate module assets and origin-scoped browser storage.
|
||||
|
||||
Each check has these boundaries:
|
||||
|
||||
| Participant | Responsibility |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Browser page | Joins the generated room as the diagnostic reference peer and displays its own diagnostic counters. |
|
||||
| Desktop or mobile LiveSync | Opens the generated Setup URI in a dedicated empty Vault and attempts the ordinary P2P connection. |
|
||||
| User | Uses an empty Vault for every device and chooses either a fresh room or the explicitly less isolated same-room second-device attempt. |
|
||||
|
||||
The generated device configuration:
|
||||
|
||||
- uses a dedicated P2P application identifier so that test rooms do not overlap ordinary LiveSync rooms;
|
||||
- uses random room, P2P, Vault-encryption, and Setup URI secrets;
|
||||
- enables P2P auto-start so that a dedicated empty test Vault attempts the connection after setup;
|
||||
- does not enable automatic broadcasting, peer acceptance, watching, or replication;
|
||||
- omits device-local names from the Setup URI; and
|
||||
- enables diagnostic WebRTC wrapping only on the browser reference peer.
|
||||
|
||||
The browser joins the room only after the user explicitly starts monitoring. Preparing the Setup URI and QR code does not contact the signalling relay. The page must remain open while monitoring.
|
||||
|
||||
The primary path checks one target device. The user selects either desktop or mobile before generating the configuration. Starting a fresh check reloads the page, creates a new room, and resets the page-scoped diagnostic baseline; this remains the clearest way to compare devices.
|
||||
|
||||
After the first connection succeeds, the page can scroll back to the existing QR code without generating new credentials. It also offers an optional same-room second-device attempt. This keeps the browser and first device connected, records the current diagnostic totals and active `RTCPeerConnection` identifiers as a local baseline, and reuses the exact Setup URI for another empty Vault. It reports an additional connection only when both the successful-connection total and the number of simultaneous active connections increase, with an active connection identifier absent from the baseline. A reconnect which replaces the first connection can therefore increase negotiation counters without satisfying the additional-connection result.
|
||||
|
||||
The same-room result remains a convenience rather than an isolated comparison. Its totals are still cumulative, a connection is not a device identity, it depends on the first device remaining connected, and unusual reconnect timing can be harder to interpret than a fresh room. The interface keeps the fresh-check action available and explains this limitation beside the additional result.
|
||||
|
||||
The page displays the following Commonlib diagnostic totals:
|
||||
|
||||
- new connection states;
|
||||
- successful connection states;
|
||||
- failed connection states; and
|
||||
- closed connection states.
|
||||
|
||||
A successful total greater than zero proves that this browser and the target device established at least one WebRTC connection in the generated room. That successful result remains valid if a later closed or failed total increases. A failed total without a successful total describes a failed attempt, not a final verdict, because the transport may retry. If no success has been observed after the documented observation period, the result is inconclusive for P2P on the current network. The page recommends CouchDB for predictable synchronisation when repeated checks on the intended networks do not connect.
|
||||
|
||||
The page does not claim to verify document transfer, conflict handling, sustained connectivity, background execution, or a direct desktop-to-mobile path. An optional final test can use two disposable empty Vaults and a note round trip directly between the user's devices. That direct test is more representative, but its result cannot be observed reliably by the browser page without a separate reporting protocol.
|
||||
|
||||
## Security and privacy
|
||||
|
||||
All credentials are generated with Web Crypto in the browser. The Setup URI is encrypted, its passphrase is shown separately, and neither value is uploaded merely by preparing the check. Their read-only fields disable browser autocomplete and spell-check; copying either value to the system clipboard remains an explicit user action. Starting monitoring uses the configured signalling relay, which is the existing public relay by default, and exposes the usual P2P signalling metadata to that service. WebRTC and optional relay or TURN behaviour retain their existing Commonlib security and privacy properties.
|
||||
|
||||
The production page does not accept a relay override from a deployed origin. A `relay` query parameter is honoured only when the page itself is served from a loopback host, and only for `ws:` or `wss:` URLs. This narrow boundary allows the real-Obsidian E2E scenario to use the local Compose relay without making an arbitrary relay query part of the hosted user workflow.
|
||||
|
||||
The generated configuration is disposable. It must be used only in a dedicated empty Vault, must not replace a production Vault's settings, and must not be retained as the credentials for a later production setup. The browser page stores no ordinary Vault files for this workflow, although browser-local database infrastructure is opened to satisfy the existing WebPeer runtime contract.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
### Display the result in the LiveSync plug-in
|
||||
|
||||
The observation belongs to the browser reference peer and does not prove LiveSync document synchronisation. Keeping the result beside the QR code makes the evidence and its owner explicit.
|
||||
|
||||
### Use browser, desktop, and mobile simultaneously as the primary comparison
|
||||
|
||||
The diagnostic totals are cumulative and do not attribute an `RTCPeerConnection` to a named target device. Separate random rooms provide a clearer result. Same-room reuse is retained only as an explicit convenience, with a counter baseline and another simultaneous active connection required for its result.
|
||||
|
||||
### Treat any failure increment as a final failure
|
||||
|
||||
Negotiation can fail and then retry successfully. A later successful increment takes precedence over earlier failure increments.
|
||||
|
||||
### Treat browser success as proof of device-to-device synchronisation
|
||||
|
||||
Browser-to-device success is a useful preflight check, but it does not exercise the exact desktop-to-mobile network path or a LiveSync note round trip.
|
||||
|
||||
### Put the Setup URI passphrase in the QR code
|
||||
|
||||
Keeping the encrypted URI and passphrase separate preserves the existing Setup URI boundary and avoids turning one captured QR code into the complete configuration secret.
|
||||
|
||||
### Distribute one standalone downloaded HTML file
|
||||
|
||||
WebPeer already depends on a compiled module graph, origin-scoped storage, and secure browser capabilities. A hosted production build reuses those boundaries and can be updated consistently; a single self-contained file would require a separate bundling and trust model.
|
||||
|
||||
## Verification
|
||||
|
||||
Unit tests decode each generated Setup URI and verify its isolation identifier, random credentials, remote-profile selection, automatic-start policy, absence of a device-local name, and browser-only diagnostic setting. Pure outcome tests cover waiting, failed attempts, later success, and closure after success.
|
||||
|
||||
The WebPeer browser smoke test verifies that the production page can prepare a desktop or mobile check, render the QR code, display a separately formatted passphrase, expose zeroed counters without contacting the signalling relay, and return to the unchanged QR code. Pure tests verify that a reconnect which replaces the first connection does not satisfy the same-room additional-connection rule. The focused real-Obsidian E2E test serves the production page from loopback, starts the local Compose Nostr relay, applies the browser-generated Setup URI through visible onboarding in one empty Vault, records the same-room baseline, reuses the unchanged URI in a second empty Vault, and requires the browser to observe another simultaneous active connection before retaining a result screenshot. Existing Commonlib tests remain authoritative for diagnostic WebRTC wrapping, Trystero negotiation, and P2P lifecycle behaviour.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users can assess whether P2P is plausible on each intended device and network before relying on it.
|
||||
- Results remain visibly scoped to connectivity rather than synchronisation correctness.
|
||||
- A fresh test requires a fresh page session and disposable empty Vault; same-room reuse requires another empty Vault and retains cumulative counters.
|
||||
- CouchDB remains the recommended predictable option when representative P2P checks repeatedly fail.
|
||||
- Direct device-to-device note transfer remains a separate, optional verification step.
|
||||
@@ -0,0 +1,351 @@
|
||||
# Architectural Decision Record: Fast Fetch Persistence and Completion Semantics
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Fast Fetch accelerates Fast Setup (Simple Fetch) by reading CouchDB's continuous
|
||||
changes feed directly, decrypting each document, and writing batches to the local
|
||||
database. It then allows LiveSync to reflect the completed database into the
|
||||
Vault.
|
||||
|
||||
This path deliberately bypasses PouchDB's ordinary replication machinery. It
|
||||
must therefore reproduce the correctness guarantees on which the rest of the
|
||||
initialisation workflow relies:
|
||||
|
||||
- a remote document is decrypted and validated before it is written locally;
|
||||
- every result from a batch write is checked;
|
||||
- a checkpoint represents the last contiguous remote sequence which is durable
|
||||
in the local database; and
|
||||
- successful completion requires CouchDB to terminate every finite changes page,
|
||||
every returned row to be durable, and a subsequent normal probe to report no
|
||||
available rows.
|
||||
|
||||
The existing implementation combines line parsing, decryption, persistence, and
|
||||
completion checks within one broad error handler. A decryption or persistence
|
||||
failure can consequently be reported as a malformed JSON line and skipped. A
|
||||
batch write can also resolve while containing individual failed results. In both
|
||||
cases the stream may continue, advance its checkpoint incorrectly, or wait
|
||||
indefinitely for a completion condition which the failed row would have
|
||||
satisfied.
|
||||
|
||||
Fast Fetch also reads the normal changes feed before opening each continuous
|
||||
page. A normal response's `pending` value counts items which remain after the
|
||||
response's `results`, so `pending` alone is not the available workload. With a
|
||||
one-row probe, the page can contain `results.length + pending` rows.
|
||||
|
||||
The documented `limit=0` behaviour cannot be used as a portable zero-payload
|
||||
probe. CouchDB's API documentation says that `limit=0` has the same effect as
|
||||
`limit=1`, while CouchDB 3.5.0 with a two-shard database was observed to return
|
||||
no result rows and leave the complete count in `pending`. Fast Fetch therefore
|
||||
uses an explicit one-row normal probe and includes no document bodies.
|
||||
|
||||
Finite continuous-feed completion differs across supported CouchDB releases.
|
||||
CouchDB 3.5.0 was observed to close a heartbeat-enabled feed with a
|
||||
`{ "last_seq": ... }` line when its finite `limit` is met. CouchDB 3.2 instead
|
||||
continues to wait for database updates after the limit has been consumed. With
|
||||
a heartbeat configured, each wait emits another heartbeat and the page can
|
||||
remain open indefinitely, even after every requested row has arrived.
|
||||
|
||||
On CouchDB 3.2, an explicit `timeout` without a heartbeat has different
|
||||
semantics from a total request deadline. Shard-result waits may emit blank
|
||||
keep-alive lines and continue processing. Once the currently available changes
|
||||
have been exhausted and the feed is waiting for another database update, the
|
||||
timeout stops that wait and returns the feed-level `last_seq`. The timeout can
|
||||
therefore terminate a finite page without limiting the duration of an active
|
||||
page transfer.
|
||||
|
||||
The CouchDB sequence token is opaque and must be handled using CouchDB's
|
||||
sequence semantics, without parsing, ordering, or comparison. On clustered
|
||||
CouchDB, a changes row and the feed-level `last_seq` may encode related
|
||||
positions with different opaque tokens. Separate requests are also not one
|
||||
locked snapshot: their rows may be partially ordered, and replica failover may
|
||||
repeat changes. Fast Fetch must therefore be idempotent and must use each
|
||||
terminal `last_seq` only by returning it to CouchDB as the next `since` value.
|
||||
|
||||
## Decision
|
||||
|
||||
### Remote page sizing and completion
|
||||
|
||||
Fast Fetch obtains an approximate progress target from a normal changes-feed
|
||||
request with `since=now`, `limit=1`, and `include_docs=false`. This token is for
|
||||
progress reporting only. It is not compared with any other token and is not
|
||||
used as a completion checkpoint.
|
||||
|
||||
Before every bounded page, Fast Fetch requests a normal changes feed from the
|
||||
current durable cursor with `limit=1` and `include_docs=false`. The probe and
|
||||
the following continuous page use the same `since`, style, and filter
|
||||
selection. Reading the probe does not consume rows from CouchDB; the continuous
|
||||
request starts again from that same cursor.
|
||||
|
||||
The number currently available is `results.length + pending`. If it is zero,
|
||||
Fast Fetch is caught up and completes without opening another stream. Otherwise,
|
||||
the next continuous request uses the smaller of that count and 10,000 as its
|
||||
finite `limit`.
|
||||
|
||||
Each finite page omits `heartbeat` and sets `timeout=1000`. This lets CouchDB
|
||||
3.2 return the page's terminator one second after it exhausts the currently
|
||||
available changes, rather than keeping the request open for future writes. The
|
||||
client immediately reconnects from that terminator while another normal probe
|
||||
reports available work. This bounded cycle also preserves the intent of the
|
||||
earlier iOS and iPadOS heartbeat workaround: Fast Fetch no longer depends on a
|
||||
silent continuous request eventually closing at CouchDB's default 60-second
|
||||
timeout.
|
||||
|
||||
The probe and page are separate HTTP requests, not a transactional snapshot.
|
||||
New writes, replica selection, or administrative changes may alter the rows
|
||||
between them. A page which returns at least one row and a valid terminator may
|
||||
therefore be shorter than the probe's estimate. Fast Fetch persists that page
|
||||
and probes again. A page which terminates without making progress after a
|
||||
positive probe is a retryable transport failure, avoiding an unbounded busy
|
||||
loop.
|
||||
|
||||
Each continuous request ends with its own `{ "last_seq": ... }` line. Fast
|
||||
Fetch treats that line separately from a changes row, flushes and validates all
|
||||
preceding local writes, and only then persists the opaque `last_seq`. The exact
|
||||
value is replayed as the next request's `since`; it is never parsed, ordered, or
|
||||
compared with a row's `seq`, another request's `last_seq`, or the database's
|
||||
`update_seq`.
|
||||
|
||||
The limit counts outer changes-result rows. A tombstone is one row and consumes
|
||||
one page slot even when no document body is present. With `style=all_docs`,
|
||||
multiple leaf revisions inside one row's `changes` array do not consume
|
||||
additional slots. Changing `include_docs` between the lightweight probe and the
|
||||
document-bearing continuous page changes the payload, not the row selection.
|
||||
|
||||
### Processing and persistence
|
||||
|
||||
Each non-blank line from the changes feed is processed through these ordered
|
||||
stages:
|
||||
|
||||
1. parse and validate the changes-feed row;
|
||||
2. decrypt and validate its document, when a document is present;
|
||||
3. add the document to the pending local batch;
|
||||
4. persist the batch;
|
||||
5. inspect every result returned by the batch write; and
|
||||
6. after the finite page ends, persist its `last_seq` terminator.
|
||||
|
||||
With `new_edits: false`, PouchDB follows CouchDB behaviour and may omit successful
|
||||
results. Fast Fetch therefore inspects every returned result and treats any
|
||||
error result as a failed batch. An empty result is valid when PouchDB accepted
|
||||
the complete batch.
|
||||
|
||||
The checkpoint may advance only to the last contiguous sequence for which all
|
||||
preceding documents are durable. A row which legitimately requires no local
|
||||
write may advance the checkpoint only after any preceding buffered documents
|
||||
have been flushed successfully. A page's `last_seq` is committed under the same
|
||||
rule before that page reports success.
|
||||
|
||||
If a batch is partly written, its checkpoint is not advanced. Retrying the batch
|
||||
with `new_edits: false` is expected to be idempotent, including for documents
|
||||
which were accepted during the first attempt.
|
||||
|
||||
Blank heartbeat lines are ignored. Malformed rows are failures; they are not
|
||||
silently skipped. Logs may describe the stage and sequence involved, but must
|
||||
not include the raw changes-feed line because it may be large or sensitive.
|
||||
|
||||
Each finite continuous changes request and its decoded reader must be terminated
|
||||
on every exit. Releasing a reader lock alone does not cancel the underlying
|
||||
request. Failure and completion paths therefore abort the request and attempt
|
||||
to cancel the reader before the bounded remote-activity scope ends.
|
||||
|
||||
### Failure classification and retry
|
||||
|
||||
The streaming boundary returns a small structured failure with a stage and an
|
||||
explicit retryability decision. The initial stages are:
|
||||
|
||||
```typescript
|
||||
type StreamingFetchFailureStage = "transport" | "authentication" | "protocol" | "decryption" | "storage";
|
||||
```
|
||||
|
||||
This type is an internal behavioural contract, not a user-interface status
|
||||
enumeration. It may carry safe diagnostic context, such as an HTTP status or a
|
||||
sequence token, without carrying document content.
|
||||
|
||||
Only explicitly recognised transient transport failures are retried
|
||||
automatically. Examples include an interrupted connection, HTTP 408, HTTP 429,
|
||||
and selected HTTP 5xx responses. Authentication, protocol, decryption, and local
|
||||
storage failures are terminal by default. A future implementation may recognise
|
||||
a narrower retryable case, but it must do so explicitly rather than retry every
|
||||
exception.
|
||||
|
||||
Each retry resumes from the last durable contiguous checkpoint. Retry exhaustion
|
||||
returns an actionable classified failure to the caller.
|
||||
|
||||
### Initialisation lifecycle
|
||||
|
||||
Fast Fetch success is the only path which may continue with the offline scan,
|
||||
finish the rebuild, resume Vault reflection, clear the Fast Fetch checkpoint,
|
||||
remove the flag file, or forget the remembered initialisation choice.
|
||||
|
||||
The LiveSync initialisation boundary uses an explicit suspension policy.
|
||||
Ordinary Fetch and Rebuild resume Vault reflection when they finish, SCRAM keeps
|
||||
file watching suspended, and Fast Fetch keeps both file watching and replication
|
||||
result parsing suspended only when initialisation fails. Fast Fetch asserts both
|
||||
suspensions before it begins and owns their final state: success clears both,
|
||||
whereas a false result or exception sets both. This final assignment also covers
|
||||
a late failure after rebuild finalisation and the legacy
|
||||
`doNotSuspendOnFetching` path.
|
||||
|
||||
On failure:
|
||||
|
||||
- the local checkpoint and any durably fetched documents are retained for a
|
||||
later retry;
|
||||
- the local database is not marked as resolved;
|
||||
- Vault reflection remains suspended;
|
||||
- the offline scan and rebuild finalisation are not run; and
|
||||
- the flag file and remembered initialisation choice remain available so that
|
||||
restart recovery can offer the same operation again.
|
||||
|
||||
Any bounded remote-activity scope must still be released in a `finally` path, as
|
||||
defined by [Bounded Remote Activity](2026_07_bounded_remote_activity.md). Keeping
|
||||
Vault reflection suspended does not permit a wake lock or similar resource to
|
||||
leak.
|
||||
|
||||
## Ownership
|
||||
|
||||
The responsibilities are divided at three injectable boundaries.
|
||||
|
||||
### Streaming Fetch
|
||||
|
||||
The Commonlib streaming implementation owns HTTP response validation, NDJSON
|
||||
parsing, invocation of the decryption delegate, batch-write result validation,
|
||||
contiguous checkpoint advancement, finite-page completion, and classified
|
||||
failures. It does not know about the Vault, setup dialogues, flag files, or
|
||||
LiveSync settings.
|
||||
|
||||
### Rebuilder
|
||||
|
||||
The Commonlib rebuilder owns the local database lifecycle, checkpoint storage,
|
||||
retry policy, marking a completed database as resolved, optional resumption of
|
||||
reflection, and final checkpoint removal. It does not parse changes-feed rows or
|
||||
interpret user-interface choices.
|
||||
|
||||
### LiveSync Fast Setup
|
||||
|
||||
LiveSync owns the setup choices, suspension of initial Vault reflection,
|
||||
invocation of Fast Fetch, the success-only offline scan and rebuild finalisation,
|
||||
and cleanup of flag files and remembered choices. It does not reinterpret
|
||||
document, encryption, or storage failures as successful setup.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This decision does not:
|
||||
|
||||
- change the Metadata and Chunks formats, encryption scheme, security seed, or
|
||||
path obfuscation;
|
||||
- change ordinary PouchDB replication, Standard Fetch, or offline-scan
|
||||
semantics;
|
||||
- provide a transaction spanning CouchDB and the local database;
|
||||
- skip corrupt or unreadable documents and continue with a partial database;
|
||||
- add an automatic fallback from Fast Fetch to Standard Fetch;
|
||||
- define the detailed failure dialogue or other setup user-interface changes;
|
||||
or
|
||||
- provide a transaction or locked snapshot across the normal probe and the
|
||||
following continuous page.
|
||||
|
||||
An explicit Standard Fetch choice remains available when a user needs the
|
||||
ordinary replication path. Any automatic fallback or richer recovery dialogue
|
||||
requires a separate decision because it changes user-visible setup behaviour.
|
||||
|
||||
## Verification
|
||||
|
||||
The implementation is verified primarily with London School interaction tests,
|
||||
using mocks at each owned boundary to prove collaboration and call order.
|
||||
|
||||
### Streaming Fetch unit tests
|
||||
|
||||
Inject the HTTP stream, decryption delegate, local batch writer, and checkpoint
|
||||
writer. Verify that:
|
||||
|
||||
- the order is decrypt, persist, inspect results, then checkpoint;
|
||||
- parsing, decryption, and batch-result failures prevent checkpoint advancement
|
||||
and completion;
|
||||
- a partly failed batch leaves the checkpoint unchanged and reports a storage
|
||||
failure;
|
||||
- rows without a local write flush earlier buffered documents before advancing;
|
||||
- a returned probe row is counted in addition to `pending`, including when
|
||||
`pending` is zero;
|
||||
- every probe uses `limit=1`, excludes document bodies, and is repeated from the
|
||||
previous page's opaque terminator;
|
||||
- each bounded page omits `heartbeat`, uses `timeout=1000`, and can complete
|
||||
under CouchDB 3.2 after its current rows have been delivered;
|
||||
- deletion and document-less rows consume a page slot;
|
||||
- a row count cannot complete a page without its `last_seq` terminator;
|
||||
- a page terminator cannot advance the checkpoint before its batch is durable;
|
||||
- a final row and `last_seq` with different opaque representations complete
|
||||
normally without a token comparison;
|
||||
- a shorter valid page is persisted and followed by another probe, while a
|
||||
zero-row page after a positive probe fails without looping;
|
||||
- workloads over 10,000 rows resume from each durable finite-page checkpoint;
|
||||
- authentication and malformed-protocol responses are terminal;
|
||||
- recognised transient transport failures are classified as retryable; and
|
||||
- diagnostics do not log the raw changes-feed line.
|
||||
|
||||
### Rebuilder unit tests
|
||||
|
||||
Inject the streaming operation and lifecycle collaborators. Verify that:
|
||||
|
||||
- transient failures retry from the latest durable checkpoint;
|
||||
- terminal failures are attempted once;
|
||||
- success marks the database as resolved, resumes reflection when requested, and
|
||||
clears the checkpoint in that order; and
|
||||
- failure does not mark the database as resolved, resume reflection, or clear
|
||||
the checkpoint.
|
||||
|
||||
### LiveSync orchestration unit tests
|
||||
|
||||
Inject Fast Fetch, the offline scanner, rebuild finalisation, and cleanup
|
||||
collaborators. Verify that failure performs none of the success-only actions and
|
||||
leaves initial Vault reflection suspended. Verify that success retains the
|
||||
existing setup sequence and cleanup.
|
||||
|
||||
### Integration and E2E tests
|
||||
|
||||
Commonlib's CouchDB integration test remains responsible for the real HTTP
|
||||
changes feed, opaque sequence tokens, deletion rows, and local batch
|
||||
persistence. It should use the maintained CI CouchDB release, a two-shard
|
||||
database, and a data set large enough to cross a local batch boundary, and
|
||||
confirm that the final checkpoint can be passed back to CouchDB as `since` with
|
||||
no result rows or pending changes. The test must not compare that token's
|
||||
representation with a separately requested target or changes-row token. The
|
||||
focused regression test covers CouchDB 3.2's page-tail behaviour; compatibility
|
||||
with a real CouchDB 3.2 server can be confirmed manually without expanding the
|
||||
permanent CI matrix.
|
||||
|
||||
LiveSync's real Obsidian Setup URI workflow remains responsible for the actual
|
||||
Fast Fetch selection, E2EE passphrase, Vault reflection, ordinary file round
|
||||
trip, and hidden-file synchronisation. Injected parsing, decryption, and
|
||||
persistence failures remain unit-test responsibilities; repeating them through
|
||||
the real Obsidian E2E does not add coverage for an unchanged framework boundary.
|
||||
This follows [Real Obsidian E2E](2026_06_real_obsidian_e2e.md).
|
||||
|
||||
## Consequences
|
||||
|
||||
- A deterministic document failure which previously appeared to be skipped now
|
||||
fails Fast Fetch. This is an intentional safety improvement because the local
|
||||
database is known to be incomplete.
|
||||
- Partial durable work and its contiguous checkpoint can be reused by a later
|
||||
attempt without exposing the partial database to the Vault.
|
||||
- Retry delays are no longer spent on authentication, corrupt content, protocol,
|
||||
or local persistence failures which cannot repair themselves.
|
||||
- Progress totals remain approximate and may grow when a later probe observes
|
||||
new work, without affecting correctness.
|
||||
- A completed page can spend up to one second waiting for its terminator before
|
||||
Fast Fetch probes and reconnects. Active page transfer is not constrained to
|
||||
one second.
|
||||
- The implementation requires coordinated changes in Commonlib and LiveSync.
|
||||
Commonlib remains the authoritative package for streaming and rebuilder
|
||||
behaviour; LiveSync consumes an immutable Commonlib release and owns its setup
|
||||
orchestration.
|
||||
- Ordinary replication remains unchanged and continues to provide the reference
|
||||
correctness contract for decrypting, persisting, and checkpointing replicated
|
||||
documents.
|
||||
|
||||
## References
|
||||
|
||||
- [Apache CouchDB changes-feed API](https://docs.couchdb.org/en/stable/api/database/changes.html)
|
||||
- [Apache CouchDB 2.0 upgrade notes for opaque update sequences](https://docs.couchdb.org/en/stable/whatsnew/2.0.html#upgrade-notes)
|
||||
- [Apache CouchDB replication protocol](https://docs.couchdb.org/en/stable/replication/protocol.html)
|
||||
@@ -0,0 +1,78 @@
|
||||
# Architectural Decision Record: Fast Fetch Transport Eligibility
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Fast Fetch accelerates Fast Setup (Simple Fetch) by reading bounded pages from
|
||||
CouchDB's continuous changes feed. It consumes each response incrementally,
|
||||
persists documents while the page is still arriving, and cancels the underlying
|
||||
request when the page completes or fails.
|
||||
|
||||
The CouchDB setting `useRequestAPI`, labelled 'Use Internal API', routes ordinary
|
||||
replication through Obsidian's `requestUrl` API to avoid browser CORS
|
||||
restrictions. This API exposes a completed response as text, JSON, or an
|
||||
`ArrayBuffer`; it does not expose the network response progressively or accept
|
||||
the Fetch API's `AbortSignal`. Wrapping its result in a `Response` does not
|
||||
restore those transport properties.
|
||||
|
||||
Custom headers can cause a browser preflight, and an authenticating proxy may
|
||||
reject that preflight before the requested header values are sent. Custom
|
||||
headers do not, however, make Fast Fetch intrinsically incompatible. A server
|
||||
with correctly configured CORS can accept the same headers through the ordinary
|
||||
Fetch API and retain streaming behaviour.
|
||||
|
||||
Ordinary PouchDB replication has a different response contract. Standard Fetch
|
||||
uses finite batches, while LiveSync uses long-poll responses whose change
|
||||
payload is bounded by the replication batch size. Both can process each response
|
||||
after it has completed and do not depend on progressively reading a
|
||||
document-bearing continuous feed.
|
||||
|
||||
## Decision
|
||||
|
||||
Fast Fetch requires a Fetch-compatible transport which exposes the response
|
||||
body progressively and honours request cancellation.
|
||||
|
||||
When `useRequestAPI` is enabled for a CouchDB remote, Fast Fetch falls back to
|
||||
Standard Fetch before entering the Fast Fetch activity or resetting the local
|
||||
database through the Fast Fetch path. The presence of custom headers alone does
|
||||
not disable Fast Fetch. Once Standard Fetch resets the local database, it
|
||||
invalidates any retained Fast Fetch checkpoint for that database.
|
||||
|
||||
Commonlib's Rebuilder owns this eligibility decision because it owns both Fast
|
||||
Fetch and the existing Standard Fetch fallback. The streaming implementation
|
||||
does not receive Obsidian's buffered request adapter, and LiveSync does not add
|
||||
proxy-specific or Cloudflare-specific policy.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Initial retrieval through Standard Fetch may be slower and issue more HTTP
|
||||
requests because PouchDB uses the configured batch size, document retrieval,
|
||||
and checkpoint operations. The decision does not assume that `requestUrl` is
|
||||
faster; its benefit here is compatibility with connections which browser CORS
|
||||
would otherwise reject.
|
||||
- LiveSync remains supported with `useRequestAPI`. Its HTTP adapter uses
|
||||
long-poll responses whose change payload is bounded by the replication batch
|
||||
size, rather than the document-bearing stream required by Fast Fetch.
|
||||
- A user whose server accepts the configured custom headers through correct
|
||||
CORS handling can leave `useRequestAPI` disabled and continue to use Fast
|
||||
Fetch.
|
||||
- The decision can be revisited if Obsidian provides a progressively readable,
|
||||
cancellable internal request API, or if a separately designed buffered
|
||||
transport establishes explicit payload bounds and equivalent cancellation
|
||||
semantics.
|
||||
|
||||
## Verification
|
||||
|
||||
Commonlib unit tests verify that `useRequestAPI` selects only the existing
|
||||
Standard Fetch activity, does not invoke Streaming Fetch, and invalidates any
|
||||
retained Fast Fetch checkpoint after the local database is reset. Existing
|
||||
tests continue to verify that custom headers are passed to Fast Fetch when
|
||||
`useRequestAPI` is disabled.
|
||||
|
||||
## References
|
||||
|
||||
- [Fast Fetch Persistence and Completion Semantics](2026_08_fast_fetch_persistence_and_completion.md)
|
||||
- [Apache CouchDB changes-feed API](https://docs.couchdb.org/en/stable/api/database/changes.html)
|
||||
@@ -141,6 +141,10 @@ This field stores an array of Chunk Document IDs.
|
||||
|
||||
\_id is generated based on the path of the Obsidian note.
|
||||
|
||||
The validation and explicit repair contract for normal-file Metadata whose
|
||||
actual ID does not match the ID derived from its stored path is defined in
|
||||
[Normal-file Metadata Document ID Validation and Repair](design_docs/metadata_document_id_validation_and_repair.md).
|
||||
|
||||
- If the path starts with `_`, it is converted to `/_` for convenience.
|
||||
- If Case Sensitive is disabled, it is converted to lowercase.
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# Normal-file Metadata Document ID Validation and Repair
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Problem and scope
|
||||
|
||||
A normal-file Metadata document is addressed by an ID derived from its recorded
|
||||
Vault-relative path. Historical data can contain a readable Metadata document
|
||||
whose actual local database ID no longer matches that derivation. An ordinary
|
||||
path-based read then looks up a different ID. It may reach a separate,
|
||||
consistently addressed Metadata document, or it may find no document at all;
|
||||
it cannot reach the mismatched document which the Offline Scanner enumerated.
|
||||
|
||||
The mismatch can repeatedly produce failed reflection, and an offline-deletion
|
||||
decision can be made before that failure. The scanner must therefore recognise
|
||||
the mismatch before any file reflection, database deletion, expired-history
|
||||
cleanup, or last-seen update.
|
||||
|
||||
This design covers ordinary Vault files. Hidden File Sync, Customisation Sync,
|
||||
and the obsolete plug-in storage namespace retain their feature-specific
|
||||
processing. A disagreement between the document-ID namespace and recorded-path
|
||||
namespace is reported, but is not repaired by this workflow.
|
||||
|
||||
## Evidence and cause boundary
|
||||
|
||||
The reported data included readable paths which could be enumerated from
|
||||
Metadata but could not be fetched again through the path-derived lookup. The
|
||||
Vault also had a history of case changes in folder names. This is consistent
|
||||
with an ID/path mismatch, but it does not prove whether a historical rename,
|
||||
interrupted migration, or earlier path-setting change created it.
|
||||
|
||||
The repair workflow must not infer that the current path is authoritative merely
|
||||
because it is readable. It is available only when the local evidence is
|
||||
unambiguous and current.
|
||||
|
||||
## Identity invariant
|
||||
|
||||
For normal-file Metadata:
|
||||
|
||||
actualDocumentId === path2id(declaredPath)
|
||||
|
||||
The active path service owns the derivation. In particular,
|
||||
handleFilenameCaseSensitive, usePathObfuscation, and the path-obfuscation
|
||||
passphrase can change the expected ID. The E2EE Security Seed and Chunk settings
|
||||
do not directly participate in this ID.
|
||||
|
||||
Inspection and repair use the current local path service. They do not query the
|
||||
remote or decide whether this device's settings should become authoritative.
|
||||
Commonlib recalculates the expected ID during its pre-mutation inspection, so a
|
||||
local ID-derivation setting change makes an earlier approval stale. An
|
||||
intentional whole-database change to ID-derivation settings requires the
|
||||
established rebuild workflow, not this one-entry repair.
|
||||
|
||||
## Offline Scanner decision
|
||||
|
||||
The Offline Scanner validates each decoded Metadata document while its actual ID
|
||||
is still available. It does this before target-file policy and path-keyed pair
|
||||
construction.
|
||||
|
||||
- Consistent normal-file Metadata continues through the existing scan.
|
||||
- Consistent special-namespace Metadata remains owned by its feature.
|
||||
- An ID/path or namespace mismatch is left unchanged and does not enter pair
|
||||
processing.
|
||||
- If consistently addressed Metadata is selected for the same case-normalised
|
||||
path, that Metadata and its storage file continue through the established
|
||||
path-based scan. A stale enumerated document must not suppress this flow.
|
||||
- If no consistently addressed Metadata is selected for that logical path, its
|
||||
storage entry is also withheld. No storage write, database deletion, or
|
||||
last-seen update is performed for that withheld path.
|
||||
- Expired logical deletion history with an inconsistent identity is left
|
||||
unchanged.
|
||||
|
||||
The ordinary scan still returns its established Boolean execution result. A
|
||||
recognised mismatch is left unchanged and omitted before file-pair processing.
|
||||
It therefore does not add a `FilePairProcessResult`, change the ordinary Boolean
|
||||
scan contract, or change Fast Setup or CLI completion policy. Detailed
|
||||
inspection remains separate.
|
||||
|
||||
## Inspection decision
|
||||
|
||||
`inspectMetadataDocumentIdentities` is read-only and enumerates the local
|
||||
database by actual document ID. This is necessary because inspection through a
|
||||
path-derived lookup cannot discover the mismatched source.
|
||||
|
||||
The existing **Inspect conflicts and file/database differences** interface shows
|
||||
one card for each mismatch. It excludes that card's path from ordinary
|
||||
path-based repair only when no consistently addressed Metadata document can be
|
||||
resolved for the same logical path. A stale entry does not hide the normal
|
||||
inspection of a resolvable entry. Multiple affected files are presented
|
||||
separately; there is no batch repair.
|
||||
|
||||
A one-entry repair is offered only when all of these checks pass:
|
||||
|
||||
- the mismatch is within the normal-file namespace;
|
||||
- the source is the current live revision and has no conflicts;
|
||||
- the recorded path is valid and selected by current synchronisation policy;
|
||||
- one case-normalised path maps to one source under the active filename setting;
|
||||
- only one mismatched source expects the target ID; and
|
||||
- the target ID is absent, or contains an exact structural copy left by an
|
||||
earlier attempt.
|
||||
|
||||
An exact structural copy has the same path, timestamps, size, type, Chunk
|
||||
references, Eden data, and logical-deletion state. Inspection does not fetch
|
||||
Chunk content or query the remote. Missing content remains the responsibility
|
||||
of the existing file and Chunk repair tools.
|
||||
|
||||
## Repair decision
|
||||
|
||||
The user explicitly confirms one actual ID, expected ID, and source revision.
|
||||
Commonlib then:
|
||||
|
||||
1. acquires the existing ordered document locks for the source and target IDs;
|
||||
2. reruns the complete inspection and rejects stale or unsafe input;
|
||||
3. reads the exact approved source revision;
|
||||
4. removes the path from the Offline Scanner's durable last-seen map;
|
||||
5. writes the expected target ID when it is absent;
|
||||
6. reads the target back and verifies the exact structural copy;
|
||||
7. writes a deletion revision for the source against the approved source
|
||||
revision; and
|
||||
8. returns control to LiveSync, which requests an ordinary Vault scan.
|
||||
|
||||
The repair result and the follow-up scan result remain separate. If the scan is
|
||||
suspended, returns false, or raises an error after the source has been removed,
|
||||
LiveSync reports that the identity repair completed and directs the operator to
|
||||
run the ordinary scan separately. It does not describe the completed mutation
|
||||
as a failed or rolled-back repair.
|
||||
|
||||
The target is always verified before the source is removed. If target creation
|
||||
fails, the source remains. If source removal fails, the exact target remains and
|
||||
the same one-entry action can finish the operation after a new inspection. This
|
||||
retry property is an implementation safety guarantee, not a separate public
|
||||
repair mode.
|
||||
|
||||
The target receives new CouchDB revision ancestry because ancestry cannot move
|
||||
between document IDs. Users are told to back up the device, pause editing and
|
||||
synchronisation on other devices, allow the change to replicate, and inspect
|
||||
again.
|
||||
|
||||
## Case handling
|
||||
|
||||
The active handleFilenameCaseSensitive setting defines whether path claims are
|
||||
folded before ambiguity is assessed. When case-insensitive handling is active,
|
||||
a consistently addressed entry may continue through the existing path-based
|
||||
flow even if a stale case variant is also reported. The stale entry is not
|
||||
automatically selected or removed unless the one-entry repair preconditions
|
||||
hold. When case-sensitive handling is active, intentional variants remain
|
||||
distinct.
|
||||
|
||||
This workflow does not rename Vault files or folders, infer a preferred folder
|
||||
name from one device, or coordinate a repair across devices. The repair changes
|
||||
one local database and relies on ordinary replication afterwards. Other devices
|
||||
must remain paused until that result has replicated and a new inspection is
|
||||
clean.
|
||||
|
||||
For widespread cross-device naming differences, the operator must choose an
|
||||
authoritative Vault, stop every participating device, correct its storage names
|
||||
outside Obsidian, rebuild the central remote from that Vault, and reset the
|
||||
other devices from the verified remote. During Fast Setup on an empty Vault,
|
||||
there are no storage names to correct: the scanner reflects every consistently
|
||||
addressable Metadata entry and reports only the references which remain
|
||||
unresolved.
|
||||
|
||||
## Non-goals
|
||||
|
||||
This change does not:
|
||||
|
||||
- repair several entries automatically or in a batch;
|
||||
- choose between competing case variants;
|
||||
- rename storage files or folders;
|
||||
- coordinate a distributed repair across devices;
|
||||
- migrate an entire database after path-obfuscation or case-setting changes;
|
||||
- repair special-namespace Metadata;
|
||||
- reconstruct unavailable Chunk content;
|
||||
- query or modify the remote directly; or
|
||||
- change Fast Setup, daemon, or CLI completion policy.
|
||||
|
||||
## Verification
|
||||
|
||||
Focused Commonlib tests cover unresolved-identity exclusion before pair
|
||||
construction, continued processing of a resolvable same-path entry, expired
|
||||
logical-deletion retention, namespace routing, read-only actual-ID inspection,
|
||||
repair preconditions, target-first ordering, stale approval, exact-target retry,
|
||||
source preservation on failure, and last-seen clearing. LiveSync tests cover
|
||||
selective presentation-path withholding, separate confirmation, cancellation,
|
||||
and the ordinary scan request after a completed repair.
|
||||
+2
-2
@@ -39,7 +39,7 @@ Use this path only when the remote database is new, or when this device is inten
|
||||
|
||||

|
||||
|
||||
8. A newly provisioned database may show `Fetch Remote Configuration Failed` because it does not contain a saved preferred configuration yet. If this is a genuinely new setup, select `Skip and proceed`. Otherwise, stop and investigate before continuing.
|
||||
8. A newly provisioned database may show `No Synchronisation Settings Found`. Select `Use this device's settings` for the intended new database. If you expected existing settings, cancel and check the selected remote. Cancellation restarts with Vault and database reflection paused; keep LiveSync disabled until the remote has been checked. If `Could Not Read Synchronisation Settings` appears, retry, or use this device's settings to continue the overwrite. The overwrite still requires a working connection.
|
||||
|
||||

|
||||
|
||||
@@ -134,7 +134,7 @@ Use this path when CouchDB is ready but a Setup URI is unavailable. It configure
|
||||
8. Select `Create or connect to database and continue`. Onboarding requires this connection test to succeed.
|
||||
9. Review `Setup Complete: Preparing to Initialise Server`, then select `Restart and Initialise Server`.
|
||||
10. Read the final overwrite warning. Select `I Understand, Overwrite Server` only when this device is intentionally the source of truth and a current backup exists.
|
||||
11. A newly created database can show `Fetch Remote Configuration Failed` because it does not yet contain a saved preferred configuration. Select `Skip and proceed` only for this known new database.
|
||||
11. A newly created database can show `No Synchronisation Settings Found`. Select `Use this device's settings` for this known new database. If you expected existing settings, cancel and check the selected remote. Cancellation restarts with Vault and database reflection paused; keep LiveSync disabled until the remote has been checked. If `Could Not Read Synchronisation Settings` appears, retry, or use this device's settings to continue the overwrite. The overwrite still requires a working connection.
|
||||
12. Acknowledge `All optional features are disabled`, then keep Obsidian open until the initialisation progress has cleared.
|
||||
|
||||
Create and synchronise an ordinary test note. Once it has reached CouchDB, follow [Create a Setup URI for another device](#create-a-setup-uri-for-another-device), then [Add another device](#add-another-device). This keeps the second device aligned with the remote profile and encryption settings which the first device actually applied.
|
||||
|
||||
+22
-1
@@ -54,6 +54,25 @@ The `Hatch` recovery controls are ordered by escalation. Running **Recreate chun
|
||||
|
||||
An absent Vault file and a logical-deletion winner already agree and do not require a repair card unless another live branch remains. If the scan reports many unrelated files, or the local database itself is incomplete or corrupt, stop the per-file workflow and use [Reset synchronisation on this device](#reset-synchronisation-on-this-device) from a trusted remote. If the central remote must instead be reconstructed from an authoritative Vault, use [Overwrite server data with this device's files](#overwrite-server-data-with-this-devices-files).
|
||||
|
||||
Metadata document-ID mismatches use a separate action in the same Inspector. Follow [Repair a Metadata document ID mismatch](#repair-a-metadata-document-id-mismatch) rather than applying a file revision by path.
|
||||
|
||||
## Repair a Metadata document ID mismatch
|
||||
|
||||
Use this workflow when **Inspect conflicts and file/database differences** reports `Metadata entry requires review and was left unchanged`. The Inspector found local Metadata whose stored document ID no longer represents its recorded path. It leaves the entry unchanged, while any consistently addressed Metadata for the same logical path remains available to ordinary inspection and Vault reflection. This inspection does not query the remote.
|
||||
|
||||
1. Back up this device. If other devices share the database, stop editing and pause synchronisation on them.
|
||||
2. Confirm that the current file-name case and path obfuscation settings are intended for this database. If either setting was deliberately changed for the whole database, stop this workflow and use Rebuild instead.
|
||||
3. Open **Self-hosted LiveSync settings** → **Hatch** → **Inspect conflicts and file/database differences**, then select **Begin inspection**.
|
||||
4. Find the affected Metadata card and review its recorded path, stored document ID, expected document ID, and source revision.
|
||||
5. Continue only when the card says `Repair is available for this entry.` Open its wrench menu and select **Repair this Metadata document ID**. If the action is unavailable, do not force an ID: the entry is ambiguous, conflicted, deleted, outside the normal-file namespace, or otherwise unsafe for one-entry repair.
|
||||
6. Review the warning and select **Repair Metadata ID**. LiveSync rechecks the source revision and expected ID, writes and verifies the target, then removes the obsolete ID.
|
||||
7. Wait for the ordinary Vault scan to complete. If LiveSync reports that the repair completed but the scan did not run, keep synchronisation paused, resolve the reported scan condition, then run the **Scan storage and database again** command.
|
||||
8. Allow this device to upload the repair. Resume the other devices one at a time, then run the inspection again and confirm that the Metadata card no longer appears and the Vault file has the intended content.
|
||||
|
||||
This action changes one local database entry. It does not rename Vault files or folders, repair several entries at once, coordinate other devices, or preserve CouchDB revision ancestry across the two document IDs.
|
||||
|
||||
If many entries reflect folder-name differences across devices, stop every device, choose the authoritative Vault, close Obsidian, correct the actual storage names with operating-system tools, then rebuild the central remote from that Vault and reset the other devices. During Fast Setup on an empty Vault, there are no storage names to correct: allow consistently addressable Metadata to be reflected, then inspect any remaining unresolved references.
|
||||
|
||||
## Reset synchronisation on this device
|
||||
|
||||
Use this when the remote copy is trusted but this device's local LiveSync database is incomplete, corrupt, or no longer aligned with it.
|
||||
@@ -68,6 +87,8 @@ On the next start, LiveSync:
|
||||
4. discards and reconstructs the local LiveSync database from the selected remote; and
|
||||
5. resumes only after the scheduled operation has completed or been cancelled safely.
|
||||
|
||||
Fast Setup retains its fetch flag, last successfully stored remote position, and selected data-processing method when an error while decrypting data, reading the remote response, or writing to the local database stops the reconstruction. File watching and database reflection remain suspended. Review the first specific error in **Show log**, correct its cause, then restart Obsidian to retry from the retained state. Do not remove the fetch flag or resume the Scram switches while you intend to continue the operation. If the same error remains, leave LiveSync suspended and [collect a report](troubleshooting.md#collect-a-report).
|
||||
|
||||
For P2P, a source peer must be online, discovered, and selected in `P2P Rebuild`. Merely opening an empty signalling room does not complete Fetch. Closing the rebuild dialogue without selecting a peer reports failure and does not treat the local database as restored.
|
||||
|
||||
Review the [Fast Setup guide](tips/fast-setup.md) before using this operation on a Vault which contains unsynchronised local work.
|
||||
@@ -105,7 +126,7 @@ Create only the flag required for the chosen operation.
|
||||
| `flag_fetch.md` or `redflag3.md` | Schedule **Reset Synchronisation on This Device** from the selected remote. |
|
||||
| `flag_rebuild.md` or `redflag2.md` | Schedule **Overwrite Server Data with This Device's Files**, or local P2P preparation when no central remote exists. |
|
||||
|
||||
Flag files themselves are excluded from synchronisation. Fetch and rebuild flags are removed by the scheduled workflow after completion or cancellation; `redflag.md` is a manual emergency stop.
|
||||
Flag files themselves are excluded from synchronisation. Fetch and rebuild flags are removed by the scheduled workflow after completion or safe cancellation. A failed Fast Setup retains its fetch flag so that a later start can retry it; `redflag.md` is a manual emergency stop.
|
||||
|
||||
## When the warning continues
|
||||
|
||||
|
||||
+15
-1
@@ -528,7 +528,19 @@ Sync automatically after merging files
|
||||
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.
|
||||
#### Allow sleep during synchronisation
|
||||
|
||||
Setting key: allowSleepDuringSynchronisation
|
||||
|
||||
Allow the operating system to sleep while finite synchronisation operations are in progress. This option applies on every platform and is disabled by default. When it is disabled, finite operations request best-effort screen-awake protection.
|
||||
|
||||
#### Allow sleep during synchronisation on the desktop
|
||||
|
||||
Setting key: allowSleepDuringSynchronisationOnDesktop
|
||||
|
||||
Desktop only. Allow the operating system to sleep during finite synchronisation operations even when the general option is disabled. This option is enabled by default, so periodic or event-driven synchronisation does not repeatedly prevent automatic desktop sleep. Disable it to retain best-effort screen-awake protection on desktop.
|
||||
|
||||
Setup URIs preserve both sleep preferences. Older Setup URIs which do not contain them use the defaults described above. The preferences cover finite remote operations, including one-shot replication, P2P peer discovery and selection, rebuilds, fetches, remote chunk fetching, and applying downloaded documents to the Vault. They do not control whether continuous replication remains active while Obsidian is hidden, and allowing sleep does not force the operating system to suspend the device.
|
||||
|
||||
### 3. Update thinning
|
||||
|
||||
@@ -737,6 +749,8 @@ Compare each Vault file with every current live revision in the local database.
|
||||
|
||||
Select **Begin inspection** to run the inspection. Each reported file and live revision has a wrench menu for read-only comparison, applying an exact database revision to the Vault, recording an exact byte match, preserving the Vault file as a child of a selected branch, retrying chunk retrieval, or explicitly discarding a branch. Destructive actions require confirmation. Follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file) before changing revision history.
|
||||
|
||||
The same inspection also reports local Metadata whose stored document ID does not agree with its recorded path. A stale entry does not suppress ordinary inspection when consistently addressed Metadata can still be resolved for that logical path; otherwise, the unresolved path is excluded from ordinary file-repair actions. When one live, unconflicted entry has an unambiguous target, its wrench menu offers a separately confirmed, one-entry repair. The target is derived from the current local file-name case and path obfuscation settings, then written and verified before the obsolete ID is removed. Ambiguous, conflicted, deleted, excluded, or otherwise unsafe entries remain read-only. This action does not rename Vault files or folders. Follow [Repair a Metadata document ID mismatch](recovery.md#repair-a-metadata-document-id-mismatch) for the complete backup, repair, propagation, and verification procedure. For widespread naming differences across devices, use that guide to choose an authoritative Vault, correct its storage names while Obsidian is closed, rebuild the central remote, and reset the other devices.
|
||||
|
||||
#### Resolve All conflicted files by the newer one
|
||||
|
||||
After confirmation, resolve every conflict by modification time. This logically deletes every version except the newest one. It is a destructive policy choice and cannot recover content which is already unavailable.
|
||||
|
||||
@@ -50,7 +50,7 @@ Use a new bucket prefix, or a prefix whose contents you deliberately intend to r
|
||||
|
||||

|
||||
|
||||
6. A new prefix may show `Fetch Remote Configuration Failed` because it has no saved configuration. Select `Skip and proceed` only for a genuinely new prefix. Otherwise, stop and check the endpoint, credentials, bucket, and prefix.
|
||||
6. A new prefix may show `No Synchronisation Settings Found`. Select `Use this device's settings` for a genuinely new prefix. If you expected existing settings, cancel and check the endpoint, credentials, bucket, and prefix. Cancellation restarts with Vault and database reflection paused; keep LiveSync disabled until the remote has been checked. If `Could Not Read Synchronisation Settings` appears, retry, or use this device's settings to continue the overwrite. The overwrite still requires a working connection.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -63,3 +63,11 @@ 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.
|
||||
|
||||
### If Fast Setup Stops
|
||||
|
||||
Fast Setup records the last successfully stored remote position as it saves documents. A transient connection interruption is retried automatically from that position. The operation reports completion only after the captured remote state has been stored successfully.
|
||||
|
||||
If an error while decrypting data, reading the remote response, or writing to the local database stops the operation, LiveSync does not run the Vault scan or finalise the reconstructed local database. It retains the saved position, the selected data-processing method, and the fetch flag. File watching and database reflection also remain suspended so that a partly reconstructed database cannot be applied to the Vault or combined with new local changes.
|
||||
|
||||
Review the first specific error in **Show log**, correct its cause, then restart Obsidian to retry from the retained state. While you intend to continue the operation, do not remove the fetch flag or manually resume the Scram switches. If the same error remains after a restart, leave LiveSync suspended, preserve the available data, and [collect a report](../troubleshooting.md#collect-a-report).
|
||||
|
||||
+35
-115
@@ -1,145 +1,65 @@
|
||||
# How to report an issue
|
||||
|
||||
Thank you for helping improve Self-hosted LiveSync!
|
||||
Thank you for helping improve Self-hosted LiveSync. A concise report with the right evidence is more useful than trying several recovery operations before reporting the original symptom.
|
||||
|
||||
This document explains how to collect the information needed for an issue report. Issues with sufficient information will be prioritised.
|
||||
Use the [issue report template](https://github.com/vrtmrz/obsidian-livesync/issues/new?template=issue-report.md) for the report itself. Use [Troubleshooting](troubleshooting.md) to diagnose a symptom or choose a recovery action.
|
||||
|
||||
---
|
||||
## Preserve the original symptom
|
||||
|
||||
## Filled example
|
||||
Do not reset a database, rebuild a remote, change transport, or enable P2P merely to see whether the problem disappears. These actions can change the evidence and may make the original cause harder to identify.
|
||||
|
||||
Here is an example of a well-filled report for reference.
|
||||
If the problem may involve data loss, corruption, or unexpected deletion, preserve a copy of every readable affected file and stop editing it on other devices before changing settings.
|
||||
|
||||
### Abstract
|
||||
Include when the problem began, whether it followed an update or restart, how often it occurs, and which device and remote type were involved.
|
||||
|
||||
The synchronisation hung up immediately after connecting.
|
||||
## Required information
|
||||
|
||||
### Expected behaviour
|
||||
### Describe the behaviour
|
||||
|
||||
- Synchronisation ends with the message `Replication completed`
|
||||
- Everything synchronised
|
||||
Complete the issue template with:
|
||||
|
||||
### Actually happened
|
||||
- a one- or two-sentence summary;
|
||||
- the expected and actual behaviour;
|
||||
- repeatable steps, or the frequency and timing when reliable reproduction is not available; and
|
||||
- the role of each relevant device, such as the device where the change originated and the device where the failure appeared.
|
||||
|
||||
- Synchronisation was cancelled with the message `TypeError: Failed to fetch` (visible in the plug-in log around lines 10–12)
|
||||
- No files synchronised
|
||||
### Obsidian debug information
|
||||
|
||||
### Reproducing procedure
|
||||
Open the command palette with `Ctrl`+`P` or `Command`+`P`, run `Show debug info`, and include its output for each relevant device. The device where the problem appeared is required. Information from the other participating devices is particularly useful for synchronisation problems.
|
||||
|
||||
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.
|
||||
### Full LiveSync report
|
||||
|
||||
### Obsidian debug info (Device 1 — Windows desktop)
|
||||
Run `Generate full report for opening the issue with debug info` on the device where the problem appeared. For a synchronisation problem, also collect a report from another participating device when its settings or logs are relevant. The command copies the current LiveSync settings summary and up to 1,000 recent log lines. It collects verbose log lines even when `Verbose Log` is disabled, so you do not need to enable that setting before reproducing the problem.
|
||||
|
||||
```
|
||||
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
|
||||
...
|
||||
```
|
||||
The command automatically redacts known credential fields in the settings summary. It cannot guarantee that private text in log messages or unrecognised configuration fields is removed. Review the complete output before sharing it. Remove or replace:
|
||||
|
||||
### Report from LiveSync
|
||||
- usernames, passwords, passphrases, tokens, keys, and custom headers;
|
||||
- private server URLs, network addresses, database names, bucket names, room identifiers, and relay details;
|
||||
- Vault names, device names, and file paths; and
|
||||
- file contents or other private text which appears in a log message.
|
||||
|
||||
```
|
||||
----remote config----
|
||||
cors:
|
||||
credentials: "true"
|
||||
...
|
||||
---- Plug-in config ---
|
||||
couchDB_URI: self-hosted
|
||||
couchDB_USER: 𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷
|
||||
...
|
||||
```
|
||||
Document and chunk identifiers can also be private metadata, but they may be necessary for diagnosing file reconstruction and chunk availability. Decide deliberately whether to share them. If you remove them, state that the report was redacted and that this may limit the diagnosis.
|
||||
|
||||
### Plug-in log
|
||||
For a large report, you may share a GitHub Gist after reviewing and redacting it. Deleting a Gist later cannot undo information which has already been disclosed.
|
||||
|
||||
```
|
||||
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)
|
||||
```
|
||||
## Additional evidence when relevant
|
||||
|
||||
---
|
||||
### A problem involving one file
|
||||
|
||||
## How to collect each piece of information
|
||||
Run `Copy database information for the active file`, or use **Hatch** → **Copy database information for a file** to select another file.
|
||||
|
||||
### Obsidian debug info
|
||||
This report describes only the local database on that device. It includes the Vault-relative path, document and chunk identifiers, local revisions, conflicts, and local chunk availability. It does not query the remote or include file contents. Review paths and identifiers as private metadata before sharing them.
|
||||
|
||||
Open the command palette (`Ctrl/Cmd + P`) and run **"Show debug info"**. Copy the output and paste it into the issue.
|
||||
### A problem which crosses a restart
|
||||
|
||||
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.
|
||||
Use `Write logs into the file` under **Hatch** only when the in-memory report cannot cover the restart. Persistent logging affects performance and can record private information. Disable it after reproducing the problem, review the log before sharing it, and remove the log file when it is no longer needed.
|
||||
|
||||
### Report from LiveSync (hatch report)
|
||||
### A connection, authentication, or CORS problem
|
||||
|
||||
1. Open LiveSync settings.
|
||||
2. Go to the **Hatch** pane.
|
||||
3. Press the **Make report** button.
|
||||
Include network evidence only when the ordinary LiveSync log cannot show the rejected response. Follow [Inspect a network failure](troubleshooting.md#inspect-a-network-failure), and remove request paths, remote addresses, authority and authorisation values, cookies, credentials, payload identifiers, and response secrets before sharing screenshots or copied data.
|
||||
|
||||
The report will be copied to your clipboard. It contains your LiveSync configuration and the remote server configuration, with credentials automatically redacted.
|
||||
## Sharing the report
|
||||
|
||||
**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.
|
||||
Paste reports into the matching collapsible sections in the issue template, or provide a link to an already-redacted Gist. A separate plug-in log is normally unnecessary because the full LiveSync report already contains the recent verbose log history.
|
||||
|
||||
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.
|
||||
If a maintainer asks for a more specialised diagnostic, collect only that additional evidence and review it again before publishing it.
|
||||
|
||||
@@ -66,6 +66,14 @@ The repair card uses compact diagnostic rows which remain readable in a narrow m
|
||||
|
||||
`Recreate chunks for current Vault files` uses current Vault content. It cannot recreate unique bytes which exist only in an unreadable historical or conflict revision.
|
||||
|
||||
## A Metadata entry requires review
|
||||
|
||||
When **Inspect conflicts and file/database differences** reports `Metadata entry requires review and was left unchanged`, the local database contains Metadata whose stored document ID does not agree with the ID derived from its recorded path. LiveSync withholds that entry from ordinary file reflection and deletion rather than guessing which identity is intended. The inspection is local and does not query the remote.
|
||||
|
||||
Do not change file-name case handling or path obfuscation merely to make the displayed IDs agree. Follow [Repair a Metadata document ID mismatch](recovery.md#repair-a-metadata-document-id-mismatch) when the card offers **Repair this Metadata document ID**. If no repair action is offered, the entry is ambiguous, conflicted, deleted, outside the normal-file namespace, or otherwise unsafe for one-entry repair. Preserve the evidence and use the wider recovery guidance instead of forcing a target ID.
|
||||
|
||||
If many entries reflect deliberate folder-name or ID-derivation differences across devices, choose an authoritative Vault and use the established Rebuild workflow. A one-entry repair is not a distributed rename or database migration.
|
||||
|
||||
## A configuration mismatch dialogue blocks synchronisation
|
||||
|
||||
Some settings must match across devices. LiveSync pauses synchronisation when the local and remote values differ rather than propagating an unexpected change silently.
|
||||
@@ -117,6 +125,8 @@ Enable Obsidian's `Detect all file extensions`, then check LiveSync selectors, i
|
||||
|
||||
## Collect a report
|
||||
|
||||
Follow [How to report an issue](to_issue_reporting.md) for the complete reporting checklist, including Obsidian debug information and the privacy review required before sharing evidence.
|
||||
|
||||
Run `Generate full report for opening the issue with debug info` to copy the current settings summary and recent verbose log lines. Remove credentials, remote URLs, Vault names, file contents, and other private information before sharing it.
|
||||
|
||||
When a problem concerns one file, run **Copy database information for the active file**, or use **Hatch** → **Copy database information for a file** to select another file. The report describes this device's local database view, including the Vault-relative path, document and chunk identifiers, local database revisions, conflicts, and local chunk availability. It does not query the remote server or include file contents. Treat paths and identifiers as private metadata before sharing.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import communityConfig from "./eslint.community.config.mjs";
|
||||
import { globalIgnores } from "eslint/config";
|
||||
|
||||
export default [
|
||||
globalIgnores(["**/*.integration.spec.ts", "**/*.integration.test.ts"]),
|
||||
...communityConfig,
|
||||
{
|
||||
files: ["src/**/*.ts"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: "./tsconfig.community-review-node-next.probe.json",
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.13",
|
||||
"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",
|
||||
|
||||
Generated
+56
-56
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.13",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"src/apps/cli",
|
||||
@@ -23,7 +23,7 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.14-canary.0",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
@@ -32,7 +32,7 @@
|
||||
"markdown-it": "^14.2.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"obsidian": "^1.13.1",
|
||||
"octagonal-wheels": "^0.1.51",
|
||||
"octagonal-wheels": "^0.1.53",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
},
|
||||
@@ -1887,9 +1887,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1968,9 +1968,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4775,9 +4775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vrtmrz/livesync-commonlib": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0.tgz",
|
||||
"integrity": "sha512-rdzEubzLStioanE67pps2XSGZ2UGyMIyeEoKsdPztQLLW8wM05PWApOPbJujIydHcDr2hFanbDFe89Lk7Mn4XQ==",
|
||||
"version": "0.1.14-canary.0",
|
||||
"resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.14-canary.0.tgz",
|
||||
"integrity": "sha512-o2RloR1NtK4FIz478dFYBDMCXSFSfMWAw9Po1SIDjcFPyLhSfpWzmOW3mPA9gq+B/DkK0/fv/y/TeF8Js4am3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
@@ -4793,7 +4793,7 @@
|
||||
"idb": "^8.0.3",
|
||||
"markdown-it": "^14.2.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"octagonal-wheels": "^0.1.51",
|
||||
"octagonal-wheels": "^0.1.53",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||
@@ -4896,9 +4896,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@wdio/config/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5264,9 +5264,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5734,9 +5734,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/babel-plugin-module-resolver/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5986,9 +5986,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
@@ -7673,9 +7673,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7770,9 +7770,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7836,9 +7836,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-n/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8013,9 +8013,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8215,9 +8215,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9052,9 +9052,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/globby/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11532,9 +11532,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/octagonal-wheels": {
|
||||
"version": "0.1.51",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.51.tgz",
|
||||
"integrity": "sha512-KTlfqKPjobHJg/t3A539srnFf+VHr1aXkHSmsNDDpiI5UFC7FamZ95dWpJfGE2EI/HULR5hveQDgkazmz8SAcg==",
|
||||
"version": "0.1.53",
|
||||
"resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.53.tgz",
|
||||
"integrity": "sha512-4NJsb96Sk6rJXhrTyjAY5GRIoWMFJsFp56b5ba9fV8/87ys2HCjMo0hior4F8k4ma72pLTJT7i6DvuihHxSsMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"idb": "^8.0.3"
|
||||
@@ -12668,9 +12668,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -15924,11 +15924,11 @@
|
||||
},
|
||||
"src/apps/cli": {
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"version": "1.0.1-cli",
|
||||
"version": "1.0.13-cli",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"octagonal-wheels": "^0.1.51",
|
||||
"octagonal-wheels": "^0.1.53",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
@@ -15949,9 +15949,9 @@
|
||||
},
|
||||
"src/apps/webapp": {
|
||||
"name": "livesync-webapp",
|
||||
"version": "1.0.1-webapp",
|
||||
"version": "1.0.13-webapp",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.51"
|
||||
"octagonal-wheels": "^0.1.53"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
@@ -15961,9 +15961,9 @@
|
||||
}
|
||||
},
|
||||
"src/apps/webpeer": {
|
||||
"version": "1.0.1-webpeer",
|
||||
"version": "1.0.13-webpeer",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.51"
|
||||
"octagonal-wheels": "^0.1.53"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.13",
|
||||
"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",
|
||||
@@ -58,6 +58,7 @@
|
||||
"test:e2e:obsidian:dialog-mounts": "tsx test/e2e-obsidian/scripts/dialog-mounts.ts",
|
||||
"test:e2e:obsidian:conflict-dialog-policy": "tsx test/e2e-obsidian/scripts/conflict-dialog-policy.ts",
|
||||
"test:e2e:obsidian:revision-repair": "tsx test/e2e-obsidian/scripts/revision-repair.ts",
|
||||
"test:e2e:obsidian:document-history-nav": "tsx test/e2e-obsidian/scripts/document-history-nav.ts",
|
||||
"test:e2e:obsidian:settings-ui": "tsx test/e2e-obsidian/scripts/settings-ui.ts",
|
||||
"test:e2e:obsidian:review-harness": "tsx test/e2e-obsidian/scripts/review-harness.ts",
|
||||
"test:e2e:obsidian:p2p-pane": "tsx test/e2e-obsidian/scripts/p2p-pane.ts",
|
||||
@@ -68,6 +69,9 @@
|
||||
"test:e2e:obsidian:minio-upload": "tsx test/e2e-obsidian/scripts/minio-upload.ts",
|
||||
"test:e2e:obsidian:object-storage-setup-uri-workflow": "tsx test/e2e-obsidian/scripts/object-storage-setup-uri-workflow.ts",
|
||||
"test:e2e:obsidian:p2p-setup-uri-workflow": "tsx test/e2e-obsidian/scripts/p2p-setup-uri-workflow.ts",
|
||||
"pretest:e2e:obsidian:p2p-connection-check": "npm run build && npm run build --workspace webpeer",
|
||||
"test:e2e:obsidian:p2p-connection-check": "tsx test/e2e-obsidian/scripts/p2p-connection-check.ts",
|
||||
"test:e2e:obsidian:p2p-connection-check:services": "npm run test:e2e:obsidian:p2p-connection-check -- --manage-p2p",
|
||||
"test:e2e:obsidian:startup-scan": "tsx test/e2e-obsidian/scripts/startup-scan.ts",
|
||||
"test:e2e:obsidian:setup-uri-workflow": "tsx test/e2e-obsidian/scripts/setup-uri-workflow.ts",
|
||||
"test:e2e:obsidian:two-vault-sync": "tsx test/e2e-obsidian/scripts/two-vault-sync.ts",
|
||||
@@ -173,7 +177,7 @@
|
||||
"@smithy/types": "^4.14.3",
|
||||
"@smithy/util-retry": "^4.4.5",
|
||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.0",
|
||||
"@vrtmrz/livesync-commonlib": "0.1.14-canary.0",
|
||||
"@vrtmrz/obsidian-plugin-kit": "0.1.3",
|
||||
"@vrtmrz/ui-interactions": "0.1.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
@@ -182,7 +186,7 @@
|
||||
"markdown-it": "^14.2.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"obsidian": "^1.13.1",
|
||||
"octagonal-wheels": "^0.1.51",
|
||||
"octagonal-wheels": "^0.1.53",
|
||||
"qrcode-generator": "^1.4.4",
|
||||
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"
|
||||
},
|
||||
|
||||
+29
-29
@@ -1,35 +1,35 @@
|
||||
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/context";
|
||||
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";
|
||||
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { __$checkInstanceBinding } from "@vrtmrz/livesync-commonlib/compat/dev/checks.js";
|
||||
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm.js";
|
||||
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess.js";
|
||||
import type { Rebuilder } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseRebuilder.js";
|
||||
import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler.js";
|
||||
import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess.js";
|
||||
import type { LiveSyncLocalDBEnv } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB.js";
|
||||
import type { LiveSyncCouchDBReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator.js";
|
||||
import type { CheckPointInfo } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncTypes.js";
|
||||
import type { LiveSyncJournalReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicatorEnv.js";
|
||||
import type { LiveSyncReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator.js";
|
||||
import { useTargetFilters } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/targetFilter.js";
|
||||
import { useRemoteConfigurationMigration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub.js";
|
||||
import { AbstractModule } from "./modules/AbstractModule.js";
|
||||
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess.js";
|
||||
import { ModuleReplicator } from "./modules/core/ModuleReplicator.js";
|
||||
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB.js";
|
||||
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO.js";
|
||||
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker.js";
|
||||
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver.js";
|
||||
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks.js";
|
||||
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.js";
|
||||
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule.js";
|
||||
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu.js";
|
||||
import { usePrepareDatabaseForUse } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/prepareDatabaseForUse.js";
|
||||
import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type.js";
|
||||
|
||||
export class LiveSyncBaseCore<
|
||||
T extends ServiceContext = ServiceContext,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
|
||||
/** One platform-neutral storage adapter contract case. */
|
||||
export interface StorageAdapterContractCase {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents.js";
|
||||
import { BrowserUiNotifications, createBrowserUi } from "@vrtmrz/browser-ui-kit";
|
||||
|
||||
import { createNativeElement } from "@/apps/browserDom";
|
||||
import { renderMessageMarkdownInto } from "./ui/renderMessageMarkdown";
|
||||
import { UiInteractionsConfirm } from "./UiInteractionsConfirm";
|
||||
import { createNativeElement } from "@/apps/browserDom.js";
|
||||
import { renderMessageMarkdownInto } from "./ui/renderMessageMarkdown.js";
|
||||
import { UiInteractionsConfirm } from "./UiInteractionsConfirm.js";
|
||||
|
||||
/**
|
||||
* Compatibility facade consumed by Commonlib while browser presentation is
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
KeyValueDatabase,
|
||||
KeyValueDatabaseFactory,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase.js";
|
||||
import { deleteDB, openDB, type IDBPDatabase } from "idb";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { createNativeElement } from "@/apps/browserDom";
|
||||
import { createNativeElement } from "@/apps/browserDom.js";
|
||||
import { mount } from "svelte";
|
||||
import MenuView from "./ui/MenuView.svelte";
|
||||
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
|
||||
export class MenuItem {
|
||||
type = "item";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import type { P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { P2PSyncSetting } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
|
||||
import type { P2PReplicatorPaneHost } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneHost";
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
type ComponentHasResult,
|
||||
SvelteDialogManagerBase,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
|
||||
import { createNativeElement } from "@/apps/browserDom";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import type { SvelteDialogManagerDependencies } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
|
||||
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog.js";
|
||||
import { createNativeElement } from "@/apps/browserDom.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import type { SvelteDialogManagerDependencies } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog.js";
|
||||
import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte";
|
||||
import { SvelteDialogSession } from "@/modules/services/SvelteDialogSession";
|
||||
import { SvelteDialogSession } from "@/modules/services/SvelteDialogSession.js";
|
||||
|
||||
export class BrowserModal {
|
||||
contentEl: HTMLElement;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
|
||||
import type { ICommandCompat } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { InjectableAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAPIService";
|
||||
import type { LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/logger.js";
|
||||
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm.js";
|
||||
import type { ICommandCompat } from "@vrtmrz/livesync-commonlib/compat/services/base/IService.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { InjectableAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAPIService.js";
|
||||
import { FetchHttpHandler } from "@smithy/fetch-http-handler";
|
||||
import { _fetch } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { _fetch } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
|
||||
declare const MANIFEST_VERSION: string | undefined;
|
||||
declare const PACKAGE_VERSION: string | undefined;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import type { AppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService";
|
||||
import type { ConfigService } from "@vrtmrz/livesync-commonlib/compat/services/base/ConfigService";
|
||||
import type { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService";
|
||||
import type { ReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/base/ReplicatorService";
|
||||
import type { InjectableAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAPIService";
|
||||
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import type { AppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService.js";
|
||||
import type { ConfigService } from "@vrtmrz/livesync-commonlib/compat/services/base/ConfigService.js";
|
||||
import type { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService.js";
|
||||
import type { ReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/base/ReplicatorService.js";
|
||||
import type { InjectableAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAPIService.js";
|
||||
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService.js";
|
||||
import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte";
|
||||
import { BrowserSvelteDialogManager } from "./BrowserSvelteDialogManager";
|
||||
import { BrowserSvelteDialogManager } from "./BrowserSvelteDialogManager.js";
|
||||
|
||||
export interface LiveSyncBrowserUIServiceDependencies<T extends ServiceContext> {
|
||||
API: InjectableAPIService<T>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
Confirm,
|
||||
ConfirmActionLayout,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm.js";
|
||||
import type { UiInteractions, UiNotifications } from "@vrtmrz/ui-interactions";
|
||||
|
||||
const DEFAULT_LABELS = {
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { KeyValueDatabaseFactory } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
|
||||
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
|
||||
import { ConfigService } from "@vrtmrz/livesync-commonlib/compat/services/base/ConfigService";
|
||||
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService";
|
||||
import { DatabaseService } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService";
|
||||
import { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService";
|
||||
import type { ISettingService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
|
||||
import { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService";
|
||||
import { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService";
|
||||
import { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService";
|
||||
import { InjectableFileProcessingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableFileProcessingService";
|
||||
import { PathServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectablePathService";
|
||||
import { InjectableRemoteService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableRemoteService";
|
||||
import { InjectableReplicationService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicationService";
|
||||
import { InjectableReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicatorService";
|
||||
import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
|
||||
import { InjectableTestService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTestService";
|
||||
import { InjectableTweakValueService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTweakValueService";
|
||||
import { InjectableVaultServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { KeyValueDatabaseFactory } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase.js";
|
||||
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser.js";
|
||||
import { ConfigService } from "@vrtmrz/livesync-commonlib/compat/services/base/ConfigService.js";
|
||||
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService.js";
|
||||
import { DatabaseService } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService.js";
|
||||
import { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService.js";
|
||||
import type { ISettingService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService.js";
|
||||
import { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService.js";
|
||||
import { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService.js";
|
||||
import { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService.js";
|
||||
import { InjectableFileProcessingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableFileProcessingService.js";
|
||||
import { PathServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectablePathService.js";
|
||||
import { InjectableRemoteService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableRemoteService.js";
|
||||
import { InjectableReplicationService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicationService.js";
|
||||
import { InjectableReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicatorService.js";
|
||||
import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub.js";
|
||||
import { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService.js";
|
||||
import { InjectableTestService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTestService.js";
|
||||
import { InjectableTweakValueService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTweakValueService.js";
|
||||
import { InjectableVaultServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService.js";
|
||||
|
||||
import { setLang, translateLiveSyncMessage } from "@/common/translation";
|
||||
import { BrowserConfirm } from "./BrowserConfirm";
|
||||
import { createBrowserKeyValueDatabaseFactory } from "./BrowserKeyValueDatabase";
|
||||
import { setLang, translateLiveSyncMessage } from "@/common/translation.js";
|
||||
import { BrowserConfirm } from "./BrowserConfirm.js";
|
||||
import { createBrowserKeyValueDatabaseFactory } from "./BrowserKeyValueDatabase.js";
|
||||
import {
|
||||
LiveSyncBrowserAPIService,
|
||||
type LiveSyncBrowserAPIServiceOptions,
|
||||
} from "./LiveSyncBrowserAPIService";
|
||||
import { LiveSyncBrowserUIService } from "./LiveSyncBrowserUIService";
|
||||
} from "./LiveSyncBrowserAPIService.js";
|
||||
import { LiveSyncBrowserUIService } from "./LiveSyncBrowserUIService.js";
|
||||
|
||||
export interface LiveSyncBrowserSettingsPersistence {
|
||||
load(): Promise<ObsidianLiveSyncSettings | undefined>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
|
||||
@@ -67,6 +67,10 @@ livesync-cli [database-path] [command] [args...]
|
||||
- `--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`.
|
||||
- `--write-settings`: Write setting migrations and other lasting changes after the command succeeds.
|
||||
- `init-settings` writes its target file. `setup`, `remote-add`, `remote-rm`, `remote-set`, and `remote-activate` write their settings changes without this option.
|
||||
- All remaining commands leave the settings file unchanged by default.
|
||||
- Temporary values used to suspend synchronisation or select a remote for one command are never written.
|
||||
|
||||
### Commands
|
||||
|
||||
@@ -333,6 +337,7 @@ Options:
|
||||
--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
|
||||
--write-settings Write setting changes after a successful command
|
||||
--help, -h Show this help message
|
||||
|
||||
Commands:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes.js";
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for Node.js
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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 { path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput";
|
||||
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import { NodePathAdapter } from "./NodePathAdapter.js";
|
||||
import { NodeTypeGuardAdapter } from "./NodeTypeGuardAdapter.js";
|
||||
import { NodeConversionAdapter } from "./NodeConversionAdapter.js";
|
||||
import { NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter.js";
|
||||
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes.js";
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput.js";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for Node.js
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { NodeFile } from "./NodeTypes.js";
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for Node.js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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";
|
||||
import { fsPromises as fs, os, path, NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
describe("NodeStorageAdapter", () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes";
|
||||
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes.js";
|
||||
|
||||
/**
|
||||
* Type guard adapter implementation for Node.js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
|
||||
/**
|
||||
* Node.js file representation
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { NodeFile, NodeFolder } from "./NodeTypes.js";
|
||||
import { NodeStorageAdapter } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for Node.js
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fsPromises, os, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { fsPromises, os, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { NodeFileSystemAdapter } from "./NodeFileSystemAdapter";
|
||||
import { NodeVaultAdapter } from "./NodeVaultAdapter";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
|
||||
/** Report a CLI-owned diagnostic without selecting its final presentation channel. */
|
||||
export type CliDiagnosticReporter = (message: string, detail?: unknown) => void;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings.js";
|
||||
|
||||
export function createDefaultCliSettings(): ObsidianLiveSyncSettings {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings.js";
|
||||
import { createDefaultCliSettings } from "./cliSettingsDefaults.ts";
|
||||
|
||||
describe("createDefaultCliSettings", () => {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { runCommand } from "./runCommand";
|
||||
import type { CLIOptions } from "./types";
|
||||
|
||||
// Mock performFullScan so daemon tests don't require a real CouchDB connection.
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner", () => ({
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner.js", () => ({
|
||||
performFullScan: vi.fn(async () => true),
|
||||
}));
|
||||
|
||||
// Mock UnresolvedErrorManager to avoid event-hub side effects.
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager", () => ({
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager.js", () => ({
|
||||
UnresolvedErrorManager: class UnresolvedErrorManager {
|
||||
showError() {}
|
||||
clearError() {}
|
||||
@@ -17,7 +17,7 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager"
|
||||
},
|
||||
}));
|
||||
|
||||
import * as offlineScanner from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
|
||||
import * as offlineScanner from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner.js";
|
||||
|
||||
function createCoreMock() {
|
||||
const standardIo = {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { P2P_DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||
import { getPeerConnectionStats } from "@vrtmrz/livesync-commonlib/compat/rpc/transports/DiagRTCPeerConnections.utils";
|
||||
import { fsPromises } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import { P2P_DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError.js";
|
||||
import { getPeerConnectionStats } from "@vrtmrz/livesync-commonlib/compat/rpc/transports/DiagRTCPeerConnections.utils.js";
|
||||
import { fsPromises } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
type CLIP2PPeer = {
|
||||
peerId: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
|
||||
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const";
|
||||
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting.js";
|
||||
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const.js";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
MILESTONE_DOCID,
|
||||
@@ -9,23 +9,23 @@ import {
|
||||
REMOTE_MINIO,
|
||||
type EntryMilestoneInfo,
|
||||
type EntryDoc,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString.js";
|
||||
import {
|
||||
activateRemoteConfiguration,
|
||||
createRemoteConfigurationId,
|
||||
} from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
|
||||
import type { CLICommandContext, CLIOptions } from "./types";
|
||||
import { toArrayBuffer, toDatabaseRelativePath } from "./utils";
|
||||
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
|
||||
import { performFullScan } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
|
||||
import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import type { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
|
||||
import { writeStderrLine, writeStdoutLine } from "@/apps/cli/cliOutput";
|
||||
} from "@vrtmrz/livesync-commonlib/remote-configurations.js";
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path.js";
|
||||
import type { CLICommandContext, CLIOptions } from "./types.js";
|
||||
import { toArrayBuffer, toDatabaseRelativePath } from "./utils.js";
|
||||
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p.js";
|
||||
import { performFullScan } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner.js";
|
||||
import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import type { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator.js";
|
||||
import type { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator.js";
|
||||
import { writeStderrLine, writeStdoutLine } from "@/apps/cli/cliOutput.js";
|
||||
|
||||
function redactConnectionString(uri: string): string {
|
||||
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import * as processSetting from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString";
|
||||
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import * as processSetting from "@vrtmrz/livesync-commonlib/compat/API/processSetting.js";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString.js";
|
||||
import { configURIBase } from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const.js";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { runCommand } from "./runCommand";
|
||||
import type { CLIOptions } from "./types";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { NodeServiceContext } from "@/apps/cli/services/NodeServiceContext";
|
||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { NodeServiceContext } from "@/apps/cli/services/NodeServiceContext.js";
|
||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult.js";
|
||||
|
||||
export type CLICommand =
|
||||
| "daemon"
|
||||
@@ -40,6 +40,7 @@ export interface CLIOptions {
|
||||
verbose?: boolean;
|
||||
debug?: boolean;
|
||||
force?: boolean;
|
||||
writeSettings?: boolean;
|
||||
command: CLICommand;
|
||||
commandArgs: string[];
|
||||
interval?: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
export function toArrayBuffer(data: Buffer): ArrayBuffer {
|
||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toDatabaseRelativePath } from "./utils";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
import { RTCPeerConnection } from "werift";
|
||||
import { main } from "./main";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { createNodeStandardIo } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { writeStderrLine } from "./cliOutput";
|
||||
import { main } from "./main.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { createNodeStandardIo } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { writeStderrLine } from "./cliOutput.js";
|
||||
|
||||
if (
|
||||
typeof (compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection === "undefined" &&
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
setLogHandler: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./services/NodeServiceHub", () => ({
|
||||
NodeServiceContext: class {},
|
||||
NodeServiceHub: class {
|
||||
API = {
|
||||
addLog: {
|
||||
setHandler: mocks.setLogHandler,
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
import { main } from "./main";
|
||||
|
||||
function createStandardIoMock() {
|
||||
return {
|
||||
readStdin: vi.fn(async () => ""),
|
||||
prompt: vi.fn(async () => ""),
|
||||
writeStdout: vi.fn(),
|
||||
writeStderr: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("CLI log handler", () => {
|
||||
const originalArgv = process.argv.slice();
|
||||
let databasePath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
databasePath = await mkdtemp(join(tmpdir(), "livesync-cli-log-handler-"));
|
||||
mocks.setLogHandler.mockReset();
|
||||
mocks.setLogHandler.mockImplementation(() => {
|
||||
throw new Error("__LOG_HANDLER_CONFIGURED__");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
process.argv = originalArgv.slice();
|
||||
await rm(databasePath, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("replaces the default Headless API log handler", async () => {
|
||||
process.argv = ["node", "livesync-cli", databasePath, "remote-ls"];
|
||||
|
||||
await expect(main(createStandardIoMock())).rejects.toThrow("__LOG_HANDLER_CONFIGURED__");
|
||||
expect(mocks.setLogHandler).toHaveBeenCalledWith(expect.any(Function), true);
|
||||
});
|
||||
});
|
||||
+135
-48
@@ -1,14 +1,14 @@
|
||||
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
|
||||
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules";
|
||||
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub.js";
|
||||
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage.js";
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import { initialiseServiceModulesCLI } from "./serviceModules/CLIServiceModules.js";
|
||||
import {
|
||||
LOG_LEVEL_VERBOSE,
|
||||
type LOG_LEVEL,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub.js";
|
||||
import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService.js";
|
||||
import {
|
||||
LOG_LEVEL_DEBUG,
|
||||
setGlobalLogFunction,
|
||||
@@ -17,18 +17,28 @@ import {
|
||||
LOG_LEVEL_URGENT,
|
||||
LOG_LEVEL_NOTICE,
|
||||
} from "octagonal-wheels/common/logger";
|
||||
import { runCommand } from "./commands/runCommand";
|
||||
import { isCLICommand } from "./commands/types";
|
||||
import type { CLICommand, CLICommandContext, CLIOptions } from "./commands/types";
|
||||
import { getPathFromUXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/typeUtils";
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path";
|
||||
import { IgnoreRules } from "./serviceModules/IgnoreRules";
|
||||
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
|
||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
|
||||
import { createNodeStandardIo, fsPromises as fs, path, fs as fsSync } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { writeStderrLine, writeStdoutLine } from "./cliOutput";
|
||||
import { createDefaultCliSettings } from "./cliSettingsDefaults";
|
||||
import { runCommand } from "./commands/runCommand.js";
|
||||
import { isCLICommand } from "./commands/types.js";
|
||||
import type { CLICommand, CLICommandContext, CLIOptions } from "./commands/types.js";
|
||||
import { getPathFromUXFileInfo } from "@vrtmrz/livesync-commonlib/compat/common/typeUtils.js";
|
||||
import { stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path.js";
|
||||
import { IgnoreRules } from "./serviceModules/IgnoreRules.js";
|
||||
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature.js";
|
||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult.js";
|
||||
import { createNodeStandardIo, fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { writeStderrLine, writeStdoutLine } from "./cliOutput.js";
|
||||
import { createDefaultCliSettings } from "./cliSettingsDefaults.js";
|
||||
import {
|
||||
applyStoredSetting,
|
||||
changedSettingKeys,
|
||||
CLI_RUNTIME_ONLY_SETTING_KEYS,
|
||||
cloneSettings,
|
||||
isSettingsWriteCommand,
|
||||
preserveStoredSetting,
|
||||
reconcileDurableSettings,
|
||||
settingsEqual,
|
||||
} from "./settingsPersistence.js";
|
||||
|
||||
const SETTINGS_FILE = ".livesync/settings.json";
|
||||
ensureGlobalNodeLocalStorage();
|
||||
@@ -92,6 +102,7 @@ Options:
|
||||
--vault <path>, -V <path> (daemon/mirror) Path to the vault directory containing .md files
|
||||
(defaults to database-path; allows separate PouchDB and vault dirs)
|
||||
--interval <N>, -i <N> (daemon only) Poll CouchDB every N seconds instead of using the _changes feed
|
||||
--write-settings Write setting changes after a successful command
|
||||
|
||||
Examples:
|
||||
livesync-cli ./my-database Run daemon (LiveSync mode)
|
||||
@@ -141,6 +152,7 @@ export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIO
|
||||
let verbose = false;
|
||||
let debug = false;
|
||||
let force = false;
|
||||
let writeSettings = false;
|
||||
let interval: number | undefined;
|
||||
let command: CLICommand = "daemon";
|
||||
const commandArgs: string[] = [];
|
||||
@@ -197,6 +209,9 @@ export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIO
|
||||
case "-f":
|
||||
force = true;
|
||||
break;
|
||||
case "--write-settings":
|
||||
writeSettings = true;
|
||||
break;
|
||||
default: {
|
||||
if (!databasePath) {
|
||||
if (command === "daemon" && isCLICommand(token)) {
|
||||
@@ -237,6 +252,7 @@ export function parseArgs(standardIo: StandardIo = createNodeStandardIo()): CLIO
|
||||
verbose,
|
||||
debug,
|
||||
force,
|
||||
writeSettings,
|
||||
command,
|
||||
commandArgs,
|
||||
interval,
|
||||
@@ -399,7 +415,7 @@ export async function main(
|
||||
if (!options.verbose) return;
|
||||
}
|
||||
writeStderrLine(standardIo, prefix, message);
|
||||
});
|
||||
}, true);
|
||||
// Prevent replication result from being processed automatically in non-daemon commands.
|
||||
// In daemon mode the default handler must run so changes are applied to the filesystem.
|
||||
if (options.command !== "daemon") {
|
||||
@@ -411,16 +427,26 @@ export async function main(
|
||||
|
||||
// Setup settings handlers
|
||||
const settingService = serviceHubInstance.setting;
|
||||
const originalSettingsText = await fs.readFile(settingsPath, "utf-8").catch(() => undefined);
|
||||
let latestPreparedSettingsText: string | undefined;
|
||||
let preparedSettingsRevision = 0;
|
||||
let commandIsRunning = false;
|
||||
let commandPreparedSettingsTexts: string[] = [];
|
||||
|
||||
(settingService as InjectableSettingService<NodeServiceContext>).saveData.setHandler(
|
||||
async (data: ObsidianLiveSyncSettings) => {
|
||||
try {
|
||||
await fs.writeFile(settingsPath, JSON.stringify(data, null, 2), "utf-8");
|
||||
latestPreparedSettingsText = JSON.stringify(data, null, 2);
|
||||
preparedSettingsRevision++;
|
||||
if (commandIsRunning) {
|
||||
commandPreparedSettingsTexts.push(latestPreparedSettingsText);
|
||||
}
|
||||
if (options.verbose) {
|
||||
writeStderrLine(standardIo, `[Settings] Saved to ${settingsPath}`);
|
||||
writeStderrLine(standardIo, `[Settings] Prepared an update for ${settingsPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
writeStderrLine(standardIo, `[Settings] Failed to save:`, error);
|
||||
writeStderrLine(standardIo, `[Settings] Failed to prepare an update:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -503,24 +529,21 @@ export async function main(
|
||||
process.on("SIGINT", () => void shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
||||
|
||||
// Save the settings file before any lifecycle events can mutate and persist them.
|
||||
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
|
||||
// various code paths persist the clobbered state to disk. We restore on shutdown.
|
||||
const settingsBackup = await fs.readFile(settingsPath, "utf-8").catch(() => null!);
|
||||
|
||||
// Restore settings file on any exit to undo lifecycle mutations.
|
||||
// Write to a temp path first so a crash mid-write doesn't leave a truncated file.
|
||||
process.on("exit", () => {
|
||||
if (settingsBackup) {
|
||||
const tmpPath = settingsPath + ".tmp";
|
||||
try {
|
||||
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
|
||||
fsSync.renameSync(tmpPath, settingsPath);
|
||||
} catch (err) {
|
||||
writeStderrLine(standardIo, "[Settings] Failed to restore settings on exit:", err);
|
||||
const writeSettingsAtomically = async (content: string | undefined): Promise<void> => {
|
||||
if (content === undefined || content === originalSettingsText) return;
|
||||
const temporaryPath = `${settingsPath}.${process.pid}.tmp`;
|
||||
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
||||
try {
|
||||
await fs.writeFile(temporaryPath, content, "utf-8");
|
||||
await fs.rename(temporaryPath, settingsPath);
|
||||
if (options.verbose) {
|
||||
writeStderrLine(standardIo, `[Settings] Saved to ${settingsPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
await fs.unlink(temporaryPath).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Start the core
|
||||
try {
|
||||
@@ -531,9 +554,15 @@ export async function main(
|
||||
writeStderrLine(standardIo, `[Error] Failed to initialize LiveSync`);
|
||||
process.exit(1);
|
||||
}
|
||||
const settingsAfterLoadText = latestPreparedSettingsText
|
||||
? preserveStoredSetting(latestPreparedSettingsText, originalSettingsText, "useIndexedDBAdapter")
|
||||
: originalSettingsText;
|
||||
|
||||
// Capture sync settings before suspendAllSync() clobbers them.
|
||||
// Used by daemon mode to restore the correct sync behaviour after the mirror scan.
|
||||
const settingsBeforeSuspend = core.services.setting.currentSettings();
|
||||
const settingsBeforeSuspend = cloneSettings(core.services.setting.currentSettings());
|
||||
const durableSettingsBeforeSuspend = cloneSettings(settingsBeforeSuspend);
|
||||
applyStoredSetting(durableSettingsBeforeSuspend, settingsAfterLoadText, "useIndexedDBAdapter");
|
||||
const originalSyncSettings = {
|
||||
liveSync: settingsBeforeSuspend.liveSync,
|
||||
syncOnStart: settingsBeforeSuspend.syncOnStart,
|
||||
@@ -544,7 +573,19 @@ export async function main(
|
||||
syncAfterMerge: settingsBeforeSuspend.syncAfterMerge,
|
||||
};
|
||||
await core.services.setting.suspendAllSync();
|
||||
const settingsAfterSuspend = cloneSettings(core.services.setting.currentSettings());
|
||||
await core.services.control.onReady();
|
||||
const settingsBeforeCommand = cloneSettings(core.services.setting.currentSettings());
|
||||
const transientSettingKeys = changedSettingKeys(settingsBeforeSuspend, settingsAfterSuspend);
|
||||
for (const key of CLI_RUNTIME_ONLY_SETTING_KEYS) {
|
||||
transientSettingKeys.add(key);
|
||||
}
|
||||
const durableSettingsBeforeCommand = reconcileDurableSettings({
|
||||
durableBase: durableSettingsBeforeSuspend,
|
||||
runtimeBaseline: settingsAfterSuspend,
|
||||
runtimeCurrent: settingsBeforeCommand,
|
||||
preserveKeys: transientSettingKeys,
|
||||
});
|
||||
|
||||
infoLog(`[Ready] LiveSync is running`);
|
||||
infoLog(`[Ready] Press Ctrl+C to stop`);
|
||||
@@ -568,14 +609,58 @@ export async function main(
|
||||
infoLog("");
|
||||
}
|
||||
|
||||
const result = await commandRunner(options, {
|
||||
databasePath,
|
||||
vaultPath,
|
||||
core,
|
||||
p2pReplicator,
|
||||
settingsPath,
|
||||
originalSyncSettings,
|
||||
});
|
||||
commandPreparedSettingsTexts = [];
|
||||
let result: boolean;
|
||||
try {
|
||||
commandIsRunning = true;
|
||||
result = await commandRunner(options, {
|
||||
databasePath,
|
||||
vaultPath,
|
||||
core,
|
||||
p2pReplicator,
|
||||
settingsPath,
|
||||
originalSyncSettings,
|
||||
});
|
||||
} finally {
|
||||
commandIsRunning = false;
|
||||
}
|
||||
|
||||
let settingsTextToCommit: string | undefined;
|
||||
if (result && options.command === "setup") {
|
||||
settingsTextToCommit = commandPreparedSettingsTexts[0];
|
||||
if (settingsTextToCommit === undefined) {
|
||||
throw new Error("The setup command completed without preparing its settings update.");
|
||||
}
|
||||
} else if (result && (isSettingsWriteCommand(options.command) || options.writeSettings)) {
|
||||
const runtimeSettingsAfterCommand = cloneSettings(core.services.setting.currentSettings());
|
||||
const durableSettingsAfterCommand = reconcileDurableSettings({
|
||||
durableBase: durableSettingsBeforeCommand,
|
||||
runtimeBaseline: settingsBeforeCommand,
|
||||
runtimeCurrent: runtimeSettingsAfterCommand,
|
||||
preserveKeys: transientSettingKeys,
|
||||
command: options.command,
|
||||
});
|
||||
|
||||
if (
|
||||
isSettingsWriteCommand(options.command) ||
|
||||
!settingsEqual(durableSettingsAfterCommand, durableSettingsBeforeSuspend)
|
||||
) {
|
||||
const runtimeSettings = cloneSettings(core.services.setting.currentSettings());
|
||||
const revisionBeforeSave = preparedSettingsRevision;
|
||||
try {
|
||||
await core.services.setting.updateSettings(() => cloneSettings(durableSettingsAfterCommand), true);
|
||||
if (preparedSettingsRevision === revisionBeforeSave || latestPreparedSettingsText === undefined) {
|
||||
throw new Error("The setting service did not prepare the requested settings update.");
|
||||
}
|
||||
settingsTextToCommit = latestPreparedSettingsText;
|
||||
} finally {
|
||||
await core.services.setting.updateSettings(() => runtimeSettings, false);
|
||||
}
|
||||
} else {
|
||||
settingsTextToCommit = settingsAfterLoadText;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
writeStderrLine(standardIo, `[Error] Command '${options.command}' failed`);
|
||||
process.exitCode = 1;
|
||||
@@ -584,10 +669,12 @@ export async function main(
|
||||
}
|
||||
|
||||
if (options.command === "daemon" && result) {
|
||||
await writeSettingsAtomically(settingsTextToCommit);
|
||||
// Keep the process running
|
||||
await new Promise(() => {});
|
||||
} else {
|
||||
await core.services.control.onUnload();
|
||||
await writeSettingsAtomically(settingsTextToCommit);
|
||||
}
|
||||
} catch (error) {
|
||||
writeStderrLine(standardIo, `[Error] Failed to start:`, error);
|
||||
|
||||
@@ -206,4 +206,13 @@ describe("CLI parseArgs", () => {
|
||||
expect(parsed.command).toBe("daemon");
|
||||
expect(parsed.interval).toBe(30);
|
||||
});
|
||||
|
||||
it("parses --write-settings as a global option", () => {
|
||||
process.argv = ["node", "livesync-cli", "./vault", "--write-settings", "ls"];
|
||||
const parsed = parseArgs();
|
||||
|
||||
expect(parsed.command).toBe("ls");
|
||||
expect(parsed.writeSettings).toBe(true);
|
||||
expect(parsed.commandArgs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
|
||||
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters.js";
|
||||
import type {
|
||||
IStorageEventTypeGuardAdapter,
|
||||
IStorageEventPersistenceAdapter,
|
||||
@@ -8,13 +8,13 @@ import type {
|
||||
IStorageEventStatusAdapter,
|
||||
IStorageEventConverterAdapter,
|
||||
IStorageEventWatchHandlers,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
|
||||
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
|
||||
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/managers/adapters.js";
|
||||
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager.js";
|
||||
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes.js";
|
||||
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||
import { fsPromises as fs, path, type Stats } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules.js";
|
||||
import { fsPromises as fs, path, type Stats } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput.js";
|
||||
|
||||
/**
|
||||
* CLI-specific type guard adapter
|
||||
@@ -121,8 +121,10 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
return {
|
||||
path: path.relative(this.basePath, filePath).replace(/\\/g, "/") as FilePath,
|
||||
stat: {
|
||||
ctime: stats?.ctimeMs ?? Date.now(),
|
||||
mtime: stats?.mtimeMs ?? Date.now(),
|
||||
// Floor to integer milliseconds; Linux fs.Stats.*Ms carry sub-millisecond
|
||||
// precision, and timestamps are stored as integer ms everywhere else.
|
||||
ctime: Math.floor(stats?.ctimeMs ?? Date.now()),
|
||||
mtime: Math.floor(stats?.mtimeMs ?? Date.now()),
|
||||
size: stats?.size ?? 0,
|
||||
type: "file",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import type { IStorageEventWatchHandlers } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
|
||||
import type { IStorageEventWatchHandlers } from "@vrtmrz/livesync-commonlib/compat/managers/adapters.js";
|
||||
import type { NodeFile } from "@/apps/cli/adapters/NodeTypes";
|
||||
|
||||
// ── chokidar mock ──────────────────────────────────────────────────────────────
|
||||
@@ -84,6 +84,27 @@ describe("CLIStorageEventManagerAdapter", () => {
|
||||
expect(created.stat?.size).toBe(42);
|
||||
});
|
||||
|
||||
it("floors sub-millisecond stat timestamps so mobile clients do not receive floats", async () => {
|
||||
const basePath = "/vault/base";
|
||||
const adapter = new CLIStorageEventManagerAdapter(basePath, undefined, true);
|
||||
const handlers = makeHandlers();
|
||||
|
||||
await adapter.watch.beginWatch(handlers);
|
||||
|
||||
const addCallback = mockWatcher.on.mock.calls.find(([event]) => event === "add")![1] as (
|
||||
filePath: string,
|
||||
stats: any
|
||||
) => void;
|
||||
|
||||
// Linux fs.Stats carry nanosecond-derived sub-millisecond precision.
|
||||
const floatStats = { ctimeMs: 1778511180024.462, mtimeMs: 1778511180999.913, size: 7 };
|
||||
addCallback(`${basePath}/note.md`, floatStats);
|
||||
|
||||
const created = (handlers.onCreate as ReturnType<typeof vi.fn>).mock.calls[0][0] as NodeFile;
|
||||
expect(created.stat?.ctime).toBe(1778511180024);
|
||||
expect(created.stat?.mtime).toBe(1778511180999);
|
||||
});
|
||||
|
||||
it("close() calls watcher.close()", async () => {
|
||||
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true);
|
||||
const handlers = makeHandlers();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
|
||||
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
|
||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager.js";
|
||||
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter.js";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules.js";
|
||||
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
|
||||
// import type { IMinimumLiveSyncCommands } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
|
||||
// import type { IMinimumLiveSyncCommands } from "@vrtmrz/livesync-commonlib/compat/services/base/IService.js";
|
||||
|
||||
export class StorageEventManagerCLI extends StorageEventManagerBase<CLIStorageEventManagerAdapter> {
|
||||
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "1.0.1-cli",
|
||||
"version": "1.0.13-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"buildRun": "npm run build && npm run cli --",
|
||||
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
|
||||
"check": "tsc -p tsconfig.json",
|
||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
|
||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/settingsPersistence.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
|
||||
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
|
||||
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
|
||||
"test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh",
|
||||
@@ -37,7 +37,7 @@
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"octagonal-wheels": "^0.1.51",
|
||||
"octagonal-wheels": "^0.1.53",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import { ServiceRebuilder } from "@vrtmrz/livesync-commonlib/compat/serviceModules/Rebuilder";
|
||||
import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||
import { StorageAccessManager } from "@vrtmrz/livesync-commonlib/compat/managers/StorageProcessingManager";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { FileAccessCLI } from "./FileAccessCLI";
|
||||
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl";
|
||||
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess";
|
||||
import { StorageEventManagerCLI } from "@/apps/cli/managers/StorageEventManagerCLI";
|
||||
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule";
|
||||
import type { IgnoreRules } from "./IgnoreRules";
|
||||
import { createFileReflectionProvenance } from "@/serviceModules/FileReflectionProvenance";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub.js";
|
||||
import { ServiceRebuilder } from "@vrtmrz/livesync-commonlib/compat/serviceModules/Rebuilder.js";
|
||||
import { ServiceFileHandler } from "@/serviceModules/FileHandler.js";
|
||||
import { StorageAccessManager } from "@vrtmrz/livesync-commonlib/compat/managers/StorageProcessingManager.js";
|
||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { FileAccessCLI } from "./FileAccessCLI.js";
|
||||
import { ServiceFileAccessCLI } from "./ServiceFileAccessImpl.js";
|
||||
import { ServiceDatabaseFileAccessCLI } from "./DatabaseFileAccess.js";
|
||||
import { StorageEventManagerCLI } from "@/apps/cli/managers/StorageEventManagerCLI.js";
|
||||
import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule.js";
|
||||
import type { IgnoreRules } from "./IgnoreRules.js";
|
||||
import { createFileReflectionProvenance } from "@/serviceModules/FileReflectionProvenance.js";
|
||||
|
||||
/**
|
||||
* Initialize service modules for CLI version
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
ServiceDatabaseFileAccessBase,
|
||||
type ServiceDatabaseFileAccessDependencies,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase";
|
||||
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase.js";
|
||||
import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess.js";
|
||||
|
||||
/**
|
||||
* CLI-specific implementation of ServiceDatabaseFileAccess
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase";
|
||||
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
|
||||
import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase.js";
|
||||
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter.js";
|
||||
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Minimatch } from "minimatch";
|
||||
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput";
|
||||
import { fsPromises as fs, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import type { CliDiagnosticReporter } from "@/apps/cli/cliOutput.js";
|
||||
|
||||
/**
|
||||
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const minimatchStats = vi.hoisted(() => ({ constructions: 0 }));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase";
|
||||
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter";
|
||||
import { ServiceFileAccessBase, type StorageAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase.js";
|
||||
import { NodeFileSystemAdapter } from "@/apps/cli/adapters/NodeFileSystemAdapter.js";
|
||||
|
||||
/**
|
||||
* CLI-specific implementation of ServiceFileAccess
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import type { KeyValueDatabase } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase";
|
||||
import type { IKeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { ServiceBase } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
|
||||
import type { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService";
|
||||
import type { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService";
|
||||
import type { IVaultService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService";
|
||||
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/logger.js";
|
||||
import type { KeyValueDatabase } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase.js";
|
||||
import type { IKeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { ServiceBase } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase.js";
|
||||
import type { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService.js";
|
||||
import type { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService.js";
|
||||
import type { IVaultService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService.js";
|
||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils";
|
||||
import { fs as nodeFs, path as nodePath } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils.js";
|
||||
import { fs as nodeFs, path as nodePath } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
|
||||
const NODE_KV_TYPED_KEY = "__nodeKvType";
|
||||
const NODE_KV_VALUES_KEY = "values";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase";
|
||||
import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase.js";
|
||||
import type { NodeKeyValueDBDependencies } from "./NodeKeyValueDBService";
|
||||
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fs as nodeFs, path as nodePath } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { fs as nodeFs, path as nodePath } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
|
||||
type LocalStorageShape = {
|
||||
getItem(key: string): string | null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fs, os, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { fs, os, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearNodeLocalStorage,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { eventHub } from "@/common/events";
|
||||
import { translateLiveSyncMessage } from "@/common/translation";
|
||||
import { ServiceContext, type StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { eventHub } from "@/common/events.js";
|
||||
import { translateLiveSyncMessage } from "@/common/translation.js";
|
||||
import { ServiceContext, type StandardIo } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
|
||||
/** Host capabilities owned by one Self-hosted LiveSync CLI composition. */
|
||||
export class NodeServiceContext extends ServiceContext {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "../../../../test/contracts/serviceContext";
|
||||
import { NodeServiceContext } from "./NodeServiceContext";
|
||||
import { NodeServiceHub } from "./NodeServiceHub";
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context";
|
||||
import type { StandardIo } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
|
||||
const TRANSLATION_KEY = "Replicator.Message.InitialiseFatalError";
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import type { AppLifecycleServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { ConfigServiceBrowserCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/ConfigServiceBrowserCompat";
|
||||
import type { AppLifecycleServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { ConfigServiceBrowserCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/ConfigServiceBrowserCompat.js";
|
||||
import type {
|
||||
ComponentHasResult,
|
||||
SvelteDialogManager,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog";
|
||||
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService";
|
||||
import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService";
|
||||
import { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService";
|
||||
import { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService";
|
||||
import { InjectableFileProcessingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableFileProcessingService";
|
||||
import { PathServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectablePathService";
|
||||
import { InjectableRemoteService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableRemoteService";
|
||||
import { InjectableReplicationService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicationService";
|
||||
import { InjectableReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicatorService";
|
||||
import { InjectableTestService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTestService";
|
||||
import { InjectableTweakValueService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTweakValueService";
|
||||
import { InjectableVaultServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService";
|
||||
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService";
|
||||
import { HeadlessAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/headless/HeadlessAPIService";
|
||||
import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
|
||||
import { NodeSettingService } from "./NodeSettingService";
|
||||
import { DatabaseService } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { path as nodePath } from "@vrtmrz/livesync-commonlib/node";
|
||||
import type { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService";
|
||||
import { PouchDB } from "@/apps/cli/lib/pouchdb-node";
|
||||
import { NodeServiceContext } from "./NodeServiceContext";
|
||||
import { setLang } from "@/common/translation";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog.js";
|
||||
import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService.js";
|
||||
import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub.js";
|
||||
import { InjectableAppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService.js";
|
||||
import { InjectableConflictService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableConflictService.js";
|
||||
import { InjectableDatabaseEventService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableDatabaseEventService.js";
|
||||
import { InjectableFileProcessingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableFileProcessingService.js";
|
||||
import { PathServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectablePathService.js";
|
||||
import { InjectableRemoteService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableRemoteService.js";
|
||||
import { InjectableReplicationService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicationService.js";
|
||||
import { InjectableReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableReplicatorService.js";
|
||||
import { InjectableTestService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTestService.js";
|
||||
import { InjectableTweakValueService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableTweakValueService.js";
|
||||
import { InjectableVaultServiceCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService.js";
|
||||
import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService.js";
|
||||
import { HeadlessAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/headless/HeadlessAPIService.js";
|
||||
import { NodeKeyValueDBService } from "./NodeKeyValueDBService.js";
|
||||
import { NodeSettingService } from "./NodeSettingService.js";
|
||||
import { DatabaseService } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService.js";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { path as nodePath } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import type { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService.js";
|
||||
import { PouchDB } from "@/apps/cli/lib/pouchdb-node.js";
|
||||
import { NodeServiceContext } from "./NodeServiceContext.js";
|
||||
import { setLang } from "@/common/translation.js";
|
||||
|
||||
export { NodeServiceContext } from "./NodeServiceContext";
|
||||
export { NodeServiceContext } from "./NodeServiceContext.js";
|
||||
|
||||
class NodeAppLifecycleService<T extends ServiceContext> extends InjectableAppLifecycleService<T> {
|
||||
constructor(context: T, dependencies: AppLifecycleServiceDependencies) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB } from "@/common/events";
|
||||
import { handlers } from "@vrtmrz/livesync-commonlib/compat/services/lib/HandlerUtils";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { SettingService, type SettingServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService";
|
||||
import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents.js";
|
||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB } from "@/common/events.js";
|
||||
import { handlers } from "@vrtmrz/livesync-commonlib/compat/services/lib/HandlerUtils.js";
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { SettingService, type SettingServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService.js";
|
||||
import {
|
||||
configureNodeLocalStorage,
|
||||
deleteNodeLocalStorageItem,
|
||||
getNodeLocalStorageItem,
|
||||
setNodeLocalStorageItem,
|
||||
} from "./NodeLocalStorage";
|
||||
} from "./NodeLocalStorage.js";
|
||||
|
||||
export class NodeSettingService<T extends ServiceContext> extends SettingService<T> {
|
||||
constructor(context: T, dependencies: SettingServiceDependencies, storagePath: string) {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString.js";
|
||||
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations.js";
|
||||
import type { CLICommand } from "./commands/types.js";
|
||||
|
||||
const SETTINGS_WRITE_COMMANDS = new Set<CLICommand>([
|
||||
"setup",
|
||||
"remote-add",
|
||||
"remote-rm",
|
||||
"remote-set",
|
||||
"remote-activate",
|
||||
]);
|
||||
|
||||
const REMOTE_SETTINGS_WRITE_COMMANDS = new Set<CLICommand>([
|
||||
"remote-add",
|
||||
"remote-rm",
|
||||
"remote-set",
|
||||
"remote-activate",
|
||||
]);
|
||||
|
||||
export const CLI_RUNTIME_ONLY_SETTING_KEYS = new Set<keyof ObsidianLiveSyncSettings>([
|
||||
"disableCheckingConfigMismatch",
|
||||
"suspendFileWatching",
|
||||
"suspendParseReplicationResult",
|
||||
]);
|
||||
|
||||
function cloneJsonValue<T>(value: T): T {
|
||||
if (value === undefined) return value;
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
export function cloneSettings(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
||||
return cloneJsonValue(settings);
|
||||
}
|
||||
|
||||
function settingValuesEqual(left: unknown, right: unknown): boolean {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
export function settingsEqual(left: ObsidianLiveSyncSettings, right: ObsidianLiveSyncSettings): boolean {
|
||||
return settingValuesEqual(left, right);
|
||||
}
|
||||
|
||||
function settingsKeys(...settings: ObsidianLiveSyncSettings[]): Set<keyof ObsidianLiveSyncSettings> {
|
||||
return new Set(settings.flatMap((value) => Object.keys(value) as Array<keyof ObsidianLiveSyncSettings>));
|
||||
}
|
||||
|
||||
function copySetting(
|
||||
target: ObsidianLiveSyncSettings,
|
||||
source: ObsidianLiveSyncSettings,
|
||||
key: keyof ObsidianLiveSyncSettings
|
||||
): void {
|
||||
const targetRecord = target as unknown as Record<string, unknown>;
|
||||
const sourceRecord = source as unknown as Record<string, unknown>;
|
||||
if (Object.prototype.hasOwnProperty.call(sourceRecord, key)) {
|
||||
targetRecord[key] = cloneJsonValue(sourceRecord[key]);
|
||||
} else {
|
||||
delete targetRecord[key];
|
||||
}
|
||||
}
|
||||
|
||||
function remoteSettingKeys(...settings: ObsidianLiveSyncSettings[]): Set<keyof ObsidianLiveSyncSettings> {
|
||||
const keys = new Set<keyof ObsidianLiveSyncSettings>([
|
||||
"remoteConfigurations",
|
||||
"activeConfigurationId",
|
||||
"P2P_ActiveRemoteConfigurationId",
|
||||
"remoteType",
|
||||
]);
|
||||
for (const current of settings) {
|
||||
for (const configuration of Object.values(current.remoteConfigurations ?? {})) {
|
||||
try {
|
||||
const parsed = ConnectionStringParser.parse(configuration.uri);
|
||||
for (const key of Object.keys(parsed.settings) as Array<keyof ObsidianLiveSyncSettings>) {
|
||||
keys.add(key);
|
||||
}
|
||||
} catch {
|
||||
// The setting service reports invalid remote configurations when loading them.
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function changedSettingKeys(
|
||||
before: ObsidianLiveSyncSettings,
|
||||
after: ObsidianLiveSyncSettings
|
||||
): Set<keyof ObsidianLiveSyncSettings> {
|
||||
const changed = new Set<keyof ObsidianLiveSyncSettings>();
|
||||
for (const key of settingsKeys(before, after)) {
|
||||
if (!settingValuesEqual(before[key], after[key])) {
|
||||
changed.add(key);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
export function isSettingsWriteCommand(command: CLICommand): boolean {
|
||||
return SETTINGS_WRITE_COMMANDS.has(command);
|
||||
}
|
||||
|
||||
export function reconcileDurableSettings(options: {
|
||||
durableBase: ObsidianLiveSyncSettings;
|
||||
runtimeBaseline: ObsidianLiveSyncSettings;
|
||||
runtimeCurrent: ObsidianLiveSyncSettings;
|
||||
preserveKeys: ReadonlySet<keyof ObsidianLiveSyncSettings>;
|
||||
command?: CLICommand;
|
||||
}): ObsidianLiveSyncSettings {
|
||||
const durable = cloneSettings(options.durableBase);
|
||||
for (const key of settingsKeys(options.runtimeBaseline, options.runtimeCurrent)) {
|
||||
if (options.preserveKeys.has(key)) continue;
|
||||
if (!settingValuesEqual(options.runtimeBaseline[key], options.runtimeCurrent[key])) {
|
||||
copySetting(durable, options.runtimeCurrent, key);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.command) {
|
||||
if (REMOTE_SETTINGS_WRITE_COMMANDS.has(options.command)) {
|
||||
copySetting(durable, options.runtimeCurrent, "remoteConfigurations");
|
||||
copySetting(durable, options.runtimeCurrent, "activeConfigurationId");
|
||||
copySetting(durable, options.runtimeCurrent, "P2P_ActiveRemoteConfigurationId");
|
||||
|
||||
if (durable.activeConfigurationId) {
|
||||
activateRemoteConfiguration(durable, durable.activeConfigurationId);
|
||||
}
|
||||
} else {
|
||||
// Commands such as remote-status may activate a profile temporarily. Only
|
||||
// the dedicated remote settings commands are allowed to retain that switch.
|
||||
for (const key of remoteSettingKeys(options.durableBase, options.runtimeBaseline, options.runtimeCurrent)) {
|
||||
copySetting(durable, options.durableBase, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return durable;
|
||||
}
|
||||
|
||||
export function preserveStoredSetting(
|
||||
candidateText: string,
|
||||
originalText: string | undefined,
|
||||
key: keyof ObsidianLiveSyncSettings
|
||||
): string {
|
||||
if (originalText === undefined) return candidateText;
|
||||
try {
|
||||
const candidate = JSON.parse(candidateText) as ObsidianLiveSyncSettings;
|
||||
const original = JSON.parse(originalText) as ObsidianLiveSyncSettings;
|
||||
if (Object.prototype.hasOwnProperty.call(original, key)) {
|
||||
copySetting(candidate, original, key);
|
||||
} else {
|
||||
delete (candidate as unknown as Record<string, unknown>)[key];
|
||||
}
|
||||
return JSON.stringify(candidate, null, 2);
|
||||
} catch {
|
||||
return candidateText;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyStoredSetting(
|
||||
target: ObsidianLiveSyncSettings,
|
||||
storedText: string | undefined,
|
||||
key: keyof ObsidianLiveSyncSettings
|
||||
): void {
|
||||
if (storedText === undefined) return;
|
||||
try {
|
||||
const stored = JSON.parse(storedText) as ObsidianLiveSyncSettings;
|
||||
if (Object.prototype.hasOwnProperty.call(stored, key)) {
|
||||
copySetting(target, stored, key);
|
||||
} else {
|
||||
delete (target as unknown as Record<string, unknown>)[key];
|
||||
}
|
||||
} catch {
|
||||
// The setting service owns validation and recovery of malformed files.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations.js";
|
||||
import {
|
||||
applyStoredSetting,
|
||||
changedSettingKeys,
|
||||
cloneSettings,
|
||||
isSettingsWriteCommand,
|
||||
preserveStoredSetting,
|
||||
reconcileDurableSettings,
|
||||
} from "./settingsPersistence";
|
||||
|
||||
function settings(overrides: Partial<ObsidianLiveSyncSettings> = {}): ObsidianLiveSyncSettings {
|
||||
return Object.assign(cloneSettings(DEFAULT_SETTINGS), overrides);
|
||||
}
|
||||
|
||||
describe("CLI settings persistence", () => {
|
||||
it("identifies commands which change the settings file automatically", () => {
|
||||
expect(isSettingsWriteCommand("setup")).toBe(true);
|
||||
expect(isSettingsWriteCommand("remote-add")).toBe(true);
|
||||
expect(isSettingsWriteCommand("remote-rm")).toBe(true);
|
||||
expect(isSettingsWriteCommand("remote-set")).toBe(true);
|
||||
expect(isSettingsWriteCommand("remote-activate")).toBe(true);
|
||||
expect(isSettingsWriteCommand("ls")).toBe(false);
|
||||
expect(isSettingsWriteCommand("remote-status")).toBe(false);
|
||||
});
|
||||
|
||||
it("retains lasting changes without retaining CLI suspension values", () => {
|
||||
const durableBase = settings({
|
||||
liveSync: true,
|
||||
syncOnStart: true,
|
||||
periodicReplication: true,
|
||||
P2P_AutoStart: true,
|
||||
settingVersion: 9,
|
||||
customChunkSize: 40,
|
||||
});
|
||||
const runtimeBaseline = settings({
|
||||
...durableBase,
|
||||
liveSync: false,
|
||||
syncOnStart: false,
|
||||
periodicReplication: false,
|
||||
P2P_AutoStart: false,
|
||||
});
|
||||
const runtimeCurrent = settings({
|
||||
...runtimeBaseline,
|
||||
settingVersion: 10,
|
||||
customChunkSize: 60,
|
||||
});
|
||||
|
||||
const reconciled = reconcileDurableSettings({
|
||||
durableBase,
|
||||
runtimeBaseline,
|
||||
runtimeCurrent,
|
||||
preserveKeys: changedSettingKeys(durableBase, runtimeBaseline),
|
||||
command: "ls",
|
||||
});
|
||||
|
||||
expect(reconciled.liveSync).toBe(true);
|
||||
expect(reconciled.syncOnStart).toBe(true);
|
||||
expect(reconciled.periodicReplication).toBe(true);
|
||||
expect(reconciled.P2P_AutoStart).toBe(true);
|
||||
expect(reconciled.settingVersion).toBe(10);
|
||||
expect(reconciled.customChunkSize).toBe(60);
|
||||
});
|
||||
|
||||
it("retains a remote profile change and restores the durable sync values", () => {
|
||||
const durableBase = settings({
|
||||
liveSync: true,
|
||||
remoteConfigurations: {},
|
||||
activeConfigurationId: "",
|
||||
});
|
||||
const runtimeBaseline = settings({ ...durableBase, liveSync: false });
|
||||
const runtimeCurrent = settings({
|
||||
...runtimeBaseline,
|
||||
remoteConfigurations: {
|
||||
main: {
|
||||
id: "main",
|
||||
name: "Main",
|
||||
uri: "sls+https://user:pass@example.com/?db=notes",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "main",
|
||||
});
|
||||
activateRemoteConfiguration(runtimeCurrent, "main");
|
||||
|
||||
const reconciled = reconcileDurableSettings({
|
||||
durableBase,
|
||||
runtimeBaseline,
|
||||
runtimeCurrent,
|
||||
preserveKeys: changedSettingKeys(durableBase, runtimeBaseline),
|
||||
command: "remote-add",
|
||||
});
|
||||
|
||||
expect(reconciled.liveSync).toBe(true);
|
||||
expect(reconciled.activeConfigurationId).toBe("main");
|
||||
expect(reconciled.remoteConfigurations.main?.name).toBe("Main");
|
||||
expect(reconciled.couchDB_URI).toBe("https://example.com");
|
||||
expect(reconciled.couchDB_DBNAME).toBe("notes");
|
||||
});
|
||||
|
||||
it("does not retain a remote profile selected temporarily by an operational command", () => {
|
||||
const durableBase = settings({
|
||||
remoteConfigurations: {
|
||||
first: {
|
||||
id: "first",
|
||||
name: "First",
|
||||
uri: "sls+https://first:pass@example.com/?db=first",
|
||||
isEncrypted: false,
|
||||
},
|
||||
second: {
|
||||
id: "second",
|
||||
name: "Second",
|
||||
uri: "sls+https://second:pass@example.net/?db=second",
|
||||
isEncrypted: false,
|
||||
},
|
||||
},
|
||||
activeConfigurationId: "first",
|
||||
});
|
||||
activateRemoteConfiguration(durableBase, "first");
|
||||
const runtimeBaseline = cloneSettings(durableBase);
|
||||
const runtimeCurrent = cloneSettings(durableBase);
|
||||
activateRemoteConfiguration(runtimeCurrent, "second");
|
||||
|
||||
const reconciled = reconcileDurableSettings({
|
||||
durableBase,
|
||||
runtimeBaseline,
|
||||
runtimeCurrent,
|
||||
preserveKeys: new Set(),
|
||||
command: "remote-status",
|
||||
});
|
||||
|
||||
expect(reconciled.activeConfigurationId).toBe("first");
|
||||
expect(reconciled.couchDB_URI).toBe("https://example.com");
|
||||
expect(reconciled.couchDB_USER).toBe("first");
|
||||
expect(reconciled.couchDB_DBNAME).toBe("first");
|
||||
});
|
||||
|
||||
it("preserves the stored adapter choice while the CLI uses its Node.js adapter", () => {
|
||||
const original = JSON.stringify({ useIndexedDBAdapter: true, settingVersion: 9 });
|
||||
const prepared = JSON.stringify({ useIndexedDBAdapter: false, settingVersion: 10 });
|
||||
const preserved = preserveStoredSetting(prepared, original, "useIndexedDBAdapter");
|
||||
const target = settings({ useIndexedDBAdapter: false });
|
||||
|
||||
applyStoredSetting(target, preserved, "useIndexedDBAdapter");
|
||||
|
||||
expect(JSON.parse(preserved).useIndexedDBAdapter).toBe(true);
|
||||
expect(target.useIndexedDBAdapter).toBe(true);
|
||||
});
|
||||
|
||||
it("does not add the CLI adapter override to an older settings file", () => {
|
||||
const original = JSON.stringify({ settingVersion: 9 });
|
||||
const prepared = JSON.stringify({ useIndexedDBAdapter: false, settingVersion: 10 });
|
||||
const preserved = preserveStoredSetting(prepared, original, "useIndexedDBAdapter");
|
||||
const target = settings({ useIndexedDBAdapter: false });
|
||||
|
||||
applyStoredSetting(target, preserved, "useIndexedDBAdapter");
|
||||
|
||||
expect(JSON.parse(preserved)).not.toHaveProperty("useIndexedDBAdapter");
|
||||
expect(target).not.toHaveProperty("useIndexedDBAdapter");
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
import { RTCPeerConnection } from "werift";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { createNodeStandardIo } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { writeStderrLine } from "@/apps/cli/cliOutput";
|
||||
import { main, type CliCommandRunner } from "@/apps/cli/main";
|
||||
import { parseTimeoutSeconds } from "@/apps/cli/commands/p2p";
|
||||
import { runP2PReplicatorReplacementProbe } from "./p2p-replicator-replacement";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { createNodeStandardIo } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
import { writeStderrLine } from "@/apps/cli/cliOutput.js";
|
||||
import { main, type CliCommandRunner } from "@/apps/cli/main.js";
|
||||
import { parseTimeoutSeconds } from "@/apps/cli/commands/p2p.js";
|
||||
import { runP2PReplicatorReplacementProbe } from "./p2p-replicator-replacement.js";
|
||||
|
||||
if (
|
||||
typeof (compatGlobal as unknown as Record<string, unknown>).RTCPeerConnection === "undefined" &&
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import type { CLICommandContext } from "@/apps/cli/commands/types";
|
||||
import { openP2PHost } from "@/apps/cli/commands/p2p";
|
||||
import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import type { CLICommandContext } from "@/apps/cli/commands/types.js";
|
||||
import { openP2PHost } from "@/apps/cli/commands/p2p.js";
|
||||
|
||||
const DEFAULT_NOTE_PATH = "p2p-replicator-replacement.md";
|
||||
const DEFAULT_NOTE_CONTENT = "Replicated after replacing the active P2P replicator.";
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"test:daemon": "deno test --env-file=.test.env -A --no-check test-daemon.ts",
|
||||
"test:decoupled-vault": "deno test --env-file=.test.env -A --no-check test-decoupled-vault.ts",
|
||||
"test:remote-commands": "deno test --env-file=.test.env -A --no-check test-remote-commands.ts",
|
||||
"test:settings-writeback": "deno test -A --no-check test-settings-writeback.ts",
|
||||
"test:push-pull": "deno test --env-file=.test.env -A --no-check test-push-pull.ts",
|
||||
"test:setup-put-cat": "deno test --env-file=.test.env -A --no-check test-setup-put-cat.ts",
|
||||
"test:mirror": "deno test --env-file=.test.env -A --no-check test-mirror.ts",
|
||||
|
||||
@@ -15,8 +15,8 @@ export async function initSettingsFile(settingsFile: string): Promise<void> {
|
||||
*/
|
||||
export async function generateSetupUriFromSettings(settingsFile: string, setupPassphrase: string): Promise<string> {
|
||||
const script = [
|
||||
"import { fs } from '@vrtmrz/livesync-commonlib/node';",
|
||||
"import { encodeSettingsToSetupURI } from '@vrtmrz/livesync-commonlib/compat/API/processSetting';",
|
||||
"import { fs } from '@vrtmrz/livesync-commonlib/node.js';",
|
||||
"import { encodeSettingsToSetupURI } from '@vrtmrz/livesync-commonlib/compat/API/processSetting.js';",
|
||||
"(async () => {",
|
||||
" const settingsPath = process.env.SETTINGS_FILE;",
|
||||
" const passphrase = process.env.SETUP_PASSPHRASE;",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { RTCPeerConnection } from "werift";
|
||||
import { TrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
|
||||
import { TrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator.js";
|
||||
|
||||
const requireFromProbe = createRequire(import.meta.url);
|
||||
const commonlibEntry = requireFromProbe.resolve("@vrtmrz/livesync-commonlib/context");
|
||||
const commonlibEntry = requireFromProbe.resolve("@vrtmrz/livesync-commonlib/context.js");
|
||||
const requireFromCommonlib = createRequire(commonlibEntry);
|
||||
const nostrEntry = requireFromCommonlib.resolve("@trystero-p2p/nostr");
|
||||
const { getRelaySockets, joinRoom, pauseRelayReconnection } = await import(pathToFileURL(nostrEntry).href);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const TASKS = [
|
||||
"test:settings-writeback",
|
||||
"test:setup-put-cat",
|
||||
"test:mirror",
|
||||
"test:daemon",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { TempDir } from "./helpers/temp.ts";
|
||||
import { runCli } from "./helpers/cli.ts";
|
||||
import { initSettingsFile } from "./helpers/settings.ts";
|
||||
|
||||
async function prepareSettingsFixture(prefix: string) {
|
||||
const workDir = await TempDir.create(prefix);
|
||||
const settingsFile = workDir.join("settings.json");
|
||||
const databaseDir = workDir.join("database");
|
||||
await Deno.mkdir(databaseDir, { recursive: true });
|
||||
await initSettingsFile(settingsFile);
|
||||
return { workDir, settingsFile, databaseDir };
|
||||
}
|
||||
|
||||
Deno.test("settings-changing commands persist durable settings without CLI runtime suspension", async () => {
|
||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-command");
|
||||
await using workDir = fixture.workDir;
|
||||
const { settingsFile, databaseDir } = fixture;
|
||||
|
||||
const settings = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
settings.liveSync = true;
|
||||
settings.syncOnStart = true;
|
||||
settings.periodicReplication = true;
|
||||
settings.P2P_Enabled = true;
|
||||
settings.P2P_AutoStart = true;
|
||||
settings.P2P_AutoBroadcast = true;
|
||||
await Deno.writeTextFile(settingsFile, JSON.stringify(settings, null, 2));
|
||||
|
||||
const result = await runCli(
|
||||
databaseDir,
|
||||
"--settings",
|
||||
settingsFile,
|
||||
"remote-add",
|
||||
"test-remote",
|
||||
"sls+https://user:pass@example.com/database"
|
||||
);
|
||||
assertEquals(result.code, 0, result.combined);
|
||||
const firstRemoteId = result.stdout.trim().split("\t")[0];
|
||||
assert(firstRemoteId, `remote-add did not return an ID: ${result.combined}`);
|
||||
|
||||
let persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
let remotes = Object.values(persisted.remoteConfigurations ?? {}) as Array<{ name?: string }>;
|
||||
assert(
|
||||
remotes.some((remote) => remote.name === "test-remote"),
|
||||
"remote-add did not persist the new profile"
|
||||
);
|
||||
assertEquals(persisted.liveSync, true);
|
||||
assertEquals(persisted.syncOnStart, true);
|
||||
assertEquals(persisted.periodicReplication, true);
|
||||
assertEquals(persisted.P2P_Enabled, true);
|
||||
assertEquals(persisted.P2P_AutoStart, true);
|
||||
assertEquals(persisted.P2P_AutoBroadcast, true);
|
||||
|
||||
const secondAdd = await runCli(
|
||||
databaseDir,
|
||||
"--settings",
|
||||
settingsFile,
|
||||
"remote-add",
|
||||
"second-remote",
|
||||
"sls+https://other:secret@example.net/second"
|
||||
);
|
||||
assertEquals(secondAdd.code, 0, secondAdd.combined);
|
||||
const secondRemoteId = secondAdd.stdout.trim().split("\t")[0];
|
||||
assert(secondRemoteId, `second remote-add did not return an ID: ${secondAdd.combined}`);
|
||||
|
||||
const activate = await runCli(databaseDir, "--settings", settingsFile, "remote-activate", secondRemoteId);
|
||||
assertEquals(activate.code, 0, activate.combined);
|
||||
persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
assertEquals(persisted.activeConfigurationId, secondRemoteId);
|
||||
|
||||
const set = await runCli(
|
||||
databaseDir,
|
||||
"--settings",
|
||||
settingsFile,
|
||||
"remote-set",
|
||||
secondRemoteId,
|
||||
"sls+https://replacement:secret@example.org/replaced"
|
||||
);
|
||||
assertEquals(set.code, 0, set.combined);
|
||||
const exported = await runCli(databaseDir, "--settings", settingsFile, "remote-export", secondRemoteId);
|
||||
assertEquals(exported.code, 0, exported.combined);
|
||||
assert(exported.stdout.includes("replacement"), "remote-set did not persist the replacement URI");
|
||||
|
||||
const remove = await runCli(databaseDir, "--settings", settingsFile, "remote-rm", secondRemoteId);
|
||||
assertEquals(remove.code, 0, remove.combined);
|
||||
persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
remotes = Object.values(persisted.remoteConfigurations ?? {}) as Array<{ id?: string }>;
|
||||
assert(!remotes.some((remote) => remote.id === secondRemoteId), "remote-rm did not persist the removal");
|
||||
assertEquals(persisted.activeConfigurationId, firstRemoteId);
|
||||
});
|
||||
|
||||
Deno.test("ordinary commands keep the settings file unchanged by default", async () => {
|
||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-readonly");
|
||||
await using workDir = fixture.workDir;
|
||||
const { settingsFile, databaseDir } = fixture;
|
||||
|
||||
const settings = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
settings.settingVersion = 9;
|
||||
const original = JSON.stringify(settings, null, 2);
|
||||
await Deno.writeTextFile(settingsFile, original);
|
||||
|
||||
const result = await runCli(databaseDir, "--settings", settingsFile, "ls");
|
||||
assertEquals(result.code, 0, result.combined);
|
||||
assertEquals(await Deno.readTextFile(settingsFile), original);
|
||||
});
|
||||
|
||||
Deno.test("--write-settings persists durable start-up setting changes", async () => {
|
||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-explicit");
|
||||
await using workDir = fixture.workDir;
|
||||
const { settingsFile, databaseDir } = fixture;
|
||||
|
||||
const settings = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
settings.settingVersion = 9;
|
||||
delete settings.useIndexedDBAdapter;
|
||||
await Deno.writeTextFile(settingsFile, JSON.stringify(settings, null, 2));
|
||||
|
||||
const result = await runCli(databaseDir, "--settings", settingsFile, "--write-settings", "ls");
|
||||
assertEquals(result.code, 0, result.combined);
|
||||
|
||||
const persisted = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
assertEquals(persisted.settingVersion, 10);
|
||||
assert(!("useIndexedDBAdapter" in persisted), "the CLI-only adapter override was written to the settings file");
|
||||
});
|
||||
|
||||
Deno.test("failed settings-changing commands leave the settings file unchanged", async () => {
|
||||
const fixture = await prepareSettingsFixture("livesync-cli-settings-failure");
|
||||
await using workDir = fixture.workDir;
|
||||
const { settingsFile, databaseDir } = fixture;
|
||||
|
||||
const original = await Deno.readTextFile(settingsFile);
|
||||
const result = await runCli(databaseDir, "--settings", settingsFile, "remote-rm", "missing-remote");
|
||||
assert(result.code !== 0, "remote-rm unexpectedly succeeded");
|
||||
assertEquals(await Deno.readTextFile(settingsFile), original);
|
||||
});
|
||||
@@ -41,6 +41,13 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
|
||||
setupResult.combined.includes("[Command] setup ->"),
|
||||
`setup command did not execute expected code path\n${setupResult.combined}`
|
||||
);
|
||||
const persistedSetup = JSON.parse(await Deno.readTextFile(settingsFile));
|
||||
assertEquals(persistedSetup.isConfigured, true, "setup did not persist the configured state");
|
||||
assert(
|
||||
typeof persistedSetup.encryptedCouchDBConnection === "string" &&
|
||||
persistedSetup.encryptedCouchDBConnection.length > 0,
|
||||
"setup did not persist the encrypted connection settings"
|
||||
);
|
||||
|
||||
const run = (...args: string[]) => runCliOrFail(vaultDir, "--settings", settingsFile, ...args);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defaultServerConditions, defaultServerMainFields, defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node";
|
||||
import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/node.js";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
|
||||
/** Diagnostic output supplied by the Webapp host. */
|
||||
export type WebAppLog = (message: unknown, level: LOG_LEVEL, key?: string) => void;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
/** Browser runtime for Self-hosted LiveSync over the File System Access API. */
|
||||
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import { ServiceContext, type LiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { initialiseServiceModulesFSAPI, type FSAPIServiceModules } from "./serviceModules/FSAPIServiceModules";
|
||||
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import { ServiceContext, type LiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
import { initialiseServiceModulesFSAPI, type FSAPIServiceModules } from "./serviceModules/FSAPIServiceModules.js";
|
||||
import {
|
||||
LOG_LEVEL_INFO,
|
||||
LOG_LEVEL_NOTICE,
|
||||
LOG_LEVEL_VERBOSE,
|
||||
type LOG_LEVEL,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import {
|
||||
collectFilesOnStorage,
|
||||
updateToDatabase,
|
||||
useOfflineScanner,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner";
|
||||
import { useRedFlagFeatures } from "@/serviceFeatures/redFlag";
|
||||
import { useCheckRemoteSize } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/checkRemoteSize";
|
||||
import { useRemoteConfiguration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig";
|
||||
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature";
|
||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner.js";
|
||||
import { useRedFlagFeatures } from "@/serviceFeatures/redFlag.js";
|
||||
import { useCheckRemoteSize } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/checkRemoteSize.js";
|
||||
import { useRemoteConfiguration } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig.js";
|
||||
import { useP2PReplicatorFeature } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/useP2PReplicatorFeature.js";
|
||||
import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import {
|
||||
createLiveSyncBrowserServiceHub,
|
||||
type LiveSyncBrowserServiceHub,
|
||||
type LiveSyncBrowserServiceHubOptions,
|
||||
} from "@/apps/browser/createLiveSyncBrowserServiceHub";
|
||||
import type { P2PReplicatorPaneHost } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneHost";
|
||||
} from "@/apps/browser/createLiveSyncBrowserServiceHub.js";
|
||||
import type { P2PReplicatorPaneHost } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneHost.js";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes.js";
|
||||
|
||||
/**
|
||||
* Conversion adapter implementation for FileSystem API
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { LOG_LEVEL_NOTICE, type FilePath, type UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import { FSAPIPathAdapter } from "./FSAPIPathAdapter";
|
||||
import { FSAPITypeGuardAdapter } from "./FSAPITypeGuardAdapter";
|
||||
import { FSAPIConversionAdapter } from "./FSAPIConversionAdapter";
|
||||
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser";
|
||||
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
|
||||
import type { FSAPIFile, FSAPIFolder, FSAPIStat } from "./FSAPITypes";
|
||||
import { LOG_LEVEL_NOTICE, type FilePath, type UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IFileSystemAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import { FSAPIPathAdapter } from "./FSAPIPathAdapter.js";
|
||||
import { FSAPITypeGuardAdapter } from "./FSAPITypeGuardAdapter.js";
|
||||
import { FSAPIConversionAdapter } from "./FSAPIConversionAdapter.js";
|
||||
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser.js";
|
||||
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter.js";
|
||||
import type { FSAPIFile, FSAPIFolder, FSAPIStat } from "./FSAPITypes.js";
|
||||
import { shareRunningResult } from "octagonal-wheels/concurrency/lock_v2";
|
||||
import type { WebAppLog } from "@/apps/webapp/WebAppLog";
|
||||
import type { WebAppLog } from "@/apps/webapp/WebAppLog.js";
|
||||
|
||||
/**
|
||||
* Complete file system adapter implementation for FileSystem API
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { FSAPIFile } from "./FSAPITypes";
|
||||
import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { FSAPIFile } from "./FSAPITypes.js";
|
||||
|
||||
/**
|
||||
* Path adapter implementation for FileSystem API
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { storageAdapterContractCases } from "@/apps/_test/storageAdapterContract";
|
||||
import { FSAPIFileSystemAdapter } from "./FSAPIFileSystemAdapter";
|
||||
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser";
|
||||
import { FileSystemAccessStorageAdapter } from "@vrtmrz/livesync-commonlib/browser.js";
|
||||
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
|
||||
import { LOG_LEVEL_NOTICE } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { LOG_LEVEL_NOTICE } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
|
||||
class MemoryFileHandle {
|
||||
readonly kind = "file";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes.js";
|
||||
|
||||
/**
|
||||
* Type guard adapter implementation for FileSystem API
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
|
||||
/**
|
||||
* FileSystem API file representation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
import type { FilePath, UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters.js";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes.js";
|
||||
|
||||
/**
|
||||
* Vault adapter implementation for FileSystem API
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createNativeElement } from "@/apps/browserDom";
|
||||
import { compatGlobal, _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { createNativeElement } from "@/apps/browserDom.js";
|
||||
import { compatGlobal, _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { mount } from "svelte";
|
||||
|
||||
import { WebAppRuntime, type WebAppRuntimeStatusKind } from "./WebAppRuntime";
|
||||
import { WebAppRuntime, type WebAppRuntimeStatusKind } from "./WebAppRuntime.js";
|
||||
import WebAppP2P from "./WebAppP2P.svelte";
|
||||
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
|
||||
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector.js";
|
||||
|
||||
const historyStore = new VaultHistoryStore();
|
||||
let runtime: WebAppRuntime | null = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
|
||||
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { FileEventItem } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { IStorageEventManagerAdapter } from "@vrtmrz/livesync-commonlib/compat/managers/adapters.js";
|
||||
import type {
|
||||
IStorageEventTypeGuardAdapter,
|
||||
IStorageEventPersistenceAdapter,
|
||||
@@ -8,12 +8,12 @@ import type {
|
||||
IStorageEventStatusAdapter,
|
||||
IStorageEventConverterAdapter,
|
||||
IStorageEventWatchHandlers,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/managers/adapters";
|
||||
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
|
||||
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { WebAppLog } from "@/apps/webapp/WebAppLog";
|
||||
} from "@vrtmrz/livesync-commonlib/compat/managers/adapters.js";
|
||||
import type { FileEventItemSentinel } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager.js";
|
||||
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes.js";
|
||||
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions.js";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types.js";
|
||||
import type { WebAppLog } from "@/apps/webapp/WebAppLog.js";
|
||||
|
||||
type FileSystemObserverRecord = {
|
||||
changedHandle?: FileSystemFileHandle | FileSystemDirectoryHandle;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager";
|
||||
import { FSAPIStorageEventManagerAdapter } from "./FSAPIStorageEventManagerAdapter";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context";
|
||||
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/managers/StorageEventManager.js";
|
||||
import { FSAPIStorageEventManagerAdapter } from "./FSAPIStorageEventManagerAdapter.js";
|
||||
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore.js";
|
||||
import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context.js";
|
||||
|
||||
export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStorageEventManagerAdapter> {
|
||||
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user