diff --git a/.dockerignore b/.dockerignore index ec80feb0..a3ee67b0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,8 +15,9 @@ main_org.js pouchdb-browser.js production/ -# Test coverage and reports +# Test coverage and reports coverage/ +_testdata/ test/bench-network/bench-results/ src/apps/cli/testdeno/bench-results/ diff --git a/.eslintrc b/.eslintrc index f55fe989..7f98b4a6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -20,8 +20,6 @@ "ignorePatterns": [ "**/node_modules/*", "**/jest.config.js", - "src/lib/coverage", - "src/lib/browsertest", "**/test.ts", "**/tests.ts", "**/**test.ts", @@ -56,4 +54,4 @@ } ] } -} \ No newline at end of file +} diff --git a/.github/workflows/cli-deno-tests.yml b/.github/workflows/cli-deno-tests.yml index afcf60e2..d249fa00 100644 --- a/.github/workflows/cli-deno-tests.yml +++ b/.github/workflows/cli-deno-tests.yml @@ -8,9 +8,6 @@ on: paths: - '.github/workflows/cli-deno-tests.yml' - 'src/apps/cli/**' - - 'src/lib/src/API/processSetting.ts' - - 'src/lib/src/replication/trystero/**' - - 'src/lib/src/rpc/**' - 'test/bench-network/**' - 'package.json' - 'package-lock.json' @@ -18,9 +15,6 @@ on: paths: - '.github/workflows/cli-deno-tests.yml' - 'src/apps/cli/**' - - 'src/lib/src/API/processSetting.ts' - - 'src/lib/src/replication/trystero/**' - - 'src/lib/src/rpc/**' - 'test/bench-network/**' - 'package.json' - 'package-lock.json' @@ -31,8 +25,6 @@ on: type: choice options: - test:ci - - test:p2p - - test:all - test:local - test:e2e-matrix default: test:ci @@ -66,12 +58,6 @@ jobs: test:ci) TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]' ;; - test:p2p) - TASK_MATRIX='["test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download"]' - ;; - test:all) - TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:p2p-host","test:p2p-peers","test:p2p-sync","test:p2p-three-nodes","test:p2p-upload-download","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]' - ;; test:local) TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon"]' ;; @@ -99,8 +85,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -168,8 +152,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Show Docker versions run: | @@ -178,7 +160,7 @@ jobs: - name: Run Compose CLI P2P E2E env: - CLI_E2E_TASK: test:p2p-sync + CLI_E2E_TASK: test:p2p:ci RELAY: ws://nostr-relay:7777/ PEERS_TIMEOUT: '20' SYNC_TIMEOUT: '60' diff --git a/.github/workflows/cli-docker.yml b/.github/workflows/cli-docker.yml index f5204400..54eaa5fd 100644 --- a/.github/workflows/cli-docker.yml +++ b/.github/workflows/cli-docker.yml @@ -2,7 +2,8 @@ # Image tag format: --cli # Example: 0.25.56-1743500000-cli # -# The image is also tagged 'latest' for convenience. +# Stable releases are also tagged with their major-minor version and 'latest'. +# Pre-releases receive immutable version and SHA-qualified tags only. # Image name: ghcr.io//livesync-cli name: Build and Push CLI Docker Image @@ -22,7 +23,6 @@ on: - "src/apps/webpeer/**" - ".github/workflows/release.yml" - ".github/workflows/unit-ci.yml" - - ".github/workflows/harness-ci.yml" workflow_dispatch: inputs: dry_run: @@ -47,8 +47,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Derive image tag id: meta @@ -61,8 +59,13 @@ jobs: # Build tag list based on the event and git ref TAGS="" if [[ "${{ github.ref }}" == refs/tags/* ]]; then - # Stable release builds - TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${MAJOR_MINOR}-cli,${IMAGE}:latest,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli" + if [[ "${VERSION}" == *-* ]]; then + # Pre-release builds must not advance stable moving tags. + TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli" + else + # Stable release builds + TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${MAJOR_MINOR}-cli,${IMAGE}:latest,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli" + fi elif [[ "${{ github.ref }}" == refs/heads/main ]]; then # Bleeding-edge / nightly builds TAGS="${IMAGE}:edge" diff --git a/.github/workflows/cli-e2e.yml b/.github/workflows/cli-e2e.yml index 17388606..6b3fcc4b 100644 --- a/.github/workflows/cli-e2e.yml +++ b/.github/workflows/cli-e2e.yml @@ -23,8 +23,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -64,4 +62,4 @@ jobs: working-directory: src/apps/cli run: | bash ./util/couchdb-stop.sh >/dev/null 2>&1 || true - bash ./util/minio-stop.sh >/dev/null 2>&1 || true \ No newline at end of file + bash ./util/minio-stop.sh >/dev/null 2>&1 || true diff --git a/.github/workflows/cli-p2p-compose-smoke.yml b/.github/workflows/cli-p2p-compose-smoke.yml index 882c8592..4b62bbd0 100644 --- a/.github/workflows/cli-p2p-compose-smoke.yml +++ b/.github/workflows/cli-p2p-compose-smoke.yml @@ -39,8 +39,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Show Docker versions run: | diff --git a/.github/workflows/finalise-release.yml b/.github/workflows/finalise-release.yml index 5e4d5dbf..d472012d 100644 --- a/.github/workflows/finalise-release.yml +++ b/.github/workflows/finalise-release.yml @@ -15,6 +15,16 @@ on: description: Full head commit SHA reviewed in the release PR required: true type: string + prerelease: + description: Mark the GitHub Release as a pre-release + required: false + type: boolean + default: false + publish_cli: + description: Create the CLI tag and publish its container image + required: false + type: boolean + default: true jobs: finalise: @@ -41,7 +51,6 @@ jobs: with: ref: ${{ steps.branch.outputs.name }} fetch-depth: 0 - submodules: recursive - name: Use Node.js uses: actions/setup-node@v4 @@ -52,6 +61,8 @@ jobs: env: VERSION: ${{ inputs.version }} EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }} + PRERELEASE: ${{ inputs.prerelease }} + PUBLISH_CLI: ${{ inputs.publish_cli }} run: | set -euo pipefail ACTUAL_HEAD_SHA="$(git rev-parse HEAD)" @@ -59,43 +70,77 @@ jobs: echo "Release branch head is ${ACTUAL_HEAD_SHA}, expected ${EXPECTED_HEAD_SHA}." >&2 exit 1 fi + if [[ "${VERSION}" == *-* && "${PRERELEASE}" != "true" ]]; then + echo "Version ${VERSION} is a pre-release version, but prerelease was not enabled." >&2 + exit 1 + fi + if [[ "${VERSION}" != *-* && "${PRERELEASE}" == "true" && "${PUBLISH_CLI}" == "true" ]]; then + echo "A stable version staged as a pre-release must use publish_cli=false so that the CLI latest and major-minor image tags do not advance before BRAT validation." >&2 + exit 1 + fi node utils/release-notes.mjs validate "${VERSION}" - name: Ensure and push release tags env: VERSION: ${{ inputs.version }} EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }} + PUBLISH_CLI: ${{ inputs.publish_cli }} run: | set -euo pipefail git fetch --tags --force - node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}" - git push --atomic origin "refs/tags/${VERSION}" "refs/tags/${VERSION}-cli" + if [[ "${PUBLISH_CLI}" == "true" ]]; then + node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}" + git push --atomic origin "refs/tags/${VERSION}" "refs/tags/${VERSION}-cli" + else + node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}" --plugin-only + git push origin "refs/tags/${VERSION}" + fi - name: Dispatch release workflows env: GH_TOKEN: ${{ github.token }} VERSION: ${{ inputs.version }} + PRERELEASE: ${{ inputs.prerelease }} + PUBLISH_CLI: ${{ inputs.publish_cli }} run: | set -euo pipefail + if [[ "${PUBLISH_CLI}" == "true" ]]; then + gh workflow run cli-docker.yml \ + --ref "${VERSION}-cli" \ + --field dry_run=false \ + --field force=false + fi gh workflow run release.yml \ --ref "${VERSION}" \ --field tag="${VERSION}" \ --field draft=true \ - --field prerelease=false - gh workflow run cli-docker.yml \ - --ref "${VERSION}-cli" \ - --field dry_run=false \ - --field force=false + --field prerelease="${PRERELEASE}" - name: Summarise next steps env: VERSION: ${{ inputs.version }} + PRERELEASE: ${{ inputs.prerelease }} + PUBLISH_CLI: ${{ inputs.publish_cli }} run: | { - echo "Ensured tags \`${VERSION}\` and \`${VERSION}-cli\` point to the reviewed release commit." + echo "Ensured the plug-in tag \`${VERSION}\` points to the reviewed release commit." + if [[ "${PUBLISH_CLI}" == "true" ]]; then + echo "The CLI tag \`${VERSION}-cli\` was also created, and finalisation explicitly dispatched the CLI container workflow." + else + echo "CLI publication was omitted." + fi echo "" echo "Dispatched the plug-in release workflow for \`${VERSION}\`. After approval for the release environment, it creates a draft GitHub Release." - echo "Dispatched the CLI Docker workflow for \`${VERSION}-cli\`. It publishes the version, major-minor, latest, and SHA-qualified image tags." echo "" - echo "Keep the release pull request in draft after publishing the GitHub Release as the latest stable release. Mark it ready and merge it only after BRAT validation succeeds." + if [[ "${VERSION}" == *-* ]]; then + echo "Publish the draft as a pre-release without replacing the latest stable release." + 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 "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." + fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/harness-ci.yml b/.github/workflows/harness-ci.yml deleted file mode 100644 index c9fff4a1..00000000 --- a/.github/workflows/harness-ci.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Run tests by Harnessed CI -name: harness-ci - -on: - workflow_dispatch: - inputs: - testsuite: - description: 'Run specific test suite (leave empty to run all)' - type: choice - options: - - '' - - 'suite/' - - 'suitep2p/' - default: '' - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '24.x' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Install test dependencies (Playwright Chromium) - run: npm run test:install-dependencies - - - name: Start test services (CouchDB) - run: npm run test:docker-couchdb:start - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Start test services (MinIO) - run: npm run test:docker-s3:start - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Start test services (Nostr Relay + WebPeer) - run: npm run test:docker-p2p:start - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} - - name: Run tests suite - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - env: - CI: true - run: npm run test suite/ - - name: Run P2P tests suite - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} - env: - CI: true - run: npm run test:p2p - - name: Stop test services (CouchDB) - run: npm run test:docker-couchdb:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Stop test services (MinIO) - run: npm run test:docker-s3:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }} - - name: Stop test services (Nostr Relay + WebPeer) - run: npm run test:docker-p2p:stop - if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }} \ No newline at end of file diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index f9d4908f..746530f4 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -37,7 +37,6 @@ jobs: with: ref: ${{ inputs.base_branch }} fetch-depth: 0 - submodules: recursive - name: Use Node.js uses: actions/setup-node@v4 @@ -45,11 +44,6 @@ jobs: node-version: "24.x" cache: npm - - name: Use Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - name: Install dependencies run: npm ci @@ -76,11 +70,10 @@ jobs: fi git switch -c "${BRANCH}" - npm version "${VERSION}" --no-git-tag-version + npm version "${VERSION}" --no-git-tag-version --allow-same-version node utils/release-notes.mjs prepare "${VERSION}" - npm run build:lib:types - git add package.json package-lock.json manifest.json versions.json updates.md src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json _types + git add package.json package-lock.json manifest.json versions.json updates.md src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json git diff --cached --check git commit -m "Releasing ${VERSION}" git push --set-upstream origin "${BRANCH}" @@ -94,24 +87,7 @@ jobs: BASE_BRANCH: ${{ inputs.base_branch }} RELEASE_BRANCH: ${{ steps.prepare.outputs.branch }} run: | - cat > /tmp/release-pr-body.md < [!IMPORTANT] - > **Merge intentionally on hold** - > - > Publishing the GitHub Release does not unblock this pull request. Keep this pull request in draft, and leave \`main\` on the previous release, until the published build has passed BRAT validation. - - ## Release checklist - - - [ ] Review and polish \`updates.md\` - - [ ] Confirm the release date - - [ ] Confirm \`manifest.json\`, \`versions.json\`, workspace package versions, and generated \`_types\` - - [ ] Confirm CI has passed - - [ ] Run the finalise release workflow with this PR's fixed head SHA - - [ ] Confirm the draft GitHub Release assets and the published CLI image - - [ ] Publish the GitHub Release as the latest stable release while keeping this pull request in draft - - [ ] Validate the published release with BRAT - - [ ] Mark this pull request ready and merge it with a merge commit - EOF + node utils/release-pr-body.mjs "${VERSION}" "${BASE_BRANCH}" > /tmp/release-pr-body.md gh pr create \ --base "${BASE_BRANCH}" \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d39e5ca1..ce155b7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,6 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: recursive ref: ${{ inputs.tag }} - name: Use Node.js uses: actions/setup-node@v4 @@ -61,17 +60,10 @@ jobs: main.js manifest.json styles.css - # Package the required files into a zip - - name: Package - run: | - mkdir ${{ github.event.repository.name }} - cp main.js manifest.json styles.css README.md ${{ github.event.repository.name }} - zip -r ${{ github.event.repository.name }}.zip ${{ github.event.repository.name }} - name: Create Release and Upload Assets uses: softprops/action-gh-release@v2 with: files: | - ${{ github.event.repository.name }}.zip main.js manifest.json styles.css diff --git a/.github/workflows/unit-ci.yml b/.github/workflows/unit-ci.yml index 5d173f33..abd8c2b1 100644 --- a/.github/workflows/unit-ci.yml +++ b/.github/workflows/unit-ci.yml @@ -17,10 +17,15 @@ on: - 'vitest.config*.ts' - 'esbuild.config.mjs' - 'eslint.config.mjs' + - 'eslint.config.common.mjs' + - 'eslint.community.config.mjs' - 'update-workspaces.mjs' - 'version-bump.mjs' - 'utils/release-*.mjs' - 'utils/release-*.unit.spec.ts' + - 'utils/couchdb/**' + - 'utils/flyio/**' + - 'utils/setup/**' - '.github/workflows/prepare-release.yml' - '.github/workflows/finalise-release.yml' - '.github/workflows/release.yml' @@ -36,10 +41,15 @@ on: - 'vitest.config*.ts' - 'esbuild.config.mjs' - 'eslint.config.mjs' + - 'eslint.config.common.mjs' + - 'eslint.community.config.mjs' - 'update-workspaces.mjs' - 'version-bump.mjs' - 'utils/release-*.mjs' - 'utils/release-*.unit.spec.ts' + - 'utils/couchdb/**' + - 'utils/flyio/**' + - 'utils/setup/**' - '.github/workflows/prepare-release.yml' - '.github/workflows/finalise-release.yml' - '.github/workflows/release.yml' @@ -49,6 +59,54 @@ permissions: contents: read jobs: + setup-tools: + name: Self-hosted setup tools + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24.x' + cache: 'npm' + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install dependencies + run: npm ci + + - name: Run setup-tool contract tests + run: npm run test:setup-tools + + - name: Create CouchDB test configuration + run: | + cat < .test.env + hostname=http://127.0.0.1:5989/ + dbname=livesync-test-db + username=admin + password=testpassword + EOF + + - name: Start CouchDB + run: npm run test:docker-couchdb:start + + - name: Provision a versioned LiveSync database + run: npx dotenv-cli -e .test.env -- env database=setup-tools-ci retry_count=1 retry_delay_ms=0 ./utils/couchdb/couchdb-init.sh + + - name: Verify the Commonlib database version + run: | + npx dotenv-cli -e .test.env -- bash -lc 'curl --fail --silent --show-error --user "${username}:${password}" "${hostname}/setup-tools-ci/obsydian_livesync_version" | node --input-type=module -e "import { VER } from \"@vrtmrz/livesync-commonlib/compat/common/types\";let input=\"\";process.stdin.on(\"data\",chunk=>input+=chunk).on(\"end\",()=>{const document=JSON.parse(input);if(document.type!==\"versioninfo\"||document.version!==VER)throw new Error(\"Unexpected LiveSync database version\");})"' + + - name: Stop CouchDB + if: always() + run: npm run test:docker-couchdb:stop || true + unit-test: name: Unit Tests runs-on: ubuntu-latest @@ -56,8 +114,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup Node.js uses: actions/setup-node@v4 @@ -68,9 +124,15 @@ jobs: - name: Install dependencies run: npm ci + - name: Run source checks + run: npm run check + - name: Run unit tests suite with coverage run: npm run test:unit:coverage + - name: Run real-Obsidian runner contract tests + run: npm run test:e2e:obsidian:runner + - name: Upload coverage report if: always() uses: actions/upload-artifact@v4 @@ -85,19 +147,35 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - submodules: recursive + + - name: Detect LiveSync-owned integration tests + id: integration_tests + shell: bash + run: | + git ls-files -- ':(glob)**/*.integration.spec.ts' ':(glob)**/*.integration.test.ts' > "$RUNNER_TEMP/livesync-integration-tests.txt" + if [[ -s "$RUNNER_TEMP/livesync-integration-tests.txt" ]]; then + echo 'present=true' >> "$GITHUB_OUTPUT" + else + echo 'present=false' >> "$GITHUB_OUTPUT" + fi + + - name: Record delegated integration coverage + if: ${{ steps.integration_tests.outputs.present != 'true' }} + run: echo 'No LiveSync-owned integration tests are present. Commonlib integration tests run in the Commonlib package CI.' >> "$GITHUB_STEP_SUMMARY" - name: Setup Node.js + if: ${{ steps.integration_tests.outputs.present == 'true' }} uses: actions/setup-node@v4 with: node-version: '24.x' cache: 'npm' - name: Install dependencies + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm ci - name: Create environment configuration files + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: | cat < .env BUILD_MODE=dev @@ -115,16 +193,19 @@ jobs: EOF - name: Start CouchDB container + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm run test:docker-couchdb:start - name: Start MinIO container + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm run test:docker-s3:start - name: Run integration tests + if: ${{ steps.integration_tests.outputs.present == 'true' }} run: npm run test:integration - name: Stop containers - if: always() + if: ${{ always() && steps.integration_tests.outputs.present == 'true' }} run: | npm run test:docker-couchdb:stop || true npm run test:docker-s3:stop || true diff --git a/.gitignore b/.gitignore index c443c8ed..f883e1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ cov_profile/** coverage src/apps/cli/dist/* +src/apps/webapp/playwright-report/ +src/apps/webapp/test-results/ _testdata/** utils/bench/splitResults.csv -.eslintcache \ No newline at end of file +.eslintcache diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ea943b62..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/lib"] - path = src/lib - url = https://github.com/vrtmrz/livesync-commonlib diff --git a/AGENTS.md b/AGENTS.md index 36c9df53..5148dabd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,10 +5,10 @@ When working on this repository (writing code, comments, documentation, or commi ## Required Reference Files Before making changes to documentation, user-facing text, or settings: -1. Read [docs/terms.md](file:///Users/vorotamoroz/dev/js/obsidian-livesync/docs/terms.md) for terminology, vocabulary conventions, and technical definitions. -2. Read [docs/settings.md](file:///Users/vorotamoroz/dev/js/obsidian-livesync/docs/settings.md) (and [docs/settings_ja.md](file:///Users/vorotamoroz/dev/js/obsidian-livesync/docs/settings_ja.md)) for UI settings and setting key mappings. -3. Read [docs/troubleshooting.md](file:///Users/vorotamoroz/dev/js/obsidian-livesync/docs/troubleshooting.md) for troubleshooting guidelines and common recovery steps (such as flag files and SCRAM state). -4. Read [devs.md](file:///Users/vorotamoroz/dev/js/obsidian-livesync/devs.md) for development workflows, module architecture, and testing infrastructure. +1. Read [docs/terms.md](docs/terms.md) for terminology, vocabulary conventions, and technical definitions. +2. Read [docs/settings.md](docs/settings.md) (and [docs/settings_ja.md](docs/settings_ja.md)) for UI settings and setting key mappings. +3. Read [docs/troubleshooting.md](docs/troubleshooting.md) for troubleshooting guidelines and common recovery steps (such as flag files and SCRAM state). +4. Read [devs.md](devs.md) for development workflows, module architecture, and testing infrastructure. --- @@ -45,10 +45,10 @@ Always adhere to the following stylistic and spelling rules: - **Fast Setup (Simple Fetch)** is the preferred flow for initial replication on secondary devices. It utilises stream-based replication for high speed and delays local file reflection to suppress temporary synchronisation warnings. - **Flag files** (such as `redflag.md`, `redflag2.md`, and `redflag3.md`) at the root of the vault control the boot-up sequence and trigger automated fetch/rebuild tasks. 3. **Subrepositories**: - - The directory [src/lib](file:///Users/vorotamoroz/dev/js/obsidian-livesync/src/lib) is a subrepository (Git submodule) pointing to the shared library `livesync-commonlib`. Do not make modifications inside this directory without careful consideration, as changes affect the shared library. + - Treat `@vrtmrz/livesync-commonlib` as an external, authoritative package. Make Commonlib changes in its repository, validate the packed artefact and downstream LiveSync consumer, and update the exact dependency here. Do not recreate a `src/lib` source mirror or generated `_types` fallback. 4. **Application Directories**: - - The directory [src/apps](file:///Users/vorotamoroz/dev/js/obsidian-livesync/src/apps) contains independent application modules: - - `cli`: A Command Line Interface application. Tests specifically for the CLI (both unit and End-to-End tests) are located and executed within [src/apps/cli](file:///Users/vorotamoroz/dev/js/obsidian-livesync/src/apps/cli) using its local `package.json` scripts. + - The directory [src/apps](src/apps) contains independent application modules: + - `cli`: A Command Line Interface application. Tests specifically for the CLI (both unit and End-to-End tests) are located and executed within [src/apps/cli](src/apps/cli) using its local `package.json` scripts. - `webapp`: A Web-based application. - `webpeer`: A Web-based peer utility. @@ -62,7 +62,8 @@ Before submitting code, you should run verification scripts locally to ensure co - Run `npm run check` to perform code verification. This runs type-checking (`tsc-check`), ESLint (`lint`), and Svelte checks (`svelte-check`). 2. **Unit Tests**: - Run `npm run test:unit` to execute fast local unit tests. - - Run `npm run test` or `npm run test:full` for full testing suites (including dockerised services). + - Run `npm run test:unit:coverage` when unit-test coverage is required. + - Run focused integration, CLI E2E, or real Obsidian E2E commands for the boundary being changed. Start only the Docker services required by that command. 3. **Build**: - Run `npm run build` to compile the production bundle (`main.js`). - Run `npm run dev` for the development watch/build task. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 487bc4fc..a97333b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,13 +6,9 @@ Thank you for your interest in contributing to Self-hosted LiveSync! We welcome To set up the development environment, please follow these steps: -1. Clone the repository recursively to ensure all Git submodules are loaded: +1. Clone the repository: ```bash - git clone --recursive https://github.com/vrtmrz/obsidian-livesync - ``` - If you have already cloned the repository without submodules, run the following command: - ```bash - git submodule update --init --recursive + git clone https://github.com/vrtmrz/obsidian-livesync ``` 2. Install the package dependencies: @@ -25,7 +21,7 @@ To set up the development environment, please follow these steps: npm run build ``` -For a more comprehensive guide on development workflows, testing configurations, and subrepos, please refer to [devs.md](devs.md). +For a more comprehensive guide on development workflows, testing configurations, and the Commonlib dependency, please refer to [devs.md](devs.md). ## Guidelines for Contributions @@ -37,10 +33,16 @@ Before submitting a pull request, you must run verification scripts locally to e ```bash npm run check ``` + This also type-checks the maintained CLI and browser applications, and applies the Community directory blocker rules. Run `npm run lint:community` separately to inspect its non-blocking recommendations. - Run unit tests: ```bash npm run test:unit ``` +- When changing the troubleshooting or recovery guides, inspect their current English UI labels and local references: + ```bash + npm run inspect:troubleshooting + ``` + This read-only Inspector prints JSON containing `ok`, `checkedFiles`, `checkedLocalReferences`, and `errors`, and exits unsuccessfully when a contract is stale. If you have the capability and a suitable environment (such as Linux and Docker), running the CLI End-to-End (E2E) tests is also highly appreciated. Instructions are detailed in [devs.md](devs.md). If you cannot run E2E tests locally, please explicitly ask to run the tests on the CI by stating 'Please run CI tests' in your pull request description. @@ -61,9 +63,9 @@ For a detailed list of vocabulary conventions and terms, please refer to [docs/t To add or update translations, please refer to [docs/adding_translations.md](docs/adding_translations.md) for detailed instructions. -### 4. Git Submodules +### 4. Commonlib changes -The `src/lib` directory is a Git submodule pointing to the shared library `livesync-commonlib`. If you wish to propose changes to the shared library, do not modify `src/lib` directly. Instead, please submit a separate pull request to the [livesync-commonlib repository](https://github.com/vrtmrz/livesync-commonlib). +Shared synchronisation behaviour is provided by the `@vrtmrz/livesync-commonlib` package. If you wish to change that library, submit a separate pull request to the [livesync-commonlib repository](https://github.com/vrtmrz/livesync-commonlib), validate its packed artefact, then update the locked dependency in this repository. Do not add a source mirror or generated fallback declarations to this repository. ## License diff --git a/README.md b/README.md index 6b3b77ac..b293b2b4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Self-hosted LiveSync is a community-developed synchronisation plug-in available on all Obsidian-compatible platforms. It leverages robust server solutions such as CouchDB or object storage systems (e.g., MinIO, S3, R2, etc.) to ensure reliable data synchronisation. -Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling you to synchronise your notes directly between devices without relying on a server. Documentation is available for [Peer-to-Peer Synchronisation](./docs/p2p_sync_updates_2026.md). +Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling devices to exchange notes without a central data-storage server. A signalling relay is still required for peer discovery. See [How peer-to-peer synchronisation works](./docs/p2p.md). ![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif) @@ -18,15 +18,11 @@ Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling yo - Use open-source solutions for the server. - Compatible solutions are supported. - Support end-to-end encryption. -- Synchronise settings, snippets, themes, and plug-ins via [Customisation Sync (Beta)](docs/settings.md#6-customization-sync-advanced) or [Hidden File Sync](docs/settings.md#7-hidden-files-advanced). -- Enable WebRTC peer-to-peer synchronisation without requiring a `host` (Experimental). - - This feature is still in the experimental stage. Please exercise caution when using it. - - WebRTC is a peer-to-peer synchronisation method, so **at least one device must be online to synchronise**. - - Instead of keeping your device online as a stable peer, you can use two pseudo-peers: - - [livesync-serverpeer](https://github.com/vrtmrz/livesync-serverpeer): A pseudo-client running on the server for receiving and sending data between devices. - - [webpeer](https://github.com/vrtmrz/obsidian-livesync/tree/main/src/apps/webpeer): A pseudo-client for receiving and sending data between devices. - - A pre-built instance is available at [fancy-syncing.vrtmrz.net/webpeer](https://fancy-syncing.vrtmrz.net/webpeer/) (hosted on the vrtmrz's blog site). This is also peer-to-peer. Feel free to use it. - - For more information, refer to the [English explanatory article](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync-en.html) or the [Japanese explanatory article](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync). +- Synchronise settings, snippets, themes, and plug-ins via [Customisation Sync (Beta)](docs/settings.md#6-customisation-sync-advanced) or [Hidden File Sync](docs/tips/hidden-file-sync.md). +- Enable supported, opt-in WebRTC peer-to-peer synchronisation. + - 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). 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. @@ -46,15 +42,27 @@ This plug-in may be particularly useful for researchers, engineers, and develope 1. [Set up CouchDB on fly.io](docs/setup_flyio.md) 2. Configure plug-in in [Quick Setup](docs/quick_setup.md) -### Manual Setup +### Setup workflows + +Choose a synchronisation method, prepare its server where required, then follow the corresponding client setup: + +1. CouchDB + 1. Prepare the server: + - [Set up your own CouchDB server](docs/setup_own_server.md). + - [Set up CouchDB on fly.io](docs/setup_flyio.md). + 2. Configure the clients by following [CouchDB Quick Setup](docs/quick_setup.md). +2. Object Storage + 1. Prepare the server. A maintained MinIO server installation guide is not currently available here, so set up an S3-compatible service or server of your choice. + 2. Configure the clients by following [Object Storage Setup](docs/setup_object_storage.md). +3. Peer-to-Peer + 1. No central data-storage server is required. The project's public signalling relay requires no server provisioning; controlled deployments can provide another compatible relay. + 2. Configure the clients by following [Peer-to-Peer Setup](docs/setup_p2p.md). + +Each workflow establishes ordinary note synchronisation on the first device, generates a Setup URI for each additional device from that working device, and verifies synchronisation in both directions. -1. Set up the server - 1. [Set up CouchDB on fly.io](docs/setup_flyio.md) - 2. [Set up your CouchDB](docs/setup_own_server.md) -2. Configure plug-in in [Quick Setup](docs/quick_setup.md) > [!TIP] > Fly.io is no longer free. Fortunately, we can still use IBM Cloudant despite some limitations. Refer to [Set up IBM Cloudant](docs/setup_cloudant.md). -> We can also use peer-to-peer synchronisation without a server. Alternatively, cheap object storage like Cloudflare R2 can be used for free. +> We can also use peer-to-peer synchronisation without a central data-storage server; a signalling relay is still used for peer discovery. Alternatively, cheap object storage like Cloudflare R2 can be used for free. > However, most importantly, we can use a server that we trust. Therefore, please set up your own server. > CouchDB can also be run on a Raspberry Pi (please be mindful of your server's security). @@ -89,6 +97,9 @@ To prevent file and database corruption, please avoid closing Obsidian until all ## Tips and Troubleshooting - If you want a faster and simpler initial replication when setting up subsequent devices, see the [Fast Setup Guide](docs/tips/fast-setup.md). +- Configure [Hidden File Sync](docs/tips/hidden-file-sync.md) only after ordinary note synchronisation works. +- If Obsidian or LiveSync cannot start normally, use [Recovery and flag files](docs/recovery.md) before changing or resetting a remote database. +- Self-hosted LiveSync 1.0 requires Obsidian 1.7.2 or later. If you need to use 1.0 on an earlier Obsidian version, please [open an issue](https://github.com/vrtmrz/obsidian-livesync/issues/new?template=issue-report.md) with your version, platform, and reason for remaining on it so that we can assess whether extending support is practical. The standard Community Plugins installer otherwise selects an older compatible plug-in release. - If you are having problems getting the plug-in working, see [Tips and Troubleshooting](docs/troubleshooting.md). ## Acknowledgements diff --git a/_tools/bakei18n.ts b/_tools/bakei18n.ts new file mode 100644 index 00000000..b86cc390 --- /dev/null +++ b/_tools/bakei18n.ts @@ -0,0 +1,13 @@ +import { allMessages } from "../src/common/messages/combinedMessages.dev.ts"; + +const { writeFileSync } = process.getBuiltinModule("node:fs"); +const path = process.getBuiltinModule("node:path"); +const __dirname = import.meta.dirname; +const currentPath = __dirname; +const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts"); + +process.stdout.write(`Writing to ${outDir}\n`); +writeFileSync( + outDir, + `export const allMessages: Readonly>>> = ${JSON.stringify(allMessages, null, 4)};` +); diff --git a/_tools/checkI18nCoverage.ts b/_tools/checkI18nCoverage.ts new file mode 100644 index 00000000..f8c6d744 --- /dev/null +++ b/_tools/checkI18nCoverage.ts @@ -0,0 +1,63 @@ +import { glob } from "tinyglobby"; +import { parse } from "yaml"; +import { objectToDotted } from "./messagelib.ts"; + +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); +const __dirname = import.meta.dirname; +const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/")); +const files = (await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir })).sort(); + +function flattenMessages(src: unknown) { + return Object.fromEntries( + Object.entries(objectToDotted(src)) + .map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value] as const) + .filter(([, value]) => typeof value === "string") + .sort(([a], [b]) => a.localeCompare(b)) + ) as Record; +} + +const localeData = new Map>(); +for (const file of files) { + const segments = file.split(/[/\\]/); + const localeFilename = segments[segments.length - 1]; + if (localeFilename === undefined) { + throw new Error(`Could not determine the locale name for ${file}`); + } + const locale = localeFilename.replace(/\.yaml$/, ""); + const content = await fsPromises.readFile(file, "utf-8"); + const parsed: unknown = parse(content); + localeData.set(locale, flattenMessages(parsed ?? {})); +} + +const baseLocale = "en"; +const base = localeData.get(baseLocale); +if (!base) { + throw new Error("en.yaml not found"); +} + +const baseKeys = Object.keys(base); +const report = Object.fromEntries( + [...localeData.entries()].map(([locale, data]) => { + const keys = new Set(Object.keys(data)); + const missing = baseKeys.filter((key) => !keys.has(key)); + const identicalToEnglish = baseKeys.filter( + (key) => keys.has(key) && locale !== baseLocale && data[key] === base[key] + ); + const translated = baseKeys.length - missing.length; + return [ + locale, + { + totalBaseKeys: baseKeys.length, + translatedKeys: translated, + missingKeys: missing.length, + identicalToEnglishCount: identicalToEnglish.length, + coverage: `${translated}/${baseKeys.length}`, + missing, + identicalToEnglish, + }, + ]; + }) +); + +process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); diff --git a/_tools/decompileRosetta.ts b/_tools/decompileRosetta.ts new file mode 100644 index 00000000..c2716382 --- /dev/null +++ b/_tools/decompileRosetta.ts @@ -0,0 +1,54 @@ +import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../src/common/rosetta"; +import { allMessages } from "../src/common/messages/combinedMessages.dev.ts"; + +const { writeFileSync } = process.getBuiltinModule("node:fs"); +const path = process.getBuiltinModule("node:path"); +const thisFileDir = __dirname; +const outDir = path.join(thisFileDir, "i18n"); + +const out = {} as Record; + +for (const [key, value] of Object.entries(allMessages)) { + for (const lang of [...SUPPORTED_I18N_LANGS, "def"]) { + if (!out[lang]) out[lang] = {}; + if (lang in value) { + out[lang][key] = value[lang as I18N_LANGS]; + } else { + if (lang === "def") { + out[lang][key] = key; + } else { + out[lang][key] = undefined; + } + } + } +} + +for (const [lang, value] of Object.entries(out)) { + const filename = `${lang}.ts`; + const escapeString = (prefix: string, key: string, str: string) => { + if (str.indexOf("\n") !== -1) { + const encoded = JSON.stringify(str); + const lineWrapped = encoded.split("\\n").join("\\\n" + prefix); + + return `${prefix}${JSON.stringify(key)}: ${lineWrapped},`; + } + return `${prefix}${JSON.stringify(key)}: ${JSON.stringify(str)},`; + }; + // const z ="a" "b" "c"; + const _stringify = (value: Record) => { + let res = "{\n"; + for (const key of Object.keys(value)) { + const v = value[key]; + if (v) { + res += escapeString("", key, v) + "\n"; + } else { + res += escapeString("// ", key, out["def"]?.[key] ?? "") + "\n"; + } + } + return res + "\n}"; + }; + void writeFileSync( + path.join(outDir, filename), + `export const PartialMessages ={\n "${lang}":${_stringify(value)}\n} as const;` + ); +} diff --git a/_tools/decompileRosettaToJson.ts b/_tools/decompileRosettaToJson.ts new file mode 100644 index 00000000..030c067e --- /dev/null +++ b/_tools/decompileRosettaToJson.ts @@ -0,0 +1,21 @@ +import { allMessages } from "../src/common/messages/combinedMessages.prod.ts"; +const __dirname = import.meta.dirname; + +const { writeFileSync } = process.getBuiltinModule("node:fs"); +const path = process.getBuiltinModule("node:path"); +const thisFileDir = __dirname; +const outDir = path.resolve(thisFileDir, "../src/common/messagesJson"); + +const out = {} as Record; + +for (const [key, value] of Object.entries(allMessages)) { + for (const [lang, langValue] of Object.entries(value)) { + if (!out[lang]) out[lang] = {}; + out[lang][key] = langValue; + } +} + +for (const [lang, value] of Object.entries(out)) { + const filename = `${lang}.json`; + void writeFileSync(path.join(outDir, filename), JSON.stringify(value, null, 4)); +} diff --git a/_tools/inspect-troubleshooting-docs.ts b/_tools/inspect-troubleshooting-docs.ts new file mode 100644 index 00000000..245bf3da --- /dev/null +++ b/_tools/inspect-troubleshooting-docs.ts @@ -0,0 +1,137 @@ +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); +const url = process.getBuiltinModule("node:url"); + +type InspectionError = { + check: "current-label" | "local-reference" | "retired-label"; + file: string; + detail: string; +}; + +export type TroubleshootingDocsInspection = { + ok: boolean; + checkedFiles: string[]; + checkedLocalReferences: number; + errors: InspectionError[]; +}; + +const guidePaths = ["docs/troubleshooting.md", "docs/recovery.md", "docs/tips/p2p-sync-tips.md"] as const; +const messageCataloguePath = "src/common/messagesJson/en.json"; +const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu; + +function repositoryRootFromThisFile(): string { + return path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), ".."); +} + +function normaliseReferenceTarget(rawTarget: string): string { + const withoutAngles = rawTarget.startsWith("<") && rawTarget.endsWith(">") ? rawTarget.slice(1, -1) : rawTarget; + return decodeURIComponent(withoutAngles); +} + +function isExternalReference(target: string): boolean { + return /^(?:https?:|mailto:|obsidian:)/u.test(target); +} + +async function inspectLocalReferences( + repositoryRoot: string, + documentPath: string, + document: string, + errors: InspectionError[] +): Promise { + let checked = 0; + for (const match of document.matchAll(markdownLinkPattern)) { + const rawTarget = match[1]; + if (!rawTarget) continue; + const target = normaliseReferenceTarget(rawTarget); + if (isExternalReference(target) || target.startsWith("#")) continue; + + const [pathPart] = target.split("#", 1); + if (!pathPart) continue; + checked++; + const referencedPath = path.resolve(repositoryRoot, path.dirname(documentPath), pathPart); + try { + await fsPromises.access(referencedPath); + } catch { + errors.push({ + check: "local-reference", + file: documentPath, + detail: `Missing local reference: ${path.relative(repositoryRoot, referencedPath)}`, + }); + } + } + return checked; +} + +export async function inspectTroubleshootingDocs( + repositoryRoot = repositoryRootFromThisFile() +): Promise { + const errors: InspectionError[] = []; + const documents = new Map(); + for (const guidePath of guidePaths) { + documents.set(guidePath, await fsPromises.readFile(path.resolve(repositoryRoot, guidePath), "utf8")); + } + + const troubleshooting = documents.get("docs/troubleshooting.md")!; + const catalogue = JSON.parse( + await fsPromises.readFile(path.resolve(repositoryRoot, messageCataloguePath), "utf8") + ) as Record; + const requiredMessageKeys = [ + "TweakMismatchResolve.Action.UseConfigured", + "TweakMismatchResolve.Action.UseMine", + "TweakMismatchResolve.Action.UseRemote", + "TweakMismatchResolve.Action.Dismiss", + "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown", + ] as const; + + for (const messageKey of requiredMessageKeys) { + const label = catalogue[messageKey]; + if (!label) { + errors.push({ + check: "current-label", + file: messageCataloguePath, + detail: `The English message catalogue does not define ${messageKey}.`, + }); + continue; + } + if (!troubleshooting.includes(label)) { + errors.push({ + check: "current-label", + file: "docs/troubleshooting.md", + detail: `The guide does not include the current UI label '${label}'.`, + }); + } + } + + for (const retiredLabel of ["`Update with mine`", "`Use configured`", "`Sync settings via Markdown files`"]) { + if (troubleshooting.includes(retiredLabel)) { + errors.push({ + check: "retired-label", + file: "docs/troubleshooting.md", + detail: `The guide still includes the retired label ${retiredLabel}.`, + }); + } + } + + let checkedLocalReferences = 0; + for (const [guidePath, document] of documents) { + checkedLocalReferences += await inspectLocalReferences(repositoryRoot, guidePath, document, errors); + } + + return { + ok: errors.length === 0, + checkedFiles: [...guidePaths], + checkedLocalReferences, + errors, + }; +} + +async function runCli(): Promise { + const result = await inspectTroubleshootingDocs(); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + if (!result.ok) process.exitCode = 1; +} + +const invokedPath = process.argv[1] ? url.pathToFileURL(path.resolve(process.argv[1])).href : undefined; +if (invokedPath === import.meta.url) { + await runCli(); +} diff --git a/_tools/inspect-troubleshooting-docs.unit.spec.ts b/_tools/inspect-troubleshooting-docs.unit.spec.ts new file mode 100644 index 00000000..f184a162 --- /dev/null +++ b/_tools/inspect-troubleshooting-docs.unit.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { inspectTroubleshootingDocs } from "./inspect-troubleshooting-docs"; + +describe("troubleshooting documentation contract", () => { + it("uses current English UI labels and resolves every local guide reference", async () => { + const result = await inspectTroubleshootingDocs(); + + expect(result.checkedFiles).toEqual([ + "docs/troubleshooting.md", + "docs/recovery.md", + "docs/tips/p2p-sync-tips.md", + ]); + expect(result.checkedLocalReferences).toBeGreaterThan(0); + expect(result.errors).toEqual([]); + expect(result.ok).toBe(true); + }); +}); diff --git a/_tools/json2yaml.ts b/_tools/json2yaml.ts new file mode 100644 index 00000000..299b5d6b --- /dev/null +++ b/_tools/json2yaml.ts @@ -0,0 +1,31 @@ +// Convert Application convenient Message Resources (JSON) to Human-Editable format (YAML) +import { stringify } from "yaml"; +import { glob } from "tinyglobby"; +import { dottedToObject } from "./messagelib"; + +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); +const __dirname = import.meta.dirname; + +const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesJson/")); +process.stdout.write(`Target directory: ${targetDir}\n`); +const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir }); +for (const file of files) { + const filePath = path.resolve(file); + process.stdout.write(`Processing file: ${filePath}\n`); + const content = await fsPromises.readFile(filePath, "utf-8"); + const jsonDataSrc: unknown = JSON.parse(content); + if (typeof jsonDataSrc !== "object" || jsonDataSrc === null || Array.isArray(jsonDataSrc)) { + throw new TypeError(`Expected ${filePath} to contain a JSON object`); + } + const jsonDataD2 = Object.fromEntries( + Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + ); + const jsonData = dottedToObject(jsonDataD2); + const yamlData = stringify(jsonData, { indent: 2 }); + const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML"); + await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8"); + process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`); +} + +// console.dir(files, { depth: 0 }); diff --git a/_tools/messagelib.ts b/_tools/messagelib.ts new file mode 100644 index 00000000..95a4c664 --- /dev/null +++ b/_tools/messagelib.ts @@ -0,0 +1,49 @@ +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function objectToDotted(obj: unknown, prefix = ""): Record { + if (!isRecord(obj)) { + throw new TypeError("Expected a message catalogue object"); + } + const flattened: Record = {}; + for (const [key, value] of Object.entries(obj)) { + const newKey = prefix ? `${prefix}.${key}` : key; + if (isRecord(value)) { + Object.assign(flattened, objectToDotted(value, newKey)); + } else { + flattened[newKey] = value; + } + } + return flattened; +} + +export function dottedToObject(obj: unknown): Record { + if (!isRecord(obj)) { + throw new TypeError("Expected a dotted message catalogue object"); + } + const nestedResult: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (key.includes(" ")) { + nestedResult[key] = value; + continue; + } + const keys = key.split("."); + let nested = nestedResult; + for (const [index, currentKey] of keys.entries()) { + if (index === keys.length - 1) { + nested[currentKey] = value; + continue; + } + const currentValue = nested[currentKey]; + if (isRecord(currentValue)) { + nested = currentValue; + continue; + } + const replacement = currentValue === undefined || currentValue === null ? {} : { _value: currentValue }; + nested[currentKey] = replacement; + nested = replacement; + } + } + return nestedResult; +} diff --git a/_tools/messagelib.unit.spec.ts b/_tools/messagelib.unit.spec.ts new file mode 100644 index 00000000..ae8f612a --- /dev/null +++ b/_tools/messagelib.unit.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { dottedToObject, objectToDotted } from "./messagelib"; + +describe("message catalogue conversion", () => { + it("flattens nested objects while preserving leaf values", () => { + expect( + objectToDotted({ + dialogue: { + title: "Title", + options: ["first", "second"], + }, + "literal key": "Literal", + }) + ).toEqual({ + "dialogue.title": "Title", + "dialogue.options": ["first", "second"], + "literal key": "Literal", + }); + }); + + it("preserves an existing leaf under _value when a dotted child follows it", () => { + expect( + dottedToObject({ + section: "Base value", + "section.child": "Child value", + "literal key": "Literal", + }) + ).toEqual({ + section: { + _value: "Base value", + child: "Child value", + }, + "literal key": "Literal", + }); + }); + + it("rejects non-object catalogue roots", () => { + expect(() => objectToDotted("not an object")).toThrow("Expected a message catalogue object"); + expect(() => dottedToObject(["not", "an", "object"])).toThrow("Expected a dotted message catalogue object"); + }); +}); diff --git a/_tools/yaml2json.ts b/_tools/yaml2json.ts new file mode 100644 index 00000000..d35a39d3 --- /dev/null +++ b/_tools/yaml2json.ts @@ -0,0 +1,30 @@ +// Convert Human-Editable format (YAML) to Application convenient Message Resources (JSON) + +import { parse } from "yaml"; +import { glob } from "tinyglobby"; +import { objectToDotted } from "./messagelib"; + +const fsPromises = process.getBuiltinModule("node:fs/promises"); +const path = process.getBuiltinModule("node:path"); +const __dirname = import.meta.dirname; + +const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/")); +process.stdout.write(`Target directory: ${targetDir}\n`); +const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir }); +for (const file of files) { + const filePath = path.resolve(file); + const content = await fsPromises.readFile(filePath, "utf-8"); + const jsonDataSrc: unknown = parse(content); + const jsonDataD2 = objectToDotted(jsonDataSrc); + const jsonData = Object.fromEntries( + Object.entries(jsonDataD2) + .map(([key, value]): [string, unknown] => [key.endsWith("._value") ? key.slice(0, -7) : key, value]) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + ); + const yamlData = JSON.stringify(jsonData, null, 4) + "\n"; + const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json"); + await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8"); + process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`); +} + +// console.dir(files, { depth: 0 }); diff --git a/_types/src/LiveSyncBaseCore.d.ts b/_types/src/LiveSyncBaseCore.d.ts deleted file mode 100644 index ee2dcf9d..00000000 --- a/_types/src/LiveSyncBaseCore.d.ts +++ /dev/null @@ -1,136 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "@lib/common/types"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { LiveSyncLocalDBEnv } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { LiveSyncCouchDBReplicatorEnv } from "@lib/replication/couchdb/LiveSyncReplicator"; -import type { CheckPointInfo } from "@lib/replication/journal/JournalSyncTypes"; -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv"; -import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; -import { AbstractModule } from "./modules/AbstractModule"; -import type { ServiceModules } from "@lib/interfaces/ServiceModule"; -import type { Constructor } from "@lib/common/utils.type"; -export declare class LiveSyncBaseCore implements LiveSyncLocalDBEnv, LiveSyncReplicatorEnv, LiveSyncJournalReplicatorEnv, LiveSyncCouchDBReplicatorEnv, HasSettings { - addOns: TCommands[]; - /** - * register an add-onn to the plug-in. - * Add-ons are features that are not essential to the core functionality of the plugin, - * @param addOn - */ - private _registerAddOn; - /** - * Get an add-on by its class name. Returns undefined if not found. - * @param cls - * @returns - */ - getAddOn(cls: string): T | undefined; - constructor(serviceHub: InjectableServiceHub, serviceModuleInitialiser: (core: LiveSyncBaseCore, serviceHub: InjectableServiceHub) => ServiceModules, extraModuleInitialiser: (core: LiveSyncBaseCore) => AbstractModule[], addOnsInitialiser: (core: LiveSyncBaseCore) => TCommands[], featuresInitialiser: (core: LiveSyncBaseCore) => void); - /** - * The service hub for managing all services. - */ - _services: InjectableServiceHub | undefined; - get services(): InjectableServiceHub; - /** - * Service Modules - */ - protected _serviceModules: ServiceModules; - get serviceModules(): ServiceModules; - /** - * The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc. - */ - private modules; - /** - * Get a module by its class. Throws an error if not found. - * Mostly used for getting SetupManager. - * @param constructor - * @returns - */ - getModule(constructor: Constructor): T; - /** - * Register a module to the plug-in. - * @param module The module to register. - */ - private _registerModule; - registerModules(extraModules?: AbstractModule[]): void; - /** - * Bind module functions to services. - */ - bindModuleFunctions(): void; - /** - * @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user. - */ - get confirm(): Confirm; - /** - * @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in. - */ - get settings(): ObsidianLiveSyncSettings; - /** - * @obsolete Use services.setting.settings instead. Set the settings of the plug-in. - */ - set settings(value: ObsidianLiveSyncSettings); - /** - * @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in. - * @returns The current settings of the plug-in. - */ - getSettings(): ObsidianLiveSyncSettings; - /** - * @obsolete Use services.database.localDatabase instead. The local database instance. - */ - get localDatabase(): import("@lib/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - /** - * @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database. - * @returns The PouchDB database instance. - */ - getDatabase(): PouchDB.Database; - /** - * @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc. - */ - get simpleStore(): SimpleStore; - /** - * @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time. - */ - get replicator(): import("@lib/replication/LiveSyncAbstractReplicator").LiveSyncAbstractReplicator; - /** - * @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc. - */ - get kvDB(): import("./lib/src/interfaces/KeyValueDatabase").KeyValueDatabase; - /** - * Storage Accessor for handling file operations. - * @obsolete Use serviceModules.storageAccess instead. - */ - get storageAccess(): StorageAccess; - /** - * Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc. - * @obsolete Use serviceModules.databaseFileAccess instead. - */ - get databaseFileAccess(): DatabaseFileAccess; - /** - * File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc. - * @obsolete Use serviceModules.fileHandler instead. - */ - get fileHandler(): IFileHandler; - /** - * Rebuilder for handling database rebuilding operations. - * @obsolete Use serviceModules.rebuilder instead. - */ - get rebuilder(): Rebuilder; - /** - * Initialise ServiceFeatures. - * (Please refer `serviceFeatures` for more details) - */ - initialiseServiceFeatures(): void; -} -export interface IMinimumLiveSyncCommands { - onunload(): void; - onload(): void | Promise; - constructor: { - name: string; - }; -} diff --git a/_types/src/common/KeyValueDB.d.ts b/_types/src/common/KeyValueDB.d.ts deleted file mode 100644 index 4658cdba..00000000 --- a/_types/src/common/KeyValueDB.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts"; -export { OpenKeyValueDatabase } from "./KeyValueDBv2.ts"; -export declare const _OpenKeyValueDatabase: (dbKey: string) => Promise; diff --git a/_types/src/common/KeyValueDBv2.d.ts b/_types/src/common/KeyValueDBv2.d.ts deleted file mode 100644 index cf86dab1..00000000 --- a/_types/src/common/KeyValueDBv2.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase"; -import { type IDBPDatabase } from "idb"; -export declare function OpenKeyValueDatabase(dbKey: string): Promise; -export declare class IDBKeyValueDatabase implements KeyValueDatabase { - protected _dbPromise: Promise> | null; - protected dbKey: string; - protected storeKey: string; - protected _isDestroyed: boolean; - protected destroyedPromise: Promise | null; - get isDestroyed(): boolean; - get ensuredDestroyed(): Promise; - getIsReady(): Promise; - protected ensureDB(): Promise>; - protected closeDB(setDestroyed?: boolean): Promise; - get DB(): Promise>; - constructor(dbKey: string); - get(key: IDBValidKey): Promise; - set(key: IDBValidKey, value: U): Promise; - del(key: IDBValidKey): Promise; - clear(): Promise; - keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise; - close(): Promise; - destroy(): Promise; -} diff --git a/_types/src/common/PeriodicProcessor.d.ts b/_types/src/common/PeriodicProcessor.d.ts deleted file mode 100644 index 0f291d06..00000000 --- a/_types/src/common/PeriodicProcessor.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>; -export declare class PeriodicProcessor { - _process: () => Promise; - _timer?: number; - _core: PeriodicProcessorHost; - constructor(core: PeriodicProcessorHost, process: () => Promise); - process(): Promise; - enable(interval: number): void; - disable(): void; -} -export {}; diff --git a/_types/src/common/SvelteItemView.d.ts b/_types/src/common/SvelteItemView.d.ts deleted file mode 100644 index f32c6632..00000000 --- a/_types/src/common/SvelteItemView.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ItemView } from "@/deps.ts"; -import { type mount } from "svelte"; -export declare abstract class SvelteItemView extends ItemView { - abstract instantiateComponent(target: HTMLElement): ReturnType | Promise>; - component?: ReturnType; - onOpen(): Promise; - _dismountComponent(): Promise; - onClose(): Promise; -} diff --git a/_types/src/common/events.d.ts b/_types/src/common/events.d.ts deleted file mode 100644 index 514419c9..00000000 --- a/_types/src/common/events.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { eventHub } from "@lib/hub/hub"; -export declare const EVENT_PLUGIN_LOADED = "plugin-loaded"; -export declare const EVENT_PLUGIN_UNLOADED = "plugin-unloaded"; -export declare const EVENT_FILE_SAVED = "file-saved"; -export declare const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed"; -export declare const EVENT_REQUEST_OPEN_SETTINGS = "request-open-settings"; -export declare const EVENT_REQUEST_OPEN_SETTING_WIZARD = "request-open-setting-wizard"; -export declare const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; -export declare const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; -export declare const EVENT_REQUEST_SHOW_SETUP_QR = "request-show-setup-qr"; -export declare const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; -export declare const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; -export declare const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor"; -export declare const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete"; -export declare const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage"; -export declare const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3"; -declare global { - interface LSEvents { - [EVENT_PLUGIN_LOADED]: undefined; - [EVENT_PLUGIN_UNLOADED]: undefined; - [EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined; - [EVENT_REQUEST_OPEN_SETTINGS]: undefined; - [EVENT_REQUEST_OPEN_SETTING_WIZARD]: undefined; - [EVENT_REQUEST_RELOAD_SETTING_TAB]: undefined; - [EVENT_LEAF_ACTIVE_CHANGED]: undefined; - [EVENT_REQUEST_OPEN_SETUP_URI]: undefined; - [EVENT_REQUEST_COPY_SETUP_URI]: undefined; - [EVENT_REQUEST_SHOW_SETUP_QR]: undefined; - [EVENT_REQUEST_RUN_DOCTOR]: string; - [EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined; - [EVENT_ANALYSE_DB_USAGE]: undefined; - [EVENT_REQUEST_PERFORM_GC_V3]: undefined; - } -} -export * from "@lib/events/coreEvents.ts"; -export { eventHub }; diff --git a/_types/src/common/obsidianEvents.d.ts b/_types/src/common/obsidianEvents.d.ts deleted file mode 100644 index 53316f35..00000000 --- a/_types/src/common/obsidianEvents.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { TFile } from "@/deps"; -import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types"; -export declare const EVENT_REQUEST_SHOW_HISTORY = "show-history"; -declare global { - interface LSEvents { - [EVENT_REQUEST_SHOW_HISTORY]: { - file: TFile; - fileOnDB: LoadedEntry; - } | { - file: FilePathWithPrefix; - fileOnDB: LoadedEntry; - }; - } -} diff --git a/_types/src/common/reportTool.d.ts b/_types/src/common/reportTool.d.ts deleted file mode 100644 index 9e67a767..00000000 --- a/_types/src/common/reportTool.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; -export declare function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore): Promise<{ - obsidianInfo: { - navigator: string; - fileSystem: string; - }; - responseConfig: Record; - pluginConfig: ObsidianLiveSyncSettings; - manifestVersion: string; - packageVersion: string; -}>; diff --git a/_types/src/common/stores.d.ts b/_types/src/common/stores.d.ts deleted file mode 100644 index f49554d9..00000000 --- a/_types/src/common/stores.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { PersistentMap } from "octagonal-wheels/dataobject/PersistentMap"; -export declare let sameChangePairs: PersistentMap; -export declare function initializeStores(vaultName: string): void; diff --git a/_types/src/common/types.d.ts b/_types/src/common/types.d.ts deleted file mode 100644 index 02d6a40c..00000000 --- a/_types/src/common/types.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type PluginManifest, TFile } from "@/deps.ts"; -import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts"; -export type { CacheData, FileEventItem } from "@lib/common/types.ts"; -export interface PluginDataEntry extends DatabaseEntry { - deviceVaultName: string; - mtime: number; - manifest: PluginManifest; - mainJs: string; - manifestJson: string; - styleCss?: string; - dataJson?: string; - _conflicts?: string[]; - type: "plugin"; -} -export interface PluginList { - [key: string]: PluginDataEntry[]; -} -export interface DevicePluginList { - [key: string]: PluginDataEntry; -} -export declare const PERIODIC_PLUGIN_SWEEP = 60; -export interface InternalFileInfo { - path: FilePath; - mtime: number; - ctime: number; - size: number; - deleted?: boolean; -} -export interface FileInfo { - path: FilePath; - mtime: number; - ctime: number; - size: number; - deleted?: boolean; - file: TFile; -} -export type queueItem = { - entry: EntryBody; - missingChildren: string[]; - timeout?: number; - done?: boolean; - warned?: boolean; -}; -export declare const FileWatchEventQueueMax = 10; -export { configURIBase, configURIBaseQR } from "@lib/common/types.ts"; -export { CHeader, PSCHeader, PSCHeaderEnd, ICHeader, ICHeaderEnd, ICHeaderLength, ICXHeader, } from "@lib/common/models/fileaccess.const.ts"; diff --git a/_types/src/common/utils.d.ts b/_types/src/common/utils.d.ts deleted file mode 100644 index 3e334a4c..00000000 --- a/_types/src/common/utils.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TAbstractFile } from "@/deps.ts"; -import { type AnyEntry, type CouchDBCredentials, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix, type UXFileInfo, type UXFileInfoStub } from "@lib/common/types.ts"; -export { ICHeader, ICXHeader } from "./types.ts"; -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase.ts"; -export { scheduleTask, cancelTask, cancelAllTasks } from "octagonal-wheels/concurrency/task"; -export declare function path2id(filename: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise; -export declare function id2path(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix; -export declare function getPathFromTFile(file: TAbstractFile): FilePath; -import { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo } from "@lib/common/typeUtils.ts"; -export { isInternalFile, getPathFromUXFileInfo, getStoragePathFromUXFileInfo, getDatabasePathFromUXFileInfo }; -export declare function memoObject(key: string, obj: T): T; -export declare function memoIfNotExist(key: string, func: () => T | Promise): Promise; -export declare function retrieveMemoObject(key: string): T | false; -export declare function disposeMemoObject(key: string): void; -export declare function isValidPath(filename: string): boolean; -export declare function trimPrefix(target: string, prefix: string): string; -export { isInternalMetadata, id2InternalMetadataId, isChunk, isCustomisationSyncMetadata, isPluginMetadata, stripInternalMetadataPrefix, } from "@lib/common/typeUtils.ts"; -export declare const _requestToCouchDBFetch: (baseUri: string, username: string, password: string, path?: string, body?: unknown, method?: string) => Promise; -export declare const _requestToCouchDB: (baseUri: string, credentials: CouchDBCredentials, origin: string, path?: string, body?: unknown, method?: string, customHeaders?: Record) => Promise; -/** - * @deprecated Use requestToCouchDBWithCredentials instead. - */ -export declare const requestToCouchDB: (baseUri: string, username: string, password: string, origin?: string, key?: string, body?: string, method?: string, customHeaders?: Record) => Promise; -export declare function requestToCouchDBWithCredentials(baseUri: string, credentials: CouchDBCredentials, origin?: string, key?: string, body?: string, method?: string, customHeaders?: Record): Promise; -import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts"; -export { BASE_IS_NEW, EVEN, TARGET_IS_NEW }; -import { compareMTime } from "@lib/common/utils.ts"; -export { compareMTime }; -export declare function markChangesAreSame(file: AnyEntry | string | UXFileInfoStub, mtime1: number, mtime2: number): true | undefined; -export declare function unmarkChanges(file: AnyEntry | string | UXFileInfoStub): void; -export declare function isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | string, mtimes: number[]): typeof EVEN | undefined; -export declare function compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; -export type MemoOption = { - key: string; - forceUpdate?: boolean; - validator?: (context: Map) => boolean; -}; -export declare function useMemo({ key, forceUpdate, validator }: MemoOption, updateFunc: (context: Map, prev: T) => T): T; -export declare function useStatic(key: string): { - value: T | undefined; -}; -export declare function useStatic(key: string, initial: T): { - value: T; -}; -export declare function disposeMemo(key: string): void; -export declare function disposeAllMemo(): void; -export declare function getLogLevel(showNotice: boolean): 32 | 64; -export type MapLike = { - set(key: K, value: V): Map; - clear(): void; - delete(key: K): boolean; - get(key: K): V | undefined; - has(key: K): boolean; - keys: () => IterableIterator; - get size(): number; -}; -export declare function autosaveCache(db: KeyValueDatabase, mapKey: string): Promise>; -export declare function onlyInNTimes(n: number, proc: (progress: number) => unknown): () => void; -export { displayRev } from "@lib/common/utils.ts"; diff --git a/_types/src/deps.d.ts b/_types/src/deps.d.ts deleted file mode 100644 index 5bf2cb25..00000000 --- a/_types/src/deps.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePath } from "@lib/common/types.ts"; -export { addIcon, App, debounce, Editor, FuzzySuggestModal, MarkdownRenderer, MarkdownView, Modal, Notice, Platform, Plugin, PluginSettingTab, requestUrl, sanitizeHTMLToDom, Setting, stringifyYaml, TAbstractFile, TextAreaComponent, TFile, TFolder, parseYaml, ItemView, WorkspaceLeaf, Menu, request, getLanguage, ButtonComponent, TextComponent, ToggleComponent, DropdownComponent, Component, } from "obsidian"; -export type { DataWriteOptions, PluginManifest, RequestUrlParam, RequestUrlResponse, MarkdownFileInfo, ListedFiles, ValueComponent, Stat, Command, ViewCreator, } from "obsidian"; -declare const normalizePath: (from: T) => T; -export { normalizePath }; -export { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; diff --git a/_types/src/features/ConfigSync/CmdConfigSync.d.ts b/_types/src/features/ConfigSync/CmdConfigSync.d.ts deleted file mode 100644 index 30c5eb94..00000000 --- a/_types/src/features/ConfigSync/CmdConfigSync.d.ts +++ /dev/null @@ -1,147 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type PluginManifest } from "@/deps.ts"; -import type { EntryDoc, LoadedEntry, FilePathWithPrefix, FilePath, AnyEntry } from "@lib/common/types.ts"; -import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts"; -import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts"; -import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import { PluginDialogModal } from "./PluginDialogModal.ts"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - DISABLE: "DISABLE"; - CUSTOMIZE: "CUSTOMIZE"; - DISABLE_CUSTOM: "DISABLE_CUSTOM"; - } -} -export declare const pluginList: import("svelte/store").Writable; -export declare const pluginIsEnumerating: import("svelte/store").Writable; -export declare const pluginV2Progress: import("svelte/store").Writable; -export type PluginDataExFile = { - filename: string; - data: string[]; - mtime: number; - size: number; - version?: string; - hash?: string; - displayName?: string; -}; -export interface IPluginDataExDisplay { - documentPath: FilePathWithPrefix; - category: string; - name: string; - term: string; - displayName?: string; - files: (LoadedEntryPluginDataExFile | PluginDataExFile)[]; - version?: string; - mtime: number; -} -export type PluginDataExDisplay = { - documentPath: FilePathWithPrefix; - category: string; - name: string; - term: string; - displayName?: string; - files: PluginDataExFile[]; - version?: string; - mtime: number; -}; -type LoadedEntryPluginDataExFile = LoadedEntry & PluginDataExFile; -export declare const pluginManifests: Map; -export declare const pluginManifestStore: import("svelte/store").Writable>; -export declare class PluginDataExDisplayV2 { - documentPath: FilePathWithPrefix; - category: string; - term: string; - files: LoadedEntryPluginDataExFile[]; - name: string; - confKey: string; - constructor(data: IPluginDataExDisplay); - setFile(file: LoadedEntryPluginDataExFile): Promise; - deleteFile(filename: string): void; - _displayName: string | undefined; - _version: string | undefined; - applyLoadedManifest(): void; - get displayName(): string; - get version(): string | undefined; - get mtime(): number; -} -export type PluginDataEx = { - documentPath?: FilePathWithPrefix; - category: string; - name: string; - displayName?: string; - term: string; - files: PluginDataExFile[]; - version?: string; - mtime: number; -}; -export declare class ConfigSync extends LiveSyncCommands { - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore); - get configDir(): string; - get kvDB(): import("../../lib/src/interfaces/KeyValueDatabase.ts").KeyValueDatabase; - get useV2(): boolean; - get useSyncPluginEtc(): boolean; - isThisModuleEnabled(): boolean; - pluginDialog?: PluginDialogModal; - periodicPluginSweepProcessor: PeriodicProcessor; - pluginList: IPluginDataExDisplay[]; - showPluginSyncModal(): void; - hidePluginSyncModal(): void; - onunload(): void; - addRibbonIcon: (icon: string, title: string, callback: (evt: MouseEvent) => unknown) => HTMLElement; - onload(): void; - getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | ""; - isTargetPath(filePath: string): boolean; - private _everyOnDatabaseInitialized; - _everyBeforeReplicate(showNotice: boolean): Promise; - _everyOnResumeProcess(): Promise; - _everyAfterResumeProcess(): Promise; - reloadPluginList(showMessage: boolean): Promise; - loadPluginData(path: FilePathWithPrefix): Promise; - pluginScanProcessor: QueueProcessor; - pluginScanProcessorV2: QueueProcessor; - filenameToUnifiedKey(path: string, termOverRide?: string): FilePathWithPrefix; - filenameWithUnifiedKey(path: string, termOverRide?: string): FilePathWithPrefix; - unifiedKeyPrefixOfTerminal(termOverRide?: string): FilePathWithPrefix; - parseUnifiedPath(unifiedPath: FilePathWithPrefix): { - category: string; - device: string; - key: string; - filename: string; - pathV1: FilePathWithPrefix; - }; - loadedManifest_mTime: Map; - createPluginDataExFileV2(unifiedPathV2: FilePathWithPrefix, loaded?: LoadedEntry): Promise; - createPluginDataFromV2(unifiedPathV2: FilePathWithPrefix): PluginDataExDisplayV2 | undefined; - updatingV2Count: number; - updatePluginListV2(showMessage: boolean, unifiedFilenameWithKey: FilePathWithPrefix): Promise; - migrateV1ToV2(showMessage: boolean, entry: AnyEntry): Promise; - updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise; - compareUsingDisplayData(dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean): Promise; - applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise; - applyData(data: IPluginDataExDisplay, content?: string): Promise; - deleteData(data: PluginDataEx): Promise; - _anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument): Promise; - _everyRealizeSettingSyncMode(): Promise; - recentProcessedInternalFiles: string[]; - makeEntryFromFile(path: FilePath): Promise; - storeCustomisationFileV2(path: FilePath, term: string, force?: boolean): Promise; - storeCustomizationFiles(path: FilePath, termOverRide?: string): Promise; - _anyProcessOptionalFileEvent(path: FilePath): Promise; - watchVaultRawEventsAsync(path: FilePath): Promise; - scanAllConfigFiles(showMessage: boolean): Promise; - deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite?: boolean): Promise; - scanInternalFiles(): Promise; - private _allAskUsingOptionalSyncFeature; - private __askHiddenFileConfiguration; - _anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise; - private _allSuspendExtraSync; - private _allConfigureOptionalSyncFeature; - configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES): Promise; - getFiles(path: string, lastDepth: number): Promise; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} -export {}; diff --git a/_types/src/features/ConfigSync/PluginDialogModal.d.ts b/_types/src/features/ConfigSync/PluginDialogModal.d.ts deleted file mode 100644 index d0fcf679..00000000 --- a/_types/src/features/ConfigSync/PluginDialogModal.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { mount } from "svelte"; -import { App, Modal } from "@/deps.ts"; -import ObsidianLiveSyncPlugin from "@/main.ts"; -export declare class PluginDialogModal extends Modal { - plugin: ObsidianLiveSyncPlugin; - component: ReturnType | undefined; - isOpened(): boolean; - constructor(app: App, plugin: ObsidianLiveSyncPlugin); - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/features/HiddenFileCommon/JsonResolveModal.d.ts b/_types/src/features/HiddenFileCommon/JsonResolveModal.d.ts deleted file mode 100644 index 992829d4..00000000 --- a/_types/src/features/HiddenFileCommon/JsonResolveModal.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Modal } from "@/deps.ts"; -import { type FilePath, type LoadedEntry } from "@lib/common/types.ts"; -import { mount } from "svelte"; -export declare class JsonResolveModal extends Modal { - filename: FilePath; - callback?: (keepRev?: string, mergedStr?: string) => Promise; - docs: LoadedEntry[]; - component?: ReturnType; - nameA: string; - nameB: string; - defaultSelect: string; - keepOrder: boolean; - hideLocal: boolean; - title: string; - constructor(app: App, filename: FilePath, docs: LoadedEntry[], callback: (keepRev?: string, mergedStr?: string) => Promise, nameA?: string, nameB?: string, defaultSelect?: string, keepOrder?: boolean, hideLocal?: boolean, title?: string); - UICallback(keepRev?: string, mergedStr?: string): Promise; - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts b/_types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts deleted file mode 100644 index 805773a2..00000000 --- a/_types/src/features/HiddenFileSync/CmdHiddenFileSync.d.ts +++ /dev/null @@ -1,154 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LoadedEntry, type FilePathWithPrefix, type FilePath, type DocumentID, type UXFileInfo, type UXStat, type MetaEntry, type UXDataWriteOptions } from "@lib/common/types.ts"; -import { type InternalFileInfo } from "@/common/types.ts"; -import { type CustomRegExp } from "@lib/common/utils.ts"; -import { type MapLike } from "@/common/utils.ts"; -import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts"; -import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts"; -import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; -import type { LiveSyncCore } from "@/main.ts"; -type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - FETCH: "FETCH"; - OVERWRITE: "OVERWRITE"; - MERGE: "MERGE"; - DISABLE: "DISABLE"; - DISABLE_HIDDEN: "DISABLE_HIDDEN"; - } -} -export declare class HiddenFileSync extends LiveSyncCommands { - isThisModuleEnabled(): boolean; - periodicInternalFileScanProcessor: PeriodicProcessor; - get kvDB(): import("../../lib/src/interfaces/KeyValueDatabase.ts").KeyValueDatabase; - getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise; - onunload(): void; - onload(): void; - private _everyOnDatabaseInitialized; - _everyBeforeReplicate(showNotice: boolean): Promise; - private _everyOnloadAfterLoadSettings; - updateSettingCache(): void; - isReady(): boolean; - performStartupScan(showNotice: boolean): Promise; - _everyOnResumeProcess(): Promise; - _everyRealizeSettingSyncMode(): Promise; - _anyProcessOptionalFileEvent(path: FilePath): Promise; - _anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise; - _anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise; - loadFileWithInfo(path: FilePath): Promise; - _fileInfoLastProcessed: MapLike; - _fileInfoLastKnown: MapLike; - _databaseInfoLastProcessed: MapLike; - statToKey(stat: UXStat | null): string; - docToKey(doc: LoadedEntry | MetaEntry): string; - fileToStatKey(file: FilePath, stat?: UXStat | null): Promise; - updateLastProcessedFile(file: FilePath, keySrc: string | UXStat): void; - updateLastProcessedAsActualFile(file: FilePath, stat?: UXStat | null): Promise; - resetLastProcessedFile(targetFiles: FilePath[] | false): void; - getLastProcessedFileMTime(file: FilePath): number; - getLastProcessedFileKey(file: FilePath): string | undefined; - getLastProcessedDatabaseKey(file: FilePath): string | undefined; - updateLastProcessedDatabase(file: FilePath, keySrc: string | MetaEntry | LoadedEntry): void; - updateLastProcessed(path: FilePath, db: MetaEntry | LoadedEntry, stat: UXStat): void; - updateLastProcessedDeletion(path: FilePath, db: MetaEntry | LoadedEntry | false): void; - ensureDir(path: FilePath): Promise; - writeFile(path: FilePath, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - __removeFile(path: FilePath): Promise<"OK" | "ALREADY" | false>; - triggerEvent(path: FilePath): Promise; - updateLastProcessedAsActualDatabase(file: FilePath, doc?: MetaEntry | LoadedEntry | null | false): Promise; - resetLastProcessedDatabase(targetFiles: FilePath[] | false): void; - adoptCurrentStorageFilesAsProcessed(targetFiles: FilePath[] | false): Promise; - adoptCurrentDatabaseFilesAsProcessed(targetFiles: FilePath[] | false): Promise; - semaphore: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject; - serializedForEvent(file: FilePath, fn: () => Promise): Promise; - useStorageFiles(files: FilePath[], showNotice?: boolean, onlyNew?: boolean): Promise; - trackScannedStorageChanges(processFiles: FilePath[], showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeleted?: boolean): Promise; - scanAllStorageChanges(showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeleted?: boolean): Promise; - /** - * check the file is changed or not, and if changed, process it. - */ - trackStorageFileModification(path: FilePath, onlyNew?: boolean, forceWrite?: boolean, includeDeleted?: boolean): Promise; - pendingConflictChecks: Set; - queueConflictCheck(path: FilePathWithPrefix): void; - finishConflictCheck(path: FilePathWithPrefix): void; - requeueConflictCheck(path: FilePathWithPrefix): void; - resolveConflictOnInternalFiles(): Promise; - resolveByNewerEntry(id: DocumentID, path: FilePathWithPrefix, currentDoc: MetaEntry, currentRev: string, conflictedRev: string): Promise; - conflictResolutionProcessor: QueueProcessor; - showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise; - getDocProps(doc: LoadedEntry): { - id: DocumentID; - rev: string | undefined; - revDisplay: string; - prefixedPath: FilePathWithPrefix; - path: FilePath; - isDeleted: boolean; - shortenedId: string; - shortenedPath: string; - }; - processReplicationResult(doc: LoadedEntry): Promise; - cacheFileRegExps: Map; - /** - * Parses the regular expression settings for hidden file synchronization. - * @returns An object containing the ignore and target filters. - */ - parseRegExpSettings(): { - ignoreFilter: CustomRegExp[]; - targetFilter: CustomRegExp[]; - }; - /** - * Checks if the target file path matches the defined patterns. - */ - isTargetFileInPatterns(path: string): boolean; - cacheCustomisationSyncIgnoredFiles: Map; - /** - * Gets the list of files ignored for customization synchronization. - * @returns An array of ignored file paths (lowercase). - */ - getCustomisationSynchronizationIgnoredFiles(): string[]; - /** - * Checks if the given path is not ignored by customization synchronization. - * @param path The file path to check. - * @returns True if the path is not ignored; otherwise, false. - */ - isNotIgnoredByCustomisationSync(path: string): boolean; - isHiddenFileSyncHandlingPath(path: FilePath): boolean; - isTargetFile(path: FilePath): Promise; - trackScannedDatabaseChange(processFiles: MetaEntry[], showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeletion?: boolean): Promise; - applyOfflineChanges(showNotice: boolean): Promise; - scanAllDatabaseChanges(showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeletion?: boolean): Promise; - useDatabaseFiles(files: MetaEntry[], showNotice?: boolean, onlyNew?: boolean): Promise; - trackDatabaseFileModification(path: FilePath, headerLine: string, preventDoubleProcess?: boolean, onlyNew?: boolean, meta?: MetaEntry | false, includeDeletion?: boolean): Promise; - queuedNotificationFiles: Set; - notifyConfigChange(): void; - queueNotification(key: FilePath): void; - rebuildMerging(showNotice: boolean, targetFiles?: FilePath[] | false): Promise; - rebuildFromStorage(showNotice: boolean, targetFiles?: FilePath[] | false, onlyNew?: boolean): Promise; - getAllDatabaseFiles(): Promise; - rebuildFromDatabase(showNotice: boolean, targetFiles?: FilePath[] | false, onlyNew?: boolean): Promise; - initialiseInternalFileSync(direction: SyncDirection, showMessage: boolean, targetFilesSrc?: string[] | false): Promise; - __loadBaseSaveData(file: FilePath, includeContent?: boolean): Promise; - storeInternalFileToDatabase(file: InternalFileInfo | UXFileInfo, forceWrite?: boolean): Promise; - deleteInternalFileOnDatabase(filenameSrc: FilePath, forceWrite?: boolean): Promise; - extractInternalFileFromDatabase(storageFilePath: FilePath, force?: boolean, metaEntry?: MetaEntry | LoadedEntry, preventDoubleProcess?: boolean, onlyNew?: boolean, includeDeletion?: boolean): Promise; - __checkIsNeedToWriteFile(storageFilePath: FilePath, content: string | ArrayBuffer): Promise; - __writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise; - __deleteFile(storageFilePath: FilePath): Promise; - private _allAskUsingOptionalSyncFeature; - private __askHiddenFileConfiguration; - private _allSuspendExtraSync; - private _allConfigureOptionalSyncFeature; - configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES): Promise; - scanInternalFileNames(): Promise; - scanInternalFiles(): Promise; - getFiles(path: string, checkFunction: (path: FilePath) => Promise | boolean): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} -export {}; diff --git a/_types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts b/_types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts deleted file mode 100644 index 9c5fe849..00000000 --- a/_types/src/features/HiddenFileSync/configureHiddenFileSyncMode.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe"; -type ConfigureHiddenFileSyncHandlers = { - disable: () => Promise; - enable: () => Promise; - initialise: (direction: HiddenFileSyncDirection) => Promise; -}; -export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled"; -export declare function configureHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES, handlers: ConfigureHiddenFileSyncHandlers): Promise; -export {}; diff --git a/_types/src/features/LiveSyncCommands.d.ts b/_types/src/features/LiveSyncCommands.d.ts deleted file mode 100644 index acc3e4ef..00000000 --- a/_types/src/features/LiveSyncCommands.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type DocumentID, type FilePath, type FilePathWithPrefix, type LOG_LEVEL } from "@lib/common/types.ts"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import type { LiveSyncCore } from "@/main.ts"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts"; -export declare abstract class LiveSyncCommands { - /** - * @deprecated This class is deprecated. Please use core - */ - plugin: ObsidianLiveSyncPlugin; - core: LiveSyncCore; - get app(): import("obsidian").App; - get settings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - get localDatabase(): import("../lib/src/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get services(): import("../lib/src/services/InjectableServices").InjectableServiceHub; - path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise; - getPath(entry: AnyEntry): FilePathWithPrefix; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore); - abstract onunload(): void; - abstract onload(): void | Promise; - _isMainReady(): boolean; - _isMainSuspended(): boolean; - _isDatabaseReady(): boolean; - _log: ReturnType; - _verbose: (msg: unknown, key?: string) => void; - _info: (msg: unknown, key?: string) => void; - _notice: (msg: unknown, key?: string) => void; - _progress: (prefix?: string, level?: LOG_LEVEL) => { - log: (msg: string) => void; - once: (msg: string) => void; - done: (msg?: string) => void; - }; - _debug: (msg: unknown, key?: string) => void; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts b/_types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts deleted file mode 100644 index d249ec1d..00000000 --- a/_types/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type DocumentID, type EntryDoc, type EntryLeaf } from "@lib/common/types"; -import { LiveSyncCommands } from "@/features/LiveSyncCommands"; -type ChunkID = DocumentID; -type NoteDocumentID = DocumentID; -type Rev = string; -type ChunkUsageMap = Map>>; -export declare class LocalDatabaseMaintenance extends LiveSyncCommands { - onunload(): void; - onload(): void | Promise; - allChunks(includeDeleted?: boolean): Promise<{ - used: Set; - existing: Map; - }>; - get database(): PouchDB.Database; - clearHash(): void; - confirm(title: string, message: string, affirmative?: string, negative?: string): Promise; - ensureAvailable(operationName: string): Promise; - /** - * Resurrect deleted chunks that are still used in the database. - */ - resurrectChunks(): Promise; - /** - * Commit deletion of files that are marked as deleted. - * This method makes the deletion permanent, and the files will not be recovered. - * After this, chunks that are used in the deleted files become ready for compaction. - */ - commitFileDeletion(): Promise; - /** - * Commit deletion of chunks that are not used in the database. - * This method makes the deletion permanent, and the chunks will not be recovered if the database run compaction. - * After this, the database can shrink the database size by compaction. - * It is recommended to compact the database after this operation (History should be kept once before compaction). - */ - commitChunkDeletion(): Promise; - /** - * Compact the database. - * This method removes all deleted chunks that are not used in the database. - * Make sure all devices are synchronized before running this method. - */ - markUnusedChunks(): Promise; - removeUnusedChunks(): Promise; - scanUnusedChunks(): Promise<{ - chunkSet: Set; - chunkUsageMap: ChunkUsageMap; - unusedSet: Set; - }>; - /** - * Track changes in the database and update the chunk usage map for garbage collection. - * Note that this only able to perform without Fetch chunks on demand. - */ - trackChanges(fromStart?: boolean, showNotice?: boolean): Promise; - performGC(showingNotice?: boolean): Promise; - analyseDatabase(): Promise; - compactDatabase(): Promise; - gcv3(): Promise; -} -export {}; diff --git a/_types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts b/_types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts deleted file mode 100644 index 34ac1b74..00000000 --- a/_types/src/features/LocalDatabaseMainte/maintenancePrerequisites.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "@lib/common/types"; -type MaintenancePrerequisiteSettings = Pick; -type MaintenancePrerequisiteOptions = { - operationName: string; - settings: MaintenancePrerequisiteSettings; - askSelectStringDialogue: (message: string, buttons: readonly ["Apply and continue", "Cancel"], options: { - title: string; - defaultAction: "Cancel"; - }) => Promise<"Apply and continue" | "Cancel" | false | undefined>; - applyPartial: (settings: Partial, saveImmediately?: boolean) => Promise; -}; -export declare function ensureLocalDatabaseMaintenancePrerequisites({ operationName, settings, askSelectStringDialogue, applyPartial, }: MaintenancePrerequisiteOptions): Promise; -export {}; diff --git a/_types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts deleted file mode 100644 index da6f4994..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2POpenReplicationModal.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Modal } from "@/deps.ts"; -import { mount } from "svelte"; -import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; -export type P2POpenReplicationModalCallback = { - onSync: (peerId: string) => Promise; - onSyncAndClose: (peerId: string) => Promise; -}; -export declare class P2POpenReplicationModal extends Modal { - liveSyncReplicator: LiveSyncTrysteroReplicator; - callback?: P2POpenReplicationModalCallback; - component?: ReturnType; - showResult: boolean; - title: string; - onClosed?: () => void; - rebuildMode: boolean; - constructor(app: App, liveSyncReplicator: LiveSyncTrysteroReplicator, callback?: P2POpenReplicationModalCallback, showResult?: boolean, title?: string, onClosed?: () => void, rebuildMode?: boolean); - onSync(peerId: string): Promise; - onSyncAndClose(peerId: string): Promise; - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts deleted file mode 100644 index 03f6f04c..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2PReplicationUI.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { App } from "@/deps.ts"; -import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; -/** - * Creates an openReplicationUI factory for Obsidian environments. - * Returns a per-replicator closure that opens the P2P Replication modal - * and performs bidirectional sync (pull then push on success). - * - * Usage: - * const factory = createOpenReplicationUI(app); - * useP2PReplicatorFeature(core, factory); - */ -export declare function createOpenReplicationUI(app: App): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise; -/** - * Creates an openRebuildUI factory for Obsidian environments. - * Opens the P2P Replication modal in "rebuild" mode — one-way pull only, - * with setOnSetup / clearOnSetup bracketing the replicateFrom call. - * - * Usage: - * const factory = createOpenRebuildUI(app); - * useP2PReplicatorFeature(core, createOpenReplicationUI(app), factory); - */ -export declare function createOpenRebuildUI(app: App): (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise; diff --git a/_types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts deleted file mode 100644 index c46f105c..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { Menu, WorkspaceLeaf } from "@/deps.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -import { type PeerStatus } from "@lib/replication/trystero/P2PReplicatorPaneCommon.ts"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult"; -export declare const VIEW_TYPE_P2P = "p2p-replicator"; -export declare class P2PReplicatorPaneView extends SvelteItemView { - core: LiveSyncBaseCore; - private _p2pResult; - icon: string; - title: string; - navigation: boolean; - getIcon(): string; - get replicator(): import("../../../lib/src/replication/trystero/LiveSyncTrysteroReplicator").LiveSyncTrysteroReplicator; - replicateFrom(peer: PeerStatus): Promise; - replicateTo(peer: PeerStatus): Promise; - getRemoteConfig(peer: PeerStatus): Promise; - toggleProp(peer: PeerStatus, prop: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand"): Promise; - m?: Menu; - constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams); - getViewType(): string; - getDisplayText(): string; - onClose(): Promise; - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -} diff --git a/_types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts b/_types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts deleted file mode 100644 index 7c3c2ecf..00000000 --- a/_types/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { WorkspaceLeaf } from "@/deps.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult"; -export declare const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status"; -export declare class P2PServerStatusPaneView extends SvelteItemView { - core: LiveSyncBaseCore; - private _p2pResult; - icon: string; - navigation: boolean; - constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, p2pResult: P2PPaneParams); - getIcon(): string; - getViewType(): string; - getDisplayText(): string; - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -} diff --git a/_types/src/lib/src/API/DirectFileManipulator.d.ts b/_types/src/lib/src/API/DirectFileManipulator.d.ts deleted file mode 100644 index 72c0c858..00000000 --- a/_types/src/lib/src/API/DirectFileManipulator.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { DirectFileManipulator } from "./DirectFileManipulatorV2.ts"; -export type { DirectFileManipulatorOptions } from "./DirectFileManipulatorV2.ts"; diff --git a/_types/src/lib/src/API/processSetting.d.ts b/_types/src/lib/src/API/processSetting.d.ts deleted file mode 100644 index 6cfe623c..00000000 --- a/_types/src/lib/src/API/processSetting.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -/** - * Encode settings to a tiny array to encode in QRCode, - * Due to size limitation of QR code, we encode settings as an array instead of object. - * @param settings settings to encode - */ -export declare function encodeSettingsToQRCodeData(settings: ObsidianLiveSyncSettings): string; -/** - * Decode settings from QR code data string - * @param qr data string from QR code - * @returns Decoded settings - */ -export declare function decodeSettingsFromQRCodeData(qr: string): ObsidianLiveSyncSettings; -export declare const enum OutputFormat { - SVG = 0, - ASCII = 1 -} -export interface SplitQRCodeData { - total: number; - parts: string[]; -} -/** - * Encode setting string to QR code in specified format - * @param settingString Setting string to encode - * @param format Output format - */ -export declare function encodeQR(settingString: string, format: OutputFormat): string | SplitQRCodeData; -type ErasureProperties = keyof ObsidianLiveSyncSettings; -/** - * Generate setup URI with encrypted settings - * @param settingString Settings to encode - * @param passphrase Passphrase to encrypt the settings - * @param removeProperties Properties to remove from the settings - * Means these properties will not be included in the generated setup URI, - * See also necessaryErasureProperties for properties that will always be removed. - * @param skipDefaultValue Whether to skip default values - * @returns Generated setup URI - */ -export declare function encodeSettingsToSetupURI(settingString: ObsidianLiveSyncSettings, passphrase: string, removeProperties?: ErasureProperties[], skipDefaultValue?: boolean): Promise; -export declare function decodeSettingsFromSetupURI(uri: string, passphrase: string): Promise; -export {}; diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitter.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitter.d.ts deleted file mode 100644 index 62dbe999..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitter.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Content-Splitter for Self-hosted LiveSync. - * Splits content into manageable chunks for efficient storage and synchronisation. - */ -import { type FilePathWithPrefix } from "@lib/common/types.ts"; -import type { ISettingService } from "@lib/services/base/IService.ts"; -/** - * ContentSplitter interface for splitting content into chunks. - */ -export type SplitOptions = { - blob: Blob; - path: FilePathWithPrefix; - pieceSize: number; - plainSplit: boolean; - minimumChunkSize: number; - useWorker: boolean; - useSegmenter: boolean; -}; -/** - * The maximum size, in bytes, of a document to be processed by the content splitter in the foreground. - */ -export declare const MAX_CHUNKS_SIZE_ON_UI = 1024; -/** - * Options for the content splitter. - */ -export type ContentSplitterOptions = { - settingService: ISettingService; -}; diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts deleted file mode 100644 index c950ce4d..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterBase.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SavingEntry } from "@lib/common/types.ts"; -import { type ContentSplitterOptions, type SplitOptions } from "./ContentSplitter.ts"; -export declare abstract class ContentSplitterCore { - /** - * Options for the content splitter. - * These settings include the chunk splitter version and other configurations. - */ - options: ContentSplitterOptions; - /** - * Task for initialising the content splitter. - * This ensures that the splitter is initialised before any operations are performed. - */ - initialised: Promise | undefined; - /** - * Constructor for the content splitter core. - * @param params Content splitter options - */ - constructor(params: ContentSplitterOptions); - /** - * Initialise the content splitter with the provided options. - * @param options Content splitter options - */ - abstract initialise(options: ContentSplitterOptions): Promise; - /** - * Split the content of the loaded entry into chunks. - * @param entry The loaded entry to be split into chunks - */ - abstract splitContent(entry: SavingEntry): Promise | Generator>; -} -export declare abstract class ContentSplitterBase extends ContentSplitterCore { - initialise(_options: ContentSplitterOptions): Promise; - /** - * Check whether the content splitter is available for the given settings. - * @param setting Content splitter options - * @returns True if the content splitter is available; false otherwise - */ - static isAvailableFor(setting: ContentSplitterOptions): boolean; - /** - * Process the content and split it into chunks. - * @param options Blob content to be split into chunks - */ - abstract processSplit(options: SplitOptions): Promise | Generator>; - getParamsFor(entry: SavingEntry): SplitOptions; - /** - * Split the content of the loaded entry into chunks. - * This method waits for the initialisation task to complete before proceeding. - * @param entry The loaded entry to be split into chunks - * @returns A generator that yields the split chunks - */ - splitContent(entry: SavingEntry): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts deleted file mode 100644 index a2b430be..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterRabinKarp.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ContentSplitterOptions, SplitOptions } from "./ContentSplitter.ts"; -import { ContentSplitterBase } from "./ContentSplitterBase.ts"; -/** - * Rabin-Karp content splitter for efficient chunking - */ -export declare class ContentSplitterRabinKarp extends ContentSplitterBase { - static isAvailableFor(setting: ContentSplitterOptions): boolean; - processSplit(options: SplitOptions): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts deleted file mode 100644 index 0030cfcf..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterV1.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ContentSplitterOptions, SplitOptions } from "./ContentSplitter"; -import { ContentSplitterBase } from "./ContentSplitterBase"; -/** - * Legacy content splitter for version 1. - */ -export declare class ContentSplitterV1 extends ContentSplitterBase { - static isAvailableFor(setting: ContentSplitterOptions): boolean; - processSplit(options: SplitOptions): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts deleted file mode 100644 index 2712a361..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitterV2.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ContentSplitterOptions, SplitOptions } from "./ContentSplitter.ts"; -import { ContentSplitterBase } from "./ContentSplitterBase.ts"; -/** - * Content splitter for version 2, which supports segmenter-based splitting. - */ -export declare class ContentSplitterV2 extends ContentSplitterBase { - static isAvailableFor(setting: ContentSplitterOptions): boolean; - processSplit(options: SplitOptions): Promise | Generator>; -} diff --git a/_types/src/lib/src/ContentSplitter/ContentSplitters.d.ts b/_types/src/lib/src/ContentSplitter/ContentSplitters.d.ts deleted file mode 100644 index 4b42efc2..00000000 --- a/_types/src/lib/src/ContentSplitter/ContentSplitters.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SavingEntry } from "@lib/common/types"; -import type { ContentSplitterOptions } from "./ContentSplitter"; -import { ContentSplitterCore, type ContentSplitterBase } from "./ContentSplitterBase"; -/** - * ContentSplitter class that manages the active content splitter based on the provided settings. - */ -export declare class ContentSplitter extends ContentSplitterCore { - _activeSplitter: ContentSplitterBase; - constructor(options: ContentSplitterOptions); - initialise(options: ContentSplitterOptions): Promise; - splitContent(entry: SavingEntry): Promise | Generator>; -} diff --git a/_types/src/lib/src/UI/svelteDialog.d.ts b/_types/src/lib/src/UI/svelteDialog.d.ts deleted file mode 100644 index ec821394..00000000 --- a/_types/src/lib/src/UI/svelteDialog.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { HasSetResult, HasGetInitialData, ComponentHasResult, GuestDialogProps, DialogSvelteComponentBaseProps, DialogControlBase, } from "@lib/services/implements/base/SvelteDialog.ts"; -export { CONTEXT_DIALOG_CONTROLS, setupDialogContext, getDialogContext, SvelteDialogManagerBase, } from "@lib/services/implements/base/SvelteDialog.ts"; diff --git a/_types/src/lib/src/bureau/bureau.d.ts b/_types/src/lib/src/bureau/bureau.d.ts deleted file mode 100644 index c1d5ec9e..00000000 --- a/_types/src/lib/src/bureau/bureau.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SlipBoard } from "octagonal-wheels/bureau/SlipBoard"; -declare global { - interface Slips extends LSSlips { - _dummy: undefined; - } -} -export declare const globalSlipBoard: SlipBoard; diff --git a/_types/src/lib/src/common/ConnectionString.d.ts b/_types/src/lib/src/common/ConnectionString.d.ts deleted file mode 100644 index 46d8dbf9..00000000 --- a/_types/src/lib/src/common/ConnectionString.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBConnection, BucketSyncSetting, P2PConnectionInfo } from "./models/setting.type"; -export type RemoteConfigurationResult = { - type: "couchdb"; - settings: CouchDBConnection; -} | { - type: "s3"; - settings: BucketSyncSetting; -} | { - type: "p2p"; - settings: P2PConnectionInfo; -} | { - type: "webdav"; - settings: never; -}; -export declare class ConnectionStringParser { - /** - * Restore settings from URI - */ - static parse(uriString: string): RemoteConfigurationResult; - /** - * 設定からURIを生成する - */ - static serialize(config: RemoteConfigurationResult): string; - private static parseCouchDB; - private static serializeCouchDB; - private static parseS3; - private static serializeS3; - private static parseP2P; - private static serializeP2P; -} diff --git a/_types/src/lib/src/common/LSError.d.ts b/_types/src/lib/src/common/LSError.d.ts deleted file mode 100644 index 1068b139..00000000 --- a/_types/src/lib/src/common/LSError.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Constructor } from "@lib/common/utils.type"; -interface ErrorWithCause extends Error { - cause?: unknown; -} -/** - * Error class for Self-hosted LiveSync errors. - * This class extends the base LiveSyncError class and provides additional context for errors related to LiveSync operations. - * It includes a name property and a cause property to capture the original error. - * The status property returns the HTTP status code if available, defaulting to 500 for internal server errors. - * The class also includes static methods to check whether an error is caused by a specific error class. - */ -export declare class LiveSyncError extends Error implements ErrorWithCause { - name: string; - cause?: Error | object | string; - overrideStatus?: number; - /** - * Returns the HTTP status code associated with the error, if available. - * If the error has a status property, it returns that; otherwise, it defaults to 500 (Internal Server Error). - * @returns {number} The HTTP status code. - */ - get status(): number; - /** - * Constructs a new LiveSyncError instance. - * @param message The error message to be displayed. - */ - constructor(message: string, options?: { - cause?: unknown; - status?: number; - }); - /** - * Determines whether an error is caused by a specific error class. - * @param error The error to examine. - * @param errorClass The error class to compare against. - * @returns True if the error is caused by the specified error class; otherwise, false. - * @example - * LiveSyncError.isCausedBy(someSyncParamsFetchError, SyncParamsNotFoundError); // Returns true if the error is caused by SyncParamsNotFoundError; this is usually represented as SyncParamsFetchError at the uppermost layer. - */ - static isCausedBy(error: unknown, errorClass: Constructor): boolean; - /** - * Creates a new instance of the error class from an existing error. - * @param error The error to wrap. - * @returns A new instance of the error class with the original error's message and stack trace. - */ - static fromError(this: T, error: unknown): InstanceType; -} -export declare class LiveSyncFatalError extends LiveSyncError { -} -export {}; diff --git a/_types/src/lib/src/common/configForDoc.d.ts b/_types/src/lib/src/common/configForDoc.d.ts deleted file mode 100644 index 47f531bb..00000000 --- a/_types/src/lib/src/common/configForDoc.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Confirm } from "@lib/interfaces/Confirm"; -import { type ObsidianLiveSyncSettings } from "./types"; -declare enum ConditionType { - PLATFORM_CASE_INSENSITIVE = "platform-case-insensitive", - PLATFORM_CASE_SENSITIVE = "platform-case-sensitive", - REMOTE_CASE_SENSITIVE = "remote-case-sensitive" -} -export declare enum RuleLevel { - Must = 0, - Necessary = 1, - Recommended = 2, - Optional = 3 -} -type BaseRule = { - level?: RuleLevel; - requireRebuild?: boolean; - requireRebuildLocal?: boolean; - recommendRebuild?: boolean; - reason?: string; - reasonFunc?: (settings: Partial) => string; - condition?: ConditionType[]; - detectionFunc?: (settings: Partial) => boolean; - value?: TValue; - valueDisplay?: string; - valueDisplayFunc?: (settings: Partial) => string; - obsoleteValues?: TValue[]; -}; -type NumberRuleExact = BaseRule<"number", number> & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type -type NumberRuleRange = BaseRule<"number", number> & { - min?: number; - max?: number; - step?: number; -}; -type StringRangeRule = BaseRule<"string", string> & { - minLength?: number; - maxLength?: number; - regexp?: string; -}; -type StringRule = BaseRule<"string", string> & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type -type BooleanRule = BaseRule<"boolean", boolean> & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type -export type RuleForType = T extends number ? NumberRuleExact | NumberRuleRange : T extends string ? StringRule | StringRangeRule : T extends boolean ? BooleanRule : never; -type DoctorCheckSettings = Omit, "remoteConfigurations" | "pluginSyncExtendedSetting">; -export type DoctorRegulation = { - version: string; - rules: { - [P in keyof DoctorCheckSettings]: RuleForType; - }; -}; -export declare const DoctorRegulationV0_24_16: DoctorRegulation; -export declare const DoctorRegulationV0_24_30: DoctorRegulation; -export declare const DoctorRegulationV0_25_0: DoctorRegulation; -export declare const DoctorRegulationV0_25_27: DoctorRegulation; -export declare const DoctorRegulation: DoctorRegulation; -export declare function checkUnsuitableValues(setting: Partial, regulation?: DoctorRegulation): DoctorRegulation; -export declare const RebuildOptions: { - readonly AutomaticAcceptable: 0; - readonly ConfirmIfRequired: 1; - readonly SkipEvenIfRequired: 2; -}; -export type RebuildOptionsType = (typeof RebuildOptions)[keyof typeof RebuildOptions]; -export type DoctorOptions = { - localRebuild: RebuildOptionsType; - remoteRebuild: RebuildOptionsType; - activateReason?: string; - forceRescan?: boolean; -}; -export type DoctorResult = { - settings: ObsidianLiveSyncSettings; - shouldRebuild: boolean; - shouldRebuildLocal: boolean; - isModified: boolean; -}; -export type HasConfirm = { - confirm: Confirm; -}; -export declare function performDoctorConsultation(env: HasConfirm, settings: ObsidianLiveSyncSettings, { localRebuild, remoteRebuild, activateReason, forceRescan, }: DoctorOptions): Promise; -export {}; diff --git a/_types/src/lib/src/common/coreEnvFunctions.d.ts b/_types/src/lib/src/common/coreEnvFunctions.d.ts deleted file mode 100644 index b3a2ce4c..00000000 --- a/_types/src/lib/src/common/coreEnvFunctions.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { getLanguage as ObsidianGetLanguage } from "obsidian"; -export declare function setGetLanguage(func: typeof ObsidianGetLanguage): void; -export declare function getLanguage(): string; -export declare const compatGlobal: typeof window; -export type CompatTimeoutHandle = ReturnType | number; -export type CompatIntervalHandle = ReturnType | number; -/** - * A wrapper around the global fetch function to ensure compatibility across different environments. - * In Obsidian, they recommend using their own requestUrl for better performance and reliability. - * However, at least for now, requestUrl cannot handle multiple concurrent requests, which causes - * problems for synchronise lively. So we will use the global fetch for now. - * If the situation changes in the future, change this function to use requestUrl. - * @param {RequestInfo} input The resource that you wish to fetch. Can be either a string or a Request object. - * @param {RequestInit} [init] An options object containing any custom settings that you want to apply to the request. - * @returns {Promise} A Promise that resolves to the Response to that request, whether it is successful or not. - */ -export declare const _fetch: { - (input: RequestInfo | URL, init?: RequestInit): Promise; - (input: RequestInfo | URL, init?: RequestInit): Promise; -} & typeof fetch; -export declare const _activeDocument: Document; diff --git a/_types/src/lib/src/common/coreEnvVars.d.ts b/_types/src/lib/src/common/coreEnvVars.d.ts deleted file mode 100644 index 2b2f7f2a..00000000 --- a/_types/src/lib/src/common/coreEnvVars.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -declare const manifestVersion: string; -declare const packageVersion: string; -export { manifestVersion, packageVersion }; diff --git a/_types/src/lib/src/common/i18n.d.ts b/_types/src/lib/src/common/i18n.d.ts deleted file mode 100644 index 39779329..00000000 --- a/_types/src/lib/src/common/i18n.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AllMessageKeys, I18N_LANGS } from "./rosetta"; -import type { TaggedType } from "./types"; -export declare let currentLang: I18N_LANGS; -export declare function getResolvedLang(lang?: I18N_LANGS): I18N_LANGS; -export declare function isAutoDisplayLanguage(lang: I18N_LANGS): boolean; -export declare function __getMissingTranslations(): string[]; -export declare function __onMissingTranslation(callback: (key: string) => void): void; -export declare function setLang(lang: I18N_LANGS): void; -export declare function $t(message: string, lang?: I18N_LANGS): string; -export declare function translateIfAvailable(message: string, lang?: I18N_LANGS): string; -/** - * TagFunction to Automatically translate. - * @param strings - * @param values - * @returns - */ -export declare function $f(strings: TemplateStringsArray, ...values: string[]): string; -export declare function $msg(key: T, params?: Record, lang?: I18N_LANGS): TaggedType; diff --git a/_types/src/lib/src/common/logger.d.ts b/_types/src/lib/src/common/logger.d.ts deleted file mode 100644 index ae7dd437..00000000 --- a/_types/src/lib/src/common/logger.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export * from "octagonal-wheels/common/logger"; -export type * from "octagonal-wheels/common/logger"; diff --git a/_types/src/lib/src/common/messages/combinedMessages.dev.d.ts b/_types/src/lib/src/common/messages/combinedMessages.dev.d.ts deleted file mode 100644 index 97e9a730..00000000 --- a/_types/src/lib/src/common/messages/combinedMessages.dev.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { PartialMessages as def } from "./def.ts"; -import { type MESSAGE } from "@lib/common/rosetta.ts"; -type MessageKeys = keyof typeof def.def; -export declare const allMessages: { - [key: string]: MESSAGE; -}; -export { type MessageKeys }; diff --git a/_types/src/lib/src/common/messages/combinedMessages.prod.d.ts b/_types/src/lib/src/common/messages/combinedMessages.prod.d.ts deleted file mode 100644 index 8ece8ed9..00000000 --- a/_types/src/lib/src/common/messages/combinedMessages.prod.d.ts +++ /dev/null @@ -1,9266 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const allMessages: { - readonly "(Active)": { - readonly def: "(Active)"; - readonly es: "(Activo)"; - readonly ja: "(有効)"; - readonly ko: "(활성)"; - readonly ru: "(Активна)"; - readonly zh: "(已启用)"; - readonly "zh-tw": "(已啟用)"; - }; - readonly "(BETA) Always overwrite with a newer file": { - readonly def: "(BETA) Always overwrite with a newer file"; - readonly es: "(BETA) Sobrescribir siempre con archivo más nuevo"; - readonly fr: "(BÊTA) Toujours écraser avec un fichier plus récent"; - readonly he: "(BETA) תמיד לדרוס עם קובץ חדש יותר"; - readonly ja: "(ベータ機能) 常に新しいファイルで上書きする"; - readonly ko: "(베타) 항상 새로운 파일로 덮어쓰기"; - readonly ru: "(БЕТА) Всегда перезаписывать более новым файлом"; - readonly zh: "始终使用更新的文件覆盖(测试版)"; - }; - readonly "(Beta) Use ignore files": { - readonly def: "(Beta) Use ignore files"; - readonly es: "(Beta) Usar archivos de ignorar"; - readonly fr: "(Bêta) Utiliser les fichiers d'exclusion"; - readonly he: "(בטא) שימוש בקבצי התעלמות"; - readonly ja: "(ベータ機能) 除外ファイル(ignore)の使用"; - readonly ko: "(베타) 제외 규칙 파일 사용"; - readonly ru: "(Бета) Использовать файлы игнорирования"; - readonly zh: "(测试版)使用忽略文件"; - }; - readonly "(Days passed, 0 to disable automatic-deletion)": { - readonly def: "(Days passed, 0 to disable automatic-deletion)"; - readonly es: "(Días transcurridos, 0 para desactivar)"; - readonly fr: "(Jours écoulés, 0 pour désactiver la suppression automatique)"; - readonly he: "(ימים שעברו; 0 לביטול מחיקה אוטומטית)"; - readonly ja: "(経過日数、0で自動削除を無効化)"; - readonly ko: "(지난 일수, 0으로 설정하면 자동 삭제 비활성화)"; - readonly ru: "(Дней прошло, 0 для отключения автоматического удаления)"; - readonly zh: "(已过天数,0为禁用自动删除)"; - }; - readonly "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": { - readonly def: "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended."; - readonly es: "(Ej: Leer chunks online) Lee chunks directamente en línea. Aumente tamaño de chunks personalizados"; - readonly fr: "(ex. Lire les fragments en ligne) Si cette option est activée, LiveSync lit les fragments directement en ligne au lieu de les répliquer localement. L'augmentation de la taille personnalisée des fragments est recommandée."; - readonly he: "(לדוגמה: קריאת נתחים אונליין) אם אפשרות זו מופעלת, LiveSync קורא נתחים ישירות מהשרת מבלי לשכפל אותם מקומית. מומלץ להגדיל את גודל הנתח המותאם אישית."; - readonly ja: "(例: チャンクをオンラインで読む) このオプションを有効にすると、LiveSyncはチャンクをローカルに複製せず、直接オンラインで読み込みます。カスタムチャンクサイズを増やすことをお勧めします。"; - readonly ko: "(예: 청크를 원격에서 읽음) 이 옵션을 활성화하면, LiveSync는 청크를 로컬에 복제하지 않고 원격에서 직접 읽습니다. 커스텀 청크 크기를 키우는 것을 권장합니다."; - readonly ru: "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended."; - readonly zh: "(例如,在线读取块)如果启用此选项,LiveSync 将直接在线读取块,而不是在本地复制块。建议增加自定义块大小"; - }; - readonly "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": { - readonly def: "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used."; - readonly es: "(MB) Saltar cambios en archivos locales/remotos mayores a este tamaño. Si se reduce, se usará versión nueva"; - readonly fr: "(Mo) Si cette valeur est définie, les modifications des fichiers locaux et distants plus grands que cette taille seront ignorées. Si le fichier redevient plus petit, une version plus récente sera utilisée."; - readonly he: "(MB) אם ערך זה מוגדר, שינויים בקבצים מקומיים ומרוחקים הגדולים מגודל זה יידלגו. אם הקובץ יקטן שוב, ייעשה שימוש בגרסה החדשה יותר."; - readonly ja: "(MB) この値を設定すると、これより大きいサイズのローカルファイルやリモートファイルの変更はスキップされます。ファイルが再び小さくなった場合は、新しいものが使用されます。"; - readonly ko: "(MB) 이 값이 설정되면, 이보다 큰 로컬 및 원격 파일의 변경 사항은 건너뜁니다. 파일이 다시 작아지면 더 새로운 파일이 사용됩니다."; - readonly ru: "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used."; - readonly zh: "(MB)如果设置了此项,大于此大小的本地和远程文件的更改将被跳过。如果文件再次变小,将使用更新的文件"; - }; - readonly "(Mega chars)": { - readonly def: "(Mega chars)"; - readonly es: "(Millones de caracteres)"; - readonly fr: "(Méga caractères)"; - readonly he: "(מגה תווים)"; - readonly ja: "(メガ文字)"; - readonly ko: "(메가 문자)"; - readonly ru: "(Мега символов)"; - readonly zh: "(百万字符)"; - }; - readonly "(Not recommended) If set, credentials will be stored in the file.": { - readonly def: "(Not recommended) If set, credentials will be stored in the file."; - readonly es: "(No recomendado) Almacena credenciales en el archivo"; - readonly fr: "(Non recommandé) Si activé, les identifiants seront stockés dans le fichier."; - readonly he: "(לא מומלץ) אם מוגדר, פרטי הגישה יישמרו בקובץ."; - readonly ja: "(非推奨) 設定した場合、認証情報がファイルに保存されます。"; - readonly ko: "(권장하지 않음) 설정한 경우 자격 증명이 파일에 저장됩니다."; - readonly ru: "(Not recommended) If set, credentials will be stored in the file."; - readonly zh: "(不建议)如果设置,凭据将存储在文件中"; - }; - readonly "(Obsolete) Use an old adapter for compatibility": { - readonly def: "(Obsolete) Use an old adapter for compatibility"; - readonly es: "(Obsoleto) Usar adaptador antiguo"; - readonly fr: "(Obsolète) Utiliser un ancien adaptateur pour la compatibilité"; - readonly he: "(מיושן) שימוש במתאם ישן לתאימות לאחור"; - readonly ja: "(廃止済み)古いアダプターを互換性のために利用"; - readonly ko: "(사용 중단) 호환성을 위해 이전 어댑터 사용"; - readonly ru: "(Устарело) Использовать старый адаптер для совместимости"; - readonly zh: "(已弃用)为兼容性使用旧适配器"; - }; - readonly "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": { - readonly def: "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."; - readonly es: "(RegExp) Déjelo vacío para sincronizar todos los archivos. Defina un filtro como expresión regular para limitar los archivos que se sincronizan."; - readonly ja: "(正規表現)空欄で全ファイルを同期します。正規表現を指定すると、同期対象のファイルを絞り込めます。"; - readonly ko: "(정규식) 비워 두면 모든 파일을 동기화합니다. 정규식을 지정하면 동기화할 파일을 제한할 수 있습니다."; - readonly ru: "(RegExp) Оставьте пустым, чтобы синхронизировать все файлы. Укажите регулярное выражение, чтобы ограничить синхронизируемые файлы."; - readonly zh: "(正则表达式)留空表示同步所有文件。可设置正则表达式来限制需要同步的文件。"; - readonly "zh-tw": "(正則表示式)留空即同步所有檔案。設定正則表示式可限制要同步的檔案。"; - }; - readonly "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": { - readonly def: "(RegExp) If this is set, any changes to local and remote files that match this will be skipped."; - readonly es: "(RegExp) Si se establece, se omitirá cualquier cambio en archivos locales y remotos que coincida con este patrón."; - readonly ja: "(正規表現)設定すると、これに一致するローカル/リモートファイルの変更はすべてスキップされます。"; - readonly ko: "(정규식) 설정하면 이 패턴과 일치하는 로컬 및 원격 파일 변경은 모두 건너뜁니다."; - readonly ru: "(RegExp) Если задано, любые изменения локальных и удалённых файлов, соответствующих этому шаблону, будут пропускаться."; - readonly zh: "(正则表达式)如果已设置,则所有匹配此模式的本地和远端文件变更都会被跳过。"; - readonly "zh-tw": "(正則表示式)若已設定,所有符合此模式的本機與遠端檔案變更都會被略過。"; - }; - readonly "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": { - readonly def: "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch."; - readonly es: "(Seleccione esto si ya utiliza la sincronización en otro ordenador o teléfono). Esta opción es adecuada si desea añadir este dispositivo a una configuración de LiveSync existente。"; - readonly ja: "(別の PC やスマートフォンですでに同期を利用している場合に選択してください。)この端末を既存の LiveSync 構成に追加する場合に適しています。"; - readonly ko: "(다른 컴퓨터나 스마트폰에서 이미 동기화를 사용 중인 경우 선택하세요.) 이 장치를 기존 LiveSync 구성에 추가하려는 경우에 적합합니다。"; - readonly ru: "(Выберите этот вариант, если вы уже используете синхронизацию на другом компьютере или смартфоне.) Он подходит, если вы хотите добавить это устройство к уже существующей конфигурации LiveSync。"; - readonly zh: "(如果你已经在另一台电脑或手机上使用同步,请选择此项。)此选项适合将当前设备加入现有 LiveSync 配置的用户。"; - readonly "zh-tw": "(如果你已經在另一台電腦或手機上使用同步,請選擇此項。)此選項適合將目前裝置加入既有 LiveSync 設定的使用者。"; - }; - readonly "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": { - readonly def: "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch."; - readonly es: "(Seleccione esto si está configurando este dispositivo como el primer dispositivo de sincronización). Esta opción es adecuada si es nuevo en LiveSync y desea configurarlo desde cero。"; - readonly ja: "(この端末を最初の同期端末として設定する場合に選択してください。)LiveSync を初めて利用し、最初から設定したい場合に適しています。"; - readonly ko: "(이 장치를 첫 번째 동기화 장치로 설정하는 경우 선택하세요.) LiveSync를 처음 사용하며 처음부터 설정하려는 경우에 적합합니다。"; - readonly ru: "(Выберите этот вариант, если настраиваете это устройство как первое устройство синхронизации.) Он подходит, если вы впервые используете LiveSync и хотите настроить всё с нуля。"; - readonly zh: "(如果你正在将此设备配置为第一台同步设备,请选择此项。)此选项适合初次使用 LiveSync,并希望从头开始配置的用户。"; - readonly "zh-tw": "(如果你正在將此裝置設定為第一台同步裝置,請選擇此項。)此選項適合初次使用 LiveSync,並希望從頭開始設定的使用者。"; - }; - readonly "> [!INFO]- The connected devices have been detected as follows:\n${devices}": { - readonly def: "> [!INFO]- The connected devices have been detected as follows:\n${devices}"; - readonly ja: "> [!INFO]- 次の接続済みデバイスが検出されました:\n${devices}"; - readonly ko: "> [!INFO]- 다음 연결된 기기가 감지되었습니다:\n${devices}"; - readonly ru: "> [!INFO]- Обнаружены следующие подключённые устройства:\n${devices}"; - readonly zh: "> [!INFO]- 已检测到以下已连接设备:\n${devices}"; - readonly "zh-tw": "> [!INFO]- 已偵測到以下已連線裝置:\n${devices}"; - }; - readonly "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": { - readonly def: "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration."; - readonly es: "Un URI de configuración es una única cadena de texto que contiene la dirección del servidor y los datos de autenticación. Si el script de instalación de su servidor generó un URI, usarlo proporciona una configuración sencilla y segura。"; - readonly ja: "Setup URI は、サーバーアドレスと認証情報を含む 1 本の文字列です。サーバーのインストールスクリプトで生成された URI がある場合は、それを使うと簡単かつ安全に設定できます。"; - readonly ko: "설정 URI는 서버 주소와 인증 정보를 포함한 단일 문자열입니다. 서버 설치 스크립트가 URI를 생성했다면 이를 사용하면 간단하고 안전하게 구성할 수 있습니다。"; - readonly ru: "Setup URI — это одна строка текста, содержащая адрес сервера и данные аутентификации. Если URI был создан скриптом установки сервера, его использование обеспечивает простую и безопасную настройку。"; - readonly zh: "Setup URI 是一段包含服务器地址与认证信息的文本。如果服务器安装脚本已经生成了 URI,使用它可以更简单且更安全地完成配置。"; - readonly "zh-tw": "Setup URI 是一段包含伺服器位址與驗證資訊的文字。如果伺服器安裝腳本已經產生 URI,使用它可以更簡單且更安全地完成設定。"; - }; - readonly "Access Key": { - readonly def: "Access Key"; - readonly es: "Clave de acceso"; - readonly fr: "Clé d'accès"; - readonly he: "מפתח גישה"; - readonly ja: "アクセスキー"; - readonly ko: "액세스 키"; - readonly ru: "Ключ доступа"; - readonly zh: "访问密钥"; - }; - readonly Activate: { - readonly def: "Activate"; - readonly es: "Activar"; - readonly ja: "有効化"; - readonly ko: "활성화"; - readonly ru: "Активировать"; - readonly zh: "启用"; - readonly "zh-tw": "啟用"; - }; - readonly "Active Remote Configuration": { - readonly def: "Active Remote Configuration"; - readonly fr: "Configuration distante active"; - readonly he: "תצורת שרת מרוחק פעיל"; - readonly ru: "Активная удалённая конфигурация"; - readonly zh: "生效中的远程配置"; - readonly "zh-tw": "目前啟用的遠端設定"; - }; - readonly "Add default patterns": { - readonly def: "Add default patterns"; - readonly es: "Añadir patrones predeterminados"; - readonly ja: "デフォルトパターンを追加"; - readonly ko: "기본 패턴 추가"; - readonly ru: "Добавить шаблоны по умолчанию"; - readonly zh: "添加默认模式"; - readonly "zh-tw": "新增預設模式"; - }; - readonly "Add new connection": { - readonly def: "Add new connection"; - readonly es: "Añadir conexión"; - readonly ja: "接続を追加"; - readonly ko: "연결 추가"; - readonly ru: "Добавить подключение"; - readonly zh: "新增连接"; - readonly "zh-tw": "新增連線"; - }; - readonly "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": { - readonly def: "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection."; - readonly ja: "すべてのデバイスで進捗値が同じです(${progress})。デバイスは同期されているようなので、Garbage Collection を続行できます。"; - readonly ko: "모든 기기의 진행 값이 동일합니다(${progress}). 기기들이 동기화된 것으로 보이므로 Garbage Collection을 진행할 수 있습니다."; - readonly ru: "У всех устройств одинаковое значение прогресса (${progress}). Похоже, ваши устройства синхронизированы, и можно продолжать Garbage Collection."; - readonly zh: "所有设备的进度值均相同(${progress})。看起来你的设备已经同步,可以继续执行垃圾回收。"; - readonly "zh-tw": "所有裝置的進度值均相同(${progress})。看起來你的裝置已同步,可以繼續執行垃圾回收。"; - }; - readonly "Always prompt merge conflicts": { - readonly def: "Always prompt merge conflicts"; - readonly es: "Siempre preguntar en conflictos"; - readonly fr: "Toujours demander pour les conflits de fusion"; - readonly he: "תמיד להציג בקשת אישור לקונפליקטי מיזוג"; - readonly ja: "常に競合は手動で解決する"; - readonly ko: "항상 병합 충돌 알림"; - readonly ru: "Всегда запрашивать разрешение конфликтов слияния"; - readonly zh: "始终提示合并冲突"; - readonly "zh-tw": "總是提示合併衝突"; - }; - readonly Analyse: { - readonly def: "Analyse"; - readonly fr: "Analyser"; - readonly he: "ניתוח"; - readonly ru: "Анализировать"; - readonly zh: "立即分析"; - readonly "zh-tw": "分析"; - }; - readonly "Analyse database usage": { - readonly def: "Analyse database usage"; - readonly fr: "Analyser l'utilisation de la base de données"; - readonly he: "ניתוח שימוש במסד נתונים"; - readonly ja: "データベース使用状況を分析"; - readonly ko: "데이터베이스 사용량 분석"; - readonly ru: "Анализ использования базы данных"; - readonly zh: "分析数据库使用情况"; - readonly "zh-tw": "分析資料庫使用情況"; - }; - readonly "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": { - readonly def: "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like."; - readonly fr: "Analyser l'utilisation de la base de données et générer un rapport TSV pour un diagnostic personnel. Vous pouvez coller le rapport généré dans le tableur de votre choix."; - readonly he: "נתח שימוש במסד הנתונים וצור דו\"ח TSV לאבחון עצמי. ניתן להדביק את הדו\"ח שנוצר בכל גיליון אלקטרוני."; - readonly ja: "データベース使用状況を分析し、自分で診断できるよう TSV レポートを生成します。生成したレポートは任意のスプレッドシートに貼り付けて確認できます。"; - readonly ko: "데이터베이스 사용량을 분석하고 직접 진단할 수 있도록 TSV 보고서를 생성합니다. 생성된 보고서는 원하는 스프레드시트에 붙여 넣어 확인할 수 있습니다."; - readonly ru: "Проанализируйте использование базы данных и создайте TSV-отчёт для самостоятельной диагностики. Полученный отчёт можно вставить в любую удобную для вас таблицу."; - readonly zh: "分析数据库使用情况并生成 TSV 报告以供您自行诊断。您可以将生成的报告粘贴到您喜欢的任何电子表格中。"; - readonly "zh-tw": "分析資料庫使用情況並產生 TSV 報告,方便你自行診斷。你可以將產生的報告貼到任何慣用的試算表中查看。"; - }; - readonly "Apply Latest Change if Conflicting": { - readonly def: "Apply Latest Change if Conflicting"; - readonly es: "Aplicar último cambio en conflictos"; - readonly fr: "Appliquer la dernière modification en cas de conflit"; - readonly he: "החל שינוי אחרון בעת קונפליקט"; - readonly ja: "競合がある場合は最新の変更を適用する"; - readonly ko: "충돌 시 최신 변경 사항 적용"; - readonly ru: "Применить последнее изменение при конфликте"; - readonly zh: "如果冲突则应用最新更改"; - readonly "zh-tw": "發生衝突時套用最新變更"; - }; - readonly "Apply preset configuration": { - readonly def: "Apply preset configuration"; - readonly es: "Aplicar configuración predefinida"; - readonly fr: "Appliquer une configuration prédéfinie"; - readonly he: "החל תצורה קבועה מראש"; - readonly ja: "プリセットを適用する"; - readonly ko: "프리셋 구성 적용"; - readonly ru: "Применить предустановленную конфигурацию"; - readonly zh: "应用预设配置"; - readonly "zh-tw": "套用預設配置"; - }; - readonly "Ask a passphrase at every launch": { - readonly def: "Ask a passphrase at every launch"; - readonly es: "Solicitar la frase de contraseña en cada inicio"; - readonly ja: "起動のたびにパスフレーズを確認"; - readonly ko: "시작할 때마다 암호문구 묻기"; - readonly ru: "Запрашивать парольную фразу при каждом запуске"; - readonly zh: "每次启动时询问密码短语"; - readonly "zh-tw": "每次啟動時都詢問密語"; - }; - readonly "Automatically Sync all files when opening Obsidian.": { - readonly def: "Automatically Sync all files when opening Obsidian."; - readonly es: "Sincronizar automáticamente todos los archivos al abrir Obsidian"; - readonly fr: "Synchroniser automatiquement tous les fichiers à l'ouverture d'Obsidian."; - readonly he: "סנכרן את כל הקבצים אוטומטית עם פתיחת Obsidian."; - readonly ja: "Obsidian起動時にすべてのファイルを自動同期します。"; - readonly ko: "Obsidian을 열 때 모든 파일을 자동으로 동기화합니다."; - readonly ru: "Автоматически синхронизировать все файлы при открытии Obsidian."; - readonly zh: "打开 Obsidian 时自动同步所有文件"; - readonly "zh-tw": "開啟 Obsidian 時自動同步所有檔案。"; - }; - readonly Back: { - readonly def: "Back"; - readonly es: "Volver"; - readonly ja: "戻る"; - readonly ko: "뒤로"; - readonly ru: "Назад"; - readonly zh: "返回"; - readonly "zh-tw": "返回"; - }; - readonly "Back to non-configured": { - readonly def: "Back to non-configured"; - readonly es: "Volver a no configurado"; - readonly ja: "未設定状態に戻す"; - readonly ko: "미구성 상태로 되돌리기"; - readonly ru: "Вернуть в состояние без настройки"; - readonly zh: "恢复为未配置状态"; - readonly "zh-tw": "恢復為未設定狀態"; - }; - readonly "Batch database update": { - readonly def: "Batch database update"; - readonly es: "Actualización por lotes de BD"; - readonly fr: "Mise à jour groupée de la base de données"; - readonly he: "עדכון אצווה למסד נתונים"; - readonly ja: "データベースのバッチ更新"; - readonly ko: "일괄 데이터베이스 업데이트"; - readonly ru: "Пакетное обновление базы данных"; - readonly zh: "批量数据库更新"; - readonly "zh-tw": "批次更新資料庫"; - }; - readonly "Batch limit": { - readonly def: "Batch limit"; - readonly es: "Límite de lotes"; - readonly fr: "Limite de lot"; - readonly he: "מגבלת אצווה"; - readonly ja: "バッチの上限"; - readonly ko: "일괄 제한"; - readonly ru: "Пакетный лимит"; - readonly zh: "批量限制"; - readonly "zh-tw": "批次上限"; - }; - readonly "Batch size": { - readonly def: "Batch size"; - readonly es: "Tamaño de lote"; - readonly fr: "Taille de lot"; - readonly he: "גודל אצווה"; - readonly ja: "バッチ容量"; - readonly ko: "일괄 크기"; - readonly ru: "Размер пакета"; - readonly zh: "批量大小"; - readonly "zh-tw": "批次大小"; - }; - readonly "Batch size of on-demand fetching": { - readonly def: "Batch size of on-demand fetching"; - readonly es: "Tamaño de lote para obtención bajo demanda"; - readonly fr: "Taille de lot pour la récupération à la demande"; - readonly he: "גודל אצווה במשיכה לפי דרישה"; - readonly ja: "オンデマンド取得のバッチサイズ"; - readonly ko: "필요 시 가져올 청크 묶음 크기"; - readonly ru: "Размер пакета при запросе по требованию"; - readonly zh: "按需获取的批量大小"; - readonly "zh-tw": "按需抓取的批次大小"; - }; - readonly "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": { - readonly def: "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this."; - readonly es: "Antes de v0.17.16 usábamos adaptador antiguo. Nuevo adaptador requiere reconstruir BD local. Desactive cuando pueda"; - readonly fr: "Avant la version v0.17.16, nous utilisions un ancien adaptateur pour la base de données locale. Le nouvel adaptateur est désormais recommandé. Cependant, il nécessite une reconstruction de la base locale. Veuillez désactiver cette option lorsque vous aurez suffisamment de temps. Si elle reste activée, il vous sera également demandé de la désactiver lors de la récupération depuis la base distante."; - readonly he: "לפני גרסה 0.17.16, השתמשנו במתאם ישן למסד הנתונים המקומי. כעת המתאם החדש מועדף. עם זאת, הדבר מצריך בנייה מחדש של מסד הנתונים המקומי. אנא כבה אפשרות זו כשיש לך זמן פנוי. אם תשאיר אותה מופעלת, גם בעת משיכה ממסד הנתונים המרוחק, תתבקש לכבות אותה."; - readonly ja: "v0.17.6までは古いアダプターをローカル用のデータベースに使用していましたが、現在は新しいアダプターを推奨しています。しかし、新しいアダプターに変更するにはローカルデータベースの再構築が必要です。有効のままにしておくと、リモートデータベースからフェッチする場合に、この設定を無効にするかの質問が表示されます。"; - readonly ko: "v0.17.16 이전에는 로컬 데이터베이스에 이전 어댑터를 사용했습니다. 이제는 새로운 어댑터를 권장합니다. 하지만 로컬 데이터베이스 재구축이 필요합니다. 충분한 시간이 있을 때 이 토글을 비활성화해 주세요. 활성화된 상태로 두면 원격 데이터베이스에서 가져올 때도 이를 비활성화하라는 메시지가 나타납니다."; - readonly ru: "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this."; - readonly zh: "在 v0.17.16 之前,我们使用旧适配器作为本地数据库。现在首选新适配器。但是,它需要重建本地数据库。请在有足够时间时禁用此开关。如果保持启用状态,并且在从远程数据库获取时,系统将要求您禁用此开关。"; - }; - readonly "Bucket Name": { - readonly def: "Bucket Name"; - readonly es: "Nombre del bucket"; - readonly fr: "Nom du bucket"; - readonly he: "שם דלי (Bucket)"; - readonly ja: "バケット名"; - readonly ko: "버킷 이름"; - readonly ru: "Имя бакета"; - readonly zh: "存储桶名称"; - readonly "zh-tw": "儲存桶名稱"; - }; - readonly Cancel: { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - readonly "zh-tw": "取消"; - }; - readonly "Cancel Garbage Collection": { - readonly def: "Cancel Garbage Collection"; - readonly ja: "Garbage Collection をキャンセル"; - readonly ko: "Garbage Collection 취소"; - readonly ru: "Отменить Garbage Collection"; - readonly zh: "取消垃圾回收"; - readonly "zh-tw": "取消垃圾回收"; - }; - readonly "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding.": { - readonly def: "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding."; - }; - readonly Check: { - readonly def: "Check"; - readonly fr: "Vérifier"; - readonly he: "בדוק"; - readonly ru: "Проверить"; - readonly zh: "立即检查"; - readonly "zh-tw": "檢查"; - }; - readonly "Check and convert non-path-obfuscated files": { - readonly def: "Check and convert non-path-obfuscated files"; - readonly es: "Comprobar y convertir archivos sin ofuscación de ruta"; - readonly ja: "パス難読化されていないファイルを確認して変換"; - readonly ko: "경로 난독화되지 않은 파일 검사 및 변환"; - readonly ru: "Проверить и преобразовать файлы без обфускации пути"; - readonly zh: "检查并转换未进行路径混淆的文件"; - readonly "zh-tw": "檢查並轉換未進行路徑混淆的檔案"; - }; - readonly "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": { - readonly def: "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary."; - readonly es: "Comprueba los documentos que aún no se hayan convertido a identificadores con ruta ofuscada y conviértelos si es necesario."; - readonly ja: "まだパス難読化 ID に変換されていないドキュメントを確認し、必要に応じて変換します。"; - readonly ko: "아직 경로 난독화 ID로 변환되지 않은 문서를 확인하고 필요하면 변환합니다."; - readonly ru: "Проверяет документы, которые ещё не были преобразованы в path-obfuscated ID, и при необходимости преобразует их."; - readonly zh: "检查尚未转换为路径混淆 ID 的文档,并在需要时将其转换。"; - readonly "zh-tw": "檢查尚未轉換為路徑混淆 ID 的文件,並在需要時進行轉換。"; - }; - readonly "cmdConfigSync.showCustomizationSync": { - readonly def: "Show Customization sync"; - readonly es: "Mostrar sincronización de personalización"; - readonly fr: "Afficher la synchronisation de personnalisation"; - readonly he: "הצג סנכרון התאמה אישית"; - readonly ja: "カスタマイズ同期を表示"; - readonly ko: "사용자 설정 동기화 표시"; - readonly ru: "Показать синхронизацию настроек"; - readonly zh: "显示自定义同步"; - readonly "zh-tw": "顯示自訂同步"; - }; - readonly "Comma separated `.gitignore, .dockerignore`": { - readonly def: "Comma separated `.gitignore, .dockerignore`"; - readonly es: "Separados por comas: `.gitignore, .dockerignore`"; - readonly fr: "Séparés par des virgules `.gitignore, .dockerignore`"; - readonly he: "רשימה מופרדת בפסיקים `.gitignore, .dockerignore`"; - readonly ja: "カンマ区切り `.gitignore, .dockerignore`"; - readonly ko: "쉼표로 구분된 `.gitignore, .dockerignore`"; - readonly ru: "Через запятую `.gitignore, .dockerignore`"; - readonly zh: "用逗号分隔,例如 `.gitignore, .dockerignore`"; - }; - readonly "Compaction in progress on remote database...": { - readonly def: "Compaction in progress on remote database..."; - readonly ja: "リモートデータベースでコンパクションを実行中です..."; - readonly ko: "원격 데이터베이스에서 압축을 진행 중입니다..."; - readonly ru: "Выполняется компакция удалённой базы данных..."; - readonly zh: "正在远程数据库上执行压缩..."; - readonly "zh-tw": "正在遠端資料庫上執行壓縮..."; - }; - readonly "Compaction on remote database completed successfully.": { - readonly def: "Compaction on remote database completed successfully."; - readonly ja: "リモートデータベースでのコンパクションが正常に完了しました。"; - readonly ko: "원격 데이터베이스 압축이 성공적으로 완료되었습니다."; - readonly ru: "Компакция удалённой базы данных успешно завершена."; - readonly zh: "远程数据库压缩已成功完成。"; - readonly "zh-tw": "遠端資料庫壓縮已成功完成。"; - }; - readonly "Compaction on remote database failed.": { - readonly def: "Compaction on remote database failed."; - readonly ja: "リモートデータベースでのコンパクションに失敗しました。"; - readonly ko: "원격 데이터베이스 압축에 실패했습니다."; - readonly ru: "Компакция удалённой базы данных завершилась ошибкой."; - readonly zh: "远程数据库压缩失败。"; - readonly "zh-tw": "遠端資料庫壓縮失敗。"; - }; - readonly "Compaction on remote database timed out.": { - readonly def: "Compaction on remote database timed out."; - readonly ja: "リモートデータベースでのコンパクションがタイムアウトしました。"; - readonly ko: "원격 데이터베이스 압축 시간이 초과되었습니다."; - readonly ru: "Время ожидания компакции удалённой базы данных истекло."; - readonly zh: "远程数据库压缩超时。"; - readonly "zh-tw": "遠端資料庫壓縮逾時。"; - }; - readonly "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": { - readonly def: "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep."; - readonly es: "Compara el contenido de los archivos entre la base de datos local y el almacenamiento. Si no coinciden, se te preguntará cuál deseas conservar."; - readonly ja: "ローカルデータベースとストレージ上のファイル内容を比較します。一致しない場合は、どちらを残すか選択できます。"; - readonly ko: "로컬 데이터베이스와 저장소 간의 파일 내용을 비교합니다. 일치하지 않으면 어떤 쪽을 유지할지 묻게 됩니다."; - readonly ru: "Сравнивает содержимое файлов между локальной базой данных и хранилищем. Если они не совпадут, вам предложат выбрать, какую версию сохранить."; - readonly zh: "比较本地数据库与存储中的文件内容;如果不一致,你将被询问要保留哪一份。"; - readonly "zh-tw": "比較本機資料庫與儲存空間中的檔案內容;若不一致,系統會詢問你要保留哪一份。"; - }; - readonly "Compatibility (Conflict Behaviour)": { - readonly def: "Compatibility (Conflict Behaviour)"; - readonly es: "Compatibilidad (comportamiento de conflictos)"; - readonly ja: "互換性(競合時の挙動)"; - readonly ko: "호환성 (충돌 동작)"; - readonly ru: "Совместимость (поведение при конфликтах)"; - readonly zh: "兼容性(冲突行为)"; - readonly "zh-tw": "相容性(衝突行為)"; - }; - readonly "Compatibility (Database structure)": { - readonly def: "Compatibility (Database structure)"; - readonly es: "Compatibilidad (estructura de la base de datos)"; - readonly ja: "互換性(データベース構造)"; - readonly ko: "호환성 (데이터베이스 구조)"; - readonly ru: "Совместимость (структура базы данных)"; - readonly zh: "兼容性(数据库结构)"; - readonly "zh-tw": "相容性(資料庫結構)"; - }; - readonly "Compatibility (Internal API Usage)": { - readonly def: "Compatibility (Internal API Usage)"; - readonly es: "Compatibilidad (uso de la API interna)"; - readonly ja: "互換性(内部 API の利用)"; - readonly ko: "호환성 (내부 API 사용)"; - readonly ru: "Совместимость (использование внутреннего API)"; - readonly zh: "兼容性(内部 API 使用)"; - readonly "zh-tw": "相容性(內部 API 使用)"; - }; - readonly "Compatibility (Metadata)": { - readonly def: "Compatibility (Metadata)"; - readonly es: "Compatibilidad (metadatos)"; - readonly ja: "互換性(メタデータ)"; - readonly ko: "호환성 (메타데이터)"; - readonly ru: "Совместимость (метаданные)"; - readonly zh: "兼容性(元数据)"; - readonly "zh-tw": "相容性(中繼資料)"; - }; - readonly "Compatibility (Remote Database)": { - readonly def: "Compatibility (Remote Database)"; - readonly es: "Compatibilidad (base de datos remota)"; - readonly ja: "互換性(リモートデータベース)"; - readonly ko: "호환성 (원격 데이터베이스)"; - readonly ru: "Совместимость (удалённая база данных)"; - readonly zh: "兼容性(远端数据库)"; - readonly "zh-tw": "相容性(遠端資料庫)"; - }; - readonly "Compatibility (Trouble addressed)": { - readonly def: "Compatibility (Trouble addressed)"; - readonly es: "Compatibilidad (problemas corregidos)"; - readonly ja: "互換性(対処済みの問題)"; - readonly ko: "호환성 (문제 대응)"; - readonly ru: "Совместимость (исправленные проблемы)"; - readonly zh: "兼容性(问题修复)"; - readonly "zh-tw": "相容性(問題修復)"; - }; - readonly "Compute revisions for chunks": { - readonly def: "Compute revisions for chunks"; - readonly fr: "Calculer les révisions pour les fragments"; - readonly he: "חשב גרסאות לנתחים"; - readonly ja: "チャンクの修正(リビジョン)を計算"; - readonly ko: "청크에 대한 리비전 계산"; - readonly ru: "Вычислять ревизии для чанков"; - readonly zh: "为 chunks 计算修订版本(以前的行为)"; - }; - readonly "Configuration Encryption": { - readonly def: "Configuration Encryption"; - readonly es: "Cifrado de configuración"; - readonly ja: "設定の暗号化"; - readonly ko: "구성 암호화"; - readonly ru: "Шифрование конфигурации"; - readonly zh: "配置加密"; - readonly "zh-tw": "設定加密"; - }; - readonly Configure: { - readonly def: "Configure"; - readonly es: "Configurar"; - readonly ja: "設定"; - readonly ko: "설정"; - readonly ru: "Настроить"; - readonly zh: "配置"; - readonly "zh-tw": "設定"; - }; - readonly "Configure And Change Remote": { - readonly def: "Configure And Change Remote"; - readonly es: "Configurar y cambiar remoto"; - readonly ja: "リモートを設定して切り替える"; - readonly ko: "원격 구성 및 변경"; - readonly ru: "Настроить и переключить удалённое хранилище"; - readonly zh: "配置并切换远端"; - readonly "zh-tw": "設定並切換遠端"; - }; - readonly "Configure E2EE": { - readonly def: "Configure E2EE"; - readonly es: "Configurar E2EE"; - readonly ja: "E2EE を設定"; - readonly ko: "E2EE 구성"; - readonly ru: "Настроить сквозное шифрование"; - readonly zh: "配置 E2EE"; - readonly "zh-tw": "設定 E2EE"; - }; - readonly "Configure Remote": { - readonly def: "Configure Remote"; - readonly es: "Configurar remoto"; - readonly ja: "リモートを設定"; - readonly ko: "원격 구성"; - readonly ru: "Настроить удалённое хранилище"; - readonly zh: "配置远端"; - readonly "zh-tw": "設定遠端"; - }; - readonly "Configure the same server information as your other devices again, manually, very advanced users only.": { - readonly def: "Configure the same server information as your other devices again, manually, very advanced users only."; - readonly es: "Configure manualmente la misma información del servidor que en sus otros dispositivos. Solo para usuarios muy avanzados。"; - readonly ja: "他の端末と同じサーバー情報を手動で再入力します。上級者向けの方法です。"; - readonly ko: "다른 장치와 동일한 서버 정보를 다시 수동으로 입력합니다. 고급 사용자 전용입니다。"; - readonly ru: "Снова вручную укажите те же параметры сервера, что и на других устройствах. Только для очень опытных пользователей。"; - readonly zh: "手动重新输入与你其他设备相同的服务器信息。仅适合高级用户。"; - readonly "zh-tw": "手動重新輸入與其他裝置相同的伺服器資訊。僅適合進階使用者。"; - }; - readonly "Connection Method": { - readonly def: "Connection Method"; - readonly es: "Método de conexión"; - readonly ja: "接続方法"; - readonly ko: "연결 방법"; - readonly ru: "Способ подключения"; - readonly zh: "连接方式"; - readonly "zh-tw": "連線方式"; - }; - readonly "Continue to CouchDB setup": { - readonly def: "Continue to CouchDB setup"; - readonly es: "Continuar con la configuración de CouchDB"; - readonly ja: "CouchDB 設定へ進む"; - readonly ko: "CouchDB 설정으로 계속"; - readonly ru: "Перейти к настройке CouchDB"; - readonly zh: "继续进行 CouchDB 设置"; - readonly "zh-tw": "繼續進行 CouchDB 設定"; - }; - readonly "Continue to Peer-to-Peer only setup": { - readonly def: "Continue to Peer-to-Peer only setup"; - readonly es: "Continuar con la configuración solo Peer-to-Peer"; - readonly ja: "Peer-to-Peer 専用設定へ進む"; - readonly ko: "Peer-to-Peer 전용 설정으로 계속"; - readonly ru: "Перейти к настройке только Peer-to-Peer"; - readonly zh: "继续进行仅 Peer-to-Peer 设置"; - readonly "zh-tw": "繼續進行僅 Peer-to-Peer 設定"; - }; - readonly "Continue to S3/MinIO/R2 setup": { - readonly def: "Continue to S3/MinIO/R2 setup"; - readonly es: "Continuar con la configuración de S3/MinIO/R2"; - readonly ja: "S3/MinIO/R2 設定へ進む"; - readonly ko: "S3/MinIO/R2 설정으로 계속"; - readonly ru: "Перейти к настройке S3/MinIO/R2"; - readonly zh: "继续进行 S3/MinIO/R2 设置"; - readonly "zh-tw": "繼續進行 S3/MinIO/R2 設定"; - }; - readonly Copy: { - readonly def: "Copy"; - readonly es: "Copiar"; - readonly ja: "コピー"; - readonly ko: "복사"; - readonly ru: "Копировать"; - readonly zh: "复制"; - readonly "zh-tw": "複製"; - }; - readonly "Copy Report to clipboard": { - readonly def: "Copy Report to clipboard"; - readonly fr: "Copier le rapport dans le presse-papiers"; - readonly he: "העתק דו\"ח ללוח"; - readonly ja: "レポートをクリップボードにコピー"; - readonly ko: "보고서를 클립보드에 복사"; - readonly ru: "Копировать отчёт в буфер обмена"; - readonly zh: "将报告复制到剪贴板"; - readonly "zh-tw": "將報告複製到剪貼簿"; - }; - readonly "CouchDB Connection Tweak": { - readonly def: "CouchDB Connection Tweak"; - readonly es: "Ajustes de conexión de CouchDB"; - readonly ja: "CouchDB 接続の調整"; - readonly ko: "CouchDB 연결 조정"; - readonly ru: "Настройки подключения CouchDB"; - readonly zh: "CouchDB 连接调优"; - readonly "zh-tw": "CouchDB 連線調校"; - }; - readonly "Cross-platform": { - readonly def: "Cross-platform"; - readonly es: "Multiplataforma"; - readonly ja: "クロスプラットフォーム"; - readonly ko: "크로스 플랫폼"; - readonly ru: "Кроссплатформенные"; - readonly zh: "跨平台"; - readonly "zh-tw": "跨平台"; - }; - readonly "Current adapter: {adapter}": { - readonly def: "Current adapter: {adapter}"; - readonly es: "Adaptador actual: {adapter}"; - readonly ja: "現在のアダプター: {adapter}"; - readonly ko: "현재 어댑터: {adapter}"; - readonly ru: "Текущий адаптер: {adapter}"; - readonly zh: "当前适配器:{adapter}"; - readonly "zh-tw": "目前的適配器:{adapter}"; - }; - readonly "Customization Sync": { - readonly def: "Customization Sync"; - readonly es: "Sincronización de personalización"; - readonly ja: "カスタマイズ同期"; - readonly ko: "사용자 지정 동기화"; - readonly ru: "Синхронизация настроек"; - readonly zh: "自定义同步"; - readonly "zh-tw": "自訂同步"; - }; - readonly "Customization Sync (Beta3)": { - readonly def: "Customization Sync (Beta3)"; - readonly es: "Sincronización de personalización (Beta3)"; - readonly ja: "カスタマイズ同期 (Beta3)"; - readonly ko: "사용자 지정 동기화 (Beta3)"; - readonly ru: "Синхронизация настроек (Beta3)"; - readonly zh: "自定义同步(Beta3)"; - readonly "zh-tw": "自訂同步(Beta3)"; - }; - readonly "Data Compression": { - readonly def: "Data Compression"; - readonly es: "Compresión de datos"; - readonly fr: "Compression des données"; - readonly he: "דחיסת נתונים"; - readonly ja: "データ圧縮"; - readonly ko: "데이터 압축"; - readonly ru: "Сжатие данных"; - readonly zh: "数据压缩"; - readonly "zh-tw": "資料壓縮"; - }; - readonly "Database -> Storage": { - readonly def: "Database -> Storage"; - readonly "zh-tw": "資料庫 -> 儲存空間"; - }; - readonly "Database Adapter": { - readonly def: "Database Adapter"; - readonly es: "Adaptador de base de datos"; - readonly ja: "データベースアダプター"; - readonly ko: "데이터베이스 어댑터"; - readonly ru: "Адаптер базы данных"; - readonly zh: "数据库适配器"; - readonly "zh-tw": "資料庫適配器"; - }; - readonly "Database Name": { - readonly def: "Database Name"; - readonly es: "Nombre de la base de datos"; - readonly fr: "Nom de la base de données"; - readonly he: "שם מסד נתונים"; - readonly ja: "データベース名"; - readonly ko: "데이터베이스 이름"; - readonly ru: "Имя базы данных"; - readonly zh: "数据库名称"; - readonly "zh-tw": "資料庫名稱"; - }; - readonly "Database suffix": { - readonly def: "Database suffix"; - readonly es: "Sufijo de base de datos"; - readonly fr: "Suffixe de la base de données"; - readonly he: "סיומת מסד נתונים"; - readonly ja: "データベースの接尾辞(suffix)"; - readonly ko: "데이터베이스 접미사"; - readonly ru: "Суффикс базы данных"; - readonly zh: "数据库后缀"; - readonly "zh-tw": "資料庫後綴"; - }; - readonly Default: { - readonly def: "Default"; - readonly es: "Predeterminado"; - readonly ja: "デフォルト"; - readonly ko: "기본값"; - readonly ru: "По умолчанию"; - readonly zh: "默认"; - readonly "zh-tw": "預設"; - }; - readonly "Delay conflict resolution of inactive files": { - readonly def: "Delay conflict resolution of inactive files"; - readonly es: "Retrasar resolución de conflictos en archivos inactivos"; - readonly fr: "Différer la résolution des conflits pour les fichiers inactifs"; - readonly he: "עכב פתרון קונפליקטים לקבצים לא פעילים"; - readonly ja: "非アクティブなファイルは、競合解決を先送りする"; - readonly ko: "비활성 파일의 충돌 해결 지연"; - readonly ru: "Отложить разрешение конфликтов для неактивных файлов"; - readonly zh: "推迟解决不活动文件"; - readonly "zh-tw": "延後處理非活動檔案的衝突"; - }; - readonly "Delay merge conflict prompt for inactive files.": { - readonly def: "Delay merge conflict prompt for inactive files."; - readonly es: "Retrasar aviso de fusión para archivos inactivos"; - readonly fr: "Différer l'invite de conflit de fusion pour les fichiers inactifs."; - readonly he: "עכב הצגת בקשת מיזוג לקבצים לא פעילים."; - readonly ja: "非アクティブなファイルの競合解決のプロンプトの表示を遅延させる"; - readonly ko: "비활성 파일의 병합 충돌 프롬프트 지연."; - readonly ru: "Отложить запрос конфликта слияния для неактивных файлов."; - readonly zh: "推迟手动解决不活动文件"; - readonly "zh-tw": "延後顯示非活動檔案的合併衝突提示。"; - }; - readonly Delete: { - readonly def: "Delete"; - readonly es: "Eliminar"; - readonly ja: "削除"; - readonly ko: "삭제"; - readonly ru: "Удалить"; - readonly zh: "删除"; - readonly "zh-tw": "刪除"; - }; - readonly "Delete all customization sync data": { - readonly def: "Delete all customization sync data"; - readonly es: "Eliminar todos los datos de sincronización de personalización"; - readonly ja: "カスタマイズ同期データをすべて削除"; - readonly ko: "모든 사용자 정의 동기화 데이터 삭제"; - readonly ru: "Удалить все данные синхронизации настроек"; - readonly zh: "删除所有自定义同步数据"; - readonly "zh-tw": "刪除所有自訂同步資料"; - }; - readonly "Delete all data on the remote server.": { - readonly def: "Delete all data on the remote server."; - readonly es: "Eliminar todos los datos del servidor remoto."; - readonly ja: "リモートサーバー上のすべてのデータを削除します。"; - readonly ko: "원격 서버의 모든 데이터를 삭제합니다."; - readonly ru: "Удалить все данные на удалённом сервере."; - readonly zh: "删除远端服务器上的所有数据。"; - readonly "zh-tw": "刪除遠端伺服器上的所有資料。"; - }; - readonly "Delete local database to reset or uninstall Self-hosted LiveSync": { - readonly def: "Delete local database to reset or uninstall Self-hosted LiveSync"; - readonly es: "Eliminar la base de datos local para restablecer o desinstalar Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSync をリセットまたはアンインストールするため、ローカルデータベースを削除"; - readonly ko: "Self-hosted LiveSync를 초기화하거나 제거하기 위해 로컬 데이터베이스를 삭제"; - readonly ru: "Удалить локальную базу данных, чтобы сбросить или удалить Self-hosted LiveSync"; - readonly zh: "删除本地数据库以重置或卸载 Self-hosted LiveSync"; - readonly "zh-tw": "刪除本機資料庫以重設或解除安裝 Self-hosted LiveSync"; - }; - readonly "Delete old metadata of deleted files on start-up": { - readonly def: "Delete old metadata of deleted files on start-up"; - readonly es: "Borrar metadatos viejos al iniciar"; - readonly fr: "Supprimer les anciennes métadonnées des fichiers effacés au démarrage"; - readonly he: "מחק מטה-נתונים ישנים של קבצים שנמחקו בעת הפעלה"; - readonly ja: "削除済みデータのメタデータをクリーンナップする"; - readonly ko: "시작 시 삭제된 파일의 오래된 메타데이터 삭제"; - readonly ru: "Удалять старые метаданные удалённых файлов при запуске"; - readonly zh: "启动时删除已删除文件的旧元数据"; - readonly "zh-tw": "啟動時刪除已刪除檔案的舊中繼資料"; - }; - readonly "Delete Remote Configuration": { - readonly def: "Delete Remote Configuration"; - readonly es: "Eliminar configuración remota"; - readonly ja: "リモート設定を削除"; - readonly ko: "원격 구성 삭제"; - readonly ru: "Удалить удалённую конфигурацию"; - readonly zh: "删除远端配置"; - readonly "zh-tw": "刪除遠端設定"; - }; - readonly "Delete remote configuration '{name}'?": { - readonly def: "Delete remote configuration '{name}'?"; - readonly es: "¿Eliminar la configuración remota '{name}'?"; - readonly ja: "リモート設定 '{name}' を削除しますか?"; - readonly ko: "'{name}' 원격 구성을 삭제할까요?"; - readonly ru: "Удалить удалённую конфигурацию '{name}'?"; - readonly zh: "要删除远端配置“{name}”吗?"; - readonly "zh-tw": "要刪除遠端設定「{name}」嗎?"; - }; - readonly desktop: { - readonly def: "desktop"; - readonly es: "equipo de escritorio"; - readonly ja: "デスクトップ"; - readonly ko: "데스크톱"; - readonly ru: "рабочий стол"; - readonly zh: "桌面设备"; - readonly "zh-tw": "桌面裝置"; - }; - readonly Developer: { - readonly def: "Developer"; - readonly es: "Desarrollador"; - readonly ja: "開発者"; - readonly ko: "개발자"; - readonly ru: "Разработчик"; - readonly zh: "开发者"; - readonly "zh-tw": "開發者"; - }; - readonly Device: { - readonly def: "Device"; - readonly ja: "デバイス"; - readonly ko: "기기"; - readonly ru: "Устройство"; - readonly zh: "设备"; - readonly "zh-tw": "裝置"; - }; - readonly "Device name": { - readonly def: "Device name"; - readonly es: "Nombre del dispositivo"; - readonly fr: "Nom de l'appareil"; - readonly he: "שם מכשיר"; - readonly ja: "デバイス名"; - readonly ko: "기기 이름"; - readonly ru: "Имя устройства"; - readonly zh: "设备名称"; - readonly "zh-tw": "裝置名稱"; - }; - readonly "Device Setup Method": { - readonly def: "Device Setup Method"; - readonly es: "Método de configuración del dispositivo"; - readonly ja: "端末の設定方法"; - readonly ko: "장치 설정 방법"; - readonly ru: "Способ настройки устройства"; - readonly zh: "设备设置方式"; - readonly "zh-tw": "裝置設定方式"; - }; - readonly "dialog.yourLanguageAvailable": { - readonly def: "Self-hosted LiveSync had translations for your language, so the %{Display language} setting was enabled.\n\nNote: Not all messages are translated. We are waiting for your contributions!\nNote 2: If you create an Issue, **please revert to Default** and then take screenshots, messages and logs. This can be done in the setting dialogue.\nMay you find it easy to use!"; - readonly fr: "Self-hosted LiveSync dispose d'une traduction pour votre langue, le paramètre %{Display language} a donc été activé.\n\nNote : Tous les messages ne sont pas traduits. Nous attendons vos contributions !\nNote 2 : Si vous créez un ticket, **veuillez revenir à Par défaut** puis prendre des captures d'écran, messages et journaux. Cela peut être fait dans la boîte de dialogue des paramètres.\nBonne utilisation !"; - readonly he: "ל-Self-hosted LiveSync יש תרגום לשפתך, ולכן הגדרת %{Display language} הופעלה.\n\nהערה: לא כל ההודעות מתורגמות. אנחנו ממתינים לתרומותיך!\nהערה 2: אם אתה פותח Issue, **אנא חזור ל-%{lang-def}** ואז צלם צילומי מסך, הודעות ויומנים. ניתן לעשות זאת בדיאלוג ההגדרות.\nנקווה שתמצא/י את הפלאגין נוח לשימוש!"; - readonly ja: "Self-hosted LiveSync に設定されている言語の翻訳がありましたので、インターフェースの表示言語が適用されました。\n\n注意: 全てのメッセージは翻訳されていません。あなたの貢献をお待ちしています!\nGithubにIssueを作成する際には、 インターフェースの表示言語 を一旦 Default に戻してから、スクショやメッセージ、ログを収集してください。これは設定から変更できます。\n\n便利に使用できれば幸いです。"; - readonly ko: "Self-hosted LiveSync에서 귀하의 언어로 번역을 제공하므로 %{Display language} 설정이 활성화되었습니다.\n\n참고: 모든 메시지가 번역되지는 않습니다. 귀하의 기여를 기다리고 있습니다!\n참고 2: 이슈를 생성하는 경우 **Default로 되돌린 후** 스크린샷, 메시지, 로그를 가져와 주세요. 이는 설정 대화 상자에서 할 수 있습니다.\n간편하게 사용하실 수 있었으면 좋겠습니다!"; - readonly ru: "Self-hosted LiveSync имеет переводы для вашего языка, поэтому была включена настройка языка Display language.\n\nПримечание: Не все сообщения переведены. Мы ждём ваших предложений!\nПримечание 2: При создании Issue, пожалуйста, вернитесь к lang-def, затем сделайте скриншоты, сообщения и логи. Это можно сделать в настройках.\nНадеемся, вам будет удобно использовать!"; - readonly zh: "Self-hosted LiveSync已提供您语言的翻译,因此启用了%{Display language}\n\n注意:并非所有消息都已翻译。我们期待您的贡献!\n注意 2:若您创建问题报告, **请切换回Default** ,然后截取屏幕截图、消息和日志,此操作可在设置对话框中完成\n愿您使用顺心!"; - readonly "zh-tw": "Self-hosted LiveSync 已提供你目前語言的翻譯,因此已啟用顯示語言設定。\n\n注意:並非所有訊息都已完成翻譯,歡迎協助補充!\n注意 2:如果你要回報問題,請先切回預設語言,再附上截圖、訊息與日誌。\n\n希望你使用愉快!"; - }; - readonly "dialog.yourLanguageAvailable.btnRevertToDefault": { - readonly def: "Keep Default"; - readonly fr: "Conserver Par défaut"; - readonly he: "השאר %{lang-def}"; - readonly ja: "Keep Default"; - readonly ko: "Default 유지"; - readonly ru: "Оставить lang-def"; - readonly zh: "保持Default"; - readonly "zh-tw": "恢復為預設語言"; - }; - readonly "dialog.yourLanguageAvailable.Title": { - readonly def: " Translation is available!"; - readonly fr: " Une traduction est disponible !"; - readonly he: " תרגום זמין!"; - readonly ja: "翻訳が利用可能です!"; - readonly ko: " 번역을 사용할 수 있습니다!"; - readonly ru: "Доступен перевод!"; - readonly zh: " 翻译可用!"; - readonly "zh-tw": "已提供你的語言翻譯!"; - }; - readonly "Disables all synchronization and restart.": { - readonly def: "Disables all synchronization and restart."; - readonly es: "Desactiva toda la sincronización y reinicia la aplicación."; - readonly ja: "すべての同期を無効にして再起動します。"; - readonly ko: "모든 동기화를 비활성화하고 재시작합니다."; - readonly ru: "Отключает всю синхронизацию и перезапускает приложение."; - readonly zh: "禁用所有同步并重新启动。"; - readonly "zh-tw": "停用所有同步並重新啟動。"; - }; - readonly "Disables logging, only shows notifications. Please disable if you report an issue.": { - readonly def: "Disables logging, only shows notifications. Please disable if you report an issue."; - readonly es: "Desactiva registros, solo muestra notificaciones. Desactívelo si reporta un problema."; - readonly fr: "Désactive la journalisation, n'affiche que les notifications. Veuillez désactiver si vous signalez un problème."; - readonly he: "מכבה רישום יומן, מציג התראות בלבד. אנא כבה אם אתה מדווח על בעיה."; - readonly ja: "ログを無効にし、通知のみを表示します。Issueを報告する場合は無効にしてください。"; - readonly ko: "로깅을 비활성화하고 알림만 표시합니다. 문제를 신고하는 경우 비활성화해 주세요."; - readonly ru: "Отключает логирование, показывает только уведомления. Пожалуйста, отключите при сообщении о проблеме."; - readonly zh: "禁用日志记录,仅显示通知。如果您报告问题,请禁用此选项"; - readonly "zh-tw": "停用日誌記錄,只顯示通知。如果你要回報問題,請關閉此選項。"; - }; - readonly "Display Language": { - readonly def: "Display Language"; - readonly es: "Idioma de visualización"; - readonly fr: "Langue d'affichage"; - readonly he: "שפת תצוגה"; - readonly ja: "インターフェースの表示言語"; - readonly ko: "표시 언어"; - readonly ru: "Язык интерфейса"; - readonly zh: "显示语言"; - readonly "zh-tw": "顯示語言"; - }; - readonly "Display name": { - readonly def: "Display name"; - readonly es: "Nombre para mostrar"; - readonly ja: "表示名"; - readonly ko: "표시 이름"; - readonly ru: "Отображаемое имя"; - readonly zh: "显示名称"; - readonly "zh-tw": "顯示名稱"; - }; - readonly "Do not check configuration mismatch before replication": { - readonly def: "Do not check configuration mismatch before replication"; - readonly es: "No verificar incompatibilidades antes de replicar"; - readonly fr: "Ne pas vérifier les incohérences de configuration avant la réplication"; - readonly he: "אל תבדוק אי-התאמה בתצורה לפני שכפול"; - readonly ja: "サーバーから同期する前に設定の不一致を確認しない"; - readonly ko: "복제 전 구성 불일치 확인 안 함"; - readonly ru: "Не проверять несовпадение конфигурации перед репликацией"; - readonly zh: "在复制前不检查配置不匹配"; - readonly "zh-tw": "複寫前不檢查設定是否不一致"; - }; - readonly "Do not keep metadata of deleted files.": { - readonly def: "Do not keep metadata of deleted files."; - readonly es: "No conservar metadatos de archivos borrados"; - readonly fr: "Ne pas conserver les métadonnées des fichiers supprimés."; - readonly he: "אל תשמור מטה-נתונים של קבצים שנמחקו."; - readonly ja: "削除済みファイルのメタデータを保持しない"; - readonly ko: "삭제된 파일의 메타데이터를 보관하지 않습니다."; - readonly ru: "Не хранить метаданные удалённых файлов."; - readonly zh: "不保留已删除文件的元数据 "; - readonly "zh-tw": "不保留已刪除檔案的中繼資料。"; - }; - readonly "Do not split chunks in the background": { - readonly def: "Do not split chunks in the background"; - readonly es: "No dividir chunks en segundo plano"; - readonly fr: "Ne pas fragmenter en arrière-plan"; - readonly he: "אל תפצל נתחים ברקע"; - readonly ja: "バックグラウンドでチャンクを分割しない"; - readonly ko: "백그라운드에서 청크 분할 안 함"; - readonly ru: "Не разделять чанки в фоновом режиме"; - readonly zh: "不在后台分割 chunks"; - readonly "zh-tw": "不在背景分割 chunks"; - }; - readonly "Do not use internal API": { - readonly def: "Do not use internal API"; - readonly es: "No usar API interna"; - readonly fr: "Ne pas utiliser l'API interne"; - readonly he: "אל תשתמש ב-API פנימי"; - readonly ja: "内部APIを使用しない"; - readonly ko: "내부 API 사용 안 함"; - readonly ru: "Не использовать внутренний API"; - readonly zh: "不使用内部 API"; - readonly "zh-tw": "不使用內部 API"; - }; - readonly "Doctor.Button.DismissThisVersion": { - readonly def: "No, and do not ask again until the next release"; - readonly fr: "Non, et ne plus demander jusqu'à la prochaine version"; - readonly he: "לא, ואל תשאל שוב עד לגרסה הבאה"; - readonly ja: "いいえ、次のリリースまで再度確認しない"; - readonly ko: "아니요, 다음 릴리스까지 다시 묻지 않음"; - readonly ru: "Нет, и не спрашивать до следующего выпуска"; - readonly zh: "拒绝,并且直到下个版本前不再询问"; - }; - readonly "Doctor.Button.Fix": { - readonly def: "Fix it"; - readonly fr: "Corriger"; - readonly he: "תקן"; - readonly ja: "修正する"; - readonly ko: "수정"; - readonly ru: "Исправить"; - readonly zh: "修复"; - }; - readonly "Doctor.Button.FixButNoRebuild": { - readonly def: "Fix it but no rebuild"; - readonly fr: "Corriger mais sans reconstruction"; - readonly he: "תקן ללא בנייה מחדש"; - readonly ja: "修正するが再構築はしない"; - readonly ko: "수정하지만 재구축하지 않음"; - readonly ru: "Исправить без перестроения"; - readonly zh: "修复但不重建"; - }; - readonly "Doctor.Button.No": { - readonly def: "No"; - readonly fr: "Non"; - readonly he: "לא"; - readonly ja: "いいえ"; - readonly ko: "아니요"; - readonly ru: "Нет"; - readonly zh: "拒绝"; - }; - readonly "Doctor.Button.Skip": { - readonly def: "Leave it as is"; - readonly fr: "Laisser tel quel"; - readonly he: "השאר כפי שהוא"; - readonly ja: "そのままにする"; - readonly ko: "그대로 두기"; - readonly ru: "Оставить как есть"; - readonly zh: "保持不变"; - }; - readonly "Doctor.Button.Yes": { - readonly def: "Yes"; - readonly fr: "Oui"; - readonly he: "כן"; - readonly ja: "はい"; - readonly ko: "예"; - readonly ru: "Да"; - readonly zh: "确定"; - }; - readonly "Doctor.Dialogue.Main": { - readonly def: "Hi! Config Doctor has been activated because of ${activateReason}!\nAnd, unfortunately some configurations were detected as potential problems.\nPlease be assured. Let's solve them one by one.\n\nTo let you know ahead of time, we will ask you about the following items.\n\n${issues}\n\nShall we get started?"; - readonly fr: "Bonjour ! Config Doctor a été activé en raison de ${activateReason} !\nEt, malheureusement, certaines configurations ont été détectées comme des problèmes potentiels.\nPas d'inquiétude. Résolvons-les un par un.\n\nPour information, nous allons vous interroger sur les éléments suivants.\n\n${issues}\n\nVoulez-vous commencer ?"; - readonly he: "שלום! רופא התצורה הופעל בגלל ${activateReason}!\nולמרבה הצער, זוהו תצורות שעשויות להיות בעייתיות.\nהיה רגוע/ה. בואו נפתור אותן אחת אחת.\n\nלידיעתך מראש, נשאל אותך על הפריטים הבאים.\n\n${issues}\n\nהאם להתחיל?"; - readonly ja: "こんにちは!${activateReason}のため、設定診断ツールが起動しました!\n残念ながら、いくつかの設定が潜在的な問題として検出されました。\nご安心ください。一つずつ解決していきましょう。\n\n事前にお知らせしますと、以下の項目についてお尋ねします。\n\n${issues}\n\n始めていいですか?"; - readonly ko: "안녕하세요! ${activateReason} 로 인해 구성 진단 마법사가 활성화되었습니다!\n그리고 일부 구성이 잠재적인 문제로 감지되었습니다.\n안심하세요. 하나씩 해결해 봅시다.\n\n대상 항목은 다음과 같습니다.\n\n${issues}\n\n시작하시겠습니까?"; - readonly ru: "Привет! Диагностика настроек активирована из-за activateReason!\nК сожалению, некоторые настройки были обнаружены как потенциальные проблемы.\nНе волнуйтесь. Давайте решим их по очереди.\n\nСообщаем вам заранее, мы спросим о следующих пунктах.\n\nissues\n\nНачнём?"; - readonly zh: "您好!配置医生已根据您的要求启动(感谢您)!!遗憾的是,检测到部分配置存在潜在问题。请放心,我们将逐一解决这些问题。\n\n提前告知您,我们将就以下事项进行确认:\n\n为数据块计算修订版本(此前行为)\n增强块大小\n\n我们开始处理吗?"; - }; - readonly "Doctor.Dialogue.MainFix": { - readonly def: "\n## ${name}\n\n| Current | Ideal |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Recommendation Level:** ${level}\n\n### Why this has been detected?\n\n${reason}\n\n${note}\n\nFix this to the ideal value?"; - readonly fr: "\n## ${name}\n\n| Actuel | Idéal |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Niveau de recommandation :** ${level}\n\n### Pourquoi ceci a-t-il été détecté ?\n\n${reason}\n\n${note}\n\nCorriger à la valeur idéale ?"; - readonly he: "\n## ${name}\n\n| נוכחי | אידאלי |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**רמת המלצה:** ${level}\n\n### מדוע זה זוהה?\n\n${reason}\n\n${note}\n\nלתקן לערך האידאלי?"; - readonly ja: "\n## ${name}\n\n| 現在の値 | 理想値 |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**推奨レベル:** ${level}\n\n### 診断理由?\n\n${reason}\n\n${note}\n\nこれを理想値に修正しますか?"; - readonly ko: "**구성 이름:** `${name}`\n**현재 값:** `${current}`, **이상적인 값:** `${ideal}`\n**권장 수준:** ${level}\n**왜 이것이 감지되었나요?**\n${reason}\n\n\n${note}\n\n이상적인 값으로 수정하시겠습니까?"; - readonly ru: "name\n\n| Текущее | Идеальное |\n|:---:|:---:|\n| current | ideal |\n\n**Уровень рекомендации:** level\n\n### Почему это было обнаружено?\n\nreason\n\nnote\n\nИсправить на идеальное значение?"; - readonly zh: "\n## ${name}\n\n| Current | Ideal |\n|:---:|:---:|\n| ${current} | ${ideal} |\n\n**Recommendation Level:** ${level}\n\n### Why this has been detected?\n\n${reason}\n\n${note}\n\nFix this to the ideal value?"; - }; - readonly "Doctor.Dialogue.Title": { - readonly def: "Self-hosted LiveSync Config Doctor"; - readonly fr: "Docteur Config Self-hosted LiveSync"; - readonly he: "Self-hosted LiveSync Config Doctor"; - readonly ja: "Self-hosted LiveSync 設定診断ツール"; - readonly ko: "Self-hosted LiveSync 구성 진단 마법사"; - readonly ru: "Диагностика Self-hosted LiveSync"; - readonly zh: "Self-hosted LiveSync 配置诊断"; - }; - readonly "Doctor.Dialogue.TitleAlmostDone": { - readonly def: "Almost done!"; - readonly fr: "Presque terminé !"; - readonly he: "כמעט סיימנו!"; - readonly ja: "あと少しです!"; - readonly ko: "거의 완료되었습니다!"; - readonly ru: "Почти готово!"; - readonly zh: "全部完成!"; - }; - readonly "Doctor.Dialogue.TitleFix": { - readonly def: "Fix issue ${current}/${total}"; - readonly fr: "Corriger le problème ${current}/${total}"; - readonly he: "תקן בעיה ${current}/${total}"; - readonly ja: "問題の修正 ${current}/${total}"; - readonly ko: "문제 해결 ${current}/${total}"; - readonly ru: "Исправление проблемы current/total"; - readonly zh: "修复问题 ${current}/${total}"; - }; - readonly "Doctor.Level.Must": { - readonly def: "Must"; - readonly fr: "Obligatoire"; - readonly he: "חובה"; - readonly ja: "必須"; - readonly ko: "필수"; - readonly ru: "Обязательно"; - readonly zh: "必须"; - }; - readonly "Doctor.Level.Necessary": { - readonly def: "Necessary"; - readonly fr: "Nécessaire"; - readonly he: "נדרש"; - readonly ja: "必要"; - readonly ko: "필수"; - readonly ru: "Необходимо"; - readonly zh: "必要"; - }; - readonly "Doctor.Level.Optional": { - readonly def: "Optional"; - readonly fr: "Optionnel"; - readonly he: "אופציונלי"; - readonly ja: "任意"; - readonly ko: "선택사항"; - readonly ru: "Опционально"; - readonly zh: "可选"; - }; - readonly "Doctor.Level.Recommended": { - readonly def: "Recommended"; - readonly fr: "Recommandé"; - readonly he: "מומלץ"; - readonly ja: "推奨"; - readonly ko: "권장"; - readonly ru: "Рекомендуется"; - readonly zh: "推荐"; - }; - readonly "Doctor.Message.NoIssues": { - readonly def: "No issues detected!"; - readonly fr: "Aucun problème détecté !"; - readonly he: "לא זוהו בעיות!"; - readonly ja: "問題は検出されませんでした!"; - readonly ko: "문제가 감지되지 않았습니다!"; - readonly ru: "Проблем не обнаружено!"; - readonly zh: "未发现问题!"; - }; - readonly "Doctor.Message.RebuildLocalRequired": { - readonly def: "Attention! A local database rebuild is required to apply this!"; - readonly fr: "Attention ! Une reconstruction de la base locale est requise pour appliquer ceci !"; - readonly he: "שים לב! נדרשת בנייה מחדש של מסד הנתונים המקומי כדי להחיל זאת!"; - readonly ja: "注意!これを適用するにはローカルデータベースの再構築が必要です!"; - readonly ko: "주의! 이를 적용하려면 로컬 데이터베이스 재구축이 필요합니다!"; - readonly ru: "Внимание! Для применения требуется перестроение локальной базы данных!"; - readonly zh: "注意!需要重建本地数据库以应用此项!"; - }; - readonly "Doctor.Message.RebuildRequired": { - readonly def: "Attention! A rebuild is required to apply this!"; - readonly fr: "Attention ! Une reconstruction est requise pour appliquer ceci !"; - readonly he: "שים לב! נדרשת בנייה מחדש כדי להחיל זאת!"; - readonly ja: "注意!これを適用するには再構築が必要です!"; - readonly ko: "주의! 이를 적용하려면 재구축이 필요합니다!"; - readonly ru: "Внимание! Для применения требуется перестроение!"; - readonly zh: "注意!需要重建才能应用此项!"; - }; - readonly "Doctor.Message.SomeSkipped": { - readonly def: "We left some issues as is. Shall I ask you again on next startup?"; - readonly fr: "Nous avons laissé certains problèmes en l'état. Dois-je vous demander à nouveau au prochain démarrage ?"; - readonly he: "השארנו כמה בעיות כפי שהן. האם לשאול שוב בהפעלה הבאה?"; - readonly ja: "いくつかの問題をそのままにしました。次回起動時に再度確認しますか?"; - readonly ko: "일부 문제를 그대로 두었습니다. 다음 시작 시 다시 질문할까요?"; - readonly ru: "Некоторые проблемы оставлены как есть. Спросить снова при следующем запуске?"; - readonly zh: "我们将某些问题留给了以后处理。是否要在下次启动时再次询问您?"; - }; - readonly "Doctor.RULES.E2EE_V02500.REASON": { - readonly def: "The End-to-End Encryption has got now more robust and faster. Also because, the previous E2EE was found to be compromised in a re-conducted code review. It should be applied as soon as possible. Really apologises for your inconvenience. And, this setting is not forward compatible. All synchronised devices must be updated to v0.25.0 or higher. Rebuilds are not required and will be converted from the new transfer to the new format, However, it is recommended to rebuild whenever possible."; - readonly fr: "Le chiffrement de bout en bout est désormais plus robuste et plus rapide. Aussi parce que le précédent E2EE s'est révélé compromis lors d'une nouvelle revue de code. Il doit être appliqué dès que possible. Nous nous excusons sincèrement pour la gêne occasionnée. Ce paramètre n'est pas compatible avec les versions antérieures. Tous les appareils synchronisés doivent être mis à jour en v0.25.0 ou supérieur. Les reconstructions ne sont pas requises et seront converties du nouveau transfert vers le nouveau format. Il est toutefois recommandé de reconstruire dans la mesure du possible."; - readonly he: "ההצפנה מקצה לקצה עודכנה לגרסה חזקה ומהירה יותר. בנוסף, בסקירת קוד שנערכה מחדש הוסבר שההצפנה הקודמת נפרצת. יש להחיל זאת בהקדם האפשרי. מתנצלים על אי הנוחות. שים לב שהגדרה זו אינה תואמת לאחור. כל המכשירים המסונכרנים חייבים להיות מעודכנים לגרסה 0.25.0 ומעלה. אין צורך בבנייה מחדש והנתונים יומרו בהדרגה לפורמט החדש, אך מומלץ לבנות מחדש בהזדמנות הראשונה."; - readonly ja: "エンドツーエンド暗号化がより堅牢で高速になりました。また、以前のE2EEは再コードレビューにより脆弱性が発見されました。できるだけ早く適用することをお勧めします。ご不便をおかけして申し訳ありません。また、この設定は下位互換性がありません。すべての同期デバイスをv0.25.0以降にアップデートする必要があります。再構築は必須ではなく、新しい転送から新しいフォーマットに変換されますが、可能な限り再構築をお勧めします。"; - readonly ru: "Сквозное шифрование стало более надёжным и быстрым. Предыдущее E2EE было скомпрометировано. Следует применить как можно скорее."; - readonly zh: "The End-to-End Encryption has got now more robust and faster. Also because, the previous E2EE was found to be compromised in a re-conducted code review. It should be applied as soon as possible. Really apologises for your inconvenience. And, this setting is not forward compatible. All synchronised devices must be updated to v0.25.0 or higher. Rebuilds are not required and will be converted from the new transfer to the new format, However, it is recommended to rebuild whenever possible."; - }; - readonly "Document History": { - readonly def: "Document History"; - readonly "zh-tw": "文件歷程"; - }; - readonly Duplicate: { - readonly def: "Duplicate"; - readonly es: "Duplicar"; - readonly ja: "複製"; - readonly ko: "복제"; - readonly ru: "Дублировать"; - readonly zh: "复制"; - readonly "zh-tw": "複製"; - }; - readonly "Duplicate remote": { - readonly def: "Duplicate remote"; - readonly es: "Duplicar remoto"; - readonly ja: "リモート設定を複製"; - readonly ko: "원격 구성 복제"; - readonly ru: "Дублировать удалённую конфигурацию"; - readonly zh: "复制远端配置"; - readonly "zh-tw": "複製遠端設定"; - }; - readonly "E2EE Configuration": { - readonly def: "E2EE Configuration"; - readonly es: "Configuración de E2EE"; - readonly ja: "E2EE 設定"; - readonly ko: "E2EE 구성"; - readonly ru: "Конфигурация сквозного шифрования"; - readonly zh: "E2EE 配置"; - readonly "zh-tw": "E2EE 設定"; - }; - readonly "Edge case addressing (Behaviour)": { - readonly def: "Edge case addressing (Behaviour)"; - readonly es: "Tratamiento de casos límite (comportamiento)"; - readonly ja: "特殊なケースへの対応(動作)"; - readonly ko: "특수 상황 처리 (동작)"; - readonly ru: "Обработка особых случаев (поведение)"; - readonly zh: "边缘情况处理(行为)"; - readonly "zh-tw": "邊緣情況處理(行為)"; - }; - readonly "Edge case addressing (Database)": { - readonly def: "Edge case addressing (Database)"; - readonly es: "Tratamiento de casos límite (base de datos)"; - readonly ja: "特殊なケースへの対応(データベース)"; - readonly ko: "특수 상황 처리 (데이터베이스)"; - readonly ru: "Обработка особых случаев (база данных)"; - readonly zh: "边缘情况处理(数据库)"; - readonly "zh-tw": "邊緣情況處理(資料庫)"; - }; - readonly "Edge case addressing (Processing)": { - readonly def: "Edge case addressing (Processing)"; - readonly es: "Tratamiento de casos límite (procesamiento)"; - readonly ja: "特殊なケースへの対応(処理)"; - readonly ko: "특수 상황 처리 (처리)"; - readonly ru: "Обработка особых случаев (обработка)"; - readonly zh: "边缘情况处理(处理)"; - readonly "zh-tw": "邊緣情況處理(處理)"; - }; - readonly "Emergency restart": { - readonly def: "Emergency restart"; - readonly es: "Reinicio de emergencia"; - readonly ja: "緊急再起動"; - readonly ko: "긴급 재시작"; - readonly ru: "Аварийный перезапуск"; - readonly zh: "紧急重启"; - readonly "zh-tw": "緊急重新啟動"; - }; - readonly "Enable advanced features": { - readonly def: "Enable advanced features"; - readonly es: "Habilitar características avanzadas"; - readonly fr: "Activer les fonctionnalités avancées"; - readonly he: "הפעל תכונות מתקדמות"; - readonly ja: "高度な機能を有効にする"; - readonly ko: "고급 기능 활성화"; - readonly ru: "Включить расширенные функции"; - readonly zh: "启用高级功能"; - readonly "zh-tw": "啟用進階功能"; - }; - readonly "Enable customization sync": { - readonly def: "Enable customization sync"; - readonly es: "Habilitar sincronización de personalización"; - readonly fr: "Activer la synchronisation de personnalisation"; - readonly he: "הפעל סנכרון התאמה אישית"; - readonly ja: "カスタマイズ同期を有効"; - readonly ko: "사용자 설정 동기화 활성화"; - readonly ru: "Включить синхронизацию настроек"; - readonly zh: "启用自定义同步"; - readonly "zh-tw": "啟用自訂同步"; - }; - readonly "Enable Developers' Debug Tools.": { - readonly def: "Enable Developers' Debug Tools."; - readonly es: "Habilitar herramientas de depuración"; - readonly fr: "Activer les outils de débogage pour développeurs."; - readonly he: "הפעל כלי ניפוי באגים למפתחים."; - readonly ja: "開発者用デバッグツールを有効にする"; - readonly ko: "개발자 디버그 도구 활성화"; - readonly ru: "Включить инструменты разработчика."; - readonly zh: "启用开发者调试工具 "; - readonly "zh-tw": "啟用開發者除錯工具。"; - }; - readonly "Enable edge case treatment features": { - readonly def: "Enable edge case treatment features"; - readonly es: "Habilitar manejo de casos límite"; - readonly fr: "Activer les fonctionnalités pour cas particuliers"; - readonly he: "הפעל תכונות לטיפול במקרי קצה"; - readonly ja: "エッジケース対応機能を有効にする"; - readonly ko: "특수 사례 처리 기능 활성화"; - readonly ru: "Включить функции обработки граничных случаев"; - readonly zh: "启用边缘情况处理功能"; - readonly "zh-tw": "啟用邊緣情況處理功能"; - }; - readonly "Enable poweruser features": { - readonly def: "Enable poweruser features"; - readonly es: "Habilitar funciones para usuarios avanzados"; - readonly fr: "Activer les fonctionnalités pour utilisateurs avancés"; - readonly he: "הפעל תכונות למשתמש מתקדם"; - readonly ja: "エキスパート機能を有効にする"; - readonly ko: "파워 유저 기능 활성화"; - readonly ru: "Включить функции для опытных пользователей"; - readonly zh: "启用高级用户功能"; - readonly "zh-tw": "啟用進階使用者功能"; - }; - readonly "Enable this if your Object Storage doesn't support CORS": { - readonly def: "Enable this if your Object Storage doesn't support CORS"; - readonly es: "Habilitar si su almacenamiento no soporta CORS"; - readonly fr: "Activer ceci si votre stockage objet ne prend pas en charge CORS"; - readonly he: "הפעל אם אחסון האובייקטים שלך לא תומך ב-CORS"; - readonly ja: "オブジェクトストレージがCORSをサポートしていない場合は有効にしてください"; - readonly ko: "객체 스토리지가 CORS를 지원하지 않는 경우 활성화하세요"; - readonly ru: "Включите, если ваше объектное хранилище не поддерживает CORS"; - readonly zh: "如果您的对象存储不支持 CORS,请启用此功能 "; - readonly "zh-tw": "如果你的物件儲存不支援 CORS,請啟用此選項"; - }; - readonly "Enable this option to automatically apply the most recent change to documents even when it conflicts": { - readonly def: "Enable this option to automatically apply the most recent change to documents even when it conflicts"; - readonly es: "Aplicar cambios recientes automáticamente aunque generen conflictos"; - readonly fr: "Activer cette option pour appliquer automatiquement la modification la plus récente aux documents même en cas de conflit"; - readonly he: "הפעל אפשרות זו כדי להחיל אוטומטית את השינוי האחרון במסמכים גם כשיש קונפליקט"; - readonly ja: "このオプションを有効にすると、競合があっても最新の変更を自動的にドキュメントに適用します"; - readonly ko: "이 옵션을 활성화하면 충돌이 있어도 문서에 가장 최근 변경 사항을 자동으로 적용합니다"; - readonly ru: "Включите эту опцию для автоматического применения последних изменений к документам даже при конфликте"; - readonly zh: "启用此选项可在文档冲突时自动应用最新的更改"; - readonly "zh-tw": "啟用此選項後,即使發生衝突也會自動套用文件的最新變更"; - }; - readonly "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": { - readonly def: "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended."; - readonly es: "Cifrar contenido en la base de datos remota. Se recomienda habilitar si usa la sincronización del plugin."; - readonly fr: "Chiffrer le contenu sur la base de données distante. Si vous utilisez la fonction de synchronisation du plugin, l'activation est recommandée."; - readonly he: "הצפן תוכן במסד הנתונים המרוחק. אם אתה משתמש בתכונת הסנכרון של התוסף, מומלץ להפעיל זאת."; - readonly ja: "リモートデータベースの暗号化(オンにすることを推奨)"; - readonly ko: "원격 데이터베이스의 내용을 암호화합니다. 플러그인의 동기화 기능을 사용하는 경우 활성화를 권장합니다."; - readonly ru: "Шифровать содержимое на удалённой базе данных. Рекомендуется включить при использовании функции синхронизации плагина."; - readonly zh: "加密远程数据库中的内容。如果您使用插件的同步功能,则建议启用此功能 "; - readonly "zh-tw": "加密遠端資料庫中的內容。如果你使用外掛的同步功能,建議啟用此選項。"; - }; - readonly "Encrypting sensitive configuration items": { - readonly def: "Encrypting sensitive configuration items"; - readonly es: "Cifrando elementos sensibles"; - readonly fr: "Chiffrement des éléments de configuration sensibles"; - readonly he: "הצפן פריטי תצורה רגישים"; - readonly ja: "機密性の高い設定項目の暗号化"; - readonly ko: "민감한 구성 항목 암호화"; - readonly ru: "Шифрование конфиденциальных настроек"; - readonly zh: "加密敏感配置项"; - readonly "zh-tw": "加密敏感設定項目"; - }; - readonly "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": { - readonly def: "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files."; - readonly es: "Frase de cifrado. Si la cambia, sobrescriba la base del servidor con los nuevos archivos cifrados."; - readonly fr: "Phrase secrète de chiffrement. Si modifiée, vous devriez écraser la base de données du serveur avec les nouveaux fichiers (chiffrés)."; - readonly he: "ביטוי סיסמה להצפנה. אם שונה, יש לדרוס את מסד הנתונים של השרת בקבצים החדשים (המוצפנים)."; - readonly ja: "暗号化パスフレーズ。変更した場合、新しい(暗号化された)ファイルでサーバーのデータベースを上書きする必要があります。"; - readonly ko: "패스프레이즈는 암호화에 사용되는 긴 암호 문구입니다. 변경한 경우, 암호화된 새 파일로 서버의 데이터베이스를 덮어써야 합니다."; - readonly ru: "Парольная фраза шифрования. При изменении нужно перезаписать базу данных сервера новыми (зашифрованными) файлами."; - readonly zh: "加密密码。如果更改,您应该用新的(加密的)文件覆盖服务器的数据库 "; - readonly "zh-tw": "加密密語。如果變更了它,你應以新的(已加密)檔案覆寫伺服器上的資料庫。"; - }; - readonly "End-to-End Encryption": { - readonly def: "End-to-End Encryption"; - readonly es: "Cifrado de extremo a extremo"; - readonly fr: "Chiffrement de bout en bout"; - readonly he: "הצפנה מקצה לקצה"; - readonly ja: "E2E暗号化"; - readonly ko: "종단간 암호화"; - readonly ru: "Сквозное шифрование"; - readonly zh: "端到端加密"; - readonly "zh-tw": "端對端加密"; - }; - readonly "Endpoint URL": { - readonly def: "Endpoint URL"; - readonly es: "URL del endpoint"; - readonly fr: "URL du point de terminaison"; - readonly he: "כתובת נקודת קצה (Endpoint URL)"; - readonly ja: "エンドポイントURL"; - readonly ko: "엔드포인트 URL"; - readonly ru: "URL конечной точки"; - readonly zh: "终端URL"; - readonly "zh-tw": "端點 URL"; - }; - readonly "Enhance chunk size": { - readonly def: "Enhance chunk size"; - readonly es: "Mejorar tamaño de chunks"; - readonly fr: "Améliorer la taille des fragments"; - readonly he: "הגדל גודל נתח"; - readonly ja: "チャンクサイズを最適化する"; - readonly ko: "청크 크기 향상"; - readonly ru: "Улучшить размер чанка"; - readonly zh: "增大块大小"; - readonly "zh-tw": "擴大 chunk 大小"; - }; - readonly "Enter Server Information": { - readonly def: "Enter Server Information"; - readonly es: "Introducir información del servidor"; - readonly ja: "サーバー情報の入力"; - readonly ko: "서버 정보 입력"; - readonly ru: "Ввести данные сервера"; - readonly zh: "输入服务器信息"; - readonly "zh-tw": "輸入伺服器資訊"; - }; - readonly "Enter the server information manually": { - readonly def: "Enter the server information manually"; - readonly es: "Introducir manualmente la información del servidor"; - readonly ja: "サーバー情報を手動で入力する"; - readonly ko: "서버 정보를 수동으로 입력"; - readonly ru: "Ввести данные сервера вручную"; - readonly zh: "手动输入服务器信息"; - readonly "zh-tw": "手動輸入伺服器資訊"; - }; - readonly Export: { - readonly def: "Export"; - readonly es: "Exportar"; - readonly ja: "エクスポート"; - readonly ko: "내보내기"; - readonly ru: "Экспорт"; - readonly zh: "导出"; - readonly "zh-tw": "匯出"; - }; - readonly "Failed to connect to remote for compaction.": { - readonly def: "Failed to connect to remote for compaction."; - readonly ja: "リモートデータベースに接続できず、コンパクションを実行できませんでした。"; - readonly ko: "압축을 위해 원격 데이터베이스에 연결하지 못했습니다."; - readonly ru: "Не удалось подключиться к удалённой базе данных для компакции."; - readonly zh: "无法连接到远程数据库以执行压缩。"; - readonly "zh-tw": "無法連線到遠端資料庫以執行壓縮。"; - }; - readonly "Failed to connect to remote for compaction. ${reason}": { - readonly def: "Failed to connect to remote for compaction. ${reason}"; - readonly ja: "リモートデータベースに接続できず、コンパクションを実行できませんでした。${reason}"; - readonly ko: "압축을 위해 원격 데이터베이스에 연결하지 못했습니다. ${reason}"; - readonly ru: "Не удалось подключиться к удалённой базе данных для компакции. ${reason}"; - readonly zh: "无法连接到远程数据库以执行压缩。${reason}"; - readonly "zh-tw": "無法連線到遠端資料庫以執行壓縮。${reason}"; - }; - readonly "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": { - readonly def: "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled."; - readonly ja: "Garbage Collection 前にワンショットレプリケーションを開始できませんでした。Garbage Collection はキャンセルされました。"; - readonly ko: "Garbage Collection 전에 일회성 복제를 시작하지 못했습니다. Garbage Collection을 취소합니다."; - readonly ru: "Не удалось запустить одноразовую репликацию перед Garbage Collection. Garbage Collection отменена."; - readonly zh: "无法在垃圾回收前启动一次性复制。垃圾回收已取消。"; - readonly "zh-tw": "無法在垃圾回收前啟動一次性複寫。垃圾回收已取消。"; - }; - readonly "Failed to start replication after Garbage Collection.": { - readonly def: "Failed to start replication after Garbage Collection."; - readonly ja: "Garbage Collection 後にレプリケーションを開始できませんでした。"; - readonly ko: "Garbage Collection 후 복제를 시작하지 못했습니다."; - readonly ru: "Не удалось запустить репликацию после Garbage Collection."; - readonly zh: "垃圾回收后无法启动复制。"; - readonly "zh-tw": "垃圾回收後無法啟動複寫。"; - }; - readonly Fetch: { - readonly def: "Fetch"; - readonly es: "Obtener"; - readonly ja: "取得"; - readonly ko: "가져오기"; - readonly ru: "Получить"; - readonly zh: "获取"; - readonly "zh-tw": "抓取"; - }; - readonly "Fetch chunks on demand": { - readonly def: "Fetch chunks on demand"; - readonly es: "Obtener chunks bajo demanda"; - readonly fr: "Récupérer les fragments à la demande"; - readonly he: "משוך נתחים לפי דרישה"; - readonly ja: "ユーザーのタイミングでチャンクの更新を確認する"; - readonly ko: "필요 시 청크 원격 가져오기"; - readonly ru: "Загружать чанки по требованию"; - readonly zh: "按需获取块"; - readonly "zh-tw": "按需抓取 chunks"; - }; - readonly "Fetch database with previous behaviour": { - readonly def: "Fetch database with previous behaviour"; - readonly es: "Obtener BD con comportamiento anterior"; - readonly fr: "Récupérer la base de données avec le comportement précédent"; - readonly he: "משוך מסד נתונים עם התנהגות קודמת"; - readonly ja: "以前の動作でデータベースを取得"; - readonly ko: "이전 동작으로 데이터베이스 가져오기"; - readonly ru: "Загрузить базу данных с предыдущим поведением"; - readonly zh: "使用以前的行为获取数据库"; - readonly "zh-tw": "以前一種行為抓取資料庫"; - }; - readonly "Fetch remote settings": { - readonly def: "Fetch remote settings"; - readonly es: "Obtener ajustes remotos"; - readonly ja: "リモート設定を取得"; - readonly ko: "원격 설정 가져오기"; - readonly ru: "Получить настройки с удалённого хранилища"; - readonly zh: "获取远端设置"; - readonly "zh-tw": "抓取遠端設定"; - }; - readonly "File to resolve conflict": { - readonly def: "File to resolve conflict"; - readonly es: "Archivo para resolver el conflicto"; - readonly ja: "競合を解決するファイル"; - readonly ko: "충돌을 해결할 파일"; - readonly ru: "Файл для разрешения конфликта"; - readonly zh: "选择要解决冲突的文件"; - readonly "zh-tw": "要解決衝突的檔案"; - }; - readonly "File to view History": { - readonly def: "File to view History"; - readonly "zh-tw": "要檢視歷程的檔案"; - }; - readonly Filename: { - readonly def: "Filename"; - readonly es: "Nombre de archivo"; - readonly fr: "Nom de fichier"; - readonly he: "שם קובץ"; - readonly ja: "ファイル名"; - readonly ko: "파일명"; - readonly ru: "Имя файла"; - readonly zh: "文件名"; - readonly "zh-tw": "檔名"; - }; - readonly "First, please select the option that best describes your current situation.": { - readonly def: "First, please select the option that best describes your current situation."; - readonly es: "Primero, seleccione la opción que describa mejor su situación actual。"; - readonly ja: "まず、現在の状況に最も近い項目を選択してください。"; - readonly ko: "먼저 현재 상황에 가장 잘 맞는 항목을 선택해 주세요。"; - readonly ru: "Сначала выберите вариант, который лучше всего описывает вашу текущую ситуацию。"; - readonly zh: "首先,请选择最符合你当前情况的选项。"; - readonly "zh-tw": "首先,請選擇最符合你目前情況的選項。"; - }; - readonly "Flag and restart": { - readonly def: "Flag and restart"; - readonly es: "Marcar y reiniciar"; - readonly ja: "フラグを立てて再起動"; - readonly ko: "표시 후 재시작"; - readonly ru: "Пометить и перезапустить"; - readonly zh: "标记后重启"; - readonly "zh-tw": "標記後重新啟動"; - }; - readonly "Forces the file to be synced when opened.": { - readonly def: "Forces the file to be synced when opened."; - readonly es: "Forzar sincronización al abrir archivo"; - readonly fr: "Force la synchronisation du fichier à son ouverture."; - readonly he: "מכריח סנכרון הקובץ בעת פתיחתו."; - readonly ja: "ファイルを開いたときに強制的に同期します。"; - readonly ko: "파일을 열 때 강제로 동기화합니다."; - readonly ru: "Принудительно синхронизировать файл при открытии."; - readonly zh: "打开文件时强制同步该文件 "; - readonly "zh-tw": "開啟檔案時強制同步該檔案。"; - }; - readonly "Fresh Start Wipe": { - readonly def: "Fresh Start Wipe"; - readonly es: "Borrado para reinicio completo"; - readonly ja: "初期化ワイプ"; - readonly ko: "새로 시작 지우기"; - readonly ru: "Полный сброс для чистого старта"; - readonly zh: "全新开始清除"; - readonly "zh-tw": "全新開始清除"; - }; - readonly "Garbage Collection cancelled by user.": { - readonly def: "Garbage Collection cancelled by user."; - readonly ja: "ユーザーによって Garbage Collection がキャンセルされました。"; - readonly ko: "사용자가 Garbage Collection을 취소했습니다."; - readonly ru: "Пользователь отменил Garbage Collection."; - readonly zh: "用户已取消垃圾回收。"; - readonly "zh-tw": "使用者已取消垃圾回收。"; - }; - readonly "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": { - readonly def: "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds."; - readonly ja: "Garbage Collection が完了しました。削除したチャンク: ${deletedChunks} / ${totalChunks}。所要時間: ${seconds} 秒。"; - readonly ko: "Garbage Collection이 완료되었습니다. 삭제된 청크: ${deletedChunks} / ${totalChunks}. 소요 시간: ${seconds}초."; - readonly ru: "Garbage Collection завершена. Удалено чанков: ${deletedChunks} / ${totalChunks}. Затраченное время: ${seconds} сек."; - readonly zh: "垃圾回收完成。已删除 chunks:${deletedChunks} / ${totalChunks}。耗时:${seconds} 秒。"; - readonly "zh-tw": "垃圾回收完成。已刪除 chunks:${deletedChunks} / ${totalChunks}。耗時:${seconds} 秒。"; - }; - readonly "Garbage Collection Confirmation": { - readonly def: "Garbage Collection Confirmation"; - readonly ja: "Garbage Collection の確認"; - readonly ko: "Garbage Collection 확인"; - readonly ru: "Подтверждение Garbage Collection"; - readonly zh: "垃圾回收确认"; - readonly "zh-tw": "垃圾回收確認"; - }; - readonly "Garbage Collection V3 (Beta)": { - readonly def: "Garbage Collection V3 (Beta)"; - readonly es: "Recolección de basura V3 (Beta)"; - readonly ja: "ガーベジコレクション V3 (Beta)"; - readonly ko: "가비지 컬렉션 V3 (Beta)"; - readonly ru: "Сборка мусора V3 (Beta)"; - readonly zh: "垃圾回收 V3(Beta)"; - readonly "zh-tw": "垃圾回收 V3(Beta)"; - }; - readonly "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": { - readonly def: "Garbage Collection: Found ${unusedChunks} unused chunks to delete."; - readonly ja: "Garbage Collection: 削除対象の未使用チャンクが ${unusedChunks} 件見つかりました。"; - readonly ko: "Garbage Collection: 삭제할 미사용 청크 ${unusedChunks}개를 찾았습니다."; - readonly ru: "Garbage Collection: найдено ${unusedChunks} неиспользуемых чанков для удаления."; - readonly zh: "垃圾回收:发现 ${unusedChunks} 个未使用的 chunks 待删除。"; - readonly "zh-tw": "垃圾回收:找到 ${unusedChunks} 個未使用的 chunks 可刪除。"; - }; - readonly "Garbage Collection: Scanned ${scanned} / ~${docCount}": { - readonly def: "Garbage Collection: Scanned ${scanned} / ~${docCount}"; - readonly ja: "Garbage Collection: ${scanned} / ~${docCount} をスキャン済み"; - readonly ko: "Garbage Collection: ${scanned} / ~${docCount} 스캔됨"; - readonly ru: "Garbage Collection: просканировано ${scanned} / ~${docCount}"; - readonly zh: "垃圾回收:已扫描 ${scanned} / ~${docCount}"; - readonly "zh-tw": "垃圾回收:已掃描 ${scanned} / ~${docCount}"; - }; - readonly "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": { - readonly def: "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}"; - readonly ja: "Garbage Collection: スキャン完了。総チャンク数: ${totalChunks}、使用中チャンク数: ${usedChunks}"; - readonly ko: "Garbage Collection: 스캔 완료. 전체 청크 수: ${totalChunks}, 사용 중인 청크 수: ${usedChunks}"; - readonly ru: "Garbage Collection: сканирование завершено. Всего чанков: ${totalChunks}, используемых чанков: ${usedChunks}"; - readonly zh: "垃圾回收:扫描完成。总 chunks:${totalChunks},已使用 chunks:${usedChunks}"; - readonly "zh-tw": "垃圾回收:掃描完成。總 chunks:${totalChunks},已使用 chunks:${usedChunks}"; - }; - readonly "Handle files as Case-Sensitive": { - readonly def: "Handle files as Case-Sensitive"; - readonly es: "Manejar archivos como sensibles a mayúsculas"; - readonly fr: "Gérer les fichiers en respectant la casse"; - readonly he: "טפל בקבצים כתלויי רישיות"; - readonly ja: "ファイルの大文字・小文字を区別する"; - readonly ko: "파일을 대소문자 구분으로 처리"; - readonly ru: "Обрабатывать файлы с учётом регистра"; - readonly zh: "将文件视为区分大小写"; - readonly "zh-tw": "將檔案視為區分大小寫"; - }; - readonly "Hidden Files": { - readonly def: "Hidden Files"; - readonly es: "Archivos ocultos"; - readonly ja: "隠しファイル"; - readonly ko: "숨김 파일"; - readonly ru: "Скрытые файлы"; - readonly zh: "隐藏文件"; - readonly "zh-tw": "隱藏檔案"; - }; - readonly "Hide completely": { - readonly def: "Hide completely"; - readonly zh: "完全隐藏"; - readonly "zh-tw": "完全隱藏"; - }; - readonly "Highlight diff": { - readonly def: "Highlight diff"; - readonly "zh-tw": "醒目顯示差異"; - }; - readonly "How to display network errors when the sync server is unreachable.": { - readonly def: "How to display network errors when the sync server is unreachable."; - readonly es: "Cómo mostrar los errores de red cuando el servidor de sincronización no está disponible."; - readonly ja: "同期サーバーに到達できない場合のネットワークエラーの表示方法を設定します。"; - readonly ko: "동기화 서버에 연결할 수 없을 때 네트워크 오류를 어떻게 표시할지 설정합니다."; - readonly ru: "Определяет, как отображать сетевые ошибки, если сервер синхронизации недоступен."; - readonly zh: "当同步服务器不可达时,如何显示网络错误。"; - readonly "zh-tw": "當同步伺服器無法連線時,如何顯示網路錯誤。"; - }; - readonly "How would you like to configure the connection to your server?": { - readonly def: "How would you like to configure the connection to your server?"; - readonly es: "¿Cómo desea configurar la conexión con su servidor?"; - readonly ja: "サーバー接続をどのように設定しますか?"; - readonly ko: "서버 연결을 어떻게 구성하시겠습니까?"; - readonly ru: "Как вы хотите настроить подключение к серверу?"; - readonly zh: "你希望如何配置与服务器的连接?"; - readonly "zh-tw": "你希望如何設定與伺服器的連線?"; - }; - readonly "I am adding a device to an existing synchronisation setup": { - readonly def: "I am adding a device to an existing synchronisation setup"; - readonly es: "Estoy agregando un dispositivo a una configuración de sincronización existente"; - readonly ja: "既存の同期構成に端末を追加します"; - readonly ko: "기존 동기화 구성에 장치를 추가합니다"; - readonly ru: "Я добавляю устройство к существующей настройке синхронизации"; - readonly zh: "我要将设备加入现有同步配置"; - readonly "zh-tw": "我要將裝置加入既有同步設定"; - }; - readonly "I am setting this up for the first time": { - readonly def: "I am setting this up for the first time"; - readonly es: "Estoy configurando esto por primera vez"; - readonly ja: "はじめて設定します"; - readonly ko: "처음으로 설정합니다"; - readonly ru: "Я настраиваю это впервые"; - readonly zh: "我是第一次进行设置"; - readonly "zh-tw": "我是第一次進行設定"; - }; - readonly "I know my server details, let me enter them": { - readonly def: "I know my server details, let me enter them"; - readonly es: "Conozco los datos de mi servidor; permítame introducirlos"; - readonly ja: "サーバー情報を把握しているので、自分で入力します"; - readonly ko: "서버 정보를 알고 있으니 직접 입력하겠습니다"; - readonly ru: "Я знаю параметры сервера, позвольте ввести их вручную"; - readonly zh: "我知道服务器详情,让我手动输入"; - readonly "zh-tw": "我知道伺服器資訊,讓我手動輸入"; - }; - readonly "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": { - readonly def: "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour)."; - readonly es: "Si se desactiva, chunks se dividen en hilo UI (comportamiento anterior)"; - readonly fr: "Si désactivé, les fragments seront découpés sur le thread UI (comportement précédent)."; - readonly he: "אם מכובה, נתחים יפוצלו בשרשור ממשק המשתמש (התנהגות קודמת)."; - readonly ja: "無効(トグル)にすると、チャンクはUIスレッドで分割されます(以前の動作)。"; - readonly ko: "비활성화(토글)되면 청크는 UI 스레드에서 분할됩니다 (이전 동작)."; - readonly ru: "Если отключено, чанки будут разделяться в основном потоке."; - readonly zh: "如果禁用(切换),chunks 将在 UI 线程上分割(以前的行为)"; - readonly "zh-tw": "若停用(關閉)此選項,chunks 會在 UI 執行緒上分割(舊有行為)。"; - }; - readonly "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": { - readonly def: "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions."; - readonly es: "Habilita sincronización eficiente por archivo. Requiere migración y actualizar todos dispositivos a v0.23.18. Pierde compatibilidad con versiones antiguas"; - readonly fr: "Si activée, la synchronisation de personnalisation efficace par fichier sera utilisée. Une petite migration est nécessaire lors de l'activation. Tous les appareils doivent être à jour en v0.23.18. Une fois cette option activée, la compatibilité avec les anciennes versions est perdue."; - readonly he: "אם מופעל, ייעשה שימוש בסנכרון התאמה אישית יעיל לפי קובץ. נדרשת הגירה קטנה בעת ההפעלה. כל המכשירים צריכים להיות מעודכנים לגרסה 0.23.18. לאחר ההפעלה, התאימות לגרסאות ישנות תיפגע."; - readonly ja: "有効にすると、ファイルごとの効率的なカスタマイズ同期が使用されます。有効化時に小規模な移行が必要です。また、すべてのデバイスをv0.23.18にアップデートする必要があります。一度有効にすると、古いバージョンとの互換性がなくなります。"; - readonly ko: "활성화하면 파일별 효율적인 사용자 설정 동기화가 사용됩니다. 이를 활성화할 때 소규모 데이터 구조 전환이 필요합니다. 모든 기기를 v0.23.18로 업데이트해야 합니다. 이를 활성화하면 이전 버전과의 호환성이 사라집니다."; - readonly ru: "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions."; - readonly zh: "如果启用,将使用基于文件的、高效的自定义同步。启用此功能需要进行一次小的迁移。所有设备都应更新到 v0.23.18。一旦启用此功能,我们将失去与旧版本的兼容性"; - readonly "zh-tw": "啟用後會使用以每個檔案為單位的高效率自訂同步。啟用時需要進行一次小型遷移,且所有裝置都應升級到 v0.23.18。啟用後將不再相容舊版本。"; - }; - readonly "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": { - readonly def: "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker."; - readonly es: "Divide chunks en máximo 100 ítems. Menos eficiente en deduplicación"; - readonly fr: "Si activée, les fragments seront découpés en 100 éléments au maximum. Cependant, la déduplication est légèrement moins efficace."; - readonly he: "אם מופעל, נתחים יפוצלו לא ליותר מ-100 פריטים. עם זאת, ביטול כפילויות יהיה חלש מעט יותר."; - readonly ja: "有効にすると、チャンクは最大100項目に分割されます。ただし、重複除去の精度は落ちます。"; - readonly ko: "활성화하면 청크는 최대 100개 항목으로 분할됩니다. 하지만 중복 제거 기능이 약간 약해집니다."; - readonly ru: "Если включено, чанки будут разделены не более чем на 100 элементов."; - readonly zh: "如果启用,数据块将被分割成不超过 100 项。但是,去重效果会稍弱"; - readonly "zh-tw": "啟用後,chunks 最多會分成 100 個項目,但去重效果會稍微變弱。"; - }; - readonly "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": { - readonly def: "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised."; - readonly es: "Chunks nuevos se mantienen temporalmente en el documento hasta estabilizarse"; - readonly fr: "Si activée, les fragments nouvellement créés sont temporairement conservés dans le document et promus en fragments indépendants une fois stabilisés."; - readonly he: "אם מופעל, נתחים שנוצרו לאחרונה נשמרים זמנית בתוך המסמך, ומוסמכים לנתחים עצמאיים לאחר יציבות."; - readonly ja: "有効にすると、新しく作成されたチャンクはドキュメント内に一時的に保持され、安定したら独立したチャンクになります。"; - readonly ko: "활성화하면 새로 생성된 변경 기록(청크)은 문서 안에 임시로 보관되며, 일정 조건을 만족하면 자동으로 문서 밖으로 분리되어 저장됩니다."; - readonly ru: "Если включено, вновь созданные чанки временно хранятся в документе."; - readonly zh: "如果启用,新创建的数据块将暂时保留在文档中,并在稳定后成为独立数据块"; - readonly "zh-tw": "啟用後,新建立的 chunks 會暫時保留在文件中,待穩定後再獨立出去。"; - }; - readonly "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": { - readonly def: "If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown."; - readonly es: "Si se activa, se mostrará el icono ⛔ en el estado en lugar del banner de advertencia de archivos. No se mostrarán detalles."; - readonly fr: "Si activée, l'icône ⛔ s'affichera dans le statut à la place de la bannière d'avertissements de fichiers. Aucun détail ne sera affiché."; - readonly he: "אם מופעל, אייקון ⛔ יוצג בסטטוס במקום פס האזהרות. לא יוצגו פרטים."; - readonly ja: "有効にすると、ファイル警告バナーの代わりにステータス内へ ⛔ アイコンのみを表示します。詳細は表示されません。"; - readonly ko: "활성화하면 파일 경고 배너 대신 상태 영역에 ⛔ 아이콘만 표시됩니다. 자세한 내용은 표시되지 않습니다."; - readonly ru: "Если включено, значок будет показан внутри статуса."; - readonly zh: "如果启用,状态栏内将显示 ⛔ 图标,而非文件警告横幅,不会显示任何详细信息。"; - readonly "zh-tw": "啟用後,將在狀態列中顯示 ⛔ 圖示,而不是檔案警告橫幅,且不會顯示詳細資訊。"; - }; - readonly "If enabled, the file under 1kb will be processed in the UI thread.": { - readonly def: "If enabled, the file under 1kb will be processed in the UI thread."; - readonly es: "Archivos <1kb se procesan en hilo UI"; - readonly fr: "Si activée, les fichiers de moins de 1 Ko seront traités sur le thread UI."; - readonly he: "אם מופעל, קבצים קטנים מ-1KB יעובדו בשרשור ממשק המשתמש."; - readonly ja: "有効にすると、1kb未満のファイルはUIスレッドで処理されます。"; - readonly ko: "활성화하면 1kb 미만의 파일은 UI 스레드에서 처리됩니다."; - readonly ru: "Если включено, файлы меньше 1КБ будут обрабатываться в основном потоке."; - readonly zh: "如果启用,小于 1kb 的文件将在 UI 线程中处理"; - readonly "zh-tw": "啟用後,小於 1KB 的檔案會在 UI 執行緒中處理。"; - }; - readonly "If enabled, the notification of hidden files change will be suppressed.": { - readonly def: "If enabled, the notification of hidden files change will be suppressed."; - readonly es: "Si se habilita, se suprimirá la notificación de cambios en archivos ocultos."; - readonly fr: "Si activée, les notifications de modifications des fichiers cachés seront supprimées."; - readonly he: "אם מופעל, התראות על שינוי בקבצים נסתרים יודחקו."; - readonly ja: "有効にすると、隠しファイルの変更通知が抑制されます。"; - readonly ko: "활성화하면 숨겨진 파일 변경 알림이 억제됩니다."; - readonly ru: "Если включено, уведомление об изменении скрытых файлов будет подавлено."; - readonly zh: "如果启用,将不再通知隐藏文件被更改"; - readonly "zh-tw": "啟用後,將不再通知隱藏檔案變更。"; - }; - readonly "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": { - readonly def: "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)"; - readonly es: "Si se habilita, todos los chunks se almacenan con la revisión hecha desde su contenido. (comportamiento anterior)"; - readonly fr: "Si activée, tous les fragments seront stockés avec la révision issue de leur contenu. (Comportement précédent)"; - readonly he: "אם מופעל, כל הנתחים יישמרו עם גרסה המבוססת על תוכנם. (התנהגות קודמת)"; - readonly ja: "有効にすると、すべてのチャンクはコンテンツから作成されたリビジョンと共に保存されます(以前の動作)。"; - readonly ko: "이 옵션이 활성화되면 모든 청크는 콘텐츠에서 생성된 리비전과 함께 저장됩니다. (이전 동작)"; - readonly ru: "Если включено, все чанки будут храниться с ревизией из содержимого."; - readonly zh: "如果启用,所有 chunks 将使用根据其内容生成的修订版本存储(以前的行为)"; - readonly "zh-tw": "啟用後,所有 chunks 都會以其內容產生的修訂版本儲存(舊有行為)。"; - }; - readonly "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": { - readonly def: "If this enabled, All files are handled as case-Sensitive (Previous behaviour)."; - readonly es: "Si se habilita, todos los archivos se manejan como sensibles a mayúsculas (comportamiento anterior)"; - readonly fr: "Si activée, tous les fichiers sont gérés en respectant la casse (comportement précédent)."; - readonly he: "אם מופעל, כל הקבצים מטופלים כתלויי רישיות (התנהגות קודמת)."; - readonly ja: "有効にすると、すべてのファイルは大文字小文字を区別して処理されます(以前の動作)。"; - readonly ko: "이 옵션이 활성화되면 모든 파일이 대소문자를 구분하여 처리됩니다 (이전 동작)."; - readonly ru: "Если включено, все файлы обрабатываются с учётом регистра."; - readonly zh: "如果启用,所有文件都将视为区分大小写(以前的行为)"; - readonly "zh-tw": "啟用後,所有檔案都會以區分大小寫方式處理(舊有行為)。"; - }; - readonly "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": { - readonly def: "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature."; - readonly es: "Divide chunks en segmentos semánticos. No todos los sistemas lo soportan"; - readonly fr: "Si activée, les fragments seront découpés en segments sémantiquement signifiants. Toutes les plateformes ne prennent pas en charge cette fonctionnalité."; - readonly he: "אם מופעל, נתחים יפוצלו לחלקים עם משמעות סמנטית. לא כל הפלטפורמות תומכות בתכונה זו."; - readonly ja: "有効にすると、チャンクは意味的に有意なセグメントに分割されます。すべてのプラットフォームがこの機能をサポートしているわけではありません。"; - readonly ko: "이 옵션을 활성화하면 청크가 문단이나 의미 단위로 나뉘어 저장됩니다. 단, 이 기능은 일부 플랫폼에서는 지원되지 않을 수 있습니다."; - readonly ru: "Если включено, чанки будут разделены на семантически значимые сегменты."; - readonly zh: "如果启用此功能,数据块将被分割成具有语义意义的段落。并非所有平台都支持此功能"; - readonly "zh-tw": "啟用後,chunks 會依語意切分成有意義的區段,並非所有平台都支援此功能。"; - }; - readonly "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": { - readonly def: "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files."; - readonly es: "Saltar cambios en archivos locales que coincidan con ignore files. Cambios remotos usan ignore files locales"; - readonly fr: "Si défini, les modifications des fichiers locaux correspondant aux fichiers d'exclusion seront ignorées. Les changements distants sont déterminés à l'aide des fichiers d'exclusion locaux."; - readonly he: "אם מוגדר, שינויים בקבצים מקומיים התואמים לקבצי ההתעלמות יידלגו. שינויים מרוחקים נקבעים לפי קבצי ההתעלמות המקומיים."; - readonly ja: "これを設定すると、除外ファイルに一致するローカルファイルの変更はスキップされます。リモートの変更はローカルの無視ファイルを使用して判定されます。"; - readonly ko: "이 옵션을 활성화하면, 제외 규칙 파일에 일치하는 로컬 파일의 변경 사항은 건너뜁니다. 원격 변경 여부 또한 로컬의 제외 규칙 파일에 따라 판단됩니다."; - readonly ru: "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files."; - readonly zh: "如果设置了此项,与忽略文件匹配的本地文件的更改将被跳过。远程更改使用本地忽略文件确定"; - }; - readonly "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": { - readonly def: "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage."; - readonly es: "Mantiene conexión 60s. Si no hay cambios, reinicia socket. Útil con proxies limitantes"; - readonly fr: "Si cette option est activée, PouchDB maintiendra la connexion ouverte pendant 60 secondes, et si aucun changement n'arrive durant cette période, fermera et rouvrira la socket au lieu de la garder ouverte indéfiniment. Utile lorsqu'un proxy limite la durée des requêtes, mais peut augmenter l'utilisation des ressources."; - readonly he: "אם אפשרות זו מופעלת, PouchDB ישמור את החיבור פתוח ל-60 שניות, ואם אין שינוי בפרק זמן זה, יסגור ויפתח מחדש את השקע, במקום להחזיק אותו פתוח ללא הגבלה. שימושי כשפרוקסי מגביל משך בקשות, אך עשוי להגביר שימוש במשאבים."; - readonly ja: "このオプションを有効にすると、PouchDBは接続を60秒間保持し、その間に通信がない場合、一度接続を閉じて再接続します。接続を無期限に保持する代わりにこの動作を行います。プロキシ(Cloudflareなど)がリクエストの持続時間を制限している場合に有用ですが、リソース使用量が増加する可能性があります。"; - readonly ko: "이 옵션이 활성화되면 PouchDB는 연결을 더이상 무한히 열어두지 않고 60초 동안 유지합니다. 그 시간 내에 변경 사항이 없으면 소켓을 닫고 다시 엽니다. 프록시가 요청 지속 시간을 제한할 때 유용하지만 리소스 사용량이 증가할 수 있습니다."; - readonly ru: "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage."; - readonly zh: "如果启用此选项,PouchDB 将保持连接打开 60 秒,如果在此时间内没有更改到达,则关闭并重新打开套接字,而不是无限期保持打开。当代理限制请求持续时间时有用,但可能会增加资源使用ß"; - }; - readonly "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": { - readonly def: "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value."; - readonly "zh-tw": "如果你在使用 IBM Cloudant 時遇到負載大小上限,請調低批次大小與批次上限。"; - }; - readonly "Ignore and Proceed": { - readonly def: "Ignore and Proceed"; - readonly ja: "無視して続行"; - readonly ko: "무시하고 계속"; - readonly ru: "Игнорировать и продолжить"; - readonly zh: "忽略并继续"; - readonly "zh-tw": "忽略並繼續"; - }; - readonly "Ignore files": { - readonly def: "Ignore files"; - readonly es: "Archivos a ignorar"; - readonly fr: "Fichiers d'exclusion"; - readonly he: "קבצי התעלמות"; - readonly ja: "除外ファイル"; - readonly ko: "제외 규칙 파일"; - readonly ru: "Файлы для игнорирования"; - readonly zh: "忽略文件"; - }; - readonly "Ignore patterns": { - readonly def: "Ignore patterns"; - readonly es: "Patrones de exclusión"; - readonly ja: "除外パターン"; - readonly ko: "무시 패턴"; - readonly ru: "Шаблоны исключения"; - readonly zh: "忽略模式"; - readonly "zh-tw": "忽略模式"; - }; - readonly "Import connection": { - readonly def: "Import connection"; - readonly es: "Importar conexión"; - readonly ja: "接続をインポート"; - readonly ko: "연결 가져오기"; - readonly ru: "Импортировать подключение"; - readonly zh: "导入连接"; - readonly "zh-tw": "匯入連線"; - }; - readonly "Incubate Chunks in Document": { - readonly def: "Incubate Chunks in Document"; - readonly es: "Incubar chunks en documento"; - readonly fr: "Incuber les fragments dans le document"; - readonly he: "בשל נתחים בתוך המסמך"; - readonly ja: "ドキュメント内でチャンクを一時保管する"; - readonly ko: "문서 내 변경 기록 임시 보관"; - readonly ru: "Инкубировать чанки в документе"; - readonly zh: "在文档中孵化块"; - }; - readonly "Initialise all journal history, On the next sync, every item will be received and sent.": { - readonly def: "Initialise all journal history, On the next sync, every item will be received and sent."; - readonly es: "Restablece todo el historial del diario. En la próxima sincronización se recibirán y enviarán todos los elementos."; - readonly ja: "すべてのジャーナル履歴を初期化します。次回の同期時に、すべての項目が再受信・再送信されます。"; - readonly ko: "모든 저널 기록을 초기화합니다. 다음 동기화 때 모든 항목을 다시 받고 다시 보냅니다."; - readonly ru: "Инициализирует всю историю журнала. При следующей синхронизации каждый элемент будет заново получен и отправлен."; - readonly zh: "初始化所有日志历史。下次同步时,所有项目都会重新接收并发送。"; - readonly "zh-tw": "初始化所有日誌歷史。下次同步時,每個項目都會重新接收與傳送。"; - }; - readonly "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": { - readonly def: "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again."; - readonly "zh-tw": "初始化接收日誌歷程。下次同步時,除了此裝置已送出的項目外,其他項目都會再次下載。"; - }; - readonly "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": { - readonly def: "Initialise journal sent history. On the next sync, every item except this device received will be sent again."; - readonly "zh-tw": "初始化傳送日誌歷程。下次同步時,除了此裝置已接收的項目外,其他項目都會再次傳送。"; - }; - readonly "Interval (sec)": { - readonly def: "Interval (sec)"; - readonly es: "Intervalo (segundos)"; - readonly fr: "Intervalle (sec)"; - readonly he: "מרווח (שניות)"; - readonly ja: "秒"; - readonly ko: "간격 (초)"; - readonly ru: "Интервал (сек)"; - readonly zh: "间隔(秒)"; - }; - readonly "K.exp": { - readonly def: "Experimental"; - readonly fr: "Expérimental"; - readonly he: "ניסיוני"; - readonly ja: "試験機能"; - readonly ko: "실험 기능"; - readonly ru: "Экспериментальная"; - readonly zh: "实验性"; - }; - readonly "K.long_p2p_sync": { - readonly def: "Peer-to-Peer Sync"; - readonly fr: "Synchronisation pair-à-pair"; - readonly he: "%{title_p2p_sync}"; - readonly ja: "Peer-to-Peer Sync (試験機能)"; - readonly ko: "피어 투 피어(P2P) 동기화 (실험 기능)"; - readonly ru: "title_p2p_sync"; - readonly zh: "Peer-to-Peer同步 (实验性)"; - }; - readonly "K.P2P": { - readonly def: "Peer-to-Peer"; - readonly fr: "Pair-à-Pair"; - readonly he: "%{Peer}-ל-%{Peer}"; - readonly ja: "Peer-to-Peer"; - readonly ko: "피어-to-피어"; - readonly ru: "Peer-к-Peer"; - readonly zh: "Peer-to-Peer"; - }; - readonly "K.Peer": { - readonly def: "Peer"; - readonly fr: "Pair"; - readonly he: "עמית"; - readonly ja: "Peer"; - readonly ko: "피어"; - readonly ru: "Устройство"; - readonly zh: "Peer"; - }; - readonly "K.ScanCustomization": { - readonly def: "Scan customization"; - readonly fr: "Analyser la personnalisation"; - readonly he: "סרוק התאמה אישית"; - readonly ja: "Scan customization"; - readonly ko: "사용자 설정 검색"; - readonly ru: "Scan customization"; - readonly zh: "扫描自定义"; - }; - readonly "K.short_p2p_sync": { - readonly def: "P2P Sync"; - readonly fr: "Sync P2P"; - readonly he: "סנכרון P2P"; - readonly ja: "P2P Sync (試験機能)"; - readonly ko: "P2P 동기화 (실험 기능)"; - readonly ru: "P2P Синхр."; - readonly zh: "P2P同步(实验性)"; - }; - readonly "K.title_p2p_sync": { - readonly def: "Peer-to-Peer Sync"; - readonly fr: "Synchronisation pair-à-pair"; - readonly he: "סנכרון עמית-לעמית"; - readonly ja: "Peer-to-Peer Sync"; - readonly ko: "피어 투 피어(P2P) 동기화"; - readonly ru: "Синхронизация между устройствами"; - readonly zh: "Peer-to-Peer同步"; - }; - readonly "Keep empty folder": { - readonly def: "Keep empty folder"; - readonly es: "Mantener carpetas vacías"; - readonly fr: "Conserver les dossiers vides"; - readonly he: "שמור תיקייה ריקה"; - readonly ja: "空フォルダの維持"; - readonly ko: "빈 폴더 유지"; - readonly ru: "Сохранять пустые папки"; - readonly zh: "保留空文件夹"; - }; - readonly lang_def: { - readonly def: "Default"; - readonly fr: "Par défaut"; - readonly he: "ברירת מחדל"; - readonly ja: "Default"; - readonly ko: "Default"; - readonly ru: "По умолчанию"; - readonly zh: "Default"; - }; - readonly "lang-de": { - readonly def: "Deutsche"; - readonly es: "Alemán"; - readonly fr: "Deutsche"; - readonly he: "Deutsche"; - readonly ja: "Deutsche"; - readonly ko: "Deutsche"; - readonly ru: "Deutsch"; - readonly zh: "Deutsche"; - }; - readonly "lang-def": { - readonly def: "Default"; - readonly fr: "Par défaut"; - readonly he: "%{lang_def}"; - readonly ja: "Default"; - readonly ko: "Default"; - readonly ru: "lang_def"; - readonly zh: "Default"; - }; - readonly "lang-es": { - readonly def: "Español"; - readonly es: "Español"; - readonly fr: "Español"; - readonly he: "Español"; - readonly ja: "Español"; - readonly ko: "Español"; - readonly ru: "Español"; - readonly zh: "Español"; - }; - readonly "lang-fr": { - readonly def: "Français"; - readonly es: "Français"; - readonly fr: "Français"; - readonly he: "Français"; - readonly ja: "Français"; - readonly ko: "Français"; - readonly ru: "Français"; - readonly zh: "Français"; - }; - readonly "lang-he": { - readonly def: "Hebrew"; - readonly he: "עברית"; - }; - readonly "lang-ja": { - readonly def: "日本語"; - readonly es: "Japonés"; - readonly fr: "日本語"; - readonly he: "日本語"; - readonly ja: "日本語"; - readonly ko: "日本語"; - readonly ru: "日本語"; - readonly zh: "日本語"; - }; - readonly "lang-ko": { - readonly def: "한국어"; - readonly fr: "한국어"; - readonly he: "한국어"; - readonly ja: "한국어"; - readonly ko: "한국어"; - readonly ru: "한국어"; - readonly zh: "한국어"; - }; - readonly "lang-ru": { - readonly def: "Русский"; - readonly es: "Ruso"; - readonly fr: "Русский"; - readonly he: "Русский"; - readonly ja: "Русский"; - readonly ko: "Русский"; - readonly ru: "Русский"; - readonly zh: "Русский"; - }; - readonly "lang-zh": { - readonly def: "简体中文"; - readonly es: "Chino simplificado"; - readonly fr: "简体中文"; - readonly he: "简体中文"; - readonly ja: "简体中文"; - readonly ko: "简体中文"; - readonly ru: "简体中文"; - readonly zh: "简体中文"; - }; - readonly "lang-zh-tw": { - readonly def: "繁體中文"; - readonly es: "Chino tradicional"; - readonly fr: "繁體中文"; - readonly he: "繁體中文"; - readonly ja: "繁體中文"; - readonly ko: "繁體中文"; - readonly ru: "繁體中文"; - readonly zh: "繁體中文"; - }; - readonly Later: { - readonly def: "Later"; - readonly es: "Más tarde"; - readonly ja: "後で"; - readonly ko: "나중에"; - readonly ru: "Позже"; - readonly zh: "稍后"; - readonly "zh-tw": "稍後"; - }; - readonly "Limit: {datetime} ({timestamp})": { - readonly def: "Limit: {datetime} ({timestamp})"; - readonly es: "Límite: {datetime} ({timestamp})"; - readonly ja: "制限: {datetime} ({timestamp})"; - readonly ko: "제한: {datetime} ({timestamp})"; - readonly ru: "Лимит: {datetime} ({timestamp})"; - readonly zh: "限制:{datetime}({timestamp})"; - readonly "zh-tw": "限制:{datetime}({timestamp})"; - }; - readonly "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": { - readonly def: "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured."; - readonly es: "LiveSync no puede manejar múltiples bóvedas con mismo nombre sin prefijo. Se configura automáticamente"; - readonly fr: "LiveSync ne peut pas gérer plusieurs coffres portant le même nom sans préfixe distinct. Ceci devrait être configuré automatiquement."; - readonly he: "LiveSync אינו יכול לטפל במספר כספות עם אותו שם ללא קידומת שונה. הדבר אמור להיות מוגדר אוטומטית."; - readonly ja: "LiveSyncは、接頭辞(プレフィックス)のない同名の保管庫(Vault)を扱うことができません。これは自動的に設定されます。"; - readonly ko: "LiveSync는 서로 다른 접두사 없이 동일한 이름을 가진 여러 볼트를 처리할 수 없습니다. 이는 자동으로 구성되어야 합니다."; - readonly ru: "LiveSync не может обработать несколько хранилищ с одинаковым именем без разных префиксов."; - readonly zh: "LiveSync 无法处理具有相同名称但没有不同前缀的多个库。这应该自动配置"; - }; - readonly "liveSyncReplicator.beforeLiveSync": { - readonly def: "Before LiveSync, start OneShot once..."; - readonly es: "Antes de LiveSync, inicia OneShot..."; - readonly fr: "Avant LiveSync, lancement d'un OneShot initial..."; - readonly he: "לפני LiveSync, התחל OneShot פעם אחת..."; - readonly ja: "LiveSyncの前に、まずOneShotを開始します..."; - readonly ko: "LiveSync 전에 OneShot을 먼저 시작합니다..."; - readonly ru: "Перед LiveSync запускаем OneShot..."; - readonly zh: "在LiveSync前,先启动OneShot一次..."; - }; - readonly "liveSyncReplicator.cantReplicateLowerValue": { - readonly def: "We can't replicate more lower value."; - readonly es: "No podemos replicar un valor más bajo."; - readonly fr: "Impossible de répliquer une valeur inférieure."; - readonly he: "לא ניתן לשכפל ערך נמוך יותר."; - readonly ja: "これ以上低い値ではレプリケーション(複製)できません。"; - readonly ko: "더 낮은 값으로 복제할 수 없습니다."; - readonly ru: "Нельзя реплицировать с меньшим значением."; - readonly zh: "我们无法复制更小的值"; - }; - readonly "liveSyncReplicator.checkingLastSyncPoint": { - readonly def: "Looking for the point last synchronized point."; - readonly es: "Buscando el último punto sincronizado."; - readonly fr: "Recherche du dernier point de synchronisation."; - readonly he: "מחפש נקודת הסנכרון האחרונה."; - readonly ja: "最後に同期したポイントを探しています。"; - readonly ko: "마지막으로 동기화된 지점을 찾고 있습니다."; - readonly ru: "Поиск последней точки синхронизации."; - readonly zh: "查找上次同步点"; - }; - readonly "liveSyncReplicator.couldNotConnectTo": { - readonly def: "Could not connect to ${uri} : ${name}\n(${db})"; - readonly es: "No se pudo conectar a ${uri} : ${name} \n(${db})"; - readonly fr: "Connexion impossible à ${uri} : ${name}\n(${db})"; - readonly he: "לא ניתן להתחבר אל ${uri} : ${name}\n(${db})"; - readonly ja: "${uri} : ${name}に接続できませんでした\n(${db})"; - readonly ko: "${uri}에 연결할 수 없습니다: ${name} \n(${db})"; - readonly ru: "Не удалось подключиться к uri : name\n(db)"; - readonly zh: "无法连接到 ${uri} : ${name}\n(${db})"; - }; - readonly "liveSyncReplicator.couldNotConnectToRemoteDb": { - readonly def: "Could not connect to remote database: ${d}"; - readonly es: "No se pudo conectar a base de datos remota: ${d}"; - readonly fr: "Connexion à la base distante impossible : ${d}"; - readonly he: "לא ניתן להתחבר למסד הנתונים המרוחק: ${d}"; - readonly ja: "リモートデータベースに接続できませんでした: ${d}"; - readonly ko: "원격 데이터베이스에 연결할 수 없습니다: ${d}"; - readonly ru: "Не удалось подключиться к удалённой базе данных: d"; - readonly zh: "无法连接到远程数据库:${d}"; - }; - readonly "liveSyncReplicator.couldNotConnectToServer": { - readonly def: "The connection to the remote has been prevented, or failed."; - readonly es: "No se pudo conectar al servidor."; - readonly fr: "Connexion au serveur impossible."; - readonly he: "לא ניתן להתחבר לשרת."; - readonly ja: "サーバーに接続できませんでした。"; - readonly ko: "서버에 연결할 수 없습니다."; - readonly ru: "Не удалось подключиться к серверу."; - readonly zh: "无法连接到服务器"; - }; - readonly "liveSyncReplicator.couldNotConnectToURI": { - readonly def: "Could not connect to ${uri}:${dbRet}"; - readonly es: "No se pudo conectar a ${uri}:${dbRet}"; - readonly fr: "Connexion impossible à ${uri}:${dbRet}"; - readonly he: "לא ניתן להתחבר אל ${uri}:${dbRet}"; - readonly ja: "${uri}に接続できませんでした: ${dbRet}"; - readonly ko: "${uri}에 연결할 수 없습니다: ${dbRet}"; - readonly ru: "Не удалось подключиться к uri:dbRet"; - readonly zh: "无法连接到 ${uri}:${dbRet}"; - }; - readonly "liveSyncReplicator.couldNotMarkResolveRemoteDb": { - readonly def: "Could not mark resolve remote database."; - readonly es: "No se pudo marcar como resuelta la base de datos remota."; - readonly fr: "Impossible de marquer la résolution de la base distante."; - readonly he: "לא ניתן לסמן פתרון למסד הנתונים המרוחק."; - readonly ja: "リモートデータベースを解決済みとしてマークできませんでした。"; - readonly ko: "원격 데이터베이스를 해결됨으로 표시할 수 없습니다."; - readonly ru: "Не удалось отметить удалённую базу данных как разрешённую."; - readonly zh: "无法标记并解决远程数据库"; - }; - readonly "liveSyncReplicator.liveSyncBegin": { - readonly def: "LiveSync begin..."; - readonly es: "Inicio de LiveSync..."; - readonly fr: "Démarrage de LiveSync..."; - readonly he: "LiveSync מתחיל..."; - readonly ja: "LiveSyncを開始..."; - readonly ko: "LiveSync 시작..."; - readonly ru: "Начало LiveSync..."; - readonly zh: "LiveSync 开始..."; - }; - readonly "liveSyncReplicator.lockRemoteDb": { - readonly def: "Lock remote database to prevent data corruption"; - readonly es: "Bloquear base de datos remota para prevenir corrupción de datos"; - readonly fr: "Verrouillage de la base distante pour éviter la corruption des données"; - readonly he: "נועל מסד נתונים מרוחק למניעת פגיעה בנתונים"; - readonly ja: "データ破損を防ぐためリモートデータベースをロック"; - readonly ko: "데이터 손상을 방지하기 위해 원격 데이터베이스를 잠급니다"; - readonly ru: "Блокировка удалённой базы данных для предотвращения повреждения данных"; - readonly zh: "锁定远程数据库以防止数据损坏"; - }; - readonly "liveSyncReplicator.markDeviceResolved": { - readonly def: "Mark this device as 'resolved'."; - readonly es: "Marcar este dispositivo como 'resuelto'."; - readonly fr: "Marquer cet appareil comme « résolu »."; - readonly he: "סמן מכשיר זה כ'נפתר'."; - readonly ja: "このデバイスを『解決済み』としてマーク。"; - readonly ko: "이 기기를 '해결됨'으로 표시합니다."; - readonly ru: "Отметить это устройство как «разрешённое»."; - readonly zh: "将此设备标记为“已解决”"; - }; - readonly "liveSyncReplicator.mismatchedTweakDetected": { - readonly def: "Some mismatches have been detected in the configuration between devices. Running a manual replication will attempt to resolve this issue."; - }; - readonly "liveSyncReplicator.oneShotSyncBegin": { - readonly def: "OneShot Sync begin... (${syncMode})"; - readonly es: "Inicio de sincronización OneShot... (${syncMode})"; - readonly fr: "Démarrage de la synchronisation OneShot... (${syncMode})"; - readonly he: "סנכרון OneShot מתחיל... (${syncMode})"; - readonly ja: "OneShot同期を開始... (${syncMode})"; - readonly ko: "OneShot 동기화 시작... (${syncMode})"; - readonly ru: "Начало OneShot синхронизации... (syncMode)"; - readonly zh: "OneShot同步开始...(${syncMode})"; - }; - readonly "liveSyncReplicator.remoteDbCorrupted": { - readonly def: "Remote database is newer or corrupted, make sure to latest version of self-hosted-livesync installed"; - readonly es: "La base de datos remota es más nueva o está dañada, asegúrese de tener la última versión de self-hosted-livesync instalada"; - readonly fr: "La base distante est plus récente ou corrompue, assurez-vous d'avoir installé la dernière version de self-hosted-livesync"; - readonly he: "מסד הנתונים המרוחק חדש יותר או פגום, ודא שגרסת self-hosted-livesync המותקנת היא העדכנית ביותר"; - readonly ja: "リモートデータベースが新しいか破損しています。self-hosted-livesyncの最新バージョンがインストールされていることを確認してください"; - readonly ko: "원격 데이터베이스가 더 최신이거나 손상되었습니다. 최신 버전의 self-hosted-livesync가 설치되어 있는지 확인하세요"; - readonly ru: "Удалённая база данных новее или повреждена, убедитесь, что установлена последняя версия self-hosted-livesync"; - readonly zh: "远程数据库较新或已损坏,请确保已安装最新版本的self-hosted-livesync"; - }; - readonly "liveSyncReplicator.remoteDbCreatedOrConnected": { - readonly def: "Remote Database Created or Connected"; - readonly es: "Base de datos remota creada o conectada"; - readonly fr: "Base distante créée ou connectée"; - readonly he: "מסד הנתונים המרוחק נוצר או חובר"; - readonly ja: "リモートデータベースが作成または接続されました"; - readonly ko: "원격 데이터베이스가 생성되거나 연결되었습니다"; - readonly ru: "Удалённая база данных создана или подключена"; - readonly zh: "远程数据库已创建或连接"; - }; - readonly "liveSyncReplicator.remoteDbDestroyed": { - readonly def: "Remote Database Destroyed"; - readonly es: "Base de datos remota destruida"; - readonly fr: "Base distante détruite"; - readonly he: "מסד הנתונים המרוחק נהרס"; - readonly ja: "リモートデータベースが削除されました"; - readonly ko: "원격 데이터베이스가 삭제되었습니다"; - readonly ru: "Удалённая база данных уничтожена"; - readonly zh: "远程数据库已销毁"; - }; - readonly "liveSyncReplicator.remoteDbDestroyError": { - readonly def: "Something happened on Remote Database Destroy:"; - readonly es: "Algo ocurrió al destruir base de datos remota:"; - readonly fr: "Un problème est survenu lors de la destruction de la base distante :"; - readonly he: "אירעה שגיאה בהריסת מסד הנתונים המרוחק:"; - readonly ja: "リモートデータベースの削除中に問題が発生しました:"; - readonly ko: "원격 데이터베이스 삭제 중 오류가 발생했습니다:"; - readonly ru: "Произошла ошибка при уничтожении удалённой базы данных:"; - readonly zh: "远程数据库销毁时发生错误:"; - }; - readonly "liveSyncReplicator.remoteDbMarkedResolved": { - readonly def: "Remote database has been marked resolved."; - readonly es: "Base de datos remota marcada como resuelta."; - readonly fr: "La base distante a été marquée comme résolue."; - readonly he: "מסד הנתונים המרוחק סומן כנפתר."; - readonly ja: "リモートデータベースが解決済みとしてマークされました。"; - readonly ko: "원격 데이터베이스가 해결됨으로 표시되었습니다."; - readonly ru: "Удалённая база данных отмечена как разрешённая."; - readonly zh: "远程数据库已标记为已解决"; - }; - readonly "liveSyncReplicator.replicationClosed": { - readonly def: "Replication closed"; - readonly es: "Replicación cerrada"; - readonly fr: "Réplication fermée"; - readonly he: "השכפול נסגר"; - readonly ja: "レプリケーション(複製)が終了しました"; - readonly ko: "복제가 종료되었습니다"; - readonly ru: "Репликация закрыта"; - readonly zh: "同步已关闭"; - }; - readonly "liveSyncReplicator.replicationInProgress": { - readonly def: "Replication is already in progress"; - readonly es: "Replicación en curso"; - readonly fr: "Une réplication est déjà en cours"; - readonly he: "שכפול כבר מתבצע"; - readonly ja: "レプリケーション(複製)は既に進行中です"; - readonly ko: "복제가 이미 진행 중입니다"; - readonly ru: "Репликация уже выполняется"; - readonly zh: "同步正在进行中"; - }; - readonly "liveSyncReplicator.retryLowerBatchSize": { - readonly def: "Retry with lower batch size:${batch_size}/${batches_limit}"; - readonly es: "Reintentar con tamaño de lote más bajo:${batch_size}/${batches_limit}"; - readonly fr: "Nouvelle tentative avec une taille de lot réduite :${batch_size}/${batches_limit}"; - readonly he: "מנסה שוב עם גודל אצווה קטן יותר:${batch_size}/${batches_limit}"; - readonly ja: "より小さいバッチサイズで再試行: ${batch_size}/${batches_limit}"; - readonly ko: "더 낮은 일괄 크기로 재시도: ${batch_size}/${batches_limit}"; - readonly ru: "Повтор с меньшим размером пакета: batch_size/batches_limit"; - readonly zh: "使用更小的批量大小重试:${batch_size}/${batches_limit}"; - }; - readonly "liveSyncReplicator.unlockRemoteDb": { - readonly def: "Unlock remote database to prevent data corruption"; - readonly es: "Desbloquear base de datos remota para prevenir corrupción de datos"; - readonly fr: "Déverrouillage de la base distante pour éviter la corruption des données"; - readonly he: "מבטל נעילת מסד הנתונים המרוחק למניעת פגיעה בנתונים"; - readonly ja: "データ破損を防ぐためリモートデータベースをアンロック"; - readonly ko: "데이터 손상을 방지하기 위해 원격 데이터베이스를 잠금 해제합니다"; - readonly ru: "Разблокировка удалённой базы данных для предотвращения повреждения данных"; - readonly zh: "解锁远程数据库以防止数据损坏"; - }; - readonly "liveSyncSetting.errorNoSuchSettingItem": { - readonly def: "No such setting item: ${key}"; - readonly es: "No existe el ajuste: ${key}"; - readonly fr: "Élément de paramètre inexistant : ${key}"; - readonly he: "פריט הגדרה לא קיים: ${key}"; - readonly ja: "その設定項目は存在しません: ${key}"; - readonly ko: "해당 설정 항목이 없습니다: ${key}"; - readonly ru: "Такого параметра настройки не существует: key"; - readonly zh: "没有此设置项:${key}"; - }; - readonly "liveSyncSetting.originalValue": { - readonly def: "Original: ${value}"; - readonly es: "Original: ${value}"; - readonly fr: "Original : ${value}"; - readonly he: "ערך מקורי: ${value}"; - readonly ja: "元の値: ${value}"; - readonly ko: "원본: ${value}"; - readonly ru: "Оригинал: value"; - readonly zh: "原始值:${value}"; - }; - readonly "liveSyncSetting.valueShouldBeInRange": { - readonly def: "The value should ${min} < value < ${max}"; - readonly es: "El valor debe estar entre ${min} y ${max}"; - readonly fr: "La valeur doit être ${min} < valeur < ${max}"; - readonly he: "הערך צריך להיות ${min} < ערך < ${max}"; - readonly ja: "値は ${min} < 値 < ${max} の範囲である必要があります"; - readonly ko: "값은 ${min} < 값 < ${max} 범위에 있어야 합니다"; - readonly ru: "Значение должно быть min < значение < max"; - readonly zh: "值应该在 ${min} < value < ${max} 之间"; - }; - readonly "liveSyncSettings.btnApply": { - readonly def: "Apply"; - readonly es: "Aplicar"; - readonly fr: "Appliquer"; - readonly he: "החל"; - readonly ja: "適用"; - readonly ko: "적용"; - readonly ru: "Применить"; - readonly zh: "应用"; - }; - readonly "Local Database Tweak": { - readonly def: "Local Database Tweak"; - readonly es: "Ajustes de la base de datos local"; - readonly ja: "ローカルデータベースの調整"; - readonly ko: "로컬 데이터베이스 조정"; - readonly ru: "Настройки локальной базы данных"; - }; - readonly Lock: { - readonly def: "Lock"; - readonly es: "Bloquear"; - readonly ja: "ロック"; - readonly ko: "잠금"; - readonly ru: "Заблокировать"; - readonly zh: "锁定"; - readonly "zh-tw": "鎖定"; - }; - readonly "Lock Server": { - readonly def: "Lock Server"; - readonly es: "Bloquear servidor"; - readonly ja: "サーバーをロック"; - readonly ko: "서버 잠금"; - readonly ru: "Заблокировать сервер"; - readonly zh: "锁定服务器"; - readonly "zh-tw": "鎖定伺服器"; - }; - readonly "Lock the remote server to prevent synchronization with other devices.": { - readonly def: "Lock the remote server to prevent synchronization with other devices."; - readonly es: "Bloquea el servidor remoto para impedir la sincronización con otros dispositivos."; - readonly ja: "他のデバイスとの同期を防ぐため、リモートサーバーをロックします。"; - readonly ko: "다른 기기와의 동기화를 방지하기 위해 원격 서버를 잠급니다."; - readonly ru: "Блокирует удалённый сервер, чтобы запретить синхронизацию с другими устройствами."; - readonly zh: "锁定远端服务器,以阻止其他设备继续同步。"; - readonly "zh-tw": "鎖定遠端伺服器,以防止其他裝置進行同步。"; - }; - readonly "logPane.autoScroll": { - readonly def: "Auto scroll"; - readonly es: "Autodesplazamiento"; - readonly fr: "Défilement automatique"; - readonly he: "גלילה אוטומטית"; - readonly ja: "自動スクロール"; - readonly ko: "자동 스크롤"; - readonly ru: "Автопрокрутка"; - readonly zh: "自动滚动"; - }; - readonly "logPane.logWindowOpened": { - readonly def: "Log window opened"; - readonly es: "Ventana de registro abierta"; - readonly fr: "Fenêtre des journaux ouverte"; - readonly he: "חלון יומן נפתח"; - readonly ja: "ログウィンドウが開かれました"; - readonly ko: "로그 창이 열렸습니다"; - readonly ru: "Окно лога открыто"; - readonly zh: "日志窗口已打开"; - }; - readonly "logPane.pause": { - readonly def: "Pause"; - readonly es: "Pausar"; - readonly fr: "Pause"; - readonly he: "השהה"; - readonly ja: "一時停止"; - readonly ko: "일시 중단"; - readonly ru: "Пауза"; - readonly zh: "暂停"; - }; - readonly "logPane.title": { - readonly def: "Self-hosted LiveSync Log"; - readonly es: "Registro de Self-hosted LiveSync"; - readonly fr: "Journaux Self-hosted LiveSync"; - readonly he: "יומן Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSync ログ"; - readonly ko: "Self-hosted LiveSync 로그"; - readonly ru: "Лог Self-hosted LiveSync"; - readonly zh: "Self-hosted LiveSync 日志"; - }; - readonly "logPane.wrap": { - readonly def: "Wrap"; - readonly es: "Ajustar"; - readonly fr: "Retour à la ligne"; - readonly he: "גלישת שורות"; - readonly ja: "折り返し"; - readonly ko: "줄 바꿈"; - readonly ru: "Перенос"; - readonly zh: "自动换行"; - }; - readonly "Maximum delay for batch database updating": { - readonly def: "Maximum delay for batch database updating"; - readonly es: "Retraso máximo para actualización por lotes"; - readonly fr: "Délai maximum pour la mise à jour groupée de la base"; - readonly he: "עיכוב מקסימלי לעדכון אצווה של מסד נתונים"; - readonly ja: "バッチデータベース更新の最大遅延"; - readonly ko: "일괄 데이터베이스 업데이트 최대 지연"; - readonly ru: "Максимальная задержка пакетного обновления базы данных"; - readonly zh: "批量数据库更新的最大延迟"; - }; - readonly "Maximum file size": { - readonly def: "Maximum file size"; - readonly es: "Tamaño máximo de archivo"; - readonly fr: "Taille maximale de fichier"; - readonly he: "גודל קובץ מקסימלי"; - readonly ja: "最大ファイル容量"; - readonly ko: "최대 파일 크기"; - readonly ru: "Максимальный размер файла"; - readonly zh: "最大文件大小"; - }; - readonly "Maximum Incubating Chunk Size": { - readonly def: "Maximum Incubating Chunk Size"; - readonly es: "Tamaño máximo de chunks incubados"; - readonly fr: "Taille maximale des fragments en incubation"; - readonly he: "גודל מקסימלי לנתח בבישול"; - readonly ja: "保持するチャンクの最大サイズ"; - readonly ko: "임시 보관 변경 기록의 최대 크기"; - readonly ru: "Максимальный размер инкубируемого чанка"; - readonly zh: "最大孵化块大小"; - }; - readonly "Maximum Incubating Chunks": { - readonly def: "Maximum Incubating Chunks"; - readonly es: "Máximo de chunks incubados"; - readonly fr: "Nombre maximum de fragments en incubation"; - readonly he: "מספר מקסימלי של נתחים בבישול"; - readonly ja: "一時保管する最大チャンク数"; - readonly ko: "임시 보관 중인 변경 기록 최대 수"; - readonly ru: "Максимальное количество инкубируемых чанков"; - readonly zh: "最大孵化块数"; - }; - readonly "Maximum Incubation Period": { - readonly def: "Maximum Incubation Period"; - readonly es: "Periodo máximo de incubación"; - readonly fr: "Période maximale d'incubation"; - readonly he: "תקופת בישול מקסימלית"; - readonly ja: "最大保持期限"; - readonly ko: "변경 기록 임시 보관 최대 시간"; - readonly ru: "Максимальный период инкубации"; - readonly zh: "最大孵化期"; - }; - readonly "MB (0 to disable).": { - readonly def: "MB (0 to disable)."; - readonly es: "MB (0 para desactivar)"; - readonly fr: "Mo (0 pour désactiver)."; - readonly he: "MB (0 לביטול)."; - readonly ja: "MB (0で無効化)。"; - readonly ko: "MB (0으로 설정하면 비활성화)."; - readonly ru: "МБ (0 для отключения)."; - readonly zh: "MB(0为禁用)"; - }; - readonly "Memory cache": { - readonly def: "Memory cache"; - readonly es: "Caché en memoria"; - readonly ja: "メモリキャッシュ"; - readonly ko: "메모리 캐시"; - readonly ru: "Кэш в памяти"; - }; - readonly "Memory cache size (by total characters)": { - readonly def: "Memory cache size (by total characters)"; - readonly es: "Tamaño caché memoria (por caracteres)"; - readonly fr: "Taille du cache mémoire (par nombre total de caractères)"; - readonly he: "גודל מטמון זיכרון (לפי סה\"כ תווים)"; - readonly ja: "全体でキャッシュする文字数"; - readonly ko: "메모리 캐시 크기 (총 문자 수)"; - readonly ru: "Размер кэша памяти (по общему количеству символов)"; - readonly zh: "内存缓存大小(按总字符数)"; - }; - readonly "Memory cache size (by total items)": { - readonly def: "Memory cache size (by total items)"; - readonly es: "Tamaño caché memoria (por ítems)"; - readonly fr: "Taille du cache mémoire (par nombre total d'éléments)"; - readonly he: "גודל מטמון זיכרון (לפי סה\"כ פריטים)"; - readonly ja: "全体のキャッシュサイズ"; - readonly ko: "메모리 캐시 크기 (총 항목 수)"; - readonly ru: "Размер кэша памяти (по общему количеству элементов)"; - readonly zh: "内存缓存大小(按总项目数)"; - }; - readonly Merge: { - readonly def: "Merge"; - readonly es: "Fusionar"; - readonly ja: "マージ"; - readonly ko: "병합"; - readonly ru: "Объединить"; - readonly zh: "合并"; - }; - readonly "Minimum delay for batch database updating": { - readonly def: "Minimum delay for batch database updating"; - readonly es: "Retraso mínimo para actualización por lotes"; - readonly fr: "Délai minimum pour la mise à jour groupée de la base"; - readonly he: "עיכוב מינימלי לעדכון אצווה של מסד נתונים"; - readonly ja: "バッチデータベース更新の最小遅延"; - readonly ko: "일괄 데이터베이스 업데이트 최소 지연"; - readonly ru: "Минимальная задержка пакетного обновления базы данных"; - readonly zh: "批量数据库更新的最小延迟"; - }; - readonly "Minimum interval for syncing": { - readonly def: "Minimum interval for syncing"; - readonly fr: "Intervalle minimum pour la synchronisation"; - readonly he: "מרווח מינימלי לסנכרון"; - readonly ja: "同期間隔の最小値"; - readonly ko: "동기화 최소 간격"; - readonly ru: "Минимальный интервал синхронизации"; - readonly zh: "同步最小间隔"; - readonly "zh-tw": "同步最小間隔"; - }; - readonly "moduleCheckRemoteSize.logCheckingStorageSizes": { - readonly def: "Checking storage sizes"; - readonly es: "Comprobando tamaños de almacenamiento"; - readonly fr: "Vérification des tailles de stockage"; - readonly he: "בודק גדלי אחסון"; - readonly ja: "ストレージサイズを確認中"; - readonly ko: "스토리지 크기 확인 중"; - readonly ru: "Проверка размеров хранилища"; - readonly zh: "正在检查存储大小"; - }; - readonly "moduleCheckRemoteSize.logCurrentStorageSize": { - readonly def: "Remote storage size: ${measuredSize}"; - readonly es: "Tamaño del almacenamiento remoto: ${measuredSize}"; - readonly fr: "Taille du stockage distant : ${measuredSize}"; - readonly he: "גודל אחסון מרוחק: ${measuredSize}"; - readonly ja: "リモートストレージサイズ: ${measuredSize}"; - readonly ko: "원격 스토리지 크기: ${measuredSize}"; - readonly ru: "Размер удалённого хранилища: measuredSize"; - readonly zh: "远程存储大小:${measuredSize}"; - }; - readonly "moduleCheckRemoteSize.logExceededWarning": { - readonly def: "Remote storage size: ${measuredSize} exceeded ${notifySize}"; - readonly es: "Tamaño del almacenamiento remoto: ${measuredSize} superó ${notifySize}"; - readonly fr: "Taille du stockage distant : ${measuredSize} a dépassé ${notifySize}"; - readonly he: "גודל אחסון מרוחק: ${measuredSize} עלה על ${notifySize}"; - readonly ja: "リモートストレージサイズ: ${measuredSize} が ${notifySize} を超過しました"; - readonly ko: "원격 스토리지 크기: ${measuredSize}가 ${notifySize}를 초과했습니다"; - readonly ru: "Размер удалённого хранилища: measuredSize превысил notifySize"; - readonly zh: "远程存储大小:${measuredSize} 超过 ${notifySize}"; - }; - readonly "moduleCheckRemoteSize.logThresholdEnlarged": { - readonly def: "Threshold has been enlarged to ${size}MB"; - readonly es: "El umbral se ha ampliado a ${size}MB"; - readonly fr: "Le seuil a été augmenté à ${size} Mo"; - readonly he: "הסף הורחב ל-${size}MB"; - readonly ja: "しきい値が ${size}MB に設定されました"; - readonly ko: "임계값이 ${size}MB로 증가되었습니다"; - readonly ru: "Порог увеличен до sizeМБ"; - readonly zh: "阈值已扩大到 ${size}MB"; - }; - readonly "moduleCheckRemoteSize.msgConfirmRebuild": { - readonly def: "This may take a bit of a long time. Do you really want to rebuild everything now?"; - readonly es: "Esto puede llevar un poco de tiempo. ¿Realmente quieres reconstruir todo ahora?"; - readonly fr: "Cela peut prendre un certain temps. Voulez-vous vraiment tout reconstruire maintenant ?"; - readonly he: "פעולה זו עשויה לקחת זמן מה. האם אתה בטוח שברצונך לבנות מחדש עכשיו?"; - readonly ja: "これは少し時間がかかる場合があります。本当に今すべてを再構築しますか?"; - readonly ko: "시간이 꽤 오래 걸릴 수 있습니다. 정말 지금 모든 것을 재구축하시겠습니까?"; - readonly ru: "Это может занять некоторое время. Вы действительно хотите перестроить всё сейчас?"; - readonly zh: "这可能需要一些时间。您真的想现在重建所有内容吗?"; - }; - readonly "moduleCheckRemoteSize.msgDatabaseGrowing": { - readonly def: "**Your database is getting larger!** But do not worry, we can address it now. The time before running out of space on the remote storage.\n\n| Measured size | Configured size |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> If you have been using it for many years, there may be unreferenced chunks - that is, garbage - accumulating in the database. Therefore, we recommend rebuilding everything. It will probably become much smaller.\n>\n> If the volume of your vault is simply increasing, it is better to rebuild everything after organizing the files. Self-hosted LiveSync does not delete the actual data even if you delete it to speed up the process. It is roughly [documented](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> If you don't mind the increase, you can increase the notification limit by 100MB. This is the case if you are running it on your own server. However, it is better to rebuild everything from time to time.\n>\n\n> [!WARNING]\n> If you perform rebuild everything, make sure all devices are synchronised. The plug-in will merge as much as possible, though.\n"; - readonly es: "**¡Tu base de datos está creciendo!** Pero no te preocupes, podemos abordarlo ahora. El tiempo antes de quedarse sin espacio en el almacenamiento remoto.\n\n| Tamaño medido | Tamaño configurado |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> Si lo has estado utilizando durante muchos años, puede haber fragmentos no referenciados - es decir, basura - acumulándose en la base de datos. Por lo tanto, recomendamos reconstruir todo. Probablemente se volverá mucho más pequeño.\n>\n> Si el volumen de tu bóveda simplemente está aumentando, es mejor reconstruir todo después de organizar los archivos. Self-hosted LiveSync no elimina los datos reales incluso si los eliminas para acelerar el proceso. Está aproximadamente [documentado](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> Si no te importa el aumento, puedes aumentar el límite de notificación en 100 MB. Este es el caso si lo estás ejecutando en tu propio servidor. Sin embargo, es mejor reconstruir todo de vez en cuando.\n>\n\n> [!WARNING]\n> Si realizas la reconstrucción completa, asegúrate de que todos los dispositivos estén sincronizados. El complemento fusionará tanto como sea posible, sin embargo.\n"; - readonly fr: "**Votre base de données grossit !** Pas d'inquiétude, nous pouvons y remédier dès maintenant, avant de manquer d'espace sur le stockage distant.\n\n| Taille mesurée | Taille configurée |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> Si vous l'utilisez depuis de nombreuses années, il peut y avoir des fragments non référencés — des déchets, en somme — accumulés dans la base. Nous recommandons donc de tout reconstruire. Cela réduira probablement beaucoup la taille.\n>\n> Si le volume de votre coffre augmente simplement, il est préférable de tout reconstruire après avoir organisé les fichiers. Self-hosted LiveSync ne supprime pas réellement les données même si vous les effacez, afin d'accélérer le processus. Ceci est documenté grossièrement [ici](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> Si cela ne vous dérange pas, vous pouvez augmenter la limite de notification de 100 Mo. C'est le cas si vous l'exécutez sur votre propre serveur. Il reste toutefois préférable de tout reconstruire de temps en temps.\n>\n\n> [!WARNING]\n> Si vous tout reconstruisez, assurez-vous que tous les appareils sont synchronisés. Le plug-in fusionnera autant que possible cependant.\n"; - readonly he: "**מסד הנתונים שלך הולך וגדל!** אל תדאג, אנחנו יכולים לטפל בזה עכשיו. הזמן שנשאר עד לאזול המקום באחסון המרוחק.\n\n| גודל נמדד | גודל מוגדר |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> אם אתה משתמש בפלאגין כבר שנים רבות, ייתכן שנצברו נתחים לא מקושרים — כלומר, זבל — במסד הנתונים. לכן, אנו ממליצים לבנות הכל מחדש. ככל הנראה מסד הנתונים יהיה קטן בהרבה לאחר מכן.\n>\n> אם נפח הכספת שלך פשוט גדל, עדיף לבנות מחדש לאחר ארגון הקבצים. Self-hosted LiveSync אינו מוחק נתונים בפועל גם כאשר אתה מוחק קבצים כדי להאיץ את התהליך. הדבר [מתועד בפירוט](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md).\n>\n> אם אינך מוטרד מהגידול, ניתן להגדיל את סף ההתראה ב-100MB. הדבר מתאים אם השרת הוא שלך. עם זאת, מומלץ לבנות מחדש מעת לעת.\n>\n\n> [!WARNING]\n> אם תבנה מחדש, ודא שכל המכשירים מסונכרנים. הפלאגין ינסה למזג כמה שניתן.\n"; - readonly ja: "**データベースが大きくなっています!** でも心配しないでください。リモートストレージの容量が不足する前に対応できます。\n\n| 測定サイズ | 設定サイズ |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 長年使用している場合、参照されていないチャンク(つまりゴミ)がデータベースに蓄積している可能性があります。そのため、すべてを再構築することをお勧めします。おそらくかなり小さくなるでしょう。\n>\n> 単純に保管庫の容量が増えている場合は、事前にファイルを整理してからすべてを再構築するのが良いでしょう。Self-hosted LiveSyncは処理速度を上げるため、削除しても実際のデータを削除しません。これはおおまかに[documentation](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)に記載されています。\n>\n> 増加を気にしない場合は、通知制限を100MB単位で増やすことができます。これは自分のサーバーで実行している場合に適しています。ただし、定期的にすべてを再構築する方が良いでしょう。\n>\n\n> [!WARNING]\n> すべてを再構築する場合は、すべてのデバイスが同期されていることを確認してください。もちろん、プラグインは可能な限り解決しようと努力はしますけど...\n"; - readonly ko: "**데이터베이스 용량이 점점 커지고 있습니다!** 하지만 걱정하지 마세요. 아직 원격 스토리지 공간이 완전히 부족해진 건 아닙니다.\n\n| 측정된 크기 | 설정된 한도 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 오랜 기간 사용했다면 참조되지 않는 청크, 즉 '쓰레기 데이터'가 쌓였을 수 있습니다. 이 경우 전체 재구성을 권장합니다. 용량이 훨씬 줄어들 수 있습니다.\n> \n> 단순히 볼트 자체 용량이 커지고 있는 것이라면, 먼저 파일을 정리한 후 전체를 재구성하는 것이 좋습니다. Self-hosted LiveSync는 처리 속도를 위해 삭제해도 실제 데이터를 바로 지우지 않습니다. 이 내용은 [기술 문서](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)에 간략히 정리되어 있습니다.\n> \n> 용량 증가가 괜찮다면 알림 임계치를 100MB 단위로 높일 수 있습니다. 직접 서버를 운영하는 경우에 적합한 방법입니다. 다만, 가끔은 전체 재구성을 해주는 것이 바람직합니다.\n\n> [!WARNING]\n> 전체 재구성을 실행할 경우, 모든 기기가 반드시 동기화되어 있어야 합니다. 플러그인이 최대한 병합하려고 시도하긴 하지만 완전하지 않을 수 있습니다."; - readonly ru: "Ваша база данных увеличивается! Но не волнуйтесь, мы можем решить это сейчас."; - readonly zh: "**您的数据库正在变大!** 但别担心,我们现在可以解决它。在远程存储空间用完之前还有时间。\n\n| 测量大小 | 配置大小 |\n| --- | --- |\n| ${estimatedSize} | ${maxSize} |\n\n> [!MORE]-\n> 如果您已经使用了很多年,数据库中可能会积累未引用的 chunks——也就是垃圾。因此,我们建议重建所有内容。它可能会变得小得多。\n> \n> 如果您的库容量只是在增加,最好在整理文件后重建所有内容。即使您为了加速过程删除了文件,Self-hosted LiveSync 也不会删除实际数据。这大致[有文档记录](https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/tech_info.md)。\n> \n> 如果您不介意增加,可以将通知限制增加 100MB。如果您在自己的服务器上运行,就是这种情况。但是,最好还是不时地重建所有内容。\n> \n\n> [!WARNING]\n> 如果您执行重建所有内容,请确保所有设备都已同步。尽管如此,插件会尽可能地合并\n"; - }; - readonly "moduleCheckRemoteSize.msgSetDBCapacity": { - readonly def: "We can set a maximum database capacity warning, **to take action before running out of space on the remote storage**.\nDo you want to enable this?\n\n> [!MORE]-\n> - 0: Do not warn about storage size.\n> This is recommended if you have enough space on the remote storage especially you have self-hosted. And you can check the storage size and rebuild manually.\n> - 800: Warn if the remote storage size exceeds 800MB.\n> This is recommended if you are using fly.io with 1GB limit or IBM Cloudant.\n> - 2000: Warn if the remote storage size exceeds 2GB.\n\nIf we have reached the limit, we will be asked to enlarge the limit step by step.\n"; - readonly es: "Podemos configurar una advertencia de capacidad máxima de base de datos, **para tomar medidas antes de quedarse sin espacio en el almacenamiento remoto**.\n¿Quieres habilitar esto?\n\n> [!MORE]-\n> - 0: No advertir sobre el tamaño del almacenamiento.\n> Esto es recomendado si tienes suficiente espacio en el almacenamiento remoto, especialmente si lo tienes autoalojado. Y puedes comprobar el tamaño del almacenamiento y reconstruir manualmente.\n> - 800: Advertir si el tamaño del almacenamiento remoto supera los 800 MB.\n> Esto es recomendado si estás usando fly.io con un límite de 1 GB o IBM Cloudant.\n> - 2000: Advertir si el tamaño del almacenamiento remoto supera los 2 GB.\n\nSi hemos alcanzado el límite, se nos pedirá que aumentemos el límite paso a paso.\n"; - readonly fr: "Nous pouvons définir un avertissement de capacité maximale de la base de données, **afin d'agir avant de manquer d'espace sur le stockage distant**.\nVoulez-vous activer ceci ?\n\n> [!MORE]-\n> - 0 : Ne pas avertir sur la taille de stockage.\n> Recommandé si vous avez suffisamment d'espace sur le stockage distant, surtout en auto-hébergement. Vous pouvez vérifier la taille et reconstruire manuellement.\n> - 800 : Avertir si la taille du stockage distant dépasse 800 Mo.\n> Recommandé si vous utilisez fly.io avec une limite de 1 Go ou IBM Cloudant.\n> - 2000 : Avertir si la taille du stockage distant dépasse 2 Go.\n\nSi la limite est atteinte, il nous sera proposé de l'augmenter étape par étape.\n"; - readonly he: "ניתן להגדיר אזהרת קיבולת מקסימלית של מסד הנתונים, **כדי לנקוט פעולה לפני שנגמר המקום באחסון המרוחד**.\nהאם להפעיל זאת?\n\n> [!MORE]-\n> - 0: אל תזהיר על גודל האחסון.\n> מומלץ אם יש לך מספיק מקום באחסון המרוחד, בעיקר אם השרת הוא שלך. ניתן לבדוק את גודל האחסון ולבנות מחדש ידנית.\n> - 800: הזהר אם גודל האחסון המרוחד עולה על 800MB.\n> מומלץ אם אתה משתמש ב-fly.io עם מגבלת 1GB או ב-IBM Cloudant.\n> - 2000: הזהר אם גודל האחסון המרוחד עולה על 2GB.\n\nאם הגענו למגבלה, תתבקש להרחיב את הסף בהדרגה.\n"; - readonly ja: "リモートストレージの容量が不足する前に対策を講じるため、**最大データベース容量の警告**を設定できます。\nこれを有効にしますか?\n\n> [!MORE]-\n> - 0: ストレージサイズについて警告しない。\n> 自宅サーバーなど、リモートストレージに十分な容量がある場合に推奨されます。ストレージサイズを確認し、手動で再構築できます。\n> - 800: リモートストレージサイズが800MBを超えたら警告。\n> 1GB制限のfly.ioやIBM Cloudantを使用している場合に推奨されます。\n> - 2000: リモートストレージサイズが2GBを超えたら警告。\n\n制限に達した場合、段階的に制限を増やすよう求められます。\n"; - readonly ko: "**원격 스토리지 공간이 부족해지기 전에 미리 조치할 수 있도록** 데이터베이스 용량 경고를 설정할 수 있습니다.\n이 기능을 활성화하시겠습니까?\n\n> [!MORE]-\n> - 0: 스토리지 용량에 대한 경고 없음\n> 자체 서버를 사용하는 등 여유 공간이 충분한 경우에 권장됩니다. 스토리지 용량을 직접 확인하고 수동으로 재구성할 수 있습니다.\n> - 800: 원격 스토리지 용량이 800MB를 초과하면 경고\n> 1GB 제한이 있는 fly.io나 IBM Cloudant 사용 시 권장됩니다.\n> - 2000: 원격 스토리지 용량이 2GB를 초과하면 경고\n\n설정한 용량 한도에 도달하면, 단계적으로 경고 한도를 늘릴지 여부를 묻게 됩니다.\n"; - readonly ru: "Можно установить предупреждение о максимальной ёмкости базы данных."; - readonly zh: "我们可以设置一个最大数据库容量警告,**以便在远程存储空间耗尽前采取行动**。\n您想启用这个功能吗?\n\n> [!MORE]-\n> - 0: 不警告存储大小。\n> 如果您在远程存储(尤其是自托管)上有足够的空间,则推荐此选项。您可以手动检查存储大小并重建。\n> - 800: 如果远程存储大小超过 800MB 则发出警告。\n> 如果您使用的是 fly.io(1GB 限制) 或 IBM Cloudant,则推荐此选项。\n> - 2000: 如果远程存储大小超过 2GB 则发出警告。\n\n如果达到限制,系统会要求我们逐步增大限制\n"; - }; - readonly "moduleCheckRemoteSize.option2GB": { - readonly def: "2GB (Standard)"; - readonly es: "2GB (Estándar)"; - readonly fr: "2 Go (Standard)"; - readonly he: "2GB (סטנדרטי)"; - readonly ja: "2GB (標準)"; - readonly ko: "2GB (표준)"; - readonly ru: "2ГБ (Стандарт)"; - readonly zh: "2GB (标准)"; - }; - readonly "moduleCheckRemoteSize.option800MB": { - readonly def: "800MB (Cloudant, fly.io)"; - readonly es: "800MB (Cloudant, fly.io)"; - readonly fr: "800 Mo (Cloudant, fly.io)"; - readonly he: "800MB (Cloudant, fly.io)"; - readonly ja: "800MB (Cloudant, fly.io)"; - readonly ko: "800MB (Cloudant, fly.io)"; - readonly ru: "800МБ (Cloudant, fly.io)"; - readonly zh: "800MB (Cloudant, fly.io)"; - }; - readonly "moduleCheckRemoteSize.optionAskMeLater": { - readonly def: "Ask me later"; - readonly es: "Pregúntame más tarde"; - readonly fr: "Me demander plus tard"; - readonly he: "שאל מאוחר יותר"; - readonly ja: "後で確認する"; - readonly ko: "나중에 물어보기"; - readonly ru: "Спросить позже"; - readonly zh: "稍后问我"; - }; - readonly "moduleCheckRemoteSize.optionDismiss": { - readonly def: "Dismiss"; - readonly es: "Descartar"; - readonly fr: "Ignorer"; - readonly he: "דחה"; - readonly ja: "無視"; - readonly ko: "무시"; - readonly ru: "Отклонить"; - readonly zh: "忽略"; - }; - readonly "moduleCheckRemoteSize.optionIncreaseLimit": { - readonly def: "increase to ${newMax}MB"; - readonly es: "aumentar a ${newMax}MB"; - readonly fr: "augmenter à ${newMax} Mo"; - readonly he: "הגדל ל-${newMax}MB"; - readonly ja: "${newMax}MBに設定"; - readonly ko: "${newMax}MB로 증가"; - readonly ru: "увеличить до newMaxМБ"; - readonly zh: "增加到 ${newMax}MB"; - }; - readonly "moduleCheckRemoteSize.optionNoWarn": { - readonly def: "No, never warn please"; - readonly es: "No, nunca advertir por favor"; - readonly fr: "Non, ne jamais avertir"; - readonly he: "לא, אל תזהיר בכלל"; - readonly ja: "いいえ、警告しないでください"; - readonly ko: "아니요, 경고하지 마세요"; - readonly ru: "Нет, не уведомлять"; - readonly zh: "不,请永远不要警告"; - }; - readonly "moduleCheckRemoteSize.optionRebuildAll": { - readonly def: "Rebuild Everything Now"; - readonly es: "Reconstruir todo ahora"; - readonly fr: "Tout reconstruire maintenant"; - readonly he: "בנה הכל מחדש עכשיו"; - readonly ja: "今すべてを再構築"; - readonly ko: "지금 모든 것 재구축"; - readonly ru: "Перестроить всё сейчас"; - readonly zh: "立即重建所有内容"; - }; - readonly "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": { - readonly def: "Remote storage size exceeded the limit"; - readonly es: "El tamaño del almacenamiento remoto superó el límite"; - readonly fr: "Taille du stockage distant au-delà de la limite"; - readonly he: "גודל האחסון המרוחד חרג מהמגבלה"; - readonly ja: "リモートストレージサイズが制限を超過しました"; - readonly ko: "원격 스토리지 크기가 제한을 초과했습니다"; - readonly ru: "Размер удалённого хранилища превысил лимит"; - readonly zh: "远程存储大小超出限制"; - }; - readonly "moduleCheckRemoteSize.titleDatabaseSizeNotify": { - readonly def: "Setting up database size notification"; - readonly es: "Configuración de notificación de tamaño de base de datos"; - readonly fr: "Configuration de la notification de taille de base"; - readonly he: "הגדרת התראה על גודל מסד נתונים"; - readonly ja: "データベースサイズ通知の設定"; - readonly ko: "데이터베이스 크기 알림 설정"; - readonly ru: "Настройка уведомления о размере базы данных"; - readonly zh: "设置数据库大小通知"; - }; - readonly "moduleInputUIObsidian.defaultTitleConfirmation": { - readonly def: "Confirmation"; - readonly es: "Confirmación"; - readonly fr: "Confirmation"; - readonly he: "אישור"; - readonly ja: "確認"; - readonly ko: "확인"; - readonly ru: "Подтверждение"; - readonly zh: "确认"; - }; - readonly "moduleInputUIObsidian.defaultTitleSelect": { - readonly def: "Select"; - readonly es: "Seleccionar"; - readonly fr: "Sélection"; - readonly he: "בחר"; - readonly ja: "選択"; - readonly ko: "선택"; - readonly ru: "Выбор"; - readonly zh: "选择"; - }; - readonly "moduleInputUIObsidian.optionNo": { - readonly def: "No"; - readonly es: "No"; - readonly fr: "Non"; - readonly he: "לא"; - readonly ja: "いいえ"; - readonly ko: "아니요"; - readonly ru: "Нет"; - readonly zh: "否"; - }; - readonly "moduleInputUIObsidian.optionYes": { - readonly def: "Yes"; - readonly es: "Sí"; - readonly fr: "Oui"; - readonly he: "כן"; - readonly ja: "はい"; - readonly ko: "예"; - readonly ru: "Да"; - readonly zh: "是"; - }; - readonly "moduleLiveSyncMain.logAdditionalSafetyScan": { - readonly def: "Additional safety scan..."; - readonly es: "Escanéo de seguridad adicional..."; - readonly fr: "Analyse de sécurité supplémentaire..."; - readonly he: "סריקת בטיחות נוספת..."; - readonly ja: "追加の安全スキャン中..."; - readonly ko: "추가 안전 검사 중..."; - readonly ru: "Дополнительная проверка безопасности..."; - readonly zh: "额外的安全扫描..."; - }; - readonly "moduleLiveSyncMain.logLoadingPlugin": { - readonly def: "Loading plugin..."; - readonly es: "Cargando complemento..."; - readonly fr: "Chargement du plugin..."; - readonly he: "טוען תוסף..."; - readonly ja: "プラグインをロード中..."; - readonly ko: "플러그인 로딩 중..."; - readonly ru: "Загрузка плагина..."; - readonly zh: "正在加载插件..."; - }; - readonly "moduleLiveSyncMain.logPluginInitCancelled": { - readonly def: "Plugin initialisation was cancelled by a module"; - readonly es: "La inicialización del complemento fue cancelada por un módulo"; - readonly fr: "L'initialisation du plugin a été annulée par un module"; - readonly he: "אתחול התוסף בוטל על ידי מודול"; - readonly ja: "プラグインの初期化がモジュールによってキャンセルされました"; - readonly ko: "모듈에 의해 플러그인 초기화가 취소되었습니다"; - readonly ru: "Инициализация плагина отменена модулем"; - readonly zh: "插件初始化被某个模块取消"; - }; - readonly "moduleLiveSyncMain.logPluginVersion": { - readonly def: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly es: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly fr: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly he: "Self-hosted LiveSync גרסה ${manifestVersion} ${packageVersion}"; - readonly ja: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly ko: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - readonly ru: "Self-hosted LiveSync vmanifestVersion packageVersion"; - readonly zh: "Self-hosted LiveSync v${manifestVersion} ${packageVersion}"; - }; - readonly "moduleLiveSyncMain.logReadChangelog": { - readonly def: "LiveSync has updated, please read the changelog!"; - readonly es: "LiveSync se ha actualizado, ¡por favor lee el registro de cambios!"; - readonly fr: "LiveSync a été mis à jour, veuillez lire le journal des modifications !"; - readonly he: "LiveSync עודכן, אנא קרא את יומן השינויים!"; - readonly ja: "LiveSyncが更新されました。変更履歴をお読みください!"; - readonly ko: "LiveSync가 업데이트되었습니다. 변경사항을 읽어보세요!"; - readonly ru: "LiveSync обновлён, пожалуйста, прочитайте список изменений!"; - readonly zh: "LiveSync 已更新,请阅读更新日志!"; - }; - readonly "moduleLiveSyncMain.logSafetyScanCompleted": { - readonly def: "Additional safety scan completed"; - readonly es: "Escanéo de seguridad adicional completado"; - readonly fr: "Analyse de sécurité supplémentaire terminée"; - readonly he: "סריקת הבטיחות הנוספת הושלמה"; - readonly ja: "追加の安全スキャンが完了しました"; - readonly ko: "추가 안전 검사가 완료되었습니다"; - readonly ru: "Дополнительная проверка безопасности завершена"; - readonly zh: "额外的安全扫描完成"; - }; - readonly "moduleLiveSyncMain.logSafetyScanFailed": { - readonly def: "Additional safety scan has failed on a module"; - readonly es: "El escaneo de seguridad adicional ha fallado en un módulo"; - readonly fr: "L'analyse de sécurité supplémentaire a échoué sur un module"; - readonly he: "סריקת הבטיחות הנוספת נכשלה במודול"; - readonly ja: "モジュールで追加の安全スキャンが失敗しました"; - readonly ko: "모듈에서 추가 안전 검사가 실패했습니다"; - readonly ru: "Дополнительная проверка безопасности не удалась в модуле"; - readonly zh: "额外的安全扫描在某个模块上失败"; - }; - readonly "moduleLiveSyncMain.logUnloadingPlugin": { - readonly def: "Unloading plugin..."; - readonly es: "Descargando complemento..."; - readonly fr: "Déchargement du plugin..."; - readonly he: "מסיר תוסף..."; - readonly ja: "プラグインをアンロード中..."; - readonly ko: "플러그인 언로딩 중..."; - readonly ru: "Выгрузка плагина..."; - readonly zh: "正在卸载插件..."; - }; - readonly "moduleLiveSyncMain.logVersionUpdate": { - readonly def: "LiveSync has been updated, In case of breaking updates, all automatic synchronization has been temporarily disabled. Ensure that all devices are up to date before enabling."; - readonly es: "LiveSync se ha actualizado, en caso de actualizaciones que rompan, toda la sincronización automática se ha desactivado temporalmente. Asegúrate de que todos los dispositivos estén actualizados antes de habilitar."; - readonly fr: "LiveSync a été mis à jour. En cas de mises à jour non rétrocompatibles, toute synchronisation automatique a été temporairement désactivée. Assurez-vous que tous les appareils sont à jour avant d'activer."; - readonly he: "LiveSync עודכן. במקרה של עדכונים משמעותיים, כל הסנכרון האוטומטי הושבת זמנית. ודא שכל המכשירים מעודכנים לפני ההפעלה."; - readonly ja: "LiveSyncが更新されました。互換性のない更新の場合、すべての自動同期が一時的に無効化されています。有効にする前に、すべてのデバイスが最新の状態であることを確認してください。"; - readonly ko: "LiveSync가 업데이트되었습니다. 호환성 문제가 있는 업데이트의 경우 모든 자동 동기화가 일시적으로 비활성화되었습니다. 활성화하기 전에 모든 기기가 최신 상태인지 확인하세요."; - readonly ru: "LiveSync обновлён. В случае критических изменений автоматическая синхронизация временно отключена. Убедитесь, что все устройства обновлены перед включением."; - readonly zh: "LiveSync 已更新,如果存在破坏性更新,所有自动同步已暂时禁用。请确保所有设备都更新到最新版本后再启用"; - }; - readonly "moduleLiveSyncMain.msgScramEnabled": { - readonly def: "Self-hosted LiveSync has been configured to ignore some events. Is this correct?\n\n| Type | Status | Note |\n|:---:|:---:|---|\n| Storage Events | ${fileWatchingStatus} | Every modification will be ignored |\n| Database Events | ${parseReplicationStatus} | Every synchronised change will be postponed |\n\nDo you want to resume them and restart Obsidian?\n\n> [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n"; - readonly es: "Self-hosted LiveSync se ha configurado para ignorar algunos eventos. ¿Es esto correcto?\n\n| Tipo | Estado | Nota |\n|:---:|:---:|---|\n| Eventos de almacenamiento | ${fileWatchingStatus} | Se ignorará cada modificación |\n| Eventos de base de datos | ${parseReplicationStatus} | Cada cambio sincronizado se pospondrá |\n\n¿Quieres reanudarlos y reiniciar Obsidian?\n\n> [!DETAILS]-\n> Estas banderas son establecidas por el complemento mientras se reconstruye o se obtiene. Si el proceso termina de forma anormal, puede mantenerse sin querer.\n> Si no estás seguro, puedes intentar volver a ejecutar estos procesos. Asegúrate de hacer una copia de seguridad de tu bóveda.\n"; - readonly fr: "Self-hosted LiveSync a été configuré pour ignorer certains événements. Est-ce correct ?\n\n| Type | Statut | Note |\n|:---:|:---:|---|\n| Événements de stockage | ${fileWatchingStatus} | Toute modification sera ignorée |\n| Événements de base | ${parseReplicationStatus} | Tout changement synchronisé sera reporté |\n\nVoulez-vous les reprendre et redémarrer Obsidian ?\n\n> [!DETAILS]-\n> Ces indicateurs sont définis par le plug-in lors d'une reconstruction ou d'une récupération. Si le processus se termine anormalement, ils peuvent rester activés involontairement.\n> Si vous n'êtes pas certain, vous pouvez relancer ces processus. Veillez à sauvegarder votre coffre.\n"; - readonly he: "Self-hosted LiveSync הוגדר להתעלם מאירועים מסוימים. האם זה נכון?\n\n| סוג | סטטוס | הערה |\n|:---:|:---:|---|\n| אירועי אחסון | ${fileWatchingStatus} | כל שינוי יתעלם |\n| אירועי מסד נתונים | ${parseReplicationStatus} | כל שינוי מסונכרן יידחה |\n\nהאם לחדש אותם ולהפעיל מחדש את Obsidian?\n\n> [!DETAILS]-\n> דגלים אלה מוגדרים על ידי הפלאגין במהלך בנייה מחדש או משיכה. אם התהליך הסתיים בצורה לא תקינה, ייתכן שהם נשארו כלא מכוון.\n> אם אינך בטוח, ניתן לנסות להריץ מחדש את התהליכים. ודא שיש לך גיבוי של הכספת.\n"; - readonly ja: "Self-hosted LiveSyncは一部のイベントを無視するように設定されています。これは正しいですか?\n\n| タイプ | ステータス | メモ |\n|:---:|:---:|---|\n| ストレージイベント | ${fileWatchingStatus} | すべての変更が無視されます |\n| データベースイベント | ${parseReplicationStatus} | すべての同期された変更が延期されます |\n\nこれらを再開してObsidianを再起動しますか?\n\n> [!DETAILS]-\n> これらのフラグは、プラグインが再構築またはフェッチ中に設定されます。プロセスが異常終了した場合、意図せず保持されることがあります。\n> 不明な場合は、これらのプロセスを再実行してみてください。必ず保管庫をバックアップしてください。\n"; - readonly ko: "Self-hosted LiveSync가 일부 이벤트를 무시하도록 설정되어 있습니다. 이 설정이 맞습니까?\n\n| 유형 | 상태 | 설명 |\n|:---:|:---:|---|\n| 스토리지 이벤트 | ${fileWatchingStatus} | 모든 수정 사항이 무시됩니다 |\n| 데이터베이스 이벤트 | ${parseReplicationStatus} | 모든 동기화 변경이 지연됩니다 |\n\n이벤트 감지를 다시 활성화하고 Obsidian을 재시작하시겠습니까?\n\n> [!DETAILS]-\n> 이러한 설정은 플러그인이 재구성 또는 데이터 가져오기 중에 자동으로 설정한 것입니다. 프로세스가 비정상적으로 종료되면 이 상태가 의도치 않게 유지될 수 있습니다.\n> 상태가 확실하지 않다면 이 과정을 다시 실행해 보세요. 재시작 전에 반드시 볼트를 백업해 주세요."; - readonly ru: "Self-hosted LiveSync has been configured to ignore some events. Is this correct?\n\n| Type | Status | Note |\n|:---:|:---:|---|\n| Storage Events | ${fileWatchingStatus} | Every modification will be ignored |\n| Database Events | ${parseReplicationStatus} | Every synchronised change will be postponed |\n\nDo you want to resume them and restart Obsidian?\n\n> [!DETAILS]-\n> These flags are set by the plug-in while rebuilding, or fetching. If the process ends abnormally, it may be kept unintended.\n> If you are not sure, you can try to rerun these processes. Make sure to back your vault up.\n"; - readonly zh: "Self-hosted LiveSync 已被配置为忽略某些事件。这样对吗?\n\n| 类型 | 状态 | 说明 |\n|:---:|:---:|---|\n| 存储事件 | ${fileWatchingStatus} | 所有修改都将被忽略 |\n| 数据库事件 | ${parseReplicationStatus} | 所有同步的更改都将被推迟 |\n\n您想恢复它们并重启 Obsidian 吗?\n\n> [!DETAILS]-\n> 这些标志是在重建或获取时由插件设置的。如果过程异常结束,它们可能会被无意中保留。\n> 如果您不确定,可以尝试重新运行这些过程。请确保备份您的库。\n"; - }; - readonly "moduleLiveSyncMain.optionKeepLiveSyncDisabled": { - readonly def: "Keep LiveSync disabled"; - readonly es: "Mantener LiveSync desactivado"; - readonly fr: "Garder LiveSync désactivé"; - readonly he: "השאר LiveSync מנוטרל"; - readonly ja: "LiveSyncを無効のままにする"; - readonly ko: "LiveSync 비활성화 유지"; - readonly ru: "Оставить LiveSync отключённым"; - readonly zh: "保持 LiveSync 禁用"; - }; - readonly "moduleLiveSyncMain.optionResumeAndRestart": { - readonly def: "Resume and restart Obsidian"; - readonly es: "Reanudar y reiniciar Obsidian"; - readonly fr: "Reprendre et redémarrer Obsidian"; - readonly he: "חדש והפעל מחדש את Obsidian"; - readonly ja: "再開してObsidianを再起動"; - readonly ko: "재개 후 Obsidian 재시작"; - readonly ru: "Продолжить и перезапустить Obsidian"; - readonly zh: "恢复并重启 Obsidian"; - }; - readonly "moduleLiveSyncMain.titleScramEnabled": { - readonly def: "Scram Enabled"; - readonly es: "Scram habilitado"; - readonly fr: "Mode Scram activé"; - readonly he: "מצב בלימה פעיל"; - readonly ja: "緊急停止(Scram)が有効"; - readonly ko: "Scram 활성화됨"; - readonly ru: "Экстренная остановка включена"; - readonly zh: "紧急停止已启用"; - }; - readonly "moduleLocalDatabase.logWaitingForReady": { - readonly def: "Waiting for ready..."; - readonly es: "Esperando a que la base de datos esté lista..."; - readonly fr: "En attente de disponibilité..."; - readonly he: "ממתין לכשירות..."; - readonly ja: "しばらくお待ちください..."; - readonly ko: "준비 대기 중..."; - readonly ru: "Ожидание готовности..."; - readonly zh: "等待就绪..."; - }; - readonly "moduleLog.showLog": { - readonly def: "Show Log"; - readonly es: "Mostrar registro"; - readonly fr: "Afficher le journal"; - readonly he: "הצג יומן"; - readonly ja: "ログを表示"; - readonly ko: "로그 표시"; - readonly ru: "Показать лог"; - readonly zh: "显示日志"; - }; - readonly "moduleMigration.docUri": { - readonly def: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly es: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README_ES.md#how-to-use"; - readonly fr: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly he: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly ja: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly ko: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly ru: "https://github.com/vrtmrz/obsidian-livesync/blob/main/README.md#how-to-use"; - readonly zh: "https://github.com/vrtmrz/obsidian-livesync/blob/main/docs/zh/README_zh.md#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8"; - }; - readonly "moduleMigration.fix0256.buttons.checkItLater": { - readonly def: "Check it later"; - readonly fr: "Vérifier plus tard"; - readonly he: "בדוק מאוחר יותר"; - readonly ja: "後で確認する"; - readonly ru: "Проверить позже"; - readonly zh: "稍后检查"; - }; - readonly "moduleMigration.fix0256.buttons.DismissForever": { - readonly def: "I have fixed it, and do not ask again"; - readonly fr: "J'ai corrigé, et ne plus demander"; - readonly he: "תיקנתי, ואל תשאל שוב"; - readonly ja: "修正済み、今後確認しない"; - readonly ru: "Исправлено, больше не спрашивать"; - readonly zh: "我已经修复了,不再询问"; - }; - readonly "moduleMigration.fix0256.buttons.fix": { - readonly def: "Fix"; - readonly fr: "Corriger"; - readonly he: "תקן"; - readonly ja: "修正"; - readonly ru: "Исправить"; - readonly zh: "修复"; - }; - readonly "moduleMigration.fix0256.message": { - readonly def: "Due to a recent bug (in v0.25.6), some files may not have been saved correctly in the sync database.\nWe have scanned our files and found some that need to be fixed.\n\n**Files ready to be fixed:**\n\n${files}\n\nThese files have size-matched original file on the storage, and are likely to be recoverable.\nWe can use them to fix the database, please click the \"Fix\" button below to fix them.\n\n${messageUnrecoverable}\n\nIf you want to run it again, you can do so from Hatch.\n"; - readonly fr: "En raison d'un bug récent (en v0.25.6), certains fichiers peuvent ne pas avoir été enregistrés correctement dans la base de synchronisation.\nNous avons analysé vos fichiers et trouvé ceux à corriger.\n\n**Fichiers prêts à être corrigés :**\n\n${files}\n\nCes fichiers ont un original de taille correspondante sur le stockage, et sont probablement récupérables.\nNous pouvons les utiliser pour corriger la base, veuillez cliquer sur le bouton « Corriger » ci-dessous pour les réparer.\n\n${messageUnrecoverable}\n\nSi vous voulez relancer l'opération, vous pouvez le faire depuis Hatch.\n"; - readonly he: "בשל באג אחרון (בגרסה 0.25.6), ייתכן שחלק מהקבצים לא נשמרו כהלכה במסד הנתונים לסנכרון.\nסרקנו את הקבצים ומצאנו כאלה שיש לתקן.\n\n**קבצים מוכנים לתיקון:**\n\n${files}\n\nלקבצים אלה יש קובץ מקורי תואם גודל באחסון, וסביר שניתן לשחזרם.\nניתן להשתמש בהם לתיקון מסד הנתונים. לחץ על כפתור \"תקן\" למטה לתיקון.\n\n${messageUnrecoverable}\n\nאם ברצונך להריץ שוב, ניתן לעשות זאת מ-Hatch.\n"; - readonly ja: "最近のバグ(v0.25.6)により、一部のファイルが同期データベースに正しく保存されていない可能性があります。\nファイルをスキャンし、修正が必要なものが見つかりました。\n\n**修正準備ができたファイル:**\n\n${files}\n\nこれらのファイルはストレージ上の元ファイルとサイズが一致しており、復元可能です。\n「修正」ボタンをクリックしてデータベースを修正できます。\n\n${messageUnrecoverable}\n\n再実行したい場合は、Hatchから実行できます。\n"; - readonly ru: "Из-за недавней ошибки некоторые файлы могут быть неправильно сохранены."; - readonly zh: "由于最近的一个 bug(在 v0.25.6 版本中),某些文件可能未正确保存到同步数据库中\n我们已经扫描了文件,并发现一些需要修复的文件\n\n**准备修复的文件:**\n\n${files}\n\n这些文件在存储中与原文件的大小匹配,可能是可恢复的\n我们可以使用它们修复数据库,请点击下方的“修复”按钮进行修复\n\n${messageUnrecoverable}\n\n\n如果你希望再次执行此操作,可以前往 Hatch 页面进行操作\n"; - }; - readonly "moduleMigration.fix0256.messageUnrecoverable": { - readonly def: "**Files cannot be fixed on this device:**\n\n${filesNotRecoverable}\n\nThese files have inconsistent metadata, and cannot be fixed on this device (mostly we cannot determine which is correct).\nTo restore them, please check your other devices (also by this feature) or restore them manually from a backup.\n"; - readonly fr: "**Fichiers non réparables sur cet appareil :**\n\n${filesNotRecoverable}\n\nCes fichiers ont des métadonnées incohérentes et ne peuvent être corrigés sur cet appareil (le plus souvent, nous ne pouvons déterminer lequel est correct).\nPour les restaurer, vérifiez vos autres appareils (par cette même fonction) ou restaurez-les manuellement depuis une sauvegarde.\n"; - readonly he: "**קבצים שלא ניתן לתקן במכשיר זה:**\n\n${filesNotRecoverable}\n\nלקבצים אלה יש מטה-נתונים לא עקביים, ולא ניתן לתקנם במכשיר זה (לרוב לא ניתן לקבוע מה נכון). לשחזורם, אנא בדוק מכשירים אחרים שלך (גם בתכונה זו) או שחזר ידנית מגיבוי.\n"; - readonly ja: "**このデバイスで修正できないファイル:**\n\n${filesNotRecoverable}\n\nこれらのファイルはメタデータに不整合があり、このデバイスでは修正できません(ほとんどの場合、どちらが正しいか判定できません)。\n復元するには、他のデバイスで確認するか、バックアップから手動で復元してください。\n"; - readonly ru: "Файлы не могут быть исправлены на этом устройстве:"; - readonly zh: "**无法在此设备上修复的文件:**\n\n${filesNotRecoverable}\n\n这些文件的元数据不一致,无法在此设备上修复(大多数情况下我们无法确定哪一个是正确的)\n要恢复它们,请检查你的其他设备(同样使用此功能),或从备份中手动恢复\n"; - }; - readonly "moduleMigration.fix0256.title": { - readonly def: "Broken files has been detected"; - readonly fr: "Fichiers corrompus détectés"; - readonly he: "זוהו קבצים פגומים"; - readonly ja: "破損ファイルが検出されました"; - readonly ru: "Обнаружены повреждённые файлы"; - readonly zh: "检测到损坏的文件"; - }; - readonly "moduleMigration.insecureChunkExist.buttons.fetch": { - readonly def: "I already rebuilt the remote. Fetch from the remote"; - readonly fr: "J'ai déjà reconstruit le distant. Récupérer depuis le distant"; - readonly he: "כבר בניתי מחדש את השרת המרוחק. משוך מהשרת המרוחד"; - readonly ja: "リモートを既に再構築した。リモートからフェッチ"; - readonly ru: "Я уже перестроил удалённую. Загрузить с удалённой"; - readonly zh: "我已经重建了远程数据库,将从远程获取"; - }; - readonly "moduleMigration.insecureChunkExist.buttons.later": { - readonly def: "I will do it later"; - readonly fr: "Je le ferai plus tard"; - readonly he: "אטפל בזה מאוחר יותר"; - readonly ja: "後で行う"; - readonly ru: "Сделаю позже"; - readonly zh: "我稍后再做"; - }; - readonly "moduleMigration.insecureChunkExist.buttons.rebuild": { - readonly def: "Rebuild Everything"; - readonly fr: "Tout reconstruire"; - readonly he: "בנה הכל מחדש"; - readonly ja: "すべてを再構築"; - readonly ru: "Перестроить всё"; - readonly zh: "重建所有内容"; - }; - readonly "moduleMigration.insecureChunkExist.laterMessage": { - readonly def: "We strongly recommend to treat this as soon as possible!"; - readonly fr: "Nous recommandons fortement de traiter ceci dès que possible !"; - readonly he: "אנו ממליצים בחום לטפל בזה בהקדם האפשרי!"; - readonly ja: "できるだけ早く対処することを強くお勧めします!"; - readonly ru: "Мы настоятельно рекомендуем обработать это как можно скорее!"; - readonly zh: "我们强烈建议尽快处理此问题!"; - }; - readonly "moduleMigration.insecureChunkExist.message": { - readonly def: "Some chunks are not securely stored and are not encrypted in databases.\n**Please rebuild the database to fix this issue**.\n\nIf your Remote Database is not configured with SSL, or using less-secure credentials, **you are at risk of exposing sensitive data**.\n\nNote: Please upgrade your Self-hosted LiveSync v0.25.6 or higher on all your devices, and back your vault up surely.\nNote2: Rebuild Everything and Fetch consumes a bit of time and traffic, please do it in off-peak hours and ensure a stable network connection.\n"; - readonly fr: "Certains fragments ne sont pas stockés de façon sécurisée et ne sont pas chiffrés dans les bases.\n**Veuillez reconstruire la base pour corriger ce problème.**\n\nSi votre base distante n'est pas configurée avec SSL, ou utilise des identifiants peu sûrs, **vous risquez d'exposer des données sensibles**.\n\nNote : Veuillez mettre à jour Self-hosted LiveSync en v0.25.6 ou supérieur sur tous vos appareils, et sauvegardez votre coffre avec soin.\nNote 2 : Tout reconstruire et Récupérer consomme un peu de temps et de bande passante, veuillez le faire hors des heures de pointe et avec une connexion réseau stable.\n"; - readonly he: "חלק מהנתחים לא מאוחסנים בצורה מאובטחת ואינם מוצפנים במסד הנתונים.\n**אנא בנה מחדש את מסד הנתונים כדי לתקן בעיה זו**.\n\nאם מסד הנתונים המרוחד אינו מוגדר עם SSL, או משתמש בפרטי גישה פחות מאובטחים, **אתה בסיכון של חשיפת מידע רגיש**.\n\nהערה: אנא שדרג את Self-hosted LiveSync לגרסה 0.25.6 ומעלה על כל מכשיריך, וגבה את הכספת שלך.\nהערה 2: בנייה מחדש ומשיכה דורשות זמן ותעבורת רשת. אנא עשה זאת בשעות שיא נמוך וודא חיבור רשת יציב.\n"; - readonly ja: "一部のチャンクが安全に保存されておらず、データベースで暗号化されていません。\n**この問題を修正するにはデータベースを再構築してください**。\n\nリモートデータベースがSSLで設定されていない、または安全性の低い認証情報を使用している場合、**機密データが漏洩するリスクがあります**。\n\n注意: すべてのデバイスでSelf-hosted LiveSync v0.25.6以降にアップグレードし、必ず保管庫をバックアップしてください。\n注意2: すべてを再構築とフェッチは時間とトラフィックを消費します。オフピーク時間に安定したネットワークで実行してください。\n"; - readonly ru: "Некоторые чанки хранятся небезопасно. Пожалуйста, перестройте базу данных."; - readonly zh: "一些块未安全存储,并且在数据库中未加密\n**请重建数据库以修复此问题**.\n\n如果你的远程数据库未配置 SSL,或者使用了不安全的凭据 **你可能面临暴露敏感数据的风险**.\n\n注意:请在所有设备上将 Self-hosted LiveSync 升级到 v0.25.6 或更高版本,并确保备份你的保险库\n\n注意2:重建所有内容和获取操作会消耗一些时间和流量,请在非高峰时段进行,并确保网络连接稳定\n"; - }; - readonly "moduleMigration.insecureChunkExist.title": { - readonly def: "Insecure chunks found!"; - readonly fr: "Fragments non sécurisés détectés !"; - readonly he: "נמצאו נתחים לא מאובטחים!"; - readonly ja: "安全でないチャンクが見つかりました!"; - readonly ru: "Обнаружены небезопасные чанки!"; - readonly zh: "发现不安全的块!"; - }; - readonly "moduleMigration.logBulkSendCorrupted": { - readonly def: "Send chunks in bulk has been enabled, however, this feature had been corrupted. Sorry for your inconvenience. Automatically disabled."; - readonly es: "El envío de fragmentos en bloque se ha habilitado, sin embargo, esta función se ha corrompido. Disculpe las molestias. Deshabilitado automáticamente."; - readonly fr: "L'envoi groupé de fragments a été activé, mais cette fonctionnalité était corrompue. Désolé pour la gêne. Désactivée automatiquement."; - readonly he: "שליחת נתחים באצווה הופעלה, אך תכונה זו הייתה פגועה. מתנצלים על אי הנוחות. נוטרלה אוטומטית."; - readonly ja: "チャンクの一括送信が有効にされていましたが、この機能に問題がありました。ご不便をおかけして申し訳ありません。自動的に無効化されました。"; - readonly ko: "청크 일괄 전송이 활성화되었지만, 이 기능에 문제가 있었습니다. 불편을 드려 죄송합니다. 자동으로 비활성화되었습니다."; - readonly ru: "Отправка чанков пакетами была включена, но эта функция была повреждена. Приносим извинения. Автоматически отключено."; - readonly zh: "已启用批量发送 chunks,但此功能已损坏。给您带来不便,我们深表歉意。已自动禁用"; - }; - readonly "moduleMigration.logFetchRemoteTweakFailed": { - readonly def: "Failed to fetch remote tweak values"; - readonly es: "Error al obtener los valores de ajuste remoto"; - readonly fr: "Échec de la récupération des valeurs d'ajustement distantes"; - readonly he: "נכשל במשיכת ערכי כיוונון מרוחקים"; - readonly ja: "リモートの調整値の取得に失敗しました"; - readonly ko: "원격 조정 값을 가져오는데 실패했습니다"; - readonly ru: "Не удалось загрузить удалённые настройки"; - readonly zh: "获取远程调整值失败"; - }; - readonly "moduleMigration.logLocalDatabaseNotReady": { - readonly def: "Something went wrong! The local database is not ready"; - readonly es: "¡Algo salió mal! La base de datos local no está lista"; - readonly fr: "Un problème est survenu ! La base locale n'est pas prête"; - readonly he: "משהו השתבש! מסד הנתונים המקומי אינו מוכן"; - readonly ja: "何か問題が発生しました!ローカルデータベースが準備できていません"; - readonly ko: "문제가 발생했습니다! 로컬 데이터베이스가 준비되지 않았습니다"; - readonly ru: "Что-то пошло не так! Локальная база данных не готова"; - readonly zh: "出错了!本地数据库尚未准备好"; - }; - readonly "moduleMigration.logMigratedSameBehaviour": { - readonly def: "Migrated to db:${current} with the same behaviour as before"; - readonly es: "Migrado a db:${current} con el mismo comportamiento que antes"; - readonly fr: "Migration vers db:${current} avec le même comportement qu'auparavant"; - readonly he: "הוגר ל-db:${current} עם אותה התנהגות כמקודם"; - readonly ja: "以前と同じ動作でdb:${current}に移行しました"; - readonly ko: "이전과 같은 방식으로 동작하도록 db:${current}로 데이터 구조 전환이 완료되었습니다"; - readonly ru: "Миграция на db:current с тем же поведением, что и раньше"; - readonly zh: "已迁移到 db:${current},行为与之前相同"; - }; - readonly "moduleMigration.logMigrationFailed": { - readonly def: "Migration failed or cancelled from ${old} to ${current}"; - readonly es: "La migración falló o se canceló de ${old} a ${current}"; - readonly fr: "Migration échouée ou annulée de ${old} vers ${current}"; - readonly he: "הגירה נכשלה או בוטלה מ-${old} ל-${current}"; - readonly ja: "${old}から${current}への移行が失敗またはキャンセルされました"; - readonly ko: "${old}에서 ${current}로의 데이터 구조 전환이 실패했거나 중단되었습니다"; - readonly ru: "Миграция не удалась или отменена с old на current"; - readonly zh: "从 ${old} 到 ${current} 的迁移失败或已取消"; - }; - readonly "moduleMigration.logRedflag2CreationFail": { - readonly def: "Failed to create redflag2"; - readonly es: "Error al crear redflag2"; - readonly fr: "Échec de création de redflag2"; - readonly he: "יצירת redflag2 נכשלה"; - readonly ja: "redflag2の作成に失敗しました"; - readonly ko: "redflag2 생성에 실패했습니다"; - readonly ru: "Не удалось создать redflag2"; - readonly zh: "创建 redflag2 失败"; - }; - readonly "moduleMigration.logRemoteTweakUnavailable": { - readonly def: "Could not get remote tweak values"; - readonly es: "No se pudieron obtener los valores de ajuste remoto"; - readonly fr: "Impossible d'obtenir les valeurs d'ajustement distantes"; - readonly he: "לא ניתן לקבל ערכי כיוונון מרוחקים"; - readonly ja: "リモートの調整値を取得できませんでした"; - readonly ko: "원격 조정 값을 가져올 수 없습니다"; - readonly ru: "Не удалось получить удалённые настройки"; - readonly zh: "无法获取远程调整值"; - }; - readonly "moduleMigration.logSetupCancelled": { - readonly def: "The setup has been cancelled, Self-hosted LiveSync waiting for your setup!"; - readonly es: "La configuración ha sido cancelada, ¡Self-hosted LiveSync está esperando tu configuración!"; - readonly fr: "La configuration a été annulée, Self-hosted LiveSync attend votre configuration !"; - readonly he: "ההגדרה בוטלה, Self-hosted LiveSync ממתין להגדרתך!"; - readonly ja: "セットアップがキャンセルされました。Self-hosted LiveSyncはセットアップを待っています!"; - readonly ko: "설정이 취소되었습니다. Self-hosted LiveSync가 설정을 기다리고 있습니다!"; - readonly ru: "Настройка отменена, Self-hosted LiveSync ожидает вашей настройки!"; - readonly zh: "设置已取消,Self-hosted LiveSync 正在等待您的设置!"; - }; - readonly "moduleMigration.msgFetchRemoteAgain": { - readonly def: "As you may already know, the self-hosted LiveSync has changed its default behaviour and database structure.\n\nAnd thankfully, with your time and efforts, the remote database appears to have already been migrated. Congratulations!\n\nHowever, we need a bit more. The configuration of this device is not compatible with the remote database. We will need to fetch the remote database again. Should we fetch from the remote again now?\n\n___Note: We cannot synchronise until the configuration has been changed and the database has been fetched again.___\n___Note2: The chunks are completely immutable, we can fetch only the metadata and difference.___"; - readonly es: "Como ya sabrás, Self-hosted LiveSync ha cambiado su comportamiento predeterminado y la estructura de la base de datos.\n\nAfortunadamente, con tu tiempo y esfuerzo, la base de datos remota parece haber sido ya migrada. ¡Felicidades!\n\nSin embargo, necesitamos un poco más. La configuración de este dispositivo no es compatible con la base de datos remota. Necesitaremos volver a obtener la base de datos remota. ¿Debemos obtenerla nuevamente ahora?\n\n___Nota: No podemos sincronizar hasta que la configuración haya sido cambiada y la base de datos haya sido obtenida nuevamente.___\n___Nota2: Los fragmentos son completamente inmutables, solo podemos obtener los metadatos y diferencias.___"; - readonly fr: "Comme vous le savez peut-être déjà, Self-hosted LiveSync a modifié son comportement par défaut et la structure de sa base de données.\n\nEt, grâce à votre temps et vos efforts, la base distante semble déjà avoir été migrée. Félicitations !\n\nCependant, il faut encore un peu plus. La configuration de cet appareil n'est pas compatible avec la base distante. Nous devrons récupérer à nouveau la base distante. Devons-nous récupérer depuis le distant maintenant ?\n\n___Note : Nous ne pouvons pas synchroniser tant que la configuration n'a pas été modifiée et que la base n'a pas été récupérée à nouveau.___\n___Note 2 : Les fragments sont complètement immuables, nous ne pouvons récupérer que les métadonnées et les différences.___"; - readonly he: "כפי שייתכן שכבר ידוע לך, Self-hosted LiveSync שינה את התנהגות ברירת המחדל ומבנה מסד הנתונים.\n\nובזכות זמנך ומאמציך, מסד הנתונים המרוחד נראה כבר הוגר. ברכות!\n\nעם זאת, נדרש עוד קצת. תצורת מכשיר זה אינה תואמת למסד הנתונים המרוחד. נצטרך למשוך את מסד הנתונים המרוחד שוב. האם למשוך מהשרת המרוחד עכשיו?\n\n___הערה: לא ניתן לסנכרן עד שהתצורה תשתנה ומסד הנתונים יימשך שוב.___\n___הערה 2: הנתחים הם בלתי-ניתנים לשינוי לחלוטין, ניתן למשוך רק את המטה-נתונים וההפרש.___"; - readonly ja: "ご存知のとおり、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。\n\nご協力のおかげで、リモートデータベースはすでに移行されているようです。おめでとうございます!\n\nしかし、もう少し必要です。このデバイスの設定はリモートデータベースと互換性がありません。リモートデータベースを再度フェッチする必要があります。今すぐリモートから再フェッチしますか?\n\n___注意: 設定が変更され、データベースが再フェッチされるまで同期できません。___\n___注意2: チャンクは完全に不変なので、メタデータと差分のみフェッチできます。___"; - readonly ko: "이미 알고 계시겠지만, Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다.\n\n다행히도 여러분의 노력 덕분에 원격 데이터베이스는 이미 성공적으로 데이터 구조 전환이 완료된 것으로 보입니다. 축하드립니다!\n\n하지만 아직 일부 추가 작업이 필요합니다. 이 기기의 설정이 원격 데이터베이스와 호환되지 않으므로, 원격 데이터를 다시 가져와야 합니다. 지금 원격 데이터베이스를 다시 가져오시겠습니까?\n\n___참고: 설정이 변경되고 데이터베이스를 다시 불러오기 전까지는 동기화가 불가능합니다.___\n___참고2: 청크는 변경이 불가능한 구조이므로, 메타데이터와 차이점만 가져올 수 있습니다.___"; - readonly ru: "Удалённая база данных, похоже, уже была мигрирована. Конфигурация этого устройства несовместима."; - readonly zh: "您可能已经知道,Self-hosted LiveSync 更改了其默认行为和数据库结构。\n\n值得庆幸的是,在您的时间和努力下,远程数据库似乎已经迁移完成。恭喜!\n\n但是,我们还需要一点点操作。此设备的配置与远程数据库不兼容。我们需要再次从远程数据库获取。我们现在应该再次从远程获取吗?\n\n___注意:在更改配置并再次获取数据库之前,我们无法进行同步。___\n___注意2:chunks 是完全不可变的,我们只能获取元数据和差异"; - }; - readonly "moduleMigration.msgInitialSetup": { - readonly def: "Your device has **not been set up yet**. Let me guide you through the setup process.\n\nPlease keep in mind that every dialogue content can be copied to the clipboard. If you need to refer to it later, you can paste it into a note in Obsidian. You can also translate it into your language using a translation tool.\n\nFirst, do you have **Setup URI**?\n\nNote: If you do not know what it is, please refer to the [documentation](${URI_DOC})."; - readonly es: "Tu dispositivo **aún no ha sido configurado**. Permíteme guiarte a través del proceso de configuración.\n\nTen en cuenta que todo el contenido del diálogo se puede copiar al portapapeles. Si necesitas consultarlo más tarde, puedes pegarlo en una nota en Obsidian. También puedes traducirlo a tu idioma utilizando una herramienta de traducción.\n\nPrimero, ¿tienes **URI de configuración**?\n\nNota: Si no sabes qué es, consulta la [documentación](${URI_DOC})."; - readonly fr: "Votre appareil n'a **pas encore été configuré**. Laissez-moi vous guider dans le processus de configuration.\n\nVeuillez noter que chaque contenu de boîte de dialogue peut être copié dans le presse-papiers. Si vous souhaitez vous y référer plus tard, vous pouvez le coller dans une note d'Obsidian. Vous pouvez également le traduire dans votre langue via un outil de traduction.\n\nTout d'abord, disposez-vous d'une **URI de configuration** ?\n\nNote : Si vous ne savez pas ce que c'est, consultez la [documentation](${URI_DOC})."; - readonly he: "המכשיר שלך **טרם הוגדר**. אנחנו כאן לעזור לך בתהליך ההגדרה.\n\nשים לב שניתן להעתיק את תוכן כל דיאלוג ללוח. אם צריך לחזור אליו מאוחר יותר, ניתן להדביק אותו כפתק ב-Obsidian. ניתן גם לתרגם לשפתך בעזרת כלי תרגום.\n\nראשית, האם יש לך **Setup URI**?\n\nהערה: אם אינך יודע מהו, אנא עיין ב[תיעוד](${URI_DOC})."; - readonly ja: "このデバイスは**まだセットアップされていません**。セットアッププロセスをご案内します。\n\nすべてのダイアログの内容はクリップボードにコピーできます。後で参照する必要があれば、Obsidianのノートに貼り付けてください。翻訳ツールを使ってお使いの言語に翻訳することもできます。\n\nまず、**セットアップURI**をお持ちですか?\n\n注意: それが何か分からない場合は、[documentation](${URI_DOC})を参照してください。"; - readonly ko: "이 기기는 **아직 초기 설정이 완료되지 않았습니다**. 지금부터 설정 과정을 안내해 드리겠습니다.\n\n모든 대화 내용은 클립보드에 복사할 수 있습니다. 나중에 참고하려면 Obsidian 노트에 붙여넣거나 번역 도구를 활용해 번역하셔도 됩니다.\n\n먼저, **Setup URI**를 가지고 계신가요?\n\n참고: Setup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요."; - readonly ru: "Ваше устройство ещё не настроено. У вас есть Setup URI?"; - readonly zh: "您的设备**尚未设置**。让我引导您完成设置过程。\n\n请记住,每个对话框内容都可以复制到剪贴板。如果以后需要参考,可以将其粘贴到 Obsidian 的笔记中。您也可以使用翻译工具将其翻译成您的语言。\n\n首先,您有**设置 URI** 吗?\n\n注意:如果您不知道这是什么,请参阅[文档](${URI_DOC})"; - }; - readonly "moduleMigration.msgRecommendSetupUri": { - readonly def: "We strongly recommend that you generate a set-up URI and use it.\nIf you do not have knowledge about it, please refer to the [documentation](${URI_DOC}) (Sorry again, but it is important).\n\nHow do you want to set it up manually?"; - readonly es: "Te recomendamos encarecidamente que generes una URI de configuración y la utilices.\nSi no tienes conocimientos al respecto, consulta la [documentación](${URI_DOC}) (Lo siento de nuevo, pero es importante).\n\n¿Cómo quieres configurarlo manualmente?"; - readonly fr: "Nous recommandons vivement de générer une URI de configuration et de l'utiliser.\nSi vous ne connaissez pas, veuillez consulter la [documentation](${URI_DOC}) (Désolé encore, mais c'est important).\n\nComment souhaitez-vous effectuer la configuration manuellement ?"; - readonly he: "אנו ממליצים בחום לייצר Setup URI ולהשתמש בו.\nאם אין לך ידע בנושא, אנא עיין ב[תיעוד](${URI_DOC}) (מתנצלים שוב, אך זה חשוב).\n\nכיצד ברצונך להגדיר ידנית?"; - readonly ja: "セットアップURIを生成して使用することを強くお勧めします。\nこれについて知識がない場合は、[documentation](${URI_DOC})を参照してください(重要です)。\n\n手動でセットアップしますか?"; - readonly ko: "Setup URI를 생성해 사용하는 것을 강력히 권장합니다.\nSetup URI가 무엇인지 잘 모르시겠다면 [문서](${URI_DOC})를 참고해 주세요. 중요한 내용이니 꼭 확인하시기 바랍니다.\n\n직접 수동 설정을 진행하시겠습니까?"; - readonly ru: "Мы рекомендуем сгенерировать Setup URI."; - readonly zh: "我们强烈建议您生成一个设置 URI 并使用它。\n如果您对此不了解,请参阅[文档](${URI_DOC})(再次抱歉,但这很重要)。\n\n您想如何手动设置?"; - }; - readonly "moduleMigration.msgSinceV02321": { - readonly def: "Since v0.23.21, the self-hosted LiveSync has changed the default behaviour and database structure. The following changes have been made:\n\n1. **Case sensitivity of filenames**\n The handling of filenames is now case-insensitive. This is a beneficial change for most platforms, other than Linux and iOS, which do not manage filename case sensitivity effectively.\n (On These, a warning will be displayed for files with the same name but different cases).\n\n2. **Revision handling of the chunks**\n Chunks are immutable, which allows their revisions to be fixed. This change will enhance the performance of file saving.\n\n___However, to enable either of these changes, both remote and local databases need to be rebuilt. This process takes a few minutes, and we recommend doing it when you have ample time.___\n\n- If you wish to maintain the previous behaviour, you can skip this process by using `${KEEP}`.\n- If you do not have enough time, please choose `${DISMISS}`. You will be prompted again later.\n- If you have rebuilt the database on another device, please select `${DISMISS}` and try synchronizing again. Since a difference has been detected, you will be prompted again."; - readonly es: "Desde la versión v0.23.21, Self-hosted LiveSync ha cambiado el comportamiento predeterminado y la estructura de la base de datos. Se han realizado los siguientes cambios:\n\n1. **Sensibilidad a mayúsculas de los nombres de archivo**\n El manejo de los nombres de archivo ahora no distingue entre mayúsculas y minúsculas. Este cambio es beneficioso para la mayoría de las plataformas, excepto Linux y iOS, que no gestionan efectivamente la sensibilidad a mayúsculas de los nombres de archivo.\n (En estos, se mostrará una advertencia para archivos con el mismo nombre pero diferentes mayúsculas).\n\n2. **Manejo de revisiones de los fragmentos**\n Los fragmentos son inmutables, lo que permite que sus revisiones sean fijas. Este cambio mejorará el rendimiento al guardar archivos.\n\n___Sin embargo, para habilitar cualquiera de estos cambios, es necesario reconstruir tanto las bases de datos remota como la local. Este proceso toma unos minutos, y recomendamos hacerlo cuando tengas tiempo suficiente.___\n\n- Si deseas mantener el comportamiento anterior, puedes omitir este proceso usando `${KEEP}`.\n- Si no tienes suficiente tiempo, por favor elige `${DISMISS}`. Se te pedirá nuevamente más tarde.\n- Si has reconstruido la base de datos en otro dispositivo, selecciona `${DISMISS}` e intenta sincronizar nuevamente. Dado que se ha detectado una diferencia, se te solicitará nuevamente."; - readonly fr: "Depuis la v0.23.21, Self-hosted LiveSync a modifié son comportement par défaut et la structure de sa base. Les changements suivants ont été effectués :\n\n1. **Sensibilité à la casse des noms de fichiers**\n La gestion des noms de fichiers est désormais insensible à la casse. C'est un changement bénéfique pour la plupart des plateformes, hormis Linux et iOS, qui ne gèrent pas efficacement la casse des noms de fichiers.\n (Sur celles-ci, un avertissement s'affichera pour les fichiers portant le même nom avec une casse différente).\n\n2. **Gestion des révisions des fragments**\n Les fragments sont immuables, ce qui permet de fixer leurs révisions. Ce changement améliore les performances d'enregistrement des fichiers.\n\n___Cependant, pour activer l'un ou l'autre de ces changements, les bases locale et distante doivent être reconstruites. Ce processus prend quelques minutes, et nous recommandons de le faire quand vous avez le temps.___\n\n- Si vous souhaitez conserver le comportement précédent, vous pouvez ignorer ce processus via `${KEEP}`.\n- Si vous n'avez pas le temps, choisissez `${DISMISS}`. Vous serez invité à nouveau plus tard.\n- Si vous avez reconstruit la base sur un autre appareil, sélectionnez `${DISMISS}` et réessayez la synchronisation. Une différence étant détectée, vous serez invité à nouveau."; - readonly he: "מאז גרסה 0.23.21, Self-hosted LiveSync שינה את התנהגות ברירת המחדל ומבנה מסד הנתונים. השינויים הבאים בוצעו:\n\n1. **תלות רישיות בשמות קבצים**\n הטיפול בשמות קבצים הוא כעת ללא תלות רישיות. זהו שינוי מועיל לרוב הפלטפורמות,\n פרט ל-Linux ו-iOS שאינן מנהלות תלות רישיות בקבצים ביעילות.\n (בפלטפורמות אלה, תוצג אזהרה עבור קבצים עם אותו שם אך רישיות שונה).\n\n2. **טיפול בגרסאות של נתחים**\n נתחים הם בלתי-ניתנים לשינוי, מה שמאפשר גרסאות קבועות. שינוי זה ישפר את\n ביצועי שמירת הקבצים.\n\n___עם זאת, כדי להפעיל אחד מהשינויים הללו, יש לבנות מחדש גם את מסד הנתונים המרוחד וגם את המקומי. תהליך זה לוקח כמה דקות, ואנו ממליצים לעשות זאת כשיש לך זמן פנוי.___\n\n- אם ברצונך לשמור את ההתנהגות הקודמת, ניתן לדלג על תהליך זה באמצעות `${KEEP}`.\n- אם אין לך מספיק זמן, אנא בחר `${DISMISS}`. תקבל תזכורת בהמשך.\n- אם בנית מחדש את מסד הנתונים במכשיר אחר, אנא בחר `${DISMISS}` ונסה לסנכרן שוב. מאחר שזוהה הפרש, תקבל תזכורת שוב."; - readonly ja: "v0.23.21以降、self-hosted LiveSyncはデフォルトの動作とデータベース構造を変更しました。以下の変更が行われました:\n\n1. **ファイル名の大文字小文字の区別**\n ファイル名の処理が大文字小文字を区別しなくなりました。これは、ファイル名の大文字小文字を効果的に管理しないLinuxとiOS以外のほとんどのプラットフォームにとって有益な変更です。\n (これらの環境では、同じ名前で大文字小文字が異なるファイルに対して警告が表示されます)。\n\n2. **チャンクのリビジョン処理**\n チャンクは不変であり、リビジョンを固定できます。この変更により、ファイル保存のパフォーマンスが向上します。\n\n___しかし、これらの変更を有効にするには、リモートとローカルの両方のデータベースを再構築する必要があります。このプロセスは数分かかります。時間に余裕があるときに行うことをお勧めします。___\n\n- 以前の動作を維持したい場合は、`${KEEP}`を使用してこのプロセスをスキップできます。\n- 時間がない場合は、`${DISMISS}`を選択してください。後で再度確認されます。\n- 別のデバイスでデータベースを再構築した場合は、`${DISMISS}`を選択して再度同期してみてください。差異が検出されたため、再度確認されます。"; - readonly ko: "v0.23.21부터 Self-hosted LiveSync의 기본 동작 방식과 데이터베이스 구조가 변경되었습니다. 주요 변경사항은 다음과 같습니다:\n\n1. **파일명 대소문자 구분 처리**\n 이제 파일명은 대소문자를 구분하지 않고 처리됩니다. 이는 파일명 구분을 제대로 지원하지 않는 Linux 및 iOS를 제외한 대부분의 플랫폼에서 유리한 변화입니다.\n (Linux나 iOS에서는 대소문자만 다른 파일이 존재할 경우 경고가 표시됩니다)\n\n2. **청크 리비전 관리 방식 개선**\n 청크는 변경 불가능한(immutable) 구조로 고정되며, 이를 통해 리비전 처리가 안정화되고 파일 저장 성능이 향상됩니다.\n\n___단, 위 기능을 활성화하려면 원격 및 로컬 데이터베이스를 모두 재구성해야 합니다. 이 과정은 수 분이 소요되므로 여유가 있을 때 실행하시는 것을 권장합니다.___\n\n- 기존 방식대로 유지하려면 `${KEEP}`을 선택해 이 과정을 건너뛸 수 있습니다.\n- 시간이 부족하다면 `${DISMISS}`를 눌러주시면 나중에 다시 안내드리겠습니다.\n- 이미 다른 기기에서 데이터베이스를 재구성하셨다면 `${DISMISS}`를 선택한 뒤 다시 동기화해 보세요. 차이점이 감지되면 다시 안내드리겠습니다."; - readonly ru: "Начиная с v0.23.21, self-hosted LiveSync изменил поведение и структуру базы данных."; - readonly zh: "自 v0.23.21 起,Self-hosted LiveSync 更改了默认行为和数据库结构。进行了以下更改:\n\n1. **文件名的区分大小写** \n现在处理文件名时不区分大小写。这对于大多数平台来说是一个有益的更改,除了 Linux 和 iOS,它们不能有效地管理文件名的大小写敏感性。\n(在这些平台上,对于名称相同但大小写不同的文件将显示警告)。\n\n2. **chunks 的版本处理** \nchunks 是不可变的,这使得它们的版本可以固定。此更改将提高文件保存的性能。\n\n___然而,要启用这些更改中的任何一个,都需要重建远程和本地数据库。这个过程需要几分钟,我们建议您在有充足时间时进行。___\n\n- 如果您希望保持以前的行为,可以使用 `${KEEP}` 跳过此过程。\n- 如果您没有足够的时间,请选择 `${DISMISS}`。稍后会再次提示您。\n- 如果您已在另一台设备上重建了数据库,请选择 `${DISMISS}` 并尝试再次同步。由于检测到差异,系统会再次提示您"; - }; - readonly "moduleMigration.optionAdjustRemote": { - readonly def: "Adjust to remote"; - readonly es: "Ajustar al remoto"; - readonly fr: "Ajuster au distant"; - readonly he: "התאם לשרת המרוחד"; - readonly ja: "リモートに合わせる"; - readonly ko: "원격에 맞추기"; - readonly ru: "Настроить под удалённую"; - readonly zh: "调整到远程设置"; - }; - readonly "moduleMigration.optionDecideLater": { - readonly def: "Decide it later"; - readonly es: "Decidirlo más tarde"; - readonly fr: "Décider plus tard"; - readonly he: "החלט מאוחר יותר"; - readonly ja: "後で決める"; - readonly ko: "나중에 결정하기"; - readonly ru: "Решить позже"; - readonly zh: "稍后决定"; - }; - readonly "moduleMigration.optionEnableBoth": { - readonly def: "Enable both"; - readonly es: "Habilitar ambos"; - readonly fr: "Activer les deux"; - readonly he: "הפעל את שניהם"; - readonly ja: "両方を有効にする"; - readonly ko: "둘 다 활성화"; - readonly ru: "Включить оба"; - readonly zh: "启用两者"; - }; - readonly "moduleMigration.optionEnableFilenameCaseInsensitive": { - readonly def: "Enable only #1"; - readonly es: "Habilitar solo #1"; - readonly fr: "Activer seulement #1"; - readonly he: "הפעל רק #1"; - readonly ja: "#1のみ有効にする"; - readonly ko: "#1만 활성화"; - readonly ru: "Включить только #1"; - readonly zh: "仅启用 #1"; - }; - readonly "moduleMigration.optionEnableFixedRevisionForChunks": { - readonly def: "Enable only #2"; - readonly es: "Habilitar solo #2"; - readonly fr: "Activer seulement #2"; - readonly he: "הפעל רק #2"; - readonly ja: "#2のみ有効にする"; - readonly ko: "#2만 활성화"; - readonly ru: "Включить только #2"; - readonly zh: "仅启用 #2"; - }; - readonly "moduleMigration.optionHaveSetupUri": { - readonly def: "Yes, I have"; - readonly es: "Sí, tengo"; - readonly fr: "Oui, j'en ai une"; - readonly he: "כן, יש לי"; - readonly ja: "はい、持っています"; - readonly ko: "예, 있습니다"; - readonly ru: "Да, есть"; - readonly zh: "是的,我有"; - }; - readonly "moduleMigration.optionKeepPreviousBehaviour": { - readonly def: "Keep previous behaviour"; - readonly es: "Mantener comportamiento anterior"; - readonly fr: "Conserver le comportement précédent"; - readonly he: "שמור על התנהגות קודמת"; - readonly ja: "以前の動作を維持"; - readonly ko: "이전 동작 유지"; - readonly ru: "Сохранить предыдущее поведение"; - readonly zh: "保持以前的行为"; - }; - readonly "moduleMigration.optionManualSetup": { - readonly def: "Set it up all manually"; - readonly es: "Configurarlo todo manualmente"; - readonly fr: "Tout configurer manuellement"; - readonly he: "הגדר הכל ידנית"; - readonly ja: "すべて手動でセットアップ"; - readonly ko: "모든 것을 수동으로 설정"; - readonly ru: "Настроить всё вручную"; - readonly zh: "全部手动设置"; - }; - readonly "moduleMigration.optionNoAskAgain": { - readonly def: "No, please ask again"; - readonly es: "No, por favor pregúntame de nuevo"; - readonly fr: "Non, demandez à nouveau"; - readonly he: "לא, אנא שאל שוב"; - readonly ja: "いいえ、後で確認する"; - readonly ko: "아니요 (나중에 다시 물어보기)"; - readonly ru: "Нет, спросить снова"; - readonly zh: "不,请稍后再次询问"; - }; - readonly "moduleMigration.optionNoSetupUri": { - readonly def: "No, I do not have"; - readonly fr: "Non, je n'en ai pas"; - readonly he: "לא, אין לי"; - readonly ja: "いいえ、持っていません"; - readonly ko: "아니요, 없습니다"; - readonly ru: "Нет, нет"; - readonly zh: "不,我没有"; - }; - readonly "moduleMigration.optionRemindNextLaunch": { - readonly def: "Remind me at the next launch"; - readonly fr: "Me rappeler au prochain lancement"; - readonly he: "הזכר לי בהפעלה הבאה"; - readonly ja: "次回起動時にリマインド"; - readonly ko: "다음 시작 시 알림"; - readonly ru: "Напомнить при следующем запуске"; - readonly zh: "下次启动时提醒我"; - }; - readonly "moduleMigration.optionSetupViaP2P": { - readonly def: "Use P2P Sync to set up"; - readonly fr: "Utiliser Sync P2P pour configurer"; - readonly he: "השתמש ב-%{short_p2p_sync} להגדרה"; - readonly ja: "P2P Sync (試験機能)を使ってセットアップ"; - readonly ko: "P2P 동기화 (실험 기능)를 사용하여 설정"; - readonly ru: "Использовать short_p2p_sync для настройки"; - readonly zh: "Use P2P同步(实验性) to set up"; - }; - readonly "moduleMigration.optionSetupWizard": { - readonly def: "Take me into the setup wizard"; - readonly fr: "Ouvrir l'assistant de configuration"; - readonly he: "קח אותי לאשף ההגדרה"; - readonly ja: "セットアップウィザードへ"; - readonly ko: "설정 마법사로 안내"; - readonly ru: "Перейти в мастер настройки"; - readonly zh: "带我进入设置向导"; - }; - readonly "moduleMigration.optionYesFetchAgain": { - readonly def: "Yes, fetch again"; - readonly fr: "Oui, récupérer à nouveau"; - readonly he: "כן, משוך שוב"; - readonly ja: "はい、再フェッチする"; - readonly ko: "예 (다시 가져오기)"; - readonly ru: "Да, загрузить снова"; - readonly zh: "是的,再次获取"; - }; - readonly "moduleMigration.titleCaseSensitivity": { - readonly def: "Case Sensitivity"; - readonly fr: "Sensibilité à la casse"; - readonly he: "תלות רישיות"; - readonly ja: "大文字小文字の区別"; - readonly ko: "대소문자 구분"; - readonly ru: "Чувствительность к регистру"; - readonly zh: "大小写敏感性"; - }; - readonly "moduleMigration.titleRecommendSetupUri": { - readonly def: "Recommendation to use Setup URI"; - readonly fr: "Recommandation d'utilisation de l'URI de configuration"; - readonly he: "המלצה לשימוש ב-Setup URI"; - readonly ja: "セットアップURIの使用を推奨"; - readonly ko: "Setup URI 사용 권장"; - readonly ru: "Рекомендация использовать Setup URI"; - readonly zh: "推荐使用设置 URI"; - }; - readonly "moduleMigration.titleWelcome": { - readonly def: "Welcome to Self-hosted LiveSync"; - readonly fr: "Bienvenue dans Self-hosted LiveSync"; - readonly he: "ברוך הבא ל-Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSyncへようこそ"; - readonly ko: "Self-hosted LiveSync에 오신 것을 환영합니다"; - readonly ru: "Добро пожаловать в Self-hosted LiveSync"; - readonly zh: "欢迎使用 Self-hosted LiveSync"; - }; - readonly "moduleObsidianMenu.replicate": { - readonly def: "Replicate"; - readonly es: "Replicar"; - readonly fr: "Répliquer"; - readonly he: "שכפל"; - readonly ja: "レプリケート"; - readonly ko: "복제"; - readonly ru: "Реплицировать"; - readonly zh: "复制"; - }; - readonly "More actions": { - readonly def: "More actions"; - readonly es: "Más acciones"; - readonly ja: "その他の操作"; - readonly ko: "추가 작업"; - readonly ru: "Другие действия"; - readonly zh: "更多操作"; - readonly "zh-tw": "更多操作"; - }; - readonly "Move remotely deleted files to the trash, instead of deleting.": { - readonly def: "Move remotely deleted files to the trash, instead of deleting."; - readonly es: "Mover archivos borrados remotos a papelera en lugar de eliminarlos"; - readonly fr: "Déplacer les fichiers supprimés à distance vers la corbeille, au lieu de les supprimer."; - readonly he: "העבר קבצים שנמחקו מרחוק לאשפה, במקום למחוק."; - readonly ja: "リモートで削除されたファイルを削除せずにゴミ箱に移動する。"; - readonly ko: "원격에서 삭제된 파일을 삭제하는 대신 휴지통으로 이동합니다."; - readonly ru: "Перемещать удалённые на удалённом сервере файлы в корзину вместо удаления."; - readonly zh: "将远程删除的文件移至回收站,而不是直接删除"; - }; - readonly "Network warning style": { - readonly def: "Network warning style"; - readonly es: "Estilo de advertencia de red"; - readonly ja: "ネットワーク警告の表示方式"; - readonly ko: "네트워크 경고 표시 방식"; - readonly ru: "Стиль сетевого предупреждения"; - readonly zh: "网络警告样式"; - readonly "zh-tw": "網路警告樣式"; - }; - readonly "New Remote": { - readonly def: "New Remote"; - readonly es: "Nuevo remoto"; - readonly ja: "新しいリモート"; - readonly ko: "새 원격"; - readonly ru: "Новое удалённое хранилище"; - readonly zh: "新建远端"; - readonly "zh-tw": "新增遠端"; - }; - readonly "No connected device information found. Cancelling Garbage Collection.": { - readonly def: "No connected device information found. Cancelling Garbage Collection."; - readonly ja: "接続済みデバイスの情報が見つかりませんでした。Garbage Collection をキャンセルします。"; - readonly ko: "연결된 기기 정보를 찾을 수 없습니다. Garbage Collection을 취소합니다."; - readonly ru: "Не найдена информация о подключённых устройствах. Garbage Collection отменяется."; - readonly zh: "未找到已连接设备的信息。正在取消垃圾回收。"; - readonly "zh-tw": "找不到已連線裝置的資訊。正在取消垃圾回收。"; - }; - readonly "No limit configured": { - readonly def: "No limit configured"; - readonly es: "Sin límite configurado"; - readonly ja: "制限は設定されていません"; - readonly ko: "제한이 설정되지 않음"; - readonly ru: "Лимит не задан"; - readonly zh: "未配置限制"; - readonly "zh-tw": "尚未設定限制"; - }; - readonly "No, please take me back": { - readonly def: "No, please take me back"; - readonly es: "No, volver atrás"; - readonly ja: "いいえ、前に戻ります"; - readonly ko: "아니요, 이전으로 돌아가겠습니다"; - readonly ru: "Нет, верните меня назад"; - readonly zh: "不,返回上一步"; - readonly "zh-tw": "不,返回上一步"; - }; - readonly "Node ID": { - readonly def: "Node ID"; - readonly ja: "ノード ID"; - readonly ko: "노드 ID"; - readonly ru: "ID узла"; - readonly zh: "节点 ID"; - readonly "zh-tw": "節點 ID"; - }; - readonly "Node Information Missing": { - readonly def: "Node Information Missing"; - readonly ja: "ノード情報がありません"; - readonly ko: "노드 정보 누락"; - readonly ru: "Отсутствует информация об узле"; - readonly zh: "节点信息缺失"; - readonly "zh-tw": "節點資訊缺失"; - }; - readonly "Non-Synchronising files": { - readonly def: "Non-Synchronising files"; - readonly es: "Archivos no sincronizados"; - readonly ja: "同期しないファイル"; - readonly ko: "동기화하지 않는 파일"; - readonly ru: "Несинхронизируемые файлы"; - readonly zh: "不同步的文件"; - readonly "zh-tw": "不同步的檔案"; - }; - readonly "Normal Files": { - readonly def: "Normal Files"; - readonly es: "Archivos normales"; - readonly ja: "通常ファイル"; - readonly ko: "일반 파일"; - readonly ru: "Обычные файлы"; - readonly zh: "普通文件"; - readonly "zh-tw": "一般檔案"; - }; - readonly 'Not all messages have been translated. And, please revert to "Default" when reporting errors.': { - readonly def: "Not all messages have been translated. And, please revert to \"Default\" when reporting errors."; - readonly es: "No todos los mensajes están traducidos. Por favor, vuelva a \"Predeterminado\" al reportar errores."; - readonly fr: "Tous les messages n'ont pas été traduits. Et veuillez revenir à « Par défaut » lorsque vous signalez des erreurs."; - readonly he: "לא כל ההודעות תורגמו. בנוסף, אנא חזור ל\"ברירת מחדל\" בעת דיווח על שגיאות."; - readonly ja: "すべてのメッセージが翻訳されているわけではありません。また、Issue報告の際にはいったん\"Default\"に戻してください"; - readonly ko: "모든 메시지가 번역되지 않았습니다. 오류 신고 시 \"기본값\"으로 되돌려 주세요."; - readonly ru: "Не все сообщения переведены. И, пожалуйста, вернитесь к «По умолчанию» при сообщении об ошибках."; - readonly zh: "并非所有消息都已翻译。请在报告错误时恢复为\"默认\""; - }; - readonly "Notify all setting files": { - readonly def: "Notify all setting files"; - readonly es: "Notificar todos los archivos de configuración"; - readonly fr: "Notifier tous les fichiers de paramètres"; - readonly he: "הודע על כל קבצי ההגדרות"; - readonly ja: "すべての設定を通知"; - readonly ko: "모든 설정 파일 알림"; - readonly ru: "Уведомлять обо всех файлах настроек"; - readonly zh: "通知所有设置文件"; - }; - readonly "Notify customized": { - readonly def: "Notify customized"; - readonly es: "Notificar personalizaciones"; - readonly fr: "Notifier les personnalisations"; - readonly he: "הודע על התאמות אישיות"; - readonly ja: "カスタマイズが行われたら通知する"; - readonly ko: "사용자 설정 알림"; - readonly ru: "Уведомлять о настройках"; - readonly zh: "通知自定义设置"; - }; - readonly "Notify when other device has newly customized.": { - readonly def: "Notify when other device has newly customized."; - readonly es: "Notificar cuando otro dispositivo personalice"; - readonly fr: "Notifier lorsqu'un autre appareil a une nouvelle personnalisation."; - readonly he: "הודע כאשר מכשיר אחר הוסיף התאמה אישית חדשה."; - readonly ja: "別の端末がカスタマイズを行なったら通知する"; - readonly ko: "다른 기기에서 새로운 사용자 설정이 있을 때 알림을 받습니다."; - readonly ru: "Уведомлять, когда другое устройство изменило настройки."; - readonly zh: "当其他设备有新的自定义设置时通知 "; - }; - readonly "Notify when the estimated remote storage size exceeds on start up": { - readonly def: "Notify when the estimated remote storage size exceeds on start up"; - readonly es: "Notificar cuando el tamaño estimado del almacenamiento remoto exceda al iniciar"; - readonly fr: "Notifier quand la taille estimée du stockage distant est dépassée au démarrage"; - readonly he: "הודע כשגודל האחסון המרוחד המשוער עולה על הסף בעת הפעלה"; - readonly ja: "起動時に予想リモートストレージサイズを超えたら通知"; - readonly ko: "시작 시 예상 원격 스토리지 크기가 초과되면 알림"; - readonly ru: "Уведомлять, когда оценочный размер удалённого хранилища превышает при запуске"; - readonly zh: "启动时当估计的远程存储大小超出时通知"; - }; - readonly "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": { - readonly def: "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time."; - readonly es: "Número de lotes a procesar. Default 40, mínimo 2. Controla documentos en memoria"; - readonly fr: "Nombre de lots à traiter à la fois. Par défaut 40. Minimum 2. Ceci, avec la taille de lot, contrôle le nombre de documents conservés en mémoire à la fois."; - readonly he: "מספר האצוות לעיבוד בכל פעם. ברירת מחדל 40, מינימום 2. יחד עם גודל האצווה קובע כמה מסמכים נשמרים בזיכרון בו-זמנית."; - readonly ja: "1度に処理するバッチの数。デフォルトは40、最小は2。この数値は、どれだけの容量の書類がメモリに保存されるかも定義します。"; - readonly ko: "한 번에 처리할 일괄 처리 수입니다. 기본값은 40입니다. 최소값은 2입니다. 이는 일괄 크기와 함께 메모리에 보관되는 문서 수를 제어합니다."; - readonly ru: "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time."; - readonly zh: "一次处理的批量数量。默认为 40。最小为 2。此设置与批量大小一起控制一次在内存中保留多少文档"; - }; - readonly "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": { - readonly def: "Number of changes to sync at a time. Defaults to 50. Minimum is 2."; - readonly es: "Número de cambios a sincronizar simultáneamente. Default 50, mínimo 2"; - readonly fr: "Nombre de modifications à synchroniser à la fois. Par défaut 50. Minimum 2."; - readonly he: "מספר השינויים לסנכרון בכל פעם. ברירת מחדל 50, מינימום 2."; - readonly ja: "一度に同期する変更の数。デフォルトは50、最小は2。"; - readonly ko: "한 번에 동기화할 변경 사항의 수입니다. 기본값은 50입니다. 최소값은 2입니다."; - readonly ru: "Количество изменений для синхронизации за раз. По умолчанию 50. Минимум 2."; - readonly zh: "一次同步的更改数量。默认为 50。最小为 2。"; - }; - readonly "Obsidian version": { - readonly def: "Obsidian version"; - readonly ja: "Obsidian バージョン"; - readonly ko: "Obsidian 버전"; - readonly ru: "Версия Obsidian"; - readonly zh: "Obsidian 版本"; - readonly "zh-tw": "Obsidian 版本"; - }; - readonly "obsidianLiveSyncSettingTab.btnApply": { - readonly def: "Apply"; - readonly es: "Aplicar"; - readonly fr: "Appliquer"; - readonly he: "החל"; - readonly ja: "適用"; - readonly ko: "적용"; - readonly ru: "Применить"; - readonly zh: "应用"; - readonly "zh-tw": "套用"; - }; - readonly "obsidianLiveSyncSettingTab.btnCheck": { - readonly def: "Check"; - readonly es: "Verificar"; - readonly fr: "Vérifier"; - readonly he: "בדוק"; - readonly ja: "確認"; - readonly ko: "확인"; - readonly ru: "Проверить"; - readonly zh: "检查"; - }; - readonly "obsidianLiveSyncSettingTab.btnCopy": { - readonly def: "Copy"; - readonly es: "Copiar"; - readonly fr: "Copier"; - readonly he: "העתק"; - readonly ja: "コピー"; - readonly ko: "복사"; - readonly ru: "Копировать"; - readonly zh: "复制"; - }; - readonly "obsidianLiveSyncSettingTab.btnDisable": { - readonly def: "Disable"; - readonly es: "Desactivar"; - readonly fr: "Désactiver"; - readonly he: "נטרל"; - readonly ja: "無効化"; - readonly ko: "비활성화"; - readonly ru: "Отключить"; - readonly zh: "禁用"; - readonly "zh-tw": "停用"; - }; - readonly "obsidianLiveSyncSettingTab.btnDiscard": { - readonly def: "Discard"; - readonly es: "Descartar"; - readonly fr: "Abandonner"; - readonly he: "ביטול שינויים"; - readonly ja: "破棄"; - readonly ko: "삭제"; - readonly ru: "Отменить"; - readonly zh: "丢弃"; - }; - readonly "obsidianLiveSyncSettingTab.btnEnable": { - readonly def: "Enable"; - readonly es: "Activar"; - readonly fr: "Activer"; - readonly he: "הפעל"; - readonly ja: "有効化"; - readonly ko: "활성화"; - readonly ru: "Включить"; - readonly zh: "启用"; - }; - readonly "obsidianLiveSyncSettingTab.btnFix": { - readonly def: "Fix"; - readonly es: "Corregir"; - readonly fr: "Corriger"; - readonly he: "תקן"; - readonly ja: "修正"; - readonly ko: "수정"; - readonly ru: "Исправить"; - readonly zh: "修复"; - }; - readonly "obsidianLiveSyncSettingTab.btnGotItAndUpdated": { - readonly def: "I got it and updated."; - readonly es: "Lo entendí y actualicé."; - readonly fr: "J'ai compris et mis à jour."; - readonly he: "הבנתי ועדכנתי."; - readonly ja: "理解しました、更新しました。"; - readonly ko: "알겠습니다. 업데이트했습니다."; - readonly ru: "Понял и обновил."; - readonly zh: "我明白了并且已更新"; - }; - readonly "obsidianLiveSyncSettingTab.btnNext": { - readonly def: "Next"; - readonly es: "Siguiente"; - readonly fr: "Suivant"; - readonly he: "הבא"; - readonly ja: "次へ"; - readonly ko: "다음"; - readonly ru: "Далее"; - readonly zh: "下一步"; - readonly "zh-tw": "下一步"; - }; - readonly "obsidianLiveSyncSettingTab.btnStart": { - readonly def: "Start"; - readonly es: "Iniciar"; - readonly fr: "Démarrer"; - readonly he: "התחל"; - readonly ja: "開始"; - readonly ko: "시작"; - readonly ru: "Старт"; - readonly zh: "开始"; - }; - readonly "obsidianLiveSyncSettingTab.btnTest": { - readonly def: "Test"; - readonly es: "Probar"; - readonly fr: "Tester"; - readonly he: "בדוק"; - readonly ja: "テスト"; - readonly ko: "테스트"; - readonly ru: "Тест"; - readonly zh: "测试"; - }; - readonly "obsidianLiveSyncSettingTab.btnUse": { - readonly def: "Use"; - readonly es: "Usar"; - readonly fr: "Utiliser"; - readonly he: "השתמש"; - readonly ja: "使用"; - readonly ko: "사용"; - readonly ru: "Использовать"; - readonly zh: "使用"; - }; - readonly "obsidianLiveSyncSettingTab.buttonFetch": { - readonly def: "Fetch"; - readonly es: "Obtener"; - readonly fr: "Récupérer"; - readonly he: "משוך"; - readonly ja: "フェッチ"; - readonly ko: "가져오기"; - readonly ru: "Загрузить"; - readonly zh: "获取"; - }; - readonly "obsidianLiveSyncSettingTab.buttonNext": { - readonly def: "Next"; - readonly es: "Siguiente"; - readonly fr: "Suivant"; - readonly he: "הבא"; - readonly ja: "次へ"; - readonly ko: "다음"; - readonly ru: "Далее"; - readonly zh: "下一步"; - readonly "zh-tw": "下一步"; - }; - readonly "obsidianLiveSyncSettingTab.defaultLanguage": { - readonly def: "Default"; - readonly es: "Predeterminado"; - readonly fr: "Par défaut"; - readonly he: "ברירת מחדל"; - readonly ja: "デフォルト"; - readonly ko: "기본값"; - readonly ru: "По умолчанию"; - readonly zh: "默认语言"; - readonly "zh-tw": "預設語言"; - }; - readonly "obsidianLiveSyncSettingTab.descConnectSetupURI": { - readonly def: "This is the recommended method to set up Self-hosted LiveSync with a Setup URI."; - readonly es: "Este es el método recomendado para configurar Self-hosted LiveSync con una URI de configuración."; - readonly fr: "Méthode recommandée pour configurer Self-hosted LiveSync avec une URI de configuration."; - readonly he: "זוהי השיטה המומלצת להגדרת Self-hosted LiveSync עם Setup URI."; - readonly ja: "セットアップURIを使用してSelf-hosted LiveSyncをセットアップする推奨方法です。"; - readonly ko: "이것은 Setup URI로 Self-hosted LiveSync를 설정하는 권장 방법입니다."; - readonly ru: "Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI."; - readonly zh: "这是使用设置 URI 设置 Self-hosted LiveSync 的推荐方法"; - }; - readonly "obsidianLiveSyncSettingTab.descCopySetupURI": { - readonly def: "Perfect for setting up a new device!"; - readonly es: "¡Perfecto para configurar un nuevo dispositivo!"; - readonly fr: "Parfait pour configurer un nouvel appareil !"; - readonly he: "מושלם להגדרת מכשיר חדש!"; - readonly ja: "新しいデバイスのセットアップにおすすめ!"; - readonly ko: "새 기기 설정에 완벽합니다!"; - readonly ru: "Идеально подходит для настройки нового устройства!"; - readonly zh: "非常适合设置新设备!"; - }; - readonly "obsidianLiveSyncSettingTab.descEnableLiveSync": { - readonly def: "Only enable this after configuring either of the above two options or completing all configuration manually."; - readonly es: "Solo habilita esto después de configurar cualquiera de las dos opciones anteriores o completar toda la configuración manualmente."; - readonly fr: "N'activez ceci qu'après avoir configuré l'une des deux options ci-dessus ou terminé toute la configuration manuellement."; - readonly he: "הפעל רק לאחר הגדרת אחת משתי האפשרויות לעיל, או לאחר השלמת כל ההגדרות ידנית."; - readonly ja: "上記の2つのオプションのいずれかを設定するか、すべての設定を手動で完了した後にのみ有効にしてください。"; - readonly ko: "위의 두 옵션 중 하나를 구성하거나 모든 구성을 수동으로 완료한 후에만 활성화하세요."; - readonly ru: "Включайте это только после настройки одного из двух вариантов выше или после полного ручного завершения всей конфигурации."; - readonly zh: "仅在配置了上述两个选项之一或手动完成所有配置后启用此选项"; - }; - readonly "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": { - readonly def: "Fetch necessary settings from already configured remote server."; - readonly es: "Obtener las configuraciones necesarias del servidor remoto ya configurado."; - readonly fr: "Récupérer les paramètres nécessaires depuis un serveur distant déjà configuré."; - readonly he: "משוך הגדרות נדרשות מהשרת המרוחד שהוגדר כבר."; - readonly ja: "既に設定済みのリモートサーバーから必要な設定を取得します。"; - readonly ko: "이미 구성된 원격 서버에서 필요한 설정을 가져옵니다."; - readonly ru: "Получить необходимые настройки с уже настроенного удалённого сервера."; - readonly zh: "从已配置的远程服务器获取必要的设置"; - }; - readonly "obsidianLiveSyncSettingTab.descManualSetup": { - readonly def: "Not recommended, but useful if you don't have a Setup URI"; - readonly es: "No recomendado, pero útil si no tienes una URI de configuración"; - readonly fr: "Non recommandé, mais utile si vous n'avez pas d'URI de configuration"; - readonly he: "לא מומלץ, אך שימושי אם אין לך Setup URI"; - readonly ja: "推奨しませんが、セットアップURIがない場合に便利です"; - readonly ko: "권장하지 않지만 Setup URI가 없는 경우에 유용합니다"; - readonly ru: "Не рекомендуется, но полезно, если у вас нет Setup URI."; - readonly zh: "不推荐,但如果您没有设置 URI 则很有用"; - }; - readonly "obsidianLiveSyncSettingTab.descTestDatabaseConnection": { - readonly def: "Open database connection. If the remote database is not found and you have permission to create a database, the database will be created."; - readonly es: "Abrir conexión a la base de datos. Si no se encuentra la base de datos remota y tienes permiso para crear una base de datos, se creará la base de datos."; - readonly fr: "Ouvrir la connexion à la base de données. Si la base distante est introuvable et que vous avez l'autorisation de créer une base, elle sera créée."; - readonly he: "פתח חיבור למסד נתונים. אם מסד הנתונים המרוחד לא נמצא ויש לך הרשאה ליצור אחד, הוא ייצור."; - readonly ja: "データベース接続を開きます。リモートデータベースが見つからず、データベースを作成する権限がある場合は、データベースが作成されます。"; - readonly ko: "데이터베이스 연결을 엽니다. 원격 데이터베이스를 찾을 수 없고 데이터베이스 생성 권한이 있는 경우, 데이터베이스가 생성됩니다."; - readonly ru: "Открыть подключение к базе данных. Если удалённая база данных не найдена и у вас есть право на её создание, база будет создана."; - readonly zh: "打开数据库连接。如果未找到远程数据库并且您有创建数据库的权限,则将创建数据库"; - }; - readonly "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": { - readonly def: "Checks and fixes any potential issues with the database config."; - readonly es: "Verifica y soluciona cualquier problema potencial con la configuración de la base de datos."; - readonly fr: "Vérifie et corrige les problèmes potentiels de la configuration de la base."; - readonly he: "בודק ומתקן בעיות אפשריות בתצורת מסד הנתונים."; - readonly ja: "データベース設定の潜在的な問題を確認し、修正します。"; - readonly ko: "데이터베이스 구성의 잠재적 문제를 확인하고 수정합니다."; - readonly ru: "Проверяет и исправляет любые потенциальные проблемы в конфигурации базы данных."; - readonly zh: "检查并修复数据库配置中的任何潜在问题"; - }; - readonly "obsidianLiveSyncSettingTab.errAccessForbidden": { - readonly def: "❗ Access forbidden."; - readonly es: "Acceso prohibido."; - readonly fr: "❗ Accès interdit."; - readonly he: "❗ גישה נדחתה."; - readonly ja: "❗ アクセスが禁止されています。"; - readonly ko: "❗ 액세스가 금지되었습니다."; - readonly ru: "❗ Доступ запрещён."; - readonly zh: "❗ 访问被禁止"; - }; - readonly "obsidianLiveSyncSettingTab.errCannotContinueTest": { - readonly def: "We could not continue the test."; - readonly es: "No se pudo continuar con la prueba."; - readonly fr: "Impossible de poursuivre le test."; - readonly he: "לא ניתן להמשיך בבדיקה."; - readonly ja: "テストを続行できませんでした。"; - readonly ko: "테스트를 계속할 수 없습니다."; - readonly ru: "Мы не можем продолжить тест."; - readonly zh: "我们无法继续测试。"; - }; - readonly "obsidianLiveSyncSettingTab.errCorsCredentials": { - readonly def: "❗ cors.credentials is wrong"; - readonly es: "❗ cors.credentials es incorrecto"; - readonly fr: "❗ cors.credentials est incorrect"; - readonly he: "❗ cors.credentials שגוי"; - readonly ja: "❗ cors.credentialsが不正です"; - readonly ko: "❗ cors.credentials가 잘못되었습니다"; - readonly ru: "❗ cors.credentials неверно"; - readonly zh: "❗ cors.credentials 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": { - readonly def: "❗ CORS is not allowing credentials"; - readonly es: "CORS no permite credenciales"; - readonly fr: "❗ CORS n'autorise pas les identifiants"; - readonly he: "❗ CORS אינו מאפשר פרטי גישה"; - readonly ja: "❗ CORSが認証情報を許可していません"; - readonly ko: "❗ CORS에서 자격 증명을 허용하지 않습니다"; - readonly ru: "❗ CORS не разрешает учётные данные"; - readonly zh: "❗ CORS 不允许凭据"; - }; - readonly "obsidianLiveSyncSettingTab.errCorsOrigins": { - readonly def: "❗ cors.origins is wrong"; - readonly es: "❗ cors.origins es incorrecto"; - readonly fr: "❗ cors.origins est incorrect"; - readonly he: "❗ cors.origins שגוי"; - readonly ja: "❗ cors.originsが不正です"; - readonly ko: "❗ cors.origins가 잘못되었습니다"; - readonly ru: "❗ cors.origins неверно"; - readonly zh: "❗ cors.origins 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errEnableCors": { - readonly def: "❗ httpd.enable_cors is wrong"; - readonly es: "❗ httpd.enable_cors es incorrecto"; - readonly fr: "❗ httpd.enable_cors est incorrect"; - readonly he: "❗ httpd.enable_cors שגוי"; - readonly ja: "❗ httpd.enable_corsが不正です"; - readonly ko: "❗ httpd.enable_cors가 잘못되었습니다"; - readonly ru: "❗ httpd.enable_cors неверно"; - readonly zh: "❗ httpd.enable_cors 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errEnableCorsChttpd": { - readonly def: "❗ chttpd.enable_cors is wrong"; - readonly fr: "❗ chttpd.enable_cors est incorrect"; - readonly he: "❗ chttpd.enable_cors שגוי"; - readonly ja: "❗ chttpd.enable_corsが不正です"; - readonly ru: "❗ chttpd.enable_cors неверно"; - readonly zh: "❗ chttpd.enable_cors 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errMaxDocumentSize": { - readonly def: "❗ couchdb.max_document_size is low)"; - readonly es: "❗ couchdb.max_document_size es bajo)"; - readonly fr: "❗ couchdb.max_document_size est trop bas)"; - readonly he: "❗ couchdb.max_document_size נמוך)"; - readonly ja: "❗ couchdb.max_document_sizeが低すぎます"; - readonly ko: "❗ couchdb.max_document_size가 낮습니다)"; - readonly ru: "❗ couchdb.max_document_size низкое"; - readonly zh: "❗ couchdb.max_document_size 设置过低)"; - }; - readonly "obsidianLiveSyncSettingTab.errMaxRequestSize": { - readonly def: "❗ chttpd.max_http_request_size is low)"; - readonly es: "❗ chttpd.max_http_request_size es bajo)"; - readonly fr: "❗ chttpd.max_http_request_size est trop bas)"; - readonly he: "❗ chttpd.max_http_request_size נמוך)"; - readonly ja: "❗ chttpd.max_http_request_sizeが低すぎます"; - readonly ko: "❗ chttpd.max_http_request_size가 낮습니다)"; - readonly ru: "❗ chttpd.max_http_request_size низкое"; - readonly zh: "❗ chttpd.max_http_request_size 设置过低)"; - }; - readonly "obsidianLiveSyncSettingTab.errMissingWwwAuth": { - readonly def: "❗ httpd.WWW-Authenticate is missing"; - readonly es: "❗ httpd.WWW-Authenticate falta"; - readonly fr: "❗ httpd.WWW-Authenticate est manquant"; - readonly he: "❗ httpd.WWW-Authenticate חסר"; - readonly ja: "❗ httpd.WWW-Authenticateが不足しています"; - readonly ko: "❗ httpd.WWW-Authenticate가 누락되었습니다"; - readonly ru: "❗ httpd.WWW-Authenticate отсутствует"; - readonly zh: "❗ 缺少 httpd.WWW-Authenticate 设置"; - }; - readonly "obsidianLiveSyncSettingTab.errRequireValidUser": { - readonly def: "❗ chttpd.require_valid_user is wrong."; - readonly es: "❗ chttpd.require_valid_user es incorrecto."; - readonly fr: "❗ chttpd.require_valid_user est incorrect."; - readonly he: "❗ chttpd.require_valid_user שגוי."; - readonly ja: "❗ chttpd.require_valid_userが不正です。"; - readonly ko: "❗ chttpd.require_valid_user가 잘못되었습니다."; - readonly ru: "❗ chttpd.require_valid_user неверно."; - readonly zh: "❗ chttpd.require_valid_user 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.errRequireValidUserAuth": { - readonly def: "❗ chttpd_auth.require_valid_user is wrong."; - readonly es: "❗ chttpd_auth.require_valid_user es incorrecto."; - readonly fr: "❗ chttpd_auth.require_valid_user est incorrect."; - readonly he: "❗ chttpd_auth.require_valid_user שגוי."; - readonly ja: "❗ chttpd_auth.require_valid_userが不正です。"; - readonly ko: "❗ chttpd_auth.require_valid_user가 잘못되었습니다."; - readonly ru: "❗ chttpd_auth.require_valid_user неверно."; - readonly zh: "❗ chttpd_auth.require_valid_user 设置错误"; - }; - readonly "obsidianLiveSyncSettingTab.labelDisabled": { - readonly def: "⏹️ : Disabled"; - readonly es: "⏹️ : Desactivado"; - readonly fr: "⏹️ : Désactivé"; - readonly he: "⏹️ : מנוטרל"; - readonly ja: "⏹️ : 無効"; - readonly ko: "⏹️ : 비활성화됨"; - readonly ru: "⏹️ : Отключено"; - readonly zh: "⏹️:已禁用"; - readonly "zh-tw": "⏹️ : 已停用"; - }; - readonly "obsidianLiveSyncSettingTab.labelEnabled": { - readonly def: "🔁 : Enabled"; - readonly es: "🔁 : Activado"; - readonly fr: "🔁 : Activé"; - readonly he: "🔁 : מופעל"; - readonly ja: "🔁 : 有効"; - readonly ko: "🔁 : 활성화됨"; - readonly ru: "🔁 : Включено"; - readonly zh: "🔁:已启用"; - readonly "zh-tw": "🔁 : 已啟用"; - }; - readonly "obsidianLiveSyncSettingTab.levelAdvanced": { - readonly def: " (Advanced)"; - readonly es: " (avanzado)"; - readonly fr: " (Avancé)"; - readonly he: " (מתקדם)"; - readonly ja: " (上級)"; - readonly ko: " (고급)"; - readonly ru: " (Расширенные)"; - readonly zh: "(进阶)"; - }; - readonly "obsidianLiveSyncSettingTab.levelEdgeCase": { - readonly def: " (Edge Case)"; - readonly es: " (excepción)"; - readonly fr: " (Cas particulier)"; - readonly he: " (מקרה קצה)"; - readonly ja: " (エッジケース)"; - readonly ko: " (특수 사례)"; - readonly ru: " (Граничные случаи)"; - readonly zh: "(边缘情况)"; - }; - readonly "obsidianLiveSyncSettingTab.levelPowerUser": { - readonly def: " (Power User)"; - readonly es: " (experto)"; - readonly fr: " (Utilisateur avancé)"; - readonly he: " (משתמש מתקדם)"; - readonly ja: " (エキスパート)"; - readonly ko: " (파워 유저)"; - readonly ru: " (Опытный пользователь)"; - readonly zh: "(高级用户)"; - }; - readonly "obsidianLiveSyncSettingTab.linkOpenInBrowser": { - readonly def: "Open in browser"; - readonly es: "Abrir en el navegador"; - readonly fr: "Ouvrir dans le navigateur"; - readonly he: "פתח בדפדפן"; - readonly ja: "ブラウザで開く"; - readonly ko: "브라우저에서 열기"; - readonly ru: "Открыть в браузере"; - readonly zh: "在浏览器中打开"; - }; - readonly "obsidianLiveSyncSettingTab.linkPageTop": { - readonly def: "Page Top"; - readonly es: "Ir arriba"; - readonly fr: "Haut de la page"; - readonly he: "ראש העמוד"; - readonly ja: "ページトップ"; - readonly ko: "페이지 상단"; - readonly ru: "В начало страницы"; - readonly zh: "页面顶部"; - }; - readonly "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": { - readonly def: "Tips and Troubleshooting"; - readonly es: "Consejos y solución de problemas"; - readonly fr: "Conseils et dépannage"; - readonly he: "טיפים ופתרון בעיות"; - readonly ja: "ヒントとトラブルシューティング"; - readonly ko: "팁 및 문제 해결"; - readonly ru: "Советы и устранение неполадок"; - readonly zh: "提示和故障排除"; - }; - readonly "obsidianLiveSyncSettingTab.linkTroubleshooting": { - readonly def: "/docs/troubleshooting.md"; - readonly es: "/docs/es/troubleshooting.md"; - readonly fr: "/docs/troubleshooting.md"; - readonly he: "/docs/troubleshooting.md"; - readonly ja: "/docs/troubleshooting.md"; - readonly ko: "/docs/troubleshooting.md"; - readonly ru: "/docs/troubleshooting.md"; - readonly zh: "/docs/troubleshooting.md"; - }; - readonly "obsidianLiveSyncSettingTab.logCannotUseCloudant": { - readonly def: "This feature cannot be used with IBM Cloudant."; - readonly es: "Esta función no se puede utilizar con IBM Cloudant."; - readonly fr: "Cette fonctionnalité ne peut pas être utilisée avec IBM Cloudant."; - readonly he: "לא ניתן להשתמש בתכונה זו עם IBM Cloudant."; - readonly ja: "この機能はIBM Cloudantでは使用できません。"; - readonly ko: "이 기능은 IBM Cloudant와 함께 사용할 수 없습니다."; - readonly ru: "Эта функция недоступна для IBM Cloudant."; - readonly zh: "此功能不能与 IBM Cloudant 一起使用 "; - }; - readonly "obsidianLiveSyncSettingTab.logCheckingConfigDone": { - readonly def: "Checking configuration done"; - readonly es: "Verificación de configuración completada"; - readonly fr: "Vérification de la configuration terminée"; - readonly he: "בדיקת התצורה הושלמה"; - readonly ja: "設定の確認が完了しました"; - readonly ko: "구성 확인 완료"; - readonly ru: "Проверка конфигурации завершена"; - readonly zh: "配置检查完成"; - }; - readonly "obsidianLiveSyncSettingTab.logCheckingConfigFailed": { - readonly def: "Checking configuration failed"; - readonly es: "La verificación de configuración falló"; - readonly fr: "Échec de la vérification de la configuration"; - readonly he: "בדיקת התצורה נכשלה"; - readonly ja: "設定の確認に失敗しました"; - readonly ko: "구성 확인 실패"; - readonly ru: "Проверка конфигурации не удалась"; - readonly zh: "配置检查失败"; - }; - readonly "obsidianLiveSyncSettingTab.logCheckingDbConfig": { - readonly def: "Checking database configuration"; - readonly es: "Verificando la configuración de la base de datos"; - readonly fr: "Vérification de la configuration de la base"; - readonly he: "בודק תצורת מסד נתונים"; - readonly ja: "データベース設定を確認中"; - readonly ko: "데이터베이스 구성 확인 중"; - readonly ru: "Проверка конфигурации базы данных"; - readonly zh: "正在检查数据库配置"; - }; - readonly "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": { - readonly def: "ERROR: Failed to check passphrase with the remote server:\n${db}."; - readonly es: "ERROR: Error al comprobar la frase de contraseña con el servidor remoto: \n${db}."; - readonly fr: "ERREUR : Échec de la vérification de la phrase secrète avec le serveur distant :\n${db}."; - readonly he: "שגיאה: בדיקת ביטוי הסיסמה עם השרת המרוחד נכשלה:\n${db}."; - readonly ja: "エラー: リモートサーバーとのパスフレーズ確認に失敗しました:\n${db}。"; - readonly ko: "오류: 원격 서버와 패스프레이즈 확인에 실패했습니다: \n${db}."; - readonly ru: "ОШИБКА: Не удалось проверить пароль с удалённым сервером: db."; - readonly zh: "错误:无法使用远程服务器检查密码:\n${db} "; - }; - readonly "obsidianLiveSyncSettingTab.logConfiguredDisabled": { - readonly def: "Configured synchronization mode: DISABLED"; - readonly es: "Modo de sincronización configurado: DESACTIVADO"; - readonly fr: "Mode de synchronisation configuré : DÉSACTIVÉ"; - readonly he: "מצב סנכרון שהוגדר: מנוטרל"; - readonly ja: "設定された同期モード: 無効"; - readonly ko: "구성된 동기화 모드: 비활성화됨"; - readonly ru: "Настроенный режим синхронизации: ОТКЛЮЧЕН"; - readonly zh: "已配置的同步模式:已禁用"; - readonly "zh-tw": "已設定的同步模式:已停用"; - }; - readonly "obsidianLiveSyncSettingTab.logConfiguredLiveSync": { - readonly def: "Configured synchronization mode: LiveSync"; - readonly es: "Modo de sincronización configurado: Sincronización en Vivo"; - readonly fr: "Mode de synchronisation configuré : LiveSync"; - readonly he: "מצב סנכרון שהוגדר: LiveSync"; - readonly ja: "設定された同期モード: LiveSync"; - readonly ko: "구성된 동기화 모드: LiveSync"; - readonly ru: "Настроенный режим синхронизации: LiveSync"; - readonly zh: "已配置的同步模式:LiveSync"; - readonly "zh-tw": "已設定的同步模式:LiveSync"; - }; - readonly "obsidianLiveSyncSettingTab.logConfiguredPeriodic": { - readonly def: "Configured synchronization mode: Periodic"; - readonly es: "Modo de sincronización configurado: Periódico"; - readonly fr: "Mode de synchronisation configuré : Périodique"; - readonly he: "מצב סנכרון שהוגדר: תקופתי"; - readonly ja: "設定された同期モード: 定期"; - readonly ko: "구성된 동기화 모드: 주기적"; - readonly ru: "Настроенный режим синхронизации: Периодический"; - readonly zh: "已配置的同步模式:定期同步"; - readonly "zh-tw": "已設定的同步模式:定期同步"; - }; - readonly "obsidianLiveSyncSettingTab.logCouchDbConfigFail": { - readonly def: "CouchDB Configuration: ${title} failed"; - readonly es: "Configuración de CouchDB: ${title} falló"; - readonly fr: "Configuration CouchDB : échec de ${title}"; - readonly he: "תצורת CouchDB: ${title} נכשלה"; - readonly ja: "CouchDB設定: ${title} 失敗"; - readonly ko: "CouchDB 구성: ${title} 실패"; - readonly ru: "Конфигурация CouchDB: title не удалась"; - readonly zh: "CouchDB 配置:${title} 失败"; - }; - readonly "obsidianLiveSyncSettingTab.logCouchDbConfigSet": { - readonly def: "CouchDB Configuration: ${title} -> Set ${key} to ${value}"; - readonly es: "Configuración de CouchDB: ${title} -> Establecer ${key} en ${value}"; - readonly fr: "Configuration CouchDB : ${title} -> ${key} défini à ${value}"; - readonly he: "תצורת CouchDB: ${title} -> הגדר ${key} ל-${value}"; - readonly ja: "CouchDB設定: ${title} -> ${key}を${value}に設定"; - readonly ko: "CouchDB 구성: ${title} -> ${key}를 ${value}로 설정"; - readonly ru: "Конфигурация CouchDB: title -> Установить key в value"; - readonly zh: "CouchDB 配置:${title} -> 设置 ${key} 为 ${value}"; - }; - readonly "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": { - readonly def: "CouchDB Configuration: ${title} successfully updated"; - readonly es: "Configuración de CouchDB: ${title} actualizado correctamente"; - readonly fr: "Configuration CouchDB : ${title} mise à jour avec succès"; - readonly he: "תצורת CouchDB: ${title} עודכנה בהצלחה"; - readonly ja: "CouchDB設定: ${title} 正常に更新されました"; - readonly ko: "CouchDB 구성: ${title} 성공적으로 업데이트됨"; - readonly ru: "Конфигурация CouchDB: title успешно обновлена"; - readonly zh: "CouchDB 配置:${title} 成功更新"; - }; - readonly "obsidianLiveSyncSettingTab.logDatabaseConnected": { - readonly def: "Database connected"; - readonly es: "Base de datos conectada"; - readonly fr: "Base de données connectée"; - readonly he: "מסד הנתונים מחובר"; - readonly ja: "データベースに接続しました"; - readonly ko: "데이터베이스 연결됨"; - readonly ru: "База данных подключена"; - readonly zh: "数据库已连接"; - }; - readonly "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": { - readonly def: "You cannot enable encryption without a passphrase"; - readonly es: "No puedes habilitar el cifrado sin una frase de contraseña"; - readonly fr: "Impossible d'activer le chiffrement sans phrase secrète"; - readonly he: "לא ניתן להפעיל הצפנה ללא ביטוי סיסמה"; - readonly ja: "パスフレーズなしでは暗号化を有効にできません"; - readonly ko: "패스프레이즈 없이는 암호화를 활성화할 수 없습니다"; - readonly ru: "Вы не можете включить шифрование без парольной фразы"; - readonly zh: "没有密码无法启用加密"; - }; - readonly "obsidianLiveSyncSettingTab.logEncryptionNoSupport": { - readonly def: "Your device does not support encryption."; - readonly es: "Tu dispositivo no admite el cifrado."; - readonly fr: "Votre appareil ne prend pas en charge le chiffrement."; - readonly he: "המכשיר שלך אינו תומך בהצפנה."; - readonly ja: "お使いのデバイスは暗号化をサポートしていません。"; - readonly ko: "기기가 암호화를 지원하지 않습니다."; - readonly ru: "Ваше устройство не поддерживает шифрование."; - readonly zh: "您的设备不支持加密 "; - }; - readonly "obsidianLiveSyncSettingTab.logErrorOccurred": { - readonly def: "An error occurred!!"; - readonly es: "¡Ocurrió un error!"; - readonly fr: "Une erreur s'est produite !!"; - readonly he: "אירעה שגיאה!!"; - readonly ja: "エラーが発生しました!!"; - readonly ko: "오류가 발생했습니다!"; - readonly ru: "Произошла ошибка!!"; - readonly zh: "发生错误!!"; - }; - readonly "obsidianLiveSyncSettingTab.logEstimatedSize": { - readonly def: "Estimated size: ${size}"; - readonly es: "Tamaño estimado: ${size}"; - readonly fr: "Taille estimée : ${size}"; - readonly he: "גודל משוער: ${size}"; - readonly ja: "推定サイズ: ${size}"; - readonly ko: "예상 크기: ${size}"; - readonly ru: "Примерный размер: size"; - readonly zh: "估计大小:${size}"; - }; - readonly "obsidianLiveSyncSettingTab.logPassphraseInvalid": { - readonly def: "Passphrase is not valid, please fix it."; - readonly es: "La frase de contraseña no es válida, por favor corrígela."; - readonly fr: "La phrase secrète est invalide, veuillez la corriger."; - readonly he: "ביטוי הסיסמה אינו תקין, אנא תקן אותו."; - readonly ja: "パスフレーズが無効です、修正してください。"; - readonly ko: "패스프레이즈가 유효하지 않습니다. 수정해 주세요."; - readonly ru: "Парольная фраза недействительна, пожалуйста, исправьте."; - readonly zh: "密码无效,请修正"; - }; - readonly "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": { - readonly def: "ERROR: Passphrase is not compatible with the remote server! Please check it again!"; - readonly es: "ERROR: ¡La frase de contraseña no es compatible con el servidor remoto! ¡Por favor, revísala de nuevo!"; - readonly fr: "ERREUR : la phrase secrète n'est pas compatible avec le serveur distant ! Veuillez vérifier à nouveau !"; - readonly he: "שגיאה: ביטוי הסיסמה אינו תואם לשרת המרוחד! אנא בדוק שוב!"; - readonly ja: "エラー: パスフレーズがリモートサーバーと適合しません!再度確認してください!"; - readonly ko: "오류: 패스프레이즈가 원격 서버와 호환되지 않습니다! 다시 확인해 주세요!"; - readonly ru: "ОШИБКА: Парольная фраза несовместима с удалённым сервером!"; - readonly zh: "错误:密码与远程服务器不兼容!请再次检查!"; - }; - readonly "obsidianLiveSyncSettingTab.logRebuildNote": { - readonly def: "Syncing has been disabled, fetch and re-enabled if desired."; - readonly es: "La sincronización ha sido desactivada, obtén y vuelve a activar si lo deseas."; - readonly fr: "La synchronisation a été désactivée, récupérez et réactivez si souhaité."; - readonly he: "הסנכרון הושבת, משוך והפעל מחדש אם רצוי."; - readonly ja: "同期が無効になりました。必要に応じてフェッチして再有効化してください。"; - readonly ko: "동기화가 비활성화되었습니다. 원하는 경우 가져오기 후 다시 활성화하세요."; - readonly ru: "Синхронизация отключена, загрузите и включите снова при желании."; - readonly zh: "同步已禁用,如果需要,请获取并重新启用"; - }; - readonly "obsidianLiveSyncSettingTab.logSelectAnyPreset": { - readonly def: "Select any preset."; - readonly es: "Selecciona cualquier preestablecido."; - readonly fr: "Sélectionnez un préréglage."; - readonly he: "בחר קביעה מראש כלשהי."; - readonly ja: "プリセットを選択してください。"; - readonly ko: "프리셋을 선택하세요."; - readonly ru: "Выберите любой пресет."; - readonly zh: "请选择任一预设。"; - readonly "zh-tw": "請選擇任一預設項目。"; - }; - readonly "obsidianLiveSyncSettingTab.logServerConfigurationCheck": { - readonly def: "obsidianLiveSyncSettingTab.logServerConfigurationCheck"; - }; - readonly "obsidianLiveSyncSettingTab.msgAreYouSureProceed": { - readonly def: "Are you sure to proceed?"; - readonly es: "¿Estás seguro de proceder?"; - readonly fr: "Êtes-vous sûr de vouloir continuer ?"; - readonly he: "האם אתה בטוח שברצונך להמשיך?"; - readonly ja: "本当に続行しますか?"; - readonly ko: "정말로 진행하시겠습니까?"; - readonly ru: "Вы уверены, что хотите продолжить?"; - readonly zh: "您确定要继续吗?"; - }; - readonly "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": { - readonly def: "Changes need to be applied!"; - readonly es: "¡Los cambios deben aplicarse!"; - readonly fr: "Des modifications doivent être appliquées !"; - readonly he: "יש להחיל שינויים!"; - readonly ja: "変更を適用する必要があります!"; - readonly ko: "변경사항을 적용해야 합니다!"; - readonly ru: "Изменения нужно применить!"; - readonly zh: "需要应用更改!"; - }; - readonly "obsidianLiveSyncSettingTab.msgConfigCheck": { - readonly def: "--Config check--"; - readonly es: "--Verificación de configuración--"; - readonly fr: "--Vérification de la configuration--"; - readonly he: "--בדיקת תצורה--"; - readonly ja: "--設定確認--"; - readonly ko: "--구성 확인--"; - readonly ru: "--Проверка конфигурации--"; - readonly zh: "--配置检查--"; - }; - readonly "obsidianLiveSyncSettingTab.msgConfigCheckFailed": { - readonly def: "The configuration check has failed. Do you want to continue anyway?"; - readonly es: "La verificación de configuración ha fallado. ¿Quieres continuar de todos modos?"; - readonly fr: "La vérification de la configuration a échoué. Voulez-vous continuer malgré tout ?"; - readonly he: "בדיקת התצורה נכשלה. האם ברצונך להמשיך בכל זאת?"; - readonly ja: "設定確認に失敗しました。それでも続行しますか?"; - readonly ko: "구성 확인에 실패했습니다. 그래도 계속하시겠습니까?"; - readonly ru: "Проверка конфигурации не удалась. Вы всё равно хотите продолжить?"; - readonly zh: "配置检查失败。仍要继续吗?"; - readonly "zh-tw": "設定檢查失敗。仍要繼續嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgConnectionCheck": { - readonly def: "--Connection check--"; - readonly es: "--Verificación de conexión--"; - readonly fr: "--Vérification de la connexion--"; - readonly he: "--בדיקת חיבור--"; - readonly ja: "--接続確認--"; - readonly ko: "--연결 확인--"; - readonly ru: "--Проверка подключения--"; - readonly zh: "--连接检查--"; - }; - readonly "obsidianLiveSyncSettingTab.msgConnectionProxyNote": { - readonly def: "If you're having trouble with the Connection-check (even after checking config), please check your reverse proxy configuration."; - readonly es: "Si tienes problemas con la verificación de conexión (incluso después de verificar la configuración), por favor verifica la configuración de tu proxy reverso."; - readonly fr: "Si vous rencontrez des problèmes de vérification de connexion (même après avoir vérifié la configuration), veuillez vérifier votre configuration de reverse proxy."; - readonly he: "אם אתה נתקל בבעיות עם בדיקת החיבור (גם לאחר בדיקת התצורה), אנא בדוק את הגדרות ה-reverse proxy שלך."; - readonly ja: "設定確認後も接続確認に問題がある場合は、リバースプロキシの設定を確認してください。"; - readonly ko: "구성 확인 후에도 연결 확인에 문제가 있는 경우, 리버스 프록시 구성을 확인해 주세요."; - readonly ru: "Если у вас проблемы с проверкой подключения, проверьте конфигурацию обратного прокси."; - readonly zh: "如果您在连接检查时遇到问题(即使检查了配置后),请检查您的反向代理配置"; - }; - readonly "obsidianLiveSyncSettingTab.msgCurrentOrigin": { - readonly def: "Current origin: ${origin}"; - readonly es: "Origen actual: {origin}"; - readonly fr: "Origine actuelle : ${origin}"; - readonly he: "מקור נוכחי: ${origin}"; - readonly ja: "現在のオリジン: ${origin}"; - readonly ko: "현재 원점: {origin}"; - readonly ru: "Текущий origin: origin"; - readonly zh: "当前源: {origin}"; - }; - readonly "obsidianLiveSyncSettingTab.msgDiscardConfirmation": { - readonly def: "Do you really want to discard existing settings and databases?"; - readonly es: "¿Realmente deseas descartar las configuraciones y bases de datos existentes?"; - readonly fr: "Voulez-vous vraiment abandonner les paramètres et bases existants ?"; - readonly he: "האם אתה בטוח שברצונך לבטל הגדרות ומסדי נתונים קיימים?"; - readonly ja: "本当に既存の設定とデータベースを破棄しますか?"; - readonly ko: "정말로 기존 설정과 데이터베이스를 삭제하시겠습니까?"; - readonly ru: "Вы действительно хотите отменить существующие настройки и базы данных?"; - readonly zh: "您真的要丢弃现有的设置和数据库吗?"; - }; - readonly "obsidianLiveSyncSettingTab.msgDone": { - readonly def: "--Done--"; - readonly es: "--Hecho--"; - readonly fr: "--Terminé--"; - readonly he: "--הסתיים--"; - readonly ja: "--完了--"; - readonly ko: "--완료--"; - readonly ru: "--Готово--"; - readonly zh: "--完成--"; - }; - readonly "obsidianLiveSyncSettingTab.msgEnableCors": { - readonly def: "Set httpd.enable_cors"; - readonly es: "Configurar httpd.enable_cors"; - readonly fr: "Définir httpd.enable_cors"; - readonly he: "הגדר httpd.enable_cors"; - readonly ja: "httpd.enable_corsを設定"; - readonly ko: "httpd.enable_cors 설정"; - readonly ru: "Установить httpd.enable_cors"; - readonly zh: "设置 httpd.enable_cors"; - }; - readonly "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": { - readonly def: "Set chttpd.enable_cors"; - readonly fr: "Définir chttpd.enable_cors"; - readonly he: "הגדר chttpd.enable_cors"; - readonly ja: "chttpd.enable_corsを設定"; - readonly ru: "Установить chttpd.enable_cors"; - readonly zh: "设置 chttpd.enable_cors"; - }; - readonly "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": { - readonly def: "We recommend enabling End-To-End Encryption, and Path Obfuscation. Are you sure you want to continue without encryption?"; - readonly es: "Recomendamos habilitar el cifrado de extremo a extremo y la obfuscación de ruta. ¿Estás seguro de querer continuar sin cifrado?"; - readonly fr: "Nous recommandons d'activer le chiffrement de bout en bout et l'obfuscation des chemins. Êtes-vous sûr de vouloir continuer sans chiffrement ?"; - readonly he: "אנו ממליצים להפעיל הצפנה מקצה לקצה ואת ערפול הנתיב. האם אתה בטוח שברצונך להמשיך ללא הצפנה?"; - readonly ja: "エンドツーエンド暗号化とパス難読化を有効にすることをお勧めします。暗号化なしで続行してもよろしいですか?"; - readonly ko: "종단간 암호화와 경로 난독화를 활성화하는 것을 권장합니다. 정말로 암호화 없이 계속하시겠습니까?"; - readonly ru: "Мы рекомендуем включить сквозное шифрование. Вы уверены, что хотите продолжить без шифрования?"; - readonly zh: "建议启用端到端加密和路径混淆。你确定要在未加密的情况下继续吗?"; - readonly "zh-tw": "我們建議啟用端對端加密與路徑混淆。你確定要在未加密的情況下繼續嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": { - readonly def: "Do you want to fetch the config from the remote server?"; - readonly es: "¿Quieres obtener la configuración del servidor remoto?"; - readonly fr: "Voulez-vous récupérer la configuration depuis le serveur distant ?"; - readonly he: "האם ברצונך למשוך את התצורה מהשרת המרוחד?"; - readonly ja: "リモートサーバーから設定を取得しますか?"; - readonly ko: "원격 서버에서 구성을 가져오시겠습니까?"; - readonly ru: "Вы хотите загрузить конфигурацию с удалённого сервера?"; - readonly zh: "要从远端服务器获取配置吗?"; - readonly "zh-tw": "要從遠端伺服器抓取設定嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgGenerateSetupURI": { - readonly def: "All done! Do you want to generate a setup URI to set up other devices?"; - readonly es: "¡Todo listo! ¿Quieres generar un URI de configuración para configurar otros dispositivos?"; - readonly fr: "Tout est prêt ! Voulez-vous générer une URI de configuration pour configurer d'autres appareils ?"; - readonly he: "הכל מוכן! האם ברצונך לייצר Setup URI להגדרת מכשירים אחרים?"; - readonly ja: "完了!他のデバイスをセットアップするためのセットアップURIを生成しますか?"; - readonly ko: "모든 작업이 완료되었습니다! 다른 기기를 설정하기 위해 Setup URI를 생성하시겠습니까?"; - readonly ru: "Всё готово! Вы хотите сгенерировать Setup URI для настройки других устройств?"; - readonly zh: "全部完成!要生成设置 URI 以便配置其他设备吗?"; - readonly "zh-tw": "全部完成!要產生 Setup URI 以便設定其他裝置嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": { - readonly def: "If the server configuration is not persistent (e.g., running on docker), the values here may change. Once you are able to connect, please update the settings in the server's local.ini."; - readonly es: "Si la configuración del servidor no es persistente (por ejemplo, ejecutándose en docker), los valores aquí pueden cambiar. Una vez que puedas conectarte, por favor actualiza las configuraciones en el local.ini del servidor."; - readonly fr: "Si la configuration du serveur n'est pas persistante (par ex. fonctionnant sur Docker), les valeurs peuvent changer. Une fois la connexion établie, mettez à jour les paramètres dans le local.ini du serveur."; - readonly he: "אם תצורת השרת אינה קבועה (למשל, פועלת ב-docker), הערכים כאן עשויים להשתנות. לאחר שתצליח להתחבר, אנא עדכן את ההגדרות ב-local.ini של השרת."; - readonly ja: "サーバー設定が永続的でない場合(例: Dockerで実行中)、ここの値は変更される可能性があります。接続できるようになったら、サーバーのlocal.iniの設定を更新してください。"; - readonly ko: "서버 설정이 영구적으로 저장되지 않는 환경(예: Docker에서 실행 중)에서는 이곳의 값들이 변경될 수 있습니다. 연결이 가능해지면 서버의 local.ini 파일에서 설정을 수동으로 업데이트해 주세요."; - readonly ru: "Если конфигурация сервера непостоянна, значения здесь могут измениться."; - readonly zh: "如果服务器配置不是持久的(例如,在 docker 上运行),此处的值可能会更改。一旦能够连接,请更新服务器 local.ini 中的设置"; - }; - readonly "obsidianLiveSyncSettingTab.msgInvalidPassphrase": { - readonly def: "Your encryption passphrase might be invalid. Are you sure you want to continue?"; - readonly es: "Tu frase de contraseña de cifrado podría ser inválida. ¿Estás seguro de querer continuar?"; - readonly fr: "Votre phrase secrète de chiffrement peut être invalide. Êtes-vous sûr de vouloir continuer ?"; - readonly he: "ביטוי הסיסמה להצפנה שלך עשוי להיות לא תקין. האם אתה בטוח שברצונך להמשיך?"; - readonly ja: "暗号化パスフレーズが無効かもしれません。続行してもよろしいですか?"; - readonly ko: "암호화 패스프레이즈가 유효하지 않을 수 있습니다. 정말로 계속하시겠습니까?"; - readonly ru: "Ваша парольная фраза шифрования может быть недействительна."; - readonly zh: "你的加密密码短语可能无效。你确定要继续吗?"; - readonly "zh-tw": "你的加密密語可能無效。你確定要繼續嗎?"; - }; - readonly "obsidianLiveSyncSettingTab.msgNewVersionNote": { - readonly def: "Here due to an upgrade notification? Please review the version history. If you're satisfied, click the button. A new update will prompt this again."; - readonly es: "¿Aquí debido a una notificación de actualización? Por favor, revise el historial de versiones. Si está satisfecho, haga clic en el botón. Una nueva actualización volverá a mostrar esto."; - readonly fr: "Arrivé ici suite à une notification de mise à jour ? Consultez l'historique des versions. Si vous êtes satisfait, cliquez sur le bouton. Une nouvelle mise à jour reproposera ceci."; - readonly he: "הגעת כאן בשל הודעת שדרוג? אנא עיין בהיסטוריית הגרסאות. אם אתה מרוצה, לחץ על הכפתור. עדכון חדש יציג זאת שוב."; - readonly ja: "アップグレード通知でここに来ましたか?バージョン履歴を確認してください。納得したらボタンをクリックしてください。新しい更新があると再度確認されます。"; - readonly ko: "업그레이드 알림으로 여기에 오셨나요? 버전 기록을 검토해 주세요. 만족하신다면 버튼을 클릭하세요. 새로운 업데이트 시 다시 안내됩니다."; - readonly ru: "Вы пришли из-за уведомления об обновлении? Просмотрите историю версий."; - readonly zh: "因为升级通知来到这里?请查看版本历史。如果您满意,请点击按钮。新的更新将再次提示此信息"; - }; - readonly "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": { - readonly def: "Configured as non-HTTPS URI. Be warned that this may not work on mobile devices."; - readonly es: "Configurado como URI que no es HTTPS. Ten en cuenta que esto puede no funcionar en dispositivos móviles."; - readonly fr: "Configuré avec une URI non HTTPS. Attention, ceci peut ne pas fonctionner sur les appareils mobiles."; - readonly he: "מוגדר כ-URI שאינו HTTPS. שים לב שהדבר עשוי שלא לפעול על מכשירים ניידים."; - readonly ja: "非HTTPS URIとして設定されています。モバイルデバイスでは動作しない可能性があります。"; - readonly ko: "비 HTTPS URI로 구성되었습니다. 모바일 기기에서는 작동하지 않을 수 있으니 주의하세요."; - readonly ru: "Настроено как не-HTTPS URI. Это может не работать на мобильных устройствах."; - readonly zh: "配置为非 HTTPS URI。请注意,这可能在移动设备上无法工作"; - }; - readonly "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": { - readonly def: "Cannot connect to non-HTTPS URI. Please update your config and try again."; - readonly es: "No se puede conectar a URI que no sean HTTPS. Por favor, actualiza tu configuración y vuelve a intentarlo."; - readonly fr: "Connexion impossible à une URI non HTTPS. Mettez à jour votre configuration et réessayez."; - readonly he: "לא ניתן להתחבר ל-URI שאינו HTTPS. אנא עדכן את התצורה ונסה שוב."; - readonly ja: "非HTTPS URIに接続できません。設定を更新して再試行してください。"; - readonly ko: "비 HTTPS URI에 연결할 수 없습니다. 구성을 업데이트하고 다시 시도해 주세요."; - readonly ru: "Не удаётся подключиться к не-HTTPS URI. Обновите конфигурацию."; - readonly zh: "无法连接到非 HTTPS URI。请更新您的配置并重试"; - }; - readonly "obsidianLiveSyncSettingTab.msgNotice": { - readonly def: "---Notice---"; - readonly es: "---Aviso---"; - readonly fr: "---Avis---"; - readonly he: "---הודעה---"; - readonly ja: "---お知らせ---"; - readonly ko: "---공지사항---"; - readonly ru: "---Уведомление---"; - readonly zh: "---注意---"; - }; - readonly "obsidianLiveSyncSettingTab.msgObjectStorageWarning": { - readonly def: "WARNING: This feature is a Work In Progress, so please keep in mind the following:\n- Append only architecture. A rebuild is required to shrink the storage.\n- A bit fragile.\n- When first syncing, all history will be transferred from the remote. Be mindful of data caps and slow speeds.\n- Only differences are synced live.\n\nIf you run into any issues, or have ideas about this feature, please create a issue on GitHub.\nI appreciate you for your great dedication."; - readonly es: "ADVERTENCIA: Esta característica está en desarrollo, así que por favor ten en cuenta lo siguiente:\n- Arquitectura de solo anexado. Se requiere una reconstrucción para reducir el almacenamiento.\n- Un poco frágil.\n- Al sincronizar por primera vez, todo el historial será transferido desde el remoto. Ten en cuenta los límites de datos y las velocidades lentas.\n- Solo las diferencias se sincronizan en vivo.\n\nSi encuentras algún problema o tienes ideas sobre esta característica, por favor crea un issue en GitHub.\nAprecio mucho tu gran dedicación."; - readonly fr: "AVERTISSEMENT : cette fonctionnalité est en cours de développement, gardez à l'esprit ce qui suit :\n- Architecture en ajout seul. Une reconstruction est nécessaire pour réduire le stockage.\n- Un peu fragile.\n- Lors de la première synchronisation, tout l'historique sera transféré depuis le distant. Attention aux limites de données et aux débits lents.\n- Seules les différences sont synchronisées en direct.\n\nSi vous rencontrez des problèmes ou avez des idées sur cette fonctionnalité, merci d'ouvrir un ticket sur GitHub.\nMerci pour votre grand dévouement."; - readonly he: "אזהרה: תכונה זו בשלב פיתוח, לכן שים לב לנקודות הבאות:\n- ארכיטקטורת הוספה בלבד. נדרשת בנייה מחדש לצמצום האחסון.\n- קצת רגיש.\n- בסנכרון הראשון, כל ההיסטוריה תועבר מהשרת המרוחד. שים לב למגבלות נתונים ומהירות.\n- רק הפרשים מסונכרנים בזמן אמת.\n\nאם נתקלת בבעיות, או שיש לך רעיונות לגבי תכונה זו, אנא פתח Issue ב-GitHub.\nאנחנו מעריכים את ההקדשה הגדולה שלך."; - readonly ja: "警告: この機能は開発中です。以下の点にご注意ください:\n- 追記専用アーキテクチャ。ストレージを縮小するには再構築が必要です。\n- やや不安定です。\n- 初回同期時、すべての履歴がリモートから転送されます。データ制限と速度に注意してください。\n- ライブ同期は差分のみです。\n\n問題があれば、またはこの機能についてアイデアがあれば、GitHubにIssueを作成してください。\nご協力に感謝します。"; - readonly ko: "⚠️ 주의: 이 기능은 아직 개발 중(WIP)입니다. 다음 사항을 유의해 주세요:\n- 추가 전용 구조(append-only)로 동작합니다. 저장 용량을 줄이려면 데이터 재구성이 필요합니다.\n- 기능이 다소 불안정할 수 있습니다.\n- 최초 동기화 시, 전체 히스토리가 원격 서버에서 전송됩니다. 데이터 용량 제한 및 느린 속도에 유의해 주세요.\n- 실시간 동기화는 변경된 부분만 처리됩니다.\n\n문제가 발생했거나 개선 아이디어가 있으시면 GitHub에 이슈를 등록해 주세요.\n기여에 깊이 감사드립니다."; - readonly ru: "ПРЕДУПРЕЖДЕНИЕ: Эта функция в разработке."; - readonly zh: "警告:此功能仍在开发中,请注意以下几点:\n- 仅追加架构。需要重建才能缩小存储空间。\n- 有点脆弱。\n- 首次同步时,所有历史记录将从远程传输。注意数据上限和慢速。\n- 只有差异会实时同步。\n\n如果您遇到任何问题,或对此功能有任何想法,请在 GitHub 上创建 issue。\n感谢您的巨大贡献"; - }; - readonly "obsidianLiveSyncSettingTab.msgOriginCheck": { - readonly def: "Origin check: ${org}"; - readonly es: "Verificación de origen: {org}"; - readonly fr: "Vérification d'origine : ${org}"; - readonly he: "בדיקת מקור: ${org}"; - readonly ja: "オリジン確認: ${org}"; - readonly ko: "원점 확인: {org}"; - readonly ru: "Проверка origin: org"; - readonly zh: "源检查: {org}"; - }; - readonly "obsidianLiveSyncSettingTab.msgRebuildRequired": { - readonly def: "Rebuilding Databases are required to apply the changes.. Please select the method to apply the changes.\n\n
\nLegends\n\n| Symbol | Meaning |\n|: ------ :| ------- |\n| ⇔ | Up to Date |\n| ⇄ | Synchronise to balance |\n| ⇐,⇒ | Transfer to overwrite |\n| ⇠,⇢ | Transfer to overwrite from other side |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nAt a glance: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nReconstruct both the local and remote databases using existing files from this device.\nThis causes a lockout other devices, and they need to perform fetching.\n## ${OPTION_FETCH}\nAt a glance: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nInitialise the local database and reconstruct it using data fetched from the remote database.\nThis case includes the case which you have rebuilt the remote database.\n## ${OPTION_ONLY_SETTING}\nStore only the settings. **Caution: This may lead to data corruption**; database reconstruction is generally necessary."; - readonly es: "Es necesario reconstruir las bases de datos para aplicar los cambios. Por favor selecciona el método para aplicar los cambios.\n\n
\nLegendas\n\n| Símbolo | Significado |\n|: ------ :| ------- |\n| ⇔ | Actualizado |\n| ⇄ | Sincronizar para equilibrar |\n| ⇐,⇒ | Transferir para sobrescribir |\n| ⇠,⇢ | Transferir para sobrescribir desde otro lado |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nA simple vista: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nReconstruir tanto la base de datos local como la remota utilizando los archivos existentes de este dispositivo.\nEsto bloquea a otros dispositivos, y necesitan realizar la obtención.\n## ${OPTION_FETCH}\nA simple vista: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nInicializa la base de datos local y la reconstruye utilizando los datos obtenidos de la base de datos remota.\nEste caso incluye el caso en el que has reconstruido la base de datos remota.\n## ${OPTION_ONLY_SETTING}\nAlmacena solo la configuración. **Precaución: esto puede provocar corrupción de datos**; generalmente es necesario reconstruir la base de datos."; - readonly fr: "La reconstruction des bases de données est nécessaire pour appliquer les changements. Veuillez sélectionner la méthode d'application.\n\n
\nLégende\n\n| Symbole | Signification |\n|: ------ :| ------- |\n| ⇔ | À jour |\n| ⇄ | Synchroniser pour équilibrer |\n| ⇐,⇒ | Transférer pour écraser |\n| ⇠,⇢ | Transférer pour écraser depuis l'autre côté |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nEn bref : 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nReconstruit les bases locale et distante à partir des fichiers existants de cet appareil.\nCeci provoque un verrouillage des autres appareils, qui devront effectuer une récupération.\n## ${OPTION_FETCH}\nEn bref : 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nInitialise la base locale et la reconstruit à partir des données récupérées depuis la base distante.\nCe cas inclut également celui où vous avez reconstruit la base distante.\n## ${OPTION_ONLY_SETTING}\nNe stocker que les paramètres. **Attention : cela peut entraîner une corruption des données** ; une reconstruction de la base est généralement nécessaire."; - readonly he: "נדרשת בנייה מחדש של מסדי הנתונים כדי להחיל את השינויים. אנא בחר את השיטה.\n\n
\nמקרא\n\n| סמל | משמעות |\n|: ------ :| ------- |\n| ⇔ | מעודכן |\n| ⇄ | סנכרן לאיזון |\n| ⇐,⇒ | העבר לדריסה |\n| ⇠,⇢ | העבר לדריסה מהצד השני |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\nבמבט: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nבנה מחדש גם את מסד הנתונים המקומי וגם המרוחד תוך שימוש בקבצים קיימים ממכשיר זה.\nפעולה זו תנעל מכשירים אחרים שיצטרכו לבצע משיכה.\n## ${OPTION_FETCH}\nבמבט: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nאתחל את מסד הנתונים המקומי ובנה אותו מחדש תוך שימוש בנתונים שנמשכו ממסד הנתונים המרוחד.\nכולל את המקרה שבו בנית מחדש את מסד הנתונים המרוחד.\n## ${OPTION_ONLY_SETTING}\nשמור רק את ההגדרות. **זהירות: עלול לגרום לפגיעה בנתונים**; בנייה מחדש של מסד הנתונים נדרשת בדרך כלל."; - readonly ja: "変更を適用するにはデータベースの再構築が必要です。変更を適用する方法を選択してください。\n\n
\n凡例\n\n| 記号 | 意味 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同期してバランスを取る |\n| ⇐,⇒ | 上書きするため転送 |\n| ⇠,⇢ | 反対側から上書きするため転送 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概要: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\nこのデバイスの既存ファイルを使用してローカルとリモートの両方のデータベースを再構築します。\n他のデバイスはロックアウトされ、フェッチが必要です。\n## ${OPTION_FETCH}\n概要: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\nローカルデータベースを初期化し、リモートデータベースから取得したデータを使用して再構築します。\nリモートデータベースを再構築した場合も含まれます。\n## ${OPTION_ONLY_SETTING}\n設定のみを保存します。**注意: データ破損につながる可能性があります**。通常、データベースの再構築が必要です。"; - readonly ko: "변경사항을 적용하려면 데이터베이스를 재구축해야 합니다. 아래 중 한 가지 방법을 선택해 주세요.\n\n
\n범례\n\n| 기호 | 의미 |\n|: ------ :| ------- |\n| ⇔ | 최신 상태 |\n| ⇄ | 동기화 균형 유지 |\n| ⇐,⇒ | 덮어쓰기 방식의 전송 |\n| ⇠,⇢ | 상대편에서 가져와 덮어쓰기 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n개요: 📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n이 기기의 기존 파일을 기반으로 로컬과 원격 데이터베이스를 모두 재구축합니다.\n이 과정에서 다른 기기는 일시적으로 접근이 제한되며, 가져오기 작업을 별도로 수행해야 합니다.\n\n## ${OPTION_FETCH}\n개요: 📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n로컬 데이터베이스를 초기화한 후, 원격 데이터베이스에서 데이터를 가져와 재구축합니다.\n이는 원격 측에서 데이터베이스를 먼저 재구축한 경우에도 해당됩니다.\n\n## ${OPTION_ONLY_SETTING}\n설정만 저장합니다. **⚠️ 주의: 이 방법은 데이터 손상을 일으킬 수 있습니다.** 일반적으로는 전체 데이터베이스 재구축이 필요합니다."; - readonly ru: "Требуется перестроение баз данных для применения изменений."; - readonly zh: "需要重建数据库以应用更改。请选择应用更改的方法。\n\n
\n图例\n\n| 符号 | 含义 |\n|: ------ :| ------- |\n| ⇔ | 最新 |\n| ⇄ | 同步以平衡 |\n| ⇐,⇒ | 传输以覆盖 |\n| ⇠,⇢ | 从另一侧传输以覆盖 |\n\n
\n\n## ${OPTION_REBUILD_BOTH}\n概览:📄 ⇒¹ 💻 ⇒² 🛰️ ⇢ⁿ 💻 ⇄ⁿ⁺¹ 📄\n使用此设备的现有文件重建本地和远程数据库。\n这将导致其他设备被锁定,并且它们需要执行获取操作。\n## ${OPTION_FETCH}\n概览:📄 ⇄² 💻 ⇐¹ 🛰️ ⇔ 💻 ⇔ 📄\n初始化本地数据库并使用从远程数据库获取的数据重建它。\n这种情况包括您已经重建了远程数据库的情况。\n## ${OPTION_ONLY_SETTING}\n仅存储设置。**注意:这可能导致数据损坏**;通常需要重建数据库"; - }; - readonly "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": { - readonly def: "Please select and apply any preset item to complete the wizard."; - readonly es: "Por favor, selecciona y aplica cualquier elemento preestablecido para completar el asistente."; - readonly fr: "Veuillez sélectionner et appliquer un préréglage pour terminer l'assistant."; - readonly he: "אנא בחר והחל פריט קבוע מראש כלשהו להשלמת האשף."; - readonly ja: "ウィザードを完了するには、プリセット項目を選択して適用してください。"; - readonly ko: "마법사를 완료하려면 프리셋 항목을 선택하고 적용해 주세요."; - readonly ru: "Выберите и примените любой пресет для завершения мастера."; - readonly zh: "请选择并应用任一预设项以完成向导。"; - readonly "zh-tw": "請選擇並套用任一預設項目以完成精靈。"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetCorsCredentials": { - readonly def: "Set cors.credentials"; - readonly es: "Configurar cors.credentials"; - readonly fr: "Définir cors.credentials"; - readonly he: "הגדר cors.credentials"; - readonly ja: "cors.credentialsを設定"; - readonly ko: "cors.credentials 설정"; - readonly ru: "Установить cors.credentials"; - readonly zh: "设置 cors.credentials"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetCorsOrigins": { - readonly def: "Set cors.origins"; - readonly es: "Configurar cors.origins"; - readonly fr: "Définir cors.origins"; - readonly he: "הגדר cors.origins"; - readonly ja: "cors.originsを設定"; - readonly ko: "cors.origins 설정"; - readonly ru: "Установить cors.origins"; - readonly zh: "设置 cors.origins"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetMaxDocSize": { - readonly def: "Set couchdb.max_document_size"; - readonly es: "Configurar couchdb.max_document_size"; - readonly fr: "Définir couchdb.max_document_size"; - readonly he: "הגדר couchdb.max_document_size"; - readonly ja: "couchdb.max_document_sizeを設定"; - readonly ko: "couchdb.max_document_size 설정"; - readonly ru: "Установить couchdb.max_document_size"; - readonly zh: "设置 couchdb.max_document_size"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": { - readonly def: "Set chttpd.max_http_request_size"; - readonly es: "Configurar chttpd.max_http_request_size"; - readonly fr: "Définir chttpd.max_http_request_size"; - readonly he: "הגדר chttpd.max_http_request_size"; - readonly ja: "chttpd.max_http_request_sizeを設定"; - readonly ko: "chttpd.max_http_request_size 설정"; - readonly ru: "Установить chttpd.max_http_request_size"; - readonly zh: "设置 chttpd.max_http_request_size"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetRequireValidUser": { - readonly def: "Set chttpd.require_valid_user = true"; - readonly es: "Configurar chttpd.require_valid_user = true"; - readonly fr: "Définir chttpd.require_valid_user = true"; - readonly he: "הגדר chttpd.require_valid_user = true"; - readonly ja: "chttpd.require_valid_user = trueを設定"; - readonly ko: "chttpd.require_valid_user = true로 설정"; - readonly ru: "Установить chttpd.require_valid_user = true"; - readonly zh: "设置 chttpd.require_valid_user = true"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": { - readonly def: "Set chttpd_auth.require_valid_user = true"; - readonly es: "Configurar chttpd_auth.require_valid_user = true"; - readonly fr: "Définir chttpd_auth.require_valid_user = true"; - readonly he: "הגדר chttpd_auth.require_valid_user = true"; - readonly ja: "chttpd_auth.require_valid_user = trueを設定"; - readonly ko: "chttpd_auth.require_valid_user = true로 설정"; - readonly ru: "Установить chttpd_auth.require_valid_user = true"; - readonly zh: "设置 chttpd_auth.require_valid_user = true"; - }; - readonly "obsidianLiveSyncSettingTab.msgSettingModified": { - readonly def: "The setting \"${setting}\" was modified from another device. Click {HERE} to reload settings. Click elsewhere to ignore changes."; - readonly es: "La configuración \"${setting}\" fue modificada desde otro dispositivo. Haz clic {HERE} para recargar la configuración. Haz clic en otro lugar para ignorar los cambios."; - readonly fr: "Le paramètre « ${setting} » a été modifié depuis un autre appareil. Cliquez sur {HERE} pour recharger les paramètres. Cliquez ailleurs pour ignorer les modifications."; - readonly he: "ההגדרה \"${setting}\" שונתה ממכשיר אחר. לחץ על {HERE} לטעינה מחדש של ההגדרות. לחץ במקום אחר להתעלמות מהשינויים."; - readonly ja: "設定\"${setting}\"が別のデバイスから変更されました。{HERE}をクリックして設定を再読み込みしてください。変更を無視するには他の場所をクリックしてください。"; - readonly ko: "\"${setting}\" 설정이 다른 기기에서 수정되었습니다. 설정을 다시 로드하려면 {HERE}를 클릭하세요. 변경사항을 무시하려면 다른 곳을 클릭하세요."; - readonly ru: "Настройка setting была изменена с другого устройства."; - readonly zh: "设置 \"${setting}\" 已从另一台设备修改。点击 {HERE} 重新加载设置。点击其他地方忽略更改"; - }; - readonly "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": { - readonly def: "These settings are unable to be changed during synchronization. Please disable all syncing in the \"Sync Settings\" to unlock."; - readonly es: "Estas configuraciones no se pueden cambiar durante la sincronización. Por favor, deshabilita toda la sincronización en las \"Configuraciones de Sincronización\" para desbloquear."; - readonly fr: "Ces paramètres ne peuvent pas être modifiés durant la synchronisation. Désactivez toute synchronisation dans « Paramètres de synchronisation » pour déverrouiller."; - readonly he: "הגדרות אלה אינן ניתנות לשינוי במהלך סנכרון. אנא נטרל את כל הסנכרון ב\"הגדרות סנכרון\" כדי לבטל נעילה."; - readonly ja: "これらの設定は同期中に変更できません。ロックを解除するには、\"同期設定\"ですべての同期を無効にしてください。"; - readonly ko: "동기화 중에는 이 설정들을 변경할 수 없습니다. 잠금을 해제하려면 \"동기화 설정\"에서 모든 동기화를 비활성화해 주세요."; - readonly ru: "Эти настройки нельзя изменить во время синхронизации."; - readonly zh: "这些设置在同步期间无法更改。请在“同步设置”中禁用所有同步以解锁"; - }; - readonly "obsidianLiveSyncSettingTab.msgSetWwwAuth": { - readonly def: "Set httpd.WWW-Authenticate"; - readonly es: "Configurar httpd.WWW-Authenticate"; - readonly fr: "Définir httpd.WWW-Authenticate"; - readonly he: "הגדר httpd.WWW-Authenticate"; - readonly ja: "httpd.WWW-Authenticateを設定"; - readonly ko: "httpd.WWW-Authenticate 설정"; - readonly ru: "Установить httpd.WWW-Authenticate"; - readonly zh: "设置 httpd.WWW-Authenticate"; - }; - readonly "obsidianLiveSyncSettingTab.nameApplySettings": { - readonly def: "Apply Settings"; - readonly es: "Aplicar configuraciones"; - readonly fr: "Appliquer les paramètres"; - readonly he: "החל הגדרות"; - readonly ja: "設定を適用"; - readonly ko: "설정 적용"; - readonly ru: "Применить настройки"; - readonly zh: "应用设置"; - }; - readonly "obsidianLiveSyncSettingTab.nameConnectSetupURI": { - readonly def: "Connect with Setup URI"; - readonly es: "Conectar con URI de configuración"; - readonly fr: "Se connecter avec une URI de configuration"; - readonly he: "התחבר עם Setup URI"; - readonly ja: "セットアップURIで接続"; - readonly ko: "Setup URI로 연결"; - readonly ru: "Подключиться через Setup URI"; - readonly zh: "使用设置 URI 连接"; - }; - readonly "obsidianLiveSyncSettingTab.nameCopySetupURI": { - readonly def: "Copy the current settings to a Setup URI"; - readonly es: "Copiar la configuración actual a una URI de configuración"; - readonly fr: "Copier les paramètres actuels vers une URI de configuration"; - readonly he: "העתק הגדרות נוכחיות ל-Setup URI"; - readonly ja: "現在の設定をセットアップURIにコピー"; - readonly ko: "현재 설정을 Setup URI로 복사"; - readonly ru: "Копировать текущие настройки в Setup URI"; - readonly zh: "将当前设置复制为设置 URI"; - }; - readonly "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": { - readonly def: "Disable Hidden files sync"; - readonly es: "Desactivar sincronización de archivos ocultos"; - readonly fr: "Désactiver la synchronisation des fichiers cachés"; - readonly he: "נטרל סנכרון קבצים נסתרים"; - readonly ja: "隠しファイル同期を無効化"; - readonly ko: "숨김 파일 동기화 비활성화"; - readonly ru: "Отключить синхронизацию скрытых файлов"; - readonly zh: "禁用隐藏文件同步"; - readonly "zh-tw": "停用隱藏檔案同步"; - }; - readonly "obsidianLiveSyncSettingTab.nameDiscardSettings": { - readonly def: "Discard existing settings and databases"; - readonly es: "Descartar configuraciones y bases de datos existentes"; - readonly fr: "Abandonner les paramètres et bases existants"; - readonly he: "בטל הגדרות ומסדי נתונים קיימים"; - readonly ja: "既存の設定とデータベースを破棄"; - readonly ko: "기존 설정 및 데이터베이스 삭제"; - readonly ru: "Отменить существующие настройки и базы данных"; - readonly zh: "丢弃现有设置和数据库"; - }; - readonly "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": { - readonly def: "Enable Hidden files sync"; - readonly es: "Activar sincronización de archivos ocultos"; - readonly fr: "Activer la synchronisation des fichiers cachés"; - readonly he: "הפעל סנכרון קבצים נסתרים"; - readonly ja: "隠しファイル同期を有効化"; - readonly ko: "숨김 파일 동기화 활성화"; - readonly ru: "Включить синхронизацию скрытых файлов"; - readonly zh: "启用隐藏文件同步"; - readonly "zh-tw": "啟用隱藏檔案同步"; - }; - readonly "obsidianLiveSyncSettingTab.nameEnableLiveSync": { - readonly def: "Enable LiveSync"; - readonly es: "Activar LiveSync"; - readonly fr: "Activer LiveSync"; - readonly he: "הפעל LiveSync"; - readonly ja: "LiveSyncを有効化"; - readonly ko: "LiveSync 활성화"; - readonly ru: "Включить LiveSync"; - readonly zh: "启用 LiveSync"; - }; - readonly "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": { - readonly def: "Hidden file synchronization"; - readonly es: "Sincronización de archivos ocultos"; - readonly fr: "Synchronisation des fichiers cachés"; - readonly he: "סנכרון קבצים נסתרים"; - readonly ja: "隠しファイル同期"; - readonly ko: "숨김 파일 동기화"; - readonly ru: "Синхронизация скрытых файлов"; - readonly zh: "隐藏文件同步"; - readonly "zh-tw": "隱藏檔案同步"; - }; - readonly "obsidianLiveSyncSettingTab.nameManualSetup": { - readonly def: "Manual Setup"; - readonly es: "Configuración manual"; - readonly fr: "Configuration manuelle"; - readonly he: "הגדרה ידנית"; - readonly ja: "手動セットアップ"; - readonly ko: "수동 설정"; - readonly ru: "Ручная настройка"; - readonly zh: "手动设置"; - }; - readonly "obsidianLiveSyncSettingTab.nameTestConnection": { - readonly def: "Test Connection"; - readonly es: "Probar conexión"; - readonly fr: "Tester la connexion"; - readonly he: "בדוק חיבור"; - readonly ja: "接続テスト"; - readonly ko: "연결 테스트"; - readonly ru: "Тест подключения"; - readonly zh: "测试连接"; - }; - readonly "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": { - readonly def: "Test Database Connection"; - readonly es: "Probar Conexión de Base de Datos"; - readonly fr: "Tester la connexion à la base de données"; - readonly he: "בדוק חיבור למסד נתונים"; - readonly ja: "データベース接続テスト"; - readonly ko: "데이터베이스 연결 테스트"; - readonly ru: "Тест подключения к базе данных"; - readonly zh: "测试数据库连接"; - }; - readonly "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": { - readonly def: "Validate Database Configuration"; - readonly es: "Validar Configuración de la Base de Datos"; - readonly fr: "Valider la configuration de la base de données"; - readonly he: "אמת תצורת מסד נתונים"; - readonly ja: "データベース設定を検証"; - readonly ko: "데이터베이스 구성 검증"; - readonly ru: "Проверить конфигурацию базы данных"; - readonly zh: "验证数据库配置"; - }; - readonly "obsidianLiveSyncSettingTab.okAdminPrivileges": { - readonly def: "✔ You have administrator privileges."; - readonly es: "✔ Tienes privilegios de administrador."; - readonly fr: "✔ Vous disposez des privilèges administrateur."; - readonly he: "✔ יש לך הרשאות מנהל."; - readonly ja: "✔ 管理者権限があります。"; - readonly ko: "✔ 관리자 권한이 있습니다."; - readonly ru: "✔ У вас есть права администратора."; - readonly zh: "✔ 您拥有管理员权限"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsCredentials": { - readonly def: "✔ cors.credentials is ok."; - readonly es: "✔ cors.credentials está correcto."; - readonly fr: "✔ cors.credentials est correct."; - readonly he: "✔ cors.credentials תקין."; - readonly ja: "✔ cors.credentialsは正常です。"; - readonly ko: "✔ cors.credentials가 정상입니다."; - readonly ru: "✔ cors.credentials в порядке."; - readonly zh: "✔ cors.credentials 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": { - readonly def: "CORS credentials OK"; - readonly es: "CORS credenciales OK"; - readonly fr: "Identifiants CORS OK"; - readonly he: "CORS credentials תקין"; - readonly ja: "CORS認証情報OK"; - readonly ko: "CORS 자격 증명 정상"; - readonly ru: "CORS учётные данные в порядке"; - readonly zh: "CORS 凭据正常"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsOriginMatched": { - readonly def: "✔ CORS origin OK"; - readonly es: "✔ Origen de CORS correcto"; - readonly fr: "✔ Origine CORS OK"; - readonly he: "✔ CORS origin תקין"; - readonly ja: "✔ CORSオリジンOK"; - readonly ko: "✔ CORS 원점 정상"; - readonly ru: "✔ CORS origin в порядке"; - readonly zh: "✔ CORS 源正常"; - }; - readonly "obsidianLiveSyncSettingTab.okCorsOrigins": { - readonly def: "✔ cors.origins is ok."; - readonly es: "✔ cors.origins está correcto."; - readonly fr: "✔ cors.origins est correct."; - readonly he: "✔ cors.origins תקין."; - readonly ja: "✔ cors.originsは正常です。"; - readonly ko: "✔ cors.origins가 정상입니다."; - readonly ru: "✔ cors.origins в порядке."; - readonly zh: "✔ cors.origins 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okEnableCors": { - readonly def: "✔ httpd.enable_cors is ok."; - readonly es: "✔ httpd.enable_cors está correcto."; - readonly fr: "✔ httpd.enable_cors est correct."; - readonly he: "✔ httpd.enable_cors תקין."; - readonly ja: "✔ httpd.enable_corsは正常です。"; - readonly ko: "✔ httpd.enable_cors가 정상입니다."; - readonly ru: "✔ httpd.enable_cors в порядке."; - readonly zh: "✔ httpd.enable_cors 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okEnableCorsChttpd": { - readonly def: "✔ chttpd.enable_cors is ok."; - readonly fr: "✔ chttpd.enable_cors est correct."; - readonly he: "✔ chttpd.enable_cors תקין."; - readonly ja: "✔ chttpd.enable_corsは正常です。"; - readonly ru: "✔ chttpd.enable_cors в порядке."; - readonly zh: "✔ chttpd.enable_cors is ok."; - }; - readonly "obsidianLiveSyncSettingTab.okMaxDocumentSize": { - readonly def: "✔ couchdb.max_document_size is ok."; - readonly es: "✔ couchdb.max_document_size está correcto."; - readonly fr: "✔ couchdb.max_document_size est correct."; - readonly he: "✔ couchdb.max_document_size תקין."; - readonly ja: "✔ couchdb.max_document_sizeは正常です。"; - readonly ko: "✔ couchdb.max_document_size가 정상입니다."; - readonly ru: "✔ couchdb.max_document_size в порядке."; - readonly zh: "✔ couchdb.max_document_size 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okMaxRequestSize": { - readonly def: "✔ chttpd.max_http_request_size is ok."; - readonly es: "✔ chttpd.max_http_request_size está correcto."; - readonly fr: "✔ chttpd.max_http_request_size est correct."; - readonly he: "✔ chttpd.max_http_request_size תקין."; - readonly ja: "✔ chttpd.max_http_request_sizeは正常です。"; - readonly ko: "✔ chttpd.max_http_request_size가 정상입니다."; - readonly ru: "✔ chttpd.max_http_request_size в порядке."; - readonly zh: "✔ chttpd.max_http_request_size 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okRequireValidUser": { - readonly def: "✔ chttpd.require_valid_user is ok."; - readonly es: "✔ chttpd.require_valid_user está correcto."; - readonly fr: "✔ chttpd.require_valid_user est correct."; - readonly he: "✔ chttpd.require_valid_user תקין."; - readonly ja: "✔ chttpd.require_valid_userは正常です。"; - readonly ko: "✔ chttpd.require_valid_user가 정상입니다."; - readonly ru: "✔ chttpd.require_valid_user в порядке."; - readonly zh: "✔ chttpd.require_valid_user 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okRequireValidUserAuth": { - readonly def: "✔ chttpd_auth.require_valid_user is ok."; - readonly es: "✔ chttpd_auth.require_valid_user está correcto."; - readonly fr: "✔ chttpd_auth.require_valid_user est correct."; - readonly he: "✔ chttpd_auth.require_valid_user תקין."; - readonly ja: "✔ chttpd_auth.require_valid_userは正常です。"; - readonly ko: "✔ chttpd_auth.require_valid_user가 정상입니다."; - readonly ru: "✔ chttpd_auth.require_valid_user в порядке."; - readonly zh: "✔ chttpd_auth.require_valid_user 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.okWwwAuth": { - readonly def: "✔ httpd.WWW-Authenticate is ok."; - readonly es: "✔ httpd.WWW-Authenticate está correcto."; - readonly fr: "✔ httpd.WWW-Authenticate est correct."; - readonly he: "✔ httpd.WWW-Authenticate תקין."; - readonly ja: "✔ httpd.WWW-Authenticateは正常です。"; - readonly ko: "✔ httpd.WWW-Authenticate가 정상입니다."; - readonly ru: "✔ httpd.WWW-Authenticate в порядке."; - readonly zh: "✔ httpd.WWW-Authenticate 设置正确"; - }; - readonly "obsidianLiveSyncSettingTab.optionApply": { - readonly def: "Apply"; - readonly es: "Aplicar"; - readonly fr: "Appliquer"; - readonly he: "החל"; - readonly ja: "適用"; - readonly ko: "적용"; - readonly ru: "Применить"; - readonly zh: "应用"; - }; - readonly "obsidianLiveSyncSettingTab.optionCancel": { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly fr: "Annuler"; - readonly he: "ביטול"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - }; - readonly "obsidianLiveSyncSettingTab.optionCouchDB": { - readonly def: "CouchDB"; - readonly es: "CouchDB"; - readonly fr: "CouchDB"; - readonly he: "CouchDB"; - readonly ja: "CouchDB"; - readonly ko: "CouchDB"; - readonly ru: "CouchDB"; - readonly zh: "CouchDB"; - }; - readonly "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": { - readonly def: "Disable all automatic"; - readonly es: "Desactivar lo automático"; - readonly fr: "Désactiver toute automatisation"; - readonly he: "נטרל את כל האוטומטי"; - readonly ja: "すべての自動を無効化"; - readonly ko: "모든 자동 비활성화"; - readonly ru: "Отключить всё автоматическое"; - readonly zh: "禁用所有自动同步"; - readonly "zh-tw": "停用所有自動同步"; - }; - readonly "obsidianLiveSyncSettingTab.optionFetchFromRemote": { - readonly def: "Fetch from Remote"; - readonly es: "Obtener del remoto"; - readonly fr: "Récupérer depuis le distant"; - readonly he: "משוך מהשרת המרוחד"; - readonly ja: "リモートからフェッチ"; - readonly ko: "원격에서 가져오기"; - readonly ru: "Загрузить с удалённого"; - readonly zh: "从远程获取"; - }; - readonly "obsidianLiveSyncSettingTab.optionHere": { - readonly def: "HERE"; - readonly es: "AQUÍ"; - readonly fr: "ICI"; - readonly he: "כאן"; - readonly ja: "ここ"; - readonly ko: "여기"; - readonly ru: "ЗДЕСЬ"; - readonly zh: "这里"; - }; - readonly "obsidianLiveSyncSettingTab.optionLiveSync": { - readonly def: "LiveSync"; - readonly es: "Sincronización LiveSync"; - readonly fr: "LiveSync"; - readonly he: "LiveSync"; - readonly ja: "LiveSync 同期"; - readonly ko: "LiveSync 동기화"; - readonly ru: "Синхронизация LiveSync"; - readonly zh: "LiveSync 同步"; - readonly "zh-tw": "LiveSync 同步"; - }; - readonly "obsidianLiveSyncSettingTab.optionMinioS3R2": { - readonly def: "Minio,S3,R2"; - readonly es: "Minio,S3,R2"; - readonly fr: "Minio, S3, R2"; - readonly he: "Minio,S3,R2"; - readonly ja: "Minio,S3,R2"; - readonly ko: "Minio,S3,R2"; - readonly ru: "Minio,S3,R2"; - readonly zh: "Minio, S3, R2"; - }; - readonly "obsidianLiveSyncSettingTab.optionOkReadEverything": { - readonly def: "OK, I have read everything."; - readonly es: "OK, he leído todo."; - readonly fr: "OK, j'ai tout lu."; - readonly he: "בסדר, קראתי הכל."; - readonly ja: "OK、すべて読みました。"; - readonly ko: "네, 모든 것을 읽었습니다."; - readonly ru: "ОК, я всё прочитал."; - readonly zh: "好的,我已经阅读了所有内容 "; - }; - readonly "obsidianLiveSyncSettingTab.optionOnEvents": { - readonly def: "On events"; - readonly es: "En eventos"; - readonly fr: "Sur événements"; - readonly he: "על אירועים"; - readonly ja: "イベント時"; - readonly ko: "이벤트 시"; - readonly ru: "По событиям"; - readonly zh: "事件触发时"; - readonly "zh-tw": "事件觸發時"; - }; - readonly "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": { - readonly def: "Periodic and on events"; - readonly es: "Periódico y en eventos"; - readonly fr: "Périodique et sur événements"; - readonly he: "תקופתי ועל אירועים"; - readonly ja: "定期およびイベント時"; - readonly ko: "주기적 및 이벤트 시"; - readonly ru: "Периодически и по событиям"; - readonly zh: "定期与事件触发"; - readonly "zh-tw": "定期與事件觸發"; - }; - readonly "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": { - readonly def: "Periodic w/ batch"; - readonly es: "Periódico con lote"; - readonly fr: "Périodique avec lot"; - readonly he: "תקופתי עם אצווה"; - readonly ja: "バッチ付き定期"; - readonly ko: "주기적 w/ 일괄"; - readonly ru: "Периодически с пакетами"; - readonly zh: "定期(批处理)"; - readonly "zh-tw": "定期(批次)"; - }; - readonly "obsidianLiveSyncSettingTab.optionRebuildBoth": { - readonly def: "Rebuild Both from This Device"; - readonly es: "Reconstructuir ambos desde este dispositivo"; - readonly fr: "Tout reconstruire depuis cet appareil"; - readonly he: "בנה שניהם מחדש ממכשיר זה"; - readonly ja: "このデバイスから両方を再構築"; - readonly ko: "이 기기에서 둘 다 재구축"; - readonly ru: "Перестроить оба с этого устройства"; - readonly zh: "从此设备重建两者"; - }; - readonly "obsidianLiveSyncSettingTab.optionSaveOnlySettings": { - readonly def: "(Danger) Save Only Settings"; - readonly es: "(Peligro) Guardar solo configuración"; - readonly fr: "(Danger) N'enregistrer que les paramètres"; - readonly he: "(סכנה) שמור הגדרות בלבד"; - readonly ja: "(危険) 設定のみ保存"; - readonly ko: "(위험) 설정만 저장"; - readonly ru: "(Опасно) Сохранить только настройки"; - readonly zh: "(危险)仅保存设置"; - }; - readonly "obsidianLiveSyncSettingTab.panelChangeLog": { - readonly def: "Change Log"; - readonly es: "Registro de cambios"; - readonly fr: "Journal des modifications"; - readonly he: "יומן שינויים"; - readonly ja: "変更履歴"; - readonly ko: "변경 로그"; - readonly ru: "История изменений"; - readonly zh: "更新日志"; - }; - readonly "obsidianLiveSyncSettingTab.panelGeneralSettings": { - readonly def: "General Settings"; - readonly es: "Configuraciones Generales"; - readonly fr: "Paramètres généraux"; - readonly he: "הגדרות כלליות"; - readonly ja: "一般設定"; - readonly ko: "일반 설정"; - readonly ru: "Основные настройки"; - readonly zh: "常规设置"; - }; - readonly "obsidianLiveSyncSettingTab.panelPrivacyEncryption": { - readonly def: "Privacy & Encryption"; - readonly es: "Privacidad y Cifrado"; - readonly fr: "Confidentialité et chiffrement"; - readonly he: "פרטיות והצפנה"; - readonly ja: "プライバシーと暗号化"; - readonly ko: "개인정보 보호 및 암호화"; - readonly ru: "Конфиденциальность и шифрование"; - readonly zh: "隐私与加密"; - }; - readonly "obsidianLiveSyncSettingTab.panelRemoteConfiguration": { - readonly def: "Remote Configuration"; - readonly es: "Configuración remota"; - readonly fr: "Configuration distante"; - readonly he: "תצורת שרת מרוחד"; - readonly ja: "リモート設定"; - readonly ko: "원격 구성"; - readonly ru: "Удалённая конфигурация"; - readonly zh: "远程配置"; - }; - readonly "obsidianLiveSyncSettingTab.panelSetup": { - readonly def: "Setup"; - readonly es: "Configuración"; - readonly fr: "Configuration"; - readonly he: "הגדרה"; - readonly ja: "セットアップ"; - readonly ko: "설정"; - readonly ru: "Настройка"; - readonly zh: "设置"; - }; - readonly "obsidianLiveSyncSettingTab.serverVersion": { - readonly def: "Server info: ${info}"; - readonly fr: "Infos serveur : ${info}"; - readonly he: "פרטי שרת: ${info}"; - readonly ja: "サーバー情報: ${info}"; - readonly ru: "Информация о сервере: info"; - readonly zh: "服务器信息: ${info}"; - }; - readonly "obsidianLiveSyncSettingTab.titleActiveRemoteServer": { - readonly def: "Active Remote Server"; - readonly fr: "Serveur distant actif"; - readonly he: "שרת מרוחד פעיל"; - readonly ja: "アクティブなリモートサーバー"; - readonly ru: "Активный удалённый сервер"; - readonly zh: "活动远程服务器"; - }; - readonly "obsidianLiveSyncSettingTab.titleAppearance": { - readonly def: "Appearance"; - readonly es: "Apariencia"; - readonly fr: "Apparence"; - readonly he: "מראה"; - readonly ja: "外観"; - readonly ko: "외관"; - readonly ru: "Внешний вид"; - readonly zh: "外观"; - readonly "zh-tw": "外觀"; - }; - readonly "obsidianLiveSyncSettingTab.titleConflictResolution": { - readonly def: "Conflict resolution"; - readonly es: "Resolución de conflictos"; - readonly fr: "Résolution des conflits"; - readonly he: "פתרון קונפליקטים"; - readonly ja: "競合解決"; - readonly ko: "충돌 해결"; - readonly ru: "Разрешение конфликтов"; - readonly zh: "冲突处理"; - readonly "zh-tw": "衝突處理"; - }; - readonly "obsidianLiveSyncSettingTab.titleCongratulations": { - readonly def: "Congratulations!"; - readonly es: "¡Felicidades!"; - readonly fr: "Félicitations !"; - readonly he: "מזל טוב!"; - readonly ja: "おめでとうございます!"; - readonly ko: "축하합니다!"; - readonly ru: "Поздравляем!"; - readonly zh: "恭喜!"; - readonly "zh-tw": "恭喜!"; - }; - readonly "obsidianLiveSyncSettingTab.titleCouchDB": { - readonly def: "CouchDB"; - readonly es: "Servidor CouchDB"; - readonly fr: "CouchDB"; - readonly he: "CouchDB"; - readonly ja: "CouchDB サーバー"; - readonly ko: "CouchDB 서버"; - readonly ru: "Сервер CouchDB"; - readonly zh: "CouchDB 服务器"; - readonly "zh-tw": "CouchDB 伺服器"; - }; - readonly "obsidianLiveSyncSettingTab.titleDeletionPropagation": { - readonly def: "Deletion Propagation"; - readonly es: "Propagación de eliminación"; - readonly fr: "Propagation des suppressions"; - readonly he: "הפצת מחיקות"; - readonly ja: "削除の伝播"; - readonly ko: "삭제 전파"; - readonly ru: "Распространение удалений"; - readonly zh: "删除传播"; - readonly "zh-tw": "刪除傳播"; - }; - readonly "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": { - readonly def: "Encryption is not enabled"; - readonly es: "El cifrado no está habilitado"; - readonly fr: "Le chiffrement n'est pas activé"; - readonly he: "ההצפנה אינה מופעלת"; - readonly ja: "暗号化が有効になっていません"; - readonly ko: "암호화가 활성화되지 않음"; - readonly ru: "Шифрование не включено"; - readonly zh: "尚未启用加密"; - readonly "zh-tw": "尚未啟用加密"; - }; - readonly "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": { - readonly def: "Encryption Passphrase Invalid"; - readonly es: "La frase de contraseña de cifrado es inválida"; - readonly fr: "Phrase secrète de chiffrement invalide"; - readonly he: "ביטוי סיסמה להצפנה לא תקין"; - readonly ja: "暗号化パスフレーズが無効です"; - readonly ko: "암호화 패스프레이즈 유효하지 않음"; - readonly ru: "Парольная фраза шифрования недействительна"; - readonly zh: "加密密码短语无效"; - readonly "zh-tw": "加密密語無效"; - }; - readonly "obsidianLiveSyncSettingTab.titleExtraFeatures": { - readonly def: "Enable extra and advanced features"; - readonly es: "Habilitar funciones extras y avanzadas"; - readonly fr: "Activer les fonctionnalités supplémentaires et avancées"; - readonly he: "הפעל תכונות נוספות ומתקדמות"; - readonly ja: "追加および上級機能を有効化"; - readonly ko: "추가 및 고급 기능 활성화"; - readonly ru: "Включить дополнительные и расширенные функции"; - readonly zh: "启用额外和高级功能"; - }; - readonly "obsidianLiveSyncSettingTab.titleFetchConfig": { - readonly def: "Fetch Config"; - readonly es: "Obtener configuración"; - readonly fr: "Récupérer la configuration"; - readonly he: "משוך תצורה"; - readonly ja: "設定を取得"; - readonly ko: "구성 가져오기"; - readonly ru: "Загрузить конфигурацию"; - readonly zh: "获取配置"; - readonly "zh-tw": "抓取設定"; - }; - readonly "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": { - readonly def: "Fetch config from remote server"; - readonly es: "Obtener configuración del servidor remoto"; - readonly fr: "Récupérer la configuration depuis le serveur distant"; - readonly he: "משוך תצורה מהשרת המרוחד"; - readonly ja: "リモートサーバーから設定を取得"; - readonly ko: "원격 서버에서 구성 가져오기"; - readonly ru: "Загрузить конфигурацию с удалённого сервера"; - readonly zh: "从远程服务器获取配置"; - }; - readonly "obsidianLiveSyncSettingTab.titleFetchSettings": { - readonly def: "Fetch Settings"; - readonly es: "Obtener configuraciones"; - readonly fr: "Récupérer les paramètres"; - readonly he: "משוך הגדרות"; - readonly ja: "設定の取得"; - readonly ko: "설정 가져오기"; - readonly ru: "Загрузить настройки"; - readonly zh: "获取设置"; - }; - readonly "obsidianLiveSyncSettingTab.titleHiddenFiles": { - readonly def: "Hidden Files"; - readonly es: "Archivos ocultos"; - readonly fr: "Fichiers cachés"; - readonly he: "קבצים נסתרים"; - readonly ja: "隠しファイル"; - readonly ko: "숨김 파일"; - readonly ru: "Скрытые файлы"; - readonly zh: "隐藏文件"; - readonly "zh-tw": "隱藏檔案"; - }; - readonly "obsidianLiveSyncSettingTab.titleLogging": { - readonly def: "Logging"; - readonly es: "Registro"; - readonly fr: "Journalisation"; - readonly he: "רישום יומן"; - readonly ja: "ログ"; - readonly ko: "로깅"; - readonly ru: "Логирование"; - readonly zh: "日志"; - readonly "zh-tw": "記錄"; - }; - readonly "obsidianLiveSyncSettingTab.titleMinioS3R2": { - readonly def: "Minio,S3,R2"; - readonly es: "MinIO, S3, R2"; - readonly fr: "Minio, S3, R2"; - readonly he: "Minio,S3,R2"; - readonly ja: "MinIO、S3、R2"; - readonly ko: "MinIO, S3, R2"; - readonly ru: "MinIO, S3, R2"; - readonly zh: "MinIO、S3、R2"; - readonly "zh-tw": "MinIO、S3、R2"; - }; - readonly "obsidianLiveSyncSettingTab.titleNotification": { - readonly def: "Notification"; - readonly es: "Notificación"; - readonly fr: "Notification"; - readonly he: "התראה"; - readonly ja: "通知"; - readonly ko: "알림"; - readonly ru: "Уведомления"; - readonly zh: "通知"; - readonly "zh-tw": "通知"; - }; - readonly "obsidianLiveSyncSettingTab.titleOnlineTips": { - readonly def: "Online Tips"; - readonly es: "Consejos en línea"; - readonly fr: "Conseils en ligne"; - readonly he: "טיפים אונליין"; - readonly ja: "オンラインヒント"; - readonly ko: "온라인 팁"; - readonly ru: "Онлайн советы"; - readonly zh: "在线提示"; - }; - readonly "obsidianLiveSyncSettingTab.titleQuickSetup": { - readonly def: "Quick Setup"; - readonly es: "Configuración rápida"; - readonly fr: "Configuration rapide"; - readonly he: "הגדרה מהירה"; - readonly ja: "クイックセットアップ"; - readonly ko: "빠른 설정"; - readonly ru: "Быстрая настройка"; - readonly zh: "快速设置"; - }; - readonly "obsidianLiveSyncSettingTab.titleRebuildRequired": { - readonly def: "Rebuild Required"; - readonly es: "Reconstrucción necesaria"; - readonly fr: "Reconstruction requise"; - readonly he: "נדרשת בנייה מחדש"; - readonly ja: "再構築が必要"; - readonly ko: "재구축 필요"; - readonly ru: "Требуется перестроение"; - readonly zh: "需要重建"; - }; - readonly "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": { - readonly def: "Remote Configuration Check Failed"; - readonly es: "La verificación de configuración remota falló"; - readonly fr: "Échec de la vérification de la configuration distante"; - readonly he: "בדיקת תצורת שרת מרוחד נכשלה"; - readonly ja: "リモート設定の確認に失敗"; - readonly ko: "원격 구성 확인 실패"; - readonly ru: "Проверка удалённой конфигурации не удалась"; - readonly zh: "远端配置检查失败"; - readonly "zh-tw": "遠端設定檢查失敗"; - }; - readonly "obsidianLiveSyncSettingTab.titleRemoteServer": { - readonly def: "Remote Server"; - readonly es: "Servidor remoto"; - readonly fr: "Serveur distant"; - readonly he: "שרת מרוחד"; - readonly ja: "リモートサーバー"; - readonly ko: "원격 서버"; - readonly ru: "Удалённый сервер"; - readonly zh: "远端服务器"; - readonly "zh-tw": "遠端伺服器"; - }; - readonly "obsidianLiveSyncSettingTab.titleReset": { - readonly def: "Reset"; - readonly es: "Reiniciar"; - readonly fr: "Réinitialiser"; - readonly he: "אתחול"; - readonly ja: "リセット"; - readonly ko: "리셋"; - readonly ru: "Сброс"; - readonly zh: "重置"; - }; - readonly "obsidianLiveSyncSettingTab.titleSetupOtherDevices": { - readonly def: "To setup other devices"; - readonly es: "Para configurar otros dispositivos"; - readonly fr: "Pour configurer d'autres appareils"; - readonly he: "להגדרת מכשירים אחרים"; - readonly ja: "他のデバイスのセットアップ"; - readonly ko: "다른 기기 설정"; - readonly ru: "Для настройки других устройств"; - readonly zh: "设置其他设备"; - }; - readonly "obsidianLiveSyncSettingTab.titleSynchronizationMethod": { - readonly def: "Synchronization Method"; - readonly es: "Método de sincronización"; - readonly fr: "Méthode de synchronisation"; - readonly he: "שיטת סנכרון"; - readonly ja: "同期方法"; - readonly ko: "동기화 방법"; - readonly ru: "Метод синхронизации"; - readonly zh: "同步方式"; - readonly "zh-tw": "同步方式"; - }; - readonly "obsidianLiveSyncSettingTab.titleSynchronizationPreset": { - readonly def: "Synchronization Preset"; - readonly es: "Preestablecimiento de sincronización"; - readonly fr: "Préréglage de synchronisation"; - readonly he: "קבוע מראש לסנכרון"; - readonly ja: "同期プリセット"; - readonly ko: "동기화 프리셋"; - readonly ru: "Пресет синхронизации"; - readonly zh: "同步预设"; - readonly "zh-tw": "同步預設"; - }; - readonly "obsidianLiveSyncSettingTab.titleSyncSettings": { - readonly def: "Sync Settings"; - readonly es: "Configuraciones de Sincronización"; - readonly fr: "Paramètres de synchronisation"; - readonly he: "הגדרות סנכרון"; - readonly ja: "同期設定"; - readonly ko: "동기화 설정"; - readonly ru: "Настройки синхронизации"; - readonly zh: "同步设置"; - }; - readonly "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": { - readonly def: "Sync Settings via Markdown"; - readonly es: "Configuración de sincronización a través de Markdown"; - readonly fr: "Synchroniser les paramètres via Markdown"; - readonly he: "סנכרון הגדרות דרך Markdown"; - readonly ja: "Markdown経由で設定を同期"; - readonly ko: "마크다운을 통한 동기화 설정"; - readonly ru: "Синхронизация настроек через Markdown"; - readonly zh: "通过 Markdown 同步设置"; - readonly "zh-tw": "透過 Markdown 同步設定"; - }; - readonly "obsidianLiveSyncSettingTab.titleUpdateThinning": { - readonly def: "Update Thinning"; - readonly es: "Actualización de adelgazamiento"; - readonly fr: "Lissage des mises à jour"; - readonly he: "דילול עדכונים"; - readonly ja: "更新の間引き"; - readonly ko: "업데이트 솎아내기"; - readonly ru: "Оптимизация обновлений"; - readonly zh: "更新精简"; - readonly "zh-tw": "更新精簡"; - }; - readonly "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": { - readonly def: "⚠ CORS Origin is unmatched ${from}->${to}"; - readonly es: "⚠ El origen de CORS no coincide: {from}->{to}"; - readonly fr: "⚠ L'origine CORS ne correspond pas ${from}->${to}"; - readonly he: "⚠ CORS Origin אינו תואם ${from}->${to}"; - readonly ja: "⚠ CORS Originが一致しません ${from}->${to}"; - readonly ko: "⚠ CORS 원점이 일치하지 않습니다 {from}->{to}"; - readonly ru: "⚠ CORS Origin не совпадает from->to"; - readonly zh: "⚠ CORS 源不匹配 {from}->{to}"; - }; - readonly "obsidianLiveSyncSettingTab.warnNoAdmin": { - readonly def: "⚠ You do not have administrator privileges."; - readonly es: "⚠ No tienes privilegios de administrador."; - readonly fr: "⚠ Vous n'avez pas les privilèges administrateur."; - readonly he: "⚠ אין לך הרשאות מנהל."; - readonly ja: "⚠ 管理者権限がありません。"; - readonly ko: "⚠ 관리자 권한이 없습니다."; - readonly ru: "⚠ У вас нет прав администратора."; - readonly zh: "⚠ 您没有管理员权限"; - }; - readonly Ok: { - readonly def: "Ok"; - readonly es: "Aceptar"; - readonly ja: "OK"; - readonly ko: "확인"; - readonly ru: "ОК"; - readonly zh: "确定"; - readonly "zh-tw": "確定"; - }; - readonly "Old Algorithm": { - readonly def: "Old Algorithm"; - readonly es: "Algoritmo antiguo"; - readonly ja: "旧アルゴリズム"; - readonly ko: "이전 알고리즘"; - readonly ru: "Старый алгоритм"; - readonly zh: "旧算法"; - readonly "zh-tw": "舊演算法"; - }; - readonly "Older fallback (Slow, W/O WebAssembly)": { - readonly def: "Older fallback (Slow, W/O WebAssembly)"; - readonly es: "Alternativa anterior (lenta, sin WebAssembly)"; - readonly ja: "旧フォールバック (低速、WebAssembly なし)"; - readonly ko: "이전 대체 방식 (느림, WebAssembly 없음)"; - readonly ru: "Старый вариант fallback (медленный, без WebAssembly)"; - readonly zh: "旧版回退(较慢,无 WebAssembly)"; - readonly "zh-tw": "舊版回退(較慢,無 WebAssembly)"; - }; - readonly Open: { - readonly def: "Open"; - readonly es: "Abrir"; - readonly ja: "開く"; - readonly ko: "열기"; - readonly ru: "Открыть"; - readonly zh: "打开"; - }; - readonly "Open the dialog": { - readonly def: "Open the dialog"; - readonly es: "Abrir el diálogo"; - readonly ja: "ダイアログを開く"; - readonly ko: "대화상자 열기"; - readonly ru: "Открыть диалог"; - readonly zh: "打开对话框"; - }; - readonly Overwrite: { - readonly def: "Overwrite"; - readonly es: "Sobrescribir"; - readonly ja: "上書き"; - readonly ko: "덮어쓰기"; - readonly ru: "Перезаписать"; - readonly zh: "覆盖"; - }; - readonly "Overwrite patterns": { - readonly def: "Overwrite patterns"; - readonly es: "Patrones de sobrescritura"; - readonly ja: "上書きパターン"; - readonly ko: "덮어쓰기 패턴"; - readonly ru: "Шаблоны перезаписи"; - readonly zh: "覆盖模式"; - readonly "zh-tw": "覆寫模式"; - }; - readonly "Overwrite remote": { - readonly def: "Overwrite remote"; - readonly es: "Sobrescribir remoto"; - readonly ja: "リモートを上書き"; - readonly ko: "원격 덮어쓰기"; - readonly ru: "Перезаписать удалённое хранилище"; - readonly zh: "覆盖远端"; - readonly "zh-tw": "覆寫遠端"; - }; - readonly "Overwrite remote with local DB and passphrase.": { - readonly def: "Overwrite remote with local DB and passphrase."; - readonly es: "Sobrescribe el remoto con la base de datos local y la frase de contraseña."; - readonly ja: "ローカル DB とパスフレーズでリモートを上書きします。"; - readonly ko: "로컬 DB와 암호문구로 원격을 덮어씁니다."; - readonly ru: "Перезаписать удалённое хранилище локальной БД и парольной фразой."; - readonly zh: "使用本地数据库和密码短语覆盖远端。"; - readonly "zh-tw": "使用本機資料庫與密語覆寫遠端。"; - }; - readonly "Overwrite Server Data with This Device's Files": { - readonly def: "Overwrite Server Data with This Device's Files"; - readonly es: "Sobrescribir los datos del servidor con los archivos de este dispositivo"; - readonly ja: "このデバイスのファイルでサーバーデータを上書き"; - readonly ko: "이 기기의 파일로 서버 데이터를 덮어쓰기"; - readonly ru: "Перезаписать данные сервера файлами с этого устройства"; - readonly zh: "用本设备文件覆盖服务器数据"; - readonly "zh-tw": "以此裝置的檔案覆寫伺服器資料"; - }; - readonly "P2P.AskPassphraseForDecrypt": { - readonly def: "The remote peer shared the configuration. Please input the passphrase to decrypt the configuration."; - readonly fr: "Le pair distant a partagé la configuration. Veuillez saisir la phrase secrète pour déchiffrer la configuration."; - readonly he: "העמית המרוחד שיתף את התצורה. אנא הזן את ביטוי הסיסמה לפענוח התצורה."; - readonly ja: "リモートピアから設定が共有されました。設定を復号するためのパスフレーズを入力してください。"; - readonly ko: "원격 피어가 구성을 공유했습니다. 구성을 복호화하려면 패스프레이즈를 입력해 주세요."; - readonly ru: "Удалённое устройство предоставило конфигурацию. Введите пароль для расшифровки."; - readonly zh: "远程对等方共享了配置,请输入密码短语以解密配置"; - }; - readonly "P2P.AskPassphraseForShare": { - readonly def: "The remote peer requested this device configuration. Please input the passphrase to share the configuration. You can ignore the request by cancelling this dialogue."; - readonly fr: "Le pair distant a demandé la configuration de cet appareil. Veuillez saisir la phrase secrète pour partager la configuration. Vous pouvez ignorer la demande en annulant cette boîte de dialogue."; - readonly he: "העמית המרוחד ביקש את תצורת מכשיר זה. אנא הזן את ביטוי הסיסמה לשיתוף התצורה. ניתן להתעלם מהבקשה על ידי ביטול הדיאלוג."; - readonly ja: "リモートピアからこのデバイスの設定が要求されました。設定を共有するためのパスフレーズを入力してください。このダイアログをキャンセルすることでリクエストを無視できます。"; - readonly ko: "원격 피어가 이 기기의 구성을 요청했습니다. 구성을 공유하려면 패스프레이즈를 입력해 주세요. 이 대화상자를 취소하여 요청을 무시할 수 있습니다."; - readonly ru: "Удалённое устройство запрашивает эту конфигурацию. Введите пароль для передачи."; - readonly zh: "远程对等方请求此设备配置,请输入密码短语以共享配置。你可以通过取消此对话框来忽略此请求"; - }; - readonly "P2P.DisabledButNeed": { - readonly def: "Peer-to-Peer Sync is disabled. Do you really want to enable it?"; - readonly fr: "Synchronisation pair-à-pair est désactivé. Voulez-vous vraiment l'activer ?"; - readonly he: "%{title_p2p_sync} מנוטרל. האם אתה בטוח שברצונך להפעיל?"; - readonly ja: "Peer-to-Peer Syncは無効になっています。本当に有効にしますか?"; - readonly ko: "피어 투 피어(P2P) 동기화가 비활성화되어 있습니다. 정말로 활성화하시겠습니까?"; - readonly ru: "title_p2p_sync отключён. Вы действительно хотите включить?"; - readonly zh: "Peer-to-Peer同步 已禁用。你确定要启用它吗?"; - }; - readonly "P2P.FailedToOpen": { - readonly def: "Failed to open P2P connection to the signalling server."; - readonly fr: "Échec d'ouverture de la connexion P2P vers le serveur de signalisation."; - readonly he: "לא ניתן לפתוח חיבור P2P לשרת האותות."; - readonly ja: "シグナリングサーバーへのP2P接続を開けませんでした。"; - readonly ko: "시그널링 서버에 P2P 연결을 열 수 없습니다."; - readonly ru: "Не удалось открыть P2P подключение к серверу сигнализации."; - readonly zh: "无法打开 P2P 连接到信令服务器"; - }; - readonly "P2P.NoAutoSyncPeers": { - readonly def: "No auto-sync peers found. Please set peers on the Peer-to-Peer Sync pane."; - readonly fr: "Aucun pair de synchronisation automatique trouvé. Veuillez définir des pairs dans le panneau Synchronisation pair-à-pair."; - readonly he: "לא נמצאו עמיתים לסנכרון אוטומטי. אנא הגדר עמיתים בלוח %{long_p2p_sync}."; - readonly ja: "自動同期ピアが見つかりません。Peer-to-Peer Sync (試験機能)ペインでピアを設定してください。"; - readonly ko: "자동 동기화 피어를 찾을 수 없습니다. 피어 투 피어(P2P) 동기화 (실험 기능) 창에서 피어를 설정해 주세요."; - readonly ru: "Автосинхронизируемые устройства не найдены."; - readonly zh: "未找到自动同步的对等方,请在 Peer-to-Peer同步 (实验性) 面板中设置对等方"; - }; - readonly "P2P.NoKnownPeers": { - readonly def: "No peers has been detected, waiting incoming other peers..."; - readonly fr: "Aucun pair détecté, en attente d'autres pairs entrants..."; - readonly he: "לא זוהו עמיתים, ממתין לעמיתים נכנסים..."; - readonly ja: "ピアが検出されていません。他のピアからの接続を待機中..."; - readonly ko: "피어가 감지되지 않았습니다. 다른 피어의 접속을 기다리고 있습니다..."; - readonly ru: "Устройства не обнаружены, ожидаем другие устройства..."; - readonly zh: "未检测到对等方,正在等待其他对等方的连接..."; - }; - readonly "P2P.Note.description": { - readonly def: " This replicator allows us to synchronise our vault with other devices\nusing a peer-to-peer connection. We can use this to synchronise our vault with our other devices without using a cloud service.\nThis replicator is based on Trystero. It also uses a signalling server to establish a connection between devices. The signalling server is used to exchange connection information between devices. It does (or,should) not know or store any of our data.\n\nThe signalling server can be hosted by anyone. This is just a Nostr relay. For the sake of simplicity and checking the behaviour of the replicator, an instance of the signalling server is hosted by vrtmrz. You can use the experimental server provided by vrtmrz, or you can use any other server.\n\nBy the way, even if the signalling server does not store our data, it can see the connection information of some of our devices. Please be aware of this. Also, be cautious when using the server provided by someone else."; - readonly fr: " Ce réplicateur permet de synchroniser notre coffre avec d'autres\nappareils via une connexion pair-à-pair. Nous pouvons l'utiliser pour synchroniser notre coffre avec nos autres appareils sans recourir à un service cloud.\nCe réplicateur est basé sur Trystero. Il utilise également un serveur de signalisation pour établir une connexion entre les appareils. Le serveur de signalisation sert à échanger les informations de connexion entre appareils. Il ne connaît (ou ne devrait connaître) ni ne stocke aucune de nos données.\n\nLe serveur de signalisation peut être hébergé par n'importe qui. Il s'agit simplement d'un relais Nostr. Par souci de simplicité et pour vérifier le comportement du réplicateur, une instance du serveur de signalisation est hébergée par vrtmrz. Vous pouvez utiliser le serveur expérimental fourni par vrtmrz, ou tout autre serveur.\n\nAu passage, même si le serveur de signalisation ne stocke pas nos données, il peut voir les informations de connexion de certains de nos appareils. Soyez-en conscient. Soyez également prudent avec un serveur fourni par quelqu'un d'autre."; - readonly he: " רפליקטור זה מאפשר לסנכרן את הכספת עם מכשירים אחרים באמצעות חיבור עמית-לעמית.\nניתן להשתמש בזה לסנכרון הכספת עם מכשירים אחרים ללא שירות ענן.\nרפליקטור זה מבוסס על Trystero. הוא משתמש גם בשרת אותות לביסוס חיבור בין מכשירים. שרת האותות משמש להחלפת מידע חיבור בין מכשירים. הוא אינו (ולא אמור) לדעת או לאחסן את הנתונים שלנו.\n\nשרת האותות יכול להיות מאוחסן על ידי כל אחד. זהו ממסר Nostr בלבד. לצורך פשטות ובדיקת התנהגות הרפליקטור, vrtmrz מאחסן עותק של שרת האותות. ניתן להשתמש בשרת הניסיוני של vrtmrz, או בכל שרת אחר.\n\nאגב, גם אם שרת האותות אינו מאחסן נתונים, הוא יכול לראות מידע חיבור של חלק ממכשיריך. אנא שים לב לכך. כמו כן, היה זהיר בשימוש בשרת של מישהו אחר."; - readonly ja: "このレプリケーターは、ピアツーピア接続を使用して、Vaultを他のデバイスと同期することができます。クラウドサービスを使用せずに、他のデバイスとVaultを同期することができます。\nこのレプリケーターはTrysteroをベースにしています。デバイス間の接続を確立するためにシグナリングサーバーを使用します。シグナリングサーバーはデバイス間で接続情報を交換するために使用されます。私たちのデータを知ったり保存したりすることはありません(または、そうあるべきではありません)。\n\nシグナリングサーバーは誰でもホストできます。これは単なるNostrリレーです。簡便さとレプリケーターの動作確認のために、vrtmrzがシグナリングサーバーのインスタンスをホストしています。vrtmrzが提供する実験用サーバーを使用することも、他のサーバーを使用することもできます。\n\nなお、シグナリングサーバーが私たちのデータを保存しなくても、一部のデバイスの接続情報を見ることができます。これにご注意ください。また、他の人が提供するサーバーを使用する場合は注意してください。"; - readonly ko: "이 복제기는 피어 투 피어(P2P) 연결을 통해 다른 기기들과 볼트를 동기화할 수 있도록 합니다. 클라우드 서비스를 거치지 않고도 기기간 동기화를 구현할 수 있습니다.\n\n이 복제기는 Trystero를 기반으로 하며, 기기 간 연결을 설정하기 위해 시그널링 서버를 사용합니다. 시그널링 서버는 단순히 연결 정보를 교환하는 용도로만 사용되며, 사용자 데이터를 저장하거나 접근하지 않습니다 (또는 그래야만 합니다).\n\n시그널링 서버는 누구나 운영할 수 있으며, 이는 단순한 Nostr 릴레이입니다. 편의성과 복제기의 작동 확인을 위해 `vrtmrz`가 자체적으로 시그널링 서버 인스턴스를 운영 중입니다. 사용자는 `vrtmrz`가 제공하는 실험용 서버를 사용할 수도 있고, 별도로 자신만의 서버를 설정할 수도 있습니다.\n\n참고로, 시그널링 서버는 사용자 데이터를 저장하지 않더라도 일부 기기의 연결 정보는 볼 수 있습니다. 이 점을 유의해 주세요. 특히 타인이 운영하는 서버를 사용할 경우 주의가 필요합니다."; - readonly ru: "Этот репликатор позволяет синхронизировать хранилище с другими устройствами с использованием однорангового соединения."; - readonly zh: " This replicator allows us to synchronise our vault with other devices\nusing a peer-to-peer connection. We can use this to synchronise our vault with our other devices without using a cloud service.\nThis replicator is based on Trystero. It also uses a signaling server to establish a connection between devices. The signaling server is used to exchange connection information between devices. It does (or,should) not know or store any of our data.\n\nThe signaling server can be hosted by anyone. This is just a Nostr relay. For the sake of simplicity and checking the behaviour of the replicator, an instance of the signaling server is hosted by vrtmrz. You can use the experimental server provided by vrtmrz, or you can use any other server.\n\nBy the way, even if the signaling server does not store our data, it can see the connection information of some of our devices. Please be aware of this. Also, be cautious when using the server provided by someone else."; - }; - readonly "P2P.Note.important_note": { - readonly def: "Peer-to-Peer Replicator."; - readonly fr: "Réplicateur pair-à-pair."; - readonly he: "רפליקטור עמית-לעמית."; - readonly ja: "ピアツーピアレプリケーターの実験的実装"; - readonly ko: "피어 투 피어(P2P) 복제기의 실험적 구현입니다."; - readonly ru: "P2P репликатор."; - readonly zh: "The Experimental Implementation of the Peer-to-Peer Replicator."; - }; - readonly "P2P.Note.important_note_sub": { - readonly def: "This feature is still on the bleeding edge. Please be aware that ensure your data is backed up before using this feature. And, we would be so happy if you could contribute to the development of this feature."; - readonly fr: "Cette fonctionnalité est encore en tout début de développement. Veillez à sauvegarder vos données avant de l'utiliser. Nous serions très heureux si vous contribuiez au développement de cette fonctionnalité."; - readonly he: "תכונה זו עדיין בשלב מתקדם. ודא שהנתונים שלך מגובים לפני השימוש. ונשמח אם תוכל לתרום לפיתוח תכונה זו."; - readonly ja: "この機能はまだ実験段階です。期待通りに動作しない可能性があることにご注意ください。さらに、バグ、セキュリティの問題、その他の問題がある可能性があります。この機能は自己責任でご使用ください。この機能の開発にご協力ください。"; - readonly ko: "이 기능은 아직 실험 단계에 있습니다. 이 기능이 예상대로 작동하지 않을 수 있음을 알아주세요. 또한 버그, 보안 문제 및 기타 문제가 있을 수 있습니다. 이 기능을 사용할 때는 본인의 책임 하에 사용하세요. 이 기능의 개발에 기여해 주세요."; - readonly ru: "Эта функция всё ещё на стадии разработки. Пожалуйста, убедитесь, что ваши данные зарезервированы."; - readonly zh: "This feature is still in the experimental stage. Please be aware that this feature may not work as expected. Furthermore, it may have some bugs, security issues, and other issues. Please use this feature at your own risk. Please contribute to the development of this feature."; - }; - readonly "P2P.Note.Summary": { - readonly def: "What is this feature? (and some important notes, please read once)"; - readonly fr: "Qu'est-ce que cette fonctionnalité ? (et quelques notes importantes, à lire)"; - readonly he: "מהי תכונה זו? (ועוד הערות חשובות, נא לקרוא פעם אחת)"; - readonly ja: "この機能について(重要な注意事項を含む、一度お読みください)"; - readonly ko: "이 기능은 무엇인가요? (설명과 참고사항이 적혀있습니다. 한 번 읽어보세요!)"; - readonly ru: "Что это за функция? (важные замечания)"; - readonly zh: "What is this feature? (and some important notes, please read once)"; - }; - readonly "P2P.NotEnabled": { - readonly def: "Peer-to-Peer Sync is not enabled. We cannot open a new connection."; - readonly fr: "Synchronisation pair-à-pair n'est pas activé. Nous ne pouvons pas ouvrir de nouvelle connexion."; - readonly he: "%{title_p2p_sync} אינו מופעל. לא ניתן לפתוח חיבור חדש."; - readonly ja: "Peer-to-Peer Syncが有効になっていません。新しい接続を開くことができません。"; - readonly ko: "피어 투 피어(P2P) 동기화가 활성화되지 않았습니다. 새로운 연결을 열 수 없습니다."; - readonly ru: "title_p2p_sync не включён. Мы не можем открыть новое подключение."; - readonly zh: "Peer-to-Peer同步 is not enabled. We cannot open a new connection."; - }; - readonly "P2P.P2PReplication": { - readonly def: "Peer-to-Peer Replication"; - readonly fr: "Réplication Pair-à-Pair"; - readonly he: "שכפול %{P2P}"; - readonly ja: "Peer-to-Peerレプリケーション(複製)"; - readonly ko: "피어-to-피어 복제"; - readonly ru: "P2P Репликация"; - readonly zh: "Peer-to-Peer Replication"; - }; - readonly "P2P.PaneTitle": { - readonly def: "Peer-to-Peer Sync"; - readonly fr: "Synchronisation pair-à-pair"; - readonly he: "%{long_p2p_sync}"; - readonly ja: "Peer-to-Peer Sync (試験機能)"; - readonly ko: "피어 투 피어(P2P) 동기화 (실험 기능)"; - readonly ru: "long_p2p_sync"; - readonly zh: "Peer-to-Peer同步 (实验性)"; - }; - readonly "P2P.ReplicatorInstanceMissing": { - readonly def: "P2P Sync replicator is not found, possibly not have been configured or enabled."; - readonly fr: "Le réplicateur Sync P2P est introuvable, peut-être non configuré ou non activé."; - readonly he: "רפליקטור סנכרון P2P לא נמצא, ייתכן שלא הוגדר או הופעל."; - readonly ja: "P2P同期レプリケーターが見つかりません。設定または有効化されていない可能性があります。"; - readonly ko: "P2P 동기화 복제기를 찾을 수 없습니다. 구성되지 않았거나 활성화되지 않았을 수 있습니다."; - readonly ru: "P2P Sync репликатор не найден, возможно, не настроен."; - readonly zh: "P2P Sync replicator is not found, possibly not have been configured or enabled."; - }; - readonly "P2P.SeemsOffline": { - readonly def: "Peer ${name} seems offline, skipped."; - readonly fr: "Le pair ${name} semble hors ligne, ignoré."; - readonly he: "העמית ${name} נראה לא מחובר, מדלג."; - readonly ja: "ピア${name}はオフラインのようです。スキップしました。"; - readonly ko: "피어 ${name}이(가) 오프라인인 것 같습니다. 건너뜁니다."; - readonly ru: "Устройство name офлайн, пропущено."; - readonly zh: "Peer ${name} seems offline, skipped."; - }; - readonly "P2P.SyncAlreadyRunning": { - readonly def: "P2P Sync is already running."; - readonly fr: "La Sync P2P est déjà en cours."; - readonly he: "סנכרון P2P כבר פועל."; - readonly ja: "P2P同期はすでに実行中です。"; - readonly ko: "P2P 동기화가 이미 실행 중입니다."; - readonly ru: "P2P Sync уже запущен."; - readonly zh: "P2P Sync is already running."; - }; - readonly "P2P.SyncCompleted": { - readonly def: "P2P Sync completed."; - readonly fr: "Sync P2P terminée."; - readonly he: "סנכרון P2P הושלם."; - readonly ja: "P2P同期が完了しました。"; - readonly ko: "P2P 동기화가 완료되었습니다."; - readonly ru: "P2P Sync завершён."; - readonly zh: "P2P Sync completed."; - }; - readonly "P2P.SyncStartedWith": { - readonly def: "P2P Sync with ${name} have been started."; - readonly fr: "La Sync P2P avec ${name} a démarré."; - readonly he: "סנכרון P2P עם ${name} התחיל."; - readonly ja: "${name}とのP2P同期を開始しました。"; - readonly ko: "${name}과의 P2P 동기화가 시작되었습니다."; - readonly ru: "P2P Sync с name начат."; - readonly zh: "P2P Sync with ${name} have been started."; - }; - readonly "paneMaintenance.markDeviceResolvedAfterBackup": { - readonly def: "paneMaintenance.markDeviceResolvedAfterBackup"; - readonly es: "Marcar el dispositivo como resuelto después de hacer una copia de seguridad"; - readonly ja: "バックアップ後にこのデバイスを解決済みにする"; - readonly ko: "백업 후 장치를 해결됨으로 표시"; - readonly ru: "Пометить устройство как обработанное после резервного копирования"; - readonly zh: "请在完成备份后将此设备标记为已处理。"; - readonly "zh-tw": "請在完成備份後將此裝置標記為已處理。"; - }; - readonly "paneMaintenance.remoteLockedAndDeviceNotAccepted": { - readonly def: "paneMaintenance.remoteLockedAndDeviceNotAccepted"; - readonly es: "La base de datos remota está bloqueada y este dispositivo aún no ha sido aceptado."; - readonly ja: "リモートデータベースはロックされており、このデバイスはまだ承認されていません。"; - readonly ko: "원격 데이터베이스가 잠겨 있으며 이 장치는 아직 승인되지 않았습니다."; - readonly ru: "Удалённая база данных заблокирована, и это устройство ещё не одобрено."; - readonly zh: "远端数据库已锁定,且此设备尚未被接受。"; - readonly "zh-tw": "遠端資料庫已鎖定,且此裝置尚未被接受。"; - }; - readonly "paneMaintenance.remoteLockedResolvedDevice": { - readonly def: "paneMaintenance.remoteLockedResolvedDevice"; - readonly es: "La base de datos remota está bloqueada, pero este dispositivo ya fue aceptado."; - readonly ja: "リモートデータベースはロックされていますが、このデバイスはすでに承認されています。"; - readonly ko: "원격 데이터베이스가 잠겨 있지만 이 장치는 이미 승인되었습니다."; - readonly ru: "Удалённая база данных заблокирована, но это устройство уже одобрено."; - readonly zh: "远端数据库已锁定,但此设备已被接受。"; - readonly "zh-tw": "遠端資料庫已鎖定,但此裝置已被接受。"; - }; - readonly "paneMaintenance.unlockDatabaseReady": { - readonly def: "paneMaintenance.unlockDatabaseReady"; - readonly es: "Desbloquear la base de datos"; - readonly ja: "データベースのロックを解除"; - readonly ko: "데이터베이스 잠금 해제"; - readonly ru: "Разблокировать базу данных"; - readonly zh: "现在可以解锁数据库。"; - readonly "zh-tw": "現在可以解鎖資料庫。"; - }; - readonly Passphrase: { - readonly def: "Passphrase"; - readonly es: "Frase de contraseña"; - readonly fr: "Phrase secrète"; - readonly he: "ביטוי סיסמה"; - readonly ja: "パスフレーズ"; - readonly ko: "패스프레이즈"; - readonly ru: "Парольная фраза"; - readonly zh: "密码"; - }; - readonly "Passphrase of sensitive configuration items": { - readonly def: "Passphrase of sensitive configuration items"; - readonly es: "Frase para elementos sensibles"; - readonly fr: "Phrase secrète des éléments de configuration sensibles"; - readonly he: "ביטוי סיסמה לפריטי תצורה רגישים"; - readonly ja: "機密性の高い設定項目にパスフレーズを使用"; - readonly ko: "민감한 구성 항목의 패스프레이즈"; - readonly ru: "Парольная фраза для конфиденциальных настроек"; - readonly zh: "敏感配置项的密码"; - }; - readonly password: { - readonly def: "password"; - readonly es: "contraseña"; - readonly fr: "mot de passe"; - readonly he: "סיסמה"; - readonly ja: "パスワード"; - readonly ko: "비밀번호"; - readonly ru: "пароль"; - readonly zh: "密码"; - }; - readonly Password: { - readonly def: "Password"; - readonly es: "Contraseña"; - readonly fr: "Mot de passe"; - readonly he: "סיסמה"; - readonly ja: "パスワード"; - readonly ko: "비밀번호"; - readonly ru: "Пароль"; - readonly zh: "密码"; - }; - readonly "Paste a connection string": { - readonly def: "Paste a connection string"; - readonly es: "Pegar cadena de conexión"; - readonly ja: "接続文字列を貼り付ける"; - readonly ko: "연결 문자열 붙여넣기"; - readonly ru: "Вставить строку подключения"; - readonly zh: "粘贴连接字符串"; - readonly "zh-tw": "貼上連線字串"; - }; - readonly "Paste the Setup URI generated from one of your active devices.": { - readonly def: "Paste the Setup URI generated from one of your active devices."; - readonly es: "Pegue el URI de configuración generado desde uno de sus dispositivos activos。"; - readonly ja: "稼働中の端末で生成した Setup URI を貼り付けてください。"; - readonly ko: "현재 사용 중인 장치 중 하나에서 생성한 설정 URI를 붙여 넣으세요。"; - readonly ru: "Вставьте Setup URI, созданный на одном из ваших активных устройств。"; - readonly zh: "粘贴从一台已在使用的设备上生成的 Setup URI。"; - readonly "zh-tw": "貼上從一台已在使用裝置上產生的 Setup URI。"; - }; - readonly "Path Obfuscation": { - readonly def: "Path Obfuscation"; - readonly es: "Ofuscación de rutas"; - readonly fr: "Obfuscation des chemins"; - readonly he: "ערפול נתיב"; - readonly ja: "パスの難読化"; - readonly ko: "경로 난독화"; - readonly ru: "Обфускация путей"; - readonly zh: "路径混淆"; - }; - readonly "Patterns to match files for overwriting instead of merging": { - readonly def: "Patterns to match files for overwriting instead of merging"; - readonly es: "Patrones para identificar archivos que se sobrescribirán en lugar de fusionarse"; - readonly ja: "マージではなく上書きするファイルを判定するパターン"; - readonly ko: "병합 대신 덮어쓸 파일을 판별하는 패턴"; - readonly ru: "Шаблоны для файлов, которые нужно перезаписывать вместо объединения"; - readonly zh: "用于匹配需覆盖而非合并文件的模式"; - readonly "zh-tw": "用於匹配需覆寫而非合併檔案的模式"; - }; - readonly "Patterns to match files for syncing": { - readonly def: "Patterns to match files for syncing"; - readonly es: "Patrones para identificar archivos que se sincronizarán"; - readonly ja: "同期対象ファイルを判定するパターン"; - readonly ko: "동기화할 파일을 판별하는 패턴"; - readonly ru: "Шаблоны для файлов, которые нужно синхронизировать"; - readonly zh: "用于匹配同步文件的模式"; - readonly "zh-tw": "用於匹配同步檔案的模式"; - }; - readonly "Peer-to-Peer only": { - readonly def: "Peer-to-Peer only"; - readonly es: "Solo Peer-to-Peer"; - readonly ja: "Peer-to-Peer のみ"; - readonly ko: "Peer-to-Peer 전용"; - readonly ru: "Только Peer-to-Peer"; - readonly zh: "仅 Peer-to-Peer"; - readonly "zh-tw": "僅 Peer-to-Peer"; - }; - readonly "Peer-to-Peer Synchronisation": { - readonly def: "Peer-to-Peer Synchronisation"; - readonly es: "Sincronización entre pares"; - readonly ja: "ピアツーピア同期"; - readonly ko: "피어 투 피어 동기화"; - readonly ru: "Одноранговая синхронизация"; - readonly zh: "点对点同步"; - readonly "zh-tw": "點對點同步"; - }; - readonly "Per-file-saved customization sync": { - readonly def: "Per-file-saved customization sync"; - readonly es: "Sincronización de personalización por archivo"; - readonly fr: "Synchronisation de personnalisation enregistrée par fichier"; - readonly he: "סנכרון התאמה אישית שנשמר לפי קובץ"; - readonly ja: "ファイルごとのカスタマイズ同期"; - readonly ko: "파일별 저장 사용자 설정 동기화"; - readonly ru: "Синхронизация настроек для каждого файла"; - readonly zh: "按文件保存的自定义同步"; - }; - readonly Perform: { - readonly def: "Perform"; - readonly es: "Ejecutar"; - readonly ja: "実行"; - readonly ko: "실행"; - readonly ru: "Выполнить"; - readonly zh: "执行"; - readonly "zh-tw": "執行"; - }; - readonly "Perform cleanup": { - readonly def: "Perform cleanup"; - readonly es: "Ejecutar limpieza"; - readonly ja: "クリーンアップを実行"; - readonly ko: "정리 실행"; - readonly ru: "Выполнить очистку"; - readonly zh: "执行清理"; - readonly "zh-tw": "執行清理"; - }; - readonly "Perform Garbage Collection": { - readonly def: "Perform Garbage Collection"; - readonly es: "Ejecutar recolección de basura"; - readonly ja: "ガーベジコレクションを実行"; - readonly ko: "가비지 컬렉션 실행"; - readonly ru: "Выполнить сборку мусора"; - readonly zh: "执行垃圾回收"; - readonly "zh-tw": "執行垃圾回收"; - }; - readonly "Perform Garbage Collection to remove unused chunks and reduce database size.": { - readonly def: "Perform Garbage Collection to remove unused chunks and reduce database size."; - readonly es: "Ejecuta la recolección de basura para eliminar chunks no usados y reducir el tamaño de la base de datos."; - readonly ja: "未使用のチャンクを削除し、データベースサイズを削減するためにガーベジコレクションを実行します。"; - readonly ko: "사용하지 않는 청크를 제거하고 데이터베이스 크기를 줄이기 위해 가비지 컬렉션을 실행합니다."; - readonly ru: "Выполняет сборку мусора, чтобы удалить неиспользуемые чанки и уменьшить размер базы данных."; - readonly zh: "执行垃圾回收以清理未使用的 chunks 并减小数据库体积。"; - readonly "zh-tw": "執行垃圾回收以移除未使用的 chunks 並減少資料庫大小。"; - }; - readonly "Periodic Sync interval": { - readonly def: "Periodic Sync interval"; - readonly es: "Intervalo de sincronización periódica"; - readonly fr: "Intervalle de synchronisation périodique"; - readonly he: "מרווח סנכרון תקופתי"; - readonly ja: "定時同期の感覚"; - readonly ko: "주기적 동기화 간격"; - readonly ru: "Интервал периодической синхронизации"; - readonly zh: "定期同步间隔"; - }; - readonly "Pick a file to resolve conflict": { - readonly def: "Pick a file to resolve conflict"; - readonly es: "Elegir un archivo para resolver el conflicto"; - readonly ja: "競合を解決するファイルを選択"; - readonly ko: "충돌을 해결할 파일 선택"; - readonly ru: "Выбрать файл для разрешения конфликта"; - readonly zh: "选择要解决冲突的文件"; - readonly "zh-tw": "選擇要解決衝突的檔案"; - }; - readonly "Pick a file to show history": { - readonly def: "Pick a file to show history"; - readonly "zh-tw": "選擇要顯示歷程的檔案"; - }; - readonly "Please disable 'Read chunks online' in settings to use Garbage Collection.": { - readonly def: "Please disable 'Read chunks online' in settings to use Garbage Collection."; - readonly ja: "Garbage Collection を使うには、設定で「Read chunks online」を無効にしてください。"; - readonly ko: "Garbage Collection을 사용하려면 설정에서 \"Read chunks online\"을 비활성화해 주세요."; - readonly ru: "Чтобы использовать Garbage Collection, отключите в настройках «Read chunks online»."; - readonly zh: "要使用垃圾回收,请在设置中禁用“Read chunks online”。"; - readonly "zh-tw": "若要使用垃圾回收,請在設定中停用「Read chunks online」。"; - }; - readonly "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": { - readonly def: "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection."; - readonly ja: "Garbage Collection を使うには、設定で「Compute revisions for chunks」を有効にしてください。"; - readonly ko: "Garbage Collection을 사용하려면 설정에서 \"Compute revisions for chunks\"를 활성화해 주세요."; - readonly ru: "Чтобы использовать Garbage Collection, включите в настройках «Compute revisions for chunks»."; - readonly zh: "要使用垃圾回收,请在设置中启用“Compute revisions for chunks”。"; - readonly "zh-tw": "若要使用垃圾回收,請在設定中啟用「Compute revisions for chunks」。"; - }; - readonly "Please select 'Cancel' explicitly to cancel this operation.": { - readonly def: "Please select 'Cancel' explicitly to cancel this operation."; - readonly ja: "この操作を中止するには、明示的に「キャンセル」を選択してください。"; - readonly ko: "이 작업을 취소하려면 반드시 \"취소\"를 명시적으로 선택해 주세요."; - readonly ru: "Чтобы отменить эту операцию, явно выберите «Отмена»."; - readonly zh: "如需取消此操作,请明确选择“取消”。"; - readonly "zh-tw": "若要取消此操作,請明確選擇「取消」。"; - }; - readonly "Please select a method to import the settings from another device.": { - readonly def: "Please select a method to import the settings from another device."; - readonly es: "Seleccione un método para importar la configuración desde otro dispositivo。"; - readonly ja: "別の端末から設定を取り込む方法を選択してください。"; - readonly ko: "다른 장치에서 설정을 가져올 방법을 선택해 주세요。"; - readonly ru: "Выберите способ импорта настроек с другого устройства。"; - readonly zh: "请选择一种从其他设备导入设置的方法。"; - readonly "zh-tw": "請選擇一種從其他裝置匯入設定的方法。"; - }; - readonly "Please select an option to proceed": { - readonly def: "Please select an option to proceed"; - readonly es: "Seleccione una opción para continuar"; - readonly ja: "続行するには項目を選択してください"; - readonly ko: "계속하려면 항목을 선택해 주세요"; - readonly ru: "Чтобы продолжить, выберите вариант"; - readonly zh: "请选择一个选项以继续"; - readonly "zh-tw": "請選擇一個選項以繼續"; - }; - readonly "Please select the type of server to which you are connecting.": { - readonly def: "Please select the type of server to which you are connecting."; - readonly es: "Seleccione el tipo de servidor al que se está conectando。"; - readonly ja: "接続するサーバーの種類を選択してください。"; - readonly ko: "연결할 서버 유형을 선택해 주세요。"; - readonly ru: "Выберите тип сервера, к которому вы подключаетесь。"; - readonly zh: "请选择你要连接的服务器类型。"; - readonly "zh-tw": "請選擇你要連線的伺服器類型。"; - }; - readonly "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": { - readonly def: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature."; - readonly es: "Define un nombre para identificar este dispositivo. Debe ser único entre tus dispositivos. Mientras no esté configurado, no podremos habilitar esta función."; - readonly ja: "このデバイスを識別するためのデバイス名を設定してください。この名前は各デバイスで一意である必要があります。設定されるまで、この機能は有効にできません。"; - readonly ko: "이 장치를 식별할 장치 이름을 설정해 주세요. 이 이름은 장치 간에 고유해야 합니다. 설정되기 전까지는 이 기능을 활성화할 수 없습니다."; - readonly ru: "Укажите имя устройства для идентификации этого устройства. Имя должно быть уникальным среди ваших устройств. Пока оно не задано, мы не можем включить эту функцию."; - readonly zh: "请设置设备名称以标识此设备。该名称在你的所有设备之间应保持唯一;未配置前无法启用此功能。"; - }; - readonly "Please set this device name": { - readonly def: "Please set this device name"; - readonly es: "Define el nombre de este dispositivo"; - readonly ja: "このデバイス名を設定してください"; - readonly ko: "이 장치 이름을 설정해 주세요"; - readonly ru: "Укажите имя этого устройства"; - readonly zh: "请设置此设备名称"; - readonly "zh-tw": "請設定此裝置名稱"; - }; - readonly "Plug-in version": { - readonly def: "Plug-in version"; - readonly ja: "プラグインバージョン"; - readonly ko: "플러그인 버전"; - readonly ru: "Версия плагина"; - readonly zh: "插件版本"; - readonly "zh-tw": "外掛版本"; - }; - readonly "Prepare the 'report' to create an issue": { - readonly def: "Prepare the 'report' to create an issue"; - readonly fr: "Préparer le « rapport » pour créer un ticket"; - readonly he: "הכן 'דו\"ח' ליצירת Issue"; - readonly ja: "Issue 作成用の「レポート」を準備"; - readonly ko: "이슈 생성을 위한 '보고서' 준비"; - readonly ru: "Подготовить «отчёт» для создания Issue"; - readonly zh: "准备 '报告' 以创建问题单"; - readonly "zh-tw": "準備建立 Issue 用的「報告」"; - }; - readonly Presets: { - readonly def: "Presets"; - readonly es: "Preconfiguraciones"; - readonly fr: "Préréglages"; - readonly he: "קביעות מראש"; - readonly ja: "プリセット"; - readonly ko: "프리셋"; - readonly ru: "Пресеты"; - readonly zh: "预设"; - }; - readonly "Proceed Garbage Collection": { - readonly def: "Proceed Garbage Collection"; - readonly ja: "Garbage Collection を続行"; - readonly ko: "Garbage Collection 계속"; - readonly ru: "Продолжить Garbage Collection"; - readonly zh: "继续执行垃圾回收"; - readonly "zh-tw": "繼續執行垃圾回收"; - }; - readonly "Proceed with Setup URI": { - readonly def: "Proceed with Setup URI"; - readonly es: "Continuar con el URI de configuración"; - readonly ja: "Setup URI で続行"; - readonly ko: "설정 URI로 계속"; - readonly ru: "Продолжить с Setup URI"; - readonly zh: "继续使用 Setup URI"; - readonly "zh-tw": "繼續使用 Setup URI"; - }; - readonly "Proceeding with Garbage Collection, ignoring missing nodes.": { - readonly def: "Proceeding with Garbage Collection, ignoring missing nodes."; - readonly ja: "不足しているノードを無視して Garbage Collection を続行します。"; - readonly ko: "누락된 노드를 무시하고 Garbage Collection을 계속 진행합니다."; - readonly ru: "Продолжаем Garbage Collection, игнорируя отсутствующие узлы."; - readonly zh: "正在继续执行垃圾回收,并忽略缺失节点。"; - readonly "zh-tw": "正在繼續執行垃圾回收,並忽略缺失節點。"; - }; - readonly "Proceeding with Garbage Collection.": { - readonly def: "Proceeding with Garbage Collection."; - readonly ja: "Garbage Collection を実行します。"; - readonly ko: "Garbage Collection을 진행합니다."; - readonly ru: "Запускаем Garbage Collection."; - readonly zh: "正在执行垃圾回收。"; - readonly "zh-tw": "正在執行垃圾回收。"; - }; - readonly "Process small files in the foreground": { - readonly def: "Process small files in the foreground"; - readonly es: "Procesar archivos pequeños en primer plano"; - readonly fr: "Traiter les petits fichiers au premier plan"; - readonly he: "עבד קבצים קטנים בחזית"; - readonly ja: "小さいファイルを最前面で処理"; - readonly ko: "포그라운드에서 작은 파일 처리"; - readonly ru: "Обрабатывать маленькие файлы в основном потоке"; - readonly zh: "在前台处理小文件"; - }; - readonly Progress: { - readonly def: "Progress"; - readonly ja: "進捗"; - readonly ko: "진행 상태"; - readonly ru: "Прогресс"; - readonly zh: "进度"; - readonly "zh-tw": "進度"; - }; - readonly "Property Encryption": { - readonly def: "Property Encryption"; - readonly fr: "Chiffrement des propriétés"; - readonly he: "הצפנת מאפיינים"; - readonly ru: "Шифрование свойств"; - readonly zh: "属性加密"; - }; - readonly "PureJS fallback (Fast, W/O WebAssembly)": { - readonly def: "PureJS fallback (Fast, W/O WebAssembly)"; - readonly es: "Alternativa PureJS (rápida, sin WebAssembly)"; - readonly ja: "PureJS フォールバック (高速、WebAssembly なし)"; - readonly ko: "PureJS 대체 방식 (빠름, WebAssembly 없음)"; - readonly ru: "Вариант PureJS (быстрый, без WebAssembly)"; - readonly zh: "PureJS 回退(快速,无 WebAssembly)"; - readonly "zh-tw": "PureJS 回退(快速,無 WebAssembly)"; - }; - readonly "Purge all download/upload cache.": { - readonly def: "Purge all download/upload cache."; - readonly es: "Purga toda la caché de descarga y carga."; - readonly ja: "ダウンロード/アップロードキャッシュをすべて削除します。"; - readonly ko: "모든 다운로드/업로드 캐시를 제거합니다."; - readonly ru: "Очистить весь кэш загрузки/выгрузки."; - readonly zh: "清除所有下载/上传缓存。"; - readonly "zh-tw": "清除所有下載/上傳快取。"; - }; - readonly "Purge all journal counter": { - readonly def: "Purge all journal counter"; - readonly es: "Purgar todos los contadores del diario"; - readonly ja: "すべてのジャーナルカウンターを削除"; - readonly ko: "모든 저널 카운터 삭제"; - readonly ru: "Очистить все счётчики журнала"; - readonly zh: "清除所有日志计数器"; - readonly "zh-tw": "清除所有日誌計數器"; - }; - readonly "Rebuild local and remote database with local files.": { - readonly def: "Rebuild local and remote database with local files."; - readonly es: "Reconstruye la base de datos local y remota usando los archivos locales."; - readonly ja: "ローカルファイルを使ってローカルとリモートのデータベースを再構築します。"; - readonly ko: "로컬 파일로 로컬 및 원격 데이터베이스를 다시 구축합니다."; - readonly ru: "Перестроить локальную и удалённую базы данных на основе локальных файлов."; - readonly zh: "使用本地文件重建本地和远端数据库。"; - readonly "zh-tw": "以本機檔案重建本機與遠端資料庫。"; - }; - readonly "Rebuilding Operations (Remote Only)": { - readonly def: "Rebuilding Operations (Remote Only)"; - readonly es: "Operaciones de reconstrucción (solo remoto)"; - readonly ja: "再構築操作 (リモートのみ)"; - readonly ko: "재구축 작업 (원격 전용)"; - readonly ru: "Операции перестроения (только удалённое хранилище)"; - readonly zh: "重建操作(仅远端)"; - readonly "zh-tw": "重建作業(僅遠端)"; - }; - readonly "Recovery and Repair": { - readonly def: "Recovery and Repair"; - readonly "zh-tw": "修復與修補"; - }; - readonly "Recreate all": { - readonly def: "Recreate all"; - readonly es: "Recrear todo"; - readonly ja: "すべて再作成"; - readonly ko: "모두 다시 생성"; - readonly ru: "Пересоздать всё"; - readonly zh: "全部重建"; - readonly "zh-tw": "全部重建"; - }; - readonly "Recreate missing chunks for all files": { - readonly def: "Recreate missing chunks for all files"; - readonly es: "Recrear fragmentos faltantes para todos los archivos"; - readonly ja: "すべてのファイルの不足チャンクを再作成"; - readonly ko: "모든 파일의 누락된 청크 다시 생성"; - readonly ru: "Пересоздать отсутствующие чанки для всех файлов"; - readonly zh: "为所有文件重建缺失的 chunks"; - readonly "zh-tw": "為所有檔案重建遺失的 chunks"; - }; - readonly "RedFlag.Fetch.Method.Desc": { - readonly def: "How do you want to fetch?\n- Create a local database once before fetching.\n **Low Traffic**, **High CPU**, **Low Risk**\n Recommended if ...\n - Files possibly inconsistent\n - Files were not so much\n- Create local file chunks before fetching.\n **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk**\n Recommended if ...\n - Files probably consistent\n - You have a lot of files.\n- Fetch everything from the remote.\n **High Traffic**, **Low CPU**, **Low to Moderate Risk**\n\n>[!INFO]- Details\n> ## Create a local database once before fetching.\n> **Low Traffic**, **High CPU**, **Low Risk**\n> This option first creates a local database using existing local files before fetching data from the remote source.\n> If matching files exist both locally and remotely, only the differences between them will be transferred.\n> However, files present in both locations will initially be handled as conflicted files. They will be resolved automatically if they are not actually conflicted, but this process may take time.\n> This is generally the safest method, minimizing data loss risk.\n> ## Create local file chunks before fetching.\n> **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk** (depending operation)\n> This option first creates chunks from local files for the database, then fetches data. Consequently, only chunks missing locally are transferred. However, all metadata is taken from the remote source.\n> Local files are then compared against this metadata at launch. The content considered newer will overwrite the older one (by modified time). This outcome is then synchronised back to the remote database.\n> This is generally safe if local files are genuinely the latest timestamp. However, it can cause problems if a file has a newer timestamp but older content (like the initial `welcome.md`).\n> This uses less CPU and faster than \"Create a local database once before fetching\", but it may lead to data loss if not used carefully.\n> ## Fetch everything from the remote.\n> **High Traffic**, **Low CPU**, **Low to Moderate Risk** (depending operation)\n> All things will be fetched from the remote.\n> Similar to the Create local file chunks before fetching, but all chunks are fetched from the remote source.\n> This is the most traditional way to fetch, typically consuming the most network traffic and time. It also carries a similar risk of overwriting remote files to the 'Create local file chunks before fetching' option.\n> However, it is often considered the most stable method because it is the longest-established and most straightforward approach."; - readonly fr: "Comment voulez-vous récupérer ?\n- Créer une base locale avant de récupérer.\n **Trafic faible**, **CPU élevé**, **Risque faible**\n Recommandé si ...\n - Fichiers possiblement incohérents\n - Fichiers peu nombreux\n- Créer des fragments de fichiers locaux avant de récupérer.\n **Trafic faible**, **CPU modéré**, **Risque faible à modéré**\n Recommandé si ...\n - Fichiers probablement cohérents\n - Vous avez beaucoup de fichiers.\n- Tout récupérer depuis le distant.\n **Trafic élevé**, **CPU faible**, **Risque faible à modéré**\n\n>[!INFO]- Détails\n> ## Créer une base locale avant de récupérer.\n> **Trafic faible**, **CPU élevé**, **Risque faible**\n> Cette option crée d'abord une base locale à partir des fichiers locaux existants avant de récupérer les données depuis la source distante.\n> Si des fichiers correspondants existent à la fois localement et à distance, seules les différences entre eux seront transférées.\n> Toutefois, les fichiers présents aux deux emplacements seront initialement traités comme en conflit. Ils seront résolus automatiquement s'ils ne le sont pas réellement, mais ce processus peut prendre du temps.\n> C'est généralement la méthode la plus sûre, minimisant le risque de perte de données.\n> ## Créer des fragments de fichiers locaux avant de récupérer.\n> **Trafic faible**, **CPU modéré**, **Risque faible à modéré** (selon l'opération)\n> Cette option crée d'abord des fragments à partir des fichiers locaux pour la base, puis récupère les données. Par conséquent, seuls les fragments manquants localement sont transférés. Cependant, toutes les métadonnées sont prises de la source distante.\n> Les fichiers locaux sont ensuite comparés à ces métadonnées au lancement. Le contenu considéré comme plus récent écrasera le plus ancien (selon la date de modification). Le résultat est ensuite synchronisé vers la base distante.\n> C'est généralement sûr si les fichiers locaux ont bien l'horodatage le plus récent. Cela peut toutefois poser problème si un fichier a un horodatage plus récent mais un contenu plus ancien (comme le `welcome.md` initial).\n> Cette méthode utilise moins de CPU et est plus rapide que « Créer une base locale avant de récupérer », mais peut entraîner une perte de données si elle n'est pas utilisée avec précaution.\n> ## Tout récupérer depuis le distant.\n> **Trafic élevé**, **CPU faible**, **Risque faible à modéré** (selon l'opération)\n> Tout sera récupéré depuis le distant.\n> Similaire à Créer des fragments de fichiers locaux avant de récupérer, mais tous les fragments sont récupérés depuis la source distante.\n> C'est la façon la plus traditionnelle de récupérer, consommant généralement le plus de trafic réseau et de temps. Elle comporte également un risque similaire d'écraser les fichiers distants à l'option « Créer des fragments de fichiers locaux avant de récupérer ».\n> Elle est toutefois souvent considérée comme la méthode la plus stable car c'est la plus ancienne et la plus directe."; - readonly he: "כיצד ברצונך למשוך?\n- %{RedFlag.Fetch.Method.FetchSafer}.\n **תעבורה נמוכה**, **מעבד גבוה**, **סיכון נמוך**\n מומלץ אם...\n - קבצים עשויים להיות לא עקביים\n - אין הרבה קבצים\n- %{RedFlag.Fetch.Method.FetchSmoother}.\n **תעבורה נמוכה**, **מעבד בינוני**, **סיכון נמוך עד בינוני**\n מומלץ אם...\n - הקבצים ככל הנראה עקביים\n - יש לך הרבה קבצים.\n- %{RedFlag.Fetch.Method.FetchTraditional}.\n **תעבורה גבוהה**, **מעבד נמוך**, **סיכון נמוך עד בינוני**\n\n>[!INFO]- פרטים\n> ## %{RedFlag.Fetch.Method.FetchSafer}.\n> **תעבורה נמוכה**, **מעבד גבוה**, **סיכון נמוך**\n> אפשרות זו יוצרת תחילה מסד נתונים מקומי תוך שימוש בקבצים מקומיים קיימים לפני משיכת נתונים מהמקור המרוחד.\n> אם קיימים קבצים תואמים גם מקומית וגם מרחוק, רק ההפרשים ביניהם יועברו.\n> עם זאת, קבצים הקיימים בשני המקומות יטופלו תחילה כקבצים מתנגשים. הם ייפתרו אוטומטית אם לא מתנגשים בפועל, אך תהליך זה עשוי לקחת זמן.\n> זוהי בדרך כלל השיטה הבטוחה ביותר, ממזערת סיכון לאובדן נתונים.\n> ## %{RedFlag.Fetch.Method.FetchSmoother}.\n> **תעבורה נמוכה**, **מעבד בינוני**, **סיכון נמוך עד בינוני** (תלוי בפעולה)\n> אפשרות זו יוצרת תחילה נתחים מקבצים מקומיים למסד הנתונים, ואז מושכת נתונים. כתוצאה מכך, רק נתחים חסרים מקומית מועברים. עם זאת, כל המטה-נתונים נלקחים מהמקור המרוחד.\n> קבצים מקומיים נבדקים לאחר מכן מול מטה-נתונים אלה בעת ההפעלה. התוכן שנחשב חדש יותר ידרוס את הישן יותר (לפי זמן שינוי).\n> בדרך כלל בטוח אם הקבצים המקומיים הם אכן חדשים ביותר. עם זאת, עלול לגרום לבעיות אם לקובץ יש חותמת זמן חדשה יותר אך תוכן ישן יותר (כמו `welcome.md` ראשוני).\n> שיטה זו משתמשת בפחות מעבד ומהירה יותר מ-\"%{RedFlag.Fetch.Method.FetchSafer}\", אך עלולה להוביל לאובדן נתונים אם לא משתמשים בה בזהירות.\n> ## %{RedFlag.Fetch.Method.FetchTraditional}.\n> **תעבורה גבוהה**, **מעבד נמוך**, **סיכון נמוך עד בינוני** (תלוי בפעולה)\n> הכל יימשך מהשרת המרוחד.\n> דומה ל-%{RedFlag.Fetch.Method.FetchSmoother}, אך כל הנתחים נמשכים מהמקור המרוחד.\n> זוהי הדרך המסורתית ביותר למשיכה, צורכת בדרך כלל את רוב תעבורת הרשת והזמן.\n> עם זאת, היא נחשבת לעתים קרובות לשיטה היציבה ביותר מכיוון שהיא הוותיקה והישירה ביותר."; - readonly ja: "どのようにフェッチしますか?\n- フェッチ前にローカルデータベースを作成\n **低トラフィック**, **高CPU負荷**, **低リスク**\n 推奨条件...\n - ファイルの整合性に不安がある\n - ファイル数がそれほど多くない\n- フェッチ前にローカルファイルチャンクを作成\n **低トラフィック**, **中程CPU負荷**, **低~中リスク**\n 推奨条件...\n - ファイルがおそらく整合している\n - ファイル数が多い\n- リモートからすべてをフェッチ\n **高トラフィック**, **低CPU負荷**, **低~中リスク**\n\n>[!INFO]- 詳細\n> ## フェッチ前にローカルデータベースを作成\n> **低トラフィック**, **高CPU負荷**, **低リスク**\n> このオプションは、リモートからデータをフェッチする前に、既存のローカルファイルを使用してローカルデータベースを作成します。\n> ローカルとリモートの両方に一致するファイルがある場合、差分のみが転送されます。\n> ただし、両方の場所に存在するファイルは最初は競合ファイルとして処理されます。実際に競合していなければ自動的に解決されますが、この処理には時間がかかる場合があります。\n> これは一般的に最も安全な方法で、データ損失のリスクを最小限に抑えます。\n> ## フェッチ前にローカルファイルチャンクを作成\n> **低トラフィック**, **中程CPU負荷**, **低~中リスク**(操作による)\n> このオプションは、最初にローカルファイルからデータベース用のチャンクを作成し、その後データをフェッチします。そのため、ローカルにないチャンクのみが転送されます。ただし、すべてのメタデータはリモートから取得されます。\n> ローカルファイルは起動時にこのメタデータと比較されます。新しいと判断されたコンテンツ(更新日時による)が古いものを上書きします。この結果はリモートデータベースに同期されます。\n> ローカルファイルが本当に最新のタイムスタンプであれば一般的に安全です。ただし、ファイルのタイムスタンプが新しくてもコンテンツが古い場合(初期の`welcome.md`など)は問題が発生する可能性があります。\n> これは\"フェッチ前にローカルデータベースを作成\"よりCPU使用量が少なく高速ですが、注意しないとデータ損失につながる可能性があります。\n> ## リモートからすべてをフェッチ\n> **高トラフィック**, **低CPU負荷**, **低~中リスク**(操作による)\n> すべてのデータがリモートからフェッチされます。\n> フェッチ前にローカルファイルチャンクを作成と似ていますが、すべてのチャンクがリモートからフェッチされます。\n> これは最も従来のフェッチ方法で、通常最もネットワークトラフィックと時間を消費します。'フェッチ前にローカルファイルチャンクを作成'オプションと同様のリモートファイル上書きのリスクがあります。\n> ただし、最も歴史があり簡単なアプローチであるため、最も安定した方法と見なされることが多いです。"; - readonly ko: "어떻게 가져오시겠습니까?\n- 가져오기 전에 로컬 데이터베이스를 한 번 생성. (권장)\n **낮은 트래픽**, **높은 CPU**, **낮은 위험**\n- 가져오기 전에 로컬 파일 청크 생성.\n **낮은 트래픽**, **보통 CPU**, **낮음에서 보통 위험**\n- 원격에서 모든 것 가져오기.\n **높은 트래픽**, **낮은 CPU**, **낮음에서 보통 위험**\n\n>[!INFO]- 세부 사항\n> ## 가져오기 전에 로컬 데이터베이스를 한 번 생성. (권장)\n> **낮은 트래픽**, **높은 CPU**, **낮은 위험**\n> 이 옵션은 원격 소스에서 데이터를 가져오기 전에 기존 로컬 파일을 사용하여 로컬 데이터베이스를 먼저 생성합니다.\n> 로컬과 원격 모두에 일치하는 파일이 있으면 둘 사이의 차이점만 전송됩니다.\n> 하지만 두 위치 모두에 있는 파일은 초기에 충돌 파일로 처리됩니다. 실제로 충돌하지 않는다면 자동으로 해결되지만 이 과정은 시간이 걸릴 수 있습니다.\n> 이는 일반적으로 가장 안전한 방법으로 데이터 손실 위험을 최소화합니다.\n> ## 가져오기 전에 로컬 파일 청크 생성.\n> **낮은 트래픽**, **보통 CPU**, **낮음에서 보통 위험** (작업에 따라)\n> 이 옵션은 먼저 로컬 파일에서 데이터베이스용 청크를 생성한 다음 데이터를 가져옵니다. 따라서 로컬에 없는 청크만 전송됩니다. 하지만 모든 메타데이터는 원격 소스에서 가져옵니다.\n> 그런 다음 로컬 파일이 시작 시 이 메타데이터와 비교됩니다. 더 새로운 것으로 간주되는 콘텐츠가 오래된 것을 덮어씁니다(수정 시간 기준). 이 결과는 원격 데이터베이스에 다시 동기화됩니다.\n> 로컬 파일이 실제로 최신 타임스탬프라면 일반적으로 안전합니다. 하지만 파일이 더 새로운 타임스탬프를 가지고 있지만 더 오래된 콘텐츠를 가지고 있다면(초기 `welcome.md`처럼) 문제가 발생할 수 있습니다.\n> 이는 \"가져오기 전에 로컬 데이터베이스를 한 번 생성\"보다 CPU를 덜 사용하고 더 빠르지만 주의 깊게 사용하지 않으면 데이터 손실로 이어질 수 있습니다.\n> ## 원격에서 모든 것 가져오기.\n> **높은 트래픽**, **낮은 CPU**, **낮음에서 보통 위험** (작업에 따라)\n> 모든 것이 원격에서 가져와집니다.\n> 가져오기 전에 로컬 파일 청크 생성와 유사하지만 모든 청크가 원격 소스에서 가져와집니다.\n> 이는 가장 전통적인 가져오기 방법으로 일반적으로 가장 많은 네트워크 트래픽과 시간을 소모합니다. 또한 '가져오기 전에 로컬 파일 청크 생성' 옵션과 유사하게 원격 파일을 덮어쓸 위험이 있습니다.\n> 하지만 가장 오래되고 가장 직접적인 접근 방식이기 때문에 종종 가장 안정적인 방법으로 간주됩니다."; - readonly ru: "Как вы хотите загрузить?"; - readonly zh: "How do you want to fetch?\n- Create a local database once before fetching.\n **Low Traffic**, **High CPU**, **Low Risk**\n Recommended if ...\n - Files possibly inconsistent\n - Files were not so much\n- Create local file chunks before fetching.\n **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk**\n Recommended if ...\n - Files probably consistent\n - You have a lot of files.\n- Fetch everything from the remote.\n **High Traffic**, **Low CPU**, **Low to Moderate Risk**\n\n>[!INFO]- Details\n> ## Create a local database once before fetching.\n> **Low Traffic**, **High CPU**, **Low Risk**\n> This option first creates a local database using existing local files before fetching data from the remote source.\n> If matching files exist both locally and remotely, only the differences between them will be transferred.\n> However, files present in both locations will initially be handled as conflicted files. They will be resolved automatically if they are not actually conflicted, but this process may take time.\n> This is generally the safest method, minimizing data loss risk.\n> ## Create local file chunks before fetching.\n> **Low Traffic**, **Moderate CPU**, **Low to Moderate Risk** (depending operation)\n> This option first creates chunks from local files for the database, then fetches data. Consequently, only chunks missing locally are transferred. However, all metadata is taken from the remote source.\n> Local files are then compared against this metadata at launch. The content considered newer will overwrite the older one (by modified time). This outcome is then synchronised back to the remote database.\n> This is generally safe if local files are genuinely the latest timestamp. However, it can cause problems if a file has a newer timestamp but older content (like the initial `welcome.md`).\n> This uses less CPU and faster than \"Create a local database once before fetching\", but it may lead to data loss if not used carefully.\n> ## Fetch everything from the remote.\n> **High Traffic**, **Low CPU**, **Low to Moderate Risk** (depending operation)\n> All things will be fetched from the remote.\n> Similar to the Create local file chunks before fetching, but all chunks are fetched from the remote source.\n> This is the most traditional way to fetch, typically consuming the most network traffic and time. It also carries a similar risk of overwriting remote files to the 'Create local file chunks before fetching' option.\n> However, it is often considered the most stable method because it is the longest-established and most straightforward approach."; - }; - readonly "RedFlag.Fetch.Method.FetchSafer": { - readonly def: "Create a local database once before fetching"; - readonly fr: "Créer une base locale avant de récupérer"; - readonly he: "צור מסד נתונים מקומי לפני המשיכה"; - readonly ja: "フェッチ前にローカルデータベースを作成"; - readonly ko: "가져오기 전에 로컬 데이터베이스를 한 번 생성"; - readonly ru: "Создать локальную базу данных перед загрузкой"; - readonly zh: "Create a local database once before fetching"; - }; - readonly "RedFlag.Fetch.Method.FetchSmoother": { - readonly def: "Create local file chunks before fetching"; - readonly fr: "Créer des fragments de fichiers locaux avant de récupérer"; - readonly he: "צור נתחי קבצים מקומיים לפני המשיכה"; - readonly ja: "フェッチ前にローカルファイルチャンクを作成"; - readonly ko: "가져오기 전에 로컬 파일 청크 생성"; - readonly ru: "Создать локальные чанки перед загрузкой"; - readonly zh: "Create local file chunks before fetching"; - }; - readonly "RedFlag.Fetch.Method.FetchTraditional": { - readonly def: "Fetch everything from the remote"; - readonly fr: "Tout récupérer depuis le distant"; - readonly he: "משוך הכל מהשרת המרוחד"; - readonly ja: "リモートからすべてをフェッチ"; - readonly ko: "원격에서 모든 것 가져오기"; - readonly ru: "Загрузить всё с удалённого"; - readonly zh: "Fetch everything from the remote"; - }; - readonly "RedFlag.Fetch.Method.Title": { - readonly def: "How do you want to fetch?"; - readonly fr: "Comment voulez-vous récupérer ?"; - readonly he: "כיצד ברצונך למשוך?"; - readonly ja: "どのようにフェッチしますか?"; - readonly ko: "어떻게 가져오시겠습니까?"; - readonly ru: "Как вы хотите загрузить?"; - readonly zh: "How do you want to fetch?"; - }; - readonly "RedFlag.FetchRemoteConfig.Buttons.Cancel": { - readonly def: "No, use local settings"; - readonly fr: "Non, utiliser les paramètres locaux"; - readonly he: "לא, השתמש בהגדרות המקומיות"; - readonly ja: "いいえ、ローカル設定を使用"; - readonly ru: "Нет, использовать локальные настройки"; - readonly zh: "No, use local settings"; - }; - readonly "RedFlag.FetchRemoteConfig.Buttons.Fetch": { - readonly def: "Yes, fetch and apply remote settings"; - readonly fr: "Oui, récupérer et appliquer les paramètres distants"; - readonly he: "כן, משוך והחל הגדרות מרוחקות"; - readonly ja: "はい、リモート設定を取得して適用"; - readonly ru: "Да, загрузить и применить удалённые настройки"; - readonly zh: "Yes, fetch and apply remote settings"; - }; - readonly "RedFlag.FetchRemoteConfig.Message": { - readonly def: "Do you want to fetch and apply remotely stored preference settings to the device?"; - readonly fr: "Voulez-vous récupérer et appliquer les préférences stockées à distance sur cet appareil ?"; - readonly he: "האם ברצונך למשוך ולהחיל הגדרות שמורות מרחוק על מכשיר זה?"; - readonly ja: "リモートに保存された設定を取得して、このデバイスに適用しますか?"; - readonly ru: "Вы хотите загрузить и применить удалённые настройки?"; - readonly zh: "Do you want to fetch and apply remotely stored preference settings to the device?"; - }; - readonly "RedFlag.FetchRemoteConfig.Title": { - readonly def: "Fetch Remote Configuration"; - readonly fr: "Récupérer la configuration distante"; - readonly he: "משוך תצורה מרוחקת"; - readonly ja: "リモート設定の取得"; - readonly ru: "Загрузить удалённую конфигурацию"; - readonly zh: "Fetch Remote Configuration"; - }; - readonly "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": { - readonly def: "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."; - readonly ja: "最新版以外のすべてのリビジョンを破棄して、使用容量を削減します。実行には、リモートサーバーとローカルクライアントの両方に同程度の空き容量が必要です。"; - readonly ko: "최신 버전이 아닌 모든 리비전을 제거하여 저장 공간을 줄입니다. 이 작업을 수행하려면 원격 서버와 로컬 클라이언트에 동일한 양의 여유 공간이 필요합니다."; - readonly zh: "通过丢弃所有非最新版本来减少存储空间。这需要远程服务器和本地客户端具备相同数量的可用空间。"; - readonly "zh-tw": "透過捨棄所有非最新版本來減少儲存空間占用。執行此操作時,遠端伺服器與本機用戶端都需要具備相同數量的可用空間。"; - }; - readonly "Reducing the frequency with which on-disk changes are reflected into the DB": { - readonly def: "Reducing the frequency with which on-disk changes are reflected into the DB"; - readonly es: "Reducir frecuencia de actualizaciones de disco a BD"; - readonly fr: "Réduire la fréquence à laquelle les modifications sur disque sont reflétées dans la base"; - readonly he: "הפחת את תדירות השתקפות שינויים בדיסק למסד הנתונים"; - readonly ja: "ローカルでの変更がデータベースに反映される頻度を下げる(所定の回数まとめて同期する、逐一反映しない)"; - readonly ko: "디스크 변경 사항이 데이터베이스에 반영되는 빈도를 줄입니다"; - readonly ru: "Уменьшение частоты отражения изменений с диска в БД"; - readonly zh: "降低将磁盘上的更改反映到数据库中的频率"; - }; - readonly Region: { - readonly def: "Region"; - readonly es: "Región"; - readonly fr: "Région"; - readonly he: "אזור"; - readonly ja: "リージョン"; - readonly ko: "지역"; - readonly ru: "Регион"; - readonly zh: "区域"; - }; - readonly Remediation: { - readonly def: "Remediation"; - readonly es: "Remediación"; - readonly ja: "是正"; - readonly ko: "복구 조치"; - readonly ru: "Исправление"; - readonly zh: "修复设置"; - readonly "zh-tw": "修復設定"; - }; - readonly "Remediation Setting Changed": { - readonly def: "Remediation Setting Changed"; - readonly es: "La configuración de remediación cambió"; - readonly ja: "是正設定が変更されました"; - readonly ko: "복구 설정이 변경됨"; - readonly ru: "Настройки исправления изменены"; - readonly zh: "修复设置已更改"; - readonly "zh-tw": "修復設定已變更"; - }; - readonly "Remote Database Tweak (In sunset)": { - readonly def: "Remote Database Tweak (In sunset)"; - readonly es: "Ajustes de base de datos remota (en retirada)"; - readonly ja: "リモートデータベースの調整 (廃止予定)"; - readonly ko: "원격 데이터베이스 조정 (폐기 예정)"; - readonly ru: "Настройки удалённой базы данных (устаревает)"; - readonly zh: "远端数据库调优(逐步淘汰)"; - readonly "zh-tw": "遠端資料庫調校(即將淘汰)"; - }; - readonly "Remote Databases": { - readonly def: "Remote Databases"; - readonly es: "Bases de datos remotas"; - readonly ja: "リモートデータベース"; - readonly ko: "원격 데이터베이스"; - readonly ru: "Удалённые базы данных"; - readonly zh: "远端数据库"; - readonly "zh-tw": "遠端資料庫"; - }; - readonly "Remote name": { - readonly def: "Remote name"; - readonly es: "Nombre del remoto"; - readonly ja: "リモート名"; - readonly ko: "원격 이름"; - readonly ru: "Имя удалённого хранилища"; - readonly zh: "远端名称"; - readonly "zh-tw": "遠端名稱"; - }; - readonly "Remote server type": { - readonly def: "Remote server type"; - readonly es: "Tipo de servidor remoto"; - readonly fr: "Type de serveur distant"; - readonly he: "סוג שרת מרוחד"; - readonly ja: "リモートの種別"; - readonly ko: "원격 서버 유형"; - readonly ru: "Тип удалённого сервера"; - readonly zh: "远程服务器类型"; - }; - readonly "Remote Type": { - readonly def: "Remote Type"; - readonly es: "Tipo de remoto"; - readonly fr: "Type de distant"; - readonly he: "סוג מרוחד"; - readonly ja: "同期方式"; - readonly ko: "원격 유형"; - readonly ru: "Удалённый тип"; - readonly zh: "远程类型"; - }; - readonly Rename: { - readonly def: "Rename"; - readonly es: "Renombrar"; - readonly ja: "名前を変更"; - readonly ko: "이름 바꾸기"; - readonly ru: "Переименовать"; - readonly zh: "重命名"; - readonly "zh-tw": "重新命名"; - }; - readonly "Replicator.Dialogue.Locked.Action.Dismiss": { - readonly def: "Cancel for reconfirmation"; - readonly fr: "Annuler pour reconfirmer"; - readonly he: "ביטול לאישור מחדש"; - readonly ja: "再確認のためキャンセル"; - readonly ko: "재확인을 위해 취소"; - readonly ru: "Отмена для подтверждения"; - readonly zh: "Cancel for reconfirmation"; - }; - readonly "Replicator.Dialogue.Locked.Action.Fetch": { - readonly def: "Reset Synchronisation on This Device"; - readonly fr: "Réinitialiser la synchronisation sur cet appareil"; - readonly he: "אפס סנכרון במכשיר זה"; - readonly ja: "このデバイスの同期をリセット"; - readonly ko: "원격 데이터베이스에서 모든 것을 다시 가져오기"; - readonly ru: "Сбросить синхронизацию на этом устройстве"; - readonly zh: "Reset Synchronisation on This Device"; - }; - readonly "Replicator.Dialogue.Locked.Action.Unlock": { - readonly def: "Unlock the remote database"; - readonly fr: "Déverrouiller la base distante"; - readonly he: "בטל נעילת מסד הנתונים המרוחד"; - readonly ja: "リモートデータベースのロックを解除"; - readonly ko: "원격 데이터베이스 잠금 해제"; - readonly ru: "Разблокировать удалённую базу данных"; - readonly zh: "Unlock the remote database"; - }; - readonly "Replicator.Dialogue.Locked.Message": { - readonly def: "Remote database is locked. This is due to a rebuild on one of the terminals.\nThe device is therefore asked to withhold the connection to avoid database corruption.\n\nThere are three options that we can do:\n\n- Reset Synchronisation on This Device\n The most preferred and reliable way. This will dispose the local database once, and reset all synchronisation information from the remote database again, In most case, we can perform this safely. However, it takes some time and should be done in stable network.\n- Unlock the remote database\n This method can only be used if we are already reliably synchronised by other replication methods. This does not simply mean that we have the same files. If you are not sure, you should avoid it.\n- Cancel for reconfirmation\n This will cancel the operation. And we will asked again on next request.\n"; - readonly fr: "La base distante est verrouillée. Ceci est dû à une reconstruction sur l'un des terminaux.\nL'appareil est donc prié de suspendre la connexion pour éviter la corruption de la base.\n\nTrois options sont possibles :\n\n- Réinitialiser la synchronisation sur cet appareil\n La méthode la plus recommandée et fiable. Elle supprime la base locale puis réinitialise toutes les informations de synchronisation depuis la base distante. Dans la plupart des cas, c'est sûr. Cela prend cependant du temps et devrait se faire sur un réseau stable.\n- Déverrouiller la base distante\n Cette méthode ne peut être utilisée que si nous sommes déjà synchronisés de manière fiable par d'autres méthodes de réplication. Cela ne signifie pas simplement que nous avons les mêmes fichiers. Dans le doute, évitez.\n- Annuler pour reconfirmer\n Ceci annule l'opération. Vous serez à nouveau interrogé à la prochaine requête.\n"; - readonly he: "מסד הנתונים המרוחד נעול. הסיבה היא בנייה מחדש באחד הטרמינלים.\nלכן המכשיר מתבקש להמנע מחיבור כדי למנוע פגיעה במסד הנתונים.\n\nקיימות שלוש אפשרויות:\n\n- %{Replicator.Dialogue.Locked.Action.Fetch}\n הדרך המועדפת והאמינה ביותר. פעולה זו תמחק את מסד הנתונים המקומי פעם,\n ותאפס את כל מידע הסנכרון ממסד הנתונים המרוחד מחדש. ברוב המקרים ניתן\n לעשות זאת בבטחה. עם זאת, דורשת זמן ויש לבצע ברשת יציבה.\n- %{Replicator.Dialogue.Locked.Action.Unlock}\n ניתן להשתמש בשיטה זו רק אם כבר מסונכרנים באופן אמין בשיטות שכפול\n אחרות. פשוט לא מספיק שיש אותם קבצים. אם אינך בטוח, הימנע מכך.\n- %{Replicator.Dialogue.Locked.Action.Dismiss}\n פעולה זו תבטל את הפעולה. תתבקש שוב בבקשה הבאה.\n"; - readonly ja: "リモートデータベースがロックされています。これはいずれかの端末での再構築が原因です。\nデータベースの破損を避けるため、このデバイスは接続を保留するよう求められています。\n\n3つのオプションがあります:\n\n- このデバイスの同期をリセット\n 最も推奨される信頼性の高い方法です。ローカルデータベースを一度破棄し、リモートデータベースからすべての同期情報を再取得します。ほとんどの場合、これは安全に実行できます。ただし、時間がかかり、安定したネットワークで実行する必要があります。\n- リモートデータベースのロックを解除\n この方法は、他のレプリケーション(複製)方法ですでに確実に同期されている場合のみ使用できます。単に同じファイルがあるという意味ではありません。確信がない場合は避けてください。\n- 再確認のためキャンセル\n 操作をキャンセルします。次回のリクエスト時に再度確認されます。\n"; - readonly ko: "원격 데이터베이스가 잠겨 있습니다. 이는 일부 터미널에서 데이터베이스를 재구축했기 때문입니다.\n따라서 현재 기기는 데이터베이스 손상을 방지하기 위해 연결을 일시적으로 보류해야 합니다.\n\n선택할 수 있는 세 가지 방법이 있습니다:\n\n- 원격 데이터베이스에서 모든 것을 다시 가져오기\n 가장 권장되고 신뢰할 수 있는 방법입니다. 로컬 데이터베이스를 초기화한 뒤, 원격 데이터베이스의 전체 데이터를 다시 가져옵니다. 대부분의 경우 안전하게 수행할 수 있으나, 시간이 다소 걸리며 안정적인 네트워크 환경에서 진행해야 합니다.\n- 원격 데이터베이스 잠금 해제\n 이 방법은 다른 동기화 방식으로 이미 완전하고 안정적으로 동기화된 경우에만 사용할 수 있습니다. 단순히 파일이 같다는 의미가 아니므로, 확신이 없다면 사용을 피하는 것이 좋습니다.\n- 재확인을 위해 취소\n 이번 작업을 취소하고, 다음 요청 시 다시 안내받습니다.\n"; - readonly ru: "Удалённая база данных заблокирована. Это связано с перестроением на одном из устройств."; - readonly zh: "Remote database is locked. This is due to a rebuild on one of the terminals.\nThe device is therefore asked to withhold the connection to avoid database corruption.\n\nThere are three options that we can do:\n\n- Reset Synchronisation on This Device\n The most preferred and reliable way. This will dispose the local database once, and fetch all from the remote database again, In most case, we can perform this safely. However, it takes some time and should be done in stable network.\n- Unlock the remote database\n This method can only be used if we are already reliably synchronised by other replication methods. This does not simply mean that we have the same files. If you are not sure, you should avoid it.\n- Cancel for reconfirmation\n This will cancel the operation. And we will asked again on next request.\n"; - }; - readonly "Replicator.Dialogue.Locked.Message.Fetch": { - readonly def: "Fetch all has been scheduled. Plug-in will be restarted to perform it."; - readonly fr: "Tout récupérer a été planifié. Le plug-in sera redémarré pour l'exécuter."; - readonly he: "משיכה מלאה תוזמנה. הפלאגין יופעל מחדש לביצועה."; - readonly ja: "全フェッチがスケジュールされました。プラグインは実行のために再起動されます。"; - readonly ko: "모든 것 가져오기가 예약되었습니다. 이를 수행하기 위해 플러그인이 재시작됩니다."; - readonly ru: "Загрузка всего запланирована. Плагин будет перезапущен."; - readonly zh: "Fetch all has been scheduled. Plug-in will be restarted to perform it."; - }; - readonly "Replicator.Dialogue.Locked.Message.Unlocked": { - readonly def: "The remote database has been unlocked. Please retry the operation."; - readonly fr: "La base distante a été déverrouillée. Veuillez réessayer l'opération."; - readonly he: "מסד הנתונים המרוחד בוטל נעילתו. אנא נסה שוב את הפעולה."; - readonly ja: "リモートデータベースのロックが解除されました。操作を再試行してください。"; - readonly ko: "원격 데이터베이스 잠금이 해제되었습니다. 작업을 다시 시도해 주세요."; - readonly ru: "Удалённая база данных разблокирована. Повторите операцию."; - readonly zh: "The remote database has been unlocked. Please retry the operation."; - }; - readonly "Replicator.Dialogue.Locked.Title": { - readonly def: "Locked"; - readonly fr: "Verrouillée"; - readonly he: "נעול"; - readonly ja: "ロック中"; - readonly ko: "잠김"; - readonly ru: "Заблокировано"; - readonly zh: "Locked"; - }; - readonly "Replicator.Message.Cleaned": { - readonly def: "Database cleaning up is in process. replication has been cancelled"; - readonly fr: "Nettoyage de la base en cours. La réplication a été annulée"; - readonly he: "ניקוי מסד הנתונים בתהליך. השכפול בוטל"; - readonly ja: "データベースのクリーナップ中です。レプリケーション(複製)はキャンセルされました。"; - readonly ko: "데이터베이스 정리가 진행 중입니다. 복제가 취소되었습니다"; - readonly ru: "Очистка базы данных в процессе. Репликация отменена"; - readonly zh: "Database cleaning up is in process. replication has been cancelled"; - }; - readonly "Replicator.Message.InitialiseFatalError": { - readonly def: "No replicator is available, this is the fatal error."; - readonly fr: "Aucun réplicateur disponible, il s'agit d'une erreur fatale."; - readonly he: "אין רפליקטור זמין, זוהי שגיאה קריטית."; - readonly ja: "レプリケーターが利用できません。これは致命的なエラーです。"; - readonly ko: "사용 가능한 복제기가 없습니다. 치명적인 오류입니다."; - readonly ru: "Репликатор недоступен, это фатальная ошибка."; - readonly zh: "No replicator is available, this is the fatal error."; - }; - readonly "Replicator.Message.Pending": { - readonly def: "Some file events are pending. Replication has been cancelled."; - readonly fr: "Des événements de fichier sont en attente. La réplication a été annulée."; - readonly he: "חלק מאירועי הקבצים ממתינים. השכפול בוטל."; - readonly ja: "ファイルイベントが保留中です。レプリケーション(複製)はキャンセルされました。"; - readonly ko: "일부 파일 이벤트가 대기 중입니다. 복제가 취소되었습니다."; - readonly ru: "Некоторые события файлов ожидают. Репликация отменена."; - readonly zh: "Some file events are pending. Replication has been cancelled."; - }; - readonly "Replicator.Message.SomeModuleFailed": { - readonly def: "Replication has been cancelled by some module failure"; - readonly fr: "La réplication a été annulée suite à l'échec d'un module"; - readonly he: "השכפול בוטל בשל כשל במודול"; - readonly ja: "一部のモジュールの失敗によりレプリケーション(複製)がキャンセルされました。"; - readonly ko: "일부 모듈 실패로 복제가 취소되었습니다"; - readonly ru: "Репликация отменена из-за сбоя модуля"; - readonly zh: "Replication has been cancelled by some module failure"; - }; - readonly "Replicator.Message.VersionUpFlash": { - readonly def: "An update has been detected. Please open the Settings dialogue and check the Change Log. Replication has been cancelled."; - readonly fr: "Une mise à jour a été détectée. Veuillez ouvrir la boîte de dialogue des paramètres et consulter le journal des modifications. La réplication a été annulée."; - readonly he: "זוהה עדכון. אנא פתח את דיאלוג ההגדרות ובדוק את יומן השינויים. השכפול בוטל."; - readonly ja: "更新が検出されました。設定ダイアログを開いて変更ログを確認してください。レプリケーション(複製)はキャンセルされました。"; - readonly ko: "설정을 열고 메시지를 확인해 주세요. 복제가 취소되었습니다."; - readonly ru: "Обновление обнаружено. Откройте настройки и проверьте историю изменений."; - readonly zh: "An update has been detected. Please open the Settings dialogue and check the Change Log. Replication has been cancelled."; - }; - readonly "Requires restart of Obsidian": { - readonly def: "Requires restart of Obsidian"; - readonly es: "Requiere reiniciar Obsidian"; - readonly fr: "Nécessite un redémarrage d'Obsidian"; - readonly he: "דורש הפעלה מחדש של Obsidian"; - readonly ja: "Obsidianの再起動が必要です"; - readonly ko: "Obsidian 재시작 필요"; - readonly ru: "Требуется перезапуск Obsidian"; - readonly zh: "需要重启 Obsidian"; - }; - readonly "Requires restart of Obsidian.": { - readonly def: "Requires restart of Obsidian."; - readonly es: "Requiere reiniciar Obsidian"; - readonly fr: "Nécessite un redémarrage d'Obsidian."; - readonly he: "דורש הפעלה מחדש של Obsidian."; - readonly ja: "Obsidianの再起動が必要です。"; - readonly ko: "Obsidian 재시작이 필요합니다."; - readonly ru: "Требуется перезапуск Obsidian."; - readonly zh: "需要重启 Obsidian "; - }; - readonly "Rerun Onboarding Wizard": { - readonly def: "Rerun Onboarding Wizard"; - readonly fr: "Relancer l'assistant d'intégration"; - readonly he: "הרץ שוב את אשף ההכוונה"; - readonly ja: "オンボーディングウィザードを再実行"; - readonly ko: "온보딩 마법사 다시 실행"; - readonly ru: "Перезапустить мастер настройки"; - readonly zh: "重新运行引导向导"; - readonly "zh-tw": "重新執行導覽精靈"; - }; - readonly "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": { - readonly def: "Rerun the onboarding wizard to set up Self-hosted LiveSync again."; - readonly fr: "Relancer l'assistant d'intégration pour reconfigurer Self-hosted LiveSync."; - readonly he: "הרץ שוב את אשף ההכוונה להגדרת Self-hosted LiveSync מחדש."; - readonly ja: "オンボーディングウィザードを再実行して、Self-hosted LiveSync をもう一度設定します。"; - readonly ko: "온보딩 마법사를 다시 실행하여 Self-hosted LiveSync를 다시 설정합니다."; - readonly ru: "Перезапустить мастер настройки для повторной настройки Self-hosted LiveSync."; - readonly zh: "重新运行引导向导以再次设置 Self-hosted LiveSync。"; - readonly "zh-tw": "重新執行導覽精靈以再次設定 Self-hosted LiveSync。"; - }; - readonly "Rerun Wizard": { - readonly def: "Rerun Wizard"; - readonly fr: "Relancer l'assistant"; - readonly he: "הרץ שוב את האשף"; - readonly ja: "ウィザードを再実行"; - readonly ko: "마법사 다시 실행"; - readonly ru: "Перезапустить мастер"; - readonly zh: "重新运行向导"; - readonly "zh-tw": "重新執行精靈"; - }; - readonly Resend: { - readonly def: "Resend"; - readonly es: "Reenviar"; - readonly ja: "再送信"; - readonly ko: "다시 보내기"; - readonly ru: "Повторно отправить"; - readonly zh: "重新发送"; - readonly "zh-tw": "重新傳送"; - }; - readonly "Resend all chunks to the remote.": { - readonly def: "Resend all chunks to the remote."; - readonly es: "Reenvía todos los chunks al remoto."; - readonly ja: "すべてのチャンクをリモートへ再送信します。"; - readonly ko: "모든 청크를 원격으로 다시 보냅니다."; - readonly ru: "Повторно отправить все чанки в удалённое хранилище."; - readonly zh: "将所有 chunks 重新发送到远端。"; - readonly "zh-tw": "將所有 chunks 重新傳送到遠端。"; - }; - readonly Reset: { - readonly def: "Reset"; - readonly es: "Restablecer"; - readonly ja: "リセット"; - readonly ko: "재설정"; - readonly ru: "Сброс"; - readonly zh: "重置"; - readonly "zh-tw": "重設"; - }; - readonly "Reset all": { - readonly def: "Reset all"; - readonly es: "Restablecer todo"; - readonly ja: "すべてリセット"; - readonly ko: "모두 재설정"; - readonly ru: "Сбросить всё"; - readonly zh: "全部重置"; - readonly "zh-tw": "全部重設"; - }; - readonly "Reset all journal counter": { - readonly def: "Reset all journal counter"; - readonly es: "Restablecer todos los contadores del diario"; - readonly ja: "すべてのジャーナルカウンターをリセット"; - readonly ko: "모든 저널 카운터 재설정"; - readonly ru: "Сбросить все счётчики журнала"; - readonly zh: "重置所有日志计数器"; - readonly "zh-tw": "重設所有日誌計數器"; - }; - readonly "Reset journal received history": { - readonly def: "Reset journal received history"; - readonly es: "Restablecer historial de recepción del diario"; - readonly ja: "ジャーナル受信履歴をリセット"; - readonly ko: "저널 수신 기록 재설정"; - readonly ru: "Сбросить историю полученных записей журнала"; - readonly zh: "重置日志接收历史"; - readonly "zh-tw": "重設日誌接收歷史"; - }; - readonly "Reset journal sent history": { - readonly def: "Reset journal sent history"; - readonly es: "Restablecer historial de envío del diario"; - readonly ja: "ジャーナル送信履歴をリセット"; - readonly ko: "저널 송신 기록 재설정"; - readonly ru: "Сбросить историю отправленных записей журнала"; - readonly zh: "重置日志发送历史"; - readonly "zh-tw": "重設日誌傳送歷史"; - }; - readonly "Reset notification threshold and check the remote database usage": { - readonly def: "Reset notification threshold and check the remote database usage"; - readonly fr: "Réinitialiser le seuil de notification et vérifier l'utilisation de la base distante"; - readonly he: "אפס סף התראה ובדוק שימוש במסד הנתונים המרוחד"; - readonly ja: "通知しきい値をリセットしてリモートデータベース使用量を確認"; - readonly ko: "알림 임계값을 초기화하고 원격 데이터베이스 사용량 확인"; - readonly ru: "Сбросить порог уведомления и проверить использование удалённой базы данных"; - readonly zh: "重置通知阈值并检查远程数据库使用情况"; - readonly "zh-tw": "重設通知閾值並檢查遠端資料庫使用情況"; - }; - readonly "Reset received": { - readonly def: "Reset received"; - readonly es: "Restablecer recepción"; - readonly ja: "受信履歴をリセット"; - readonly ko: "수신 기록 재설정"; - readonly ru: "Сбросить полученные"; - readonly zh: "重置接收记录"; - readonly "zh-tw": "重設接收紀錄"; - }; - readonly "Reset sent history": { - readonly def: "Reset sent history"; - readonly es: "Restablecer historial de envío"; - readonly ja: "送信履歴をリセット"; - readonly ko: "송신 기록 재설정"; - readonly ru: "Сбросить историю отправки"; - readonly zh: "重置发送历史"; - readonly "zh-tw": "重設傳送歷史"; - }; - readonly "Reset Synchronisation information": { - readonly def: "Reset Synchronisation information"; - readonly es: "Restablecer información de sincronización"; - readonly ja: "同期情報をリセット"; - readonly ko: "동기화 정보 재설정"; - readonly ru: "Сбросить информацию о синхронизации"; - readonly zh: "重置同步信息"; - readonly "zh-tw": "重設同步資訊"; - }; - readonly "Reset Synchronisation on This Device": { - readonly def: "Reset Synchronisation on This Device"; - readonly es: "Restablecer sincronización en este dispositivo"; - readonly ja: "このデバイスの同期状態をリセット"; - readonly ko: "이 장치의 동기화 상태 재설정"; - readonly ru: "Сбросить синхронизацию на этом устройстве"; - readonly zh: "重置此设备上的同步"; - readonly "zh-tw": "重設此裝置上的同步"; - }; - readonly "Reset the remote storage size threshold and check the remote storage size again.": { - readonly def: "Reset the remote storage size threshold and check the remote storage size again."; - readonly fr: "Réinitialiser le seuil de taille du stockage distant et vérifier à nouveau la taille du stockage distant."; - readonly he: "אפס את סף גודל האחסון המרוחד ובדוק שוב את גודל האחסון המרוחד."; - readonly ja: "リモートストレージ容量のしきい値をリセットし、リモートストレージ容量を再確認します。"; - readonly ko: "원격 저장소 크기 임계값을 초기화하고 원격 저장소 크기를 다시 확인합니다."; - readonly ru: "Сбросить порог размера удалённого хранилища и проверить размер хранилища снова."; - readonly zh: "重置远程存储大小阈值并再次检查远程存储大小。"; - readonly "zh-tw": "重設遠端儲存空間大小閾值,並再次檢查遠端儲存空間大小。"; - }; - readonly "Resolve All": { - readonly def: "Resolve All"; - readonly es: "Resolver todo"; - readonly ja: "すべて解決"; - readonly ko: "모두 해결"; - readonly ru: "Разрешить всё"; - readonly zh: "全部处理"; - readonly "zh-tw": "全部處理"; - }; - readonly "Resolve all conflicted files": { - readonly def: "Resolve all conflicted files"; - readonly es: "Resolver todos los archivos en conflicto"; - readonly ja: "競合しているすべてのファイルを解決"; - readonly ko: "충돌한 모든 파일 해결"; - readonly ru: "Разрешить все конфликтующие файлы"; - readonly zh: "解决所有冲突文件"; - readonly "zh-tw": "解決所有衝突檔案"; - }; - readonly "Resolve All conflicted files by the newer one": { - readonly def: "Resolve All conflicted files by the newer one"; - readonly es: "Resolver todos los archivos en conflicto con la versión más reciente"; - readonly ja: "競合したすべてのファイルを新しい方で解決"; - readonly ko: "충돌한 모든 파일을 최신 버전으로 해결"; - readonly ru: "Разрешить все конфликтующие файлы в пользу более новой версии"; - readonly zh: "将所有冲突文件解析为较新的版本"; - readonly "zh-tw": "將所有衝突檔案統一為較新的版本"; - }; - readonly "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": { - readonly def: "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one."; - readonly es: "Resuelve todos los archivos en conflicto conservando la versión más reciente. Precaución: esto sobrescribirá la versión anterior y no podrá recuperarse."; - readonly ja: "競合しているすべてのファイルを新しい方の内容で解決します。注意:古い方は上書きされ、復元できません。"; - readonly ko: "충돌한 모든 파일을 더 최신 버전으로 해결합니다. 주의: 이전 버전은 덮어써지며 복원할 수 없습니다."; - readonly ru: "Разрешает все конфликтующие файлы в пользу более новой версии. Внимание: старая версия будет перезаписана и её нельзя будет восстановить."; - readonly zh: "将所有冲突文件统一保留较新的版本。注意:这会覆盖较旧的版本,且被覆盖的内容无法恢复。"; - readonly "zh-tw": "將所有衝突檔案統一保留較新的版本。注意:這會覆寫較舊的版本,且被覆寫的內容無法復原。"; - }; - readonly "Restart Now": { - readonly def: "Restart Now"; - readonly es: "Reiniciar ahora"; - readonly ja: "今すぐ再起動"; - readonly ko: "지금 재시작"; - readonly ru: "Перезапустить сейчас"; - readonly zh: "立即重启"; - readonly "zh-tw": "立即重新啟動"; - }; - readonly "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": { - readonly def: "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?"; - readonly "zh-tw": "強烈建議重新啟動 Obsidian。在重新啟動之前,部分變更可能尚未生效,顯示也可能不一致。你確定要現在重新啟動嗎?"; - }; - readonly "Restore or reconstruct local database from remote.": { - readonly def: "Restore or reconstruct local database from remote."; - readonly es: "Restaura o reconstruye la base de datos local desde el remoto."; - readonly ja: "リモートからローカルデータベースを復元または再構築します。"; - readonly ko: "원격에서 로컬 데이터베이스를 복원하거나 재구축합니다."; - readonly ru: "Восстановить или перестроить локальную базу данных из удалённой."; - readonly zh: "从远端恢复或重建本地数据库。"; - readonly "zh-tw": "從遠端還原或重建本機資料庫。"; - }; - readonly "Run Doctor": { - readonly def: "Run Doctor"; - readonly fr: "Lancer le Docteur"; - readonly he: "הפעל Doctor"; - readonly ja: "診断を実行"; - readonly ko: "진단 실행"; - readonly ru: "Запустить диагностику"; - readonly zh: "立即诊断"; - readonly "zh-tw": "執行診斷"; - }; - readonly "S3/MinIO/R2 Object Storage": { - readonly def: "S3/MinIO/R2 Object Storage"; - readonly es: "Almacenamiento de objetos S3/MinIO/R2"; - readonly ja: "S3/MinIO/R2 オブジェクトストレージ"; - readonly ko: "S3/MinIO/R2 객체 스토리지"; - readonly ru: "Объектное хранилище S3/MinIO/R2"; - readonly zh: "S3/MinIO/R2 对象存储"; - readonly "zh-tw": "S3/MinIO/R2 物件儲存"; - }; - readonly "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": { - readonly def: "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform."; - readonly es: "Guardar configuración en archivo markdown. Se notificarán nuevos ajustes. Puede definir diferentes archivos por plataforma"; - readonly fr: "Enregistrer les paramètres dans un fichier markdown. Vous serez notifié à l'arrivée de nouveaux paramètres. Vous pouvez définir des fichiers différents selon la plateforme."; - readonly he: "שמור הגדרות לקובץ Markdown. תיודע כשהגדרות חדשות יגיעו. ניתן להגדיר קבצים שונים לפי פלטפורמה."; - readonly ja: "Markdownファイルに設定を保存します。新しい設定が到着すると通知されます。プラットフォームごとに異なるファイルを設定できます。"; - readonly ko: "설정을 마크다운 파일에 저장합니다. 새로운 설정이 도착하면 알림을 받게 됩니다. 플랫폼별로 다른 파일을 설정할 수 있습니다."; - readonly ru: "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform."; - readonly zh: "将设置保存到一个 Markdown 文件中。当新设置到达时,您将收到通知。您可以根据平台设置不同的文件 "; - }; - readonly "Saving will be performed forcefully after this number of seconds.": { - readonly def: "Saving will be performed forcefully after this number of seconds."; - readonly es: "Guardado forzado tras esta cantidad de segundos"; - readonly fr: "L'enregistrement sera forcé au bout de ce nombre de secondes."; - readonly he: "השמירה תתבצע בכפייה לאחר מספר שניות זה."; - readonly ja: "この秒数後に強制的に保存されます。"; - readonly ko: "이 시간(초) 후에 강제로 저장이 수행됩니다."; - readonly ru: "Сохранение будет принудительно выполнено после этого количества секунд."; - readonly zh: "在此秒数后将强制执行保存 "; - }; - readonly "Scan a QR Code (Recommended for mobile)": { - readonly def: "Scan a QR Code (Recommended for mobile)"; - readonly es: "Escanear un código QR (recomendado para móviles)"; - readonly ja: "QR コードをスキャンする(モバイル推奨)"; - readonly ko: "QR 코드 스캔(모바일 권장)"; - readonly ru: "Сканировать QR-код (рекомендуется для мобильных устройств)"; - readonly zh: "扫描二维码(移动端推荐)"; - readonly "zh-tw": "掃描 QR Code(行動裝置推薦)"; - }; - readonly "Scan changes on customization sync": { - readonly def: "Scan changes on customization sync"; - readonly es: "Escanear cambios en sincronización de personalización"; - readonly fr: "Analyser les modifications de synchronisation de personnalisation"; - readonly he: "סרוק שינויים בסנכרון התאמה אישית"; - readonly ja: "カスタマイズされた同期時に、変更をスキャンする"; - readonly ko: "사용자 설정 동기화 시 변경 사항 검색"; - readonly ru: "Сканировать изменения при синхронизации настроек"; - readonly zh: "在自定义同步时扫描更改"; - }; - readonly "Scan customization automatically": { - readonly def: "Scan customization automatically"; - readonly es: "Escanear personalización automáticamente"; - readonly fr: "Analyser automatiquement la personnalisation"; - readonly he: "סרוק התאמה אישית אוטומטית"; - readonly ja: "自動的にカスタマイズをスキャン"; - readonly ko: "사용자 설정 자동 검색"; - readonly ru: "Сканировать настройки автоматически"; - readonly zh: "自动扫描自定义设置"; - }; - readonly "Scan customization before replicating.": { - readonly def: "Scan customization before replicating."; - readonly es: "Escanear personalización antes de replicar"; - readonly fr: "Analyser la personnalisation avant de répliquer."; - readonly he: "סרוק התאמה אישית לפני שכפול."; - readonly ja: "レプリケーション(複製)前に、カスタマイズをスキャン"; - readonly ko: "복제하기 전에 사용자 설정을 검색합니다."; - readonly ru: "Сканировать настройки перед репликацией."; - readonly zh: "在复制前扫描自定义设置 "; - }; - readonly "Scan customization every 1 minute.": { - readonly def: "Scan customization every 1 minute."; - readonly es: "Escanear personalización cada 1 minuto"; - readonly fr: "Analyser la personnalisation toutes les 1 minute."; - readonly he: "סרוק התאמה אישית כל דקה."; - readonly ja: "カスタマイズのスキャンを1分ごとに行う"; - readonly ko: "1분마다 사용자 설정을 검색합니다."; - readonly ru: "Сканировать настройки каждую минуту."; - readonly zh: "每1分钟扫描自定义设置 "; - }; - readonly "Scan customization periodically": { - readonly def: "Scan customization periodically"; - readonly es: "Escanear personalización periódicamente"; - readonly fr: "Analyser la personnalisation périodiquement"; - readonly he: "סרוק התאמה אישית תקופתית"; - readonly ja: "定期的にカスタマイズをスキャン"; - readonly ko: "주기적으로 사용자 설정 검색"; - readonly ru: "Сканировать настройки периодически"; - readonly zh: "定期扫描自定义设置"; - }; - readonly "Scan for Broken files": { - readonly def: "Scan for Broken files"; - readonly ja: "破損ファイルをスキャン"; - readonly ko: "손상된 파일 검사"; - readonly zh: "扫描损坏文件"; - readonly "zh-tw": "掃描損壞檔案"; - }; - readonly "Scan for hidden files before replication": { - readonly def: "Scan for hidden files before replication"; - readonly es: "Escanear archivos ocultos antes de replicar"; - readonly fr: "Analyser les fichiers cachés avant réplication"; - readonly he: "סרוק קבצים נסתרים לפני שכפול"; - readonly ja: "レプリケーション(複製)開始前に、隠しファイルのスキャンを行う"; - readonly ko: "복제 전 숨겨진 파일 검색"; - readonly ru: "Сканировать скрытые файлы перед репликацией"; - readonly zh: "复制前扫描隐藏文件"; - }; - readonly "Scan hidden files periodically": { - readonly def: "Scan hidden files periodically"; - readonly es: "Escanear archivos ocultos periódicamente"; - readonly fr: "Analyser les fichiers cachés périodiquement"; - readonly he: "סרוק קבצים נסתרים תקופתית"; - readonly ja: "定期的に隠しファイルのスキャンを行う"; - readonly ko: "주기적으로 숨겨진 파일 검색"; - readonly ru: "Сканировать скрытые файлы периодически"; - readonly zh: "定期扫描隐藏文件"; - }; - readonly "Scan the QR code displayed on an active device using this device's camera.": { - readonly def: "Scan the QR code displayed on an active device using this device's camera."; - readonly es: "Escanee con la cámara de este dispositivo el código QR mostrado en un dispositivo activo。"; - readonly ja: "稼働中の端末に表示された QR コードを、この端末のカメラで読み取ってください。"; - readonly ko: "이 장치의 카메라로 활성 장치에 표시된 QR 코드를 스캔하세요。"; - readonly ru: "Отсканируйте QR-код, показанный на активном устройстве, с помощью камеры этого устройства。"; - readonly zh: "使用当前设备的摄像头扫描另一台已在使用设备上显示的二维码。"; - readonly "zh-tw": "使用目前裝置的相機掃描另一台已在使用裝置上顯示的 QR Code。"; - }; - readonly "Schedule and Restart": { - readonly def: "Schedule and Restart"; - readonly es: "Programar y reiniciar"; - readonly ja: "予約して再起動"; - readonly ko: "예약 후 재시작"; - readonly ru: "Запланировать и перезапустить"; - readonly zh: "安排并重启"; - readonly "zh-tw": "排程後重新啟動"; - }; - readonly "Scram Switches": { - readonly def: "Scram Switches"; - readonly ja: "緊急対応スイッチ"; - readonly ko: "긴급 전환 스위치"; - readonly zh: "紧急开关"; - readonly "zh-tw": "緊急處置開關"; - }; - readonly "Scram!": { - readonly def: "Scram!"; - readonly es: "Medidas de emergencia"; - readonly ja: "緊急停止"; - readonly ko: "긴급 조치"; - readonly ru: "Экстренные меры"; - readonly zh: "紧急处置"; - readonly "zh-tw": "緊急處置"; - }; - readonly "Seconds, 0 to disable": { - readonly def: "Seconds, 0 to disable"; - readonly es: "Segundos, 0 para desactivar"; - readonly fr: "Secondes, 0 pour désactiver"; - readonly he: "שניות, 0 לביטול"; - readonly ja: "秒数、0で無効"; - readonly ko: "초 단위, 0으로 설정하면 비활성화"; - readonly ru: "Секунд, 0 для отключения"; - readonly zh: "秒,0为禁用"; - }; - readonly "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": { - readonly def: "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving."; - readonly es: "Segundos. Guardado en BD local se retrasará hasta este valor tras dejar de escribir/guardar"; - readonly fr: "Secondes. L'enregistrement dans la base locale sera différé de cette valeur après l'arrêt de la frappe ou de l'enregistrement."; - readonly he: "שניות. השמירה למסד הנתונים המקומי תתעכב בערך זה לאחר הפסקת הקלדה או שמירה."; - readonly ja: "秒。入力や保存を停止してからこの値の間、ローカルデータベースへの保存が遅延されます。"; - readonly ko: "초 단위입니다. 타이핑이나 저장을 중단한 후 이 시간동안 로컬 데이터베이스 저장이 지연됩니다."; - readonly ru: "Секунды. Сохранение в локальную базу данных будет отложено."; - readonly zh: "秒。在我们停止输入或保存后,保存到本地数据库将延迟此值 "; - }; - readonly "Secret Key": { - readonly def: "Secret Key"; - readonly es: "Clave secreta"; - readonly fr: "Clé secrète"; - readonly he: "מפתח סודי"; - readonly ja: "シークレットキー"; - readonly ko: "시크릿 키"; - readonly ru: "Секретный ключ"; - readonly zh: "Secret Key"; - }; - readonly "Select the database adapter to use.": { - readonly def: "Select the database adapter to use."; - readonly es: "Selecciona el adaptador de base de datos que se usará."; - readonly ja: "使用するデータベースアダプターを選択します。"; - readonly ko: "사용할 데이터베이스 어댑터를 선택합니다."; - readonly ru: "Выберите используемый адаптер базы данных."; - readonly zh: "选择要使用的数据库适配器。"; - readonly "zh-tw": "選擇要使用的資料庫適配器。"; - }; - readonly Send: { - readonly def: "Send"; - readonly es: "Enviar"; - readonly ja: "送信"; - readonly ko: "보내기"; - readonly ru: "Отправить"; - readonly zh: "发送"; - readonly "zh-tw": "傳送"; - }; - readonly "Send chunks": { - readonly def: "Send chunks"; - readonly es: "Enviar chunks"; - readonly ja: "チャンクを送信"; - readonly ko: "청크 보내기"; - readonly ru: "Отправить чанки"; - readonly zh: "发送 chunks"; - readonly "zh-tw": "傳送 chunks"; - }; - readonly "Server URI": { - readonly def: "Server URI"; - readonly es: "URI del servidor"; - readonly fr: "URI du serveur"; - readonly he: "כתובת שרת (URI)"; - readonly ja: "URI"; - readonly ko: "서버 URI"; - readonly ru: "URI сервера"; - readonly zh: "服务器 URI"; - }; - readonly "Setting.GenerateKeyPair.Desc": { - readonly def: "We have generated a key pair!\n\nNote: This key pair will never be shown again. Please save it in a safe place. If you have lost it, you need to generate a new key pair.\nNote 2: The public key is in spki format, and the Private key is in pkcs8 format. For the sake of convenience, newlines are converted to `\\n` in public key.\nNote 3: The public key should be configured in the remote database, and the private key should be configured in local devices.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### Public Key\n> ```\n${public_key}\n> ```\n>\n> ### Private Key\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly es: "Hemos generado un par de claves.\n\nNota: Este par de claves no volverá a mostrarse. Guárdalo en un lugar seguro. Si lo pierdes, tendrás que generar uno nuevo.\nNota 2: La clave pública está en formato spki y la clave privada en formato pkcs8. Para mayor comodidad, los saltos de línea de la clave pública se convierten en `\\n`.\nNota 3: La clave pública debe configurarse en la base de datos remota y la clave privada en los dispositivos locales.\n\n>[!SOLO PARA TUS OJOS]-\n>
\n>\n> ### Clave pública\n> ```\n${public_key}\n> ```\n>\n> ### Clave privada\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Ambas para copiar]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
"; - readonly fr: "Nous avons généré une paire de clés !\n\nNote : cette paire de clés ne sera plus jamais affichée. Veuillez la conserver dans un endroit sûr. Si vous la perdez, vous devrez générer une nouvelle paire.\nNote 2 : la clé publique est au format spki, et la clé privée au format pkcs8. Pour plus de commodité, les retours à la ligne sont convertis en `\\n` dans la clé publique.\nNote 3 : la clé publique doit être configurée dans la base distante, et la clé privée sur les appareils locaux.\n\n>[!POUR VOS YEUX SEULEMENT]-\n>
\n>\n> ### Clé publique\n> ```\n${public_key}\n> ```\n>\n> ### Clé privée\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Les deux pour copier]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly he: "יצרנו זוג מפתחות!\n\nהערה: זוג מפתחות זה לא יוצג שוב. אנא שמור אותו במקום בטוח. אם אבד לך, יהיה צורך לייצר זוג מפתחות חדש.\nהערה 2: המפתח הציבורי הוא בפורמט spki, והמפתח הפרטי הוא בפורמט pkcs8. לנוחות, שורות חדשות ממוירות ל-`\\n` במפתח הציבורי.\nהערה 3: יש להגדיר את המפתח הציבורי במסד הנתונים המרוחד, ואת המפתח הפרטי במכשירים המקומיים.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### מפתח ציבורי\n> ```\n${public_key}\n> ```\n>\n> ### מפתח פרטי\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly ja: "キーペアを生成しました!\n\n注意: このキーペアは再度表示されません。安全な場所に保存してください。紛失した場合は、新しいキーペアを生成する必要があります。\n注意2: 公開鍵はspki形式、秘密鍵はpkcs8形式です。利便性のため、公開鍵の改行は`\\n`に変換されています。\n注意3: 公開鍵はリモートデータベースに、秘密鍵はローカルデバイスに設定してください。\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### 公開鍵\n> ```\n${public_key}\n> ```\n>\n> ### 秘密鍵\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n"; - readonly ko: "키 페어를 생성했습니다!\n\n참고: 이 키 페어는 다시 표시되지 않습니다. 안전한 곳에 저장해 주세요. 분실하면 새 키 페어를 생성해야 합니다.\n참고 2: 공개 키는 spki 형식이고, 개인 키는 pkcs8 형식입니다. 편의상 공개 키의 줄 바꿈은 `\\n`으로 변환됩니다.\n참고 3: 공개 키는 원격 데이터베이스에서 구성되어야 하고, 개인 키는 로컬 기기에서 구성되어야 합니다.\n\n>[!FOR YOUR EYES ONLY]-\n>
\n>\n> ### 공개 키\n> ```\n${public_key}\n> ```\n>\n> ### 개인 키\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!Both for copying]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
\n\n\n"; - readonly ru: "Мы сгенерировали пару ключей!"; - readonly zh: "我们已经生成了一组密钥对!\n\n注意:这组密钥对之后将不会再次显示。请务必妥善保管;如果丢失,你需要重新生成新的密钥对。\n注意 2:公钥采用 spki 格式,私钥采用 pkcs8 格式。为方便复制,公钥中的换行会被转换为 `\\n`。\n注意 3:公钥应配置在远端数据库中,私钥应配置在本地设备上。\n\n>[!仅限本人查看]-\n>
\n>\n> ### 公钥\n> ```\n${public_key}\n> ```\n>\n> ### 私钥\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!整段复制]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
"; - readonly "zh-tw": "我們已產生新的金鑰對!\n\n注意:此金鑰對之後將不會再次顯示。請務必妥善保存;若遺失,必須重新產生新的金鑰對。\n注意 2:公鑰採用 spki 格式,私鑰採用 pkcs8 格式。為了方便複製,公鑰中的換行會轉換為 `\\n`。\n注意 3:公鑰應設定在遠端資料庫中,私鑰則應設定在本機裝置上。\n\n>[!僅供本人查看]-\n>
\n>\n> ### 公鑰\n> ```\n${public_key}\n> ```\n>\n> ### 私鑰\n> ```\n${private_key}\n> ```\n>\n>
\n\n>[!便於整段複製]-\n>\n>
\n>\n> ```\n${public_key}\n${private_key}\n> ```\n>\n>
"; - }; - readonly "Setting.GenerateKeyPair.Title": { - readonly def: "New key pair has been generated!"; - readonly es: "¡Se ha generado un nuevo par de claves!"; - readonly fr: "Une nouvelle paire de clés a été générée !"; - readonly he: "זוג מפתחות חדש נוצר!"; - readonly ja: "新しいキーペアが生成されました!"; - readonly ko: "새 키 페어가 생성되었습니다!"; - readonly ru: "Новая пара ключей сгенерирована!"; - readonly zh: "已生成新的密钥对!"; - readonly "zh-tw": "已產生新的金鑰對!"; - }; - readonly "Setting.TroubleShooting": { - readonly def: "TroubleShooting"; - readonly fr: "Dépannage"; - readonly he: "פתרון בעיות"; - readonly ja: "トラブルシューティング"; - readonly ko: "문제 해결"; - readonly ru: "Устранение неполадок"; - readonly zh: "故障排除"; - }; - readonly "Setting.TroubleShooting.Doctor": { - readonly def: "Setting Doctor"; - readonly fr: "Docteur des paramètres"; - readonly he: "Doctor הגדרות"; - readonly ja: "設定診断ツール"; - readonly ko: "설정 진단 마법사"; - readonly ru: "Диагностика настроек"; - readonly zh: "设置诊断"; - }; - readonly "Setting.TroubleShooting.Doctor.Desc": { - readonly def: "Detects non optimal settings. (Same as during migration)"; - readonly fr: "Détecte les paramètres non optimaux. (Identique à la migration)"; - readonly he: "מזהה הגדרות לא אופטימליות. (זהה לפעולה במהלך הגירה)"; - readonly ja: "最適でない設定を検出します。(マイグレーション時と同じ)"; - readonly ko: "최적화되지 않은 설정을 감지합니다. (데이터 구조 전환 시와 동일)"; - readonly ru: "Обнаруживает неоптимальные настройки."; - readonly zh: "检测系统中不合理的设置。(与迁移期间逻辑相同)"; - }; - readonly "Setting.TroubleShooting.ScanBrokenFiles": { - readonly def: "Scan for broken files"; - readonly fr: "Analyser les fichiers corrompus"; - readonly he: "סרוק קבצים פגומים"; - readonly ja: "破損ファイルのスキャン"; - readonly ko: "손상된 파일 검사"; - readonly ru: "Сканировать повреждённые файлы"; - readonly zh: "扫描损坏或异常的文件"; - }; - readonly "Setting.TroubleShooting.ScanBrokenFiles.Desc": { - readonly def: "Scans for files that are not stored correctly in the database."; - readonly fr: "Analyse les fichiers qui ne sont pas stockés correctement dans la base."; - readonly he: "סורק קבצים שלא נשמרו כהלכה במסד הנתונים."; - readonly ja: "データベースに正しく保存されていないファイルをスキャンします。"; - readonly ko: "데이터베이스에 올바르게 저장되지 않은 파일을 검사합니다."; - readonly ru: "Сканирует файлы, которые неправильно хранятся в базе данных."; - readonly zh: "扫描数据库中未正确存储的文件。"; - }; - readonly "SettingTab.Message.AskRebuild": { - readonly def: "Your changes require fetching from the remote database. Do you want to proceed?"; - readonly fr: "Vos modifications nécessitent une récupération depuis la base distante. Voulez-vous continuer ?"; - readonly he: "השינויים שלך מצריכים משיכה ממסד הנתונים המרוחד. האם להמשיך?"; - readonly ja: "変更にはリモートデータベースからのフェッチが必要です。続行しますか?"; - readonly ko: "변경 사항을 적용하려면 원격 데이터베이스에서 가져와야 합니다. 계속 진행하시겠습니까?"; - readonly ru: "Ваши изменения требуют загрузки из удалённой базы данных. Хотите продолжить?"; - readonly zh: "Your changes require fetching from the remote database. Do you want to proceed?"; - }; - readonly "Setup URI dialog cancelled.": { - readonly def: "Setup URI dialog cancelled."; - readonly ja: "Setup URI ダイアログはキャンセルされました。"; - readonly ko: "Setup URI 대화 상자가 취소되었습니다."; - readonly ru: "Диалог Setup URI был отменён."; - readonly zh: "Setup URI 对话框已取消。"; - readonly "zh-tw": "Setup URI 對話框已取消。"; - }; - readonly "Setup.Apply.Buttons.ApplyAndFetch": { - readonly def: "Apply and Fetch"; - readonly fr: "Appliquer et récupérer"; - readonly he: "החל ומשוך"; - readonly ja: "適用してフェッチ"; - readonly ru: "Применить и загрузить"; - readonly zh: "Apply and Fetch"; - }; - readonly "Setup.Apply.Buttons.ApplyAndMerge": { - readonly def: "Apply and Merge"; - readonly fr: "Appliquer et fusionner"; - readonly he: "החל ומזג"; - readonly ja: "適用してマージ"; - readonly ru: "Применить и объединить"; - readonly zh: "Apply and Merge"; - }; - readonly "Setup.Apply.Buttons.ApplyAndRebuild": { - readonly def: "Apply and Rebuild"; - readonly fr: "Appliquer et reconstruire"; - readonly he: "החל ובנה מחדש"; - readonly ja: "適用して再構築"; - readonly ru: "Применить и перестроить"; - readonly zh: "Apply and Rebuild"; - }; - readonly "Setup.Apply.Buttons.Cancel": { - readonly def: "Discard and Cancel"; - readonly fr: "Abandonner et annuler"; - readonly he: "בטל ובטל"; - readonly ja: "破棄してキャンセル"; - readonly ru: "Отменить и отменить"; - readonly zh: "Discard and Cancel"; - }; - readonly "Setup.Apply.Buttons.OnlyApply": { - readonly def: "Only Apply"; - readonly fr: "Appliquer seulement"; - readonly he: "החל בלבד"; - readonly ja: "適用のみ"; - readonly ru: "Только применить"; - readonly zh: "Only Apply"; - }; - readonly "Setup.Apply.Message": { - readonly def: "The new configuration is ready. Let us proceed to apply it.\nThere are several ways to apply this:\n\n- Apply and Fetch\n Configure this device as a new client. After applying, synchronise from the remote server.\n- Apply and Merge\n Configure on a device that already has the file. It processes the local files and transfers the differences. Conflicts may arise.\n- Apply and Rebuild\n Rebuild the remote using local files. This is typically done if the server becomes corrupted or we wish to start from scratch.\n Other devices will be locked and required to re-fetch.\n- Only Apply\n Apply only. Conflicts may arise if a rebuild is required."; - readonly fr: "La nouvelle configuration est prête. Procédons à son application.\nPlusieurs manières de l'appliquer :\n\n- Appliquer et récupérer\n Configurer cet appareil comme nouveau client. Après application, synchroniser depuis le serveur distant.\n- Appliquer et fusionner\n Configurer sur un appareil qui possède déjà les fichiers. Traite les fichiers locaux et transfère les différences. Des conflits peuvent apparaître.\n- Appliquer et reconstruire\n Reconstruire le distant à partir des fichiers locaux. Typiquement effectué si le serveur est corrompu ou si l'on souhaite repartir de zéro.\n Les autres appareils seront verrouillés et devront refaire une récupération.\n- Appliquer seulement\n Appliquer uniquement. Des conflits peuvent apparaître si une reconstruction est nécessaire."; - readonly he: "התצורה החדשה מוכנה. בואו נמשיך להחיל אותה.\nישנן מספר דרכים להחיל זאת:\n\n- החל ומשוך\n הגדר מכשיר זה כלקוח חדש. לאחר ההחלה, סנכרן מהשרת המרוחד.\n- החל ומזג\n הגדר על מכשיר שכבר יש בו קבצים. מעבד קבצים מקומיים ומעביר הפרשים. עלולים\n לקום קונפליקטים.\n- החל ובנה מחדש\n בנה את השרת המרוחד מחדש תוך שימוש בקבצים מקומיים. נעשה בדרך כלל אם השרת\n מושחת או אם רוצים להתחיל מאפס. מכשירים אחרים יינעלו ויצטרכו למשוך מחדש.\n- החל בלבד\n החל בלבד. עלולים לקום קונפליקטים אם נדרשת בנייה מחדש."; - readonly ja: "新しい設定の準備ができました。適用に進みましょう。\n適用方法はいくつかあります:\n\n- 適用してフェッチ\n このデバイスを新しいクライアントとして設定します。適用後、リモートサーバーから同期します。\n- 適用してマージ\n 既にファイルがあるデバイスで設定します。ローカルファイルを処理し、差分を転送します。競合が発生する場合があります。\n- 適用して再構築\n ローカルファイルを使用してリモートを再構築します。これは通常、サーバーが破損した場合や最初からやり直したい場合に行います。\n 他のデバイスはロックされ、再フェッチが必要になります。\n- 適用のみ\n 適用のみを行います。再構築が必要な場合、競合が発生する可能性があります。"; - readonly ru: "Новая конфигурация готова. Есть несколько способов применить её."; - readonly zh: "The new configuration is ready. Let us proceed to apply it.\nThere are several ways to apply this:\n\n- Apply and Fetch\n Configure this device as a new client. After applying, synchronise from the remote server.\n- Apply and Merge\n Configure on a device that already has the file. It processes the local files and transfers the differences. Conflicts may arise.\n- Apply and Rebuild\n Rebuild the remote using local files. This is typically done if the server becomes corrupted or we wish to start from scratch.\n Other devices will be locked and required to re-fetch.\n- Only Apply\n Apply only. Conflicts may arise if a rebuild is required."; - }; - readonly "Setup.Apply.Title": { - readonly def: "Apply new configuration from the ${method}"; - readonly fr: "Appliquer la nouvelle configuration depuis ${method}"; - readonly he: "החל תצורה חדשה מה-${method}"; - readonly ja: "${method}からの新しい設定を適用"; - readonly ru: "Применить новую конфигурацию из method"; - readonly zh: "Apply new configuration from the ${method}"; - }; - readonly "Setup.Apply.WarningRebuildRecommended": { - readonly def: "NOTE: after adjusting the settings, it has been determined that a rebuild is required; Just Import is not recommended."; - readonly fr: "NOTE : après ajustement des paramètres, il a été déterminé qu'une reconstruction est requise ; un simple import n'est pas recommandé."; - readonly he: "שים לב: לאחר כוונון ההגדרות, נקבע שנדרשת בנייה מחדש; ייבוא בלבד אינו מומלץ."; - readonly ja: "注意: 設定の調整後、再構築が必要と判断されました。インポートのみは推奨されません。"; - readonly ru: "ПРИМЕЧАНИЕ: после настройки изменений определено, что требуется перестроение."; - readonly zh: "NOTE: after adjusting the settings, it has been determined that a rebuild is required; Just Import is not recommended."; - }; - readonly "Setup.Doctor.Buttons.No": { - readonly def: "No, please use the settings in the URI as is"; - readonly fr: "Non, utiliser les paramètres de l'URI tels quels"; - readonly he: "לא, אנא השתמש בהגדרות ה-URI כפי שהן"; - readonly ja: "いいえ、URIの設定をそのまま使用"; - readonly ru: "Нет, использовать настройки из URI как есть"; - readonly zh: "No, please use the settings in the URI as is"; - }; - readonly "Setup.Doctor.Buttons.Yes": { - readonly def: "Yes, please consult the doctor"; - readonly fr: "Oui, consulter le docteur"; - readonly he: "כן, אנא יעץ ל-Doctor"; - readonly ja: "はい、診断ツールに相談する"; - readonly ru: "Да, пожалуйста, запустить диагностику"; - readonly zh: "Yes, please consult the doctor"; - }; - readonly "Setup.Doctor.Message": { - readonly def: "Self-hosted LiveSync has gradually become longer in history and some recommended settings have changed.\n\nNow, setup is a very good time to do this.\n\nDo you want to run Doctor to check if the imported settings are optimal compared to the latest state?"; - readonly fr: "Self-hosted LiveSync s'est progressivement étoffé et certains paramètres recommandés ont évolué.\n\nLa configuration est un bon moment pour le faire.\n\nVoulez-vous lancer le Docteur pour vérifier si les paramètres importés sont optimaux par rapport au dernier état ?"; - readonly he: "Self-hosted LiveSync הפך ארוך יותר בהיסטוריה שלו וחלק מההגדרות המומלצות השתנו.\n\nעכשיו, הגדרה היא זמן מצוין לכך.\n\nהאם ברצונך להפעיל את Doctor כדי לבדוק אם ההגדרות המיובאות אופטימליות בהשוואה למצב הנוכחי?"; - readonly ja: "Self-hosted LiveSyncは徐々に歴史が長くなり、一部の推奨設定が変更されています。\n\nセットアップは、これを行う非常に良い機会です。\n\nインポートされた設定が最新の状態と比較して最適かどうかを確認するために、診断ツールを実行しますか?"; - readonly ru: "Self-hosted LiveSync постепенно набрал историю и некоторые рекомендуемые настройки изменились."; - readonly zh: "Self-hosted LiveSync has gradually become longer in history and some recommended settings have changed.\n\nNow, setup is a very good time to do this.\n\nDo you want to run Doctor to check if the imported settings are optimal compared to the latest state?"; - }; - readonly "Setup.Doctor.Title": { - readonly def: "Do you want to consult the doctor?"; - readonly fr: "Voulez-vous consulter le docteur ?"; - readonly he: "האם ברצונך להתייעץ עם ה-Doctor?"; - readonly ja: "診断ツールに相談しますか?"; - readonly ru: "Хотите запустить диагностику?"; - readonly zh: "Do you want to consult the doctor?"; - }; - readonly "Setup.FetchRemoteConf.Buttons.Fetch": { - readonly def: "Yes, please fetch the configuration"; - readonly fr: "Oui, récupérer la configuration"; - readonly he: "כן, אנא משוך את התצורה"; - readonly ja: "はい、設定を取得"; - readonly ru: "Да, загрузить конфигурацию"; - readonly zh: "Yes, please fetch the configuration"; - }; - readonly "Setup.FetchRemoteConf.Buttons.Skip": { - readonly def: "No, please use the settings in the URI"; - readonly fr: "Non, utiliser les paramètres de l'URI"; - readonly he: "לא, אנא השתמש בהגדרות ב-URI"; - readonly ja: "いいえ、URIの設定を使用"; - readonly ru: "Нет, использовать настройки из URI"; - readonly zh: "No, please use the settings in the URI"; - }; - readonly "Setup.FetchRemoteConf.Message": { - readonly def: "If we have already synchronised once with another device, the remote database stores the suitable configuration values between the synchronised devices. The plug-in would like to retrieve them for robust configuration.\n\nHowever, we have to make sure the one thing. Are we currently in a situation where we can access the network safely and retrieve the settings?\n\nNote: Mostly, you are safe to do this, that your remote database is hosted with a SSL certificate, and your network is not compromised."; - readonly fr: "Si nous avons déjà synchronisé une fois avec un autre appareil, la base distante stocke les valeurs de configuration adaptées entre les appareils synchronisés. Le plug-in souhaiterait les récupérer pour une configuration robuste.\n\nMais il faut s'assurer d'une chose. Sommes-nous actuellement dans une situation où nous pouvons accéder au réseau en toute sécurité et récupérer les paramètres ?\n\nNote : le plus souvent, c'est sûr si votre base distante est hébergée avec un certificat SSL et si votre réseau n'est pas compromis."; - readonly he: "אם סנכרנו כבר פעם עם מכשיר אחר, מסד הנתונים המרוחד מאחסן ערכי תצורה מתאימים בין המכשירים המסונכרנים. הפלאגין ירצה לאחזר אותם לתצורה חזקה יותר.\n\nעם זאת, עלינו לוודא דבר אחד. האם אנחנו כרגע במצב שבו ניתן לגשת לרשת בבטחה ולאחזר את ההגדרות?\nהערה: ברוב המקרים, אתה בטוח לעשות זאת, כל עוד מסד הנתונים המרוחד שלך מאוחסן עם תעודת SSL, ורשתך אינה פגומה."; - readonly ja: "既に他のデバイスと同期したことがある場合、リモートデータベースには同期されたデバイス間の適切な設定値が保存されています。プラグインは堅牢な設定のためにそれらを取得したいと考えています。\n\nただし、1つ確認が必要です。現在、ネットワークに安全にアクセスして設定を取得できる状況ですか?\n\n注意: リモートデータベースがSSL証明書でホストされており、ネットワークが侵害されていなければ、ほとんどの場合安全に実行できます。"; - readonly ru: "Если мы уже синхронизировались с другим устройством, удалённая база данных хранит подходящие значения конфигурации."; - readonly zh: "If we have already synchronised once with another device, the remote database stores the suitable configuration values between the synchronised devices. The plug-in would like to retrieve them for robust configuration.\n\nHowever, we have to make sure the one thing. Are we currently in a situation where we can access the network safely and retrieve the settings?\n\nNote: Mostly, you are safe to do this, that your remote database is hosted with a SSL certificate, and your network is not compromised."; - }; - readonly "Setup.FetchRemoteConf.Title": { - readonly def: "Fetch configuration from remote database?"; - readonly fr: "Récupérer la configuration depuis la base distante ?"; - readonly he: "אחזר תצורה ממסד הנתונים המרוחד?"; - readonly ja: "リモートデータベースから設定を取得しますか?"; - readonly ru: "Загрузить конфигурацию с удалённой базы данных?"; - readonly zh: "Fetch configuration from remote database?"; - }; - readonly "Setup.QRCode": { - readonly def: "We have generated a QR code to transfer the settings. Please scan the QR code with your phone or other device.\nNote: The QR code is not encrypted, so be careful to open this.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly fr: "Nous avons généré un QR code pour transférer les paramètres. Scannez-le avec votre téléphone ou un autre appareil.\nNote : le QR code n'est pas chiffré, soyez prudent en l'affichant.\n\n>[!POUR VOS YEUX SEULEMENT]-\n>
${qr_image}
"; - readonly he: "יצרנו קוד QR להעברת ההגדרות. אנא סרוק את קוד ה-QR עם הטלפון או מכשיר אחר.\nהערה: קוד ה-QR אינו מוצפן, אז היה זהיר בפתיחתו.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly ja: "設定を転送するためのQRコードを生成しました。スマートフォンや他のデバイスでQRコードをスキャンしてください。\n注意: QRコードは暗号化されていないため、開く際は注意してください。\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly ko: "설정을 전송하기 위한 QR 코드를 생성했습니다. 휴대폰이나 다른 기기로 QR 코드를 스캔해 주세요.\n참고: QR 코드는 암호화되지 않았으므로 열 때 주의하세요.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - readonly ru: "Мы сгенерировали QR-код для передачи настроек. Отсканируйте QR-код телефоном."; - readonly zh: "We have generated a QR code to transfer the settings. Please scan the QR code with your phone or other device.\nNote: The QR code is not encrypted, so be careful to open this.\n\n>[!FOR YOUR EYES ONLY]-\n>
${qr_image}
"; - }; - readonly "Setup.RemoteE2EE.AdvancedTitle": { - readonly def: "Advanced"; - readonly es: "Avanzado"; - readonly ja: "詳細設定"; - readonly ko: "고급"; - readonly ru: "Дополнительно"; - readonly zh: "高级"; - readonly "zh-tw": "進階"; - }; - readonly "Setup.RemoteE2EE.AlgorithmWarning": { - readonly def: "Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data."; - readonly es: "Cambiar el algoritmo de cifrado impedirá el acceso a cualquier dato cifrado anteriormente con otro algoritmo. Asegúrate de que todos tus dispositivos estén configurados para usar el mismo algoritmo y así mantener el acceso a tus datos."; - readonly ja: "暗号化アルゴリズムを変更すると、別のアルゴリズムで暗号化された既存データにはアクセスできなくなります。すべての端末で同じアルゴリズムを使うよう設定し、データにアクセスできる状態を維持してください。"; - readonly ko: "암호화 알고리즘을 변경하면 다른 알고리즘으로 암호화된 기존 데이터에 접근할 수 없게 됩니다. 모든 기기에서 동일한 알고리즘을 사용하도록 설정해 데이터 접근성을 유지하세요."; - readonly ru: "Изменение алгоритма шифрования лишит доступа к данным, которые ранее были зашифрованы другим алгоритмом. Убедитесь, что все ваши устройства настроены на использование одного и того же алгоритма, чтобы сохранить доступ к данным."; - readonly zh: "更改加密算法会导致之前使用其他算法加密的数据无法访问。请确保所有设备都配置为使用同一算法,以保持对数据的访问能力。"; - readonly "zh-tw": "變更加密演算法後,先前以其他演算法加密的資料將無法再存取。請確認所有裝置都設定為使用相同演算法,以維持對資料的存取能力。"; - }; - readonly "Setup.RemoteE2EE.ButtonCancel": { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - readonly "zh-tw": "取消"; - }; - readonly "Setup.RemoteE2EE.ButtonProceed": { - readonly def: "Proceed"; - readonly es: "Continuar"; - readonly ja: "進む"; - readonly ko: "진행"; - readonly ru: "Продолжить"; - readonly zh: "继续"; - readonly "zh-tw": "繼續"; - }; - readonly "Setup.RemoteE2EE.DefaultAlgorithmDesc": { - readonly def: "In most cases, you should stick with the default algorithm (${algorithm}). This setting is only required if you have an existing Vault encrypted in a different format."; - readonly es: "En la mayoría de los casos, debes mantener el algoritmo predeterminado (${algorithm}). Este ajuste solo es necesario si ya tienes un Vault cifrado con un formato diferente."; - readonly ja: "ほとんどの場合は、既定のアルゴリズム(${algorithm})をそのまま使用してください。この設定が必要になるのは、既存の Vault が別の形式で暗号化されている場合のみです。"; - readonly ko: "대부분의 경우 기본 알고리즘(${algorithm})을 그대로 사용하는 것이 좋습니다. 이 설정은 기존 Vault가 다른 형식으로 암호화되어 있는 경우에만 필요합니다."; - readonly ru: "В большинстве случаев следует оставить алгоритм по умолчанию (${algorithm}). Этот параметр нужен только в том случае, если у вас уже есть Vault, зашифрованный в другом формате."; - readonly zh: "在大多数情况下,你应继续使用默认算法(${algorithm})。只有当你已有一个采用不同格式加密的 Vault 时,才需要调整此设置。"; - readonly "zh-tw": "在大多數情況下,建議維持使用預設演算法(${algorithm})。只有當你現有的 Vault 是以不同格式加密時,才需要調整這項設定。"; - }; - readonly "Setup.RemoteE2EE.Guidance": { - readonly def: "Please configure your end-to-end encryption settings."; - readonly es: "Configura tus ajustes de cifrado de extremo a extremo."; - readonly ja: "エンドツーエンド暗号化の設定を行ってください。"; - readonly ko: "엔드투엔드 암호화 설정을 구성해 주세요."; - readonly ru: "Пожалуйста, настройте параметры сквозного шифрования."; - readonly zh: "请配置你的端到端加密设置。"; - readonly "zh-tw": "請設定你的端對端加密選項。"; - }; - readonly "Setup.RemoteE2EE.LabelEncrypt": { - readonly def: "End-to-End Encryption"; - readonly es: "Cifrado de extremo a extremo"; - readonly ja: "エンドツーエンド暗号化"; - readonly ko: "엔드투엔드 암호화"; - readonly ru: "Сквозное шифрование"; - readonly zh: "端到端加密"; - readonly "zh-tw": "端對端加密"; - }; - readonly "Setup.RemoteE2EE.LabelEncryptionAlgorithm": { - readonly def: "Encryption Algorithm"; - readonly es: "Algoritmo de cifrado"; - readonly ja: "暗号化アルゴリズム"; - readonly ko: "암호화 알고리즘"; - readonly ru: "Алгоритм шифрования"; - readonly zh: "加密算法"; - readonly "zh-tw": "加密演算法"; - }; - readonly "Setup.RemoteE2EE.LabelObfuscateProperties": { - readonly def: "Obfuscate Properties"; - readonly es: "Ofuscar propiedades"; - readonly ja: "プロパティを難読化"; - readonly ko: "속성 난독화"; - readonly ru: "Обфусцировать свойства"; - readonly zh: "混淆属性"; - readonly "zh-tw": "混淆屬性"; - }; - readonly "Setup.RemoteE2EE.MultiDestinationWarning": { - readonly def: "This setting must be the same even when connecting to multiple synchronisation destinations."; - readonly es: "Este ajuste debe ser el mismo incluso cuando te conectes a varios destinos de sincronización."; - readonly ja: "複数の同期先へ接続する場合でも、この設定は同一である必要があります。"; - readonly ko: "여러 동기화 대상에 연결하는 경우에도 이 설정은 동일해야 합니다."; - readonly ru: "Этот параметр должен быть одинаковым даже при подключении к нескольким направлениям синхронизации."; - readonly zh: "即使连接到多个同步目标,此设置也必须保持一致。"; - readonly "zh-tw": "即使連線到多個同步目標,這項設定也必須保持一致。"; - }; - readonly "Setup.RemoteE2EE.ObfuscatePropertiesDesc": { - readonly def: "Obfuscating properties (e.g., path of file, size, creation and modification dates) adds an additional layer of security by making it harder to identify the structure and names of your files and folders on the remote server. This helps protect your privacy and makes it more difficult for unauthorized users to infer information about your data."; - readonly es: "Ofuscar propiedades (por ejemplo, la ruta del archivo, el tamaño y las fechas de creación y modificación) añade una capa adicional de seguridad al dificultar la identificación de la estructura y los nombres de tus archivos y carpetas en el servidor remoto. Esto ayuda a proteger tu privacidad y dificulta que usuarios no autorizados deduzcan información sobre tus datos."; - readonly ja: "プロパティ(ファイルパス、サイズ、作成日時、更新日時など)を難読化すると、リモートサーバー上のファイルやフォルダーの構造や名前を特定しにくくできるため、追加の保護層になります。これによりプライバシーが守られ、権限のない第三者がデータに関する情報を推測しにくくなります。"; - readonly ko: "속성(예: 파일 경로, 크기, 생성일 및 수정일)을 난독화하면 원격 서버에서 파일과 폴더의 구조 및 이름을 식별하기 어렵게 만들어 보안을 한층 강화할 수 있습니다. 이는 개인 정보를 보호하고 권한 없는 사용자가 데이터에 관한 정보를 추론하기 어렵게 만듭니다."; - readonly ru: "Обфускация свойств (например, пути к файлу, размера, дат создания и изменения) добавляет дополнительный уровень защиты, затрудняя определение структуры и названий ваших файлов и папок на удалённом сервере. Это помогает защитить вашу конфиденциальность и усложняет для посторонних вывод информации о ваших данных."; - readonly zh: "混淆属性(例如文件路径、大小、创建和修改日期)可以增加一层额外保护,使远程服务器上的文件与文件夹结构及名称更难被识别。这有助于保护你的隐私,也让未授权用户更难推断你的数据相关信息。"; - readonly "zh-tw": "混淆屬性(例如檔案路徑、大小、建立時間與修改時間)可以額外增加一層安全保護,讓遠端伺服器上的檔案與資料夾結構及名稱更難被辨識。這有助於保護你的隱私,也讓未授權使用者更難推測你的資料資訊。"; - }; - readonly "Setup.RemoteE2EE.PassphraseValidationLine1": { - readonly def: "Please be aware that the End-to-End Encryption passphrase is not validated until the synchronisation process actually commences. This is a security measure designed to protect your data."; - readonly es: "Ten en cuenta que la frase de contraseña del cifrado de extremo a extremo no se valida hasta que el proceso de sincronización comienza realmente. Esta es una medida de seguridad diseñada para proteger tus datos."; - readonly ja: "エンドツーエンド暗号化のパスフレーズは、実際に同期処理が開始されるまで検証されない点にご注意ください。これはデータを保護するためのセキュリティ対策です。"; - readonly ko: "엔드투엔드 암호화 패스프레이즈는 실제 동기화가 시작되기 전까지 검증되지 않는다는 점에 유의하세요. 이것은 데이터를 보호하기 위한 보안 조치입니다."; - readonly ru: "Обратите внимание: парольная фраза для сквозного шифрования не проверяется до фактического начала процесса синхронизации. Это сделано в целях безопасности ваших данных."; - readonly zh: "请注意,在同步过程真正开始之前,端到端加密密码短语不会被校验。这是一项用于保护你数据的安全措施。"; - readonly "zh-tw": "請注意,端對端加密的密語要到同步程序實際開始時才會進行驗證。這是為了保護你資料而設計的安全措施。"; - }; - readonly "Setup.RemoteE2EE.PassphraseValidationLine2": { - readonly def: "Therefore, we ask that you exercise extreme caution when configuring server information manually. If an incorrect passphrase is entered, the data on the server will become corrupted. Please understand that this is intended behaviour."; - readonly es: "Por lo tanto, te pedimos que tengas muchísimo cuidado al configurar manualmente la información del servidor. Si introduces una frase de contraseña incorrecta, los datos del servidor se corromperán. Ten en cuenta que este comportamiento es intencionado."; - readonly ja: "そのため、サーバー情報を手動で設定する際は細心の注意を払ってください。誤ったパスフレーズを入力すると、サーバー上のデータが破損します。これは意図された動作ですので、あらかじめご理解ください。"; - readonly ko: "따라서 서버 정보를 수동으로 구성할 때는 각별히 주의해 주세요. 잘못된 패스프레이즈를 입력하면 서버의 데이터가 손상됩니다. 이는 의도된 동작이니 반드시 이해하고 진행해 주세요."; - readonly ru: "Поэтому при ручной настройке информации о сервере требуется предельная осторожность. Если будет введена неверная парольная фраза, данные на сервере будут повреждены. Пожалуйста, учтите, что это ожидаемое поведение."; - readonly zh: "因此,在手动配置服务器信息时请务必格外小心。如果输入了错误的密码短语,服务器上的数据将会损坏。请理解这属于预期行为。"; - readonly "zh-tw": "因此,在手動設定伺服器資訊時請務必格外小心。如果輸入了錯誤的密語,伺服器上的資料將會損毀。請理解這是系統的預期行為。"; - }; - readonly "Setup.RemoteE2EE.PlaceholderPassphrase": { - readonly def: "Enter your passphrase"; - readonly es: "Introduce tu frase de contraseña"; - readonly ja: "パスフレーズを入力してください"; - readonly ko: "패스프레이즈를 입력하세요"; - readonly ru: "Введите парольную фразу"; - readonly zh: "输入你的密码短语"; - readonly "zh-tw": "輸入你的密語"; - }; - readonly "Setup.RemoteE2EE.StronglyRecommendedLine1": { - readonly def: "Enabling end-to-end encryption ensures that your data is encrypted on your device before being sent to the remote server. This means that even if someone gains access to the server, they won't be able to read your data without the passphrase. Make sure to remember your passphrase, as it will be required to decrypt your data on other devices."; - readonly es: "Al habilitar el cifrado de extremo a extremo, tus datos se cifran en tu dispositivo antes de enviarse al servidor remoto. Esto significa que, incluso si alguien obtiene acceso al servidor, no podrá leer tus datos sin la frase de contraseña. Asegúrate de recordarla, ya que también será necesaria para descifrar tus datos en otros dispositivos."; - readonly ja: "エンドツーエンド暗号化を有効にすると、データはリモートサーバーへ送信される前にこの端末上で暗号化されます。つまり、たとえ誰かがサーバーへアクセスできても、パスフレーズがなければデータを読むことはできません。他の端末でデータを復号する際にも必要になるため、パスフレーズは必ず覚えておいてください。"; - readonly ko: "엔드투엔드 암호화를 활성화하면 데이터가 원격 서버로 전송되기 전에 이 기기에서 암호화됩니다. 즉, 누군가 서버에 접근하더라도 패스프레이즈 없이는 데이터를 읽을 수 없습니다. 다른 기기에서 데이터를 복호화할 때도 필요하므로 패스프레이즈를 반드시 기억해 두세요."; - readonly ru: "При включении сквозного шифрования ваши данные шифруются на устройстве до отправки на удалённый сервер. Это означает, что даже если кто-то получит доступ к серверу, он не сможет прочитать ваши данные без парольной фразы. Обязательно запомните парольную фразу, так как она потребуется для расшифровки данных на других устройствах."; - readonly zh: "启用端到端加密后,数据会先在你的设备上加密,再发送到远程服务器。这意味着即使有人获得了服务器访问权限,没有密码短语也无法读取你的数据。请务必记住你的密码短语,因为在其他设备上解密数据时也需要它。"; - readonly "zh-tw": "啟用端對端加密後,資料會先在你的裝置上完成加密,再傳送到遠端伺服器。這表示即使有人取得伺服器存取權,沒有密語也無法讀取你的資料。請務必記住你的密語,因為其他裝置在解密資料時也需要它。"; - }; - readonly "Setup.RemoteE2EE.StronglyRecommendedLine2": { - readonly def: "Also, please note that if you are using Peer-to-Peer synchronization, this configuration will be used when you switch to other methods and connect to a remote server in the future."; - readonly es: "Además, ten en cuenta que si estás usando sincronización Peer-to-Peer, esta configuración se utilizará cuando más adelante cambies a otros métodos y te conectes a un servidor remoto."; - readonly ja: "また、Peer-to-Peer 同期を使用している場合でも、将来ほかの方式へ切り替えてリモートサーバーへ接続するときには、この設定が使われます。"; - readonly ko: "또한 Peer-to-Peer 동기화를 사용 중이더라도, 나중에 다른 방식으로 전환하여 원격 서버에 연결하면 이 설정이 그대로 사용됩니다."; - readonly ru: "Также обратите внимание: если вы используете синхронизацию Peer-to-Peer, эта конфигурация будет использована, когда вы позже переключитесь на другие методы и подключитесь к удалённому серверу."; - readonly zh: "另外请注意,如果你正在使用 Peer-to-Peer 同步,当你以后切换到其他同步方式并连接远程服务器时,也会使用这组配置。"; - readonly "zh-tw": "另外請注意,即使你目前使用的是 Peer-to-Peer 同步,日後若切換到其他方式並連線到遠端伺服器時,也會沿用這組設定。"; - }; - readonly "Setup.RemoteE2EE.StronglyRecommendedTitle": { - readonly def: "Strongly Recommended"; - readonly es: "Muy recomendable"; - readonly ja: "強く推奨"; - readonly ko: "강력 권장"; - readonly ru: "Настоятельно рекомендуется"; - readonly zh: "强烈推荐"; - readonly "zh-tw": "強烈建議"; - }; - readonly "Setup.RemoteE2EE.Title": { - readonly def: "End-to-End Encryption"; - readonly es: "Cifrado de extremo a extremo"; - readonly ja: "エンドツーエンド暗号化"; - readonly ko: "엔드투엔드 암호화"; - readonly ru: "Сквозное шифрование"; - readonly zh: "端到端加密"; - readonly "zh-tw": "端對端加密"; - }; - readonly "Setup.ScanQRCode.ButtonClose": { - readonly def: "Close this dialog"; - readonly es: "Cerrar este diálogo"; - readonly ja: "このダイアログを閉じる"; - readonly ko: "이 대화 상자 닫기"; - readonly ru: "Закрыть это окно"; - readonly zh: "关闭此对话框"; - readonly "zh-tw": "關閉此對話框"; - }; - readonly "Setup.ScanQRCode.Guidance": { - readonly def: "Please follow the steps below to import settings from your existing device."; - readonly es: "Sigue los pasos de abajo para importar los ajustes desde tu dispositivo actual."; - readonly ja: "既存の端末から設定を取り込むには、以下の手順に従ってください。"; - readonly ko: "기존 기기에서 설정을 가져오려면 아래 단계를 따라 주세요."; - readonly ru: "Чтобы импортировать настройки с существующего устройства, выполните следующие шаги."; - readonly zh: "请按照以下步骤从现有设备导入设置。"; - readonly "zh-tw": "請依照以下步驟,從現有裝置匯入設定。"; - }; - readonly "Setup.ScanQRCode.Step1": { - readonly def: "On this device, please keep this Vault open."; - readonly es: "En este dispositivo, mantén este Vault abierto."; - readonly ja: "この端末では、この Vault を開いたままにしてください。"; - readonly ko: "이 기기에서는 이 Vault를 계속 열어 두세요."; - readonly ru: "На этом устройстве оставьте данный Vault открытым."; - readonly zh: "在这台设备上,请保持此 Vault 处于打开状态。"; - readonly "zh-tw": "在這台裝置上,請保持此 Vault 開啟。"; - }; - readonly "Setup.ScanQRCode.Step2": { - readonly def: "On the source device, open Obsidian."; - readonly es: "En el dispositivo de origen, abre Obsidian."; - readonly ja: "元の端末で Obsidian を開きます。"; - readonly ko: "원본 기기에서 Obsidian을 엽니다."; - readonly ru: "На исходном устройстве откройте Obsidian."; - readonly zh: "在源设备上打开 Obsidian。"; - readonly "zh-tw": "在來源裝置上開啟 Obsidian。"; - }; - readonly "Setup.ScanQRCode.Step3": { - readonly def: "On the source device, from the command palette, run the 'Show settings as a QR code' command."; - readonly es: "En el dispositivo de origen, ejecuta desde la paleta de comandos la orden \"Mostrar ajustes como código QR\"."; - readonly ja: "元の端末でコマンドパレットから「設定を QR コードとして表示」を実行します。"; - readonly ko: "원본 기기에서 명령 팔레트를 열고 \"설정을 QR 코드로 표시\" 명령을 실행합니다."; - readonly ru: "На исходном устройстве в палитре команд выполните команду «Показать настройки как QR-код»."; - readonly zh: "在源设备上,从命令面板运行“将设置显示为二维码”命令。"; - readonly "zh-tw": "在來源裝置上,從命令面板執行「將設定顯示為 QR 碼」命令。"; - }; - readonly "Setup.ScanQRCode.Step4": { - readonly def: "On this device, switch to the camera app or use a QR code scanner to scan the displayed QR code."; - readonly es: "En este dispositivo, cambia a la cámara o usa un escáner QR para escanear el código mostrado."; - readonly ja: "この端末でカメラアプリに切り替えるか QR コードスキャナーを使って、表示された QR コードを読み取ってください。"; - readonly ko: "이 기기에서 카메라 앱으로 전환하거나 QR 코드 스캐너를 사용해 표시된 QR 코드를 스캔하세요."; - readonly ru: "На этом устройстве откройте приложение камеры или используйте сканер QR-кодов, чтобы считать показанный QR-код."; - readonly zh: "在这台设备上,切换到相机应用或使用二维码扫描器扫描显示出的二维码。"; - readonly "zh-tw": "在這台裝置上切換到相機 App,或使用 QR 碼掃描器掃描顯示出的 QR 碼。"; - }; - readonly "Setup.ScanQRCode.Title": { - readonly def: "Scan QR Code"; - readonly es: "Escanear código QR"; - readonly ja: "QRコードをスキャン"; - readonly ko: "QR 코드 스캔"; - readonly ru: "Сканировать QR-код"; - readonly zh: "扫描二维码"; - readonly "zh-tw": "掃描 QR 碼"; - }; - readonly "Setup.ShowQRCode": { - readonly def: "Show QR code"; - readonly fr: "Afficher le QR code"; - readonly he: "הצג קוד QR"; - readonly ja: "QRコードを表示"; - readonly ko: "QR 코드 표시"; - readonly ru: "Показать QR код"; - readonly zh: "使用QR码"; - }; - readonly "Setup.ShowQRCode.Desc": { - readonly def: "Show QR code to transfer the settings."; - readonly fr: "Afficher le QR code pour transférer les paramètres."; - readonly he: "הצג קוד QR להעברת ההגדרות."; - readonly ja: "設定を転送するためのQRコードを表示します。"; - readonly ko: "설정을 전송하기 위한 QR 코드를 표시합니다."; - readonly ru: "Показать QR код для передачи настроек."; - readonly zh: "使用QR码来传递配置"; - }; - readonly "Setup.UseSetupURI.ButtonCancel": { - readonly def: "Cancel"; - readonly es: "Cancelar"; - readonly ja: "キャンセル"; - readonly ko: "취소"; - readonly ru: "Отмена"; - readonly zh: "取消"; - readonly "zh-tw": "取消"; - }; - readonly "Setup.UseSetupURI.ButtonProceed": { - readonly def: "Test Settings and Continue"; - readonly es: "Probar ajustes y continuar"; - readonly ja: "設定をテストして続行"; - readonly ko: "설정 테스트 후 계속"; - readonly ru: "Проверить настройки и продолжить"; - readonly zh: "测试设置并继续"; - readonly "zh-tw": "測試設定並繼續"; - }; - readonly "Setup.UseSetupURI.ErrorFailedToParse": { - readonly def: "Failed to parse the Setup URI. Please check the URI and passphrase."; - readonly es: "No se pudo procesar la URI de configuración. Revisa la URI y la frase de contraseña."; - readonly ja: "Setup URI を解析できませんでした。URI とパスフレーズを確認してください。"; - readonly ko: "Setup URI를 해석하지 못했습니다. URI와 패스프레이즈를 확인해 주세요."; - readonly ru: "Не удалось обработать Setup URI. Проверьте URI и парольную фразу."; - readonly zh: "无法解析 Setup URI,请检查 URI 和密码短语。"; - readonly "zh-tw": "無法解析 Setup URI,請檢查 URI 與密語。"; - }; - readonly "Setup.UseSetupURI.ErrorPassphraseRequired": { - readonly def: "Please enter the vault passphrase."; - readonly es: "Introduce la frase de contraseña del Vault."; - readonly ja: "Vault のパスフレーズを入力してください。"; - readonly ko: "Vault 패스프레이즈를 입력해 주세요."; - readonly ru: "Пожалуйста, введите парольную фразу Vault."; - readonly zh: "请输入 Vault 密码短语。"; - readonly "zh-tw": "請輸入 Vault 的密語。"; - }; - readonly "Setup.UseSetupURI.GuidanceLine1": { - readonly def: "Please enter the Setup URI that was generated during server installation or on another device, along with the vault passphrase."; - readonly es: "Introduce la URI de configuración que se generó durante la instalación del servidor o en otro dispositivo, junto con la frase de contraseña del Vault."; - readonly ja: "サーバーのセットアップ時または別の端末で生成された Setup URI と、Vault のパスフレーズを入力してください。"; - readonly ko: "서버 설치 중 또는 다른 기기에서 생성된 Setup URI와 Vault 패스프레이즈를 입력해 주세요."; - readonly ru: "Введите Setup URI, созданный во время установки сервера или на другом устройстве, а также парольную фразу Vault."; - readonly zh: "请输入在服务器安装期间或另一台设备上生成的 Setup URI,以及 Vault 密码短语。"; - readonly "zh-tw": "請輸入在伺服器安裝期間或其他裝置上產生的 Setup URI,以及 Vault 的密語。"; - }; - readonly "Setup.UseSetupURI.GuidanceLine2": { - readonly def: "Note that you can generate a new Setup URI by running the \"Copy settings as a new Setup URI\" command in the command palette."; - readonly es: "Ten en cuenta que puedes generar una nueva URI de configuración ejecutando el comando \"Copiar ajustes como nueva URI de configuración\" desde la paleta de comandos."; - readonly ja: "コマンドパレットで「設定を新しい Setup URI としてコピー」を実行すると、新しい Setup URI を生成できます。"; - readonly ko: "명령 팔레트에서 \"설정을 새 Setup URI로 복사\" 명령을 실행하면 새 Setup URI를 생성할 수 있습니다."; - readonly ru: "Новый Setup URI можно создать, выполнив в палитре команд команду «Скопировать настройки как новый Setup URI»."; - readonly zh: "你可以在命令面板中运行“将设置复制为新的 Setup URI”命令来生成新的 Setup URI。"; - readonly "zh-tw": "你可以在命令面板中執行「將設定複製為新的 Setup URI」命令來產生新的 Setup URI。"; - }; - readonly "Setup.UseSetupURI.InvalidInfo": { - readonly def: "The Setup URI is invalid. Please check it and try again."; - readonly es: "La URI de configuración no es válida. Revísala e inténtalo de nuevo."; - readonly ja: "Setup URI が無効です。内容を確認して再試行してください。"; - readonly ko: "Setup URI가 올바르지 않습니다. 확인한 뒤 다시 시도해 주세요."; - readonly ru: "Setup URI некорректен. Проверьте его и попробуйте снова."; - readonly zh: "Setup URI 无效,请检查后重试。"; - readonly "zh-tw": "Setup URI 無效,請檢查後再試一次。"; - }; - readonly "Setup.UseSetupURI.LabelPassphrase": { - readonly def: "Vault passphrase"; - readonly es: "Frase de contraseña del Vault"; - readonly ja: "Vault のパスフレーズ"; - readonly ko: "Vault 패스프레이즈"; - readonly ru: "Парольная фраза Vault"; - readonly zh: "Vault 密码短语"; - readonly "zh-tw": "Vault 密語"; - }; - readonly "Setup.UseSetupURI.LabelSetupURI": { - readonly def: "Setup URI"; - readonly es: "URI de configuración"; - readonly ja: "Setup URI"; - readonly ko: "Setup URI"; - readonly ru: "Setup URI"; - readonly zh: "Setup URI"; - readonly "zh-tw": "Setup URI"; - }; - readonly "Setup.UseSetupURI.PlaceholderPassphrase": { - readonly def: "Enter your vault passphrase"; - readonly es: "Introduce la frase de contraseña del Vault"; - readonly ja: "Vault のパスフレーズを入力してください"; - readonly ko: "Vault 패스프레이즈를 입력하세요"; - readonly ru: "Введите парольную фразу Vault"; - readonly zh: "输入 Vault 密码短语"; - readonly "zh-tw": "輸入 Vault 密語"; - }; - readonly "Setup.UseSetupURI.Title": { - readonly def: "Enter Setup URI"; - readonly es: "Introducir URI de configuración"; - readonly ja: "Setup URI を入力"; - readonly ko: "Setup URI 입력"; - readonly ru: "Ввести Setup URI"; - readonly zh: "输入 Setup URI"; - readonly "zh-tw": "輸入 Setup URI"; - }; - readonly "Setup.UseSetupURI.ValidInfo": { - readonly def: "The Setup URI is valid and ready to use."; - readonly es: "La URI de configuración es válida y está lista para usarse."; - readonly ja: "Setup URI は有効で、使用できます。"; - readonly ko: "Setup URI가 유효하며 사용할 준비가 되었습니다."; - readonly ru: "Setup URI корректен и готов к использованию."; - readonly zh: "Setup URI 有效,可以使用。"; - readonly "zh-tw": "Setup URI 有效,可以使用。"; - }; - readonly "Should we keep folders that don't have any files inside?": { - readonly def: "Should we keep folders that don't have any files inside?"; - readonly es: "¿Mantener carpetas vacías?"; - readonly fr: "Conserver les dossiers ne contenant aucun fichier ?"; - readonly he: "האם לשמור תיקיות שאין בהן קבצים?"; - readonly ja: "中にファイルがないフォルダーを保持しますか?"; - readonly ko: "내부에 파일이 없는 폴더를 유지하시겠습니까?"; - readonly ru: "Сохранять папки без файлов?"; - readonly zh: "我们是否应该保留内部没有任何文件的文件夹?"; - }; - readonly "Should we only check for conflicts when a file is opened?": { - readonly def: "Should we only check for conflicts when a file is opened?"; - readonly es: "¿Solo comprobar conflictos al abrir archivo?"; - readonly fr: "Ne vérifier les conflits qu'à l'ouverture d'un fichier ?"; - readonly he: "האם לבדוק קונפליקטים רק בעת פתיחת קובץ?"; - readonly ja: "ファイルを開いたときのみ競合をチェックしますか?"; - readonly ko: "파일을 열 때만 충돌을 확인하시겠습니까?"; - readonly ru: "Проверять конфликты только при открытии файла?"; - readonly zh: "我们是否应该仅在文件打开时检查冲突?"; - }; - readonly "Should we prompt you about conflicting files when a file is opened?": { - readonly def: "Should we prompt you about conflicting files when a file is opened?"; - readonly es: "¿Notificar sobre conflictos al abrir archivo?"; - readonly fr: "Vous demander au sujet des fichiers en conflit à l'ouverture d'un fichier ?"; - readonly he: "האם להציג בקשה לגבי קבצים מתנגשים בעת פתיחת קובץ?"; - readonly ja: "ファイルを開いたときに競合ファイルについて確認を求めますか?"; - readonly ko: "파일을 열 때 충돌하는 파일에 대해 알림을 표시하시겠습니까?"; - readonly ru: "Спрашивать о конфликтующих файлах при открытии файла?"; - readonly zh: "当文件打开时,是否提示冲突文件?"; - }; - readonly "Should we prompt you for every single merge, even if we can safely merge automatcially?": { - readonly def: "Should we prompt you for every single merge, even if we can safely merge automatcially?"; - readonly es: "¿Preguntar en cada fusión aunque sea automática?"; - readonly fr: "Vous demander pour chaque fusion, même si nous pouvons fusionner automatiquement en toute sécurité ?"; - readonly he: "האם להציג בקשת אישור לכל מיזוג יחיד, גם אם ניתן למזג בבטחה אוטומטית?"; - readonly ja: "自動的に安全にマージできる場合でも、すべてのマージについて確認を求めますか?"; - readonly ko: "안전하게 자동 병합할 수 있는 경우에도 모든 병합에 대해 알림을 받으시겠습니까?"; - readonly ru: "Спрашивать о каждом слиянии, даже если мы можем безопасно слить автоматически?"; - readonly zh: "即使我们可以安全地自动合并,是否也应该为每一次合并提示您?"; - }; - readonly "Show full banner": { - readonly def: "Show full banner"; - readonly es: "Mostrar banner completo"; - readonly ja: "完全なバナーを表示"; - readonly ko: "전체 배너 표시"; - readonly ru: "Показывать полный баннер"; - readonly zh: "显示完整横幅"; - readonly "zh-tw": "顯示完整橫幅"; - }; - readonly "Show history": { - readonly def: "Show history"; - readonly "zh-tw": "顯示歷程"; - }; - readonly "Show icon only": { - readonly def: "Show icon only"; - readonly zh: "仅显示图标"; - readonly "zh-tw": "僅顯示圖示"; - }; - readonly "Show only notifications": { - readonly def: "Show only notifications"; - readonly es: "Mostrar solo notificaciones"; - readonly fr: "N'afficher que les notifications"; - readonly he: "הצג התראות בלבד"; - readonly ja: "通知のみ表示"; - readonly ko: "알림만 표시"; - readonly ru: "Показывать только уведомления"; - readonly zh: "仅显示通知"; - }; - readonly "Show status as icons only": { - readonly def: "Show status as icons only"; - readonly es: "Mostrar estado solo con íconos"; - readonly fr: "N'afficher le statut que sous forme d'icônes"; - readonly he: "הצג סטטוס כאייקונים בלבד"; - readonly ja: "ステータス表示をアイコンのみにする"; - readonly ko: "아이콘으로만 상태 표시"; - readonly ru: "Показывать статус только иконками"; - readonly zh: "仅以图标显示状态"; - }; - readonly "Show status icon instead of file warnings banner": { - readonly def: "Show status icon instead of file warnings banner"; - readonly es: "Mostrar icono de estado en lugar del banner de advertencia de archivos"; - readonly fr: "Afficher l'icône de statut au lieu de la bannière d'avertissements"; - readonly he: "הצג אייקון סטטוס במקום פס אזהרות הקובץ"; - readonly ja: "ファイル警告バナーの代わりにステータスアイコンを表示"; - readonly ko: "파일 경고 배너 대신 상태 아이콘 표시"; - readonly ru: "Показывать иконку статуса вместо предупреждения о файлах"; - readonly zh: "显示状态图标,而非文件警告横幅"; - readonly "zh-tw": "以狀態圖示取代檔案警告橫幅"; - }; - readonly "Show status inside the editor": { - readonly def: "Show status inside the editor"; - readonly es: "Mostrar estado dentro del editor"; - readonly fr: "Afficher le statut dans l'éditeur"; - readonly he: "הצג סטטוס בתוך העורך"; - readonly ja: "ステータスをエディタ内に表示"; - readonly ko: "편집기 내부에 상태 표시"; - readonly ru: "Показывать статус внутри редактора"; - readonly zh: "在编辑器内显示状态"; - }; - readonly "Show status on the status bar": { - readonly def: "Show status on the status bar"; - readonly es: "Mostrar estado en la barra de estado"; - readonly fr: "Afficher le statut dans la barre d'état"; - readonly he: "הצג סטטוס בשורת המצב"; - readonly ja: "ステータスバーに、ステータスを表示"; - readonly ko: "상태 바에 상태 표시"; - readonly ru: "Показывать статус в строке состояния"; - readonly zh: "在状态栏上显示状态"; - }; - readonly "Show verbose log. Please enable if you report an issue.": { - readonly def: "Show verbose log. Please enable if you report an issue."; - readonly es: "Mostrar registro detallado. Actívelo si reporta un problema."; - readonly fr: "Afficher un journal verbeux. À activer si vous signalez un problème."; - readonly he: "הצג יומן מפורט. אנא הפעל אם אתה מדווח על בעיה."; - readonly ja: "エラー以外の詳細ログ項目も表示する。問題が発生した場合は有効にしてください。"; - readonly ko: "자세한 로그를 표시합니다. 문제를 신고하는 경우 활성화해 주세요."; - readonly ru: "Показывать подробный лог. Пожалуйста, включите при сообщении о проблеме."; - readonly zh: "显示详细日志。如果您报告问题,请启用此选项 "; - }; - readonly "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": { - readonly def: "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding."; - readonly ja: "一部のデバイスで進捗値が異なっています(最大: ${maxProgress}、最小: ${minProgress})。\nこれは一部のデバイスで同期が完了していない可能性を示しており、競合の原因になることがあります。続行する前に、すべてのデバイスが同期済みであることを確認することを強くおすすめします。"; - readonly ko: "일부 기기의 진행 값이 다릅니다(최대: ${maxProgress}, 최소: ${minProgress}).\n이는 일부 기기가 동기화를 완료하지 않았음을 의미할 수 있으며, 충돌로 이어질 수 있습니다. 계속 진행하기 전에 모든 기기가 동기화되었는지 반드시 확인하는 것을 강력히 권장합니다."; - readonly ru: "У некоторых устройств различаются значения прогресса (макс.: ${maxProgress}, мин.: ${minProgress}).\nЭто может означать, что некоторые устройства ещё не завершили синхронизацию, что может привести к конфликтам. Настоятельно рекомендуется перед продолжением убедиться, что все устройства синхронизированы."; - readonly zh: "某些设备的进度值不同(最大:${maxProgress},最小:${minProgress})。\n这可能表示某些设备尚未完成同步,从而可能引发冲突。强烈建议在继续之前先确认所有设备都已同步。"; - readonly "zh-tw": "某些裝置的進度值不同(最大:${maxProgress},最小:${minProgress})。\n這可能表示某些裝置尚未完成同步,進而可能導致衝突。強烈建議在繼續之前先確認所有裝置都已同步。"; - }; - readonly "Starts synchronisation when a file is saved.": { - readonly def: "Starts synchronisation when a file is saved."; - readonly es: "Inicia sincronización al guardar un archivo"; - readonly fr: "Démarre la synchronisation à l'enregistrement d'un fichier."; - readonly he: "מתחיל סנכרון כאשר קובץ נשמר."; - readonly ja: "ファイルが保存されたときに同期を開始します。"; - readonly ko: "파일이 저장될 때 동기화를 시작합니다."; - readonly ru: "Запускать синхронизацию при сохранении файла."; - readonly zh: "当文件保存时启动同步 "; - }; - readonly "Stop reflecting database changes to storage files.": { - readonly def: "Stop reflecting database changes to storage files."; - readonly es: "Dejar de reflejar cambios de BD en archivos"; - readonly fr: "Arrêter de répercuter les modifications de la base vers les fichiers de stockage."; - readonly he: "הפסק לשקף שינויי מסד נתונים לקבצי אחסון."; - readonly ja: "データベースの変更をストレージファイルに反映させない"; - readonly ko: "데이터베이스 변경 사항을 스토리지 파일에 반영하는 것을 중단합니다."; - readonly ru: "Остановить отражение изменений базы данных в файлы хранилища."; - readonly zh: "停止将数据库更改反映到存储文件 "; - }; - readonly "Stop watching for file changes.": { - readonly def: "Stop watching for file changes."; - readonly es: "Dejar de monitorear cambios en archivos"; - readonly fr: "Arrêter la surveillance des modifications de fichiers."; - readonly he: "הפסק לעקוב אחר שינויי קבצים."; - readonly ja: "監視の停止"; - readonly ko: "파일 변경 사항 감시를 중단합니다."; - readonly ru: "Остановить отслеживание изменений файлов."; - readonly zh: "停止监视文件更改 "; - }; - readonly "Storage -> Database": { - readonly def: "Storage -> Database"; - readonly "zh-tw": "儲存空間 -> 資料庫"; - }; - readonly "Suppress notification of hidden files change": { - readonly def: "Suppress notification of hidden files change"; - readonly es: "Suprimir notificaciones de cambios en archivos ocultos"; - readonly fr: "Supprimer les notifications de modification des fichiers cachés"; - readonly he: "דחוק התראת שינוי קבצים נסתרים"; - readonly ja: "隠しファイルの変更通知を抑制"; - readonly ko: "숨겨진 파일 변경 알림 억제"; - readonly ru: "Подавлять уведомления об изменении скрытых файлов"; - readonly zh: "暂停隐藏文件更改的通知"; - }; - readonly "Suspend database reflecting": { - readonly def: "Suspend database reflecting"; - readonly es: "Suspender reflejo de base de datos"; - readonly fr: "Suspendre la répercussion dans la base"; - readonly he: "השהה שיקוף מסד נתונים"; - readonly ja: "データベース反映の一時停止"; - readonly ko: "데이터베이스 반영 일시 중단"; - readonly ru: "Приостановить отражение базы данных"; - readonly zh: "暂停数据库反映"; - }; - readonly "Suspend file watching": { - readonly def: "Suspend file watching"; - readonly es: "Suspender monitorización de archivos"; - readonly fr: "Suspendre la surveillance des fichiers"; - readonly he: "השהה מעקב קבצים"; - readonly ja: "監視の一時停止"; - readonly ko: "파일 감시 일시 중단"; - readonly ru: "Приостановить отслеживание файлов"; - readonly zh: "暂停文件监视"; - }; - readonly "Switch to IDB": { - readonly def: "Switch to IDB"; - readonly es: "Cambiar a IDB"; - readonly ja: "IDB に切り替える"; - readonly ko: "IDB로 전환"; - readonly ru: "Переключиться на IDB"; - readonly zh: "切换到 IDB"; - readonly "zh-tw": "切換至 IDB"; - }; - readonly "Switch to IndexedDB": { - readonly def: "Switch to IndexedDB"; - readonly es: "Cambiar a IndexedDB"; - readonly ja: "IndexedDB に切り替える"; - readonly ko: "IndexedDB로 전환"; - readonly ru: "Переключиться на IndexedDB"; - readonly zh: "切换到 IndexedDB"; - readonly "zh-tw": "切換至 IndexedDB"; - }; - readonly "Sync after merging file": { - readonly def: "Sync after merging file"; - readonly es: "Sincronizar tras fusionar archivo"; - readonly fr: "Synchroniser après fusion d'un fichier"; - readonly he: "סנכרן לאחר מיזוג קובץ"; - readonly ja: "ファイルがマージ(統合)された時に同期"; - readonly ko: "파일 병합 후 동기화"; - readonly ru: "Синхронизировать после слияния файла"; - readonly zh: "合并文件后同步"; - }; - readonly "Sync automatically after merging files": { - readonly def: "Sync automatically after merging files"; - readonly es: "Sincronizar automáticamente tras fusionar archivos"; - readonly fr: "Synchroniser automatiquement après fusion des fichiers"; - readonly he: "סנכרן אוטומטית לאחר מיזוג קבצים"; - readonly ja: "ファイルのマージ後に自動的に同期"; - readonly ko: "파일 병합 후 자동으로 동기화"; - readonly ru: "Синхронизировать автоматически после слияния файлов"; - readonly zh: "合并文件后自动同步"; - }; - readonly "Sync Mode": { - readonly def: "Sync Mode"; - readonly es: "Modo de sincronización"; - readonly fr: "Mode de synchronisation"; - readonly he: "מצב סנכרון"; - readonly ja: "同期モード"; - readonly ko: "동기화 모드"; - readonly ru: "Режим синхронизации"; - readonly zh: "同步模式"; - }; - readonly "Sync on Editor Save": { - readonly def: "Sync on Editor Save"; - readonly es: "Sincronizar al guardar en editor"; - readonly fr: "Synchroniser à l'enregistrement dans l'éditeur"; - readonly he: "סנכרן בשמירת עורך"; - readonly ja: "エディタでの保存時に、同期されます"; - readonly ko: "편집기 저장 시 동기화"; - readonly ru: "Синхронизация при сохранении в редакторе"; - readonly zh: "编辑器保存时同步"; - }; - readonly "Sync on File Open": { - readonly def: "Sync on File Open"; - readonly es: "Sincronizar al abrir archivo"; - readonly fr: "Synchroniser à l'ouverture d'un fichier"; - readonly he: "סנכרן בפתיחת קובץ"; - readonly ja: "ファイルを開いた時に同期"; - readonly ko: "파일 열기 시 동기화"; - readonly ru: "Синхронизация при открытии файла"; - readonly zh: "打开文件时同步"; - }; - readonly "Sync on Save": { - readonly def: "Sync on Save"; - readonly es: "Sincronizar al guardar"; - readonly fr: "Synchroniser à l'enregistrement"; - readonly he: "סנכרן בשמירה"; - readonly ja: "保存時に同期"; - readonly ko: "저장 시 동기화"; - readonly ru: "Синхронизация при сохранении"; - readonly zh: "保存时同步"; - }; - readonly "Sync on Startup": { - readonly def: "Sync on Startup"; - readonly es: "Sincronizar al iniciar"; - readonly fr: "Synchroniser au démarrage"; - readonly he: "סנכרן בהפעלה"; - readonly ja: "起動時同期"; - readonly ko: "시작 시 동기화"; - readonly ru: "Синхронизация при запуске"; - readonly zh: "启动时同步"; - }; - readonly "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": { - readonly def: "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage."; - readonly es: "Sincronización mediante archivos de registro. Debe haber configurado un almacenamiento de objetos compatible con S3/MinIO/R2。"; - readonly ja: "ジャーナルファイルを利用する同期方式です。S3/MinIO/R2 互換のオブジェクトストレージを事前に構成しておく必要があります。"; - readonly ko: "저널 파일을 활용하는 동기화 방식입니다. S3/MinIO/R2 호환 객체 스토리지를 미리 구성해야 합니다。"; - readonly ru: "Синхронизация с использованием файлов журнала. Необходимо заранее настроить объектное хранилище, совместимое с S3/MinIO/R2。"; - readonly zh: "通过日志文件进行同步。你需要事先部署好兼容 S3/MinIO/R2 的对象存储服务。"; - readonly "zh-tw": "透過日誌檔進行同步。你需要事先部署好相容 S3/MinIO/R2 的物件儲存服務。"; - }; - readonly "Synchronising files": { - readonly def: "Synchronising files"; - readonly es: "Archivos sincronizados"; - readonly ja: "同期するファイル"; - readonly ko: "동기화할 파일"; - readonly ru: "Синхронизируемые файлы"; - readonly zh: "同步文件"; - readonly "zh-tw": "同步中的檔案"; - }; - readonly Syncing: { - readonly def: "Syncing"; - readonly es: "Sincronización"; - readonly ja: "同期"; - readonly ko: "동기화"; - readonly ru: "Синхронизация"; - readonly zh: "同步中"; - readonly "zh-tw": "同步中"; - }; - readonly "Target patterns": { - readonly def: "Target patterns"; - readonly es: "Patrones objetivo"; - readonly ja: "対象パターン"; - readonly ko: "대상 패턴"; - readonly ru: "Целевые шаблоны"; - readonly zh: "目标模式"; - readonly "zh-tw": "目標模式"; - }; - readonly "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": { - readonly def: "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned."; - readonly es: "Solo pruebas - Resolver conflictos sincronizando copias nuevas (puede sobrescribir modificaciones)"; - readonly fr: "Test uniquement - Résout les conflits de fichiers en synchronisant les copies plus récentes, ce qui peut écraser des fichiers modifiés. Prudence."; - readonly he: "לבדיקה בלבד - פתור קונפליקטי קבצים על ידי סנכרון עותקים חדשים יותר של הקובץ, פעולה זו עלולה לדרוס קבצים שונו. היה מוזהר."; - readonly ja: "テスト用 - ファイルの新しいコピーを同期してファイル競合を解決します。これにより変更されたファイルが上書きされる可能性があります。注意してください。"; - readonly ko: "테스트 전용 - 파일의 새로운 사본을 동기화하여 파일 충돌을 해결하며, 수정된 파일을 덮어쓸 수 있습니다. 주의하세요."; - readonly ru: "Только для тестирования - разрешать конфликты файлов синхронизацией новых копий."; - readonly zh: "仅供测试 - 通过同步文件的较新副本来解决文件冲突,这可能会覆盖修改过的文件。请注意 "; - }; - readonly "The delay for consecutive on-demand fetches": { - readonly def: "The delay for consecutive on-demand fetches"; - readonly es: "Retraso entre obtenciones consecutivas"; - readonly fr: "Le délai entre récupérations consécutives à la demande"; - readonly he: "העיכוב עבור משיכות לפי דרישה עוקבות"; - readonly ja: "連続したオンデマンドフェッチの遅延"; - readonly ko: "연속 청크 요청 간 대기 시간"; - readonly ru: "Задержка для последовательных запросов по требованию"; - readonly zh: "连续按需获取的延迟"; - }; - readonly "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": { - readonly def: "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once."; - readonly ja: "次の承認済みノードにはノード情報がありません:\n- ${missingNodes}\n\nこれは、それらがしばらく接続されていないか、古いバージョンのままになっていることを示しています。\n可能であれば、まずすべてのデバイスを更新することをおすすめします。すでに使用していないデバイスがある場合は、リモートを一度ロックすることで承認済みノードをすべてクリアできます。"; - readonly ko: "다음 승인된 노드에는 노드 정보가 없습니다:\n- ${missingNodes}\n\n이는 해당 노드가 한동안 연결되지 않았거나 이전 버전에 머물러 있음을 의미합니다.\n가능하다면 먼저 모든 기기를 업데이트하는 것이 좋습니다. 더 이상 사용하지 않는 기기가 있다면 원격을 한 번 잠가 승인된 노드를 모두 정리할 수 있습니다."; - readonly ru: "Для следующих принятых узлов отсутствует информация об узле:\n- ${missingNodes}\n\nЭто означает, что они давно не подключались или остались на старой версии.\nПо возможности рекомендуется сначала обновить все устройства. Если у вас есть устройства, которые больше не используются, вы можете очистить список всех принятых узлов, один раз заблокировав удалённую базу."; - readonly zh: "以下已接受节点缺少节点信息:\n- ${missingNodes}\n\n这表示它们已有一段时间未连接,或仍停留在较旧版本。\n如有可能,建议先更新所有设备。如果有已不再使用的设备,可以先锁定一次远程端以清除全部已接受节点。"; - readonly "zh-tw": "以下已接受節點缺少節點資訊:\n- ${missingNodes}\n\n這表示它們已有一段時間未連線,或仍停留在較舊版本。\n如有可能,建議先更新所有裝置。如果有已不再使用的裝置,可以先鎖定一次遠端端以清除全部已接受節點。"; - }; - readonly "The Hash algorithm for chunk IDs": { - readonly def: "The Hash algorithm for chunk IDs"; - readonly es: "Algoritmo hash para IDs de chunks"; - readonly fr: "L'algorithme de hachage pour les identifiants de fragments"; - readonly he: "אלגוריתם Hash עבור מזהי נתחים"; - readonly ja: "チャンクIDのハッシュアルゴリズム"; - readonly ko: "청크 ID용 해시 알고리즘"; - readonly ru: "Хэш-алгоритм для ID чанков"; - readonly zh: "块 ID 的哈希算法(实验性)"; - }; - readonly "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": { - readonly def: "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead."; - readonly "zh-tw": "IndexedDB 適配器在某些情況下通常能提供較佳效能,但在 LiveSync 模式下已發現可能導致記憶體洩漏。使用 LiveSync 模式時,請改用 IDB 適配器。"; - }; - readonly "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": { - readonly def: "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks."; - readonly es: "Duración máxima para incubar chunks. Excedentes se independizan"; - readonly fr: "La durée maximale pendant laquelle les fragments peuvent être incubés dans le document. Les fragments dépassant cette période seront promus en fragments indépendants."; - readonly he: "משך הזמן המקסימלי שנתחים יכולים להישמר זמנית בתוך המסמך. נתחים שחורגים מתקופה זו יהפכו לנתחים עצמאיים."; - readonly ja: "ドキュメント内でチャンクを保持できる最大期間。この期間を超えたチャンクは独立したチャンクに昇格します。"; - readonly ko: "변경 기록이 문서에 함께 보관될 수 있는 최대 시간입니다. 초과 시 문서에서 분리되어 개별로 저장됩니다."; - readonly ru: "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks."; - readonly zh: "文档中可以孵化的数据块的最大持续时间。超过此时间的数据块将成为独立数据块 "; - }; - readonly "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": { - readonly def: "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks."; - readonly es: "Número máximo de chunks que pueden incubarse en el documento. Excedentes se independizan"; - readonly fr: "Le nombre maximum de fragments pouvant être incubés dans le document. Les fragments dépassant ce nombre seront immédiatement promus en fragments indépendants."; - readonly he: "המספר המקסימלי של נתחים שיכולים להישמר זמנית בתוך המסמך. נתחים שחורגים ממספר זה יהפכו מיד לנתחים עצמאיים."; - readonly ja: "ドキュメント内で保持できるチャンクの最大数。この数を超えたチャンクは即座に独立したチャンクに昇格します。"; - readonly ko: "문서 안에 임시로 보관할 수 있는 변경 기록의 최대 개수입니다. 이 수를 초과하면 즉시 독립된 청크로 분리되어 저장됩니다."; - readonly ru: "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks."; - readonly zh: "文档中可以孵化的数据块的最大数量。超过此数量的数据块将立即成为独立数据块 "; - }; - readonly "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": { - readonly def: "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks."; - readonly es: "Tamaño total máximo de chunks incubados. Excedentes se independizan"; - readonly fr: "La taille totale maximale des fragments pouvant être incubés dans le document. Les fragments dépassant cette taille seront immédiatement promus en fragments indépendants."; - readonly he: "הגודל הכולל המקסימלי של נתחים שיכולים להישמר זמנית בתוך המסמך. נתחים שחורגים מגודל זה יהפכו מיד לנתחים עצמאיים."; - readonly ja: "ドキュメント内で保持できるチャンクの最大合計サイズ。このサイズを超えたチャンクは即座に独立したチャンクに昇格します。"; - readonly ko: "문서 안에 임시로 보관할 수 있는 변경 기록의 전체 크기 제한입니다. 초과 시 자동으로 분리됩니다."; - readonly ru: "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks."; - readonly zh: "文档中可以孵化的数据块的最大总大小。超过此大小的数据块将立即成为独立数据块 "; - }; - readonly "The minimum interval for automatic synchronisation on event.": { - readonly def: "The minimum interval for automatic synchronisation on event."; - readonly fr: "L'intervalle minimum pour la synchronisation automatique sur événement."; - readonly he: "מרווח הזמן המינימלי לסנכרון אוטומטי על אירוע."; - readonly ja: "イベント発生時の自動同期における最小間隔です。"; - readonly ko: "이벤트 발생 시 자동 동기화의 최소 간격입니다."; - readonly ru: "Минимальный интервал автоматической синхронизации по событию."; - readonly zh: "基于事件自动同步的最小间隔。"; - readonly "zh-tw": "事件觸發自動同步的最小間隔。"; - }; - readonly "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": { - readonly def: "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer."; - readonly es: "Esta función permite la sincronización directa entre dispositivos. No requiere servidor, pero ambos dispositivos deben estar en línea al mismo tiempo para que la sincronización se produzca, y algunas funciones pueden ser limitadas. La conexión a Internet solo se necesita para la señalización (detección de pares), no para la transferencia de datos。"; - readonly ja: "この機能では端末同士を直接同期できます。サーバーは不要ですが、同期を行うには両端末が同時にオンラインである必要があり、一部機能は制限されます。インターネット接続はシグナリング(ピア検出)にのみ必要で、データ転送自体には使われません。"; - readonly ko: "이 기능은 장치 간 직접 동기화를 제공합니다. 서버는 필요 없지만 동기화가 이루어지려면 두 장치가 동시에 온라인 상태여야 하며 일부 기능은 제한될 수 있습니다. 인터넷 연결은 시그널링(피어 감지)에만 필요하며 데이터 전송 자체에는 필요하지 않습니다。"; - readonly ru: "Эта функция обеспечивает прямую синхронизацию между устройствами. Сервер не требуется, но для синхронизации оба устройства должны быть одновременно в сети, а некоторые функции могут быть ограничены. Подключение к Интернету нужно только для сигнализации (обнаружения пиров), а не для передачи данных。"; - readonly zh: "此功能可在设备之间直接同步,无需服务器;但同步时两台设备必须同时在线,且部分功能可能受限。互联网连接仅用于信令(发现对端),不用于数据传输。"; - readonly "zh-tw": "此功能可在裝置之間直接同步,無需伺服器;但同步時兩台裝置必須同時在線,且部分功能可能受限。網際網路連線僅用於訊號交換(偵測對端),不用於資料傳輸。"; - }; - readonly "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": { - readonly def: "This is an advanced option for users who do not have a URI or who wish to configure detailed settings."; - readonly es: "Esta es una opción avanzada para usuarios que no disponen de un URI o que desean configurar parámetros detallados。"; - readonly ja: "URI を持っていない場合や、詳細設定を手動で行いたいユーザー向けの上級者オプションです。"; - readonly ko: "URI가 없거나 세부 설정을 직접 구성하려는 사용자를 위한 고급 옵션입니다。"; - readonly ru: "Это расширенный вариант для пользователей, у которых нет URI или которые хотят вручную задать подробные параметры。"; - readonly zh: "这是面向没有 URI 或希望手动配置详细参数的高级选项。"; - readonly "zh-tw": "這是面向沒有 URI 或希望手動設定詳細參數的進階選項。"; - }; - readonly "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": { - readonly def: "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance."; - readonly es: "Este es el método de sincronización más adecuado para el diseño. Todas las funciones están disponibles. Debe tener configurada una instancia de CouchDB。"; - readonly ja: "この設計に最も適した同期方式です。すべての機能が利用できます。CouchDB インスタンスを事前に構成しておく必要があります。"; - readonly ko: "이 설계에 가장 적합한 동기화 방식입니다. 모든 기능을 사용할 수 있습니다. CouchDB 인스턴스를 미리 구성해야 합니다。"; - readonly ru: "Это наиболее подходящий для данной архитектуры способ синхронизации. Доступны все функции. Необходимо заранее развернуть экземпляр CouchDB。"; - readonly zh: "这是最符合当前设计的同步方式,所有功能均可用。你需要事先部署好 CouchDB 实例。"; - readonly "zh-tw": "這是最符合目前設計的同步方式,所有功能皆可使用。你需要事先部署好 CouchDB 實例。"; - }; - readonly "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": { - readonly def: "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again."; - readonly es: "Esta frase no se copia a otros dispositivos. Usará `Default` hasta reconfigurar"; - readonly fr: "Cette phrase secrète ne sera pas copiée vers un autre appareil. Elle sera définie à `Default` jusqu'à ce que vous la configuriez à nouveau."; - readonly he: "ביטוי סיסמה זה לא יועתק למכשיר אחר. הוא יוגדר ל-`Default` עד שתגדיר אותו שוב."; - readonly ja: "このパスフレーズは他のデバイスにコピーされません。再度設定するまで`Default`に設定されます。"; - readonly ko: "이 패스프레이즈는 다른 기기로 복사되지 않습니다. 다시 구성할 때까지 `기본값`으로 설정됩니다."; - readonly ru: "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again."; - readonly zh: "此密码不会复制到另一台设备。在您再次配置之前,它将设置为 `Default` "; - }; - readonly "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": { - readonly def: "This will recreate chunks for all files. If there were missing chunks, this may fix the errors."; - readonly es: "Esto recreará los fragmentos de todos los archivos. Si faltaban fragmentos, esto puede corregir los errores."; - readonly ja: "すべてのファイルについてチャンクを再作成します。欠損しているチャンクがあった場合、エラーが解消される可能性があります。"; - readonly ko: "모든 파일의 청크를 다시 생성합니다. 누락된 청크가 있었다면 이 작업으로 오류가 해결될 수 있습니다."; - readonly ru: "Это пересоздаст чанки для всех файлов. Если какие-то чанки отсутствовали, это может исправить ошибки."; - readonly zh: "这会为所有文件重新生成 chunks。如果之前存在缺失的 chunks,这可能修复相关错误。"; - readonly "zh-tw": "這會為所有檔案重新建立 chunks。若先前有遺失的 chunks,這可能修復相關錯誤。"; - }; - readonly "Transfer Tweak": { - readonly def: "Transfer Tweak"; - readonly es: "Ajustes de transferencia"; - readonly ja: "転送の調整"; - readonly ko: "전송 조정"; - readonly ru: "Настройки передачи"; - }; - readonly "TweakMismatchResolve.Action.DisableAutoAcceptCompatible": { - readonly def: "Disable auto-accept"; - }; - readonly "TweakMismatchResolve.Action.Dismiss": { - readonly def: "Dismiss"; - readonly fr: "Ignorer"; - readonly he: "דחה"; - readonly ja: "無視"; - readonly ko: "무시"; - readonly ru: "Отмена"; - readonly zh: "Dismiss"; - }; - readonly "TweakMismatchResolve.Action.EnableAutoAcceptCompatible": { - readonly def: "Enable auto-accept"; - }; - readonly "TweakMismatchResolve.Action.UseConfigured": { - readonly def: "Use configured settings"; - readonly fr: "Utiliser les paramètres configurés"; - readonly he: "השתמש בהגדרות המוגדרות"; - readonly ja: "設定済みの設定を使用"; - readonly ko: "구성된 설정 사용"; - readonly ru: "Использовать настроенные параметры"; - readonly zh: "Use configured settings"; - }; - readonly "TweakMismatchResolve.Action.UseMine": { - readonly def: "Update remote database settings"; - readonly fr: "Mettre à jour les paramètres de la base distante"; - readonly he: "עדכן הגדרות מסד הנתונים המרוחד"; - readonly ja: "リモートデータベースの設定を更新"; - readonly ko: "원격 데이터베이스 설정 업데이트"; - readonly ru: "Обновить настройки удалённой базы данных"; - readonly zh: "Update remote database settings"; - }; - readonly "TweakMismatchResolve.Action.UseMineAcceptIncompatible": { - readonly def: "Update remote database settings but keep as is"; - readonly fr: "Mettre à jour la base distante mais garder en l'état"; - readonly he: "עדכן הגדרות מסד הנתונים המרוחד אך השאר כפי שהוא"; - readonly ja: "リモートデータベースの設定を更新するがそのまま維持"; - readonly ko: "원격 데이터베이스 설정 업데이트하지만 그대로 유지"; - readonly ru: "Обновить настройки, но оставить как есть"; - readonly zh: "Update remote database settings but keep as is"; - }; - readonly "TweakMismatchResolve.Action.UseMineWithRebuild": { - readonly def: "Update remote database settings and rebuild again"; - readonly fr: "Mettre à jour la base distante et reconstruire"; - readonly he: "עדכן הגדרות מסד הנתונים המרוחד ובנה מחדש"; - readonly ja: "リモートデータベースの設定を更新して再構築"; - readonly ko: "원격 데이터베이스 설정 업데이트하고 다시 재구축"; - readonly ru: "Обновить настройки и перестроить снова"; - readonly zh: "Update remote database settings and rebuild again"; - }; - readonly "TweakMismatchResolve.Action.UseRemote": { - readonly def: "Apply settings to this device"; - readonly fr: "Appliquer les paramètres à cet appareil"; - readonly he: "החל הגדרות על מכשיר זה"; - readonly ja: "このデバイスに設定を適用"; - readonly ko: "이 기기에 설정 적용"; - readonly ru: "Применить настройки к этому устройству"; - readonly zh: "Apply settings to this device"; - }; - readonly "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": { - readonly def: "Apply settings to this device, but and ignore incompatibility"; - readonly fr: "Appliquer à cet appareil, mais ignorer l'incompatibilité"; - readonly he: "החל הגדרות על מכשיר זה, אך התעלם מאי-תאימות"; - readonly ja: "このデバイスに設定を適用し、非互換性を無視"; - readonly ko: "이 기기에 설정 적용하지만 호환성 문제 무시"; - readonly ru: "Применить настройки, но игнорировать несовместимость"; - readonly zh: "Apply settings to this device, but and ignore incompatibility"; - }; - readonly "TweakMismatchResolve.Action.UseRemoteWithRebuild": { - readonly def: "Apply settings to this device, and fetch again"; - readonly fr: "Appliquer à cet appareil et récupérer à nouveau"; - readonly he: "החל הגדרות על מכשיר זה ומשוך שוב"; - readonly ja: "このデバイスに設定を適用し、再フェッチ"; - readonly ko: "이 기기에 설정 적용하고 다시 가져오기"; - readonly ru: "Применить настройки и загрузить снова"; - readonly zh: "Apply settings to this device, and fetch again"; - }; - readonly "TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined": { - readonly def: "\nIt appears that the settings differ for each device. You can now automatically apply compatible changes to these configurations.\nWould you like to enable this `auto-accept` setting?"; - }; - readonly "TweakMismatchResolve.Message.Main": { - readonly def: "\nThe settings in the remote database are as follows. These values are configured by other devices, which are synchronised with this device at least once.\n\nIf you want to use these settings, please select Use configured settings.\nIf you want to keep the settings of this device, please select Dismiss.\n\n${table}\n\n>[!TIP]\n> If you want to synchronise all settings, please use `Sync settings via markdown` after applying minimal configuration with this feature.\n\n${additionalMessage}"; - readonly fr: "\nLes paramètres de la base distante sont les suivants. Ces valeurs sont configurées par d'autres appareils, synchronisés au moins une fois avec celui-ci.\n\nPour utiliser ces paramètres, sélectionnez Utiliser les paramètres configurés.\nPour conserver les paramètres de cet appareil, sélectionnez Ignorer.\n\n${table}\n\n>[!ASTUCE]\n> Pour synchroniser tous les paramètres, utilisez « Synchroniser les paramètres via markdown » après application de la configuration minimale avec cette fonctionnalité.\n\n${additionalMessage}"; - readonly he: "\nההגדרות במסד הנתונים המרוחד הן כדלקמן. ערכים אלה הוגדרו על ידי מכשירים אחרים, אשר סונכרנו עם מכשיר זה לפחות פעם אחת.\n\nאם ברצונך להשתמש בהגדרות אלה, אנא בחר %{TweakMismatchResolve.Action.UseConfigured}.\nאם ברצונך לשמור את הגדרות מכשיר זה, אנא בחר %{TweakMismatchResolve.Action.Dismiss}.\n\n${table}\n\n>[!TIP]\n> אם ברצונך לסנכרן את כל ההגדרות, אנא השתמש ב-`סנכרון הגדרות דרך Markdown` לאחר החלת תצורה מינימלית עם תכונה זו.\n\n${additionalMessage}"; - readonly ja: "\nリモートデータベースの設定は以下の通りです。これらの値は、このデバイスと少なくとも1回同期された他のデバイスによって設定されています。\n\nこれらの設定を使用する場合は、設定済みの設定を使用を選択してください。\nこのデバイスの設定を維持する場合は、無視を選択してください。\n\n${table}\n\n>[!TIP]\n> すべての設定を同期したい場合は、この機能で最小限の設定を適用した後、`Sync settings via markdown`を使用してください。\n\n${additionalMessage}"; - readonly ko: "\n원격 데이터베이스의 설정은 다음과 같습니다. 이 값들은 이 기기와 최소 한 번 동기화된 다른 기기에서 구성된 것입니다.\n\n이 설정을 사용하려면 구성된 설정 사용를 선택해 주세요.\n이 기기의 설정을 유지하려면 무시를 선택해 주세요.\n\n${table}\n\n>[!TIP]\n> 모든 설정을 동기화하려면 이 기능으로 최소 구성을 적용한 후 `마크다운을 통한 설정 동기화`를 사용해 주세요.\n\n${additionalMessage}"; - readonly ru: "Настройки в удалённой базе данных следующие. Эти значения настроены другими устройствами."; - readonly zh: "\nThe settings in the remote database are as follows. These values are configured by other devices, which are synchronised with this device at least once.\n\nIf you want to use these settings, please select Use configured settings.\nIf you want to keep the settings of this device, please select Dismiss.\n\n${table}\n\n>[!TIP]\n> If you want to synchronise all settings, please use `Sync settings via markdown` after applying minimal configuration with this feature.\n\n${additionalMessage}"; - }; - readonly "TweakMismatchResolve.Message.MainTweakResolving": { - readonly def: "Your configuration has not been matched with the one on the remote server.\n\nFollowing configuration should be matched:\n\n${table}\n\nLet us know your decision.\n\n${additionalMessage}"; - readonly fr: "Votre configuration ne correspond pas à celle du serveur distant.\n\nLa configuration suivante devrait correspondre :\n\n${table}\n\nFaites-nous part de votre décision.\n\n${additionalMessage}"; - readonly he: "התצורה שלך אינה תואמת לזו שבשרת המרוחד.\n\nיש להתאים את התצורות הבאות:\n\n${table}\n\nאנא הודע לנו על החלטתך.\n\n${additionalMessage}"; - readonly ja: "設定がリモートサーバーの設定と一致しません。\n\n以下の設定が一致している必要があります:\n\n${table}\n\n判断をお知らせください。\n\n${additionalMessage}"; - readonly ko: "구성이 원격 서버의 것과 일치하지 않습니다.\n\n다음 구성이 일치해야 합니다:\n\n${table}\n\n결정을 알려주세요.\n\n${additionalMessage}"; - readonly ru: "Ваша конфигурация не совпадает с удалённым сервером."; - readonly zh: "Your configuration has not been matched with the one on the remote server.\n\nFollowing configuration should be matched:\n\n${table}\n\nLet us know your decision.\n\n${additionalMessage}"; - }; - readonly "TweakMismatchResolve.Message.mineUpdated": { - readonly def: "The device configuration have been adjusted."; - }; - readonly "TweakMismatchResolve.Message.remoteUpdated": { - readonly def: "The configuration stored remotely has been updated."; - }; - readonly "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": { - readonly def: "\n>[!NOTICE]\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - readonly fr: "\n>[!AVIS]\n> Certains changements sont compatibles mais peuvent consommer du stockage et du trafic supplémentaires. Une reconstruction est recommandée. Cependant, elle peut ne pas être effectuée maintenant, mais pourra l'être lors d'une maintenance future.\n> ***Assurez-vous d'avoir du temps et une connexion stable pour appliquer !***"; - readonly he: "\n>[!NOTICE]\n> חלק מהשינויים תואמים אך עלולים לצרוך אחסון ותעבורה נוספים. מומלצת בנייה מחדש. עם זאת, ייתכן שבנייה מחדש לא תתבצע כעת, אך תיושם בתחזוקה עתידית.\n> ***ודא שיש לך זמן ושאתה מחובר לרשת יציבה לפני ההחלה!***"; - readonly ja: "\n>[!NOTICE]\n> 一部の変更は互換性がありますが、追加のストレージと転送量を消費する可能性があります。再構築をお勧めします。ただし、再構築は現時点では実行されない場合がありますが、将来のメンテナンスで実装される可能性があります。\n> ***適用には時間と安定したネットワーク接続が必要です!***"; - readonly ko: "\n>[!NOTICE]\n> 일부 변경사항은 호환 가능하지만 추가 스토리지 및 전송량을 소모할 수 있습니다. 재구축을 권장합니다. 하지만 현재 재구축을 수행하지 않더라도 향후 유지보수에서 구현될 수 있습니다.\n> ***시간적 여유가 있고 안정적인 네트워크에 연결된 상태에서 적용해 주세요!***"; - readonly ru: "Некоторые изменения совместимы, но могут потребовать дополнительного хранилища. Рекомендуется перестроение."; - readonly zh: "\n>[!NOTICE]\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - }; - readonly "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": { - readonly def: "\n>[!WARNING]\n> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - readonly fr: "\n>[!AVERTISSEMENT]\n> Certaines configurations distantes ne sont pas compatibles avec la base locale de cet appareil. Une reconstruction de la base locale sera requise.\n> ***Assurez-vous d'avoir du temps et une connexion stable pour appliquer !***"; - readonly he: "\n>[!WARNING]\n> חלק מהתצורות המרוחקות אינן תואמות למסד הנתונים המקומי של מכשיר זה. נדרשת בנייה מחדש של מסד הנתונים המקומי.\n> ***ודא שיש לך זמן ושאתה מחובר לרשת יציבה לפני ההחלה!***"; - readonly ja: "\n>[!WARNING]\n> 一部のリモート設定はこのデバイスのローカルデータベースと互換性がありません。ローカルデータベースの再構築が必要です。\n> ***適用には時間と安定したネットワーク接続が必要です!***"; - readonly ko: "\n>[!WARNING]\n> 일부 원격 구성이 이 기기의 로컬 데이터베이스와 호환되지 않습니다. 로컬 데이터베이스 재구축이 필요합니다.\n> ***시간적 여유가 있고 안정적인 네트워크에 연결된 상태에서 적용해 주세요!***"; - readonly ru: "Некоторые удалённые конфигурации несовместимы с локальной базой данных. Требуется перестроение."; - readonly zh: "\n>[!WARNING]\n> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.\n> ***Please ensure that you have time and are connected to a stable network to apply!***"; - }; - readonly "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": { - readonly def: "\n>[!NOTICE]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> If you want to rebuild, it takes a few minutes or more. **Make sure it is safe to perform it now.**"; - readonly fr: "\n>[!AVIS]\n> Nous avons détecté que certaines valeurs diffèrent et rendent la base locale incompatible avec la base distante.\n> Certains changements sont compatibles mais peuvent consommer du stockage et du trafic supplémentaires. Une reconstruction est recommandée. Cependant, elle peut ne pas être effectuée maintenant, mais pourra l'être lors d'une maintenance future.\n> Si vous souhaitez reconstruire, cela prend quelques minutes ou plus. **Assurez-vous qu'il est sûr de le faire maintenant.**"; - readonly he: "\n>[!NOTICE]\n> זיהינו שחלק מהערכים שונים, מה שגורם לאי-תאימות בין מסד הנתונים המקומי למרוחד.\n> חלק מהשינויים תואמים אך עלולים לצרוך אחסון ותעבורה נוספים. מומלצת בנייה מחדש. עם זאת, ייתכן שבנייה מחדש לא תתבצע כעת, אך תיושם בתחזוקה עתידית.\n> אם ברצונך לבנות מחדש, הדבר ייקח כמה דקות או יותר. **ודא שבטוח לבצע זאת עכשיו.**"; - readonly ja: "\n>[!NOTICE]\n> ローカルデータベースとリモートデータベースの非互換性を引き起こす値の違いが検出されました。\n> 一部の変更は互換性がありますが、追加のストレージと転送量を消費する可能性があります。再構築をお勧めします。ただし、再構築は現時点では実行されない場合がありますが、将来のメンテナンスで実装される可能性があります。\n> 再構築を行う場合は数分以上かかります。**今実行しても安全か確認してください。**"; - readonly ko: "\n>[!NOTICE]\n> 로컬 데이터베이스와 원격 데이터베이스가 호환되지 않도록 만드는 값들이 다른 것을 감지했습니다.\n> 일부 변경사항은 호환 가능하지만 추가 스토리지 및 전송량을 소모할 수 있습니다. 재구축을 권장합니다. 하지만 현재 재구축을 수행하지 않더라도 향후 유지보수에서 구현될 수 있습니다.\n> 재구축을 원한다면 몇 분 이상 소요됩니다. **지금 수행해도 안전한지 확인해 주세요.**"; - readonly ru: "Обнаружены значения, несовместимые с удалённой базой данных. Рекомендуется перестроение."; - readonly zh: "\n>[!NOTICE]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Some changes are compatible but may consume extra storage and transfer volumes. A rebuild is recommended. However, a rebuild may not be performed at present, but may be implemented in future maintenance.\n> If you want to rebuild, it takes a few minutes or more. **Make sure it is safe to perform it now.**"; - }; - readonly "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": { - readonly def: "\n>[!WARNING]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Either local or remote rebuilds are required. Both of them takes a few minutes or more. **Make sure it is safe to perform it now.**"; - readonly fr: "\n>[!AVERTISSEMENT]\n> Nous avons détecté que certaines valeurs diffèrent et rendent la base locale incompatible avec la base distante.\n> Une reconstruction locale ou distante est nécessaire. L'une comme l'autre prend quelques minutes ou plus. **Assurez-vous qu'il est sûr de le faire maintenant.**"; - readonly he: "\n>[!WARNING]\n> זיהינו שחלק מהערכים שונים, מה שגורם לאי-תאימות בין מסד הנתונים המקומי למרוחד.\n> נדרשת בנייה מחדש של המסד המקומי או המרוחד. שניהם ייקחו כמה דקות או יותר. **ודא שבטוח לבצע זאת עכשיו.**"; - readonly ja: "\n>[!WARNING]\n> ローカルデータベースとリモートデータベースの非互換性を引き起こす値の違いが検出されました。\n> ローカルまたはリモートの再構築が必要です。どちらも数分以上かかります。**今実行しても安全か確認してください。**"; - readonly ko: "\n>[!WARNING]\n> 로컬 데이터베이스와 원격 데이터베이스가 호환되지 않도록 만드는 값들이 다른 것을 감지했습니다.\n> 로컬 또는 원격 재구축이 필요합니다. 둘 다 몇 분 이상 소요됩니다. **지금 수행해도 안전한지 확인해 주세요.**"; - readonly ru: "Обнаружены значения, несовместимые с удалённой базой данных. Требуется перестроение."; - readonly zh: "\n>[!WARNING]\n> We have detected that some of the values are different to make incompatible the local database with the remote database.\n> Either local or remote rebuilds are required. Both of them takes a few minutes or more. **Make sure it is safe to perform it now.**"; - }; - readonly "TweakMismatchResolve.Table": { - readonly def: "| Value name | This device | On Remote |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly fr: "| Nom de la valeur | Cet appareil | Sur le distant |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly he: "| שם ערך | מכשיר זה | מרוחד |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly ja: "| 値の名前 | このデバイス | リモート |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly ko: "| 값 이름 | 이 기기 | 원격 |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - readonly ru: "| Имя значения | Это устройство | На удалённом |\n|: --- |: ---- :|: ---- :|"; - readonly zh: "| Value name | This device | On Remote |\n|: --- |: ---- :|: ---- :|\n${rows}\n\n"; - }; - readonly "TweakMismatchResolve.Table.Row": { - readonly def: "| ${name} | ${self} | ${remote} |"; - readonly fr: "| ${name} | ${self} | ${remote} |"; - readonly he: "| ${name} | ${self} | ${remote} |"; - readonly ja: "| ${name} | ${self} | ${remote} |"; - readonly ko: "| ${name} | ${self} | ${remote} |"; - readonly ru: "| name | self | remote |"; - readonly zh: "| ${name} | ${self} | ${remote} |"; - }; - readonly "TweakMismatchResolve.Title": { - readonly def: "Configuration Mismatch Detected"; - readonly fr: "Incohérence de configuration détectée"; - readonly he: "זוהתה אי-התאמה בתצורה"; - readonly ja: "設定の不一致が検出されました"; - readonly ko: "구성 불일치 감지"; - readonly ru: "Обнаружено несоответствие конфигурации"; - readonly zh: "Configuration Mismatch Detected"; - }; - readonly "TweakMismatchResolve.Title.AutoAcceptCompatible": { - readonly def: "Auto-Accept Available"; - }; - readonly "TweakMismatchResolve.Title.TweakResolving": { - readonly def: "Configuration Mismatch Detected"; - readonly fr: "Incohérence de configuration détectée"; - readonly he: "זוהתה אי-התאמה בתצורה"; - readonly ja: "設定の不一致が検出されました"; - readonly ko: "구성 불일치 감지"; - readonly ru: "Обнаружено несоответствие конфигурации"; - readonly zh: "Configuration Mismatch Detected"; - }; - readonly "TweakMismatchResolve.Title.UseRemoteConfig": { - readonly def: "Use Remote Configuration"; - readonly fr: "Utiliser la configuration distante"; - readonly he: "השתמש בתצורה המרוחקת"; - readonly ja: "リモート設定を使用"; - readonly ko: "원격 구성 사용"; - readonly ru: "Использовать удалённую конфигурацию"; - readonly zh: "Use Remote Configuration"; - }; - readonly "Ui.Common.Signal.Caution": { - readonly def: "CAUTION"; - readonly zh: "注意"; - }; - readonly "Ui.Common.Signal.Danger": { - readonly def: "DANGER"; - readonly zh: "危险"; - }; - readonly "Ui.Common.Signal.Notice": { - readonly def: "NOTICE"; - readonly zh: "提示"; - }; - readonly "Ui.Common.Signal.Warning": { - readonly def: "WARNING"; - readonly zh: "警告"; - }; - readonly "Ui.Settings.Advanced.LocalDatabaseTweak": { - readonly def: "Local Database Tweak"; - readonly zh: "本地数据库调整"; - }; - readonly "Ui.Settings.Advanced.MemoryCache": { - readonly def: "Memory Cache"; - readonly zh: "内存缓存"; - }; - readonly "Ui.Settings.Advanced.TransferTweak": { - readonly def: "Transfer Tweak"; - readonly zh: "传输调整"; - }; - readonly "Ui.Settings.Common.Analyse": { - readonly def: "Analyse"; - readonly zh: "分析"; - }; - readonly "Ui.Settings.Common.Back": { - readonly def: "Back"; - readonly zh: "返回"; - }; - readonly "Ui.Settings.Common.Check": { - readonly def: "Check"; - readonly zh: "检查"; - }; - readonly "Ui.Settings.Common.Configure": { - readonly def: "Configure"; - readonly zh: "配置"; - }; - readonly "Ui.Settings.Common.Continue": { - readonly def: "Continue"; - readonly zh: "继续"; - }; - readonly "Ui.Settings.Common.Delete": { - readonly def: "Delete"; - readonly zh: "删除"; - }; - readonly "Ui.Settings.Common.Fetch": { - readonly def: "Fetch"; - readonly zh: "获取"; - }; - readonly "Ui.Settings.Common.Lock": { - readonly def: "Lock"; - readonly zh: "锁定"; - }; - readonly "Ui.Settings.Common.Merge": { - readonly def: "Merge"; - readonly zh: "合并"; - }; - readonly "Ui.Settings.Common.Open": { - readonly def: "Open"; - readonly zh: "打开"; - }; - readonly "Ui.Settings.Common.Overwrite": { - readonly def: "Overwrite"; - readonly zh: "覆盖"; - }; - readonly "Ui.Settings.Common.Perform": { - readonly def: "Perform"; - readonly zh: "执行"; - }; - readonly "Ui.Settings.Common.ResetAll": { - readonly def: "Reset all"; - readonly zh: "全部重置"; - }; - readonly "Ui.Settings.Common.ResolveAll": { - readonly def: "Resolve All"; - readonly zh: "全部解决"; - }; - readonly "Ui.Settings.Common.Scan": { - readonly def: "Scan"; - readonly zh: "扫描"; - }; - readonly "Ui.Settings.Common.Send": { - readonly def: "Send"; - readonly zh: "发送"; - }; - readonly "Ui.Settings.Common.Use": { - readonly def: "Use"; - readonly zh: "使用"; - }; - readonly "Ui.Settings.Common.VerifyAll": { - readonly def: "Verify all"; - readonly zh: "全部校验"; - }; - readonly "Ui.Settings.CustomizationSync.OpenDesc": { - readonly def: "Open the dialog"; - readonly zh: "打开此对话框"; - }; - readonly "Ui.Settings.CustomizationSync.Panel": { - readonly def: "Customization Sync"; - readonly zh: "自定义同步"; - }; - readonly "Ui.Settings.CustomizationSync.WarnChangeDeviceName": { - readonly def: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name."; - readonly zh: "启用此功能时无法修改设备名称。请先关闭此功能,再修改设备名称。"; - }; - readonly "Ui.Settings.CustomizationSync.WarnSetDeviceName": { - readonly def: "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature."; - readonly zh: "请先设置用于标识此设备的设备名称。该名称应在你的设备之间保持唯一。未设置前无法启用此功能。"; - }; - readonly "Ui.Settings.Hatch.AnalyseDatabaseUsage": { - readonly def: "Analyse database usage"; - readonly zh: "分析数据库使用情况"; - }; - readonly "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": { - readonly def: "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like."; - readonly zh: "分析数据库使用情况,并生成 TSV 报告供你自行诊断。你可以将生成的报告粘贴到任意电子表格工具中查看。"; - }; - readonly "Ui.Settings.Hatch.BackToNonConfigured": { - readonly def: "Back to non-configured"; - readonly zh: "返回未配置状态"; - }; - readonly "Ui.Settings.Hatch.ConvertNonObfuscated": { - readonly def: "Check and convert non-path-obfuscated files"; - readonly zh: "检查并转换未进行路径混淆的文件"; - }; - readonly "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": { - readonly def: "Check the local database for files that were stored without path obfuscation and convert them when needed."; - readonly zh: "检查本地数据库中未按路径混淆方式存储的文件,并在需要时将其转换为正确格式。"; - }; - readonly "Ui.Settings.Hatch.CopyIssueReport": { - readonly def: "Copy Report to clipboard"; - readonly zh: "复制报告到剪贴板"; - }; - readonly "Ui.Settings.Hatch.DatabaseLabel": { - readonly def: "Database: ${details}"; - readonly zh: "数据库:${details}"; - }; - readonly "Ui.Settings.Hatch.DatabaseToStorage": { - readonly def: "Database -> Storage"; - readonly zh: "数据库 -> 存储"; - }; - readonly "Ui.Settings.Hatch.DeleteCustomizationSyncData": { - readonly def: "Delete all customization sync data"; - readonly zh: "删除所有自定义同步数据"; - }; - readonly "Ui.Settings.Hatch.GeneratedReport": { - readonly def: "Generated report"; - readonly zh: "已生成的报告"; - }; - readonly "Ui.Settings.Hatch.Missing": { - readonly def: "Missing"; - readonly zh: "缺失"; - }; - readonly "Ui.Settings.Hatch.ModifiedSize": { - readonly def: "Modified: ${modified}, Size: ${size}"; - readonly zh: "修改时间:${modified},大小:${size}"; - }; - readonly "Ui.Settings.Hatch.ModifiedSizeActual": { - readonly def: "Modified: ${modified}, Size: ${size} (actual size: ${actualSize})"; - readonly zh: "修改时间:${modified},大小:${size}(实际大小:${actualSize})"; - }; - readonly "Ui.Settings.Hatch.PrepareIssueReport": { - readonly def: "Prepare the 'report' to create an issue"; - readonly zh: "准备用于提交问题的报告"; - }; - readonly "Ui.Settings.Hatch.RecoveryAndRepair": { - readonly def: "Recovery and Repair"; - readonly zh: "恢复与修复"; - }; - readonly "Ui.Settings.Hatch.RecreateAll": { - readonly def: "Recreate all"; - readonly zh: "全部重建"; - }; - readonly "Ui.Settings.Hatch.RecreateMissingChunks": { - readonly def: "Recreate missing chunks for all files"; - readonly zh: "为所有文件重新创建缺失的数据块"; - }; - readonly "Ui.Settings.Hatch.RecreateMissingChunksDesc": { - readonly def: "This will recreate chunks for all files. If there were missing chunks, this may fix the errors."; - readonly zh: "此操作会为所有文件重新创建数据块。如果存在缺失的数据块,可能会修复相关错误。"; - }; - readonly "Ui.Settings.Hatch.ResetPanel": { - readonly def: "Reset"; - readonly zh: "重置"; - }; - readonly "Ui.Settings.Hatch.ResetRemoteUsage": { - readonly def: "Reset notification threshold and check the remote database usage"; - readonly zh: "重置通知阈值并检查远程数据库使用情况"; - }; - readonly "Ui.Settings.Hatch.ResetRemoteUsageDesc": { - readonly def: "Reset the remote storage size threshold and check the remote storage size again."; - readonly zh: "重置远程存储大小阈值,并再次检查远程存储大小。"; - }; - readonly "Ui.Settings.Hatch.ResolveAllConflictedFiles": { - readonly def: "Resolve all conflicted files by the newer one"; - readonly zh: "使用较新的版本解决所有冲突文件"; - }; - readonly "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": { - readonly def: "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one."; - readonly zh: "使用较新的版本解决所有冲突文件。注意:此操作会覆盖较旧版本,且无法恢复被覆盖的内容。"; - }; - readonly "Ui.Settings.Hatch.RunDoctor": { - readonly def: "Run Doctor"; - readonly zh: "运行诊断"; - }; - readonly "Ui.Settings.Hatch.ScanBrokenFiles": { - readonly def: "Scan for broken files"; - readonly zh: "扫描损坏文件"; - }; - readonly "Ui.Settings.Hatch.ScramSwitches": { - readonly def: "Scram Switches"; - readonly zh: "紧急开关"; - }; - readonly "Ui.Settings.Hatch.ShowHistory": { - readonly def: "Show history"; - readonly zh: "查看历史"; - }; - readonly "Ui.Settings.Hatch.StorageLabel": { - readonly def: "Storage: ${details}"; - readonly zh: "存储:${details}"; - }; - readonly "Ui.Settings.Hatch.StorageToDatabase": { - readonly def: "Storage -> Database"; - readonly zh: "存储 -> 数据库"; - }; - readonly "Ui.Settings.Hatch.VerifyAndRepairAllFiles": { - readonly def: "Verify and repair all files"; - readonly zh: "校验并修复所有文件"; - }; - readonly "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": { - readonly def: "Compare the content of files between the local database and storage. If they do not match, you will be asked which one to keep."; - readonly zh: "比较本地数据库与存储中的文件内容。如果内容不一致,系统会询问你保留哪一份。"; - }; - readonly "Ui.Settings.Maintenance.Cleanup": { - readonly def: "Perform cleanup"; - readonly zh: "执行清理"; - }; - readonly "Ui.Settings.Maintenance.CleanupDesc": { - readonly def: "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."; - readonly zh: "丢弃所有非最新修订版本,以减少存储空间占用。此操作要求远程服务器和本地客户端都具备同等大小的可用空间。"; - }; - readonly "Ui.Settings.Maintenance.DeleteLocalDatabase": { - readonly def: "Delete local database to reset or uninstall Self-hosted LiveSync"; - readonly zh: "删除本地数据库以重置或卸载 Self-hosted LiveSync"; - }; - readonly "Ui.Settings.Maintenance.EmergencyRestart": { - readonly def: "Emergency restart"; - readonly zh: "紧急重启"; - }; - readonly "Ui.Settings.Maintenance.EmergencyRestartDesc": { - readonly def: "Disable all synchronisation and restart."; - readonly zh: "禁用所有同步并重新启动。"; - }; - readonly "Ui.Settings.Maintenance.FreshStartWipe": { - readonly def: "Fresh Start Wipe"; - readonly zh: "全新开始清空"; - }; - readonly "Ui.Settings.Maintenance.FreshStartWipeDesc": { - readonly def: "Delete all data on the remote server."; - readonly zh: "删除远程服务器上的所有数据。"; - }; - readonly "Ui.Settings.Maintenance.GarbageCollection": { - readonly def: "Garbage Collection V3 (Beta)"; - readonly zh: "垃圾回收 V3(测试版)"; - }; - readonly "Ui.Settings.Maintenance.GarbageCollectionAction": { - readonly def: "Perform Garbage Collection"; - readonly zh: "执行垃圾回收"; - }; - readonly "Ui.Settings.Maintenance.GarbageCollectionDesc": { - readonly def: "Perform Garbage Collection to remove unused chunks and reduce database size."; - readonly zh: "执行垃圾回收以移除未使用的数据块并减少数据库大小。"; - }; - readonly "Ui.Settings.Maintenance.LockServer": { - readonly def: "Lock Server"; - readonly zh: "锁定服务器"; - }; - readonly "Ui.Settings.Maintenance.LockServerDesc": { - readonly def: "Lock the remote server to prevent synchronisation with other devices."; - readonly zh: "锁定远程服务器,防止与其他设备继续同步。"; - }; - readonly "Ui.Settings.Maintenance.OverwriteRemote": { - readonly def: "Overwrite remote"; - readonly zh: "覆盖远程端"; - }; - readonly "Ui.Settings.Maintenance.OverwriteRemoteDesc": { - readonly def: "Overwrite remote with local DB and passphrase."; - readonly zh: "使用本地数据库和密码短语覆盖远程端数据。"; - }; - readonly "Ui.Settings.Maintenance.OverwriteServerData": { - readonly def: "Overwrite Server Data with This Device's Files"; - readonly zh: "用此设备的文件覆盖服务器数据"; - }; - readonly "Ui.Settings.Maintenance.OverwriteServerDataDesc": { - readonly def: "Rebuild the local and remote database with files from this device."; - readonly zh: "使用此设备上的文件重建本地和远程数据库。"; - }; - readonly "Ui.Settings.Maintenance.PurgeAllJournalCounter": { - readonly def: "Purge all journal counter"; - readonly zh: "清空全部日志计数器"; - }; - readonly "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": { - readonly def: "Purge all download and upload caches."; - readonly zh: "清空所有下载与上传缓存。"; - }; - readonly "Ui.Settings.Maintenance.RebuildingOperations": { - readonly def: "Rebuilding Operations (Remote Only)"; - readonly zh: "重建操作(仅远程端)"; - }; - readonly "Ui.Settings.Maintenance.Resend": { - readonly def: "Resend"; - readonly zh: "重新发送"; - }; - readonly "Ui.Settings.Maintenance.ResendDesc": { - readonly def: "Resend all chunks to the remote."; - readonly zh: "将所有数据块重新发送到远程端。"; - }; - readonly "Ui.Settings.Maintenance.Reset": { - readonly def: "Reset"; - readonly zh: "重置"; - }; - readonly "Ui.Settings.Maintenance.ResetAllJournalCounter": { - readonly def: "Reset all journal counter"; - readonly zh: "重置全部日志计数器"; - }; - readonly "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": { - readonly def: "Initialise all journal history. On the next sync, every item will be received and sent again."; - readonly zh: "初始化全部日志历史。下次同步时,所有项目都会重新接收并重新发送。"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalReceived": { - readonly def: "Reset journal received history"; - readonly zh: "重置日志接收历史"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalReceivedDesc": { - readonly def: "Initialise journal received history. On the next sync, every item except those sent by this device will be downloaded again."; - readonly zh: "初始化日志接收历史。下次同步时,除当前设备发送的项目外,其余项目都会重新下载。"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalSent": { - readonly def: "Reset journal sent history"; - readonly zh: "重置日志发送历史"; - }; - readonly "Ui.Settings.Maintenance.ResetJournalSentDesc": { - readonly def: "Initialise journal sent history. On the next sync, every item except those received by this device will be sent again."; - readonly zh: "初始化日志发送历史。下次同步时,除当前设备已接收的项目外,其余项目都会重新发送。"; - }; - readonly "Ui.Settings.Maintenance.ResetLocalSyncInfo": { - readonly def: "Reset Synchronisation information"; - readonly zh: "重置同步信息"; - }; - readonly "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": { - readonly def: "Restore or reconstruct local database from remote."; - readonly zh: "从远程端恢复或重建本地数据库。"; - }; - readonly "Ui.Settings.Maintenance.ResetReceived": { - readonly def: "Reset received"; - readonly zh: "重置接收记录"; - }; - readonly "Ui.Settings.Maintenance.ResetSentHistory": { - readonly def: "Reset sent history"; - readonly zh: "重置发送记录"; - }; - readonly "Ui.Settings.Maintenance.ResetThisDevice": { - readonly def: "Reset Synchronisation on This Device"; - readonly zh: "重置此设备上的同步状态"; - }; - readonly "Ui.Settings.Maintenance.ScheduleAndRestart": { - readonly def: "Schedule and Restart"; - readonly zh: "计划执行并重启"; - }; - readonly "Ui.Settings.Maintenance.Scram": { - readonly def: "Scram!"; - readonly zh: "紧急处理"; - }; - readonly "Ui.Settings.Maintenance.SendChunks": { - readonly def: "Send chunks"; - readonly zh: "发送数据块"; - }; - readonly "Ui.Settings.Maintenance.Syncing": { - readonly def: "Syncing"; - readonly zh: "同步"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedReadyAction": { - readonly def: "I am ready, unlock the database"; - readonly zh: "我已准备好,立即解锁数据库"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedReadyText": { - readonly def: "To prevent unwanted vault corruption, the remote database has been locked for synchronisation. (This device is marked as 'resolved'.) When all your devices are marked as 'resolved', unlock the database. This warning will continue to appear until replication confirms the device is resolved."; - readonly zh: "为防止意外的数据仓库损坏,远程数据库已被锁定,暂停同步。(此设备已被标记为“已确认”)当你的所有设备都标记为“已确认”后,再解锁数据库。在复制过程确认此设备已完成确认之前,此警告会持续显示。"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedResolveAction": { - readonly def: "I have made a backup, mark this device as resolved"; - readonly zh: "我已完成备份,将此设备标记为“已确认”"; - }; - readonly "Ui.Settings.Maintenance.WarningLockedResolveText": { - readonly def: "The remote database is locked for synchronisation to prevent vault corruption because this device is not marked as 'resolved'. Please back up your vault, reset the local database, and select 'Mark this device as resolved'. This warning will persist until replication confirms the device is resolved."; - readonly zh: "为防止数据仓库损坏,由于此设备尚未标记为“已确认”,远程数据库已被锁定,暂停同步。请先备份你的仓库、重置本地数据库,然后选择“将此设备标记为已确认”。在复制过程确认此设备已完成确认之前,此警告会持续显示。"; - }; - readonly "Ui.Settings.Maintenance.WriteRedFlagAndRestart": { - readonly def: "Flag and restart"; - readonly zh: "标记并重启"; - }; - readonly "Ui.Settings.Patches.CompatibilityConflict": { - readonly def: "Compatibility (Conflict Behaviour)"; - readonly zh: "兼容性(冲突行为)"; - }; - readonly "Ui.Settings.Patches.CompatibilityDatabase": { - readonly def: "Compatibility (Database structure)"; - readonly zh: "兼容性(数据库结构)"; - }; - readonly "Ui.Settings.Patches.CompatibilityInternalApi": { - readonly def: "Compatibility (Internal API Usage)"; - readonly zh: "兼容性(内部 API 使用)"; - }; - readonly "Ui.Settings.Patches.CompatibilityMetadata": { - readonly def: "Compatibility (Metadata)"; - readonly zh: "兼容性(元数据)"; - }; - readonly "Ui.Settings.Patches.CompatibilityRemote": { - readonly def: "Compatibility (Remote Database)"; - readonly zh: "兼容性(远程数据库)"; - }; - readonly "Ui.Settings.Patches.CompatibilityTrouble": { - readonly def: "Compatibility (Trouble addressed)"; - readonly zh: "兼容性(已处理问题)"; - }; - readonly "Ui.Settings.Patches.CurrentAdapter": { - readonly def: "Current adapter: ${adapter}"; - readonly zh: "当前适配器:${adapter}"; - }; - readonly "Ui.Settings.Patches.DatabaseAdapter": { - readonly def: "Database Adapter"; - readonly zh: "数据库适配器"; - }; - readonly "Ui.Settings.Patches.DatabaseAdapterDesc": { - readonly def: "Select the database adapter to use."; - readonly zh: "选择要使用的数据库适配器。"; - }; - readonly "Ui.Settings.Patches.EdgeCaseBehaviour": { - readonly def: "Edge case addressing (Behaviour)"; - readonly zh: "边界情况处理(行为)"; - }; - readonly "Ui.Settings.Patches.EdgeCaseDatabase": { - readonly def: "Edge case addressing (Database)"; - readonly zh: "边界情况处理(数据库)"; - }; - readonly "Ui.Settings.Patches.EdgeCaseProcessing": { - readonly def: "Edge case addressing (Processing)"; - readonly zh: "边界情况处理(处理流程)"; - }; - readonly "Ui.Settings.Patches.IndexedDbWarning": { - readonly def: "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use the IDB adapter instead."; - readonly zh: "IndexedDB 适配器在某些场景下通常具有更好的性能,但在 LiveSync 模式下已发现可能导致内存泄漏。使用 LiveSync 模式时,请改用 IDB 适配器。"; - }; - readonly "Ui.Settings.Patches.MigratingToIdb": { - readonly def: "Migrating all data to IDB..."; - readonly zh: "正在将所有数据迁移到 IDB..."; - }; - readonly "Ui.Settings.Patches.MigratingToIndexedDb": { - readonly def: "Migrating all data to IndexedDB..."; - readonly zh: "正在将所有数据迁移到 IndexedDB..."; - }; - readonly "Ui.Settings.Patches.MigrationIdbCompleted": { - readonly def: "Migration to IDB completed. Obsidian will be restarted with the new configuration immediately."; - readonly zh: "已完成迁移到 IDB。Obsidian 将立即使用新配置重新启动。"; - }; - readonly "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": { - readonly def: "Migration to IDB completed. Please switch the adapter and restart Obsidian."; - readonly zh: "已完成迁移到 IDB。请切换适配器并重新启动 Obsidian。"; - }; - readonly "Ui.Settings.Patches.MigrationIndexedDbCompleted": { - readonly def: "Migration to IndexedDB completed. Obsidian will be restarted with the new configuration immediately."; - readonly zh: "已完成迁移到 IndexedDB。Obsidian 将立即使用新配置重新启动。"; - }; - readonly "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": { - readonly def: "Migration to IndexedDB completed. Please switch the adapter and restart Obsidian."; - readonly zh: "已完成迁移到 IndexedDB。请切换适配器并重新启动 Obsidian。"; - }; - readonly "Ui.Settings.Patches.MigrationWarning": { - readonly def: "Changing this setting requires migrating existing data, which may take some time, and restarting Obsidian. Please make sure to back up your data before proceeding."; - readonly zh: "修改此设置需要迁移现有数据(可能需要一些时间)并重新启动 Obsidian。请先备份你的数据后再继续。"; - }; - readonly "Ui.Settings.Patches.OperationToIdb": { - readonly def: "to IDB"; - readonly zh: "迁移到 IDB"; - }; - readonly "Ui.Settings.Patches.OperationToIndexedDb": { - readonly def: "to IndexedDB"; - readonly zh: "迁移到 IndexedDB"; - }; - readonly "Ui.Settings.Patches.Remediation": { - readonly def: "Remediation"; - readonly zh: "修正"; - }; - readonly "Ui.Settings.Patches.RemediationChanged": { - readonly def: "Remediation Setting Changed"; - readonly zh: "修正设置已更改"; - }; - readonly "Ui.Settings.Patches.RemediationNoLimit": { - readonly def: "No limit configured"; - readonly zh: "未设置限制"; - }; - readonly "Ui.Settings.Patches.RemediationRestarting": { - readonly def: "Remediation setting changed. Restarting Obsidian..."; - readonly zh: "修正设置已更改,正在重新启动 Obsidian..."; - }; - readonly "Ui.Settings.Patches.RemediationRestartLater": { - readonly def: "Later"; - readonly zh: "稍后"; - }; - readonly "Ui.Settings.Patches.RemediationRestartMessage": { - readonly def: "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and the display may be inconsistent. Are you sure you want to restart now?"; - readonly zh: "强烈建议重新启动 Obsidian。在重启之前,部分更改可能不会生效,界面显示也可能不一致。确定要现在重启吗?"; - }; - readonly "Ui.Settings.Patches.RemediationRestartNow": { - readonly def: "Restart Now"; - readonly zh: "立即重启"; - }; - readonly "Ui.Settings.Patches.RemediationSuffixChanged": { - readonly def: "Suffix has been changed. Reopening database..."; - readonly zh: "后缀已更改,正在重新打开数据库..."; - }; - readonly "Ui.Settings.Patches.RemediationWithValue": { - readonly def: "Limit: ${date} (${timestamp})"; - readonly zh: "限制:${date}(${timestamp})"; - }; - readonly "Ui.Settings.Patches.RemoteDatabaseSunset": { - readonly def: "Remote Database Tweak (In sunset)"; - readonly zh: "远程数据库调整(即将弃用)"; - }; - readonly "Ui.Settings.Patches.SwitchToIDB": { - readonly def: "Switch to IDB"; - readonly zh: "切换到 IDB"; - }; - readonly "Ui.Settings.Patches.SwitchToIndexedDb": { - readonly def: "Switch to IndexedDB"; - readonly zh: "切换到 IndexedDB"; - }; - readonly "Ui.Settings.PowerUsers.ConfigurationEncryption": { - readonly def: "Configuration Encryption"; - readonly zh: "配置加密"; - }; - readonly "Ui.Settings.PowerUsers.ConnectionTweak": { - readonly def: "CouchDB Connection Tweak"; - readonly zh: "CouchDB 连接调整"; - }; - readonly "Ui.Settings.PowerUsers.ConnectionTweakDesc": { - readonly def: "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value."; - readonly zh: "如果你在使用 IBM Cloudant 时遇到负载大小限制,请将 batch size 和 batch limit 调低。"; - }; - readonly "Ui.Settings.PowerUsers.Default": { - readonly def: "Default"; - readonly zh: "默认"; - }; - readonly "Ui.Settings.PowerUsers.Developer": { - readonly def: "Developer"; - readonly zh: "开发者"; - }; - readonly "Ui.Settings.PowerUsers.EncryptSensitiveConfig": { - readonly def: "Encrypt sensitive configuration items"; - readonly zh: "加密敏感配置项"; - }; - readonly "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": { - readonly def: "Ask for a passphrase at every launch"; - readonly zh: "每次启动时询问密码短语"; - }; - readonly "Ui.Settings.PowerUsers.UseCustomPassphrase": { - readonly def: "Use a custom passphrase"; - readonly zh: "使用自定义密码短语"; - }; - readonly "Ui.Settings.Remote.Activate": { - readonly def: "Activate"; - readonly zh: "启用"; - }; - readonly "Ui.Settings.Remote.ActiveSuffix": { - readonly def: " (Active)"; - readonly zh: "(当前启用)"; - }; - readonly "Ui.Settings.Remote.AddConnection": { - readonly def: "Add new connection"; - readonly zh: "新增连接"; - }; - readonly "Ui.Settings.Remote.AddRemoteDefaultName": { - readonly def: "New Remote"; - readonly zh: "新远程端"; - }; - readonly "Ui.Settings.Remote.ConfigureAndChangeRemote": { - readonly def: "Configure and change remote"; - readonly zh: "配置并切换远程端"; - }; - readonly "Ui.Settings.Remote.ConfigureE2EE": { - readonly def: "Configure E2EE"; - readonly zh: "配置端到端加密"; - }; - readonly "Ui.Settings.Remote.ConfigureRemote": { - readonly def: "Configure Remote"; - readonly zh: "配置远程端"; - }; - readonly "Ui.Settings.Remote.DeleteRemoteConfirm": { - readonly def: "Delete remote configuration '${name}'?"; - readonly zh: "确定要删除远程配置“${name}”吗?"; - }; - readonly "Ui.Settings.Remote.DeleteRemoteTitle": { - readonly def: "Delete Remote Configuration"; - readonly zh: "删除远程配置"; - }; - readonly "Ui.Settings.Remote.DisplayName": { - readonly def: "Display name"; - readonly zh: "显示名称"; - }; - readonly "Ui.Settings.Remote.DuplicateRemote": { - readonly def: "Duplicate remote"; - readonly zh: "复制远程配置"; - }; - readonly "Ui.Settings.Remote.DuplicateRemoteSuffix": { - readonly def: "${name} (Copy)"; - readonly zh: "${name}(副本)"; - }; - readonly "Ui.Settings.Remote.E2EEConfiguration": { - readonly def: "E2EE Configuration"; - readonly zh: "端到端加密配置"; - }; - readonly "Ui.Settings.Remote.Export": { - readonly def: "Export"; - readonly zh: "导出"; - }; - readonly "Ui.Settings.Remote.FetchRemoteSettings": { - readonly def: "Fetch remote settings"; - readonly zh: "获取远程设置"; - }; - readonly "Ui.Settings.Remote.ImportConnection": { - readonly def: "Import connection"; - readonly zh: "导入连接"; - }; - readonly "Ui.Settings.Remote.ImportConnectionPrompt": { - readonly def: "Paste a connection string"; - readonly zh: "粘贴连接字符串"; - }; - readonly "Ui.Settings.Remote.ImportedCouchDb": { - readonly def: "Imported CouchDB"; - readonly zh: "已导入的 CouchDB"; - }; - readonly "Ui.Settings.Remote.ImportedRemote": { - readonly def: "Remote"; - readonly zh: "远程端"; - }; - readonly "Ui.Settings.Remote.MoreActions": { - readonly def: "More actions"; - readonly zh: "更多操作"; - }; - readonly "Ui.Settings.Remote.PeerToPeerPanel": { - readonly def: "Peer-to-Peer Synchronisation"; - readonly zh: "点对点同步"; - }; - readonly "Ui.Settings.Remote.RemoteConfigurationPrefix": { - readonly def: "Remote configuration"; - readonly zh: "远程配置"; - }; - readonly "Ui.Settings.Remote.RemoteDatabases": { - readonly def: "Remote Databases"; - readonly zh: "远程数据库"; - }; - readonly "Ui.Settings.Remote.RemoteName": { - readonly def: "Remote name"; - readonly zh: "远程名称"; - }; - readonly "Ui.Settings.Remote.RemoteNameCouchDb": { - readonly def: "CouchDB ${host}"; - readonly zh: "CouchDB ${host}"; - }; - readonly "Ui.Settings.Remote.RemoteNameP2P": { - readonly def: "P2P ${room}"; - readonly zh: "P2P ${room}"; - }; - readonly "Ui.Settings.Remote.RemoteNameS3": { - readonly def: "S3 ${bucket}"; - readonly zh: "S3 ${bucket}"; - }; - readonly "Ui.Settings.Remote.Rename": { - readonly def: "Rename"; - readonly zh: "重命名"; - }; - readonly "Ui.Settings.Selector.AddDefaultPatterns": { - readonly def: "Add default patterns"; - readonly zh: "添加默认模式"; - }; - readonly "Ui.Settings.Selector.CrossPlatform": { - readonly def: "Cross-platform"; - readonly zh: "跨平台"; - }; - readonly "Ui.Settings.Selector.Default": { - readonly def: "Default"; - readonly zh: "默认"; - }; - readonly "Ui.Settings.Selector.HiddenFiles": { - readonly def: "Hidden Files"; - readonly zh: "隐藏文件"; - }; - readonly "Ui.Settings.Selector.IgnorePatterns": { - readonly def: "Ignore patterns"; - readonly zh: "忽略模式"; - }; - readonly "Ui.Settings.Selector.NonSynchronisingFiles": { - readonly def: "Non-Synchronising files"; - readonly zh: "不同步文件"; - }; - readonly "Ui.Settings.Selector.NonSynchronisingFilesDesc": { - readonly def: "(RegExp) If this is set, any changes to local and remote files that match this will be skipped."; - readonly zh: "(RegExp)如果设置了该项,则本地和远程中匹配这些规则的文件变更将被跳过。"; - }; - readonly "Ui.Settings.Selector.NormalFiles": { - readonly def: "Normal Files"; - readonly zh: "普通文件"; - }; - readonly "Ui.Settings.Selector.OverwritePatterns": { - readonly def: "Overwrite patterns"; - readonly zh: "覆盖模式"; - }; - readonly "Ui.Settings.Selector.OverwritePatternsDesc": { - readonly def: "Patterns to match files for overwriting instead of merging"; - readonly zh: "匹配后将执行覆盖而非合并的文件模式"; - }; - readonly "Ui.Settings.Selector.SynchronisingFiles": { - readonly def: "Synchronising files"; - readonly zh: "同步文件"; - }; - readonly "Ui.Settings.Selector.SynchronisingFilesDesc": { - readonly def: "(RegExp) Empty to sync all files. Set a regular expression filter to limit synchronised files."; - readonly zh: "(RegExp)留空则同步所有文件。可设置正则表达式以限制需要同步的文件。"; - }; - readonly "Ui.Settings.Selector.TargetPatterns": { - readonly def: "Target patterns"; - readonly zh: "目标模式"; - }; - readonly "Ui.Settings.Selector.TargetPatternsDesc": { - readonly def: "Patterns to match files for syncing"; - readonly zh: "用于匹配需要同步文件的模式"; - }; - readonly "Ui.Settings.Setup.RerunWizardButton": { - readonly def: "Rerun Wizard"; - readonly zh: "重新运行向导"; - }; - readonly "Ui.Settings.Setup.RerunWizardDesc": { - readonly def: "Rerun the onboarding wizard to set up Self-hosted LiveSync again."; - readonly zh: "重新运行引导向导,再次设置 Self-hosted LiveSync。"; - }; - readonly "Ui.Settings.Setup.RerunWizardName": { - readonly def: "Rerun Onboarding Wizard"; - readonly zh: "重新运行引导向导"; - }; - readonly "Ui.Settings.SyncSettings.Fetch": { - readonly def: "Fetch"; - readonly zh: "获取"; - }; - readonly "Ui.Settings.SyncSettings.Merge": { - readonly def: "Merge"; - readonly zh: "合并"; - }; - readonly "Ui.Settings.SyncSettings.Overwrite": { - readonly def: "Overwrite"; - readonly zh: "覆盖"; - }; - readonly "Ui.SetupWizard.Common.Back": { - readonly def: "No, please take me back"; - readonly zh: "不,带我返回"; - }; - readonly "Ui.SetupWizard.Common.Cancel": { - readonly def: "Cancel"; - readonly zh: "取消"; - }; - readonly "Ui.SetupWizard.Common.ProceedSelectOption": { - readonly def: "Please select an option to proceed"; - readonly zh: "请选择一个选项后继续"; - }; - readonly "Ui.SetupWizard.Intro.ExistingOption": { - readonly def: "I am adding a device to an existing synchronisation setup"; - readonly zh: "将此设备加入已有同步配置"; - }; - readonly "Ui.SetupWizard.Intro.ExistingOptionDesc": { - readonly def: "Select this if you are already using synchronisation on another computer or smartphone. Use this option to connect this device to that existing setup."; - readonly zh: "如果你已经在另一台电脑或手机上使用同步,请选择此项。此选项用于将当前设备连接到既有同步配置。"; - }; - readonly "Ui.SetupWizard.Intro.Guidance": { - readonly def: "We will now guide you through a few questions to simplify the synchronisation setup."; - readonly zh: "接下来我们会通过几个问题,帮助你更轻松地完成同步配置。"; - }; - readonly "Ui.SetupWizard.Intro.NewOption": { - readonly def: "I am setting this up for the first time"; - readonly zh: "首次设置同步"; - }; - readonly "Ui.SetupWizard.Intro.NewOptionDesc": { - readonly def: "Select this if you are configuring this device as the first synchronisation device."; - readonly zh: "如果你正把这台设备作为第一台同步设备进行配置,请选择此项。"; - }; - readonly "Ui.SetupWizard.Intro.ProceedExisting": { - readonly def: "Yes, I want to add this device to my existing synchronisation"; - readonly zh: "是的,我要将此设备加入现有同步"; - }; - readonly "Ui.SetupWizard.Intro.ProceedNew": { - readonly def: "Yes, I want to set up a new synchronisation"; - readonly zh: "是的,我要开始新的同步配置"; - }; - readonly "Ui.SetupWizard.Intro.Question": { - readonly def: "First, please select the option that best describes your current situation."; - readonly zh: "首先,请选择最符合你当前情况的选项。"; - }; - readonly "Ui.SetupWizard.Intro.Title": { - readonly def: "Welcome to Self-hosted LiveSync"; - readonly zh: "欢迎使用 Self-hosted LiveSync"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": { - readonly def: "The remote is already set up, and the configuration is compatible (or became compatible through this operation)."; - readonly zh: "远程端已配置完成,且当前配置兼容(或已通过本次操作变为兼容)。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": { - readonly def: "Unless you are certain, selecting this option is risky. It assumes the server configuration is compatible with this device. If that is not the case, data loss may occur. Please make sure you understand the consequences."; - readonly zh: "除非你非常确定,否则选择此项存在风险。它假定服务器配置与当前设备兼容。如果事实并非如此,可能会导致数据丢失。请确认你了解后果。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ExistingOption": { - readonly def: "My remote server is already set up. I want to join this device."; - readonly zh: "远程服务器已经配置完成,我想让此设备加入同步。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": { - readonly def: "Selecting this option will make this device join the existing server. You need to fetch the existing synchronisation data from the server to this device."; - readonly zh: "选择此项后,此设备会加入已有服务器。你需要将服务器上的现有同步数据获取到此设备。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.Guidance": { - readonly def: "The connection to the server has been configured successfully. As the next step, the local database, in other words the synchronisation information, must be rebuilt."; - readonly zh: "服务器连接已成功配置。下一步需要重建本地数据库,也就是同步状态信息。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.NewOption": { - readonly def: "I am setting up a new server for the first time / I want to reset my existing server."; - readonly zh: "我是第一次配置新服务器 / 我想重置现有服务器。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": { - readonly def: "Selecting this option will initialise the server using the current data on this device. Any existing data on the server will be completely overwritten."; - readonly zh: "选择此项后,服务器会使用当前设备上的数据进行初始化。服务器上的现有数据将被完全覆盖。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": { - readonly def: "Apply the settings"; - readonly zh: "应用这些设置"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.ProceedNext": { - readonly def: "Proceed to the next step."; - readonly zh: "继续下一步"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.Question": { - readonly def: "Please select your situation."; - readonly zh: "请选择你的当前情况。"; - }; - readonly "Ui.SetupWizard.OutroAskUserMode.Title": { - readonly def: "Mostly Complete: Decision Required"; - readonly zh: "即将完成:还需要做出选择"; - }; - readonly "Ui.SetupWizard.OutroNewUser.GuidancePrimary": { - readonly def: "The connection to the server has been configured successfully. As the next step, the synchronisation data on the server will be built from the current data on this device."; - readonly zh: "服务器连接已成功配置。下一步将根据当前设备上的数据,在服务器端建立同步数据。"; - }; - readonly "Ui.SetupWizard.OutroNewUser.GuidanceWarning": { - readonly def: "After restarting, the data on this device will be uploaded to the server as the master copy. Please note that any unintended data currently on the server will be completely overwritten."; - readonly zh: "重启后,当前设备上的数据会作为主副本上传到服务器。请注意,服务器上现有的非预期数据将被完全覆盖。"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Important": { - readonly def: "IMPORTANT"; - readonly zh: "重要"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Proceed": { - readonly def: "Restart and Initialise Server"; - readonly zh: "重启并初始化服务器"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Question": { - readonly def: "Please select the button below to restart and proceed to the final confirmation."; - readonly zh: "请选择下方按钮,重启并进入最终确认步骤。"; - }; - readonly "Ui.SetupWizard.OutroNewUser.Title": { - readonly def: "Setup Complete: Preparing to Initialise Server"; - readonly zh: "设置完成:准备初始化服务器"; - }; - readonly "Ui.SetupWizard.SelectExisting.Guidance": { - readonly def: "You are adding this device to an existing synchronisation setup."; - readonly zh: "你正在将此设备加入已有同步配置。"; - }; - readonly "Ui.SetupWizard.SelectExisting.ManualOption": { - readonly def: "Enter the server information manually"; - readonly zh: "手动输入服务器信息"; - }; - readonly "Ui.SetupWizard.SelectExisting.ManualOptionDesc": { - readonly def: "Configure the same server information as your other devices again manually. This is intended only for advanced users."; - readonly zh: "手动重新配置与你其他设备相同的服务器信息。此方式仅适用于高级用户。"; - }; - readonly "Ui.SetupWizard.SelectExisting.ProceedManual": { - readonly def: "I know my server details, let me enter them"; - readonly zh: "我知道服务器信息,让我手动输入"; - }; - readonly "Ui.SetupWizard.SelectExisting.ProceedQr": { - readonly def: "Scan the QR code displayed on an active device using this device's camera."; - readonly zh: "使用本设备摄像头扫描活动设备上显示的二维码"; - }; - readonly "Ui.SetupWizard.SelectExisting.ProceedSetupUri": { - readonly def: "Proceed with Setup URI"; - readonly zh: "使用 Setup URI 继续"; - }; - readonly "Ui.SetupWizard.SelectExisting.QrOption": { - readonly def: "Scan a QR Code (Recommended for mobile)"; - readonly zh: "扫描二维码(移动端推荐)"; - }; - readonly "Ui.SetupWizard.SelectExisting.QrOptionDesc": { - readonly def: "Scan the QR code displayed on an active device using this device's camera."; - readonly zh: "使用本设备摄像头扫描活动设备上显示的二维码。"; - }; - readonly "Ui.SetupWizard.SelectExisting.Question": { - readonly def: "Please select a method to import the settings from another device."; - readonly zh: "请选择一种从其他设备导入设置的方法。"; - }; - readonly "Ui.SetupWizard.SelectExisting.SetupUriOption": { - readonly def: "Use a Setup URI (Recommended)"; - readonly zh: "使用 Setup URI(推荐)"; - }; - readonly "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": { - readonly def: "Paste the Setup URI generated from one of your active devices."; - readonly zh: "粘贴从某台已启用设备生成的 Setup URI。"; - }; - readonly "Ui.SetupWizard.SelectExisting.Title": { - readonly def: "Device Setup Method"; - readonly zh: "设备设置方式"; - }; - readonly "Ui.SetupWizard.SelectNew.Guidance": { - readonly def: "We will now proceed with the server configuration."; - readonly zh: "接下来将继续配置服务器连接信息。"; - }; - readonly "Ui.SetupWizard.SelectNew.ManualOption": { - readonly def: "Enter the server information manually"; - readonly zh: "手动输入服务器信息"; - }; - readonly "Ui.SetupWizard.SelectNew.ManualOptionDesc": { - readonly def: "This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings."; - readonly zh: "如果你没有 Setup URI,或希望自行配置更详细的参数,可选择此高级选项。"; - }; - readonly "Ui.SetupWizard.SelectNew.ProceedManual": { - readonly def: "I know my server details, let me enter them"; - readonly zh: "我知道服务器信息,让我手动输入"; - }; - readonly "Ui.SetupWizard.SelectNew.ProceedSetupUri": { - readonly def: "Proceed with Setup URI"; - readonly zh: "使用 Setup URI 继续"; - }; - readonly "Ui.SetupWizard.SelectNew.Question": { - readonly def: "How would you like to configure the connection to your server?"; - readonly zh: "你希望如何配置服务器连接?"; - }; - readonly "Ui.SetupWizard.SelectNew.SetupUriOption": { - readonly def: "Use a Setup URI (Recommended)"; - readonly zh: "使用 Setup URI(推荐)"; - }; - readonly "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": { - readonly def: "A Setup URI is a single string containing your server address and authentication details. If one was generated by your server installation script, it provides a simple and secure configuration method."; - readonly zh: "Setup URI 是一段包含服务器地址和认证信息的文本。如果你的服务器安装脚本已经生成了它,这是最简单且安全的配置方式。"; - }; - readonly "Ui.SetupWizard.SelectNew.Title": { - readonly def: "Connection Method"; - readonly zh: "连接方式"; - }; - readonly "Ui.SetupWizard.SetupRemote.BucketOption": { - readonly def: "S3/MinIO/R2 Object Storage"; - readonly zh: "S3/MinIO/R2 对象存储"; - }; - readonly "Ui.SetupWizard.SetupRemote.BucketOptionDesc": { - readonly def: "Synchronisation using journal files. You must already have an S3/MinIO/R2 compatible object storage service set up."; - readonly zh: "使用日志文件进行同步。你需要先准备好兼容 S3/MinIO/R2 的对象存储服务。"; - }; - readonly "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": { - readonly def: "This is the most suitable synchronisation method for the current design. All features are available. You must already have a CouchDB instance set up."; - readonly zh: "这是当前设计下最适合的同步方式,所有功能都可用。你需要先准备好 CouchDB 实例。"; - }; - readonly "Ui.SetupWizard.SetupRemote.Guidance": { - readonly def: "Please select the type of server you are connecting to."; - readonly zh: "请选择你要连接的服务器类型。"; - }; - readonly "Ui.SetupWizard.SetupRemote.P2POption": { - readonly def: "Peer-to-Peer only"; - readonly zh: "仅点对点"; - }; - readonly "Ui.SetupWizard.SetupRemote.P2POptionDesc": { - readonly def: "This enables direct synchronisation between devices. No server is required, but both devices must be online at the same time and some features may be limited. Internet connectivity is required only for signalling, not for data transfer."; - readonly zh: "启用设备之间的直接同步。无需服务器,但两台设备必须同时在线,且部分功能可能受限。互联网连接仅用于信令,不用于传输数据。"; - }; - readonly "Ui.SetupWizard.SetupRemote.ProceedBucket": { - readonly def: "Continue to S3/MinIO/R2 setup"; - readonly zh: "继续配置 S3/MinIO/R2"; - }; - readonly "Ui.SetupWizard.SetupRemote.ProceedCouchDb": { - readonly def: "Continue to CouchDB setup"; - readonly zh: "继续配置 CouchDB"; - }; - readonly "Ui.SetupWizard.SetupRemote.ProceedP2P": { - readonly def: "Continue to Peer-to-Peer only setup"; - readonly zh: "继续配置仅点对点模式"; - }; - readonly "Ui.SetupWizard.SetupRemote.Title": { - readonly def: "Enter Server Information"; - readonly zh: "输入服务器信息"; - }; - readonly "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": { - readonly def: "Unique name between all synchronized devices. To edit this setting, please disable customization sync once."; - readonly es: "Nombre único entre dispositivos sincronizados. Para editarlo, desactive sincronización de personalización"; - readonly fr: "Nom unique parmi tous les appareils synchronisés. Pour modifier ce paramètre, désactivez d'abord la synchronisation de personnalisation."; - readonly he: "שם ייחודי בין כל המכשירים המסונכרנים. כדי לערוך הגדרה זו, אנא נטרל את סנכרון ההתאמה האישית פעם אחת."; - readonly ja: "同期するすべての端末間で重複しない(一意の)名前。この設定を変更する場合、カスタマイズ同期を無効にしてください。"; - readonly ko: "모든 동기화된 기기 간 고유 이름입니다. 이 설정을 편집하려면 사용자 설정 동기화를 한 번 비활성화해 주세요."; - readonly ru: "Уникальное имя между всеми синхронизируемыми устройствами."; - readonly zh: "所有同步设备之间的唯一名称。要编辑此设置,请首先禁用自定义同步"; - }; - readonly "Use a custom passphrase": { - readonly def: "Use a custom passphrase"; - readonly es: "Usar una frase de contraseña personalizada"; - readonly ja: "カスタムパスフレーズを使う"; - readonly ko: "사용자 지정 암호문구 사용"; - readonly ru: "Использовать пользовательскую парольную фразу"; - readonly zh: "使用自定义密码短语"; - }; - readonly "Use a Setup URI (Recommended)": { - readonly def: "Use a Setup URI (Recommended)"; - readonly es: "Usar un URI de configuración (recomendado)"; - readonly ja: "Setup URI を使う(推奨)"; - readonly ko: "설정 URI 사용(권장)"; - readonly ru: "Использовать Setup URI (рекомендуется)"; - readonly zh: "使用 Setup URI(推荐)"; - readonly "zh-tw": "使用 Setup URI(推薦)"; - }; - readonly "Use Custom HTTP Handler": { - readonly def: "Use Custom HTTP Handler"; - readonly es: "Usar manejador HTTP personalizado"; - readonly fr: "Utiliser un gestionnaire HTTP personnalisé"; - readonly he: "השתמש ב-HTTP Handler מותאם אישית"; - readonly ja: "カスタムHTTPハンドラーの利用"; - readonly ko: "커스텀 HTTP 핸들러 사용"; - readonly ru: "Использовать пользовательский HTTP обработчик"; - readonly zh: "使用自定义 HTTP 处理程序"; - }; - readonly "Use dynamic iteration count": { - readonly def: "Use dynamic iteration count"; - readonly es: "Usar conteo de iteraciones dinámico"; - readonly fr: "Utiliser un compteur d'itérations dynamique"; - readonly he: "השתמש בספירת איטרציות דינמית"; - readonly ja: "動的な繰り返し回数"; - readonly ko: "동적 반복 횟수 사용"; - readonly ru: "Использовать динамическое количество итераций"; - readonly zh: "使用动态迭代次数"; - }; - readonly "Use Segmented-splitter": { - readonly def: "Use Segmented-splitter"; - readonly es: "Usar divisor segmentado"; - readonly fr: "Utiliser le découpeur segmenté"; - readonly he: "השתמש ב-Segmented-splitter"; - readonly ja: "セグメント分割を使用"; - readonly ko: "의미 기반 분할 사용"; - readonly ru: "Использовать сегментный разделитель"; - readonly zh: "使用分段分割器"; - }; - readonly "Use splitting-limit-capped chunk splitter": { - readonly def: "Use splitting-limit-capped chunk splitter"; - readonly es: "Usar divisor de chunks con límite"; - readonly fr: "Utiliser le découpeur de fragments plafonné"; - readonly he: "השתמש ב-chunk splitter עם מגבלת פיצול"; - readonly ja: "分割制限付きチャンク分割を使用"; - readonly ko: "분할 제한 상한 청크 분할기 사용"; - readonly ru: "Использовать разделитель чанков с ограничением"; - readonly zh: "使用分割限制上限的块分割器"; - }; - readonly "Use the trash bin": { - readonly def: "Use the trash bin"; - readonly es: "Usar papelera"; - readonly fr: "Utiliser la corbeille"; - readonly he: "השתמש בסל האשפה"; - readonly ja: "ゴミ箱を使用"; - readonly ko: "휴지통 사용"; - readonly ru: "Использовать корзину"; - readonly zh: "使用回收站"; - }; - readonly "Use timeouts instead of heartbeats": { - readonly def: "Use timeouts instead of heartbeats"; - readonly es: "Usar timeouts en lugar de latidos"; - readonly fr: "Utiliser des délais d'attente au lieu de battements"; - readonly he: "השתמש בפסק זמן במקום פעימות לב"; - readonly ja: "ハートビートの代わりにタイムアウトを使用"; - readonly ko: "하트비트 대신 타임아웃 사용"; - readonly ru: "Использовать таймауты вместо пульса"; - readonly zh: "使用超时而不是心跳"; - }; - readonly username: { - readonly def: "username"; - readonly es: "nombre de usuario"; - readonly fr: "nom d'utilisateur"; - readonly he: "שם משתמש"; - readonly ja: "ユーザー名"; - readonly ko: "사용자명"; - readonly ru: "имя пользователя"; - readonly zh: "用户名"; - }; - readonly Username: { - readonly def: "Username"; - readonly es: "Usuario"; - readonly fr: "Nom d'utilisateur"; - readonly he: "שם משתמש"; - readonly ja: "ユーザー名"; - readonly ko: "사용자명"; - readonly ru: "Имя пользователя"; - readonly zh: "用户名"; - }; - readonly "Verbose Log": { - readonly def: "Verbose Log"; - readonly es: "Registro detallado"; - readonly fr: "Journal verbeux"; - readonly he: "יומן מפורט"; - readonly ja: "エラー以外のログ項目"; - readonly ko: "자세한 로그"; - readonly ru: "Подробный лог"; - readonly zh: "详细日志"; - }; - readonly "Verify all": { - readonly def: "Verify all"; - readonly es: "Verificar todo"; - readonly ja: "すべて検証"; - readonly ko: "모두 검증"; - readonly ru: "Проверить всё"; - readonly zh: "全部校验"; - readonly "zh-tw": "全部驗證"; - }; - readonly "Verify and repair all files": { - readonly def: "Verify and repair all files"; - readonly es: "Verificar y reparar todos los archivos"; - readonly ja: "すべてのファイルを検証して修復"; - readonly ko: "모든 파일 검증 및 복구"; - readonly ru: "Проверить и восстановить все файлы"; - readonly zh: "校验并修复所有文件"; - readonly "zh-tw": "驗證並修復所有檔案"; - }; - readonly "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": { - readonly def: "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information."; - readonly es: "¡Advertencia! Impacta rendimiento. Los logs no se sincronizan con nombre predeterminado. Contienen información confidencial"; - readonly fr: "Attention ! Ceci aura un impact important sur les performances. De plus, les journaux ne seront pas synchronisés sous le nom par défaut. Soyez prudent avec les journaux ; ils contiennent souvent des informations confidentielles."; - readonly he: "אזהרה! לכך תהיה השפעה רצינית על הביצועים. בנוסף, היומנים לא יסונכרנו תחת השם ברירת המחדל. אנא היה זהיר עם יומנים; הם לרוב מכילים מידע סודי שלך."; - readonly ja: "警告!これはパフォーマンスに重大な影響を与えます。また、ログはデフォルト名では同期されません。ログには機密情報が含まれることが多いため、注意してください。"; - readonly ko: "경고! 이는 성능에 심각한 영향을 미칩니다. 로그는 기본 이름으로 동기화되지 않습니다. 로그에는 종종 기밀 정보가 포함되어 있으므로 주의해 주세요."; - readonly ru: "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information."; - readonly zh: "警告!这将严重影响性能。并且日志不会以默认名称同步。请小心处理日志;它们通常包含您的敏感信息 "; - }; - readonly "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": { - readonly def: "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name."; - readonly es: "No podemos cambiar el nombre del dispositivo mientras esta función esté habilitada. Deshabilita la función para cambiarlo."; - readonly ja: "この機能が有効な間はデバイス名を変更できません。変更するにはこの機能を無効にしてください。"; - readonly ko: "이 기능이 활성화되어 있는 동안에는 장치 이름을 변경할 수 없습니다. 장치 이름을 변경하려면 이 기능을 비활성화하세요."; - readonly ru: "Невозможно изменить имя устройства, пока эта функция включена. Отключите её, чтобы изменить имя устройства."; - readonly zh: "启用此功能时无法更改设备名称。如需修改设备名称,请先禁用此功能。"; - }; - readonly "We will now guide you through a few questions to simplify the synchronisation setup.": { - readonly def: "We will now guide you through a few questions to simplify the synchronisation setup."; - readonly es: "Ahora le guiaremos con unas pocas preguntas para simplificar la configuración de la sincronización。"; - readonly ja: "これからいくつかの質問に沿って、同期設定を簡単に進めます。"; - readonly ko: "동기화 설정을 더 쉽게 진행할 수 있도록 몇 가지 질문으로 안내해 드리겠습니다。"; - readonly ru: "Сейчас мы зададим несколько вопросов, чтобы упростить настройку синхронизации。"; - readonly zh: "接下来我们会通过几个问题,引导你更轻松地完成同步设置。"; - readonly "zh-tw": "接下來我們會透過幾個問題,引導你更輕鬆地完成同步設定。"; - }; - readonly "We will now proceed with the server configuration.": { - readonly def: "We will now proceed with the server configuration."; - readonly es: "Ahora continuaremos con la configuración del servidor。"; - readonly ja: "次にサーバー設定を進めます。"; - readonly ko: "이제 서버 구성을 진행하겠습니다。"; - readonly ru: "Теперь перейдём к настройке сервера。"; - readonly zh: "接下来将继续进行服务器配置。"; - readonly "zh-tw": "接下來將繼續進行伺服器設定。"; - }; - readonly "Welcome to Self-hosted LiveSync": { - readonly def: "Welcome to Self-hosted LiveSync"; - readonly es: "Bienvenido a Self-hosted LiveSync"; - readonly ja: "Self-hosted LiveSync へようこそ"; - readonly ko: "Self-hosted LiveSync에 오신 것을 환영합니다"; - readonly ru: "Добро пожаловать в Self-hosted LiveSync"; - readonly zh: "欢迎使用 Self-hosted LiveSync"; - readonly "zh-tw": "歡迎使用 Self-hosted LiveSync"; - }; - readonly "When you save a file in the editor, start a sync automatically": { - readonly def: "When you save a file in the editor, start a sync automatically"; - readonly es: "Iniciar sincronización automática al guardar en editor"; - readonly fr: "À l'enregistrement d'un fichier dans l'éditeur, démarrer automatiquement une synchronisation"; - readonly he: "כאשר אתה שומר קובץ בעורך, התחל סנכרון אוטומטית"; - readonly ja: "エディタでファイルを保存すると、自動的に同期を開始します"; - readonly ko: "편집기에서 파일을 저장할 때 자동으로 동기화를 시작합니다"; - readonly ru: "Когда вы сохраняете файл в редакторе, автоматически запускать синхронизацию"; - readonly zh: "当您在编辑器中保存文件时,自动开始同步"; - }; - readonly "Write credentials in the file": { - readonly def: "Write credentials in the file"; - readonly es: "Escribir credenciales en archivo"; - readonly fr: "Écrire les identifiants dans le fichier"; - readonly he: "כתוב פרטי גישה בקובץ"; - readonly ja: "認証情報のファイル内保存"; - readonly ko: "파일에 자격 증명 저장"; - readonly ru: "Записывать учётные данные в файл"; - readonly zh: "将凭据写入文件"; - }; - readonly "Write logs into the file": { - readonly def: "Write logs into the file"; - readonly es: "Escribir logs en archivo"; - readonly fr: "Écrire les journaux dans le fichier"; - readonly he: "כתוב יומנים לקובץ"; - readonly ja: "ファイルにログを記録"; - readonly ko: "파일에 로그 기록"; - readonly ru: "Записывать логи в файл"; - readonly zh: "将日志写入文件"; - }; - readonly "xxhash32 (Fast but less collision resistance)": { - readonly def: "xxhash32 (Fast but less collision resistance)"; - readonly es: "xxhash32 (rápido, pero con menor resistencia a colisiones)"; - readonly ja: "xxhash32 (高速ですが衝突耐性は低め)"; - readonly ko: "xxhash32 (빠르지만 충돌 저항성은 낮음)"; - readonly ru: "xxhash32 (быстрый, но с меньшей устойчивостью к коллизиям)"; - readonly zh: "xxhash32(速度快,但抗碰撞能力较弱)"; - readonly "zh-tw": "xxhash32(速度快,但抗碰撞能力較弱)"; - }; - readonly "xxhash64 (Fastest)": { - readonly def: "xxhash64 (Fastest)"; - readonly es: "xxhash64 (el más rápido)"; - readonly ja: "xxhash64 (最速)"; - readonly ko: "xxhash64 (가장 빠름)"; - readonly ru: "xxhash64 (самый быстрый)"; - readonly zh: "xxhash64(最快)"; - readonly "zh-tw": "xxhash64(最快)"; - }; - readonly "Yes, I want to add this device to my existing synchronisation": { - readonly def: "Yes, I want to add this device to my existing synchronisation"; - readonly es: "Sí, quiero añadir este dispositivo a mi sincronización existente"; - readonly ja: "はい、この端末を既存の同期に追加します"; - readonly ko: "예, 이 장치를 기존 동기화에 추가하겠습니다"; - readonly ru: "Да, я хочу добавить это устройство к существующей синхронизации"; - readonly zh: "是的,我要把这台设备加入现有同步"; - readonly "zh-tw": "是的,我要把這台裝置加入既有同步"; - }; - readonly "Yes, I want to set up a new synchronisation": { - readonly def: "Yes, I want to set up a new synchronisation"; - readonly es: "Sí, quiero configurar una nueva sincronización"; - readonly ja: "はい、新しい同期を設定します"; - readonly ko: "예, 새 동기화를 설정하겠습니다"; - readonly ru: "Да, я хочу настроить новую синхронизацию"; - readonly zh: "是的,我要配置新的同步"; - readonly "zh-tw": "是的,我要設定新的同步"; - }; - readonly "You are adding this device to an existing synchronisation setup.": { - readonly def: "You are adding this device to an existing synchronisation setup."; - readonly es: "Está añadiendo este dispositivo a una configuración de sincronización existente。"; - readonly ja: "この端末を既存の同期構成に追加しようとしています。"; - readonly ko: "이 장치를 기존 동기화 구성에 추가하려고 합니다。"; - readonly ru: "Вы добавляете это устройство к существующей настройке синхронизации。"; - readonly zh: "你正在将此设备加入到现有同步配置中。"; - readonly "zh-tw": "你正在將此裝置加入既有同步設定中。"; - }; - readonly "Compute revisions for chunks (Previous behaviour)": { - readonly es: "Calcular revisiones para chunks (comportamiento anterior)"; - }; - readonly "Setup.> [!INFO]- The connected devices have been detected as follows:\n${devices}": { - readonly es: "> [!INFO]- Se detectaron los siguientes dispositivos conectados:\n${devices}"; - }; - readonly "Setup.All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": { - readonly es: "Todos los dispositivos tienen el mismo valor de progreso (${progress}). Parece que tus dispositivos están sincronizados y se puede continuar con la recolección de basura."; - }; - readonly "Setup.Cancel Garbage Collection": { - readonly es: "Cancelar la recolección de basura"; - }; - readonly "Setup.Compaction in progress on remote database...": { - readonly es: "La compactación está en curso en la base de datos remota..."; - }; - readonly "Setup.Compaction on remote database completed successfully.": { - readonly es: "La compactación en la base de datos remota se completó correctamente."; - }; - readonly "Setup.Compaction on remote database failed.": { - readonly es: "La compactación en la base de datos remota falló."; - }; - readonly "Setup.Compaction on remote database timed out.": { - readonly es: "La compactación en la base de datos remota agotó el tiempo de espera."; - }; - readonly "Setup.Device": { - readonly es: "Dispositivo"; - }; - readonly "Setup.Failed to connect to remote for compaction.": { - readonly es: "No se pudo conectar a la base de datos remota para la compactación."; - }; - readonly "Setup.Failed to connect to remote for compaction. ${reason}": { - readonly es: "No se pudo conectar a la base de datos remota para la compactación. ${reason}"; - }; - readonly "Setup.Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": { - readonly es: "No se pudo iniciar la replicación de una sola vez antes de la recolección de basura. La recolección de basura se canceló."; - }; - readonly "Setup.Failed to start replication after Garbage Collection.": { - readonly es: "No se pudo iniciar la replicación después de la recolección de basura."; - }; - readonly "Setup.Garbage Collection cancelled by user.": { - readonly es: "El usuario canceló la recolección de basura."; - }; - readonly "Setup.Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": { - readonly es: "Recolección de basura completada. Chunks eliminados: ${deletedChunks} / ${totalChunks}. Tiempo empleado: ${seconds} segundos."; - }; - readonly "Setup.Garbage Collection Confirmation": { - readonly es: "Confirmación de recolección de basura"; - }; - readonly "Setup.Garbage Collection: Found ${unusedChunks} unused chunks to delete.": { - readonly es: "Recolección de basura: se encontraron ${unusedChunks} chunks no usados para eliminar."; - }; - readonly "Setup.Garbage Collection: Scanned ${scanned} / ~${docCount}": { - readonly es: "Recolección de basura: escaneados ${scanned} / ~${docCount}"; - }; - readonly "Setup.Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": { - readonly es: "Recolección de basura: escaneo completado. Chunks totales: ${totalChunks}, chunks usados: ${usedChunks}"; - }; - readonly "Setup.Ignore and Proceed": { - readonly es: "Ignorar y continuar"; - }; - readonly "Setup.No connected device information found. Cancelling Garbage Collection.": { - readonly es: "No se encontró información de dispositivos conectados. Cancelando la recolección de basura."; - }; - readonly "Setup.Node ID": { - readonly es: "ID del nodo"; - }; - readonly "Setup.Node Information Missing": { - readonly es: "Falta información del nodo"; - }; - readonly "Setup.Obsidian version": { - readonly es: "Versión de Obsidian"; - }; - readonly "Setup.optionNoSetupUri": { - readonly es: "No, no tengo"; - }; - readonly "Setup.optionRemindNextLaunch": { - readonly es: "Recordármelo en el próximo inicio"; - }; - readonly "Setup.optionSetupWizard": { - readonly es: "Llévame al asistente de configuración"; - }; - readonly "Setup.optionYesFetchAgain": { - readonly es: "Sí, obtener nuevamente"; - }; - readonly "Setup.Please disable 'Read chunks online' in settings to use Garbage Collection.": { - readonly es: "Desactiva \"Read chunks online\" en los ajustes para usar la recolección de basura."; - }; - readonly "Setup.Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": { - readonly es: "Activa \"Compute revisions for chunks\" en los ajustes para usar la recolección de basura."; - }; - readonly "Setup.Please select 'Cancel' explicitly to cancel this operation.": { - readonly es: "Selecciona explícitamente \"Cancelar\" para cancelar esta operación."; - }; - readonly "Setup.Plug-in version": { - readonly es: "Versión del complemento"; - }; - readonly "Setup.Proceed Garbage Collection": { - readonly es: "Continuar con la recolección de basura"; - }; - readonly "Setup.Proceeding with Garbage Collection, ignoring missing nodes.": { - readonly es: "Continuando con la recolección de basura e ignorando los nodos faltantes."; - }; - readonly "Setup.Proceeding with Garbage Collection.": { - readonly es: "Continuando con la recolección de basura."; - }; - readonly "Setup.Progress": { - readonly es: "Progreso"; - }; - readonly "Setup.Setup URI dialog cancelled.": { - readonly es: "Se canceló el diálogo de Setup URI."; - }; - readonly "Setup.Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": { - readonly es: "Algunos dispositivos tienen valores de progreso diferentes (máx.: ${maxProgress}, mín.: ${minProgress}).\nEsto puede indicar que algunos dispositivos no han completado la sincronización, lo que podría causar conflictos. Se recomienda encarecidamente confirmar que todos los dispositivos estén sincronizados antes de continuar."; - }; - readonly "Setup.The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": { - readonly es: "Los siguientes nodos aceptados no tienen información del nodo:\n- ${missingNodes}\n\nEsto indica que no se han conectado desde hace algún tiempo o que se han quedado en una versión anterior.\nSi es posible, es preferible actualizar todos los dispositivos. Si tienes dispositivos que ya no se usan, puedes borrar todos los nodos aceptados bloqueando el remoto una vez."; - }; - readonly "Setup.titleCaseSensitivity": { - readonly es: "Sensibilidad a mayúsculas"; - }; - readonly "Setup.titleRecommendSetupUri": { - readonly es: "Recomendación de uso de URI de configuración"; - }; - readonly "Setup.titleWelcome": { - readonly es: "Bienvenido a Self-hosted LiveSync"; - }; - readonly "(Not recommended) If set, credentials will be stored in the file": { - readonly ru: "(Не рекомендуется) Если установлено, учётные данные будут сохранены в файле"; - }; - readonly "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding.": { - readonly ru: "До v0.17.16 мы использовали старый адаптер для локальной базы данных. Теперь предпочтителен новый адаптер. Однако требуется перестроение локальной базы данных."; - }; - readonly descConnectSetupURI: { - readonly ru: "Это рекомендуемый способ настройки Self-hosted LiveSync с помощью Setup URI."; - }; - readonly descCopySetupURI: { - readonly ru: "Идеально для настройки нового устройства!"; - }; - readonly descEnableLiveSync: { - readonly ru: "Включайте это только после настройки одного из двух вариантов выше."; - }; - readonly descFetchConfigFromRemote: { - readonly ru: "Загрузить необходимые настройки с уже настроенного удалённого сервера."; - }; - readonly descManualSetup: { - readonly ru: "Не рекомендуется, но полезно, если у вас нет Setup URI"; - }; - readonly descTestDatabaseConnection: { - readonly ru: "Открыть подключение к базе данных."; - }; - readonly descValidateDatabaseConfig: { - readonly ru: "Проверяет и исправляет потенциальные проблемы с конфигурацией базы данных."; - }; - readonly "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this.": { - readonly ru: "Если включено, будет использоваться эффективная синхронизация настроек для каждого файла."; - }; - readonly "If this is set, changes to local files which are matched by the ignore files will be skipped.": { - readonly ru: "Если установлено, изменения файлов из списка игнорирования будут пропущены."; - }; - readonly "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely.": { - readonly ru: "Если эта опция включена, PouchDB будет держать соединение открытым 60 секунд."; - }; - readonly "Number of batches to process at a time. Defaults to 40. Minimum is 2.": { - readonly ru: "Количество пакетов для обработки за раз. По умолчанию 40. Минимум 2."; - }; - readonly "Save settings to a markdown file.": { - readonly ru: "Сохранить настройки в файл markdown."; - }; - readonly "The maximum duration for which chunks can be incubated within the document.": { - readonly ru: "Максимальная продолжительность инкубации чанков в документе."; - }; - readonly "The maximum number of chunks that can be incubated within the document.": { - readonly ru: "Максимальное количество инкубируемых чанков в документе."; - }; - readonly "The maximum total size of chunks that can be incubated within the document.": { - readonly ru: "Максимальный общий размер инкубируемых чанков в документе."; - }; - readonly "This passphrase will not be copied to another device. It will be set to until you configure it again.": { - readonly ru: "Эта парольная фраза не будет скопирована на другое устройство."; - }; - readonly "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name.": { - readonly ru: "Внимание! Это серьёзно повлияет на производительность."; - }; -}; diff --git a/_types/src/lib/src/common/messages/de.d.ts b/_types/src/lib/src/common/messages/de.d.ts deleted file mode 100644 index d7fe2e52..00000000 --- a/_types/src/lib/src/common/messages/de.d.ts +++ /dev/null @@ -1,299 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly de: { - "(Active)": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - Back: string; - "Back to non-configured": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync (Beta3)": string; - "Database Adapter": string; - Default: string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "Disables all synchronization and restart.": string; - "Display name": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Encrypting sensitive configuration items": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Hidden Files": string; - "Hide completely": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "Ignore and Proceed": string; - "Ignore patterns": string; - "Import connection": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "More actions": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set this device name": string; - "Plug-in version": string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - Rename: string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "S3/MinIO/R2 Object Storage": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram!": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setup URI dialog cancelled.": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Show full banner": string; - "Show icon only": string; - "Show status icon instead of file warnings banner": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Use a Setup URI (Recommended)": string; - "Verify all": string; - "Verify and repair all files": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/def.d.ts b/_types/src/lib/src/common/messages/def.d.ts deleted file mode 100644 index 5b94a24d..00000000 --- a/_types/src/lib/src/common/messages/def.d.ts +++ /dev/null @@ -1,1121 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly def: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Changing this setting requires migrating existing data (a bit time may be taken) and restarting Obsidian. Please make sure to back up your data before proceeding.": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database -> Storage": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - "Document History": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - "File to view History": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "Hide completely": string; - "Highlight diff": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": string; - "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-he": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.mismatchedTweakDetected": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.logServerConfigurationCheck": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Pick a file to show history": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "Property Encryption": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recovery and Repair": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show history": string; - "Show icon only": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Storage -> Database": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.DisableAutoAcceptCompatible": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.EnableAutoAcceptCompatible": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.mineUpdated": string; - "TweakMismatchResolve.Message.remoteUpdated": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.AutoAcceptCompatible": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Ui.Common.Signal.Caution": string; - "Ui.Common.Signal.Danger": string; - "Ui.Common.Signal.Notice": string; - "Ui.Common.Signal.Warning": string; - "Ui.Settings.Advanced.LocalDatabaseTweak": string; - "Ui.Settings.Advanced.MemoryCache": string; - "Ui.Settings.Advanced.TransferTweak": string; - "Ui.Settings.Common.Analyse": string; - "Ui.Settings.Common.Back": string; - "Ui.Settings.Common.Check": string; - "Ui.Settings.Common.Configure": string; - "Ui.Settings.Common.Continue": string; - "Ui.Settings.Common.Delete": string; - "Ui.Settings.Common.Fetch": string; - "Ui.Settings.Common.Lock": string; - "Ui.Settings.Common.Merge": string; - "Ui.Settings.Common.Open": string; - "Ui.Settings.Common.Overwrite": string; - "Ui.Settings.Common.Perform": string; - "Ui.Settings.Common.ResetAll": string; - "Ui.Settings.Common.ResolveAll": string; - "Ui.Settings.Common.Scan": string; - "Ui.Settings.Common.Send": string; - "Ui.Settings.Common.Use": string; - "Ui.Settings.Common.VerifyAll": string; - "Ui.Settings.CustomizationSync.OpenDesc": string; - "Ui.Settings.CustomizationSync.Panel": string; - "Ui.Settings.CustomizationSync.WarnChangeDeviceName": string; - "Ui.Settings.CustomizationSync.WarnSetDeviceName": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsage": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": string; - "Ui.Settings.Hatch.BackToNonConfigured": string; - "Ui.Settings.Hatch.ConvertNonObfuscated": string; - "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": string; - "Ui.Settings.Hatch.CopyIssueReport": string; - "Ui.Settings.Hatch.DatabaseLabel": string; - "Ui.Settings.Hatch.DatabaseToStorage": string; - "Ui.Settings.Hatch.DeleteCustomizationSyncData": string; - "Ui.Settings.Hatch.GeneratedReport": string; - "Ui.Settings.Hatch.Missing": string; - "Ui.Settings.Hatch.ModifiedSize": string; - "Ui.Settings.Hatch.ModifiedSizeActual": string; - "Ui.Settings.Hatch.PrepareIssueReport": string; - "Ui.Settings.Hatch.RecoveryAndRepair": string; - "Ui.Settings.Hatch.RecreateAll": string; - "Ui.Settings.Hatch.RecreateMissingChunks": string; - "Ui.Settings.Hatch.RecreateMissingChunksDesc": string; - "Ui.Settings.Hatch.ResetPanel": string; - "Ui.Settings.Hatch.ResetRemoteUsage": string; - "Ui.Settings.Hatch.ResetRemoteUsageDesc": string; - "Ui.Settings.Hatch.ResolveAllConflictedFiles": string; - "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": string; - "Ui.Settings.Hatch.RunDoctor": string; - "Ui.Settings.Hatch.ScanBrokenFiles": string; - "Ui.Settings.Hatch.ScramSwitches": string; - "Ui.Settings.Hatch.ShowHistory": string; - "Ui.Settings.Hatch.StorageLabel": string; - "Ui.Settings.Hatch.StorageToDatabase": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFiles": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": string; - "Ui.Settings.Maintenance.Cleanup": string; - "Ui.Settings.Maintenance.CleanupDesc": string; - "Ui.Settings.Maintenance.DeleteLocalDatabase": string; - "Ui.Settings.Maintenance.EmergencyRestart": string; - "Ui.Settings.Maintenance.EmergencyRestartDesc": string; - "Ui.Settings.Maintenance.FreshStartWipe": string; - "Ui.Settings.Maintenance.FreshStartWipeDesc": string; - "Ui.Settings.Maintenance.GarbageCollection": string; - "Ui.Settings.Maintenance.GarbageCollectionAction": string; - "Ui.Settings.Maintenance.GarbageCollectionDesc": string; - "Ui.Settings.Maintenance.LockServer": string; - "Ui.Settings.Maintenance.LockServerDesc": string; - "Ui.Settings.Maintenance.OverwriteRemote": string; - "Ui.Settings.Maintenance.OverwriteRemoteDesc": string; - "Ui.Settings.Maintenance.OverwriteServerData": string; - "Ui.Settings.Maintenance.OverwriteServerDataDesc": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounter": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.RebuildingOperations": string; - "Ui.Settings.Maintenance.Resend": string; - "Ui.Settings.Maintenance.ResendDesc": string; - "Ui.Settings.Maintenance.Reset": string; - "Ui.Settings.Maintenance.ResetAllJournalCounter": string; - "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.ResetJournalReceived": string; - "Ui.Settings.Maintenance.ResetJournalReceivedDesc": string; - "Ui.Settings.Maintenance.ResetJournalSent": string; - "Ui.Settings.Maintenance.ResetJournalSentDesc": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfo": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": string; - "Ui.Settings.Maintenance.ResetReceived": string; - "Ui.Settings.Maintenance.ResetSentHistory": string; - "Ui.Settings.Maintenance.ResetThisDevice": string; - "Ui.Settings.Maintenance.ScheduleAndRestart": string; - "Ui.Settings.Maintenance.Scram": string; - "Ui.Settings.Maintenance.SendChunks": string; - "Ui.Settings.Maintenance.Syncing": string; - "Ui.Settings.Maintenance.WarningLockedReadyAction": string; - "Ui.Settings.Maintenance.WarningLockedReadyText": string; - "Ui.Settings.Maintenance.WarningLockedResolveAction": string; - "Ui.Settings.Maintenance.WarningLockedResolveText": string; - "Ui.Settings.Maintenance.WriteRedFlagAndRestart": string; - "Ui.Settings.Patches.CompatibilityConflict": string; - "Ui.Settings.Patches.CompatibilityDatabase": string; - "Ui.Settings.Patches.CompatibilityInternalApi": string; - "Ui.Settings.Patches.CompatibilityMetadata": string; - "Ui.Settings.Patches.CompatibilityRemote": string; - "Ui.Settings.Patches.CompatibilityTrouble": string; - "Ui.Settings.Patches.CurrentAdapter": string; - "Ui.Settings.Patches.DatabaseAdapter": string; - "Ui.Settings.Patches.DatabaseAdapterDesc": string; - "Ui.Settings.Patches.EdgeCaseBehaviour": string; - "Ui.Settings.Patches.EdgeCaseDatabase": string; - "Ui.Settings.Patches.EdgeCaseProcessing": string; - "Ui.Settings.Patches.IndexedDbWarning": string; - "Ui.Settings.Patches.MigratingToIdb": string; - "Ui.Settings.Patches.MigratingToIndexedDb": string; - "Ui.Settings.Patches.MigrationIdbCompleted": string; - "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationIndexedDbCompleted": string; - "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationWarning": string; - "Ui.Settings.Patches.OperationToIdb": string; - "Ui.Settings.Patches.OperationToIndexedDb": string; - "Ui.Settings.Patches.Remediation": string; - "Ui.Settings.Patches.RemediationChanged": string; - "Ui.Settings.Patches.RemediationNoLimit": string; - "Ui.Settings.Patches.RemediationRestarting": string; - "Ui.Settings.Patches.RemediationRestartLater": string; - "Ui.Settings.Patches.RemediationRestartMessage": string; - "Ui.Settings.Patches.RemediationRestartNow": string; - "Ui.Settings.Patches.RemediationSuffixChanged": string; - "Ui.Settings.Patches.RemediationWithValue": string; - "Ui.Settings.Patches.RemoteDatabaseSunset": string; - "Ui.Settings.Patches.SwitchToIDB": string; - "Ui.Settings.Patches.SwitchToIndexedDb": string; - "Ui.Settings.PowerUsers.ConfigurationEncryption": string; - "Ui.Settings.PowerUsers.ConnectionTweak": string; - "Ui.Settings.PowerUsers.ConnectionTweakDesc": string; - "Ui.Settings.PowerUsers.Default": string; - "Ui.Settings.PowerUsers.Developer": string; - "Ui.Settings.PowerUsers.EncryptSensitiveConfig": string; - "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": string; - "Ui.Settings.PowerUsers.UseCustomPassphrase": string; - "Ui.Settings.Remote.Activate": string; - "Ui.Settings.Remote.ActiveSuffix": string; - "Ui.Settings.Remote.AddConnection": string; - "Ui.Settings.Remote.AddRemoteDefaultName": string; - "Ui.Settings.Remote.ConfigureAndChangeRemote": string; - "Ui.Settings.Remote.ConfigureE2EE": string; - "Ui.Settings.Remote.ConfigureRemote": string; - "Ui.Settings.Remote.DeleteRemoteConfirm": string; - "Ui.Settings.Remote.DeleteRemoteTitle": string; - "Ui.Settings.Remote.DisplayName": string; - "Ui.Settings.Remote.DuplicateRemote": string; - "Ui.Settings.Remote.DuplicateRemoteSuffix": string; - "Ui.Settings.Remote.E2EEConfiguration": string; - "Ui.Settings.Remote.Export": string; - "Ui.Settings.Remote.FetchRemoteSettings": string; - "Ui.Settings.Remote.ImportConnection": string; - "Ui.Settings.Remote.ImportConnectionPrompt": string; - "Ui.Settings.Remote.ImportedCouchDb": string; - "Ui.Settings.Remote.ImportedRemote": string; - "Ui.Settings.Remote.MoreActions": string; - "Ui.Settings.Remote.PeerToPeerPanel": string; - "Ui.Settings.Remote.RemoteConfigurationPrefix": string; - "Ui.Settings.Remote.RemoteDatabases": string; - "Ui.Settings.Remote.RemoteName": string; - "Ui.Settings.Remote.RemoteNameCouchDb": string; - "Ui.Settings.Remote.RemoteNameP2P": string; - "Ui.Settings.Remote.RemoteNameS3": string; - "Ui.Settings.Remote.Rename": string; - "Ui.Settings.Selector.AddDefaultPatterns": string; - "Ui.Settings.Selector.CrossPlatform": string; - "Ui.Settings.Selector.Default": string; - "Ui.Settings.Selector.HiddenFiles": string; - "Ui.Settings.Selector.IgnorePatterns": string; - "Ui.Settings.Selector.NonSynchronisingFiles": string; - "Ui.Settings.Selector.NonSynchronisingFilesDesc": string; - "Ui.Settings.Selector.NormalFiles": string; - "Ui.Settings.Selector.OverwritePatterns": string; - "Ui.Settings.Selector.OverwritePatternsDesc": string; - "Ui.Settings.Selector.SynchronisingFiles": string; - "Ui.Settings.Selector.SynchronisingFilesDesc": string; - "Ui.Settings.Selector.TargetPatterns": string; - "Ui.Settings.Selector.TargetPatternsDesc": string; - "Ui.Settings.Setup.RerunWizardButton": string; - "Ui.Settings.Setup.RerunWizardDesc": string; - "Ui.Settings.Setup.RerunWizardName": string; - "Ui.Settings.SyncSettings.Fetch": string; - "Ui.Settings.SyncSettings.Merge": string; - "Ui.Settings.SyncSettings.Overwrite": string; - "Ui.SetupWizard.Common.Back": string; - "Ui.SetupWizard.Common.Cancel": string; - "Ui.SetupWizard.Common.ProceedSelectOption": string; - "Ui.SetupWizard.Intro.ExistingOption": string; - "Ui.SetupWizard.Intro.ExistingOptionDesc": string; - "Ui.SetupWizard.Intro.Guidance": string; - "Ui.SetupWizard.Intro.NewOption": string; - "Ui.SetupWizard.Intro.NewOptionDesc": string; - "Ui.SetupWizard.Intro.ProceedExisting": string; - "Ui.SetupWizard.Intro.ProceedNew": string; - "Ui.SetupWizard.Intro.Question": string; - "Ui.SetupWizard.Intro.Title": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOption": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.Guidance": string; - "Ui.SetupWizard.OutroAskUserMode.NewOption": string; - "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedNext": string; - "Ui.SetupWizard.OutroAskUserMode.Question": string; - "Ui.SetupWizard.OutroAskUserMode.Title": string; - "Ui.SetupWizard.OutroNewUser.GuidancePrimary": string; - "Ui.SetupWizard.OutroNewUser.GuidanceWarning": string; - "Ui.SetupWizard.OutroNewUser.Important": string; - "Ui.SetupWizard.OutroNewUser.Proceed": string; - "Ui.SetupWizard.OutroNewUser.Question": string; - "Ui.SetupWizard.OutroNewUser.Title": string; - "Ui.SetupWizard.SelectExisting.Guidance": string; - "Ui.SetupWizard.SelectExisting.ManualOption": string; - "Ui.SetupWizard.SelectExisting.ManualOptionDesc": string; - "Ui.SetupWizard.SelectExisting.ProceedManual": string; - "Ui.SetupWizard.SelectExisting.ProceedQr": string; - "Ui.SetupWizard.SelectExisting.ProceedSetupUri": string; - "Ui.SetupWizard.SelectExisting.QrOption": string; - "Ui.SetupWizard.SelectExisting.QrOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Question": string; - "Ui.SetupWizard.SelectExisting.SetupUriOption": string; - "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Title": string; - "Ui.SetupWizard.SelectNew.Guidance": string; - "Ui.SetupWizard.SelectNew.ManualOption": string; - "Ui.SetupWizard.SelectNew.ManualOptionDesc": string; - "Ui.SetupWizard.SelectNew.ProceedManual": string; - "Ui.SetupWizard.SelectNew.ProceedSetupUri": string; - "Ui.SetupWizard.SelectNew.Question": string; - "Ui.SetupWizard.SelectNew.SetupUriOption": string; - "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectNew.Title": string; - "Ui.SetupWizard.SetupRemote.BucketOption": string; - "Ui.SetupWizard.SetupRemote.BucketOptionDesc": string; - "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": string; - "Ui.SetupWizard.SetupRemote.Guidance": string; - "Ui.SetupWizard.SetupRemote.P2POption": string; - "Ui.SetupWizard.SetupRemote.P2POptionDesc": string; - "Ui.SetupWizard.SetupRemote.ProceedBucket": string; - "Ui.SetupWizard.SetupRemote.ProceedCouchDb": string; - "Ui.SetupWizard.SetupRemote.ProceedP2P": string; - "Ui.SetupWizard.SetupRemote.Title": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/es.d.ts b/_types/src/lib/src/common/messages/es.d.ts deleted file mode 100644 index e6924555..00000000 --- a/_types/src/lib/src/common/messages/es.d.ts +++ /dev/null @@ -1,691 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly es: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "Always prompt merge conflicts": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks (Previous behaviour)": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - "Device name": string; - "Device Setup Method": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection V3 (Beta)": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "Keep empty folder": string; - "lang-de": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No limit configured": string; - "No, please take me back": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - Presets: string; - "Proceed with Setup URI": string; - "Process small files in the foreground": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setup.> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "Setup.All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Setup.Cancel Garbage Collection": string; - "Setup.Compaction in progress on remote database...": string; - "Setup.Compaction on remote database completed successfully.": string; - "Setup.Compaction on remote database failed.": string; - "Setup.Compaction on remote database timed out.": string; - "Setup.Device": string; - "Setup.Failed to connect to remote for compaction.": string; - "Setup.Failed to connect to remote for compaction. ${reason}": string; - "Setup.Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Setup.Failed to start replication after Garbage Collection.": string; - "Setup.Garbage Collection cancelled by user.": string; - "Setup.Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Setup.Garbage Collection Confirmation": string; - "Setup.Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Setup.Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Setup.Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Setup.Ignore and Proceed": string; - "Setup.No connected device information found. Cancelling Garbage Collection.": string; - "Setup.Node ID": string; - "Setup.Node Information Missing": string; - "Setup.Obsidian version": string; - "Setup.optionNoSetupUri": string; - "Setup.optionRemindNextLaunch": string; - "Setup.optionSetupWizard": string; - "Setup.optionYesFetchAgain": string; - "Setup.Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Setup.Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Setup.Please select 'Cancel' explicitly to cancel this operation.": string; - "Setup.Plug-in version": string; - "Setup.Proceed Garbage Collection": string; - "Setup.Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Setup.Proceeding with Garbage Collection.": string; - "Setup.Progress": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.Setup URI dialog cancelled.": string; - "Setup.Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Setup.The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "Setup.titleCaseSensitivity": string; - "Setup.titleRecommendSetupUri": string; - "Setup.titleWelcome": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/fr.d.ts b/_types/src/lib/src/common/messages/fr.d.ts deleted file mode 100644 index 8eaeed4e..00000000 --- a/_types/src/lib/src/common/messages/fr.d.ts +++ /dev/null @@ -1,584 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly fr: { - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "Access Key": string; - "Active Remote Configuration": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Automatically Sync all files when opening Obsidian.": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Check: string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compute revisions for chunks": string; - "Copy Report to clipboard": string; - "Data Compression": string; - "Database Name": string; - "Database suffix": string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - "Delete old metadata of deleted files on start-up": string; - "Device name": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - Filename: string; - "Forces the file to be synced when opened.": string; - "Handle files as Case-Sensitive": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore files": string; - "Incubate Chunks in Document": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Path Obfuscation": string; - "Per-file-saved customization sync": string; - "Periodic Sync interval": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Process small files in the foreground": string; - "Property Encryption": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - "Remote server type": string; - "Remote Type": string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - "Reset notification threshold and check the remote database usage": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Run Doctor": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/he.d.ts b/_types/src/lib/src/common/messages/he.d.ts deleted file mode 100644 index 2930fa5d..00000000 --- a/_types/src/lib/src/common/messages/he.d.ts +++ /dev/null @@ -1,585 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly he: { - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "Access Key": string; - "Active Remote Configuration": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Automatically Sync all files when opening Obsidian.": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Check: string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compute revisions for chunks": string; - "Copy Report to clipboard": string; - "Data Compression": string; - "Database Name": string; - "Database suffix": string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - "Delete old metadata of deleted files on start-up": string; - "Device name": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - Filename: string; - "Forces the file to be synced when opened.": string; - "Handle files as Case-Sensitive": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore files": string; - "Incubate Chunks in Document": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-he": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Path Obfuscation": string; - "Per-file-saved customization sync": string; - "Periodic Sync interval": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Process small files in the foreground": string; - "Property Encryption": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - "Remote server type": string; - "Remote Type": string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - "Reset notification threshold and check the remote database usage": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Run Doctor": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/ja.d.ts b/_types/src/lib/src/common/messages/ja.d.ts deleted file mode 100644 index bcc2abfd..00000000 --- a/_types/src/lib/src/common/messages/ja.d.ts +++ /dev/null @@ -1,840 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly ja: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/ko.d.ts b/_types/src/lib/src/common/messages/ko.d.ts deleted file mode 100644 index 77eb7a6b..00000000 --- a/_types/src/lib/src/common/messages/ko.d.ts +++ /dev/null @@ -1,802 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly ko: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/ru.d.ts b/_types/src/lib/src/common/messages/ru.d.ts deleted file mode 100644 index b95e153d..00000000 --- a/_types/src/lib/src/common/messages/ru.d.ts +++ /dev/null @@ -1,860 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly ru: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding.": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - descConnectSetupURI: string; - descCopySetupURI: string; - descEnableLiveSync: string; - descFetchConfigFromRemote: string; - descManualSetup: string; - descTestDatabaseConnection: string; - descValidateDatabaseConfig: string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this.": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - "Local Database Tweak": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2.": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "Property Encryption": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file.": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document.": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to until you configure it again.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Transfer Tweak": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name.": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/zh-tw.d.ts b/_types/src/lib/src/common/messages/zh-tw.d.ts deleted file mode 100644 index d2193760..00000000 --- a/_types/src/lib/src/common/messages/zh-tw.d.ts +++ /dev/null @@ -1,386 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly "zh-tw": { - "(Active)": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database -> Storage": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Document History": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - "File to view History": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "Hide completely": string; - "Highlight diff": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If you reached the payload size limit when using IBM Cloudant, please decrease batch size and batch limit to a lower value.": string; - "Ignore and Proceed": string; - "Ignore patterns": string; - "Import connection": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Initialise journal received history. On the next sync, every item except this device sent will be downloaded again.": string; - "Initialise journal sent history. On the next sync, every item except this device received will be sent again.": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "Minimum interval for syncing": string; - "More actions": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Pick a file to resolve conflict": string; - "Pick a file to show history": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - Progress: string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recovery and Repair": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - Rename: string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan for Broken files": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setup URI dialog cancelled.": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Show full banner": string; - "Show history": string; - "Show icon only": string; - "Show status icon instead of file warnings banner": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Storage -> Database": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The IndexedDB adapter often offers superior performance in certain scenarios, but it has been found to cause memory leaks when used with LiveSync mode. When using LiveSync mode, please use IDB adapter instead.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "Use a Setup URI (Recommended)": string; - "Verify all": string; - "Verify and repair all files": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/messages/zh.d.ts b/_types/src/lib/src/common/messages/zh.d.ts deleted file mode 100644 index 473845e4..00000000 --- a/_types/src/lib/src/common/messages/zh.d.ts +++ /dev/null @@ -1,1095 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const PartialMessages: { - readonly zh: { - "(Active)": string; - "(BETA) Always overwrite with a newer file": string; - "(Beta) Use ignore files": string; - "(Days passed, 0 to disable automatic-deletion)": string; - "(ex. Read chunks online) If this option is enabled, LiveSync reads chunks online directly instead of replicating them locally. Increasing Custom chunk size is recommended.": string; - "(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.": string; - "(Mega chars)": string; - "(Not recommended) If set, credentials will be stored in the file.": string; - "(Obsolete) Use an old adapter for compatibility": string; - "(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files.": string; - "(RegExp) If this is set, any changes to local and remote files that match this will be skipped.": string; - "(Select this if you are already using synchronisation on another computer or smartphone.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "(Select this if you are configuring this device as the first synchronisation device.) This option is suitable if you are new to LiveSync and want to set it up from scratch.": string; - "> [!INFO]- The connected devices have been detected as follows:\n${devices}": string; - "A Setup URI is a single string of text containing your server address and authentication details. Using a URI, if one was generated by your server installation script, provides a simple and secure configuration.": string; - "Access Key": string; - Activate: string; - "Active Remote Configuration": string; - "Add default patterns": string; - "Add new connection": string; - "All devices have the same progress value (${progress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.": string; - "Always prompt merge conflicts": string; - Analyse: string; - "Analyse database usage": string; - "Analyse database usage and generate a TSV report for diagnosis yourself. You can paste the generated report with any spreadsheet you like.": string; - "Apply Latest Change if Conflicting": string; - "Apply preset configuration": string; - "Ask a passphrase at every launch": string; - "Automatically Sync all files when opening Obsidian.": string; - Back: string; - "Back to non-configured": string; - "Batch database update": string; - "Batch limit": string; - "Batch size": string; - "Batch size of on-demand fetching": string; - "Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this.": string; - "Bucket Name": string; - Cancel: string; - "Cancel Garbage Collection": string; - Check: string; - "Check and convert non-path-obfuscated files": string; - "Check for documents that have not been converted to path-obfuscated IDs and convert them if necessary.": string; - "cmdConfigSync.showCustomizationSync": string; - "Comma separated `.gitignore, .dockerignore`": string; - "Compaction in progress on remote database...": string; - "Compaction on remote database completed successfully.": string; - "Compaction on remote database failed.": string; - "Compaction on remote database timed out.": string; - "Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep.": string; - "Compatibility (Conflict Behaviour)": string; - "Compatibility (Database structure)": string; - "Compatibility (Internal API Usage)": string; - "Compatibility (Metadata)": string; - "Compatibility (Remote Database)": string; - "Compatibility (Trouble addressed)": string; - "Compute revisions for chunks": string; - "Configuration Encryption": string; - Configure: string; - "Configure And Change Remote": string; - "Configure E2EE": string; - "Configure Remote": string; - "Configure the same server information as your other devices again, manually, very advanced users only.": string; - "Connection Method": string; - "Continue to CouchDB setup": string; - "Continue to Peer-to-Peer only setup": string; - "Continue to S3/MinIO/R2 setup": string; - Copy: string; - "Copy Report to clipboard": string; - "CouchDB Connection Tweak": string; - "Cross-platform": string; - "Current adapter: {adapter}": string; - "Customization Sync": string; - "Customization Sync (Beta3)": string; - "Data Compression": string; - "Database Adapter": string; - "Database Name": string; - "Database suffix": string; - Default: string; - "Delay conflict resolution of inactive files": string; - "Delay merge conflict prompt for inactive files.": string; - Delete: string; - "Delete all customization sync data": string; - "Delete all data on the remote server.": string; - "Delete local database to reset or uninstall Self-hosted LiveSync": string; - "Delete old metadata of deleted files on start-up": string; - "Delete Remote Configuration": string; - "Delete remote configuration '{name}'?": string; - desktop: string; - Developer: string; - Device: string; - "Device name": string; - "Device Setup Method": string; - "dialog.yourLanguageAvailable": string; - "dialog.yourLanguageAvailable.btnRevertToDefault": string; - "dialog.yourLanguageAvailable.Title": string; - "Disables all synchronization and restart.": string; - "Disables logging, only shows notifications. Please disable if you report an issue.": string; - "Display Language": string; - "Display name": string; - "Do not check configuration mismatch before replication": string; - "Do not keep metadata of deleted files.": string; - "Do not split chunks in the background": string; - "Do not use internal API": string; - "Doctor.Button.DismissThisVersion": string; - "Doctor.Button.Fix": string; - "Doctor.Button.FixButNoRebuild": string; - "Doctor.Button.No": string; - "Doctor.Button.Skip": string; - "Doctor.Button.Yes": string; - "Doctor.Dialogue.Main": string; - "Doctor.Dialogue.MainFix": string; - "Doctor.Dialogue.Title": string; - "Doctor.Dialogue.TitleAlmostDone": string; - "Doctor.Dialogue.TitleFix": string; - "Doctor.Level.Must": string; - "Doctor.Level.Necessary": string; - "Doctor.Level.Optional": string; - "Doctor.Level.Recommended": string; - "Doctor.Message.NoIssues": string; - "Doctor.Message.RebuildLocalRequired": string; - "Doctor.Message.RebuildRequired": string; - "Doctor.Message.SomeSkipped": string; - "Doctor.RULES.E2EE_V02500.REASON": string; - Duplicate: string; - "Duplicate remote": string; - "E2EE Configuration": string; - "Edge case addressing (Behaviour)": string; - "Edge case addressing (Database)": string; - "Edge case addressing (Processing)": string; - "Emergency restart": string; - "Enable advanced features": string; - "Enable customization sync": string; - "Enable Developers' Debug Tools.": string; - "Enable edge case treatment features": string; - "Enable poweruser features": string; - "Enable this if your Object Storage doesn't support CORS": string; - "Enable this option to automatically apply the most recent change to documents even when it conflicts": string; - "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": string; - "Encrypting sensitive configuration items": string; - "Encryption phassphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": string; - "End-to-End Encryption": string; - "Endpoint URL": string; - "Enhance chunk size": string; - "Enter Server Information": string; - "Enter the server information manually": string; - Export: string; - "Failed to connect to remote for compaction.": string; - "Failed to connect to remote for compaction. ${reason}": string; - "Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled.": string; - "Failed to start replication after Garbage Collection.": string; - Fetch: string; - "Fetch chunks on demand": string; - "Fetch database with previous behaviour": string; - "Fetch remote settings": string; - "File to resolve conflict": string; - Filename: string; - "First, please select the option that best describes your current situation.": string; - "Flag and restart": string; - "Forces the file to be synced when opened.": string; - "Fresh Start Wipe": string; - "Garbage Collection cancelled by user.": string; - "Garbage Collection completed. Deleted chunks: ${deletedChunks} / ${totalChunks}. Time taken: ${seconds} seconds.": string; - "Garbage Collection Confirmation": string; - "Garbage Collection V3 (Beta)": string; - "Garbage Collection: Found ${unusedChunks} unused chunks to delete.": string; - "Garbage Collection: Scanned ${scanned} / ~${docCount}": string; - "Garbage Collection: Scanning completed. Total chunks: ${totalChunks}, Used chunks: ${usedChunks}": string; - "Handle files as Case-Sensitive": string; - "Hidden Files": string; - "Hide completely": string; - "How to display network errors when the sync server is unreachable.": string; - "How would you like to configure the connection to your server?": string; - "I am adding a device to an existing synchronisation setup": string; - "I am setting this up for the first time": string; - "I know my server details, let me enter them": string; - "If disabled(toggled), chunks will be split on the UI thread (Previous behaviour).": string; - "If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions.": string; - "If enabled, chunks will be split into no more than 100 items. However, dedupe is slightly weaker.": string; - "If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised.": string; - "If enabled, the \u26D4 icon will be shown inside the status instead of the file warnings banner. No details will be shown.": string; - "If enabled, the file under 1kb will be processed in the UI thread.": string; - "If enabled, the notification of hidden files change will be suppressed.": string; - "If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour)": string; - "If this enabled, All files are handled as case-Sensitive (Previous behaviour).": string; - "If this enabled, chunks will be split into semantically meaningful segments. Not all platforms support this feature.": string; - "If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.": string; - "If this option is enabled, PouchDB will hold the connection open for 60 seconds, and if no change arrives in that time, close and reopen the socket, instead of holding it open indefinitely. Useful when a proxy limits request duration but can increase resource usage.": string; - "Ignore and Proceed": string; - "Ignore files": string; - "Ignore patterns": string; - "Import connection": string; - "Incubate Chunks in Document": string; - "Initialise all journal history, On the next sync, every item will be received and sent.": string; - "Interval (sec)": string; - "K.exp": string; - "K.long_p2p_sync": string; - "K.P2P": string; - "K.Peer": string; - "K.ScanCustomization": string; - "K.short_p2p_sync": string; - "K.title_p2p_sync": string; - "Keep empty folder": string; - lang_def: string; - "lang-de": string; - "lang-def": string; - "lang-es": string; - "lang-fr": string; - "lang-ja": string; - "lang-ko": string; - "lang-ru": string; - "lang-zh": string; - "lang-zh-tw": string; - Later: string; - "Limit: {datetime} ({timestamp})": string; - "LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured.": string; - "liveSyncReplicator.beforeLiveSync": string; - "liveSyncReplicator.cantReplicateLowerValue": string; - "liveSyncReplicator.checkingLastSyncPoint": string; - "liveSyncReplicator.couldNotConnectTo": string; - "liveSyncReplicator.couldNotConnectToRemoteDb": string; - "liveSyncReplicator.couldNotConnectToServer": string; - "liveSyncReplicator.couldNotConnectToURI": string; - "liveSyncReplicator.couldNotMarkResolveRemoteDb": string; - "liveSyncReplicator.liveSyncBegin": string; - "liveSyncReplicator.lockRemoteDb": string; - "liveSyncReplicator.markDeviceResolved": string; - "liveSyncReplicator.oneShotSyncBegin": string; - "liveSyncReplicator.remoteDbCorrupted": string; - "liveSyncReplicator.remoteDbCreatedOrConnected": string; - "liveSyncReplicator.remoteDbDestroyed": string; - "liveSyncReplicator.remoteDbDestroyError": string; - "liveSyncReplicator.remoteDbMarkedResolved": string; - "liveSyncReplicator.replicationClosed": string; - "liveSyncReplicator.replicationInProgress": string; - "liveSyncReplicator.retryLowerBatchSize": string; - "liveSyncReplicator.unlockRemoteDb": string; - "liveSyncSetting.errorNoSuchSettingItem": string; - "liveSyncSetting.originalValue": string; - "liveSyncSetting.valueShouldBeInRange": string; - "liveSyncSettings.btnApply": string; - Lock: string; - "Lock Server": string; - "Lock the remote server to prevent synchronization with other devices.": string; - "logPane.autoScroll": string; - "logPane.logWindowOpened": string; - "logPane.pause": string; - "logPane.title": string; - "logPane.wrap": string; - "Maximum delay for batch database updating": string; - "Maximum file size": string; - "Maximum Incubating Chunk Size": string; - "Maximum Incubating Chunks": string; - "Maximum Incubation Period": string; - "MB (0 to disable).": string; - "Memory cache size (by total characters)": string; - "Memory cache size (by total items)": string; - Merge: string; - "Minimum delay for batch database updating": string; - "Minimum interval for syncing": string; - "moduleCheckRemoteSize.logCheckingStorageSizes": string; - "moduleCheckRemoteSize.logCurrentStorageSize": string; - "moduleCheckRemoteSize.logExceededWarning": string; - "moduleCheckRemoteSize.logThresholdEnlarged": string; - "moduleCheckRemoteSize.msgConfirmRebuild": string; - "moduleCheckRemoteSize.msgDatabaseGrowing": string; - "moduleCheckRemoteSize.msgSetDBCapacity": string; - "moduleCheckRemoteSize.option2GB": string; - "moduleCheckRemoteSize.option800MB": string; - "moduleCheckRemoteSize.optionAskMeLater": string; - "moduleCheckRemoteSize.optionDismiss": string; - "moduleCheckRemoteSize.optionIncreaseLimit": string; - "moduleCheckRemoteSize.optionNoWarn": string; - "moduleCheckRemoteSize.optionRebuildAll": string; - "moduleCheckRemoteSize.titleDatabaseSizeLimitExceeded": string; - "moduleCheckRemoteSize.titleDatabaseSizeNotify": string; - "moduleInputUIObsidian.defaultTitleConfirmation": string; - "moduleInputUIObsidian.defaultTitleSelect": string; - "moduleInputUIObsidian.optionNo": string; - "moduleInputUIObsidian.optionYes": string; - "moduleLiveSyncMain.logAdditionalSafetyScan": string; - "moduleLiveSyncMain.logLoadingPlugin": string; - "moduleLiveSyncMain.logPluginInitCancelled": string; - "moduleLiveSyncMain.logPluginVersion": string; - "moduleLiveSyncMain.logReadChangelog": string; - "moduleLiveSyncMain.logSafetyScanCompleted": string; - "moduleLiveSyncMain.logSafetyScanFailed": string; - "moduleLiveSyncMain.logUnloadingPlugin": string; - "moduleLiveSyncMain.logVersionUpdate": string; - "moduleLiveSyncMain.msgScramEnabled": string; - "moduleLiveSyncMain.optionKeepLiveSyncDisabled": string; - "moduleLiveSyncMain.optionResumeAndRestart": string; - "moduleLiveSyncMain.titleScramEnabled": string; - "moduleLocalDatabase.logWaitingForReady": string; - "moduleLog.showLog": string; - "moduleMigration.docUri": string; - "moduleMigration.fix0256.buttons.checkItLater": string; - "moduleMigration.fix0256.buttons.DismissForever": string; - "moduleMigration.fix0256.buttons.fix": string; - "moduleMigration.fix0256.message": string; - "moduleMigration.fix0256.messageUnrecoverable": string; - "moduleMigration.fix0256.title": string; - "moduleMigration.insecureChunkExist.buttons.fetch": string; - "moduleMigration.insecureChunkExist.buttons.later": string; - "moduleMigration.insecureChunkExist.buttons.rebuild": string; - "moduleMigration.insecureChunkExist.laterMessage": string; - "moduleMigration.insecureChunkExist.message": string; - "moduleMigration.insecureChunkExist.title": string; - "moduleMigration.logBulkSendCorrupted": string; - "moduleMigration.logFetchRemoteTweakFailed": string; - "moduleMigration.logLocalDatabaseNotReady": string; - "moduleMigration.logMigratedSameBehaviour": string; - "moduleMigration.logMigrationFailed": string; - "moduleMigration.logRedflag2CreationFail": string; - "moduleMigration.logRemoteTweakUnavailable": string; - "moduleMigration.logSetupCancelled": string; - "moduleMigration.msgFetchRemoteAgain": string; - "moduleMigration.msgInitialSetup": string; - "moduleMigration.msgRecommendSetupUri": string; - "moduleMigration.msgSinceV02321": string; - "moduleMigration.optionAdjustRemote": string; - "moduleMigration.optionDecideLater": string; - "moduleMigration.optionEnableBoth": string; - "moduleMigration.optionEnableFilenameCaseInsensitive": string; - "moduleMigration.optionEnableFixedRevisionForChunks": string; - "moduleMigration.optionHaveSetupUri": string; - "moduleMigration.optionKeepPreviousBehaviour": string; - "moduleMigration.optionManualSetup": string; - "moduleMigration.optionNoAskAgain": string; - "moduleMigration.optionNoSetupUri": string; - "moduleMigration.optionRemindNextLaunch": string; - "moduleMigration.optionSetupViaP2P": string; - "moduleMigration.optionSetupWizard": string; - "moduleMigration.optionYesFetchAgain": string; - "moduleMigration.titleCaseSensitivity": string; - "moduleMigration.titleRecommendSetupUri": string; - "moduleMigration.titleWelcome": string; - "moduleObsidianMenu.replicate": string; - "More actions": string; - "Move remotely deleted files to the trash, instead of deleting.": string; - "Network warning style": string; - "New Remote": string; - "No connected device information found. Cancelling Garbage Collection.": string; - "No limit configured": string; - "No, please take me back": string; - "Node ID": string; - "Node Information Missing": string; - "Non-Synchronising files": string; - "Normal Files": string; - "Not all messages have been translated. And, please revert to \"Default\" when reporting errors.": string; - "Notify all setting files": string; - "Notify customized": string; - "Notify when other device has newly customized.": string; - "Notify when the estimated remote storage size exceeds on start up": string; - "Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.": string; - "Number of changes to sync at a time. Defaults to 50. Minimum is 2.": string; - "Obsidian version": string; - "obsidianLiveSyncSettingTab.btnApply": string; - "obsidianLiveSyncSettingTab.btnCheck": string; - "obsidianLiveSyncSettingTab.btnCopy": string; - "obsidianLiveSyncSettingTab.btnDisable": string; - "obsidianLiveSyncSettingTab.btnDiscard": string; - "obsidianLiveSyncSettingTab.btnEnable": string; - "obsidianLiveSyncSettingTab.btnFix": string; - "obsidianLiveSyncSettingTab.btnGotItAndUpdated": string; - "obsidianLiveSyncSettingTab.btnNext": string; - "obsidianLiveSyncSettingTab.btnStart": string; - "obsidianLiveSyncSettingTab.btnTest": string; - "obsidianLiveSyncSettingTab.btnUse": string; - "obsidianLiveSyncSettingTab.buttonFetch": string; - "obsidianLiveSyncSettingTab.buttonNext": string; - "obsidianLiveSyncSettingTab.defaultLanguage": string; - "obsidianLiveSyncSettingTab.descConnectSetupURI": string; - "obsidianLiveSyncSettingTab.descCopySetupURI": string; - "obsidianLiveSyncSettingTab.descEnableLiveSync": string; - "obsidianLiveSyncSettingTab.descFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.descManualSetup": string; - "obsidianLiveSyncSettingTab.descTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.descValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.errAccessForbidden": string; - "obsidianLiveSyncSettingTab.errCannotContinueTest": string; - "obsidianLiveSyncSettingTab.errCorsCredentials": string; - "obsidianLiveSyncSettingTab.errCorsNotAllowingCredentials": string; - "obsidianLiveSyncSettingTab.errCorsOrigins": string; - "obsidianLiveSyncSettingTab.errEnableCors": string; - "obsidianLiveSyncSettingTab.errEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.errMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.errMaxRequestSize": string; - "obsidianLiveSyncSettingTab.errMissingWwwAuth": string; - "obsidianLiveSyncSettingTab.errRequireValidUser": string; - "obsidianLiveSyncSettingTab.errRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.labelDisabled": string; - "obsidianLiveSyncSettingTab.labelEnabled": string; - "obsidianLiveSyncSettingTab.levelAdvanced": string; - "obsidianLiveSyncSettingTab.levelEdgeCase": string; - "obsidianLiveSyncSettingTab.levelPowerUser": string; - "obsidianLiveSyncSettingTab.linkOpenInBrowser": string; - "obsidianLiveSyncSettingTab.linkPageTop": string; - "obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting": string; - "obsidianLiveSyncSettingTab.linkTroubleshooting": string; - "obsidianLiveSyncSettingTab.logCannotUseCloudant": string; - "obsidianLiveSyncSettingTab.logCheckingConfigDone": string; - "obsidianLiveSyncSettingTab.logCheckingConfigFailed": string; - "obsidianLiveSyncSettingTab.logCheckingDbConfig": string; - "obsidianLiveSyncSettingTab.logCheckPassphraseFailed": string; - "obsidianLiveSyncSettingTab.logConfiguredDisabled": string; - "obsidianLiveSyncSettingTab.logConfiguredLiveSync": string; - "obsidianLiveSyncSettingTab.logConfiguredPeriodic": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigFail": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigSet": string; - "obsidianLiveSyncSettingTab.logCouchDbConfigUpdated": string; - "obsidianLiveSyncSettingTab.logDatabaseConnected": string; - "obsidianLiveSyncSettingTab.logEncryptionNoPassphrase": string; - "obsidianLiveSyncSettingTab.logEncryptionNoSupport": string; - "obsidianLiveSyncSettingTab.logErrorOccurred": string; - "obsidianLiveSyncSettingTab.logEstimatedSize": string; - "obsidianLiveSyncSettingTab.logPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.logPassphraseNotCompatible": string; - "obsidianLiveSyncSettingTab.logRebuildNote": string; - "obsidianLiveSyncSettingTab.logSelectAnyPreset": string; - "obsidianLiveSyncSettingTab.msgAreYouSureProceed": string; - "obsidianLiveSyncSettingTab.msgChangesNeedToBeApplied": string; - "obsidianLiveSyncSettingTab.msgConfigCheck": string; - "obsidianLiveSyncSettingTab.msgConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.msgConnectionCheck": string; - "obsidianLiveSyncSettingTab.msgConnectionProxyNote": string; - "obsidianLiveSyncSettingTab.msgCurrentOrigin": string; - "obsidianLiveSyncSettingTab.msgDiscardConfirmation": string; - "obsidianLiveSyncSettingTab.msgDone": string; - "obsidianLiveSyncSettingTab.msgEnableCors": string; - "obsidianLiveSyncSettingTab.msgEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.msgEnableEncryptionRecommendation": string; - "obsidianLiveSyncSettingTab.msgFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.msgGenerateSetupURI": string; - "obsidianLiveSyncSettingTab.msgIfConfigNotPersistent": string; - "obsidianLiveSyncSettingTab.msgInvalidPassphrase": string; - "obsidianLiveSyncSettingTab.msgNewVersionNote": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSInfo": string; - "obsidianLiveSyncSettingTab.msgNonHTTPSWarning": string; - "obsidianLiveSyncSettingTab.msgNotice": string; - "obsidianLiveSyncSettingTab.msgObjectStorageWarning": string; - "obsidianLiveSyncSettingTab.msgOriginCheck": string; - "obsidianLiveSyncSettingTab.msgRebuildRequired": string; - "obsidianLiveSyncSettingTab.msgSelectAndApplyPreset": string; - "obsidianLiveSyncSettingTab.msgSetCorsCredentials": string; - "obsidianLiveSyncSettingTab.msgSetCorsOrigins": string; - "obsidianLiveSyncSettingTab.msgSetMaxDocSize": string; - "obsidianLiveSyncSettingTab.msgSetMaxRequestSize": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUser": string; - "obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.msgSettingModified": string; - "obsidianLiveSyncSettingTab.msgSettingsUnchangeableDuringSync": string; - "obsidianLiveSyncSettingTab.msgSetWwwAuth": string; - "obsidianLiveSyncSettingTab.nameApplySettings": string; - "obsidianLiveSyncSettingTab.nameConnectSetupURI": string; - "obsidianLiveSyncSettingTab.nameCopySetupURI": string; - "obsidianLiveSyncSettingTab.nameDisableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameDiscardSettings": string; - "obsidianLiveSyncSettingTab.nameEnableHiddenFileSync": string; - "obsidianLiveSyncSettingTab.nameEnableLiveSync": string; - "obsidianLiveSyncSettingTab.nameHiddenFileSynchronization": string; - "obsidianLiveSyncSettingTab.nameManualSetup": string; - "obsidianLiveSyncSettingTab.nameTestConnection": string; - "obsidianLiveSyncSettingTab.nameTestDatabaseConnection": string; - "obsidianLiveSyncSettingTab.nameValidateDatabaseConfig": string; - "obsidianLiveSyncSettingTab.okAdminPrivileges": string; - "obsidianLiveSyncSettingTab.okCorsCredentials": string; - "obsidianLiveSyncSettingTab.okCorsCredentialsForOrigin": string; - "obsidianLiveSyncSettingTab.okCorsOriginMatched": string; - "obsidianLiveSyncSettingTab.okCorsOrigins": string; - "obsidianLiveSyncSettingTab.okEnableCors": string; - "obsidianLiveSyncSettingTab.okEnableCorsChttpd": string; - "obsidianLiveSyncSettingTab.okMaxDocumentSize": string; - "obsidianLiveSyncSettingTab.okMaxRequestSize": string; - "obsidianLiveSyncSettingTab.okRequireValidUser": string; - "obsidianLiveSyncSettingTab.okRequireValidUserAuth": string; - "obsidianLiveSyncSettingTab.okWwwAuth": string; - "obsidianLiveSyncSettingTab.optionApply": string; - "obsidianLiveSyncSettingTab.optionCancel": string; - "obsidianLiveSyncSettingTab.optionCouchDB": string; - "obsidianLiveSyncSettingTab.optionDisableAllAutomatic": string; - "obsidianLiveSyncSettingTab.optionFetchFromRemote": string; - "obsidianLiveSyncSettingTab.optionHere": string; - "obsidianLiveSyncSettingTab.optionLiveSync": string; - "obsidianLiveSyncSettingTab.optionMinioS3R2": string; - "obsidianLiveSyncSettingTab.optionOkReadEverything": string; - "obsidianLiveSyncSettingTab.optionOnEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicAndEvents": string; - "obsidianLiveSyncSettingTab.optionPeriodicWithBatch": string; - "obsidianLiveSyncSettingTab.optionRebuildBoth": string; - "obsidianLiveSyncSettingTab.optionSaveOnlySettings": string; - "obsidianLiveSyncSettingTab.panelChangeLog": string; - "obsidianLiveSyncSettingTab.panelGeneralSettings": string; - "obsidianLiveSyncSettingTab.panelPrivacyEncryption": string; - "obsidianLiveSyncSettingTab.panelRemoteConfiguration": string; - "obsidianLiveSyncSettingTab.panelSetup": string; - "obsidianLiveSyncSettingTab.serverVersion": string; - "obsidianLiveSyncSettingTab.titleActiveRemoteServer": string; - "obsidianLiveSyncSettingTab.titleAppearance": string; - "obsidianLiveSyncSettingTab.titleConflictResolution": string; - "obsidianLiveSyncSettingTab.titleCongratulations": string; - "obsidianLiveSyncSettingTab.titleCouchDB": string; - "obsidianLiveSyncSettingTab.titleDeletionPropagation": string; - "obsidianLiveSyncSettingTab.titleEncryptionNotEnabled": string; - "obsidianLiveSyncSettingTab.titleEncryptionPassphraseInvalid": string; - "obsidianLiveSyncSettingTab.titleExtraFeatures": string; - "obsidianLiveSyncSettingTab.titleFetchConfig": string; - "obsidianLiveSyncSettingTab.titleFetchConfigFromRemote": string; - "obsidianLiveSyncSettingTab.titleFetchSettings": string; - "obsidianLiveSyncSettingTab.titleHiddenFiles": string; - "obsidianLiveSyncSettingTab.titleLogging": string; - "obsidianLiveSyncSettingTab.titleMinioS3R2": string; - "obsidianLiveSyncSettingTab.titleNotification": string; - "obsidianLiveSyncSettingTab.titleOnlineTips": string; - "obsidianLiveSyncSettingTab.titleQuickSetup": string; - "obsidianLiveSyncSettingTab.titleRebuildRequired": string; - "obsidianLiveSyncSettingTab.titleRemoteConfigCheckFailed": string; - "obsidianLiveSyncSettingTab.titleRemoteServer": string; - "obsidianLiveSyncSettingTab.titleReset": string; - "obsidianLiveSyncSettingTab.titleSetupOtherDevices": string; - "obsidianLiveSyncSettingTab.titleSynchronizationMethod": string; - "obsidianLiveSyncSettingTab.titleSynchronizationPreset": string; - "obsidianLiveSyncSettingTab.titleSyncSettings": string; - "obsidianLiveSyncSettingTab.titleSyncSettingsViaMarkdown": string; - "obsidianLiveSyncSettingTab.titleUpdateThinning": string; - "obsidianLiveSyncSettingTab.warnCorsOriginUnmatched": string; - "obsidianLiveSyncSettingTab.warnNoAdmin": string; - Ok: string; - "Old Algorithm": string; - "Older fallback (Slow, W/O WebAssembly)": string; - Open: string; - "Open the dialog": string; - Overwrite: string; - "Overwrite patterns": string; - "Overwrite remote": string; - "Overwrite remote with local DB and passphrase.": string; - "Overwrite Server Data with This Device's Files": string; - "P2P.AskPassphraseForDecrypt": string; - "P2P.AskPassphraseForShare": string; - "P2P.DisabledButNeed": string; - "P2P.FailedToOpen": string; - "P2P.NoAutoSyncPeers": string; - "P2P.NoKnownPeers": string; - "P2P.Note.description": string; - "P2P.Note.important_note": string; - "P2P.Note.important_note_sub": string; - "P2P.Note.Summary": string; - "P2P.NotEnabled": string; - "P2P.P2PReplication": string; - "P2P.PaneTitle": string; - "P2P.ReplicatorInstanceMissing": string; - "P2P.SeemsOffline": string; - "P2P.SyncAlreadyRunning": string; - "P2P.SyncCompleted": string; - "P2P.SyncStartedWith": string; - "paneMaintenance.markDeviceResolvedAfterBackup": string; - "paneMaintenance.remoteLockedAndDeviceNotAccepted": string; - "paneMaintenance.remoteLockedResolvedDevice": string; - "paneMaintenance.unlockDatabaseReady": string; - Passphrase: string; - "Passphrase of sensitive configuration items": string; - password: string; - Password: string; - "Paste a connection string": string; - "Paste the Setup URI generated from one of your active devices.": string; - "Path Obfuscation": string; - "Patterns to match files for overwriting instead of merging": string; - "Patterns to match files for syncing": string; - "Peer-to-Peer only": string; - "Peer-to-Peer Synchronisation": string; - "Per-file-saved customization sync": string; - Perform: string; - "Perform cleanup": string; - "Perform Garbage Collection": string; - "Perform Garbage Collection to remove unused chunks and reduce database size.": string; - "Periodic Sync interval": string; - "Pick a file to resolve conflict": string; - "Please disable 'Read chunks online' in settings to use Garbage Collection.": string; - "Please enable 'Compute revisions for chunks' in settings to use Garbage Collection.": string; - "Please select 'Cancel' explicitly to cancel this operation.": string; - "Please select a method to import the settings from another device.": string; - "Please select an option to proceed": string; - "Please select the type of server to which you are connecting.": string; - "Please set device name to identify this device. This name should be unique among your devices. While not configured, we cannot enable this feature.": string; - "Please set this device name": string; - "Plug-in version": string; - "Prepare the 'report' to create an issue": string; - Presets: string; - "Proceed Garbage Collection": string; - "Proceed with Setup URI": string; - "Proceeding with Garbage Collection, ignoring missing nodes.": string; - "Proceeding with Garbage Collection.": string; - "Process small files in the foreground": string; - Progress: string; - "Property Encryption": string; - "PureJS fallback (Fast, W/O WebAssembly)": string; - "Purge all download/upload cache.": string; - "Purge all journal counter": string; - "Rebuild local and remote database with local files.": string; - "Rebuilding Operations (Remote Only)": string; - "Recreate all": string; - "Recreate missing chunks for all files": string; - "RedFlag.Fetch.Method.Desc": string; - "RedFlag.Fetch.Method.FetchSafer": string; - "RedFlag.Fetch.Method.FetchSmoother": string; - "RedFlag.Fetch.Method.FetchTraditional": string; - "RedFlag.Fetch.Method.Title": string; - "RedFlag.FetchRemoteConfig.Buttons.Cancel": string; - "RedFlag.FetchRemoteConfig.Buttons.Fetch": string; - "RedFlag.FetchRemoteConfig.Message": string; - "RedFlag.FetchRemoteConfig.Title": string; - "Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client.": string; - "Reducing the frequency with which on-disk changes are reflected into the DB": string; - Region: string; - Remediation: string; - "Remediation Setting Changed": string; - "Remote Database Tweak (In sunset)": string; - "Remote Databases": string; - "Remote name": string; - "Remote server type": string; - "Remote Type": string; - Rename: string; - "Replicator.Dialogue.Locked.Action.Dismiss": string; - "Replicator.Dialogue.Locked.Action.Fetch": string; - "Replicator.Dialogue.Locked.Action.Unlock": string; - "Replicator.Dialogue.Locked.Message": string; - "Replicator.Dialogue.Locked.Message.Fetch": string; - "Replicator.Dialogue.Locked.Message.Unlocked": string; - "Replicator.Dialogue.Locked.Title": string; - "Replicator.Message.Cleaned": string; - "Replicator.Message.InitialiseFatalError": string; - "Replicator.Message.Pending": string; - "Replicator.Message.SomeModuleFailed": string; - "Replicator.Message.VersionUpFlash": string; - "Requires restart of Obsidian": string; - "Requires restart of Obsidian.": string; - "Rerun Onboarding Wizard": string; - "Rerun the onboarding wizard to set up Self-hosted LiveSync again.": string; - "Rerun Wizard": string; - Resend: string; - "Resend all chunks to the remote.": string; - Reset: string; - "Reset all": string; - "Reset all journal counter": string; - "Reset journal received history": string; - "Reset journal sent history": string; - "Reset notification threshold and check the remote database usage": string; - "Reset received": string; - "Reset sent history": string; - "Reset Synchronisation information": string; - "Reset Synchronisation on This Device": string; - "Reset the remote storage size threshold and check the remote storage size again.": string; - "Resolve All": string; - "Resolve all conflicted files": string; - "Resolve All conflicted files by the newer one": string; - "Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one.": string; - "Restart Now": string; - "Restore or reconstruct local database from remote.": string; - "Run Doctor": string; - "S3/MinIO/R2 Object Storage": string; - "Save settings to a markdown file. You will be notified when new settings arrive. You can set different files by the platform.": string; - "Saving will be performed forcefully after this number of seconds.": string; - "Scan a QR Code (Recommended for mobile)": string; - "Scan changes on customization sync": string; - "Scan customization automatically": string; - "Scan customization before replicating.": string; - "Scan customization every 1 minute.": string; - "Scan customization periodically": string; - "Scan for Broken files": string; - "Scan for hidden files before replication": string; - "Scan hidden files periodically": string; - "Scan the QR code displayed on an active device using this device's camera.": string; - "Schedule and Restart": string; - "Scram Switches": string; - "Scram!": string; - "Seconds, 0 to disable": string; - "Seconds. Saving to the local database will be delayed until this value after we stop typing or saving.": string; - "Secret Key": string; - "Select the database adapter to use.": string; - Send: string; - "Send chunks": string; - "Server URI": string; - "Setting.GenerateKeyPair.Desc": string; - "Setting.GenerateKeyPair.Title": string; - "Setting.TroubleShooting": string; - "Setting.TroubleShooting.Doctor": string; - "Setting.TroubleShooting.Doctor.Desc": string; - "Setting.TroubleShooting.ScanBrokenFiles": string; - "Setting.TroubleShooting.ScanBrokenFiles.Desc": string; - "SettingTab.Message.AskRebuild": string; - "Setup URI dialog cancelled.": string; - "Setup.Apply.Buttons.ApplyAndFetch": string; - "Setup.Apply.Buttons.ApplyAndMerge": string; - "Setup.Apply.Buttons.ApplyAndRebuild": string; - "Setup.Apply.Buttons.Cancel": string; - "Setup.Apply.Buttons.OnlyApply": string; - "Setup.Apply.Message": string; - "Setup.Apply.Title": string; - "Setup.Apply.WarningRebuildRecommended": string; - "Setup.Doctor.Buttons.No": string; - "Setup.Doctor.Buttons.Yes": string; - "Setup.Doctor.Message": string; - "Setup.Doctor.Title": string; - "Setup.FetchRemoteConf.Buttons.Fetch": string; - "Setup.FetchRemoteConf.Buttons.Skip": string; - "Setup.FetchRemoteConf.Message": string; - "Setup.FetchRemoteConf.Title": string; - "Setup.QRCode": string; - "Setup.RemoteE2EE.AdvancedTitle": string; - "Setup.RemoteE2EE.AlgorithmWarning": string; - "Setup.RemoteE2EE.ButtonCancel": string; - "Setup.RemoteE2EE.ButtonProceed": string; - "Setup.RemoteE2EE.DefaultAlgorithmDesc": string; - "Setup.RemoteE2EE.Guidance": string; - "Setup.RemoteE2EE.LabelEncrypt": string; - "Setup.RemoteE2EE.LabelEncryptionAlgorithm": string; - "Setup.RemoteE2EE.LabelObfuscateProperties": string; - "Setup.RemoteE2EE.MultiDestinationWarning": string; - "Setup.RemoteE2EE.ObfuscatePropertiesDesc": string; - "Setup.RemoteE2EE.PassphraseValidationLine1": string; - "Setup.RemoteE2EE.PassphraseValidationLine2": string; - "Setup.RemoteE2EE.PlaceholderPassphrase": string; - "Setup.RemoteE2EE.StronglyRecommendedLine1": string; - "Setup.RemoteE2EE.StronglyRecommendedLine2": string; - "Setup.RemoteE2EE.StronglyRecommendedTitle": string; - "Setup.RemoteE2EE.Title": string; - "Setup.ScanQRCode.ButtonClose": string; - "Setup.ScanQRCode.Guidance": string; - "Setup.ScanQRCode.Step1": string; - "Setup.ScanQRCode.Step2": string; - "Setup.ScanQRCode.Step3": string; - "Setup.ScanQRCode.Step4": string; - "Setup.ScanQRCode.Title": string; - "Setup.ShowQRCode": string; - "Setup.ShowQRCode.Desc": string; - "Setup.UseSetupURI.ButtonCancel": string; - "Setup.UseSetupURI.ButtonProceed": string; - "Setup.UseSetupURI.ErrorFailedToParse": string; - "Setup.UseSetupURI.ErrorPassphraseRequired": string; - "Setup.UseSetupURI.GuidanceLine1": string; - "Setup.UseSetupURI.GuidanceLine2": string; - "Setup.UseSetupURI.InvalidInfo": string; - "Setup.UseSetupURI.LabelPassphrase": string; - "Setup.UseSetupURI.LabelSetupURI": string; - "Setup.UseSetupURI.PlaceholderPassphrase": string; - "Setup.UseSetupURI.Title": string; - "Setup.UseSetupURI.ValidInfo": string; - "Should we keep folders that don't have any files inside?": string; - "Should we only check for conflicts when a file is opened?": string; - "Should we prompt you about conflicting files when a file is opened?": string; - "Should we prompt you for every single merge, even if we can safely merge automatcially?": string; - "Show full banner": string; - "Show icon only": string; - "Show only notifications": string; - "Show status as icons only": string; - "Show status icon instead of file warnings banner": string; - "Show status inside the editor": string; - "Show status on the status bar": string; - "Show verbose log. Please enable if you report an issue.": string; - "Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).\nThis may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.": string; - "Starts synchronisation when a file is saved.": string; - "Stop reflecting database changes to storage files.": string; - "Stop watching for file changes.": string; - "Suppress notification of hidden files change": string; - "Suspend database reflecting": string; - "Suspend file watching": string; - "Switch to IDB": string; - "Switch to IndexedDB": string; - "Sync after merging file": string; - "Sync automatically after merging files": string; - "Sync Mode": string; - "Sync on Editor Save": string; - "Sync on File Open": string; - "Sync on Save": string; - "Sync on Startup": string; - "Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.": string; - "Synchronising files": string; - Syncing: string; - "Target patterns": string; - "Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned.": string; - "The delay for consecutive on-demand fetches": string; - "The following accepted nodes are missing its node information:\n- ${missingNodes}\n\nThis indicates that they have not been connected for some time or have been left on an older version.\nIt is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.": string; - "The Hash algorithm for chunk IDs": string; - "The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks.": string; - "The maximum number of chunks that can be incubated within the document. Chunks exceeding this number will immediately graduate to independent chunks.": string; - "The maximum total size of chunks that can be incubated within the document. Chunks exceeding this size will immediately graduate to independent chunks.": string; - "The minimum interval for automatic synchronisation on event.": string; - "This feature enables direct synchronisation between devices. No server is required, but both devices must be online at the same time for synchronisation to occur, and some features may be limited. Internet connection is only required to signalling (detecting peers) and not for data transfer.": string; - "This is an advanced option for users who do not have a URI or who wish to configure detailed settings.": string; - "This is the most suitable synchronisation method for the design. All functions are available. You must have set up a CouchDB instance.": string; - "This passphrase will not be copied to another device. It will be set to `Default` until you configure it again.": string; - "This will recreate chunks for all files. If there were missing chunks, this may fix the errors.": string; - "TweakMismatchResolve.Action.Dismiss": string; - "TweakMismatchResolve.Action.UseConfigured": string; - "TweakMismatchResolve.Action.UseMine": string; - "TweakMismatchResolve.Action.UseMineAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseMineWithRebuild": string; - "TweakMismatchResolve.Action.UseRemote": string; - "TweakMismatchResolve.Action.UseRemoteAcceptIncompatible": string; - "TweakMismatchResolve.Action.UseRemoteWithRebuild": string; - "TweakMismatchResolve.Message.Main": string; - "TweakMismatchResolve.Message.MainTweakResolving": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended": string; - "TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended": string; - "TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired": string; - "TweakMismatchResolve.Table": string; - "TweakMismatchResolve.Table.Row": string; - "TweakMismatchResolve.Title": string; - "TweakMismatchResolve.Title.TweakResolving": string; - "TweakMismatchResolve.Title.UseRemoteConfig": string; - "Ui.Common.Signal.Caution": string; - "Ui.Common.Signal.Danger": string; - "Ui.Common.Signal.Notice": string; - "Ui.Common.Signal.Warning": string; - "Ui.Settings.Advanced.LocalDatabaseTweak": string; - "Ui.Settings.Advanced.MemoryCache": string; - "Ui.Settings.Advanced.TransferTweak": string; - "Ui.Settings.Common.Analyse": string; - "Ui.Settings.Common.Back": string; - "Ui.Settings.Common.Check": string; - "Ui.Settings.Common.Configure": string; - "Ui.Settings.Common.Continue": string; - "Ui.Settings.Common.Delete": string; - "Ui.Settings.Common.Fetch": string; - "Ui.Settings.Common.Lock": string; - "Ui.Settings.Common.Merge": string; - "Ui.Settings.Common.Open": string; - "Ui.Settings.Common.Overwrite": string; - "Ui.Settings.Common.Perform": string; - "Ui.Settings.Common.ResetAll": string; - "Ui.Settings.Common.ResolveAll": string; - "Ui.Settings.Common.Scan": string; - "Ui.Settings.Common.Send": string; - "Ui.Settings.Common.Use": string; - "Ui.Settings.Common.VerifyAll": string; - "Ui.Settings.CustomizationSync.OpenDesc": string; - "Ui.Settings.CustomizationSync.Panel": string; - "Ui.Settings.CustomizationSync.WarnChangeDeviceName": string; - "Ui.Settings.CustomizationSync.WarnSetDeviceName": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsage": string; - "Ui.Settings.Hatch.AnalyseDatabaseUsageDesc": string; - "Ui.Settings.Hatch.BackToNonConfigured": string; - "Ui.Settings.Hatch.ConvertNonObfuscated": string; - "Ui.Settings.Hatch.ConvertNonObfuscatedDesc": string; - "Ui.Settings.Hatch.CopyIssueReport": string; - "Ui.Settings.Hatch.DatabaseLabel": string; - "Ui.Settings.Hatch.DatabaseToStorage": string; - "Ui.Settings.Hatch.DeleteCustomizationSyncData": string; - "Ui.Settings.Hatch.GeneratedReport": string; - "Ui.Settings.Hatch.Missing": string; - "Ui.Settings.Hatch.ModifiedSize": string; - "Ui.Settings.Hatch.ModifiedSizeActual": string; - "Ui.Settings.Hatch.PrepareIssueReport": string; - "Ui.Settings.Hatch.RecoveryAndRepair": string; - "Ui.Settings.Hatch.RecreateAll": string; - "Ui.Settings.Hatch.RecreateMissingChunks": string; - "Ui.Settings.Hatch.RecreateMissingChunksDesc": string; - "Ui.Settings.Hatch.ResetPanel": string; - "Ui.Settings.Hatch.ResetRemoteUsage": string; - "Ui.Settings.Hatch.ResetRemoteUsageDesc": string; - "Ui.Settings.Hatch.ResolveAllConflictedFiles": string; - "Ui.Settings.Hatch.ResolveAllConflictedFilesDesc": string; - "Ui.Settings.Hatch.RunDoctor": string; - "Ui.Settings.Hatch.ScanBrokenFiles": string; - "Ui.Settings.Hatch.ScramSwitches": string; - "Ui.Settings.Hatch.ShowHistory": string; - "Ui.Settings.Hatch.StorageLabel": string; - "Ui.Settings.Hatch.StorageToDatabase": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFiles": string; - "Ui.Settings.Hatch.VerifyAndRepairAllFilesDesc": string; - "Ui.Settings.Maintenance.Cleanup": string; - "Ui.Settings.Maintenance.CleanupDesc": string; - "Ui.Settings.Maintenance.DeleteLocalDatabase": string; - "Ui.Settings.Maintenance.EmergencyRestart": string; - "Ui.Settings.Maintenance.EmergencyRestartDesc": string; - "Ui.Settings.Maintenance.FreshStartWipe": string; - "Ui.Settings.Maintenance.FreshStartWipeDesc": string; - "Ui.Settings.Maintenance.GarbageCollection": string; - "Ui.Settings.Maintenance.GarbageCollectionAction": string; - "Ui.Settings.Maintenance.GarbageCollectionDesc": string; - "Ui.Settings.Maintenance.LockServer": string; - "Ui.Settings.Maintenance.LockServerDesc": string; - "Ui.Settings.Maintenance.OverwriteRemote": string; - "Ui.Settings.Maintenance.OverwriteRemoteDesc": string; - "Ui.Settings.Maintenance.OverwriteServerData": string; - "Ui.Settings.Maintenance.OverwriteServerDataDesc": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounter": string; - "Ui.Settings.Maintenance.PurgeAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.RebuildingOperations": string; - "Ui.Settings.Maintenance.Resend": string; - "Ui.Settings.Maintenance.ResendDesc": string; - "Ui.Settings.Maintenance.Reset": string; - "Ui.Settings.Maintenance.ResetAllJournalCounter": string; - "Ui.Settings.Maintenance.ResetAllJournalCounterDesc": string; - "Ui.Settings.Maintenance.ResetJournalReceived": string; - "Ui.Settings.Maintenance.ResetJournalReceivedDesc": string; - "Ui.Settings.Maintenance.ResetJournalSent": string; - "Ui.Settings.Maintenance.ResetJournalSentDesc": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfo": string; - "Ui.Settings.Maintenance.ResetLocalSyncInfoDesc": string; - "Ui.Settings.Maintenance.ResetReceived": string; - "Ui.Settings.Maintenance.ResetSentHistory": string; - "Ui.Settings.Maintenance.ResetThisDevice": string; - "Ui.Settings.Maintenance.ScheduleAndRestart": string; - "Ui.Settings.Maintenance.Scram": string; - "Ui.Settings.Maintenance.SendChunks": string; - "Ui.Settings.Maintenance.Syncing": string; - "Ui.Settings.Maintenance.WarningLockedReadyAction": string; - "Ui.Settings.Maintenance.WarningLockedReadyText": string; - "Ui.Settings.Maintenance.WarningLockedResolveAction": string; - "Ui.Settings.Maintenance.WarningLockedResolveText": string; - "Ui.Settings.Maintenance.WriteRedFlagAndRestart": string; - "Ui.Settings.Patches.CompatibilityConflict": string; - "Ui.Settings.Patches.CompatibilityDatabase": string; - "Ui.Settings.Patches.CompatibilityInternalApi": string; - "Ui.Settings.Patches.CompatibilityMetadata": string; - "Ui.Settings.Patches.CompatibilityRemote": string; - "Ui.Settings.Patches.CompatibilityTrouble": string; - "Ui.Settings.Patches.CurrentAdapter": string; - "Ui.Settings.Patches.DatabaseAdapter": string; - "Ui.Settings.Patches.DatabaseAdapterDesc": string; - "Ui.Settings.Patches.EdgeCaseBehaviour": string; - "Ui.Settings.Patches.EdgeCaseDatabase": string; - "Ui.Settings.Patches.EdgeCaseProcessing": string; - "Ui.Settings.Patches.IndexedDbWarning": string; - "Ui.Settings.Patches.MigratingToIdb": string; - "Ui.Settings.Patches.MigratingToIndexedDb": string; - "Ui.Settings.Patches.MigrationIdbCompleted": string; - "Ui.Settings.Patches.MigrationIdbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationIndexedDbCompleted": string; - "Ui.Settings.Patches.MigrationIndexedDbCompletedFollowUp": string; - "Ui.Settings.Patches.MigrationWarning": string; - "Ui.Settings.Patches.OperationToIdb": string; - "Ui.Settings.Patches.OperationToIndexedDb": string; - "Ui.Settings.Patches.Remediation": string; - "Ui.Settings.Patches.RemediationChanged": string; - "Ui.Settings.Patches.RemediationNoLimit": string; - "Ui.Settings.Patches.RemediationRestarting": string; - "Ui.Settings.Patches.RemediationRestartLater": string; - "Ui.Settings.Patches.RemediationRestartMessage": string; - "Ui.Settings.Patches.RemediationRestartNow": string; - "Ui.Settings.Patches.RemediationSuffixChanged": string; - "Ui.Settings.Patches.RemediationWithValue": string; - "Ui.Settings.Patches.RemoteDatabaseSunset": string; - "Ui.Settings.Patches.SwitchToIDB": string; - "Ui.Settings.Patches.SwitchToIndexedDb": string; - "Ui.Settings.PowerUsers.ConfigurationEncryption": string; - "Ui.Settings.PowerUsers.ConnectionTweak": string; - "Ui.Settings.PowerUsers.ConnectionTweakDesc": string; - "Ui.Settings.PowerUsers.Default": string; - "Ui.Settings.PowerUsers.Developer": string; - "Ui.Settings.PowerUsers.EncryptSensitiveConfig": string; - "Ui.Settings.PowerUsers.PromptPassphraseEveryLaunch": string; - "Ui.Settings.PowerUsers.UseCustomPassphrase": string; - "Ui.Settings.Remote.Activate": string; - "Ui.Settings.Remote.ActiveSuffix": string; - "Ui.Settings.Remote.AddConnection": string; - "Ui.Settings.Remote.AddRemoteDefaultName": string; - "Ui.Settings.Remote.ConfigureAndChangeRemote": string; - "Ui.Settings.Remote.ConfigureE2EE": string; - "Ui.Settings.Remote.ConfigureRemote": string; - "Ui.Settings.Remote.DeleteRemoteConfirm": string; - "Ui.Settings.Remote.DeleteRemoteTitle": string; - "Ui.Settings.Remote.DisplayName": string; - "Ui.Settings.Remote.DuplicateRemote": string; - "Ui.Settings.Remote.DuplicateRemoteSuffix": string; - "Ui.Settings.Remote.E2EEConfiguration": string; - "Ui.Settings.Remote.Export": string; - "Ui.Settings.Remote.FetchRemoteSettings": string; - "Ui.Settings.Remote.ImportConnection": string; - "Ui.Settings.Remote.ImportConnectionPrompt": string; - "Ui.Settings.Remote.ImportedCouchDb": string; - "Ui.Settings.Remote.ImportedRemote": string; - "Ui.Settings.Remote.MoreActions": string; - "Ui.Settings.Remote.PeerToPeerPanel": string; - "Ui.Settings.Remote.RemoteConfigurationPrefix": string; - "Ui.Settings.Remote.RemoteDatabases": string; - "Ui.Settings.Remote.RemoteName": string; - "Ui.Settings.Remote.RemoteNameCouchDb": string; - "Ui.Settings.Remote.RemoteNameP2P": string; - "Ui.Settings.Remote.RemoteNameS3": string; - "Ui.Settings.Remote.Rename": string; - "Ui.Settings.Selector.AddDefaultPatterns": string; - "Ui.Settings.Selector.CrossPlatform": string; - "Ui.Settings.Selector.Default": string; - "Ui.Settings.Selector.HiddenFiles": string; - "Ui.Settings.Selector.IgnorePatterns": string; - "Ui.Settings.Selector.NonSynchronisingFiles": string; - "Ui.Settings.Selector.NonSynchronisingFilesDesc": string; - "Ui.Settings.Selector.NormalFiles": string; - "Ui.Settings.Selector.OverwritePatterns": string; - "Ui.Settings.Selector.OverwritePatternsDesc": string; - "Ui.Settings.Selector.SynchronisingFiles": string; - "Ui.Settings.Selector.SynchronisingFilesDesc": string; - "Ui.Settings.Selector.TargetPatterns": string; - "Ui.Settings.Selector.TargetPatternsDesc": string; - "Ui.Settings.Setup.RerunWizardButton": string; - "Ui.Settings.Setup.RerunWizardDesc": string; - "Ui.Settings.Setup.RerunWizardName": string; - "Ui.Settings.SyncSettings.Fetch": string; - "Ui.Settings.SyncSettings.Merge": string; - "Ui.Settings.SyncSettings.Overwrite": string; - "Ui.SetupWizard.Common.Back": string; - "Ui.SetupWizard.Common.Cancel": string; - "Ui.SetupWizard.Common.ProceedSelectOption": string; - "Ui.SetupWizard.Intro.ExistingOption": string; - "Ui.SetupWizard.Intro.ExistingOptionDesc": string; - "Ui.SetupWizard.Intro.Guidance": string; - "Ui.SetupWizard.Intro.NewOption": string; - "Ui.SetupWizard.Intro.NewOptionDesc": string; - "Ui.SetupWizard.Intro.ProceedExisting": string; - "Ui.SetupWizard.Intro.ProceedNew": string; - "Ui.SetupWizard.Intro.Question": string; - "Ui.SetupWizard.Intro.Title": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOption": string; - "Ui.SetupWizard.OutroAskUserMode.CompatibleOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOption": string; - "Ui.SetupWizard.OutroAskUserMode.ExistingOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.Guidance": string; - "Ui.SetupWizard.OutroAskUserMode.NewOption": string; - "Ui.SetupWizard.OutroAskUserMode.NewOptionDesc": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedApplySettings": string; - "Ui.SetupWizard.OutroAskUserMode.ProceedNext": string; - "Ui.SetupWizard.OutroAskUserMode.Question": string; - "Ui.SetupWizard.OutroAskUserMode.Title": string; - "Ui.SetupWizard.OutroNewUser.GuidancePrimary": string; - "Ui.SetupWizard.OutroNewUser.GuidanceWarning": string; - "Ui.SetupWizard.OutroNewUser.Important": string; - "Ui.SetupWizard.OutroNewUser.Proceed": string; - "Ui.SetupWizard.OutroNewUser.Question": string; - "Ui.SetupWizard.OutroNewUser.Title": string; - "Ui.SetupWizard.SelectExisting.Guidance": string; - "Ui.SetupWizard.SelectExisting.ManualOption": string; - "Ui.SetupWizard.SelectExisting.ManualOptionDesc": string; - "Ui.SetupWizard.SelectExisting.ProceedManual": string; - "Ui.SetupWizard.SelectExisting.ProceedQr": string; - "Ui.SetupWizard.SelectExisting.ProceedSetupUri": string; - "Ui.SetupWizard.SelectExisting.QrOption": string; - "Ui.SetupWizard.SelectExisting.QrOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Question": string; - "Ui.SetupWizard.SelectExisting.SetupUriOption": string; - "Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectExisting.Title": string; - "Ui.SetupWizard.SelectNew.Guidance": string; - "Ui.SetupWizard.SelectNew.ManualOption": string; - "Ui.SetupWizard.SelectNew.ManualOptionDesc": string; - "Ui.SetupWizard.SelectNew.ProceedManual": string; - "Ui.SetupWizard.SelectNew.ProceedSetupUri": string; - "Ui.SetupWizard.SelectNew.Question": string; - "Ui.SetupWizard.SelectNew.SetupUriOption": string; - "Ui.SetupWizard.SelectNew.SetupUriOptionDesc": string; - "Ui.SetupWizard.SelectNew.Title": string; - "Ui.SetupWizard.SetupRemote.BucketOption": string; - "Ui.SetupWizard.SetupRemote.BucketOptionDesc": string; - "Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": string; - "Ui.SetupWizard.SetupRemote.Guidance": string; - "Ui.SetupWizard.SetupRemote.P2POption": string; - "Ui.SetupWizard.SetupRemote.P2POptionDesc": string; - "Ui.SetupWizard.SetupRemote.ProceedBucket": string; - "Ui.SetupWizard.SetupRemote.ProceedCouchDb": string; - "Ui.SetupWizard.SetupRemote.ProceedP2P": string; - "Ui.SetupWizard.SetupRemote.Title": string; - "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": string; - "Use a custom passphrase": string; - "Use a Setup URI (Recommended)": string; - "Use Custom HTTP Handler": string; - "Use dynamic iteration count": string; - "Use Segmented-splitter": string; - "Use splitting-limit-capped chunk splitter": string; - "Use the trash bin": string; - "Use timeouts instead of heartbeats": string; - username: string; - Username: string; - "Verbose Log": string; - "Verify all": string; - "Verify and repair all files": string; - "Warning! This will have a serious impact on performance. And the logs will not be synchronised under the default name. Please be careful with logs; they often contain your confidential information.": string; - "We cannot change the device name while this feature is enabled. Please disable this feature to change the device name.": string; - "We will now guide you through a few questions to simplify the synchronisation setup.": string; - "We will now proceed with the server configuration.": string; - "Welcome to Self-hosted LiveSync": string; - "When you save a file in the editor, start a sync automatically": string; - "Write credentials in the file": string; - "Write logs into the file": string; - "xxhash32 (Fast but less collision resistance)": string; - "xxhash64 (Fastest)": string; - "Yes, I want to add this device to my existing synchronisation": string; - "Yes, I want to set up a new synchronisation": string; - "You are adding this device to an existing synchronisation setup.": string; - }; -}; diff --git a/_types/src/lib/src/common/models/auth.type.d.ts b/_types/src/lib/src/common/models/auth.type.d.ts deleted file mode 100644 index ebc33799..00000000 --- a/_types/src/lib/src/common/models/auth.type.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type CouchDBCredentials = BasicCredentials | JWTCredentials; -export type JWTAlgorithm = "HS256" | "HS512" | "ES256" | "ES512" | ""; -export type Credential = { - username: string; - password: string; -}; -export type BasicCredentials = { - username: string; - password: string; - type: "basic"; -}; -export type JWTCredentials = { - jwtAlgorithm: JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - type: "jwt"; -}; -export interface JWTHeader { - alg: string; - typ: string; - kid?: string; -} -export interface JWTPayload { - sub: string; - exp: number; - iss?: string; - iat: number; - [key: string]: unknown; -} -export interface JWTParams { - header: JWTHeader; - payload: JWTPayload; - credentials: JWTCredentials; -} -export interface PreparedJWT { - header: JWTHeader; - payload: JWTPayload; - token: string; -} diff --git a/_types/src/lib/src/common/models/db.const.d.ts b/_types/src/lib/src/common/models/db.const.d.ts deleted file mode 100644 index 58f5801f..00000000 --- a/_types/src/lib/src/common/models/db.const.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID } from "./db.type"; -export declare const VERSIONING_DOCID: DocumentID; -export declare const MILESTONE_DOCID: DocumentID; -export declare const NODEINFO_DOCID: DocumentID; -export declare const SYNCINFO_ID: DocumentID; -export declare const EntryTypes: { - readonly NOTE_LEGACY: "notes"; - readonly NOTE_BINARY: "newnote"; - readonly NOTE_PLAIN: "plain"; - readonly INTERNAL_FILE: "internalfile"; - readonly CHUNK: "leaf"; - readonly CHUNK_PACK: "chunkpack"; - readonly VERSION_INFO: "versioninfo"; - readonly SYNC_INFO: "syncinfo"; - readonly SYNC_PARAMETERS: "sync-parameters"; - readonly MILESTONE_INFO: "milestoneinfo"; - readonly NODE_INFO: "nodeinfo"; -}; -export declare const NoteTypes: ("notes" | "newnote" | "plain")[]; -export declare const ChunkTypes: ("leaf" | "chunkpack")[]; diff --git a/_types/src/lib/src/common/models/db.definition.d.ts b/_types/src/lib/src/common/models/db.definition.d.ts deleted file mode 100644 index b916d2e8..00000000 --- a/_types/src/lib/src/common/models/db.definition.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { MILESTONE_DOCID, NODEINFO_DOCID } from "./db.const"; -import type { AnyEntry, ChunkVersionRange, DatabaseEntry, EntryChunkPack, EntryLeaf, EntryTypes, EntryVersionInfo, InternalFileEntry, LoadedEntry, MetaEntry, NewEntry, NoteEntry, PlainEntry } from "./db.type"; -import type { TweakValues } from "./tweak.definition"; -export type NodeKey = string; -export interface DeviceInfo { - /** - * Name of the device (Initially from deviceAndVaultName setting, configurable). - */ - device_name: string; - /** - * Vault name (From vaultName setting). - */ - vault_name: string; - /** - * Obsidian App version of the device. - */ - app_version: string; - /** - * Plugin version of the device. - */ - plugin_version: string; - progress: string; -} -export interface NodeData extends DeviceInfo { - /** - * Epoch time in milliseconds when the device last connected. - */ - last_connected: number; -} -export interface EntryMilestoneInfo extends DatabaseEntry { - _id: typeof MILESTONE_DOCID; - type: EntryTypes["MILESTONE_INFO"]; - created: number; - accepted_nodes: string[]; - node_info: { - [key: NodeKey]: NodeData; - }; - locked: boolean; - cleaned?: boolean; - node_chunk_info: { - [key: NodeKey]: ChunkVersionRange; - }; - tweak_values: { - [key: NodeKey]: TweakValues; - }; -} -export interface EntryNodeInfo extends DatabaseEntry { - _id: typeof NODEINFO_DOCID; - type: EntryTypes["NODE_INFO"]; - nodeid: string; - v20220607?: boolean; -} -export type EntryBody = NoteEntry | NewEntry | PlainEntry | InternalFileEntry; -export type EntryDoc = EntryBody | LoadedEntry | EntryLeaf | EntryVersionInfo | EntryMilestoneInfo | EntryNodeInfo | EntryChunkPack; -export type EntryDocResponse = EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta; -export declare function isMetaEntry(entry: AnyEntry): entry is MetaEntry; diff --git a/_types/src/lib/src/common/models/db.type.d.ts b/_types/src/lib/src/common/models/db.type.d.ts deleted file mode 100644 index 801d979f..00000000 --- a/_types/src/lib/src/common/models/db.type.d.ts +++ /dev/null @@ -1,144 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { TaggedType } from "octagonal-wheels/common/types"; -import type { EntryTypes, SYNCINFO_ID } from "./db.const"; -export type FilePath = TaggedType; -export type FilePathWithPrefixLC = TaggedType; -export type FilePathWithPrefix = TaggedType | FilePath | FilePathWithPrefixLC; -export type DocumentID = TaggedType; -export type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes]; -export type EntryTypes = typeof EntryTypes; -export type EntryTypeNotes = EntryTypes["NOTE_BINARY"] | EntryTypes["NOTE_PLAIN"]; -export type EntryTypeNotesWithLegacy = EntryTypeNotes | EntryTypes["NOTE_LEGACY"]; -/** - * Represents an entry in the database. - */ -export interface DatabaseEntry { - /** - * The ID of the document. - */ - _id: DocumentID; - /** - * The revision of the document. - */ - _rev?: string; - /** - * Deleted flag. - */ - _deleted?: boolean; - /** - * Conflicts (if exists). - */ - _conflicts?: string[]; -} -/** - * Represents the base structure for an entry that represents a file. - */ -export type EntryBase = { - /** - * The creation time of the file. - */ - ctime: number; - /** - * The modification time of the file. - */ - mtime: number; - /** - * The size of the file. - */ - size: number; - /** - * Deleted flag. - */ - deleted?: boolean; -}; -export type EdenChunk = { - data: string; - epoch: number; -}; -export type EntryWithEden = { - eden: Record; -}; -export type NoteEntry = DatabaseEntry & EntryBase & EntryWithEden & { - /** - * The path of the file. - */ - path: FilePathWithPrefix; - /** - * Contents of the file. - */ - data: string | string[]; - /** - * The type of the entry. - */ - type: EntryTypes["NOTE_LEGACY"]; -}; -export type NewEntry = DatabaseEntry & EntryBase & EntryWithEden & { - /** - * The path of the file. - */ - path: FilePathWithPrefix; - /** - * Chunk IDs indicating the contents of the file. - */ - children: string[]; - /** - * The type of the entry. - */ - type: EntryTypes["NOTE_BINARY"]; -}; -export type PlainEntry = DatabaseEntry & EntryBase & EntryWithEden & { - /** - * The path of the file. - */ - path: FilePathWithPrefix; - /** - * Chunk IDs indicating the contents of the file. - */ - children: string[]; - /** - * The type of the entry. - */ - type: EntryTypes["NOTE_PLAIN"]; -}; -export type InternalFileEntry = DatabaseEntry & NewEntry & EntryBase & { - deleted?: boolean; -}; -export type AnyEntry = NoteEntry | NewEntry | PlainEntry | InternalFileEntry; -export type LoadedEntry = AnyEntry & { - data: string | string[]; - datatype: EntryTypeNotes; -}; -export type SavingEntry = AnyEntry & { - data: Blob; - datatype: EntryTypeNotes; -}; -export type MetaEntry = AnyEntry & { - children: string[]; -}; -export type EntryLeaf = DatabaseEntry & { - type: EntryTypes["CHUNK"]; - data: string; - isCorrupted?: boolean; -}; -export type EntryChunkPack = DatabaseEntry & { - type: EntryTypes["CHUNK_PACK"]; - data: string; -}; -export interface EntryVersionInfo extends DatabaseEntry { - type: EntryTypes["VERSION_INFO"]; - version: number; -} -export interface EntryHasPath { - path: FilePathWithPrefix | FilePath; -} -export interface ChunkVersionRange { - min: number; - max: number; - current: number; -} -export interface SyncInfo extends DatabaseEntry { - _id: typeof SYNCINFO_ID; - type: EntryTypes["SYNC_INFO"]; - data: string; -} diff --git a/_types/src/lib/src/common/models/diff.definition.d.ts b/_types/src/lib/src/common/models/diff.definition.d.ts deleted file mode 100644 index 8f5e797c..00000000 --- a/_types/src/lib/src/common/models/diff.definition.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AUTO_MERGED, CANCELLED, MISSING_OR_ERROR, NOT_CONFLICTED } from "./shared.const.symbols"; -export type diff_result_leaf = { - rev: string; - data: string; - ctime: number; - mtime: number; - deleted?: boolean; -}; -export type dmp_result = Array<[number, string]>; -export type diff_result = { - left: diff_result_leaf; - right: diff_result_leaf; - diff: dmp_result; -}; -export type DIFF_CHECK_RESULT_AUTO = typeof CANCELLED | typeof AUTO_MERGED | typeof NOT_CONFLICTED | typeof MISSING_OR_ERROR; -export type diff_check_result = DIFF_CHECK_RESULT_AUTO | diff_result; diff --git a/_types/src/lib/src/common/models/fileaccess.const.d.ts b/_types/src/lib/src/common/models/fileaccess.const.d.ts deleted file mode 100644 index ea346e21..00000000 --- a/_types/src/lib/src/common/models/fileaccess.const.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const CHeader = "h:"; -export declare const PSCHeader = "ps:"; -export declare const PSCHeaderEnd = "ps;"; -export declare const ICHeader = "i:"; -export declare const ICHeaderEnd = "i;"; -export declare const ICHeaderLength: number; -export declare const ICXHeader = "ix:"; diff --git a/_types/src/lib/src/common/models/fileaccess.type.d.ts b/_types/src/lib/src/common/models/fileaccess.type.d.ts deleted file mode 100644 index 29c97d96..00000000 --- a/_types/src/lib/src/common/models/fileaccess.type.d.ts +++ /dev/null @@ -1,74 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix } from "./db.type"; -export type UXStat = { - size: number; - mtime: number; - ctime: number; - type: "file" | "folder"; -}; -export type UXFileInfoStub = { - name: string; - path: FilePath | FilePathWithPrefix; - stat: UXStat; - deleted?: boolean; - isFolder?: false; - isInternal?: boolean; -}; -export type UXFileInfo = UXFileInfoStub & { - body: Blob; -}; -export type UXAbstractInfoStub = UXFileInfoStub | UXFolderInfo; -export type UXInternalFileInfoStub = { - name: string; - path: FilePath | FilePathWithPrefix; - deleted?: boolean; - isFolder?: false; - isInternal: true; - stat: undefined; -}; -export type UXFolderInfo = { - name: string; - path: FilePath | FilePathWithPrefix; - deleted?: boolean; - isFolder: true; - children: UXFileInfoStub[]; - parent: FilePath | FilePathWithPrefix | undefined; -}; -export type UXDataWriteOptions = { - /** - * Time of creation, represented as a unix timestamp, in milliseconds. - * Omit this if you want to keep the default behaviour. - * @public - * */ - ctime?: number; - /** - * Time of last modification, represented as a unix timestamp, in milliseconds. - * Omit this if you want to keep the default behaviour. - * @public - */ - mtime?: number; -}; -export type CacheData = string | ArrayBuffer; -export type FileEventType = "CREATE" | "DELETE" | "CHANGED" | "RENAME" | "INTERNAL"; -export type FileEventArgs = { - file: UXFileInfoStub | UXInternalFileInfoStub; - cache?: CacheData; - oldPath?: string; - ctx?: unknown; -}; -export type FileEventItem = { - type: FileEventType; - args: FileEventArgs; - key: string; - skipBatchWait?: boolean; - cancelled?: boolean; - batched?: boolean; -}; -export interface FileWithFileStat extends Omit { - path: FilePath; -} -export interface FileWithStatAsProp { - path: FilePath; - stat: Omit; -} diff --git a/_types/src/lib/src/common/models/redflag.const.d.ts b/_types/src/lib/src/common/models/redflag.const.d.ts deleted file mode 100644 index 3453f971..00000000 --- a/_types/src/lib/src/common/models/redflag.const.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "./db.type"; -export declare const PREFIXMD_LOGFILE = "livesync_log_"; -export declare const PREFIXMD_LOGFILE_UC = "LIVESYNC_LOG_"; -export declare const FlagFilesOriginal: { - readonly SUSPEND_ALL: FilePath; - readonly REBUILD_ALL: FilePath; - readonly FETCH_ALL: FilePath; -}; -export declare const FlagFilesHumanReadable: { - readonly REBUILD_ALL: FilePath; - readonly FETCH_ALL: FilePath; -}; -/** - * @deprecated Use `FlagFilesOriginal.SUSPEND_ALL` instead. - */ -export declare const FLAGMD_REDFLAG: FilePath; -/** - * @deprecated Use `FlagFilesHumanReadable.REBUILD_ALL` instead. - */ -export declare const FLAGMD_REDFLAG2: FilePath; -/** - * @deprecated Use `FlagFilesHumanReadable.FETCH_ALL` instead. - */ -export declare const FLAGMD_REDFLAG2_HR: FilePath; -/** - * @deprecated Use `FlagFilesOriginal.FETCH_ALL` instead. - */ -export declare const FLAGMD_REDFLAG3: FilePath; -/** - * @deprecated Use `FlagFilesHumanReadable.FETCH_ALL` instead. - */ -export declare const FLAGMD_REDFLAG3_HR: FilePath; diff --git a/_types/src/lib/src/common/models/setting.const.d.ts b/_types/src/lib/src/common/models/setting.const.d.ts deleted file mode 100644 index b21cd139..00000000 --- a/_types/src/lib/src/common/models/setting.const.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const SETTING_VERSION_INITIAL = 0; -export declare const SETTING_VERSION_SUPPORT_CASE_INSENSITIVE = 10; -export declare const CURRENT_SETTING_VERSION = 10; -export declare const RemoteTypes: { - readonly REMOTE_COUCHDB: ""; - readonly REMOTE_MINIO: "MINIO"; - readonly REMOTE_P2P: "ONLY_P2P"; -}; -export declare const REMOTE_COUCHDB: ""; -export declare const REMOTE_MINIO: "MINIO"; -export declare const REMOTE_P2P: "ONLY_P2P"; -export declare const E2EEAlgorithmNames: { - readonly "": "V1: Legacy"; - readonly v2: "V2: AES-256-GCM With HKDF"; - readonly forceV1: "Force-V1: Force Legacy (Not recommended)"; -}; -export declare const E2EEAlgorithms: { - readonly V1: ""; - readonly V2: "v2"; - readonly ForceV1: "forceV1"; -}; -export declare const HashAlgorithms: { - readonly XXHASH32: "xxhash32"; - readonly XXHASH64: "xxhash64"; - readonly MIXED_PUREJS: "mixed-purejs"; - readonly SHA1: "sha1"; - readonly LEGACY: ""; -}; -export declare const ChunkAlgorithmNames: { - readonly v1: "V1: Legacy"; - readonly v2: "V2: Simple (Default)"; - readonly "v2-segmenter": "V2.5: Lexical chunks"; - readonly "v3-rabin-karp": "V3: Fine deduplication"; -}; -export declare const ChunkAlgorithms: { - readonly V1: "v1"; - readonly V2: "v2"; - readonly V2Segmenter: "v2-segmenter"; - readonly RabinKarp: "v3-rabin-karp"; -}; -export declare const MODE_SELECTIVE = 0; -export declare const MODE_AUTOMATIC = 1; -export declare const MODE_PAUSED = 2; -export declare const MODE_SHINY = 3; -export declare const NetworkWarningStyles: { - readonly BANNER: ""; - readonly ICON: "icon"; - readonly HIDDEN: "hidden"; -}; diff --git a/_types/src/lib/src/common/models/setting.const.defaults.d.ts b/_types/src/lib/src/common/models/setting.const.defaults.d.ts deleted file mode 100644 index d51b78de..00000000 --- a/_types/src/lib/src/common/models/setting.const.defaults.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings, type P2PSyncSetting } from "./setting.type"; -export declare const P2P_DEFAULT_SETTINGS: P2PSyncSetting; -export declare const DEFAULT_SETTINGS: ObsidianLiveSyncSettings; diff --git a/_types/src/lib/src/common/models/setting.const.preferred.d.ts b/_types/src/lib/src/common/models/setting.const.preferred.d.ts deleted file mode 100644 index bfeb921e..00000000 --- a/_types/src/lib/src/common/models/setting.const.preferred.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const PREFERRED_BASE: Partial; -export declare const PREFERRED_SETTING_CLOUDANT: Partial; -export declare const PREFERRED_SETTING_SELF_HOSTED: Partial; -export declare const PREFERRED_JOURNAL_SYNC: Partial; diff --git a/_types/src/lib/src/common/models/setting.const.qr.d.ts b/_types/src/lib/src/common/models/setting.const.qr.d.ts deleted file mode 100644 index 1cccc1e3..00000000 --- a/_types/src/lib/src/common/models/setting.const.qr.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const KeyIndexOfSettings: Record; diff --git a/_types/src/lib/src/common/models/setting.type.d.ts b/_types/src/lib/src/common/models/setting.type.d.ts deleted file mode 100644 index 59c5a925..00000000 --- a/_types/src/lib/src/common/models/setting.type.d.ts +++ /dev/null @@ -1,902 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ChunkAlgorithms, E2EEAlgorithms, HashAlgorithms, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY, RemoteTypes } from "./setting.const"; -import type { I18N_LANGS } from "@lib/common/rosetta"; -import type { CustomRegExpSourceList } from "./shared.type.util"; -import type { JWTAlgorithm } from "./auth.type"; -/** - * Represents the connection details required to connect to a CouchDB instance. - */ -export interface CouchDBConnection { - /** - * The URI of the CouchDB instance. - */ - couchDB_URI: string; - /** - * The username to use when connecting to the CouchDB instance. - */ - couchDB_USER: string; - /** - * The password to use when connecting to the CouchDB instance. - */ - couchDB_PASSWORD: string; - /** - * The name of the database to use. - */ - couchDB_DBNAME: string; - /** - * e.g. `x-some-header: some-value\n x-some-header2: some-value2` - */ - couchDB_CustomHeaders: string; - useJWT: boolean; - jwtAlgorithm: JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - /** - * Use Request API to avoid `inevitable` CORS problem. - * Seems stable, so promoted to the normal setting. - */ - useRequestAPI: boolean; -} -/** - * Interface representing the settings for periodic replication. - */ -interface PeriodicReplicationSettings { - /** - * Indicates whether periodic replication is enabled. - */ - periodicReplication: boolean; - /** - * The interval, in milliseconds, at which periodic replication occurs. - */ - periodicReplicationInterval: number; -} -export type ConfigPassphraseStore = "" | "LOCALSTORAGE" | "ASK_AT_LAUNCH"; -/** - * Represents the user settings that are encrypted. - */ -interface EncryptedUserSettings { - /** - * The store for the configuration passphrase. - */ - configPassphraseStore: ConfigPassphraseStore; - /** - * The encrypted passphrase used for E2EE. - */ - encryptedPassphrase: string; - /** - * The encrypted connection details for CouchDB. - */ - encryptedCouchDBConnection: string; -} -/** - * Interface representing the settings for different sync invocation methods. - */ -interface SyncMethodSettings { - /** - * Synchronise in Live. This is an exclusive setting against other sync methods. - */ - liveSync: boolean; - /** - * automatically run sync on save. - * File modification will trigger the sync, even if the file is not changed on the editor. - */ - syncOnSave: boolean; - /** - * automatically run sync on starting the plug-in. - */ - syncOnStart: boolean; - /** - * automatically run sync on opening a file. - */ - syncOnFileOpen: boolean; - /** - * automatically run sync on editor save. - * Different from syncOnSave, this is only reacts to the editor save event. - */ - syncOnEditorSave: boolean; - /** - * Desktop only, opt-in. Keep replication running while the window is hidden or minimised, - * instead of suspending it until the window becomes visible again. The trigger is - * document.hidden, not window focus. Applies to the background-capable sync modes (LiveSync - * and Periodic). Ignored on mobile. Default false. - */ - keepReplicationActiveInBackground: boolean; - /** - * The minimum delay between synchronisation operations (in milliseconds). - * If the operation is triggered before this delay, the operation will be delayed until the delay is over, and executed as a single operation. - */ - syncMinimumInterval: number; -} -/** - * Interface representing the settings for file handling. - */ -interface FileHandlingSettings { - /** - * Use trash instead of actually delete. - */ - trashInsteadDelete: boolean; - /** - * Do not delete the folder even if it has got empty. - */ - doNotDeleteFolder: boolean; - /** - * Thinning out the changes and make a single change for the same file. - */ - batchSave: boolean; - batchSaveMinimumDelay: number; - batchSaveMaximumDelay: number; - /** - * Maximum size of the file to be synchronized (in MB). - */ - syncMaxSizeInMB: number; - /** - * Use ignore files. - */ - useIgnoreFiles: boolean; - /** - * Ignore files pattern, i,e, `.gitignore, .obsidianignore` (This should be separated by comma) - */ - ignoreFiles: string; - /** - * Do not prevent write if the size is mismatched. - */ - processSizeMismatchedFiles: boolean; -} -/** - * Interface representing the settings for Hidden File Sync. - */ -interface InternalFileSettings { - /** - * Synchronise internal files. - */ - syncInternalFiles: boolean; - /** - * Scan internal files before replication. - */ - syncInternalFilesBeforeReplication: boolean; - /** - * Interval for scanning internal files (in seconds). - */ - syncInternalFilesInterval: number; - /** - * Ignore patterns for internal files. - * (Comma separated list of regular expressions) - */ - syncInternalFilesIgnorePatterns: CustomRegExpSourceList<",">; - /** - * Limit patterns for internal files. - */ - syncInternalFilesTargetPatterns: CustomRegExpSourceList<",">; - /** - * Enable watch internal file changes (This option uses the unexposed API) - */ - watchInternalFileChanges: boolean; - /** - * Suppress notification of hidden files change. - */ - suppressNotifyHiddenFilesChange: boolean; - /** - * Overwrite instead of merging patterns for internal files. - */ - syncInternalFileOverwritePatterns: CustomRegExpSourceList<",">; -} -export type SYNC_MODE = typeof MODE_SELECTIVE | typeof MODE_AUTOMATIC | typeof MODE_PAUSED | typeof MODE_SHINY; -export interface PluginSyncSettingEntry { - key: string; - mode: SYNC_MODE; - files: string[]; -} -/** - * Interface representing the settings for plugin synchronisation. - */ -interface PluginSyncSettings { - /** - * Indicates whether plugin synchronisation is enabled. - */ - usePluginSync: boolean; - /** - * Indicates whether plugin settings synchronisation is enabled. - */ - usePluginSettings: boolean; - /** - * Indicates whether to show the device's own plugins. - */ - showOwnPlugins: boolean; - /** - * Indicates whether to automatically scan plugins. - */ - autoSweepPlugins: boolean; - /** - * Indicates whether to periodically scan plugins automatically. - */ - autoSweepPluginsPeriodic: boolean; - /** - * Indicates whether to notify when a plugin or setting is updated. - */ - notifyPluginOrSettingUpdated: boolean; - /** - * The name of the device and vault. - * This is used to identify the device and vault among synchronised devices and vaults. - * Hence, this should be unique among devices and vaults. - */ - deviceAndVaultName: string; - /** - * Indicates whether the v2 of plugin synchronisation is enabled. - */ - usePluginSyncV2: boolean; - /** - * Indicates whether additional plugin synchronisation settings are enabled. - * This setting is hidden from the UI. - */ - usePluginEtc: boolean; - /** - * Extended settings for plugin synchronisation. - */ - pluginSyncExtendedSetting: Record; -} -/** - * Interface representing the user interface settings. - */ -interface UISettings { - /** - * Indicates whether verbose logging has been enabled. - */ - showVerboseLog: boolean; - /** - * Indicates whether less information should be shown in the log. - */ - lessInformationInLog: boolean; - /** - * Indicates whether longer status line should be shown inside the editor. - */ - showLongerLogInsideEditor: boolean; - /** - * Indicates whether the status line should be shown on the editor. - */ - showStatusOnEditor: boolean; - /** - * Indicates whether the status line should be shown on the status bar. - */ - showStatusOnStatusbar: boolean; - /** - * Indicates whether only icons instead of status line should be shown on the editor. - */ - showOnlyIconsOnEditor: boolean; - /** - * Hide File warning notice bar. - */ - hideFileWarningNotice: boolean; - /** - * How to display connection error warnings. - * "banner" shows the full banner, "icon" shows only an icon, "hidden" suppresses entirely. - */ - networkWarningStyle: "" | "icon" | "hidden"; - /** - * The language to be used for display. - */ - displayLanguage: I18N_LANGS; -} -/** - * Interface representing the settings for mode of exposing advanced things. - */ -interface ModeSettings { - /** - * Indicates whether the advanced mode is enabled. - */ - useAdvancedMode: boolean; - /** - * Indicates whether the power user mode is enabled. - */ - usePowerUserMode: boolean; - /** - * Indicates whether the edge case mode is enabled. - */ - useEdgeCaseMode: boolean; -} -/** - * Interface representing the settings for debug mode. - */ -interface DebugModeSettings { - /** - * Indicates whether the debug tools of Self-hosted LiveSync are enabled. - */ - enableDebugTools: boolean; - /** - * Indicates whether to write log to the file. - */ - writeLogToTheFile: boolean; -} -/** - * Interface representing additional tweak settings. - */ -interface ExtraTweakSettings { - /** - * The threshold value for notifying about the size of remote storage. - * When the size of the remote storage exceeds this threshold, a notification will be triggered. - */ - notifyThresholdOfRemoteStorageSize: number; -} -/** - * Interface representing the settings for beta tweaks. - */ -interface BetaTweakSettings { - /** - * Indicates whether to disable the WebWorker for generating chunks. - */ - disableWorkerForGeneratingChunks: boolean; - /** - * Indicates whether to process small files in the UI thread. - */ - processSmallFilesInUIThread: boolean; -} -/** - * Interface representing the settings for synchronising settings via file. - */ -interface SettingSyncSettings { - /** - * The file path where the settings is stored. - */ - settingSyncFile: string; - /** - * Indicates whether to write credentials for settings synchronising. - */ - writeCredentialsForSettingSync: boolean; - /** - * Indicates whether to notify all settings synchronising files events. - */ - notifyAllSettingSyncFile: boolean; -} -/** - * Represents settings that are considered obsolete and are not configurable from the UI. - */ -interface ObsoleteSettings { - /** - * Saving delay (in milliseconds). - */ - savingDelay: number; - /** - * Garbage collection delay (in milliseconds). Now, no longer GC is implemented. - */ - gcDelay: number; - /** - * Skip older files on sync. No effect now. - */ - skipOlderFilesOnSync: boolean; - /** - * Use the IndexedDB adapter. Now always true. Should be. - */ - useIndexedDBAdapter: boolean; -} -/** - * Interface representing some data stored in the settings for the plugin. - */ -interface DataOnSettings { - /** - * VersionUp flash message which is shown when some incompatible changes are made during the update. - */ - versionUpFlash: string; - /** - * Setting file version, to migrate the settings. - */ - settingVersion: number; - /** - * Indicates whether the setting of the plug-in is configured once. - */ - isConfigured?: boolean; - /** - * The user-last-read version number. - */ - lastReadUpdates: number; - /** - * The last checked version by the doctor. - */ - doctorProcessedVersion: string; -} -/** - * Interface representing the settings for a safety valve mechanism. - */ -interface SafetyValveSettings { - /** - * Indicates whether file watching should be suspended. - */ - suspendFileWatching: boolean; - /** - * Indicates whether parsing and reflecting of replication results should be suspended. - */ - suspendParseReplicationResult: boolean; - /** - * Indicates whether suspension should be avoided during fetching operations. - */ - doNotSuspendOnFetching: boolean; - /** - * Maximum file modification time applied to reflected file events - */ - maxMTimeForReflectEvents: number; -} -/** - * Represents the settings required to synchronise with a bucket. - */ -export interface BucketSyncSetting { - /** - * The access key to use when connecting to the bucket. - */ - accessKey: string; - /** - * The secret to use when connecting to the bucket. - */ - secretKey: string; - /** - * The name of bucket to use. - */ - bucket: string; - /** - * The region of the bucket. - */ - region: string; - /** - * The endpoint of the bucket. - */ - endpoint: string; - /** - * Indicates whether to use a custom request handler. - * (This is for CORS issue). - */ - useCustomRequestHandler: boolean; - bucketCustomHeaders: string; - /** - * The prefix to use for the bucket (e.g., "my-bucket/", means mostly like a folder). - */ - bucketPrefix: string; - /** - * Indicates whether to force path style access. - */ - forcePathStyle: boolean; -} -export interface LocalDBSettings { - /** - * Indicates whether to use the IndexedDB adapter for the local database. - * @deprecated - */ - useIndexedDBAdapter: boolean; -} -export type RemoteType = (typeof RemoteTypes)[keyof typeof RemoteTypes]; -export declare enum AutoAccepting { - NONE = 0, - ALL = 1 -} -export interface P2PConnectionInfo { - /** - * Indicates whether P2P connection is enabled. - */ - P2P_Enabled: boolean; - /** - * Nostr relay server URL. (Comma separated list) - * This is only for the channelling server to establish for the P2P connection. - * No data is transferred through this server. - */ - P2P_relays: string; - /** - * The room ID for `your devices`. This should be unique among the users. - * (Or, lines will be got mixed up). - */ - P2P_roomID: string; - /** - * The passphrase for your devices. - * It can be empty, but it will help you if you have a duplicate Room ID. - */ - P2P_passphrase: string; - /** - * The Application ID for the P2P connection. - * This is used to identify the application using the P2P network. - * In Self-hosted LiveSync, fixed to "self-hosted-livesync". - */ - P2P_AppID: string; - /** - * Indicates whether to auto-start the P2P connection on launch. - */ - P2P_AutoStart: boolean; - /** - * Indicates whether to automatically broadcast changes to connected peers. - */ - P2P_AutoBroadcast: boolean; - /** - * The name of the device peer (This only for editing-setting purpose, not saved in the actual setting file, due to avoid setting-sync issues). - */ - P2P_DevicePeerName?: string; - /** - * The TURN server URLs for the P2P connection. (Comma separated list) - */ - P2P_turnServers: string; - /** - * The TURN username for the P2P connection. - */ - P2P_turnUsername: string; - /** - * The TURN credential (password, secret, etc...) for the P2P connection. - */ - P2P_turnCredential: string; - /** - * Use Diagnostic Wrapper for RTCPeerConnection to collect statistics. - */ - P2P_useDiagRTC?: boolean; -} -export interface P2PSyncSetting extends P2PConnectionInfo { - P2P_AutoAccepting: AutoAccepting; - P2P_AutoSyncPeers: string; - P2P_AutoWatchPeers: string; - P2P_SyncOnReplication: string; - P2P_RebuildFrom: string; - P2P_AutoAcceptingPeers: string; - P2P_AutoDenyingPeers: string; - P2P_IsHeadless?: boolean; -} -/** - * Interface representing the settings for a remote type. - */ -export interface RemoteTypeSettings { - /** - * The type of the remote. - */ - remoteType: RemoteType; -} -export type E2EEAlgorithm = (typeof E2EEAlgorithms)[keyof typeof E2EEAlgorithms] | ""; -/** - * Represents the settings used for End-to-End encryption. - */ -export interface EncryptionSettings { - /** - * Indicates whether E2EE is enabled. - */ - encrypt: boolean; - /** - * The passphrase used for E2EE. - */ - passphrase: string; - /** - * Indicates whether path obfuscation is used. - * If not, the path will be stored as it is, as the document ID. - */ - usePathObfuscation: boolean; - /** - * The algorithm used for hashing the passphrase. - * This is used for E2EE. - */ - E2EEAlgorithm: E2EEAlgorithm; -} -export type HashAlgorithm = (typeof HashAlgorithms)[keyof typeof HashAlgorithms]; -export type ChunkSplitterVersion = (typeof ChunkAlgorithms)[keyof typeof ChunkAlgorithms] | ""; -/** - * Interface representing the settings for chunk processing. - */ -interface ChunkSettings { - /** - * The algorithm used for hashing chunks. - */ - hashAlg: HashAlgorithm; - /** - * The minimum size of a chunk in chars. - */ - minimumChunkSize: number; - /** - * The custom size of a chunk. - * Note: This value used as a coefficient for the normal chunk size. - */ - customChunkSize: number; - /** - * The threshold for considering a line as long. - * (Not respected in v0.24.x). - */ - longLineThreshold: number; - /** - * Flag indicating whether to use a segmenter for chunking. - * @deprecated use chunkSplitterVersion instead. - */ - useSegmenter: boolean; - /** - * Flag indicating whether to enable version 2 of the chunk splitter. - * @deprecated use chunkSplitterVersion instead. - */ - enableChunkSplitterV2: boolean; - /** - * Flag indicating whether to avoid using a fixed revision for chunks. - */ - doNotUseFixedRevisionForChunks: boolean; - /** - * The version of the chunk splitter to use. - */ - chunkSplitterVersion: ChunkSplitterVersion; -} -/** - * Settings for on-demand chunk fetching. - */ -interface OnDemandChunkSettings { - /** - * Indicates whether chunks should be fetch online. (means replication transfers only metadata). - */ - readChunksOnline: boolean; - /** - * Indicates whether to use only local chunks without fetching online. - */ - useOnlyLocalChunk: boolean; - /** - * The number of concurrent chunk reads allowed when fetching online. - */ - concurrencyOfReadChunksOnline: number; - /** - * The minimum interval (in milliseconds) between consecutive online chunk fetching. - */ - minimumIntervalOfReadChunksOnline: number; -} -/** - * Configuration settings for Eden. - */ -interface EdenSettings { - /** - * Indicates whether Eden is enabled. - */ - useEden: boolean; - /** - * The maximum number of chunks allowed in Eden. - */ - maxChunksInEden: number; - /** - * The maximum total length allowed in Eden. - */ - maxTotalLengthInEden: number; - /** - * The maximum age allowed in Eden. - */ - maxAgeInEden: number; -} -/** - * Interface representing obsolete settings for an remote database. - */ -interface ObsoleteRemoteDBSettings { - /** - * Indicates whether to check the integrity of the data on save. - */ - checkIntegrityOnSave: boolean; - /** - * Indicates whether to use history tracking. - * (Now always true) - */ - useHistory: boolean; - /** - * Indicates whether to disable using API of Obsidian. - * (Now always true: Note: Obsidian cannot handle multiple requests at the same time). - */ - disableRequestURI: boolean; - /** - * Indicates whether to send data in bulk chunks. - */ - sendChunksBulk: boolean; - /** - * The maximum size of the bulk chunks to be sent. - */ - sendChunksBulkMaxSize: number; - /** - * Indicates whether to use a dynamic iteration count. - */ - useDynamicIterationCount: boolean; - /** - * Indicates weather to pace the replication processing interval. - * Now (v0.24.x) not be respected. - */ - doNotPaceReplication: boolean; -} -/** - * Interface representing the settings for beta tweaks for the remote database. - */ -interface BetaRemoteDBSettings { - /** - * Indicates whether compression is enabled for the remote database. - */ - enableCompression: boolean; -} -/** - * Interface representing the some data stored on the settings. - */ -interface DataOnRemoteDBSettings { - /** - * VersionUp flash message which is shown when some incompatible changes are made during the update. - */ - versionUpFlash: string; - /** - * Unix timestamp (ms) of the latest tweak update. - * Used to determine which side has newer tweak values. - */ - tweakModified: number | undefined; -} -/** - * Interface representing the settings for replication. - */ -interface ReplicationSetting { - /** - * The maximum number of documents to be processed in a batch. - */ - batch_size: number; - /** - * The maximum number of batches to be processed. - */ - batches_limit: number; -} -/** - * Interface representing the settings for targetting files. - */ -interface FileHandlingSettings { - /** - * The regular expression for files to be synchronised. - */ - syncOnlyRegEx: CustomRegExpSourceList<"|[]|">; - /** - * The regular expression for files to be ignored during synchronisation. - */ - syncIgnoreRegEx: CustomRegExpSourceList<"|[]|">; -} -/** - * Interface representing the settings for processing behaviour. - */ -interface ProcessingBehaviourSettings { - /** - * Hash cache maximum count. - */ - hashCacheMaxCount: number; - /** - * Hash cache maximum amount. - */ - hashCacheMaxAmount: number; -} -/** - * Interface representing the settings for remote database tweaks. - */ -interface RemoteDBTweakSettings { - /** - * Indicates whether to ignore the version check. - */ - ignoreVersionCheck: boolean; - /** - * Indicates whether to ignore and continue syncing even if the configuration-mismatch is detected. - * (Note: Mismatched settings can lead to inappropriate de-duplication, leading to storage wastage and increased traffic). - */ - disableCheckingConfigMismatch: boolean; - /** - * Automatically accepts compatible-but-lossy tweak mismatches. - * If undefined, the feature is not configured yet. - */ - autoAcceptCompatibleTweak: boolean | undefined; -} -/** - * Interface representing the settings for optional and not exposed remote database settings. - */ -interface OptionalAndNotExposedRemoteDBSettings { - /** - * Indicates whether to accept empty passphrase. - * This not meant to `Not be encrypted`, but `Be encrypted with empty passphrase`. - */ - permitEmptyPassphrase: boolean; -} -/** - * Interface representing the settings for cross-platform interoperability. - */ -interface CrossPlatformInteroperabilitySettings { - /** - * Indicates whether to handle filename case sensitively. - */ - handleFilenameCaseSensitive: boolean; -} -/** - * Interface representing the settings for conflict handling. - */ -interface ConflictHandlingSettings { - /** - * Indicates whether to check conflicts only on file open. - */ - checkConflictOnlyOnOpen: boolean; - /** - * Indicates whether to show the merge dialog only on active file. - */ - showMergeDialogOnlyOnActive: boolean; -} -/** - * Settings that define the behavior of the merge process. - */ -interface MergeBehaviourSettings { - /** - * Indicates whether to synchronise after merging. - */ - syncAfterMerge: boolean; - /** - * Determines if conflicts should be resolved by choosing the newer file. - */ - resolveConflictsByNewerFile: boolean; - /** - * Specifies whether to write documents even if there are conflicts. - */ - writeDocumentsIfConflicted: boolean; - /** - * Disables automatic merging of markdown files. - */ - disableMarkdownAutoMerge: boolean; -} -/** - * Configuration settings for handling edge cases in the application. - */ -interface EdgeCaseHandlingSettings { - /** - * An optional suffix to append to the database name after the vault name. - */ - additionalSuffixOfDatabaseName: string | undefined; - /** - * Flag to disable the worker thread for generating chunks. - */ - disableWorkerForGeneratingChunks: boolean; - /** - * Flag to process small files in the UI thread instead of a worker thread. - */ - processSmallFilesInUIThread: boolean; - /** - * Indicates whether to use timeout for PouchDB replication. - */ - useTimeouts: boolean; -} -/** - * Configuration settings for handling deleted files. - */ -interface DeletedFileMetadataSettings { - /** - * Indicates whether to delete metadata of deleted files. - */ - deleteMetadataOfDeletedFiles: boolean; - /** - * The number of days to wait before automatically deleting metadata of deleted files. - */ - automaticallyDeleteMetadataOfDeletedFiles: number; -} -/** - * Represents a single remote configuration. - */ -export interface RemoteConfiguration { - /** - * Unique identifier for this configuration. - */ - id: string; - /** - * Display name for the configuration. - */ - name: string; - /** - * The connection string (URI) for the remote. - * This may be an encrypted string if configPassphraseStore is set. - */ - uri: string; - /** - * Indicates whether this configuration is encrypted. - */ - isEncrypted: boolean; -} -export interface RemoteConfigurations { - /** - * The list of remote configurations. - */ - remoteConfigurations: Record; - /** - * The ID of the currently active remote configuration. - */ - activeConfigurationId: string; - /** - * The ID of the active remote configuration dedicated for P2P features. - * If empty, P2P features should request explicit selection from the user. - */ - P2P_ActiveRemoteConfigurationId: string; -} -interface ObsidianLiveSyncSettings_PluginSetting extends SyncMethodSettings, UISettings, FileHandlingSettings, MergeBehaviourSettings, EncryptedUserSettings, PeriodicReplicationSettings, InternalFileSettings, PluginSyncSettings, ModeSettings, ExtraTweakSettings, BetaTweakSettings, ObsoleteSettings, DebugModeSettings, SettingSyncSettings, SafetyValveSettings, DataOnSettings, RemoteConfigurations { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -export type RemoteDBSettings = CouchDBConnection & BucketSyncSetting & RemoteTypeSettings & EncryptionSettings & ChunkSettings & EdenSettings & DataOnRemoteDBSettings & ObsoleteRemoteDBSettings & OnDemandChunkSettings & BetaRemoteDBSettings & ReplicationSetting & RemoteDBTweakSettings & FileHandlingSettings & ProcessingBehaviourSettings & OptionalAndNotExposedRemoteDBSettings & CrossPlatformInteroperabilitySettings & ConflictHandlingSettings & EdgeCaseHandlingSettings & DeletedFileMetadataSettings & P2PSyncSetting & RemoteConfigurations; -export type ObsidianLiveSyncSettings = ObsidianLiveSyncSettings_PluginSetting & RemoteDBSettings & LocalDBSettings; -export interface HasSettings> { - settings: T; -} -export {}; diff --git a/_types/src/lib/src/common/models/shared.const.behabiour.d.ts b/_types/src/lib/src/common/models/shared.const.behabiour.d.ts deleted file mode 100644 index 02c8309a..00000000 --- a/_types/src/lib/src/common/models/shared.const.behabiour.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const MAX_DOC_SIZE = 1000; -export declare const MAX_DOC_SIZE_BIN = 102400; -export declare const VER = 12; -export declare const RECENT_MODIFIED_DOCS_QTY = 30; -export declare const LEAF_WAIT_TIMEOUT = 30000; -export declare const LEAF_WAIT_ONLY_REMOTE = 5000; -export declare const LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR = 5000; -export declare const REPLICATION_BUSY_TIMEOUT = 3000000; -export declare const SALT_OF_PASSPHRASE = "rHGMPtr6oWw7VSa3W3wpa8fT8U"; -export declare const SALT_OF_ID = "a83hrf7f\u0003y7sa8g31"; -export declare const SEED_MURMURHASH = 305419896; -export declare const IDPrefixes: { - Obfuscated: string; - Chunk: string; - EncryptedChunk: string; -}; -/** - * @deprecated Use `IDPrefixes.Obfuscated` instead. - */ -export declare const PREFIX_OBFUSCATED = "f:"; -/** - * @deprecated Use `IDPrefixes.Chunk` instead. - */ -export declare const PREFIX_CHUNK = "h:"; -/** - * @deprecated Use `IDPrefixes.EncryptedChunk` instead. - */ -export declare const PREFIX_ENCRYPTED_CHUNK = "h:+"; diff --git a/_types/src/lib/src/common/models/shared.const.d.ts b/_types/src/lib/src/common/models/shared.const.d.ts deleted file mode 100644 index 6ddc7491..00000000 --- a/_types/src/lib/src/common/models/shared.const.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const SETTING_KEY_P2P_DEVICE_NAME = "p2p_device_name"; -export declare const configURIBase = "obsidian://setuplivesync?settings="; -export declare const configURIBaseQR = "obsidian://setuplivesync?settingsQR="; -export declare const SuffixDatabaseName = "-livesync-v2"; -export declare const ExtraSuffixIndexedDB = "-indexeddb"; diff --git a/_types/src/lib/src/common/models/shared.const.symbols.d.ts b/_types/src/lib/src/common/models/shared.const.symbols.d.ts deleted file mode 100644 index d06dcf57..00000000 --- a/_types/src/lib/src/common/models/shared.const.symbols.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const CANCELLED: unique symbol; -export declare const AUTO_MERGED: unique symbol; -export declare const NOT_CONFLICTED: unique symbol; -export declare const MISSING_OR_ERROR: unique symbol; -export declare const LEAVE_TO_SUBSEQUENT: unique symbol; -export declare const TIME_ARGUMENT_INFINITY: unique symbol; -export declare const BASE_IS_NEW: unique symbol; -export declare const TARGET_IS_NEW: unique symbol; -export declare const EVEN: unique symbol; diff --git a/_types/src/lib/src/common/models/shared.definition.configNames.d.ts b/_types/src/lib/src/common/models/shared.definition.configNames.d.ts deleted file mode 100644 index 1d644a98..00000000 --- a/_types/src/lib/src/common/models/shared.definition.configNames.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const LEVEL_ADVANCED = "ADVANCED"; -export declare const LEVEL_POWER_USER = "POWER_USER"; -export declare const LEVEL_EDGE_CASE = "EDGE_CASE"; -export type ConfigLevel = "" | "ADVANCED" | "POWER_USER" | "EDGE_CASE"; -export type ConfigurationItem = { - name: string; - desc?: string; - placeHolder?: string; - status?: "BETA" | "ALPHA" | "EXPERIMENTAL"; - obsolete?: boolean; - level?: ConfigLevel; - isHidden?: boolean; - isAdvanced?: boolean; -}; -export declare const configurationNames: Partial>; -/** - * Get human readable Configuration stability - * @param status - * @returns - */ -export declare function statusDisplay(status?: string): string; -/** - * Get human readable configuration name. - * @param key configuration key - * @param alt - * @returns - */ -export declare function confName(key: keyof ObsidianLiveSyncSettings, alt?: string): string; -/** - * Get human readable configuration description. - * @param key configuration key - * @param alt - * @returns - */ -export declare function confDesc(key: keyof ObsidianLiveSyncSettings, alt?: string): string | undefined; diff --git a/_types/src/lib/src/common/models/shared.definition.d.ts b/_types/src/lib/src/common/models/shared.definition.d.ts deleted file mode 100644 index 317274f1..00000000 --- a/_types/src/lib/src/common/models/shared.definition.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const DatabaseConnectingStatuses: { - readonly STARTED: "STARTED"; - readonly NOT_CONNECTED: "NOT_CONNECTED"; - readonly PAUSED: "PAUSED"; - readonly CONNECTED: "CONNECTED"; - readonly COMPLETED: "COMPLETED"; - readonly CLOSED: "CLOSED"; - readonly ERRORED: "ERRORED"; - readonly JOURNAL_SEND: "JOURNAL_SEND"; - readonly JOURNAL_RECEIVE: "JOURNAL_RECEIVE"; -}; -export type DatabaseConnectingStatus = (typeof DatabaseConnectingStatuses)[keyof typeof DatabaseConnectingStatuses]; -export type ReplicationStatics = { - sent: number; - arrived: number; - maxPullSeq: number; - maxPushSeq: number; - lastSyncPullSeq: number; - lastSyncPushSeq: number; - syncStatus: DatabaseConnectingStatus; -}; -export declare const DEFAULT_REPLICATION_STATICS: ReplicationStatics; diff --git a/_types/src/lib/src/common/models/shared.type.util.d.ts b/_types/src/lib/src/common/models/shared.type.util.d.ts deleted file mode 100644 index c4a68acc..00000000 --- a/_types/src/lib/src/common/models/shared.type.util.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { TaggedType } from "octagonal-wheels/common/types"; -export type { TaggedType }; -export type CustomRegExpSource = TaggedType; -export type CustomRegExpSourceList = TaggedType; -export type ParsedCustomRegExp = [isInverted: boolean, pattern: string]; -export type Prettify = { - [K in keyof T]: T[K]; -} & {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type diff --git a/_types/src/lib/src/common/models/sync.definition.d.ts b/_types/src/lib/src/common/models/sync.definition.d.ts deleted file mode 100644 index b6c19729..00000000 --- a/_types/src/lib/src/common/models/sync.definition.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { EntryTypes } from "./db.const"; -import type { DatabaseEntry, DocumentID } from "./db.type"; -export declare const ProtocolVersions: { - readonly UNSET: undefined; - readonly LEGACY: 1; - readonly ADVANCED_E2EE: 2; -}; -export type ProtocolVersion = (typeof ProtocolVersions)[keyof typeof ProtocolVersions]; -export declare const DOCID_SYNC_PARAMETERS: DocumentID; -export declare const DOCID_JOURNAL_SYNC_PARAMETERS: DocumentID; -export interface SyncParameters extends DatabaseEntry { - _id: typeof DOCID_SYNC_PARAMETERS; - _rev?: string; - type: (typeof EntryTypes)["SYNC_PARAMETERS"]; - protocolVersion: ProtocolVersion; - pbkdf2salt: string; -} -export declare const DEFAULT_SYNC_PARAMETERS: SyncParameters; diff --git a/_types/src/lib/src/common/models/tweak.definition.d.ts b/_types/src/lib/src/common/models/tweak.definition.d.ts deleted file mode 100644 index 15d03669..00000000 --- a/_types/src/lib/src/common/models/tweak.definition.d.ts +++ /dev/null @@ -1,193 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "./setting.type"; -export declare const TweakValuesShouldMatchedTemplate: Partial; -type TweakKeys = keyof TweakValues; -export declare const IncompatibleChanges: TweakKeys[]; -export declare const CompatibleButLossyChanges: TweakKeys[]; -type IncompatibleRecommendationPatterns = { - key: T; - isRecommendation?: boolean; -} & ({ - from: TweakValues[T]; - to: TweakValues[T]; -} | { - from: TweakValues[T]; -} | { - to: TweakValues[T]; -}); -export declare const IncompatibleChangesInSpecificPattern: IncompatibleRecommendationPatterns[]; -export declare const TweakValuesRecommendedTemplate: Partial; -export declare const TweakValuesDefault: Partial; -export declare const TweakValuesTemplate: { - tweakModified: number; - liveSync?: boolean | undefined; - syncOnSave?: boolean | undefined; - syncOnStart?: boolean | undefined; - syncOnFileOpen?: boolean | undefined; - syncOnEditorSave?: boolean | undefined; - keepReplicationActiveInBackground?: boolean | undefined; - syncMinimumInterval?: number | undefined; - showVerboseLog?: boolean | undefined; - lessInformationInLog?: boolean | undefined; - showLongerLogInsideEditor?: boolean | undefined; - showStatusOnEditor?: boolean | undefined; - showStatusOnStatusbar?: boolean | undefined; - showOnlyIconsOnEditor?: boolean | undefined; - hideFileWarningNotice?: boolean | undefined; - networkWarningStyle?: "" | "icon" | "hidden" | undefined; - displayLanguage?: import("../rosetta").I18N_LANGS | undefined; - trashInsteadDelete?: boolean | undefined; - doNotDeleteFolder?: boolean | undefined; - batchSave?: boolean | undefined; - batchSaveMinimumDelay?: number | undefined; - batchSaveMaximumDelay?: number | undefined; - syncMaxSizeInMB?: number | undefined; - useIgnoreFiles?: boolean | undefined; - ignoreFiles?: string | undefined; - processSizeMismatchedFiles?: boolean | undefined; - syncOnlyRegEx?: import("./shared.type.util").CustomRegExpSourceList<"|[]|"> | undefined; - syncIgnoreRegEx?: import("./shared.type.util").CustomRegExpSourceList<"|[]|"> | undefined; - syncAfterMerge?: boolean | undefined; - resolveConflictsByNewerFile?: boolean | undefined; - writeDocumentsIfConflicted?: boolean | undefined; - disableMarkdownAutoMerge?: boolean | undefined; - configPassphraseStore?: import("./setting.type").ConfigPassphraseStore | undefined; - encryptedPassphrase?: string | undefined; - encryptedCouchDBConnection?: string | undefined; - periodicReplication?: boolean | undefined; - periodicReplicationInterval?: number | undefined; - syncInternalFiles?: boolean | undefined; - syncInternalFilesBeforeReplication?: boolean | undefined; - syncInternalFilesInterval?: number | undefined; - syncInternalFilesIgnorePatterns?: import("./shared.type.util").CustomRegExpSourceList<","> | undefined; - syncInternalFilesTargetPatterns?: import("./shared.type.util").CustomRegExpSourceList<","> | undefined; - watchInternalFileChanges?: boolean | undefined; - suppressNotifyHiddenFilesChange?: boolean | undefined; - syncInternalFileOverwritePatterns?: import("./shared.type.util").CustomRegExpSourceList<","> | undefined; - usePluginSync?: boolean | undefined; - usePluginSettings?: boolean | undefined; - showOwnPlugins?: boolean | undefined; - autoSweepPlugins?: boolean | undefined; - autoSweepPluginsPeriodic?: boolean | undefined; - notifyPluginOrSettingUpdated?: boolean | undefined; - deviceAndVaultName?: string | undefined; - usePluginSyncV2?: boolean | undefined; - usePluginEtc?: boolean | undefined; - pluginSyncExtendedSetting?: Record | undefined; - useAdvancedMode?: boolean | undefined; - usePowerUserMode?: boolean | undefined; - useEdgeCaseMode?: boolean | undefined; - notifyThresholdOfRemoteStorageSize?: number | undefined; - disableWorkerForGeneratingChunks?: boolean | undefined; - processSmallFilesInUIThread?: boolean | undefined; - savingDelay?: number | undefined; - gcDelay?: number | undefined; - skipOlderFilesOnSync?: boolean | undefined; - useIndexedDBAdapter?: boolean | undefined; - enableDebugTools?: boolean | undefined; - writeLogToTheFile?: boolean | undefined; - settingSyncFile?: string | undefined; - writeCredentialsForSettingSync?: boolean | undefined; - notifyAllSettingSyncFile?: boolean | undefined; - suspendFileWatching?: boolean | undefined; - suspendParseReplicationResult?: boolean | undefined; - doNotSuspendOnFetching?: boolean | undefined; - maxMTimeForReflectEvents?: number | undefined; - versionUpFlash?: string | undefined; - settingVersion?: number | undefined; - isConfigured?: boolean; - lastReadUpdates?: number | undefined; - doctorProcessedVersion?: string | undefined; - remoteConfigurations?: Record | undefined; - activeConfigurationId?: string | undefined; - P2P_ActiveRemoteConfigurationId?: string | undefined; - couchDB_URI?: string | undefined; - couchDB_USER?: string | undefined; - couchDB_PASSWORD?: string | undefined; - couchDB_DBNAME?: string | undefined; - couchDB_CustomHeaders?: string | undefined; - useJWT?: boolean | undefined; - jwtAlgorithm?: import("./auth.type").JWTAlgorithm | undefined; - jwtKey?: string | undefined; - jwtKid?: string | undefined; - jwtSub?: string | undefined; - jwtExpDuration?: number | undefined; - useRequestAPI?: boolean | undefined; - accessKey?: string | undefined; - secretKey?: string | undefined; - bucket?: string | undefined; - region?: string | undefined; - endpoint?: string | undefined; - useCustomRequestHandler?: boolean | undefined; - bucketCustomHeaders?: string | undefined; - bucketPrefix?: string | undefined; - forcePathStyle?: boolean | undefined; - remoteType?: import("./setting.type").RemoteType | undefined; - encrypt?: boolean | undefined; - passphrase?: string | undefined; - usePathObfuscation?: boolean | undefined; - E2EEAlgorithm?: import("./setting.type").E2EEAlgorithm | undefined; - hashAlg?: import("./setting.type").HashAlgorithm | undefined; - minimumChunkSize?: number | undefined; - customChunkSize?: number | undefined; - longLineThreshold?: number | undefined; - useSegmenter?: boolean | undefined; - enableChunkSplitterV2?: boolean | undefined; - doNotUseFixedRevisionForChunks?: boolean | undefined; - chunkSplitterVersion?: import("./setting.type").ChunkSplitterVersion | undefined; - useEden?: boolean | undefined; - maxChunksInEden?: number | undefined; - maxTotalLengthInEden?: number | undefined; - maxAgeInEden?: number | undefined; - checkIntegrityOnSave?: boolean | undefined; - useHistory?: boolean | undefined; - disableRequestURI?: boolean | undefined; - sendChunksBulk?: boolean | undefined; - sendChunksBulkMaxSize?: number | undefined; - useDynamicIterationCount?: boolean | undefined; - doNotPaceReplication?: boolean | undefined; - readChunksOnline?: boolean | undefined; - useOnlyLocalChunk?: boolean | undefined; - concurrencyOfReadChunksOnline?: number | undefined; - minimumIntervalOfReadChunksOnline?: number | undefined; - enableCompression?: boolean | undefined; - batch_size?: number | undefined; - batches_limit?: number | undefined; - ignoreVersionCheck?: boolean | undefined; - disableCheckingConfigMismatch?: boolean | undefined; - autoAcceptCompatibleTweak?: boolean | undefined; - hashCacheMaxCount?: number | undefined; - hashCacheMaxAmount?: number | undefined; - permitEmptyPassphrase?: boolean | undefined; - handleFilenameCaseSensitive?: boolean | undefined; - checkConflictOnlyOnOpen?: boolean | undefined; - showMergeDialogOnlyOnActive?: boolean | undefined; - additionalSuffixOfDatabaseName?: string | undefined; - useTimeouts?: boolean | undefined; - deleteMetadataOfDeletedFiles?: boolean | undefined; - automaticallyDeleteMetadataOfDeletedFiles?: number | undefined; - P2P_AutoAccepting?: import("./setting.type").AutoAccepting | undefined; - P2P_AutoSyncPeers?: string | undefined; - P2P_AutoWatchPeers?: string | undefined; - P2P_SyncOnReplication?: string | undefined; - P2P_RebuildFrom?: string | undefined; - P2P_AutoAcceptingPeers?: string | undefined; - P2P_AutoDenyingPeers?: string | undefined; - P2P_IsHeadless?: boolean; - P2P_Enabled?: boolean | undefined; - P2P_relays?: string | undefined; - P2P_roomID?: string | undefined; - P2P_passphrase?: string | undefined; - P2P_AppID?: string | undefined; - P2P_AutoStart?: boolean | undefined; - P2P_AutoBroadcast?: boolean | undefined; - P2P_DevicePeerName?: string; - P2P_turnServers?: string | undefined; - P2P_turnUsername?: string | undefined; - P2P_turnCredential?: string | undefined; - P2P_useDiagRTC?: boolean; -}; -export type TweakValues = Partial; -export declare const DEVICE_ID_PREFERRED = "PREFERRED"; -export {}; diff --git a/_types/src/lib/src/common/rosetta.d.ts b/_types/src/lib/src/common/rosetta.d.ts deleted file mode 100644 index 80cf769d..00000000 --- a/_types/src/lib/src/common/rosetta.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** -# Rosetta stone -- To localise messages to your language, please write a translation to this file and submit a PR. -- Please order languages in alphabetic order, if you write multiple items. - -## Notice to ensure that your favours are not wasted. - -If you plan to utilise machine translation engines to contribute translated resources, -please ensure the engine's terms of service are compatible with our project's license. -Your diligence in this matter helps maintain compliance and avoid potential licensing issues. -Thank you for your consideration. - -Usually, our projects (Self-hosted LiveSync and its families) are licensed under MIT License. -To see details, please refer to the LICENSES file on each repository. - -## How to internationalise untranslated items? -1. Change the message literal to use `$msg` - "Could not parse YAML" -> $msg('anyKey') -2. Create `ls-debug` folder under the `.obsidian` folder of your vault. -3. Run Self-hosted LiveSync in dev mode (npm run dev). -4. You will get the `missing-translation-YYYY-MM-DD.jsonl` under `ls-debug`. Please copy and paste inside `allMessages` and write the translations. -5. Send me the PR! -*/ -declare const LANG_DE = "de"; -declare const LANG_ES = "es"; -declare const LANG_FR = "fr"; -declare const LANG_HE = "he"; -declare const LANG_JA = "ja"; -declare const LANG_RU = "ru"; -declare const LANG_ZH = "zh"; -declare const LANG_KO = "ko"; -declare const LANG_ZH_TW = "zh-tw"; -declare const LANG_DEF = "def"; -export declare const SUPPORTED_I18N_LANGS: string[]; -export type I18N_LANGS = typeof LANG_DEF | typeof LANG_DE | typeof LANG_ES | typeof LANG_FR | typeof LANG_HE | typeof LANG_JA | typeof LANG_KO | typeof LANG_RU | typeof LANG_ZH | typeof LANG_ZH_TW | ""; -export type MESSAGE = { - [key in I18N_LANGS]?: string; -}; -import { type MessageKeys } from "./messages/combinedMessages.dev"; -export declare function expandKeywords, U extends Record>(message: T, lang: I18N_LANGS, recurseLimit?: number): T; -export type AllMessageKeys = MessageKeys; -export {}; diff --git a/_types/src/lib/src/common/settingConstants.d.ts b/_types/src/lib/src/common/settingConstants.d.ts deleted file mode 100644 index 24a721f1..00000000 --- a/_types/src/lib/src/common/settingConstants.d.ts +++ /dev/null @@ -1,217 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ConfigurationItem, type ObsidianLiveSyncSettings } from "./types.ts"; -type ExtractPropertiesByType = { - [K in keyof T as T[K] extends U ? K : never]: T[K] extends U ? K : never; -}; -export type FilterStringKeys = keyof ExtractPropertiesByType; -export type FilterBooleanKeys = keyof ExtractPropertiesByType; -export type FilterNumberKeys = keyof ExtractPropertiesByType; -import type { FilePath, FilePathWithPrefixLC, FilePathWithPrefix, DocumentID } from "./models/db.type.ts"; -export type { FilePath, FilePathWithPrefixLC, FilePathWithPrefix, DocumentID }; -/** - * Self-hosted LiveSync settings pane items. - */ -export type OnDialogSettings = { - configPassphrase: string; - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; - dummy: number; - deviceAndVaultName: string; -}; -/** - * Default settings for the OnDialogSettings. - * Not used for the common library, but used for the Obsidian plugin. - */ -export declare const OnDialogSettingsDefault: OnDialogSettings; -export declare const AllSettingDefault: { - configPassphrase: string; - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; - dummy: number; - deviceAndVaultName: string; - liveSync: boolean; - syncOnSave: boolean; - syncOnStart: boolean; - syncOnFileOpen: boolean; - syncOnEditorSave: boolean; - keepReplicationActiveInBackground: boolean; - syncMinimumInterval: number; - showVerboseLog: boolean; - lessInformationInLog: boolean; - showLongerLogInsideEditor: boolean; - showStatusOnEditor: boolean; - showStatusOnStatusbar: boolean; - showOnlyIconsOnEditor: boolean; - hideFileWarningNotice: boolean; - networkWarningStyle: "" | "icon" | "hidden"; - displayLanguage: import("./rosetta.ts").I18N_LANGS; - trashInsteadDelete: boolean; - doNotDeleteFolder: boolean; - batchSave: boolean; - batchSaveMinimumDelay: number; - batchSaveMaximumDelay: number; - syncMaxSizeInMB: number; - useIgnoreFiles: boolean; - ignoreFiles: string; - processSizeMismatchedFiles: boolean; - syncOnlyRegEx: import("./types.ts").CustomRegExpSourceList<"|[]|">; - syncIgnoreRegEx: import("./types.ts").CustomRegExpSourceList<"|[]|">; - syncAfterMerge: boolean; - resolveConflictsByNewerFile: boolean; - writeDocumentsIfConflicted: boolean; - disableMarkdownAutoMerge: boolean; - configPassphraseStore: import("./types.ts").ConfigPassphraseStore; - encryptedPassphrase: string; - encryptedCouchDBConnection: string; - periodicReplication: boolean; - periodicReplicationInterval: number; - syncInternalFiles: boolean; - syncInternalFilesBeforeReplication: boolean; - syncInternalFilesInterval: number; - syncInternalFilesIgnorePatterns: import("./types.ts").CustomRegExpSourceList<",">; - syncInternalFilesTargetPatterns: import("./types.ts").CustomRegExpSourceList<",">; - watchInternalFileChanges: boolean; - suppressNotifyHiddenFilesChange: boolean; - syncInternalFileOverwritePatterns: import("./types.ts").CustomRegExpSourceList<",">; - usePluginSync: boolean; - usePluginSettings: boolean; - showOwnPlugins: boolean; - autoSweepPlugins: boolean; - autoSweepPluginsPeriodic: boolean; - notifyPluginOrSettingUpdated: boolean; - usePluginSyncV2: boolean; - usePluginEtc: boolean; - pluginSyncExtendedSetting: Record; - useAdvancedMode: boolean; - usePowerUserMode: boolean; - useEdgeCaseMode: boolean; - notifyThresholdOfRemoteStorageSize: number; - disableWorkerForGeneratingChunks: boolean; - processSmallFilesInUIThread: boolean; - savingDelay: number; - gcDelay: number; - skipOlderFilesOnSync: boolean; - useIndexedDBAdapter: boolean; - enableDebugTools: boolean; - writeLogToTheFile: boolean; - settingSyncFile: string; - writeCredentialsForSettingSync: boolean; - notifyAllSettingSyncFile: boolean; - suspendFileWatching: boolean; - suspendParseReplicationResult: boolean; - doNotSuspendOnFetching: boolean; - maxMTimeForReflectEvents: number; - versionUpFlash: string; - settingVersion: number; - isConfigured?: boolean; - lastReadUpdates: number; - doctorProcessedVersion: string; - remoteConfigurations: Record; - activeConfigurationId: string; - P2P_ActiveRemoteConfigurationId: string; - couchDB_URI: string; - couchDB_USER: string; - couchDB_PASSWORD: string; - couchDB_DBNAME: string; - couchDB_CustomHeaders: string; - useJWT: boolean; - jwtAlgorithm: import("./models/auth.type.ts").JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - useRequestAPI: boolean; - accessKey: string; - secretKey: string; - bucket: string; - region: string; - endpoint: string; - useCustomRequestHandler: boolean; - bucketCustomHeaders: string; - bucketPrefix: string; - forcePathStyle: boolean; - remoteType: import("./types.ts").RemoteType; - encrypt: boolean; - passphrase: string; - usePathObfuscation: boolean; - E2EEAlgorithm: import("./types.ts").E2EEAlgorithm; - hashAlg: import("./types.ts").HashAlgorithm; - minimumChunkSize: number; - customChunkSize: number; - longLineThreshold: number; - useSegmenter: boolean; - enableChunkSplitterV2: boolean; - doNotUseFixedRevisionForChunks: boolean; - chunkSplitterVersion: import("./types.ts").ChunkSplitterVersion; - useEden: boolean; - maxChunksInEden: number; - maxTotalLengthInEden: number; - maxAgeInEden: number; - tweakModified: number | undefined; - checkIntegrityOnSave: boolean; - useHistory: boolean; - disableRequestURI: boolean; - sendChunksBulk: boolean; - sendChunksBulkMaxSize: number; - useDynamicIterationCount: boolean; - doNotPaceReplication: boolean; - readChunksOnline: boolean; - useOnlyLocalChunk: boolean; - concurrencyOfReadChunksOnline: number; - minimumIntervalOfReadChunksOnline: number; - enableCompression: boolean; - batch_size: number; - batches_limit: number; - ignoreVersionCheck: boolean; - disableCheckingConfigMismatch: boolean; - autoAcceptCompatibleTweak: boolean | undefined; - hashCacheMaxCount: number; - hashCacheMaxAmount: number; - permitEmptyPassphrase: boolean; - handleFilenameCaseSensitive: boolean; - checkConflictOnlyOnOpen: boolean; - showMergeDialogOnlyOnActive: boolean; - additionalSuffixOfDatabaseName: string | undefined; - useTimeouts: boolean; - deleteMetadataOfDeletedFiles: boolean; - automaticallyDeleteMetadataOfDeletedFiles: number; - P2P_AutoAccepting: import("./types.ts").AutoAccepting; - P2P_AutoSyncPeers: string; - P2P_AutoWatchPeers: string; - P2P_SyncOnReplication: string; - P2P_RebuildFrom: string; - P2P_AutoAcceptingPeers: string; - P2P_AutoDenyingPeers: string; - P2P_IsHeadless?: boolean; - P2P_Enabled: boolean; - P2P_relays: string; - P2P_roomID: string; - P2P_passphrase: string; - P2P_AppID: string; - P2P_AutoStart: boolean; - P2P_AutoBroadcast: boolean; - P2P_DevicePeerName?: string; - P2P_turnServers: string; - P2P_turnUsername: string; - P2P_turnCredential: string; - P2P_useDiagRTC?: boolean; -}; -export type AllSettings = ObsidianLiveSyncSettings & OnDialogSettings; -export type AllStringItemKey = FilterStringKeys; -export type AllNumericItemKey = FilterNumberKeys; -export type AllBooleanItemKey = FilterBooleanKeys; -export type AllSettingItemKey = AllStringItemKey | AllNumericItemKey | AllBooleanItemKey; -export type ValueOf = T extends AllStringItemKey ? string : T extends AllNumericItemKey ? number : T extends AllBooleanItemKey ? boolean : AllSettings[T]; -export declare const SettingInformation: Partial>; -export declare function getConfig(key: AllSettingItemKey): false | { - name: string; - desc?: string; - placeHolder?: string; - status?: "BETA" | "ALPHA" | "EXPERIMENTAL"; - obsolete?: boolean; - level?: import("./types.ts").ConfigLevel; - isHidden?: boolean; - isAdvanced?: boolean; -}; -export declare function getConfName(key: AllSettingItemKey): string; diff --git a/_types/src/lib/src/common/typeUtils.d.ts b/_types/src/lib/src/common/typeUtils.d.ts deleted file mode 100644 index 0b2da7b2..00000000 --- a/_types/src/lib/src/common/typeUtils.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, FilePath, FilePathWithPrefix } from "./models/db.type"; -import type { UXFileInfoStub } from "./types"; -/** - * returns is internal chunk of file - * @param id ID - * @returns - */ -export declare function isInternalMetadata(id: FilePath | FilePathWithPrefix | DocumentID): boolean; -export declare function isInternalFile(file: UXFileInfoStub | string | FilePathWithPrefix): boolean; -export declare function stripInternalMetadataPrefix(id: T): T; -export declare function id2InternalMetadataId(id: DocumentID): DocumentID; -export declare function isChunk(str: string): boolean; -export declare function isPluginMetadata(str: string): boolean; -export declare function isCustomisationSyncMetadata(str: string): boolean; -export declare function getPathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix): FilePathWithPrefix; -export declare function getStoragePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix): FilePath; -export declare function getDatabasePathFromUXFileInfo(file: UXFileInfoStub | string | FilePathWithPrefix): FilePathWithPrefix; diff --git a/_types/src/lib/src/common/types.d.ts b/_types/src/lib/src/common/types.d.ts deleted file mode 100644 index ba7965d5..00000000 --- a/_types/src/lib/src/common/types.d.ts +++ /dev/null @@ -1,109 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { TaggedType } from "./models/shared.type.util.ts"; -export { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, } from "octagonal-wheels/common/logger"; -export type { LOG_LEVEL } from "octagonal-wheels/common/logger"; -import { RESULT_NOT_FOUND, RESULT_TIMED_OUT } from "octagonal-wheels/common/const"; -import type { Credential } from "./models/auth.type.ts"; -import type { AnyEntry, ChunkVersionRange, DatabaseEntry, EdenChunk, EntryBase, EntryChunkPack, EntryHasPath, EntryLeaf, EntryType, EntryTypeNotes, EntryTypeNotesWithLegacy, EntryVersionInfo, EntryWithEden, InternalFileEntry, LoadedEntry, MetaEntry, NewEntry, NoteEntry, PlainEntry, SavingEntry, SyncInfo } from "./models/db.type.ts"; -import { ChunkTypes, EntryTypes, MILESTONE_DOCID, NODEINFO_DOCID, NoteTypes, SYNCINFO_ID, VERSIONING_DOCID } from "./models/db.const.ts"; -import { AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, MISSING_OR_ERROR, NOT_CONFLICTED, TIME_ARGUMENT_INFINITY } from "./models/shared.const.symbols.ts"; -import { IDPrefixes, LEAF_WAIT_ONLY_REMOTE, LEAF_WAIT_TIMEOUT, LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR, MAX_DOC_SIZE, MAX_DOC_SIZE_BIN, PREFIX_CHUNK, PREFIX_ENCRYPTED_CHUNK, PREFIX_OBFUSCATED, RECENT_MODIFIED_DOCS_QTY, REPLICATION_BUSY_TIMEOUT, SALT_OF_ID, SALT_OF_PASSPHRASE, SEED_MURMURHASH, VER } from "./models/shared.const.behabiour.ts"; -import { AutoAccepting, type BucketSyncSetting, type ChunkSplitterVersion, type ConfigPassphraseStore, type CouchDBConnection, type E2EEAlgorithm, type EncryptionSettings, type HashAlgorithm, type HasSettings, type LocalDBSettings, type ObsidianLiveSyncSettings, type P2PConnectionInfo, type P2PSyncSetting, type PluginSyncSettingEntry, type RemoteDBSettings, type RemoteType, type RemoteTypeSettings, type SYNC_MODE } from "./models/setting.type.ts"; -import { ChunkAlgorithmNames, ChunkAlgorithms, CURRENT_SETTING_VERSION, E2EEAlgorithmNames, E2EEAlgorithms, HashAlgorithms, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, RemoteTypes, SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SELECTIVE, MODE_SHINY } from "./models/setting.const.ts"; -import { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED } from "./models/setting.const.preferred.ts"; -import { P2P_DEFAULT_SETTINGS, DEFAULT_SETTINGS } from "./models/setting.const.defaults.ts"; -import { KeyIndexOfSettings } from "./models/setting.const.qr.ts"; -import type { DeviceInfo, EntryBody, EntryDoc, EntryDocResponse, EntryMilestoneInfo, EntryNodeInfo, NodeData, NodeKey } from "./models/db.definition.ts"; -import { isMetaEntry } from "./models/db.definition.ts"; -import type { CouchDBCredentials, BasicCredentials, JWTCredentials, JWTHeader, JWTPayload, JWTParams, PreparedJWT } from "./models/auth.type.ts"; -import type { CacheData, FileEventArgs, FileEventItem, FileEventType, UXAbstractInfoStub, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub, UXStat } from "./models/fileaccess.type.ts"; -import { SETTING_KEY_P2P_DEVICE_NAME, configURIBase, configURIBaseQR, SuffixDatabaseName, ExtraSuffixIndexedDB } from "./models/shared.const.ts"; -import { configurationNames, LEVEL_ADVANCED, LEVEL_POWER_USER, LEVEL_EDGE_CASE, type ConfigLevel, type ConfigurationItem, statusDisplay, confName, confDesc } from "./models/shared.definition.configNames.ts"; -import type { CustomRegExpSource, CustomRegExpSourceList, ParsedCustomRegExp, Prettify } from "./models/shared.type.util.ts"; -import { ProtocolVersions, type ProtocolVersion, DOCID_SYNC_PARAMETERS, DOCID_JOURNAL_SYNC_PARAMETERS, type SyncParameters, DEFAULT_SYNC_PARAMETERS } from "./models/sync.definition.ts"; -import { TweakValuesShouldMatchedTemplate, IncompatibleChanges, CompatibleButLossyChanges, IncompatibleChangesInSpecificPattern, TweakValuesRecommendedTemplate, TweakValuesDefault, TweakValuesTemplate, type TweakValues, DEVICE_ID_PREFERRED } from "./models/tweak.definition.ts"; -import type { diff_result_leaf, dmp_result, diff_result, DIFF_CHECK_RESULT_AUTO, diff_check_result } from "./models/diff.definition.ts"; -import { PREFIXMD_LOGFILE, PREFIXMD_LOGFILE_UC, FlagFilesOriginal, FlagFilesHumanReadable, FLAGMD_REDFLAG, FLAGMD_REDFLAG2, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3, FLAGMD_REDFLAG3_HR } from "./models/redflag.const.ts"; -import { DatabaseConnectingStatuses, type DatabaseConnectingStatus } from "./models/shared.definition.ts"; -export { RESULT_NOT_FOUND, RESULT_TIMED_OUT }; -export type { FilePath, FilePathWithPrefixLC, FilePathWithPrefix, DocumentID } from "./models/db.type.ts"; -export { MAX_DOC_SIZE, MAX_DOC_SIZE_BIN, VER, RECENT_MODIFIED_DOCS_QTY, LEAF_WAIT_TIMEOUT, LEAF_WAIT_ONLY_REMOTE, LEAF_WAIT_TIMEOUT_SEQUENTIAL_REPLICATOR, REPLICATION_BUSY_TIMEOUT, CANCELLED, AUTO_MERGED, NOT_CONFLICTED, MISSING_OR_ERROR, LEAVE_TO_SUBSEQUENT, TIME_ARGUMENT_INFINITY, VERSIONING_DOCID, MILESTONE_DOCID, NODEINFO_DOCID, }; -export { type CouchDBConnection }; -export type { ConfigPassphraseStore }; -export { MODE_SELECTIVE, MODE_AUTOMATIC, MODE_PAUSED, MODE_SHINY, type SYNC_MODE }; -export { type PluginSyncSettingEntry }; -export { SETTING_VERSION_INITIAL, SETTING_VERSION_SUPPORT_CASE_INSENSITIVE, CURRENT_SETTING_VERSION }; -export type { BucketSyncSetting, LocalDBSettings }; -export { RemoteTypes, REMOTE_COUCHDB, REMOTE_MINIO, REMOTE_P2P, type RemoteType, AutoAccepting }; -export type { P2PConnectionInfo, P2PSyncSetting }; -export { P2P_DEFAULT_SETTINGS }; -export type { RemoteTypeSettings }; -export { E2EEAlgorithmNames, E2EEAlgorithms, type E2EEAlgorithm }; -export type { EncryptionSettings }; -export { HashAlgorithms, type HashAlgorithm, ChunkAlgorithmNames, ChunkAlgorithms, type ChunkSplitterVersion }; -export type { RemoteDBSettings }; -export type { ObsidianLiveSyncSettings }; -export { DEFAULT_SETTINGS }; -export { KeyIndexOfSettings }; -export { type HasSettings }; -export { PREFERRED_BASE, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, PREFERRED_JOURNAL_SYNC }; -export { EntryTypes, NoteTypes, ChunkTypes, type EntryType, type EntryTypeNotes, type EntryTypeNotesWithLegacy, type DatabaseEntry, type EntryBase, type EdenChunk, type EntryWithEden, type NoteEntry, type NewEntry, type PlainEntry, type InternalFileEntry, type AnyEntry, type LoadedEntry, type SavingEntry, type MetaEntry, isMetaEntry, type EntryLeaf, type EntryChunkPack, type EntryVersionInfo, type EntryHasPath, }; -export type { ChunkVersionRange }; -export { TweakValuesShouldMatchedTemplate }; -export { IncompatibleChanges }; -export { CompatibleButLossyChanges }; -export { IncompatibleChangesInSpecificPattern }; -export { TweakValuesRecommendedTemplate }; -export { TweakValuesDefault }; -export { configurationNames }; -export { LEVEL_ADVANCED }; -export { LEVEL_POWER_USER }; -export { LEVEL_EDGE_CASE }; -export type { ConfigLevel }; -export type { ConfigurationItem }; -export { statusDisplay }; -export { confName }; -export { confDesc }; -export { TweakValuesTemplate }; -export type { TweakValues }; -export { DEVICE_ID_PREFERRED }; -export type { NodeKey }; -export type { DeviceInfo }; -export type { NodeData }; -export type { EntryMilestoneInfo }; -export type { EntryNodeInfo }; -export type { EntryBody }; -export type { EntryDoc }; -export type { diff_result_leaf }; -export type { dmp_result }; -export type { diff_result }; -export type { DIFF_CHECK_RESULT_AUTO }; -export type { diff_check_result }; -export type { Credential }; -export type { EntryDocResponse }; -export { DatabaseConnectingStatuses }; -export type { DatabaseConnectingStatus }; -export { PREFIXMD_LOGFILE, PREFIXMD_LOGFILE_UC, FlagFilesOriginal, FlagFilesHumanReadable, FLAGMD_REDFLAG, FLAGMD_REDFLAG2, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3, FLAGMD_REDFLAG3_HR, }; -export { SYNCINFO_ID }; -export type { SyncInfo }; -export { SALT_OF_PASSPHRASE, SALT_OF_ID, SEED_MURMURHASH, IDPrefixes, PREFIX_OBFUSCATED, PREFIX_CHUNK, PREFIX_ENCRYPTED_CHUNK, }; -export type { UXStat, UXFileInfo, UXAbstractInfoStub, UXFileInfoStub, UXInternalFileInfoStub, UXFolderInfo, UXDataWriteOptions, CacheData, FileEventType, FileEventArgs, FileEventItem, }; -export type { Prettify }; -export type { CouchDBCredentials }; -export type { BasicCredentials }; -export type { JWTCredentials }; -export type { JWTHeader }; -export type { JWTPayload }; -export type { JWTParams }; -export type { PreparedJWT }; -export type { CustomRegExpSource }; -export type { CustomRegExpSourceList }; -export type { ParsedCustomRegExp }; -export { ProtocolVersions }; -export type { ProtocolVersion }; -export { DOCID_SYNC_PARAMETERS }; -export { DOCID_JOURNAL_SYNC_PARAMETERS }; -export type { SyncParameters }; -export { DEFAULT_SYNC_PARAMETERS }; -export { SETTING_KEY_P2P_DEVICE_NAME, configURIBase, configURIBaseQR, SuffixDatabaseName, ExtraSuffixIndexedDB }; diff --git a/_types/src/lib/src/common/utils.d.ts b/_types/src/lib/src/common/utils.d.ts deleted file mode 100644 index 286c212a..00000000 --- a/_types/src/lib/src/common/utils.d.ts +++ /dev/null @@ -1,131 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type DatabaseEntry, type EntryLeaf, type SyncInfo, type LoadedEntry, type SavingEntry, type NewEntry, type PlainEntry, type CustomRegExpSource, type ParsedCustomRegExp, type CustomRegExpSourceList, type ObsidianLiveSyncSettings, type RemoteDBSettings, type P2PConnectionInfo, type BucketSyncSetting, type CouchDBConnection, type EncryptionSettings } from "./types.ts"; -import { replaceAll, replaceAllPairs } from "octagonal-wheels/string"; -export { replaceAll, replaceAllPairs }; -import { concatUInt8Array } from "octagonal-wheels/binary"; -export { concatUInt8Array }; -import { delay, fireAndForget } from "octagonal-wheels/promises"; -export { delay, fireAndForget }; -import { arrayToChunkedArray, unique } from "octagonal-wheels/collection"; -export { arrayToChunkedArray, unique }; -import { extractObject, isObjectDifferent } from "octagonal-wheels/object"; -export { extractObject, isObjectDifferent }; -import { sendValue, sendSignal, waitForSignal, waitForValue } from "octagonal-wheels/messagepassing/signal"; -export { sendValue, sendSignal, waitForSignal, waitForValue }; -import { throttle } from "octagonal-wheels/function"; -export { throttle }; -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "./models/shared.const.symbols.ts"; -export type { SimpleStore }; -export { sizeToHumanReadable } from "octagonal-wheels/number"; -export declare function resolveWithIgnoreKnownError(p: Promise, def: T): Promise; -export declare const Parallels: (ps?: Set>) => { - add: (p: Promise) => Set>; - wait: (limit: number) => false | Promise; - all: () => Promise; -}; -export declare function allSettledWithConcurrencyLimit(processes: Promise[], limit: number): Promise; -export declare function getDocData(doc: string | string[]): string; -export declare function getDocDataAsArray(doc: string | string[]): string[]; -export declare function getDocDataAsArrayBuffer(doc: string | string[] | ArrayBuffer): Uint8Array; -export declare function isTextBlob(blob: Blob): boolean; -export declare function createTextBlob(data: string | string[]): Blob; -export declare function createBinaryBlob(data: Uint8Array | ArrayBuffer): Blob; -export declare function createBlob(data: string | string[] | Uint8Array | ArrayBuffer | Blob): Blob; -export declare function isTextDocument(doc: LoadedEntry): boolean; -export declare function readAsBlob(doc: LoadedEntry): Blob; -export declare function readContent(doc: LoadedEntry): string | ArrayBuffer; -export declare function isDocContentSame(docA: string | string[] | Blob | ArrayBuffer, docB: string | string[] | Blob | ArrayBuffer): Promise; -export declare function isObfuscatedEntry(doc: DatabaseEntry): doc is AnyEntry; -export declare function isEncryptedChunkEntry(doc: DatabaseEntry): doc is EntryLeaf; -export declare function isSyncInfoEntry(doc: DatabaseEntry): doc is SyncInfo; -export declare function memorizeFuncWithLRUCache(func: (key: T) => U): (key: T) => U | undefined; -export declare function memorizeFuncWithLRUCacheMulti(func: (...keys: T) => U): (keys: T) => U | undefined; -/** - * - * @param exclusion return only not exclusion - * @returns - * - * ["something",false,"aaaaa"].filter(onlyNot(false)) => yields ["something","aaaaaa"]. but, as string[]. - */ -export declare function onlyNot(exclusion: B): (item: A | B) => item is Exclude; -/** - * Run task with keeping minimum interval - * @param key waiting key - * @param interval interval (ms) - * @param task task to perform. - * @returns result of task - * @remarks This function is not designed to be concurrent. - */ -export declare function runWithInterval(key: string, interval: number, task: () => Promise): Promise; -/** - * Run task with keeping minimum interval on start - * @param key waiting key - * @param interval interval (ms) - * @param task task to perform. - * @returns result of task - * @remarks This function is not designed to be concurrent. - */ -export declare function runWithStartInterval(key: string, interval: number, task: () => Promise): Promise; -export declare const globalConcurrencyController: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject; -export declare function determineTypeFromBlob(data: Blob): "newnote" | "plain"; -export declare function determineType(path: string, data: string | string[] | Uint8Array | ArrayBuffer | Blob): "newnote" | "plain"; -export declare function isAnyNote(doc: DatabaseEntry): doc is NewEntry | PlainEntry; -export declare function isLoadedEntry(doc: DatabaseEntry): doc is LoadedEntry; -export declare function isDeletedEntry(doc: LoadedEntry): boolean; -export declare function createSavingEntryFromLoadedEntry(doc: LoadedEntry): SavingEntry; -export declare function setAllItems(set: Set, items: T[]): Set; -export declare function escapeNewLineFromString(str: string): string; -export declare function unescapeNewLineFromString(str: string): string; -export declare function escapeMarkdownValue(value: T): T; -export declare function timeDeltaToHumanReadable(delta: number): string; -export declare function wrapException(func: () => Promise>): Promise | Error>; -export declare function toRanges(sorted: number[]): string; -export declare function isDirty(key: string, value: unknown): boolean; -export declare function tryParseJSON(str: string, fallbackValue?: T): T | undefined; -export { mergeObject, applyPatch, generatePatchObj, flattenObject, isObjectMargeApplicable, isSensibleMargeApplicable, } from "./utils.patch.ts"; -export declare function parseHeaderValues(strHeader: string): Record; -/*** - * Parse custom regular expression - * @param regexp - * @returns [negate: boolean, regexp: string] - * @example `!!foo` => [true, "foo"] - * @example `foo` => [false, "foo"] - */ -export declare function parseCustomRegExp(regexp: CustomRegExpSource): ParsedCustomRegExp; -export declare function matchRegExp(regexp: CustomRegExpSource, target: string): boolean; -export declare function isValidRegExp(regexp: CustomRegExpSource): boolean; -export declare function isInvertedRegExp(regexp: CustomRegExpSource): boolean; -export declare function constructCustomRegExpList(items: CustomRegExpSource[], delimiter: D): CustomRegExpSourceList; -export declare function splitCustomRegExpList(list: CustomRegExpSourceList, delimiter: D): CustomRegExpSource[]; -export declare class CustomRegExp { - regexp: RegExp; - negate: boolean; - pattern: string; - constructor(regexp: CustomRegExpSource, flags?: string); - test(str: string): boolean; -} -type RegExpSettingKey = "syncOnlyRegEx" | "syncIgnoreRegEx" | "syncInternalFilesIgnorePatterns" | "syncInternalFilesTargetPatterns" | "syncInternalFileOverwritePatterns"; -export declare function getFileRegExp(settings: ObsidianLiveSyncSettings | RemoteDBSettings, key: RegExpSettingKey): CustomRegExp[]; -/** - * Copies properties from the source object to the target object only if they exist in the target. - * @param source The object to copy properties from. - * @param target The object to copy properties to. - */ -export declare function copyTo(source: U, target: T): void; -export declare function pickBucketSyncSettings(setting: ObsidianLiveSyncSettings): BucketSyncSetting; -export declare function pickCouchDBSyncSettings(setting: ObsidianLiveSyncSettings): CouchDBConnection; -export declare function pickEncryptionSettings(setting: ObsidianLiveSyncSettings | EncryptionSettings): EncryptionSettings; -export declare function pickP2PSyncSettings(setting: Partial & P2PConnectionInfo): P2PConnectionInfo; -export declare function wrapByDefault(func: () => T, onError: (err: Error) => U): T | U; -export declare function compareMTime(baseMTime: number, targetMTime: number): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; -export declare function displayRev(rev: string): string; -/** - * Generate a random P2P Room ID in the format `123-456-789-abc`. - */ -export declare function generateP2PRoomId(): string; -/** - * Extract the stable suffix (last segment) from a Room ID. - */ -export declare function extractP2PRoomSuffix(roomId: string): string; diff --git a/_types/src/lib/src/common/utils.doc.d.ts b/_types/src/lib/src/common/utils.doc.d.ts deleted file mode 100644 index 9cf61d47..00000000 --- a/_types/src/lib/src/common/utils.doc.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function isErrorOf(ex: unknown, statusCode: number): boolean; -/** - * Checks if the error is effectively a 404 error from CouchDB or PouchDB. - * @param ex some error object, expected to be from CouchDB or PouchDB. - * @returns true if the error is a 404 not found error, false otherwise. - * @throws if the input is not an object or does not have a numeric "status" property. - */ -export declare function isNotFoundError(ex: unknown): boolean; -export declare function isConflictError(ex: unknown): boolean; -export declare function isUnauthorizedError(ex: unknown): boolean; -export declare function tryGetFilePath(entry: unknown): string | undefined; diff --git a/_types/src/lib/src/common/utils.object.d.ts b/_types/src/lib/src/common/utils.object.d.ts deleted file mode 100644 index ea17d4fc..00000000 --- a/_types/src/lib/src/common/utils.object.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function asCopy(obj: T): T; -export declare function ensureError(error: unknown): Error; diff --git a/_types/src/lib/src/common/utils.patch.d.ts b/_types/src/lib/src/common/utils.patch.d.ts deleted file mode 100644 index 432e9433..00000000 --- a/_types/src/lib/src/common/utils.patch.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function generatePatchObj(from: Record, to: Record): Record; -export declare function applyPatch(from: Record, patch: Record): Record; -export declare function mergeObject(objA: Record | [unknown], objB: Record | [unknown]): unknown[] | { - [k: string]: unknown; -}; -export declare function flattenObject(obj: Record, path?: string[]): [string, unknown][]; -export declare function isSensibleMargeApplicable(path: string): boolean; -export declare function isObjectMargeApplicable(path: string): boolean; diff --git a/_types/src/lib/src/common/utils.type.d.ts b/_types/src/lib/src/common/utils.type.d.ts deleted file mode 100644 index a0e11728..00000000 --- a/_types/src/lib/src/common/utils.type.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type Constructor = new (...args: any[]) => T; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration diff --git a/_types/src/lib/src/dataobject/StoredMap.d.ts b/_types/src/lib/src/dataobject/StoredMap.d.ts deleted file mode 100644 index 3962e99f..00000000 --- a/_types/src/lib/src/dataobject/StoredMap.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -export declare class StoredMapLike { - _store: SimpleStore; - _cache: Map; - _prefix: string; - constructor(store: SimpleStore>, prefix?: string); - addPrefix(key: string): string; - get(key: string): Promise; - set(key: string, value: U): Promise; - delete(key: string): Promise; - has(key: string): Promise; -} diff --git a/_types/src/lib/src/dev/checks.d.ts b/_types/src/lib/src/dev/checks.d.ts deleted file mode 100644 index 2e11adf3..00000000 --- a/_types/src/lib/src/dev/checks.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -interface InstanceHaveOnBindFunction { - onBindFunction: (...params: T[]) => void; -} -export declare function __$checkInstanceBinding>(instance: T): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export {}; diff --git a/_types/src/lib/src/encryption/encryptHKDF.d.ts b/_types/src/lib/src/encryption/encryptHKDF.d.ts deleted file mode 100644 index ff5ff860..00000000 --- a/_types/src/lib/src/encryption/encryptHKDF.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { encryptHKDFWorker, decryptHKDFWorker } from "@lib/worker/bgWorker.ts"; -export declare const encryptHKDF: typeof encryptHKDFWorker; -export declare const decryptHKDF: typeof decryptHKDFWorker; diff --git a/_types/src/lib/src/encryption/stringEncryption.d.ts b/_types/src/lib/src/encryption/stringEncryption.d.ts deleted file mode 100644 index 6a7c7d20..00000000 --- a/_types/src/lib/src/encryption/stringEncryption.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Encrypts a string using a passphrase, unless the string is already encrypted. - * - * If the input string begins with `ENCRYPT_V2_PREFIX` or `HKDF_SALTED_ENCRYPTED_PREFIX`, - * we assume it is already encrypted and return it unchanged. - * Otherwise, we encrypt the string using an ephemeral salt and the provided passphrase. - * - * @param source - The plaintext string to encrypt, or an already encrypted string. - * @param passphrase - The passphrase used for encryption. - * @returns A promise resolving to the encrypted string, or the original string if it is already encrypted. - */ -export declare function encryptString(source: string, passphrase: string): Promise; -/** - * Decrypts an encrypted string using the provided passphrase. - * - * This function determines the encryption format by inspecting the string prefix, then applies - * the appropriate decryption method. It supports several encryption formats, including - * HKDF salted encryption and legacy formats (V1, V2, V3). If the format is not supported, - * an error is thrown. - * - * @param encrypted - The encrypted string to decrypt. - * @param passphrase - The passphrase used for decryption. - * @returns A promise resolving to the decrypted string. - * @throws {Error} If the encryption format is unsupported. - */ -export declare function decryptString(encrypted: string, passphrase: string): Promise; -export declare function tryDecryptString(encrypted: string, passphrase: string | false): Promise; diff --git a/_types/src/lib/src/events/coreEvents.d.ts b/_types/src/lib/src/events/coreEvents.d.ts deleted file mode 100644 index 0602935c..00000000 --- a/_types/src/lib/src/events/coreEvents.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix, ObsidianLiveSyncSettings } from "@lib/common/types"; -export declare const EVENT_LAYOUT_READY = "layout-ready"; -export declare const EVENT_PLUGIN_LOADED = "plugin-loaded"; -export declare const EVENT_PLUGIN_UNLOADED = "plugin-unloaded"; -export declare const EVENT_SETTING_SAVED = "setting-saved"; -export declare const EVENT_FILE_RENAMED = "file-renamed"; -export declare const EVENT_FILE_SAVED = "file-saved"; -export declare const EVENT_LEAF_ACTIVE_CHANGED = "leaf-active-changed"; -export declare const EVENT_DATABASE_REBUILT = "database-rebuilt"; -export declare const EVENT_LOG_ADDED = "log-added"; -export declare const EVENT_REQUEST_OPEN_SETUP_URI = "request-open-setup-uri"; -export declare const EVENT_REQUEST_COPY_SETUP_URI = "request-copy-setup-uri"; -export declare const EVENT_REQUEST_SHOW_SETUP_QR = "request-show-setup-qr"; -export declare const EVENT_REQUEST_RELOAD_SETTING_TAB = "reload-setting-tab"; -export declare const EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG = "request-open-plugin-sync-dialog"; -export declare const EVENT_FILE_CHANGED = "event-file-changed"; -export declare const EVENT_REQUEST_OPEN_P2P_SETTINGS = "request-open-p2p-settings"; -export declare const EVENT_REQUEST_OPEN_P2P = "request-open-p2p"; -export declare const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p"; -export declare const EVENT_PLATFORM_UNLOADED = "platform-unloaded"; -export declare const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error"; -export declare const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size"; -declare global { - interface LSEvents { - [EVENT_FILE_SAVED]: undefined; - [EVENT_SETTING_SAVED]: ObsidianLiveSyncSettings; - [EVENT_LAYOUT_READY]: undefined; - [EVENT_FILE_CHANGED]: { - file: FilePathWithPrefix; - automated: boolean; - }; - [EVENT_FILE_RENAMED]: { - newPath: FilePathWithPrefix; - old: FilePathWithPrefix; - }; - [EVENT_DATABASE_REBUILT]: undefined; - [EVENT_REQUEST_OPEN_P2P_SETTINGS]: undefined; - [EVENT_REQUEST_SHOW_SETUP_QR]: undefined; - [EVENT_REQUEST_OPEN_P2P]: undefined; - [EVENT_REQUEST_CLOSE_P2P]: undefined; - [EVENT_PLATFORM_UNLOADED]: undefined; - [EVENT_ON_UNRESOLVED_ERROR]: undefined; - [EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined; - } -} diff --git a/_types/src/lib/src/hub/hub.d.ts b/_types/src/lib/src/hub/hub.d.ts deleted file mode 100644 index f608abfa..00000000 --- a/_types/src/lib/src/hub/hub.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { EventHub } from "octagonal-wheels/events"; -declare global { - interface LSEvents { - hello: string; - world: undefined; - } -} -export declare const eventHub: EventHub; diff --git a/_types/src/lib/src/index.d.ts b/_types/src/lib/src/index.d.ts deleted file mode 100644 index cab0387e..00000000 --- a/_types/src/lib/src/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { DirectFileManipulator, type DirectFileManipulatorOptions } from "./API/DirectFileManipulator.ts"; diff --git a/_types/src/lib/src/interfaces/AsyncActivityRunner.d.ts b/_types/src/lib/src/interfaces/AsyncActivityRunner.d.ts deleted file mode 100644 index 242f990a..00000000 --- a/_types/src/lib/src/interfaces/AsyncActivityRunner.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** Options attached to one bounded asynchronous activity. */ -export interface AsyncActivityOptions { - /** An optional diagnostic label supplied to the activity owner. */ - label?: string; -} -/** - * Runs bounded asynchronous work inside a consumer-owned activity scope. - * - * The common library deliberately does not prescribe what the scope does. A - * browser host may keep the screen awake, while a headless host may omit the - * runner and execute the task directly. - */ -export interface AsyncActivityRunner { - /** Runs the task and returns its result without changing its error semantics. */ - run(task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; -} -/** Runs a task through the injected activity owner, or directly when none is supplied. */ -export declare function runWithOptionalActivity(runner: AsyncActivityRunner | undefined, task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; diff --git a/_types/src/lib/src/interfaces/Confirm.d.ts b/_types/src/lib/src/interfaces/Confirm.d.ts deleted file mode 100644 index 7a651178..00000000 --- a/_types/src/lib/src/interfaces/Confirm.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export interface Confirm { - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} diff --git a/_types/src/lib/src/interfaces/DatabaseFileAccess.d.ts b/_types/src/lib/src/interfaces/DatabaseFileAccess.d.ts deleted file mode 100644 index 45c910c0..00000000 --- a/_types/src/lib/src/interfaces/DatabaseFileAccess.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix, LoadedEntry, MetaEntry, UXFileInfo, UXFileInfoStub } from "@lib/common/types"; -export interface DatabaseFileAccess { - delete: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string) => Promise; - store: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise; - storeAsConflictedRevision: (file: UXFileInfo, currentRev: string, skipCheck?: boolean) => Promise; - storeContent(path: FilePathWithPrefix, content: string): Promise; - createChunks: (file: UXFileInfo, force?: boolean, skipCheck?: boolean) => Promise; - hasContentInRevisionHistory: (file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string) => Promise; - fetch: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise; - fetchEntryFromMeta: (meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean) => Promise; - fetchEntryMeta: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean) => Promise; - fetchEntry: (file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean) => Promise; - getConflictedRevs: (file: UXFileInfoStub | FilePathWithPrefix) => Promise; -} diff --git a/_types/src/lib/src/interfaces/DatabaseRebuilder.d.ts b/_types/src/lib/src/interfaces/DatabaseRebuilder.d.ts deleted file mode 100644 index 2b1adba6..00000000 --- a/_types/src/lib/src/interfaces/DatabaseRebuilder.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export interface Rebuilder { - $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise; - $rebuildRemote(): Promise; - $rebuildEverything(): Promise; - $fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise; - $fetchLocalDBFast(autoResume: boolean): Promise; - scheduleRebuild(): Promise; - scheduleFetch(): Promise; - /** - * Declares the finish of the rebuild process and unlock remote, resume reflecting the changes. - */ - finishRebuild(): Promise; -} diff --git a/_types/src/lib/src/interfaces/FileHandler.d.ts b/_types/src/lib/src/interfaces/FileHandler.d.ts deleted file mode 100644 index 50d44abb..00000000 --- a/_types/src/lib/src/interfaces/FileHandler.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix, MetaEntry } from "@lib/common/models/db.type"; -import type { UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/models/fileaccess.type"; -export interface IFileHandler { - readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise; - storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise; - deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise; - renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise; - deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise; - resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise; - dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise; - dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise; - createAllChunks(showingNotice?: boolean): Promise; -} diff --git a/_types/src/lib/src/interfaces/KeyValueDatabase.d.ts b/_types/src/lib/src/interfaces/KeyValueDatabase.d.ts deleted file mode 100644 index cd5ab750..00000000 --- a/_types/src/lib/src/interfaces/KeyValueDatabase.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export interface KeyValueDatabase { - get(key: IDBValidKey): Promise; - set(key: IDBValidKey, value: T): Promise; - del(key: IDBValidKey): Promise; - clear(): Promise; - keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise; - close(): Promise; - destroy(): Promise; -} diff --git a/_types/src/lib/src/interfaces/ServiceModule.d.ts b/_types/src/lib/src/interfaces/ServiceModule.d.ts deleted file mode 100644 index afd68d6d..00000000 --- a/_types/src/lib/src/interfaces/ServiceModule.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { LogFunction } from "@lib/services/lib/logUtils"; -import type { ServiceHub } from "@lib/services/ServiceHub"; -import type { IServiceHub } from "@lib/services/base/IService"; -export interface ServiceModules { - storageAccess: StorageAccess; - /** - * Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc. - */ - databaseFileAccess: DatabaseFileAccess; - /** - * File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc. - */ - fileHandler: IFileHandler; - /** - * Rebuilder for handling database rebuilding operations. - */ - rebuilder: Rebuilder; -} -export type RequiredServices = Pick; -export type RequiredServiceModules = Pick; -export type RequiredServicesInterfaces = Pick; -export type RequiredServiceModulesInterfaces = Pick; -export type NecessaryServices = { - services: RequiredServices; - serviceModules: RequiredServiceModules; -}; -export type NecessaryServicesInterfaces = { - services: RequiredServicesInterfaces; - serviceModules: RequiredServiceModulesInterfaces; -}; -export type ServiceFeatureFunction = (host: NecessaryServices) => TR; -type ServiceFeatureContext = T & { - _log: LogFunction; -}; -export type ServiceFeatureFunctionWithContext = (host: NecessaryServices, context: ServiceFeatureContext) => TR; -/** - * Helper function to create a service feature with proper typing. - * @param featureFunction The feature function to be wrapped. - * @returns The same feature function with proper typing. - * @example - * const myFeatureDef = createServiceFeature(({ services: { API }, serviceModules: { storageAccess } }) => { - * // ... - * }); - * const myFeature = myFeatureDef.bind(null, this); // <- `this` may `ObsidianLiveSyncPlugin` or a custom context object - * appLifecycle.onLayoutReady(myFeature); - */ -export declare function createServiceFeature(featureFunction: ServiceFeatureFunction): ServiceFeatureFunction; -type ContextFactory = (host: NecessaryServices) => ServiceFeatureContext; -export declare function serviceFeature(): { - create(featureFunction: ServiceFeatureFunction): ServiceFeatureFunction; - withContext(ContextFactory: ContextFactory): { - create: (featureFunction: ServiceFeatureFunctionWithContext) => (host: NecessaryServices, context: ServiceFeatureContext) => TR; - }; -}; -export {}; diff --git a/_types/src/lib/src/interfaces/StorageAccess.d.ts b/_types/src/lib/src/interfaces/StorageAccess.d.ts deleted file mode 100644 index 23f185ca..00000000 --- a/_types/src/lib/src/interfaces/StorageAccess.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "@lib/common/types"; -import type { CustomRegExp } from "@lib/common/utils"; -import type { FileWithFileStat, FileWithStatAsProp } from "@lib/common/models/fileaccess.type"; -export interface IStorageAccessManager { - processWriteFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - processReadFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean; - recentlyTouched(file: FileWithStatAsProp | FileWithFileStat): boolean; - touch(file: FileWithStatAsProp | FileWithFileStat): void; - clearTouched(): void; -} -export interface StorageAccess { - normalisePath(path: string): string; - restoreState(): Promise; - deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise; - renameFile(file: UXFileInfoStub | FilePathWithPrefix, newPath: FilePathWithPrefix): Promise; - writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - readFileAuto(path: string): Promise; - readFileText(path: string): Promise; - isExists(path: string): Promise; - writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise; - stat(path: string): Promise; - statHidden(path: string): Promise; - removeHidden(path: string): Promise; - readHiddenFileAuto(path: string): Promise; - readHiddenFileBinary(path: string): Promise; - readHiddenFileText(path: string): Promise; - isExistsIncludeHidden(path: string): Promise; - ensureDir(path: string): Promise; - triggerFileEvent(event: string, path: string): void; - triggerHiddenFile(path: string): Promise; - getFileStub(path: string): Promise; - readStubContent(stub: UXFileInfoStub): Promise; - getStub(path: string): Promise; - getFiles(): Promise; - getFileNames(): Promise; - touched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - clearTouched(): void; - delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise; - trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise; - getFilesIncludeHidden(basePath: string, includeFilter?: CustomRegExp[], excludeFilter?: CustomRegExp[], skipFolder?: string[]): Promise; -} diff --git a/_types/src/lib/src/interfaces/StorageEventManager.d.ts b/_types/src/lib/src/interfaces/StorageEventManager.d.ts deleted file mode 100644 index ed9b7ee1..00000000 --- a/_types/src/lib/src/interfaces/StorageEventManager.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FileEventType, FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -export type FileEvent = { - type: FileEventType; - file: UXFileInfoStub | UXInternalFileInfoStub; - oldPath?: string; - cachedData?: string; - skipBatchWait?: boolean; - cancelled?: boolean; -}; -export declare abstract class StorageEventManager { - abstract beginWatch(): Promise; - abstract appendQueue(items: FileEvent[], ctx?: unknown): Promise; - abstract isWaiting(filename: FilePath): boolean; - abstract waitForIdle(): Promise; - abstract restoreState(): Promise; -} diff --git a/_types/src/lib/src/managers/ChangeManager.d.ts b/_types/src/lib/src/managers/ChangeManager.d.ts deleted file mode 100644 index 6661907a..00000000 --- a/_types/src/lib/src/managers/ChangeManager.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { FallbackWeakRef } from "octagonal-wheels/common/polyfill"; -/** - * Options for configuring the ChangeManager. - */ -export interface ChangeManagerOptions { - /** - * The PouchDB database instance to monitor for changes. - */ - database: PouchDB.Database; -} -export type ChangeManagerCallback = (change: PouchDB.Core.ChangesResponseChange) => void | Promise; -/** - * Manages and dispatches changes from a PouchDB database to registered callbacks. - * - * @template T The type of documents stored in the PouchDB database. - */ -export declare class ChangeManager { - /** - * The PouchDB database instance being monitored. - */ - _database: PouchDB.Database; - /** - * Creates a new instance of the ChangeManager. - * - * @param options - Configuration options for the ChangeManager. - */ - constructor(options: ChangeManagerOptions); - /** - * A list of registered callbacks wrapped in WeakRefs to avoid memory leaks. - */ - _callbacks: FallbackWeakRef>[]; - /** - * Registers a new callback to be invoked when a change occurs. - * - * @param callback - The callback function to register. - */ - addCallback(callback: ChangeManagerCallback): () => void; - removeCallback(callback: ChangeManagerCallback): void; - /** - * The PouchDB changes feed instance, if active. - */ - _changes?: PouchDB.Core.Changes; - /** - * Handles a change event from the PouchDB changes feed. - * - * @param changeResponse - The change response object from the PouchDB changes feed. - */ - _onChange(changeResponse: PouchDB.Core.ChangesResponseChange): Promise; - /** - * Sets up the PouchDB changes feed listener to monitor for database changes. - */ - setupListener(): void; - /** - * Tears down the PouchDB changes feed listener and cleans up resources. - */ - teardown(): void; - /** - * Restarts the PouchDB changes feed listener. - */ - restartWatch(): void; -} diff --git a/_types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts b/_types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts deleted file mode 100644 index 05d94f73..00000000 --- a/_types/src/lib/src/managers/ChunkDeliveryCoordinator.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -import type { DocumentID } from "@lib/common/types"; -/** - * A last-resort leak fuse for an accepted delivery claim which reports no - * observable progress. It releases logical ownership so a waiter, and an - * entered bounded activity with its Wake Lock and indicator, cannot be retained - * forever by a stalled transport or never-settling Promise. - * - * Five minutes is a conservative operational ceiling, not a normal - * chunk-arrival budget, evidence of remote absence, or a transport deadline. - * The transport contract cannot yet abort the underlying request when it fires. - */ -export declare const DEFAULT_CHUNK_DELIVERY_STALL_TIMEOUT_MS: number; -export type ActivityCountSource = Pick, "value" | "onChanged" | "offChanged">; -export type ChunkDeliveryClaimOptions = { - stallTimeoutMs?: number; - onStalled?: (ids: readonly DocumentID[]) => void; -}; -export type ChunkDeliveryChangeListener = (ids?: readonly DocumentID[]) => void; -/** A finite claim that keeps chunk arrival waiters paused until every owned identifier settles. */ -export interface ChunkDeliveryClaim { - readonly done: Promise; - readonly pendingIds: readonly DocumentID[]; - release(): void; - settle(id: DocumentID): void; - touch(): void; -} -/** - * Coordinates on-demand chunk ownership with broader finite remote activity. - * - * `ChunkFetcher` owns claims. `ArrivalWaitLayer` observes only whether an - * identifier may still be delivered, so it does not depend on a replicator - * service or on fetch scheduling details. - */ -export declare class ChunkDeliveryCoordinator { - private readonly finiteReplicationActivity?; - private readonly activeClaimCounts; - private readonly claimReleases; - private readonly listeners; - private finiteActivityWasActive; - private readonly finiteActivityChanged; - private disposed; - constructor(finiteReplicationActivity?: ActivityCountSource | undefined); - claim(ids: readonly DocumentID[], options?: ChunkDeliveryClaimOptions): ChunkDeliveryClaim; - isActivityActiveFor(id: DocumentID): boolean; - isClaimActiveFor(id: DocumentID): boolean; - isFiniteReplicationActive(): boolean; - onChanged(listener: ChunkDeliveryChangeListener): () => void; - dispose(): void; - private notifyChanged; -} diff --git a/_types/src/lib/src/managers/ChunkFetcher.d.ts b/_types/src/lib/src/managers/ChunkFetcher.d.ts deleted file mode 100644 index f6c0f209..00000000 --- a/_types/src/lib/src/managers/ChunkFetcher.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type DocumentID } from "@lib/common/types.ts"; -import { type ChunkManager } from "./ChunkManager.ts"; -import type { IReplicatorService, ISettingService } from "@lib/services/base/IService.ts"; -export declare const EVENT_MISSING_CHUNKS = "missingChunks"; -export declare const EVENT_MISSING_CHUNK_REMOTE = "missingChunkRemote"; -export declare const EVENT_CHUNK_FETCHED = "chunkFetched"; -export type ChunkFetcherOptions = { - settingService: ISettingService; - chunkManager: ChunkManager; - replicatorService: IReplicatorService; - deliveryStallTimeoutMs?: number; -}; -export declare class ChunkFetcher { - options: ChunkFetcherOptions; - get chunkManager(): ChunkManager; - queue: DocumentID[]; - private readonly pendingClaims; - private destroyed; - get interval(): number; - get concurrency(): number; - abort: AbortController; - constructor(options: ChunkFetcherOptions); - destroy(): void; - onEventHandler: (ids: DocumentID[]) => void; - onEvent(ids: DocumentID[]): void; - private ensureClaims; - private removePendingDelivery; - private onClaimStalled; - private settleClaim; - private touchClaims; - private waitForActivityBoundary; - /** - * Processing requests - */ - currentProcessing: number; - /** - * Time of the last request to the remote server. - * This is used to manage the interval between requests. - * Even if concurrency allows, every start of a request will ensure that the interval is respected. - */ - previousRequestTime: number; - canRequestMore(): boolean; - requestMissingChunks(): Promise; -} diff --git a/_types/src/lib/src/managers/ChunkManager.d.ts b/_types/src/lib/src/managers/ChunkManager.d.ts deleted file mode 100644 index 616e4dab..00000000 --- a/_types/src/lib/src/managers/ChunkManager.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { LayeredChunkManager } from "./LayeredChunkManager"; -export { LayeredChunkManager as ChunkManager }; diff --git a/_types/src/lib/src/managers/ConflictManager.d.ts b/_types/src/lib/src/managers/ConflictManager.d.ts deleted file mode 100644 index 86bbd2fe..00000000 --- a/_types/src/lib/src/managers/ConflictManager.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type Diff } from "diff-match-patch"; -import { type EntryDoc, type FilePathWithPrefix, type diff_result_leaf, type LoadedEntry, type DIFF_CHECK_RESULT_AUTO } from "@lib/common/types.ts"; -import type { EntryManager } from "@lib/managers/EntryManager/EntryManager.ts"; -import type { IPathService } from "@lib/services/base/IService.ts"; -type AutoMergeOutcomeOK = { - ok: DIFF_CHECK_RESULT_AUTO; -}; -type AutoMergeCanBeDoneByDeletingRev = { - result: string; - conflictedRev: string; -}; -type UserActionRequired = { - leftRev: string; - rightRev: string; - leftLeaf: diff_result_leaf | false; - rightLeaf: diff_result_leaf | false; -}; -export type AutoMergeResult = Promise; -export interface ConflictManagerOptions { - entryManager: EntryManager; - pathService: IPathService; - database: PouchDB.Database; -} -export declare class ConflictManager { - options: ConflictManagerOptions; - constructor(options: ConflictManagerOptions); - get database(): PouchDB.Database; - getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise; - mergeSensibly(path: FilePathWithPrefix, baseRev: string, currentRev: string, conflictedRev: string): Promise; - mergeObject(path: FilePathWithPrefix, baseRev: string, currentRev: string, conflictedRev: string): Promise; - tryAutoMergeSensibly(path: FilePathWithPrefix, test: LoadedEntry, conflicts: string[]): Promise; - tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult; -} -export {}; diff --git a/_types/src/lib/src/managers/EntryManager/EntryManager.d.ts b/_types/src/lib/src/managers/EntryManager/EntryManager.d.ts deleted file mode 100644 index 3c94752e..00000000 --- a/_types/src/lib/src/managers/EntryManager/EntryManager.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePathWithPrefix, type FilePath, type LoadedEntry, type EntryDoc, type SavingEntry, type MetaEntry } from "@lib/common/types"; -import type { ChunkManager } from "@lib/managers/ChunkManager"; -import type { ContentSplitter } from "@lib/ContentSplitter/ContentSplitters"; -import type { HashManager } from "@lib/managers/HashManager/HashManager"; -import type { GeneratedChunk } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { IPathService, ISettingService } from "@lib/services/base/IService"; -export interface EntryManagerOptions { - hashManager: HashManager; - chunkManager: ChunkManager; - splitter: ContentSplitter; - database: PouchDB.Database; - settingService: ISettingService; - pathService: IPathService; -} -export declare class EntryManager { - options: EntryManagerOptions; - constructor(options: EntryManagerOptions); - get localDatabase(): PouchDB.Database; - get hashManager(): HashManager; - get chunkManager(): ChunkManager; - get splitter(): ContentSplitter; - get serviceHost(): { - services: { - setting: ISettingService; - path: IPathService; - }; - serviceModules: {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type - }; - get isOnDemandChunkEnabled(): boolean; - isTargetFile(filenameSrc: string): boolean; - prepareChunk(piece: string): Promise; - getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise; - getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise; - getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise; - deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise; - putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise; -} diff --git a/_types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts b/_types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts deleted file mode 100644 index 1dcc5a61..00000000 --- a/_types/src/lib/src/managers/EntryManager/EntryManagerImpls.d.ts +++ /dev/null @@ -1,89 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SavingEntry, type DocumentID, type EntryDoc, type EntryBase, type FilePath, type FilePathWithPrefix, type LoadedEntry, type ObsidianLiveSyncSettings, type MetaEntry } from "@lib/common/types"; -import type { ContentSplitter } from "@lib/ContentSplitter/ContentSplitters"; -import type { HashManager } from "@lib/managers/HashManager/HashManager"; -import type { LayeredChunkManager as ChunkManager } from "@lib/managers/LayeredChunkManager"; -import type { NecessaryServicesInterfaces } from "@lib/interfaces/ServiceModule"; -import type { GeneratedChunk } from "@lib/pouchdb/LiveSyncLocalDB"; -type Managers = { - hashManager: HashManager; - chunkManager: ChunkManager; - splitter: ContentSplitter; - localDatabase: PouchDB.Database; -}; -type NecessaryManagers = Pick; -export declare function createChunks(managers: NecessaryManagers<"chunkManager" | "hashManager" | "splitter">, dispFilename: string, note: SavingEntry): Promise; -export declare function putDBEntry(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager" | "hashManager" | "splitter">, note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise; -export declare function isTargetFile(host: NecessaryServicesInterfaces<"setting", never>, filenameSrc: string): boolean; -export declare function prepareChunk({ chunkManager, hashManager }: NecessaryManagers<"chunkManager" | "hashManager">, piece: string): Promise; -export declare function getDBEntryMetaByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise; -export declare function isLegacyNote(meta: LoadedEntry | MetaEntry): meta is (import("@lib/common/types").DatabaseEntry & EntryBase & import("@lib/common/types").EntryWithEden & { - path: FilePathWithPrefix; - data: string | string[]; - type: import("../../common/models/db.type").EntryTypes["NOTE_LEGACY"]; -} & { - data: string | string[]; - datatype: import("@lib/common/types").EntryTypeNotes; -}) | (import("@lib/common/types").DatabaseEntry & EntryBase & import("@lib/common/types").EntryWithEden & { - path: FilePathWithPrefix; - data: string | string[]; - type: import("../../common/models/db.type").EntryTypes["NOTE_LEGACY"]; -} & { - children: string[]; -}); -export declare function canUseOnDemandChunking(settings: ObsidianLiveSyncSettings): boolean; -/** - * Decide how to retrieve chunks based on settings and waitForReady flag. - * `waitForReady` allows an already-observable finite delivery lifecycle to finish. - */ -export declare function computeChunkRetrievalMethod(waitForReady: boolean, settings: ObsidianLiveSyncSettings): { - waitForDelivery: boolean; - preventRemoteRequest: boolean; -}; -export declare function getDBEntryFromMeta(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase, chunkManager }: NecessaryManagers<"localDatabase" | "chunkManager">, meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise; -export declare function getDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, managers: NecessaryManagers<"localDatabase" | "chunkManager">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise; -export declare function deleteDBEntryByPath(host: NecessaryServicesInterfaces<"path" | "setting", never>, { localDatabase }: NecessaryManagers<"localDatabase">, path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise; -export {}; diff --git a/_types/src/lib/src/managers/HashManager/HashManager.d.ts b/_types/src/lib/src/managers/HashManager/HashManager.d.ts deleted file mode 100644 index 63da548b..00000000 --- a/_types/src/lib/src/managers/HashManager/HashManager.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -import { HashManagerCore, type HashManagerCoreOptions } from "./HashManagerCore.ts"; -/** - * Class for managing hash managers and performing hash calculations. - * Selects an appropriate manager according to the available hash algorithm. - */ -export declare class HashManager extends HashManagerCore { - /** - * Instance of the hash manager currently in use. - */ - manager: HashManagerCore; - /** - * Checks whether the specified hash algorithm is available. - * - * @param hashAlg The hash algorithm to check - * @returns True if available - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Selects and initialises an available hash manager. - * - * @returns True if initialisation is successful - * @throws Throws an error if no available manager exists - */ - setManager(): Promise; - /** - * Constructs a new HashManager. - * - * @param options Initialisation options - */ - constructor(options: HashManagerCoreOptions); - /** - * Initialises the hash manager. - * - * @returns True if initialisation is successful - * @throws Throws an error if initialisation fails - */ - processInitialise(): Promise; - /** - * Computes the hash value for the specified string. - * - * @param piece The string to be hashed - * @returns The hash value (returned as a Promise) - */ - computeHash(piece: string): Promise; - /** - * Computes the hash value without encryption. - * - * @param piece The string to be hashed - * @returns The hash value (returned as a Promise) - */ - computeHashWithoutEncryption(piece: string): Promise; - /** - * Computes the hash value with encryption. - * - * @param piece The string to be hashed - * @returns The hash value (returned as a Promise) - */ - computeHashWithEncryption(piece: string): Promise; -} diff --git a/_types/src/lib/src/managers/HashManager/HashManagerCore.d.ts b/_types/src/lib/src/managers/HashManager/HashManagerCore.d.ts deleted file mode 100644 index 9c7ab7ac..00000000 --- a/_types/src/lib/src/managers/HashManager/HashManagerCore.d.ts +++ /dev/null @@ -1,116 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ISettingService } from "@lib/services/base/IService.ts"; -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -/** - * Prefix for encrypted hashes. - * - * This constant is prepended to hash strings when encryption is enabled. - */ -export declare const HashEncryptedPrefix = "+"; -/** - * Options for initialising {@link HashManagerCore}. - */ -export type HashManagerCoreOptions = { - /** - * Remote database settings used for hash management. - */ - settingService: ISettingService; -}; -/** - * Abstract base class for hash management. - * - * Provides core logic for handling passphrase hashing, encryption toggling, - * and initialisation routines. Subclasses should implement encryption-specific - * hash computation methods. - */ -export declare abstract class HashManagerCore { - protected settingService: ISettingService; - /** - * Indicates whether encryption is enabled for hash computation. - */ - useEncryption: boolean; - /** - * Hashed passphrase as a string, used for hash operations. - */ - hashedPassphrase: string; - /** - * Hashed passphrase as a 32-bit number, used for hash operations. - */ - hashedPassphrase32: number; - /** - * Options used for initialisation and configuration. - */ - options: HashManagerCoreOptions; - /** - * Constructs a new {@link HashManagerCore} instance. - * - * @param options - Configuration options for hash management. - */ - constructor(options: HashManagerCoreOptions); - /** - * Applies the given options to the hash manager. - * - * Updates encryption settings and computes passphrase hashes. - * - * @param options - Optional configuration to apply. - */ - applyOptions(options?: HashManagerCoreOptions): void; - /** - * Performs initialisation logic specific to the hash manager implementation. - * - * Subclasses must implement this method. - * - * @returns Promise resolving to true if initialisation succeeds. - */ - abstract processInitialise(): Promise; - /** - * Task representing the initialisation process. - */ - initialiseTask?: Promise; - /** - * Ensures the hash manager is initialised. - * - * Returns a promise that resolves when initialisation is complete. - * - * @returns Promise resolving to true if initialisation succeeds. - */ - initialise(): Promise; - /** - * Computes a hash for the given string. - * - * If encryption is enabled, the hash is computed with encryption and prefixed. - * Otherwise, a plain hash is computed. - * - * @param piece - The input string to hash. - * @returns Promise resolving to the computed hash string. - */ - computeHash(piece: string): Promise; - /** - * Computes a hash for the given string without encryption. - * - * Subclasses must implement this method. - * - * @param piece - The input string to hash. - * @returns Promise resolving to the computed hash string. - */ - abstract computeHashWithoutEncryption(piece: string): Promise; - /** - * Computes a hash for the given string with encryption. - * - * Subclasses must implement this method. - * - * @param piece - The input string to hash. - * @returns Promise resolving to the computed encrypted hash string. - */ - abstract computeHashWithEncryption(piece: string): Promise; - /** - * Determines whether the hash manager is available for the specified algorithm. - * - * Subclasses should override this method to indicate supported algorithms. - * - * @param hashAlg - The hash algorithm to check. - * @returns True if available, false otherwise. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; -} diff --git a/_types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts b/_types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts deleted file mode 100644 index 37c59c78..00000000 --- a/_types/src/lib/src/managers/HashManager/PureJSHashManager.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { HashManagerCore } from "./HashManagerCore.ts"; -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -/** - * Provides hash management using the "mixed-purejs" algorithm. - * - * This manager utilises a pure JavaScript implementation for hashing. - * It is available only when the hash algorithm is set to "mixed-purejs". - */ -export declare class PureJSHashManager extends HashManagerCore { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg The hash algorithm to check. - * @returns True if the algorithm is "mixed-purejs". - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Initialises the hash manager. - * @returns Always resolves to true. - */ - processInitialise(): Promise; - /** - * Computes a hash for the given input, including encryption. - * @param input The input string to hash. - * @returns The computed hash as a promise. - */ - computeHashWithEncryption(input: string): Promise; - /** - * Computes a hash for the given input, without encryption. - * @param input The input string to hash. - * @returns The computed hash as a promise. - */ - computeHashWithoutEncryption(input: string): Promise; -} -/** - * Provides hash management using the "sha1" algorithm. - * - * This manager utilises a pure JavaScript SHA-1 implementation. - * It is available only when the hash algorithm is set to "sha1". - */ -export declare class SHA1HashManager extends HashManagerCore { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg The hash algorithm to check. - * @returns True if the algorithm is "sha1". - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Initialises the hash manager. - * @returns Always resolves to true. - */ - processInitialise(): Promise; - /** - * Computes a SHA-1 hash for the given input, including encryption. - * @param input The input string to hash. - * @returns The computed SHA-1 hash as a promise. - */ - computeHashWithEncryption(input: string): Promise; - /** - * Computes a SHA-1 hash for the given input, without encryption. - * @param input The input string to hash. - * @returns The computed SHA-1 hash as a promise. - */ - computeHashWithoutEncryption(input: string): Promise; -} -/** - * Fallback hash manager using the pure JavaScript implementation. - * - * This manager is always available and acts as a fallback when no specific algorithm matches. - */ -export declare class FallbackPureJSHashManager extends PureJSHashManager { - /** - * Always returns true, indicating this manager is available for any algorithm. - * @param _hashAlg The hash algorithm (ignored). - * @returns True. - */ - static isAvailableFor(_hashAlg: HashAlgorithm): boolean; -} diff --git a/_types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts b/_types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts deleted file mode 100644 index 752efede..00000000 --- a/_types/src/lib/src/managers/HashManager/XXHashHashManager.d.ts +++ /dev/null @@ -1,98 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { HashManagerCore, type HashManagerCoreOptions } from "./HashManagerCore.ts"; -import type { XXHashAPI } from "xxhash-wasm-102"; -import type { HashAlgorithm } from "@lib/common/models/setting.type.ts"; -/** - * Abstract base class for hash managers using XXHash algorithms. - * Provides initialisation and common properties for XXHash-based managers. - */ -export declare abstract class XXHashHashManager extends HashManagerCore { - /** - * Instance of XXHash API used for hashing operations. - */ - xxhash: XXHashAPI; - /** - * Constructs a new XXHashHashManager. - * @param options - Options for the hash manager core. - */ - constructor(options: HashManagerCoreOptions); - /** - * Initialises the XXHash API instance. - * @returns A promise resolving to true when initialisation is complete. - */ - processInitialise(): Promise; -} -/** - * Hash manager for the legacy hash algorithm (empty string). - * Utilises XXHash32 raw hashing. - */ -export declare class XXHash32RawHashManager extends XXHashHashManager { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg - The hash algorithm to check. - * @returns True if available, false otherwise. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Computes a hash for the given piece using encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithEncryption(piece: string): Promise; - /** - * Computes a hash for the given piece without encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithoutEncryption(piece: string): Promise; -} -/** - * Hash manager for the XXHash64 algorithm ("xxhash64"). - */ -export declare class XXHash64HashManager extends XXHashHashManager { - /** - * Determines whether this manager is available for the specified algorithm. - * @param hashAlg - The hash algorithm to check. - * @returns True if available, false otherwise. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Computes a hash for the given piece using encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithEncryption(piece: string): Promise; - /** - * Computes a hash for the given piece without encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithoutEncryption(piece: string): Promise; -} -/** - * Fallback hash manager utilising XXHash32. - * Used when no specific algorithm is matched. - * Please be careful with this manager, as it is different from XXHash32RawHashManager. - */ -export declare class FallbackWasmHashManager extends XXHashHashManager { - /** - * Determines whether this manager is available for the specified algorithm. - * Always returns true as a fallback. - * @param hashAlg - The hash algorithm to check. - * @returns True. - */ - static isAvailableFor(hashAlg: HashAlgorithm): boolean; - /** - * Computes a hash for the given piece using encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithEncryption(piece: string): Promise; - /** - * Computes a hash for the given piece without encryption. - * @param piece - The input string to hash. - * @returns A promise resolving to the hash string. - */ - computeHashWithoutEncryption(piece: string): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager.d.ts b/_types/src/lib/src/managers/LayeredChunkManager.d.ts deleted file mode 100644 index 263e9c09..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryDoc, EntryLeaf } from "@lib/common/types.ts"; -import type { ChangeManager } from "@lib/managers/ChangeManager.ts"; -import type { ChunkManagerEventMap, ChunkManagerOptions, ChunkReadOptions, ChunkWriteOptions, WriteResult } from "./LayeredChunkManager/types.ts"; -import { ChunkDeliveryCoordinator } from "./ChunkDeliveryCoordinator.ts"; -/** - * ChunkManager class that manages chunk operations such as reading, writing, and caching. - * Now uses a middleware layer architecture for read and write operations. - */ -export declare class LayeredChunkManager { - protected options: ChunkManagerOptions; - protected eventTarget: EventTarget; - private cacheLayer; - private databaseReadLayer; - private readLayers; - private writeLayers; - private arrivalWaitLayer; - readonly deliveryCoordinator: ChunkDeliveryCoordinator; - get changeManager(): ChangeManager; - get database(): PouchDB.Database; - get cacheStatistics(): { - size: number; - allocCount: number; - derefCount: number; - }; - addListener(type: K, listener: (this: LayeredChunkManager, ev: ChunkManagerEventMap[K]) => void, options?: boolean | AddEventListenerOptions): () => void; - emitEvent(type: K, detail: ChunkManagerEventMap[K]): void; - protected abort: AbortController; - protected offChangeHandler: ReturnType; - protected initialised: Promise; - _initialise(): Promise; - constructor(options: ChunkManagerOptions); - destroy(): void; - getCachedChunk(id: DocumentID): EntryLeaf | false; - getChunkIDFromCache(data: string): DocumentID | false; - cacheChunk(chunk: EntryLeaf): void; - clearCaches(): void; - read(ids: DocumentID[], options: ChunkReadOptions, preloadedChunks?: Record): Promise<(EntryLeaf | false)[]>; - private executeReadPipeline; - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID): Promise; - private executeWritePipeline; - private isChunkDoc; - private onChunkArrived; - protected onChunkArrivedHandler: (doc: EntryLeaf, deleted?: boolean) => void; - private onChange; - protected onChangeHandler: (change: PouchDB.Core.ChangesResponseChange) => void; - onMissingChunkRemote(id: DocumentID): void; - protected onMissingChunkRemoteHandler: (id: DocumentID) => void; - protected concurrentTransactions: number; - protected stabilised: Promise; - transaction(callback: () => Promise): Promise; - _stabilise(): Promise; - __stabilise(): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts deleted file mode 100644 index cea41834..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/ArrivalWaitLayer.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryLeaf } from "@lib/common/types"; -import type { IReadLayer } from "./ChunkLayerInterfaces"; -import type { ChunkReadOptions } from "./types.ts"; -import { ChunkDeliveryCoordinator } from "@lib/managers/ChunkDeliveryCoordinator.ts"; -export type ChunkAvailabilityRecheck = (ids: readonly DocumentID[]) => Promise; -/** - * Waits only for a delivery lifecycle which is already observable when the - * local miss is handled. It does not guess at an arrival delay. - */ -export declare class ArrivalWaitLayer implements IReadLayer { - private readonly recheckAvailability?; - private readonly waitingMap; - private readonly eventEmitter; - private readonly deliveryCoordinator; - private readonly ownsDeliveryCoordinator; - private readonly stopObservingActivity; - constructor(eventEmitter: (eventName: string, data: DocumentID[]) => void, deliveryCoordinator?: ChunkDeliveryCoordinator, recheckAvailability?: ChunkAvailabilityRecheck | undefined); - private enqueueWaiting; - private settle; - private settleAfterObservedActivity; - private refreshActivity; - /** Handle a chunk document becoming available. */ - onChunkArrived(doc: EntryLeaf, deleted?: boolean): void; - /** Handle an explicit remote-missing result. */ - onMissingChunk(id: DocumentID): void; - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; - clearWaiting(): void; - tearDown(): void; - getWaitingCount(): number; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts deleted file mode 100644 index ac695273..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/CacheLayer.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryLeaf } from "@lib/common/types"; -import type { IReadLayer, IWriteLayer } from "./ChunkLayerInterfaces"; -import type { ChunkReadOptions, ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Cache layer - manages in-memory cache of chunks. - * Implements both IReadLayer and IWriteLayer for unified cache management. - * This layer is self-contained and handles cache operations for both read and write operations. - */ -export declare class CacheLayer implements IReadLayer, IWriteLayer { - private caches; - private maxCacheSize; - allocCount: number; - derefCount: number; - constructor(maxCacheSize: number); - /** - * Get a cached chunk - */ - getCachedChunk(id: DocumentID): EntryLeaf | false; - /** - * Find chunk ID by data content - */ - getChunkIDFromCache(data: string): DocumentID | false; - /** - * Cache a chunk - */ - cacheChunk(chunk: EntryLeaf): void; - /** - * Reorder chunk for LRU (move to end) - */ - reorderChunk(id: DocumentID): void; - /** - * Delete a cached chunk - */ - deleteCachedChunk(id: DocumentID): void; - /** - * Clear all caches - */ - clearCaches(): void; - /** - * Tear down the layer (clear caches on shutdown) - */ - tearDown(): void; - /** - * Get current cache statistics - */ - getStatistics(): { - size: number; - allocCount: number; - derefCount: number; - }; - /** - * IReadLayer implementation - read from cache - */ - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; - /** - * IWriteLayer implementation - cache chunks after database write - */ - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts deleted file mode 100644 index 72e1d071..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/ChunkLayerInterfaces.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryLeaf } from "@lib/common/types.ts"; -import type { ChunkReadOptions, ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Interface for read layers in the chunk reading pipeline. - * Each layer processes a chunk read request and passes it to the next layer. - */ -export interface IReadLayer { - /** - * Process a read request for the given chunk IDs. - * The next layer in the pipeline should be called to continue processing. - * - * @param ids - The chunk IDs to read - * @param options - Read options - * @param next - The next layer to process the remaining IDs - * @returns A promise that resolves to an array of chunks or false values - */ - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; - tearDown?(): void; -} -/** - * Interface for write layers in the chunk writing pipeline. - * Each layer processes chunk write requests and passes them to the next layer. - */ -export interface IWriteLayer { - /** - * Process a write request for the given chunks. - * The next layer in the pipeline should be called to continue processing. - * - * @param chunks - The chunks to write - * @param options - Write options - * @param origin - The origin of the write request - * @param next - The next layer to process the remaining chunks - * @returns A promise that resolves to the write result - */ - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; - tearDown?(): void; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts deleted file mode 100644 index 13775b21..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseReadLayer.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryLeaf, DocumentID, EntryDoc } from "@lib/common/types"; -import type { IReadLayer } from "./ChunkLayerInterfaces"; -import type { ChunkReadOptions } from "./types.ts"; -/** - * Database read layer - reads chunks from the database - */ -export declare class DatabaseReadLayer implements IReadLayer { - private database; - constructor(database: PouchDB.Database); - private isChunkDoc; - private getError; - private isMissingError; - read(ids: DocumentID[], options: ChunkReadOptions, next: (remaining: DocumentID[]) => Promise<(EntryLeaf | false)[]>): Promise<(EntryLeaf | false)[]>; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts deleted file mode 100644 index cfab49e4..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/DatabaseWriteLayer.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryLeaf, DocumentID, EntryDoc } from "@lib/common/types"; -import type { IWriteLayer } from "./ChunkLayerInterfaces"; -import type { ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Database write layer - writes chunks to the database - */ -export declare class DatabaseWriteLayer implements IWriteLayer { - private database; - constructor(database: PouchDB.Database); - write(chunks: EntryLeaf[], options: ChunkWriteOptions | undefined, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts deleted file mode 100644 index 9b39ef5e..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/HotPackLayer.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryLeaf, DocumentID } from "@lib/common/types"; -import type { IWriteLayer } from "./ChunkLayerInterfaces"; -import type { ChunkWriteOptions, WriteResult } from "./types.ts"; -/** - * Hot pack layer - placeholder for hot pack processing - */ -export declare class HotPackLayer implements IWriteLayer { - write(chunks: EntryLeaf[], options: ChunkWriteOptions, origin: DocumentID, next: (remaining: EntryLeaf[]) => Promise): Promise; -} diff --git a/_types/src/lib/src/managers/LayeredChunkManager/types.d.ts b/_types/src/lib/src/managers/LayeredChunkManager/types.d.ts deleted file mode 100644 index caedc001..00000000 --- a/_types/src/lib/src/managers/LayeredChunkManager/types.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryDoc } from "@lib/common/models/db.definition"; -import type { DocumentID, EntryLeaf } from "@lib/common/models/db.type"; -import type { ISettingService } from "@lib/services/base/IService"; -import type { ChangeManager } from "@lib/managers/ChangeManager"; -import type { EVENT_CHUNK_FETCHED, EVENT_MISSING_CHUNK_REMOTE, EVENT_MISSING_CHUNKS } from "@lib/managers/ChunkFetcher"; -import type { ActivityCountSource } from "@lib/managers/ChunkDeliveryCoordinator"; -export type ChunkManagerOptions = { - database: PouchDB.Database; - changeManager: ChangeManager; - settingService: ISettingService; - /** Finite replication which may still deliver a requested chunk. */ - finiteReplicationActivity?: ActivityCountSource; -}; -export type ChunkReadOptions = { - skipCache?: boolean; - /** Wait for an already-observable finite delivery lifecycle. */ - waitForDelivery?: boolean; - /** @deprecated Use `waitForDelivery`. Positive values no longer represent an arrival duration. */ - timeout?: number; - preventRemoteRequest?: boolean; -}; -export type ChunkWriteOptions = { - skipCache?: boolean; - force?: boolean; -}; -export type WriteResult = { - result: boolean; - processed: { - cached: number; - hotPack: number; - written: number; - duplicated: number; - }; -}; -export type ChunkManagerEventMap = { - [EVENT_MISSING_CHUNK_REMOTE]: DocumentID; - [EVENT_MISSING_CHUNKS]: DocumentID[]; - [EVENT_CHUNK_FETCHED]: EntryLeaf; -}; diff --git a/_types/src/lib/src/managers/LiveSyncManagers.d.ts b/_types/src/lib/src/managers/LiveSyncManagers.d.ts deleted file mode 100644 index d6476441..00000000 --- a/_types/src/lib/src/managers/LiveSyncManagers.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc } from "@lib/common/types"; -import { ContentSplitter } from "@lib/ContentSplitter/ContentSplitters.ts"; -import { ChangeManager } from "@lib/managers/ChangeManager.ts"; -import { ChunkFetcher } from "@lib/managers/ChunkFetcher.ts"; -import { ChunkManager } from "@lib/managers/ChunkManager.ts"; -import { ConflictManager } from "@lib/managers/ConflictManager.ts"; -import { EntryManager } from "@lib/managers/EntryManager/EntryManager.ts"; -import { HashManager } from "@lib/managers/HashManager/HashManager.ts"; -import type { APIService } from "@lib/services/base/APIService.ts"; -import type { IDatabaseService, IPathService, IReplicatorService, ISettingService } from "@lib/services/base/IService.ts"; -import { type LogFunction } from "@lib/services/lib/logUtils.ts"; -export interface LiveSyncManagersOptions { - database: PouchDB.Database; - databaseService: IDatabaseService; - settingService: TSettingService; - pathService: IPathService; - replicatorService: IReplicatorService; - APIService: APIService; -} -export declare class LiveSyncManagers { - protected _pathService: IPathService; - protected _replicatorService: IReplicatorService; - protected _settingService: ISettingService; - protected _APIService: APIService; - hashManager: HashManager; - chunkFetcher: ChunkFetcher; - changeManager: ChangeManager; - chunkManager: ChunkManager; - splitter: ContentSplitter; - entryManager: EntryManager; - conflictManager: ConflictManager; - protected options: LiveSyncManagersOptions; - protected log: LogFunction; - constructor(options: LiveSyncManagersOptions); - teardownManagers(): Promise; - protected getManagerMembers(): { - changeManager: ChangeManager; - hashManager: HashManager; - splitter: ContentSplitter; - chunkManager: ChunkManager; - chunkFetcher: ChunkFetcher; - entryManager: EntryManager; - conflictManager: ConflictManager; - }; - initialise(): Promise; - reinitialise(): Promise; - clearCaches(): void; - prepareHashFunction(): Promise; -} diff --git a/_types/src/lib/src/managers/StorageEventManager.d.ts b/_types/src/lib/src/managers/StorageEventManager.d.ts deleted file mode 100644 index e7dcc1a8..00000000 --- a/_types/src/lib/src/managers/StorageEventManager.d.ts +++ /dev/null @@ -1,137 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FileEventType, type FilePath, type UXFileInfoStub, type UXFolderInfo, type UXInternalFileInfoStub } from "@lib/common/types.ts"; -import { type FileEventItem } from "@lib/common/types.ts"; -import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess.ts"; -import { type PromiseWithResolvers } from "octagonal-wheels/promises"; -import { StorageEventManager, type FileEvent } from "@lib/interfaces/StorageEventManager.ts"; -import type { IAPIService, IVaultService } from "@lib/services/base/IService.ts"; -import type { SettingService } from "@lib/services/base/SettingService.ts"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService.ts"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -import type { IStorageEventManagerAdapter } from "./adapters"; -import { type CompatTimeoutHandle } from "@lib/common/coreEnvFunctions"; -type WaitInfo = { - since: number; - type: FileEventType; - canProceed: PromiseWithResolvers; - timerHandler: CompatTimeoutHandle; - event: FileEventItem; -}; -declare const TYPE_SENTINEL_FLUSH = "SENTINEL_FLUSH"; -type FileEventItemSentinelFlush = { - type: typeof TYPE_SENTINEL_FLUSH; -}; -export type FileEventItemSentinel = FileEventItemSentinelFlush; -export interface StorageEventManagerBaseDependencies { - setting: SettingService; - vaultService: IVaultService; - fileProcessing: FileProcessingService; - storageAccessManager: IStorageAccessManager; - APIService: IAPIService; -} -/** - * Type helper to extract the file type from a storage event manager adapter - */ -export type ExtractFile = T extends IStorageEventManagerAdapter ? F : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the folder type from a storage event manager adapter - */ -export type ExtractFolder = T extends IStorageEventManagerAdapter ? D : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Base class for storage event management - * Uses adapter pattern for platform-specific implementations - * - * @template TAdapter - The storage event manager adapter type - */ -export declare abstract class StorageEventManagerBase> extends StorageEventManager { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - _log: ReturnType; - protected setting: SettingService; - protected vaultService: IVaultService; - protected fileProcessing: FileProcessingService; - protected storageAccess: IStorageAccessManager; - protected adapter: TAdapter; - protected get shouldBatchSave(): boolean; - protected get batchSaveMinimumDelay(): number; - protected get batchSaveMaximumDelay(): number; - get settings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - constructor(adapter: TAdapter, dependencies: StorageEventManagerBaseDependencies); - _saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise; - _loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null>; - isFolder(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | ExtractFolder | ExtractFile): boolean; - isFile(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | ExtractFolder | ExtractFile): boolean; - protected updateStatus(): void; - /** - * Snapshot restoration promise. - * Snapshot will be restored before starting to watch vault changes. - * In designed time, this has been called from Initialisation process, which has been implemented on `ModuleInitializerFile.ts`. - */ - snapShotRestored: Promise | null; - /** - * Restore the previous snapshot if exists. - * @returns - */ - restoreState(): Promise; - appendQueue(params: FileEvent[], ctx?: unknown): Promise; - protected bufferedQueuedItems: (FileEventItem | FileEventItemSentinel)[]; - enqueue(newItem: FileEventItem): void; - /** - * Immediately take snapshot. - */ - private _triggerTakeSnapshot; - /** - * Trigger taking snapshot after throttled period. - */ - triggerTakeSnapshot: import("octagonal-wheels/function").ThrottledFunction<() => void>; - protected concurrentProcessing: import("octagonal-wheels/concurrency/semaphore_v2").SemaphoreObject; - protected _waitingMap: Map; - private _waitForIdle; - /** - * Wait until all queued events are processed. - * Subsequent new events will not be waited, but new events will not be added. - * @returns - */ - waitForIdle(): Promise; - /** - * Proceed waiting for the given key immediately. - */ - private _proceedWaiting; - /** - * Cancel waiting for the given key. - */ - private _cancelWaiting; - /** - * Add waiting for the given key. - * @param key - * @param event - * @param waitedSince Optional waited since timestamp to calculate the remaining delay. - */ - private _addWaiting; - /** - * Process the given file event. - */ - processFileEvent(fei: FileEventItem): Promise; - _takeSnapshot(): Promise; - _restoreFromSnapshot(): Promise; - protected runQueuedEvents(): Promise; - protected processingCount: number; - protected requestProcessQueue(fei: FileEventItem): Promise; - isWaiting(filename: FilePath): boolean; - protected handleFileEvent(queue: FileEventItem): Promise; - protected cancelRelativeEvent(item: FileEventItem): void; - /** - * Begin watching for storage events - */ - beginWatch(): Promise; - /** - * Platform-agnostic event handlers - */ - protected watchEditorChange(editor: TEditor, info: TInfo): void; - protected watchVaultCreate(file: TFile, ctx?: TCtx): void; - protected watchVaultChange(file: TFile, ctx?: TCtx): void; - protected watchVaultDelete(file: TFile, ctx?: TCtx): void; - protected watchVaultRename(file: TFile, oldPath: string, ctx?: TCtx): void; - protected watchVaultRawEvents(path: FilePath): void; - protected _watchVaultRawEvents(path: FilePath): Promise; -} -export {}; diff --git a/_types/src/lib/src/managers/StorageProcessingManager.d.ts b/_types/src/lib/src/managers/StorageProcessingManager.d.ts deleted file mode 100644 index 8eb8bff1..00000000 --- a/_types/src/lib/src/managers/StorageProcessingManager.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix } from "@lib/common/models/db.type"; -import type { UXFileInfoStub } from "@lib/common/models/fileaccess.type"; -import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess"; -import type { FileWithFileStat, FileWithStatAsProp } from "@lib/common/models/fileaccess.type"; -export declare class StorageAccessManager implements IStorageAccessManager { - processingFiles: Set; - processWriteFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - processReadFile(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise): Promise; - isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean; - private touchedFiles; - touch(file: FileWithFileStat | FileWithStatAsProp): void; - recentlyTouched(file: FileWithStatAsProp | FileWithFileStat): boolean; - clearTouched(): void; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts deleted file mode 100644 index fb0da087..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventConverterAdapter.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -/** - * Adapter interface for converting platform-specific file types to UX types - * - * @template TFile - Platform-specific file type - */ -export interface IStorageEventConverterAdapter { - /** - * Convert platform-specific file to UXFileInfoStub - */ - toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub; - /** - * Convert path to UXInternalFileInfoStub - */ - toInternalFileInfo(path: FilePath): UXInternalFileInfoStub; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts deleted file mode 100644 index 3aac1c44..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventManagerAdapter.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IStorageEventTypeGuardAdapter } from "./IStorageEventTypeGuardAdapter"; -import type { IStorageEventPersistenceAdapter } from "./IStorageEventPersistenceAdapter"; -import type { IStorageEventWatchAdapter } from "./IStorageEventWatchAdapter"; -import type { IStorageEventStatusAdapter } from "./IStorageEventStatusAdapter"; -import type { IStorageEventConverterAdapter } from "./IStorageEventConverterAdapter"; -/** - * Composite adapter interface for StorageEventManager - * - * @template TFile - Platform-specific file type - * @template TFolder - Platform-specific folder type - */ -export interface IStorageEventManagerAdapter { - readonly typeGuard: IStorageEventTypeGuardAdapter; - readonly persistence: IStorageEventPersistenceAdapter; - readonly watch: IStorageEventWatchAdapter; - readonly status: IStorageEventStatusAdapter; - readonly converter: IStorageEventConverterAdapter; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts deleted file mode 100644 index 1c82860d..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventPersistenceAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FileEventItem } from "@lib/common/types"; -import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager"; -/** - * Adapter interface for snapshot persistence operations - */ -export interface IStorageEventPersistenceAdapter { - /** - * Save the snapshot of pending events - */ - saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise; - /** - * Load the snapshot of pending events - */ - loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null>; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts deleted file mode 100644 index b3a8a892..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventStatusAdapter.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Adapter interface for status update operations - */ -export interface IStorageEventStatusAdapter { - /** - * Update the status display - */ - updateStatus(status: { - batched: number; - processing: number; - totalQueued: number; - }): void; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts deleted file mode 100644 index c402f5b3..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventTypeGuardAdapter.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Adapter interface for type guard operations in StorageEventManager - * - * @template TFile - Platform-specific file type - * @template TFolder - Platform-specific folder type - */ -export interface IStorageEventTypeGuardAdapter { - /** - * Check if the given item is a file - */ - isFile(file: unknown): file is TFile; - /** - * Check if the given item is a folder - */ - isFolder(item: unknown): item is TFolder; -} diff --git a/_types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts b/_types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts deleted file mode 100644 index 07394791..00000000 --- a/_types/src/lib/src/managers/adapters/IStorageEventWatchAdapter.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types"; -/** - * Event handlers for storage events - */ -export interface IStorageEventWatchHandlers { - onCreate: (file: TFile, ctx?: TCtx) => void; - onChange: (file: TFile, ctx?: TCtx) => void; - onDelete: (file: TFile, ctx?: TCtx) => void; - onRename: (file: TFile, oldPath: string, ctx?: TCtx) => void; - onRaw: (path: FilePath) => void; - onEditorChange?: (editor: TEditor, info: TInfo) => void; -} -/** - * Adapter interface for watching vault/storage events - */ -export interface IStorageEventWatchAdapter { - /** - * Begin watching for storage events - */ - beginWatch(handlers: IStorageEventWatchHandlers): Promise; -} diff --git a/_types/src/lib/src/managers/adapters/index.d.ts b/_types/src/lib/src/managers/adapters/index.d.ts deleted file mode 100644 index a7420210..00000000 --- a/_types/src/lib/src/managers/adapters/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { IStorageEventTypeGuardAdapter } from "./IStorageEventTypeGuardAdapter"; -export type { IStorageEventPersistenceAdapter } from "./IStorageEventPersistenceAdapter"; -export type { IStorageEventWatchAdapter, IStorageEventWatchHandlers } from "./IStorageEventWatchAdapter"; -export type { IStorageEventStatusAdapter } from "./IStorageEventStatusAdapter"; -export type { IStorageEventConverterAdapter } from "./IStorageEventConverterAdapter"; -export type { IStorageEventManagerAdapter } from "./IStorageEventManagerAdapter"; diff --git a/_types/src/lib/src/mock_and_interop/stores.d.ts b/_types/src/lib/src/mock_and_interop/stores.d.ts deleted file mode 100644 index 810ad2f0..00000000 --- a/_types/src/lib/src/mock_and_interop/stores.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LOG_LEVEL } from "@lib/common/types.ts"; -export type LockStats = { - pending: string[]; - running: string[]; - count: number; -}; -export declare const lockStats: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<{ - pending: never[]; - running: never[]; - count: number; -}>; -export declare const collectingChunks: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export declare const pluginScanningCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export declare const hiddenFilesProcessingCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export declare const hiddenFilesEventCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -export type LogEntry = { - message: string | Error; - level?: LOG_LEVEL; - key?: string; -}; -export declare const logMessages: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; diff --git a/_types/src/lib/src/mock_and_interop/wrapper.d.ts b/_types/src/lib/src/mock_and_interop/wrapper.d.ts deleted file mode 100644 index f31cc6b8..00000000 --- a/_types/src/lib/src/mock_and_interop/wrapper.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare class WrappedNotice { - constructor(message: string | DocumentFragment, timeout?: number); - setMessage(message: string | DocumentFragment): this; - hide(): void; -} -export declare function setNoticeClass(notice: typeof WrappedNotice): void; -export declare function NewNotice(message: string | DocumentFragment, timeout?: number): WrappedNotice; diff --git a/_types/src/lib/src/mods.d.ts b/_types/src/lib/src/mods.d.ts deleted file mode 100644 index d70f75ac..00000000 --- a/_types/src/lib/src/mods.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function getWebCrypto(): Promise; diff --git a/_types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts b/_types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts deleted file mode 100644 index 3c744468..00000000 --- a/_types/src/lib/src/pouchdb/LiveSyncDBFunctions.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type EntryMilestoneInfo, type RemoteDBSettings, type ChunkVersionRange, type TweakValues, type DeviceInfo } from "@lib/common/types.ts"; -export type ENSURE_DB_RESULT = "OK" | "INCOMPATIBLE" | "LOCKED" | "NODE_LOCKED" | "NODE_CLEANED" | ["MISMATCHED", TweakValues]; -/** - * Ensures that the remote database is compatible with the current device. - * - * @param infoSrc - The information about the remote database (which retrieved from the remote). - * @param setting - The current settings. - * @param deviceNodeID - The ID of the current device node. - * @param currentVersionRange - The current version range of the database. - * @param updateCallback - The callback function to update the remote milestone. - * @returns A promise that resolves to the result of ensuring compatibility. - */ -export declare function ensureRemoteIsCompatible(infoSrc: EntryMilestoneInfo | false, setting: RemoteDBSettings, deviceNodeID: string, currentVersionRange: ChunkVersionRange, nodeDeviceInfo: DeviceInfo, updateCallback: (info: EntryMilestoneInfo) => Promise): Promise; -export declare function ensureDatabaseIsCompatible(db: PouchDB.Database, setting: RemoteDBSettings, deviceNodeID: string, currentVersionRange: ChunkVersionRange, nodeDeviceInfo: DeviceInfo): Promise; diff --git a/_types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts b/_types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts deleted file mode 100644 index f44997e4..00000000 --- a/_types/src/lib/src/pouchdb/LiveSyncLocalDB.d.ts +++ /dev/null @@ -1,195 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type EntryLeaf, type Credential, type RemoteDBSettings, type DocumentID, type FilePathWithPrefix, type FilePath, type DatabaseEntry, type LoadedEntry, type MetaEntry, type SavingEntry, type diff_result_leaf } from "@lib/common/types.ts"; -import { eventHub } from "@lib/hub/hub.ts"; -import { LiveSyncManagers } from "@lib/managers/LiveSyncManagers.ts"; -import type { AutoMergeResult } from "@lib/managers/ConflictManager.ts"; -import type { IServiceHub } from "@lib/services/base/IService.ts"; -import { type LogFunction } from "@lib/services/lib/logUtils.ts"; -export declare const REMOTE_CHUNK_FETCHED = "remote-chunk-fetched"; -export type REMOTE_CHUNK_FETCHED = typeof REMOTE_CHUNK_FETCHED; -declare global { - interface LSEvents { - [REMOTE_CHUNK_FETCHED]: EntryLeaf; - } -} -export type ChunkRetrievalResultSuccess = { - _id: DocumentID; - data: string; - type: "leaf"; -}; -export type ChunkRetrievalResultError = { - _id: DocumentID; - error: string; -}; -export type ChunkRetrievalResult = ChunkRetrievalResultSuccess | ChunkRetrievalResultError; -export interface LiveSyncLocalDBEnv { - services: Pick; -} -export declare function getNoFromRev(rev: string): number; -export type GeneratedChunk = { - isNew: boolean; - id: DocumentID; - piece: string; -}; -export declare class LiveSyncLocalDB { - auth: Credential; - dbname: string; - settings: RemoteDBSettings; - localDatabase: PouchDB.Database; - _log: LogFunction; - private _managers?; - get managers(): LiveSyncManagers; - isReady: boolean; - needScanning: boolean; - env: LiveSyncLocalDBEnv; - clearCaches(): void; - _prepareHashFunctions(): Promise; - onunload(): void; - refreshSettings(): void; - offRemoteChunkFetchedHandler?: ReturnType; - constructor(dbname: string, env: LiveSyncLocalDBEnv); - close(): Promise; - onNewLeaf(chunk: EntryLeaf): void; - initializeDatabase(): Promise; - /** - * Retrieve all used and existing chunks in the database. - * @param includeDeleted include deleted chunks in the result. - * @returns {used: Set, existing: Map} used: Set of chunk ids that are used in the database. existing: Map of chunk id and EntryLeaf that are existing in the database. - */ - allChunks(includeDeleted?: boolean): Promise<{ - used: Set; - existing: Map; - }>; - resetDatabase(): Promise; - findEntries(startKey: string, endKey: string, opt: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator<(DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>; - findAllDocs(opt?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator<(DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>; - findEntryNames(startKey: string, endKey: string, opt: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator; - findAllDocNames(opt?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator; - findAllNormalDocs(opt?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions): AsyncGenerator<(DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_PLAIN"]; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta) | (DatabaseEntry & import("@lib/common/types.ts").EntryBase & import("@lib/common/types.ts").EntryWithEden & { - path: FilePathWithPrefix; - children: string[]; - type: import("../common/models/db.type.ts").EntryTypes["NOTE_BINARY"]; - } & { - deleted?: boolean; - } & { - data: string | string[]; - datatype: import("@lib/common/types.ts").EntryTypeNotes; - } & PouchDB.Core.AllDocsMeta & PouchDB.Core.IdMeta & PouchDB.Core.RevisionIdMeta), void, unknown>; - removeRevision(docId: DocumentID, revision: string): Promise; - getRaw(docId: DocumentID, options?: PouchDB.Core.GetOptions): Promise; - removeRaw(docId: DocumentID, revision: string, options?: PouchDB.Core.Options): Promise; - putRaw(doc: T, options?: PouchDB.Core.PutOptions): Promise; - allDocsRaw(options?: PouchDB.Core.AllDocsWithKeyOptions | PouchDB.Core.AllDocsWithKeysOptions | PouchDB.Core.AllDocsWithinRangeOptions | PouchDB.Core.AllDocsOptions): Promise>; - bulkDocsRaw(docs: Array>, options?: PouchDB.Core.BulkDocsOptions): Promise>; - isTargetFile(filenameSrc: string): boolean; - getDBEntryMeta(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, includeDeleted?: boolean): Promise; - getDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions, dump?: boolean, waitForReady?: boolean, includeDeleted?: boolean): Promise; - getDBEntryFromMeta(meta: LoadedEntry | MetaEntry, dump?: boolean, waitForReady?: boolean): Promise; - deleteDBEntry(path: FilePathWithPrefix | FilePath, opt?: PouchDB.Core.GetOptions): Promise; - putDBEntry(note: SavingEntry, onlyChunks?: boolean, conflictBaseRev?: string): Promise; - getConflictedDoc(path: FilePathWithPrefix, rev: string): Promise; - tryAutoMerge(path: FilePathWithPrefix, enableMarkdownAutoMerge: boolean): AutoMergeResult; -} diff --git a/_types/src/lib/src/pouchdb/ReplicatorShim.d.ts b/_types/src/lib/src/pouchdb/ReplicatorShim.d.ts deleted file mode 100644 index 29a06cac..00000000 --- a/_types/src/lib/src/pouchdb/ReplicatorShim.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type SomeDocument = PouchDB.Core.ExistingDocument & PouchDB.Core.ChangesMeta; -/** - * Minimal subset of the PouchDB public API required by {@link replicateShim}. - * Both a real `PouchDB.Database` and an {@link RpcPouchDBProxy} satisfy this - * interface, allowing replication across an RPC transport. - */ -export type PouchDBShim = { - info: () => Promise; - changes: (options: PouchDB.Core.ChangesOptions) => PromiseLike>; - revsDiff: (diff: PouchDB.Core.RevisionDiffOptions) => Promise; - bulkDocs: (docs: PouchDB.Core.PutDocument[], options?: PouchDB.Core.BulkDocsOptions) => Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>; - bulkGet: (options: PouchDB.Core.BulkGetOptions) => Promise>; - put: (doc: PouchDB.Core.PutDocument, options?: PouchDB.Core.PutOptions) => Promise; - get: (id: string, options?: PouchDB.Core.GetOptions) => Promise; -}; -type CompatibleDatabase = PouchDB.Database> | PouchDBShim>; -/** Upserts a document by `id`, calling `func` to produce the updated version. */ -export declare function upsert = CompatibleDatabase, T extends SomeDocument = SomeDocument>(db: TDB, id: string, func: (doc: T) => T): Promise; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export type ShimReplicationOptionBase = { - rewind?: boolean; - batch_size?: number; -}; -export type ShimReplicationOneShot = { - live?: false; - controller?: AbortController; -} & ShimReplicationOptionBase; -export type ShimReplicationOptionContinuous = { - live: true; - controller: AbortController; -} & ShimReplicationOptionBase; -export type ShimReplicationOption = ShimReplicationOneShot | ShimReplicationOptionContinuous; -export type ProgressInfo = { - lastSeq: number; - maxSeqInBatch: number; -}; -export type ShimReplicationProgressReportFunc = (progress: SomeDocument[], progressInfo: ProgressInfo) => Promise; -/** - * Replicate documents from `sourceDB` into `targetDB` using a CouchDB-style - * checkpoint protocol. - * - * Both parameters accept either a real `PouchDB.Database` or any object - * implementing {@link PouchDBShim} — including {@link RpcPouchDBProxy} — so - * replication can span an RPC transport boundary. - * - * @param targetDB Destination database (usually local). - * @param sourceDB Source database (may be remote / RPC-backed). - * @param progress Called after each batch with the written documents. - * @param option Replication options (live mode, batch size, abort signal). - */ -export declare function replicateShim, U extends CompatibleDatabase, V extends object>(targetDB: T, sourceDB: U, progress: ShimReplicationProgressReportFunc, option?: ShimReplicationOption): Promise; -export {}; diff --git a/_types/src/lib/src/pouchdb/StreamingFetch.d.ts b/_types/src/lib/src/pouchdb/StreamingFetch.d.ts deleted file mode 100644 index e8d7e18c..00000000 --- a/_types/src/lib/src/pouchdb/StreamingFetch.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EntryDoc } from "@lib/common/models/db.definition"; -import type { AnyEntry, EntryLeaf } from "@lib/common/models/db.type"; -type DBSequence = number | string; -export type FetchChangesForInitialSyncProgress = { - totalFetched: number; - totalValidFetched: number; - targetSeq: number | string; - docsToFetch: number; - totalBytes: number; -}; -/** - * Fetches initial data from CouchDB as a stream and writes it into PouchDB. - * @param downloadToDB PouchDB instance. - * @param remoteDbUrl CouchDB database URL (for example: 'https://xxx.com/mydb'). - * @param decryptFunction Function to decrypt each document. - * @param since Sequence ID to start fetching changes from (default is '0'). - */ -export declare function fetchChangesForInitialSync(downloadToDB: PouchDB.Database, remoteDbUrl: string, authHeader: string, decryptFunction: (doc: EntryDoc) => Promise, since?: number | string, onProgress?: (progress: FetchChangesForInitialSyncProgress) => void, onCheckpoint?: (sequence: DBSequence) => void | Promise): Promise; -export {}; diff --git a/_types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts b/_types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts deleted file mode 100644 index 467855c2..00000000 --- a/_types/src/lib/src/pouchdb/StreamingFetch.integration.spec.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export {}; diff --git a/_types/src/lib/src/pouchdb/chunks.d.ts b/_types/src/lib/src/pouchdb/chunks.d.ts deleted file mode 100644 index 033d0865..00000000 --- a/_types/src/lib/src/pouchdb/chunks.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBConnection } from "@lib/common/types"; -export declare function purgeUnreferencedChunks(db: PouchDB.Database, dryRun: boolean, connSetting?: CouchDBConnection, performCompact?: boolean): Promise; -export declare function transferChunks(key: string, label: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database, items: { - id: string; - rev: string; -}[]): Promise; -export declare function balanceChunkPurgedDBs(local: PouchDB.Database, remote: PouchDB.Database): Promise; -export declare function fetchAllUsedChunks(local: PouchDB.Database, remote: PouchDB.Database): Promise; -export declare function purgeChunksLocal(db: PouchDB.Database, docs: { - id: string; - rev: string; -}[]): Promise; -export declare function collectUnbalancedChunkIDs(local: PouchDB.Database, remote: PouchDB.Database): Promise<{ - onlyOnLocal: { - id: string; - rev: string; - }[]; - onlyOnRemote: { - id: string; - rev: string; - }[]; -}>; -export declare function collectChunks(db: PouchDB.Database, type: "INUSE" | "DANGLING" | "ALL"): Promise<{ - id: string; - rev: string; -}[]>; -export declare function collectChunksUsage(db: PouchDB.Database): Promise<{ - value: number; - key: string[]; -}[]>; -export declare function collectUnreferencedChunks(db: PouchDB.Database): Promise<{ - id: string; - rev: string; -}[]>; -export declare function purgeChunksRemote(setting: CouchDBConnection, docs: { - id: string; - rev: string; -}[]): Promise; diff --git a/_types/src/lib/src/pouchdb/compress.d.ts b/_types/src/lib/src/pouchdb/compress.d.ts deleted file mode 100644 index 218643b9..00000000 --- a/_types/src/lib/src/pouchdb/compress.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import * as fflate from "fflate"; -import type { EntryDoc } from "@lib/common/types"; -export declare function _compressText(text: string): Promise; -export declare const wrappedInflate: (data: Uint8Array, opts: fflate.AsyncInflateOptions) => Promise; -export declare const wrappedDeflate: (data: Uint8Array, opts: fflate.AsyncDeflateOptions) => Promise; -export declare function _decompressText(compressed: string, _useUTF16?: boolean): Promise; -export declare function compressDoc(doc: EntryDoc): Promise; -export declare function decompressDoc(doc: EntryDoc): Promise; -export declare function wrapFflateFunc(func: (data: T, opts: U, cb: fflate.FlateCallback) => unknown): (data: T, opts: U) => Promise; -export declare const replicationFilter: (db: PouchDB.Database, compress: boolean) => void; -export declare const MARK_SHIFT_COMPRESSED = "\u000ELZ\u001D"; diff --git a/_types/src/lib/src/pouchdb/encryption.d.ts b/_types/src/lib/src/pouchdb/encryption.d.ts deleted file mode 100644 index c509c9a3..00000000 --- a/_types/src/lib/src/pouchdb/encryption.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type AnyEntry, type EntryLeaf, type DocumentID, type E2EEAlgorithm } from "@lib/common/types"; -import { encryptWorker, decryptWorker, encryptHKDFWorker, decryptHKDFWorker } from "@lib/worker/bgWorker.ts"; -export declare const encrypt: typeof encryptWorker; -export declare const decrypt: typeof decryptWorker; -export declare const encryptHKDF: typeof encryptHKDFWorker; -export declare const decryptHKDF: typeof decryptHKDFWorker; -export declare let preprocessOutgoing: (doc: AnyEntry | EntryLeaf) => Promise; -export declare let preprocessIncoming: (doc: EntryDoc) => Promise; -export declare function getConfiguredFunctionsForEncryption(passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise, algorithm: E2EEAlgorithm): { - incoming: (doc: AnyEntry | EntryLeaf) => Promise; - outgoing: (doc: EntryDoc) => Promise; -}; -export declare const enableEncryption: (db: PouchDB.Database, passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise, algorithm: E2EEAlgorithm) => void; -export declare function disableEncryption(): void; -export declare const EDEN_ENCRYPTED_KEY: DocumentID; -export declare const EDEN_ENCRYPTED_KEY_HKDF: DocumentID; -export declare function shouldEncryptEden(doc: AnyEntry | EntryLeaf): doc is AnyEntry; -export declare function shouldEncryptEdenHKDF(doc: AnyEntry | EntryLeaf): doc is AnyEntry; -export declare function shouldDecryptEden(doc: AnyEntry | EntryLeaf): doc is AnyEntry; -export declare function shouldDecryptEdenHKDF(doc: AnyEntry | EntryLeaf): doc is AnyEntry; diff --git a/_types/src/lib/src/pouchdb/negotiation.d.ts b/_types/src/lib/src/pouchdb/negotiation.d.ts deleted file mode 100644 index c2c5782f..00000000 --- a/_types/src/lib/src/pouchdb/negotiation.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const checkRemoteVersion: (db: PouchDB.Database, migrate: (from: number, to: number) => Promise, barrier?: number) => Promise; -export declare const bumpRemoteVersion: (db: PouchDB.Database, barrier?: number) => Promise; -export declare const checkSyncInfo: (db: PouchDB.Database) => Promise; -/** - * Counts the number of remote (potentially) compromised chunks in the database. - * @param db The PouchDB database instance. - * @returns The number of compromised chunks or false if an error occurs. - */ -export declare function countCompromisedChunks(db: PouchDB.Database): Promise; diff --git a/_types/src/lib/src/pouchdb/pouchdb-browser.d.ts b/_types/src/lib/src/pouchdb/pouchdb-browser.d.ts deleted file mode 100644 index 0405eca3..00000000 --- a/_types/src/lib/src/pouchdb/pouchdb-browser.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import PouchDB from "pouchdb-core"; -export { PouchDB }; diff --git a/_types/src/lib/src/pouchdb/pouchdb-http.d.ts b/_types/src/lib/src/pouchdb/pouchdb-http.d.ts deleted file mode 100644 index 0405eca3..00000000 --- a/_types/src/lib/src/pouchdb/pouchdb-http.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import PouchDB from "pouchdb-core"; -export { PouchDB }; diff --git a/_types/src/lib/src/pouchdb/pouchdb-test.d.ts b/_types/src/lib/src/pouchdb/pouchdb-test.d.ts deleted file mode 100644 index 0405eca3..00000000 --- a/_types/src/lib/src/pouchdb/pouchdb-test.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import PouchDB from "pouchdb-core"; -export { PouchDB }; diff --git a/_types/src/lib/src/pouchdb/utils_couchdb.d.ts b/_types/src/lib/src/pouchdb/utils_couchdb.d.ts deleted file mode 100644 index 584f3a75..00000000 --- a/_types/src/lib/src/pouchdb/utils_couchdb.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const isValidRemoteCouchDBURI: (uri: string) => boolean; -export declare function isCloudantURI(uri: string): boolean; -export declare function isErrorOfMissingDoc(ex: unknown): boolean; -export declare const _requestToCouchDBFetch: (baseUri: string, username: string, password: string, path?: string, body?: unknown, method?: string) => Promise; diff --git a/_types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts b/_types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts deleted file mode 100644 index 2decc291..00000000 --- a/_types/src/lib/src/replication/LiveSyncAbstractReplicator.d.ts +++ /dev/null @@ -1,69 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type DatabaseConnectingStatus, type RemoteDBSettings, type EntryLeaf, type TweakValues, type NodeData } from "@lib/common/types.ts"; -import type { RequiredServices } from "@lib/interfaces/ServiceModule"; -export type ReplicationCallback = (e: PouchDB.Core.ExistingDocument[]) => Promise | boolean; -export type ReplicationStat = { - sent: number; - arrived: number; - maxPullSeq: number; - maxPushSeq: number; - lastSyncPullSeq: number; - lastSyncPushSeq: number; - syncStatus: DatabaseConnectingStatus; -}; -export interface LiveSyncReplicatorEnv { - services: RequiredServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote">; -} -export type RemoteDBStatus = { - [key: string]: unknown; - estimatedSize?: number; -}; -export declare abstract class LiveSyncAbstractReplicator { - syncStatus: DatabaseConnectingStatus; - docArrived: number; - docSent: number; - lastSyncPullSeq: number; - maxPullSeq: number; - lastSyncPushSeq: number; - maxPushSeq: number; - controller?: AbortController; - originalSetting: RemoteDBSettings; - nodeid: string; - remoteLocked: boolean; - remoteCleaned: boolean; - remoteLockedAndDeviceNotAccepted: boolean; - tweakSettingsMismatched: boolean; - preferredTweakValue?: TweakValues; - abstract get isChunkSendingSupported(): boolean; - get database(): import("../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get rawDatabase(): PouchDB.Database; - get currentSettings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database | undefined, showResult: boolean, fromSeq?: number | string): Promise; - abstract getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise; - ensurePBKDF2Salt(setting: RemoteDBSettings, showMessage?: boolean, useCache?: boolean): Promise; - env: LiveSyncReplicatorEnv; - initializeDatabaseForReplication(): Promise; - constructor(env: LiveSyncReplicatorEnv); - abstract terminateSync(): void; - abstract openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, ignoreCleanLock: boolean): Promise; - updateInfo: () => void; - abstract tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise; - abstract replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean, sendChunksInBulkDisabled?: boolean): Promise; - abstract replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - abstract closeReplication(): void; - abstract tryResetRemoteDatabase(setting: RemoteDBSettings): Promise; - abstract tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise; - abstract markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise; - abstract markRemoteResolved(setting: RemoteDBSettings): Promise; - abstract resetRemoteTweakSettings(setting: RemoteDBSettings): Promise; - abstract setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise; - abstract fetchRemoteChunks(missingChunks: string[], showResult: boolean): Promise; - abstract getRemoteStatus(setting: RemoteDBSettings): Promise; - abstract getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise; - abstract countCompromisedChunks(setting?: RemoteDBSettings): Promise; - abstract getConnectedDeviceList(setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; -} diff --git a/_types/src/lib/src/replication/SyncParamsHandler.d.ts b/_types/src/lib/src/replication/SyncParamsHandler.d.ts deleted file mode 100644 index 73d6f363..00000000 --- a/_types/src/lib/src/replication/SyncParamsHandler.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SyncParameters } from "@lib/common/types.ts"; -import { LiveSyncError } from "@lib/common/LSError.ts"; -/** - * Creates a SyncParamsHandler for managing synchronisation parameters. - */ -type putFunc = (params: SyncParameters) => Promise; -/** - * Fetches synchronisation parameters from the server. - */ -type getFunc = () => Promise; -/** - * The function to create new synchronisation parameters. - * Note that this function should not return `pbkdf2salt` in the result; it should be generated and stored in the handler. - */ -type createFunc = () => Promise; -type CreateSyncParamsHanderOptions = { - put: putFunc; - get: getFunc; - create: createFunc; -}; -export type SyncParamsHandler = { - fetch: (refresh?: boolean) => Promise; - getPBKDF2Salt: (refresh?: boolean) => Promise; -}; -export declare function createSyncParamsHanderForServer(key: string, options: CreateSyncParamsHanderOptions): SyncParamsHandler; -export declare function clearHandlers(): void; -export declare class SyncParamsHandlerError extends LiveSyncError { -} -export declare class SyncParamsFetchError extends SyncParamsHandlerError { -} -export declare class SyncParamsNotFoundError extends SyncParamsHandlerError { -} -export declare class SyncParamsUpdateError extends SyncParamsHandlerError { -} -export {}; diff --git a/_types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts b/_types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts deleted file mode 100644 index 8a9fb4df..00000000 --- a/_types/src/lib/src/replication/couchdb/LiveSyncReplicator.d.ts +++ /dev/null @@ -1,87 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type RemoteDBSettings, type EntryLeaf, type TweakValues, type SyncParameters, type DatabaseEntry, type NodeData } from "@lib/common/types.ts"; -import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import type { ServiceHub } from "@lib/services/ServiceHub.ts"; -export interface LiveSyncCouchDBReplicatorEnv extends LiveSyncReplicatorEnv { - services: ServiceHub; -} -export declare class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator { - get isChunkSendingSupported(): boolean; - isMobile(): boolean; - constructor(env: LiveSyncCouchDBReplicatorEnv); - getInitialSyncParameters(setting: RemoteDBSettings): Promise; - getSyncParameters(setting: RemoteDBSettings): Promise; - putSyncParameters(setting: RemoteDBSettings, params: SyncParameters): Promise; - getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise; - migrate(from: number, to: number): Promise; - terminateSync(): void; - openReplication(setting: RemoteDBSettings, keepAlive: boolean, showResult: boolean, ignoreCleanLock: boolean): Promise; - replicationActivated(showResult: boolean): void; - replicationChangeDetected(e: PouchDB.Replication.SyncResult, showResult: boolean, docSentOnStart: number, docArrivedOnStart: number): Promise; - replicationCompleted(showResult: boolean): void; - replicationDenied(e: unknown): void; - replicationErrored(e: unknown): void; - replicationPaused(): void; - processSync(syncHandler: PouchDB.Replication.Sync | PouchDB.Replication.Replication, showResult: boolean, docSentOnStart: number, docArrivedOnStart: number, syncMode: "sync" | "pullOnly" | "pushOnly", retrying: boolean, reportCancelledAsDone?: boolean): Promise<"DONE" | "NEED_RETRY" | "NEED_RESURRECT" | "FAILED" | "CANCELLED">; - getEmptyMaxEntry(remoteID: number): { - _id: string; - maxSeq: number | string; - remoteID: number; - seqStatusMap: Record; - _rev: string | undefined; - }; - getLastTransferredSeqOfChunks(localDB: PouchDB.Database, remoteID: number): Promise>; - updateMaxTransferredSeqOnChunks(localDB: PouchDB.Database, remoteID: number, seqStatusMap: Record): Promise>; - sendChunks(setting: RemoteDBSettings, remoteDB: PouchDB.Database | undefined, showResult: boolean, fromSeq?: number | string): Promise; - openOneShotReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean, syncMode: "sync" | "pullOnly" | "pushOnly", ignoreCleanLock?: boolean): Promise; - replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - checkReplicationConnectivity(setting: RemoteDBSettings, keepAlive: boolean, skipCheck: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise; - info: PouchDB.Core.DatabaseInfo; - syncOptionBase: PouchDB.Replication.SyncOptions; - syncOption: PouchDB.Replication.SyncOptions; - }>; - openContinuousReplication(setting: RemoteDBSettings, showResult: boolean, retrying: boolean): Promise; - closeReplication(): void; - tryResetRemoteDatabase(setting: RemoteDBSettings): Promise; - tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise; - markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise; - markRemoteResolved(setting: RemoteDBSettings): Promise; - connectRemoteCouchDBWithSetting(settings: RemoteDBSettings, isMobile: boolean, performSetup?: boolean, skipInfo?: boolean): Promise; - info: PouchDB.Core.DatabaseInfo; - }> | "Empty passphrases cannot be used without explicit permission"; - _ensureConnection(settings: RemoteDBSettings, performSetup?: boolean): Promise>; - /** - * Fetch a document from the remote database directly. - * @param settings RemoteDBSettings for the connection. - * @param id Document ID to fetch. - * @param db Optional PouchDB instance to use. If provided, it will use this instance instead of creating a new connection (then settings will be ignored). - * @returns The fetched document or false if the document does not exist. - * @throws {Error} Other errors that may occur during the fetch operation. - */ - fetchRemoteDocument(settings: RemoteDBSettings, id: string, db?: PouchDB.Database): Promise; - /** - * Puts a document to the remote database directly - * @param settings RemoteDBSettings for the connection. - * @param doc Document to put. - * @param db Optional PouchDB instance to use. If provided, it will use this instance instead of creating a new connection (then settings will be ignored). - * @returns Response from the remote database or false if an error occurred. - * @throws {Error} If the document could not be put. - */ - putRemoteDocument(settings: RemoteDBSettings, doc: T, db?: PouchDB.Database): Promise; - fetchRemoteChunks(missingChunks: string[], showResult: boolean): Promise; - tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise; - resetRemoteTweakSettings(setting: RemoteDBSettings): Promise; - setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise; - getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise; - compactRemote(setting: RemoteDBSettings): Promise; - getRemoteStatus(setting: RemoteDBSettings): Promise; - countCompromisedChunks(setting?: RemoteDBSettings): Promise; - getConnectedDeviceList(setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; -} diff --git a/_types/src/lib/src/replication/httplib.d.ts b/_types/src/lib/src/replication/httplib.d.ts deleted file mode 100644 index 1561073d..00000000 --- a/_types/src/lib/src/replication/httplib.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBCredentials, JWTCredentials, JWTHeader, JWTParams, JWTPayload, PreparedJWT, RemoteDBSettings } from "@lib/common/types"; -import { Computed } from "octagonal-wheels/dataobject/Computed"; -/** - * Generates a credential object based on the provided settings. - * @param settings - RemoteDBSettings - * @returns {CouchDBCredentials} credentials object - */ -export declare function generateCredentialObject(settings: RemoteDBSettings): { - jwtAlgorithm: import("../common/models/auth.type").JWTAlgorithm; - jwtKey: string; - jwtKid: string; - jwtSub: string; - jwtExpDuration: number; - type: "jwt"; - username?: undefined; - password?: undefined; -} | { - username: string; - password: string; - type: "basic"; - jwtAlgorithm?: undefined; - jwtKey?: undefined; - jwtKid?: undefined; - jwtSub?: undefined; - jwtExpDuration?: undefined; -}; -/** - * Generates a basic authentication header for CouchDB credentials using the provided username and password. - * And it caches the result for performance if the credentials are not changed. - */ -export declare class BasicHeaderGenerator { - _header: Computed<[source: CouchDBCredentials], string>; - /** - * Generates a basic authentication header for CouchDB credentials using the provided username and password. - * @param auth - CouchDBCredentials - * @returns {Promise} The basic authentication header (without "Basic" prefix). - */ - getBasicHeader(auth: CouchDBCredentials): Promise; -} -/** - * Generates a JWT token based on the provided credentials and parameters. - * And it caches the result for performance if the credentials are not changed. - */ -export declare class JWTTokenGenerator { - _importKey(auth: JWTCredentials): Promise; - _currentCryptoKey: Computed<[auth: JWTCredentials], CryptoKey>; - _jwt: Computed<[params: JWTParams], { - token: string; - header: JWTHeader; - payload: JWTPayload; - credentials: JWTCredentials; - }>; - _jwtParams: Computed<[source: JWTCredentials], { - header: JWTHeader; - payload: { - exp: number; - iat: number; - sub: string; - "_couchdb.roles": string[]; - }; - credentials: JWTCredentials; - }>; - getJWT(auth: JWTCredentials): Promise; - /** - * Generates a JWT token based on the provided credentials and parameters. - * @param auth - JWTCredentials - * @returns {Promise} The JWT token (with "Bearer" prefix). - */ - getBearerToken(auth: JWTCredentials): Promise; -} -export declare class AuthorizationHeaderGenerator { - _basicHeader: BasicHeaderGenerator; - _jwtHeader: JWTTokenGenerator; - getAuthorizationHeader(auth: CouchDBCredentials): Promise; -} diff --git a/_types/src/lib/src/replication/journal/JournalSyncCore.d.ts b/_types/src/lib/src/replication/journal/JournalSyncCore.d.ts deleted file mode 100644 index 82fe710f..00000000 --- a/_types/src/lib/src/replication/journal/JournalSyncCore.d.ts +++ /dev/null @@ -1,72 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type SyncParameters, type BucketSyncSetting, type RemoteDBSettings } from "@lib/common/types.ts"; -import type { ReplicationCallback, ReplicationStat } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import { type SimpleStore } from "@lib/common/utils.ts"; -import { type CheckPointInfo } from "./JournalSyncTypes.ts"; -import type { LiveSyncJournalReplicatorEnv } from "./LiveSyncJournalReplicatorEnv.ts"; -import type { IJournalStorage } from "./objectstore/JournalStorageAdapter.ts"; -type ProcessingEntry = PouchDB.Core.PutDocument & PouchDB.Core.GetMeta; -export declare class JournalSyncCore { - _settings: BucketSyncSetting; - storage: IJournalStorage; - get db(): PouchDB.Database; - get currentSettings(): import("@lib/common/types.ts").ObsidianLiveSyncSettings; - hash: string; - processReplication: ReplicationCallback; - batchSize: number; - env: LiveSyncJournalReplicatorEnv; - store: SimpleStore; - requestedStop: boolean; - getInitialSyncParameters(): Promise; - getSyncParameters(): Promise; - putSyncParameters(params: SyncParameters): Promise; - getHash(settings: BucketSyncSetting): string; - constructor(settings: BucketSyncSetting, store: SimpleStore, env: LiveSyncJournalReplicatorEnv, storage: IJournalStorage); - downloadJson(key: string): Promise; - uploadJson(key: string, body: T): Promise; - applyNewConfig(settings: BucketSyncSetting, store: SimpleStore, env: LiveSyncJournalReplicatorEnv): void; - updateInfo(info: Partial): void; - updateCheckPointInfo(func: (infoFrom: CheckPointInfo) => CheckPointInfo): Promise; - _currentCheckPointInfo: { - lastLocalSeq: number | string; - journalEpoch: string; - knownIDs: Set; - sentIDs: Set; - receivedFiles: Set; - sentFiles: Set; - }; - getCheckpointInfo(): Promise; - resetAllCaches(): void; - resetCheckpointInfo(): Promise; - private getJournalEpochFromSyncParams; - ensureCheckpointCachesAreFresh(): Promise; - isAvailable(): Promise; - resetBucket(): Promise; - getRemoteKey(): string; - getReplicationPBKDF2Salt(refresh?: boolean): Promise; - isEncryptionPrevented(fileName: string): boolean; - private decryptDataV2; - private decryptDataV1; - decryptDownloaded(key: string, encrypted: Uint8Array, set: RemoteDBSettings): Promise; - encryptForUpload(key: string, data: Uint8Array, set: RemoteDBSettings): Promise; - getDocKey(doc: EntryDoc): string; - _createJournalPack(override?: number | string): Promise<{ - changes: EntryDoc[]; - hasNext: boolean; - packLastSeq: string | number; - }>; - private _createSendReadableStream; - private _createSendCompressTransformStream; - private _createSendUploadWritableStream; - sendLocalJournal(showMessage?: boolean): Promise; - _getRemoteJournals(): Promise; - processDocuments(allDocs: ProcessingEntry[]): Promise; - private _createReceiveReadableStream; - private _createReceiveTransformStream; - private _createReceiveWritableStream; - receiveRemoteJournal(showMessage?: boolean): Promise; - sync(showResult?: boolean): Promise; - requestStop(): void; -} -export {}; diff --git a/_types/src/lib/src/replication/journal/JournalSyncTypes.d.ts b/_types/src/lib/src/replication/journal/JournalSyncTypes.d.ts deleted file mode 100644 index d5123da4..00000000 --- a/_types/src/lib/src/replication/journal/JournalSyncTypes.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type CheckPointInfo = { - lastLocalSeq: number | string; - journalEpoch: string; - knownIDs: Set; - sentIDs: Set; - receivedFiles: Set; - sentFiles: Set; -}; -export declare const CheckPointInfoDefault: CheckPointInfo; diff --git a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts b/_types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts deleted file mode 100644 index e682e2ff..00000000 --- a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicator.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings, type EntryLeaf, type ChunkVersionRange, type TweakValues, type NodeData } from "@lib/common/types.ts"; -import { JournalSyncCore } from "./JournalSyncCore.ts"; -import { LiveSyncAbstractReplicator, type RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import { type ENSURE_DB_RESULT } from "@lib/pouchdb/LiveSyncDBFunctions.ts"; -import type { CheckPointInfo } from "./JournalSyncTypes.ts"; -import { type SimpleStore } from "@lib/common/utils.ts"; -import type { LiveSyncJournalReplicatorEnv } from "./LiveSyncJournalReplicatorEnv.ts"; -export declare class LiveSyncJournalReplicator extends LiveSyncAbstractReplicator { - env: LiveSyncJournalReplicatorEnv; - get isChunkSendingSupported(): boolean; - get client(): JournalSyncCore; - get simpleStore(): SimpleStore; - _client: JournalSyncCore; - getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise; - setupJournalSyncClient(): JournalSyncCore; - ensureBucketIsCompatible(deviceNodeID: string, currentVersionRange: ChunkVersionRange): Promise; - constructor(env: LiveSyncJournalReplicatorEnv); - migrate(from: number, to: number): Promise; - terminateSync(): void; - openReplication(setting: RemoteDBSettings, _: boolean, showResult: boolean, ignoreCleanLock?: boolean): Promise; - replicateAllToServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - checkReplicationConnectivity(skipCheck: boolean, ignoreCleanLock?: boolean, showMessage?: boolean): Promise; - fetchRemoteChunks(missingChunks: string[], showResult: boolean): Promise; - closeReplication(): void; - tryResetRemoteDatabase(setting: RemoteDBSettings): Promise; - tryCreateRemoteDatabase(setting: RemoteDBSettings): Promise; - markRemoteLocked(setting: RemoteDBSettings, locked: boolean, lockByClean: boolean): Promise; - markRemoteResolved(setting: RemoteDBSettings): Promise; - tryConnectRemote(setting: RemoteDBSettings, showResult?: boolean): Promise; - resetRemoteTweakSettings(setting: RemoteDBSettings): Promise; - setPreferredRemoteTweakSettings(setting: RemoteDBSettings): Promise; - getRemotePreferredTweakValues(setting: RemoteDBSettings): Promise; - getRemoteStatus(setting: RemoteDBSettings): Promise; - countCompromisedChunks(): Promise; - getConnectedDeviceList(setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; -} diff --git a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts b/_types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts deleted file mode 100644 index 22c7f1c9..00000000 --- a/_types/src/lib/src/replication/journal/LiveSyncJournalReplicatorEnv.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator"; -export interface LiveSyncJournalReplicatorEnv extends LiveSyncReplicatorEnv { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} diff --git a/_types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts b/_types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts deleted file mode 100644 index f4cdd0ed..00000000 --- a/_types/src/lib/src/replication/journal/objectstore/JournalStorageAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import type { BucketSyncSetting } from "@lib/common/types.ts"; -export interface IJournalStorage { - upload(key: string, data: Uint8Array, mime: string): Promise; - download(key: string, ignoreCache?: boolean): Promise; - listFiles(from: string, limit?: number): Promise; - deleteFiles(keys: string[]): Promise; - isAvailable(): Promise; - getUsage(): Promise; - applyNewConfig(settings: BucketSyncSetting): void; -} -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv.ts"; -export interface IJournalStorageAdapterClass { - new (settings: BucketSyncSetting, env: LiveSyncJournalReplicatorEnv): IJournalStorage; -} diff --git a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts b/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts deleted file mode 100644 index 02effd95..00000000 --- a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { S3 } from "@aws-sdk/client-s3"; -import { type BucketSyncSetting } from "@lib/common/types.ts"; -import type { RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator.ts"; -import type { IJournalStorage } from "./JournalStorageAdapter.ts"; -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv.ts"; -export declare class MinioStorageAdapter implements IJournalStorage { - _instance?: S3; - _settings: BucketSyncSetting; - _env: LiveSyncJournalReplicatorEnv; - constructor(settings: BucketSyncSetting, env: LiveSyncJournalReplicatorEnv); - private runTrackedRequest; - applyNewConfig(settings: BucketSyncSetting): void; - get customHeaders(): [string, string][]; - _getClient(): S3; - upload(key: string, data: Uint8Array, mime: string): Promise; - download(key: string, ignoreCache?: boolean): Promise; - listFiles(from: string, limit?: number): Promise; - deleteFiles(keys: string[]): Promise; - isAvailable(): Promise; - getUsage(): Promise; -} diff --git a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts b/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts deleted file mode 100644 index 467855c2..00000000 --- a/_types/src/lib/src/replication/journal/objectstore/MinioStorageAdapter.integration.spec.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export {}; diff --git a/_types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts b/_types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts deleted file mode 100644 index 8b43daa5..00000000 --- a/_types/src/lib/src/replication/trystero/LiveSyncTrysteroReplicator.d.ts +++ /dev/null @@ -1,92 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings, type EntryLeaf, type TweakValues, type LOG_LEVEL, type NodeData } from "@lib/common/types"; -import { LiveSyncAbstractReplicator, type LiveSyncReplicatorEnv, type RemoteDBStatus } from "@lib/replication/LiveSyncAbstractReplicator"; -import { TrysteroReplicator } from "./TrysteroReplicator"; -import { P2PHost, type AcceptanceDecision, type RevokeAcceptanceDecision } from "./TrysteroReplicatorP2PServer"; -import type { Advertisement } from "./types"; -export interface LiveSyncTrysteroReplicatorEnv extends LiveSyncReplicatorEnv { - /** - * Injected by the host platform (e.g. Obsidian) to show a UI for peer selection. - * When not set, openReplication falls back to replicateFromCommand (CLI-safe). - */ - openReplicationUI?: (showResult: boolean) => Promise; - /** - * Injected by the host platform to show a UI for selecting a peer to rebuild from. - * When not set, replicateAllFromServer falls back to the headless selectPeer dialog. - */ - openRebuildUI?: (showResult: boolean) => Promise; -} -export declare class LiveSyncTrysteroReplicator extends LiveSyncAbstractReplicator { - private _p2pHost?; - private _replicator?; - get openReplicationUI(): ((showResult: boolean) => Promise) | undefined; - get rawReplicator(): TrysteroReplicator | undefined; - get rawHost(): P2PHost | undefined; - get isChunkSendingSupported(): boolean; - getReplicationPBKDF2Salt(_setting: RemoteDBSettings, _refresh?: boolean): Promise; - terminateSync(): void; - private _buildEnv; - open(): Promise; - close(): Promise; - closeReplication(): void; - get server(): P2PHost | undefined; - get knownAdvertisements(): Advertisement[]; - enableBroadcastChanges(): void; - disableBroadcastChanges(): void; - requestStatus(): void; - onNewPeer(peer: Advertisement): Promise | undefined; - onPeerLeaved(peerId: string): void; - replicateFromCommand(showResult?: boolean): Promise; - replicateFrom(peerId: string, showNotice?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - requestSynchroniseToPeer(peerId: string): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - getRemoteConfig(peerId: string): Promise; - watchPeer(peerId: string): void; - unwatchPeer(peerId: string): void; - sync(peerId: string, showNotice?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - } | undefined>; - setOnSetup(): void; - clearOnSetup(): void; - makeDecision(decision: AcceptanceDecision): Promise; - revokeDecision(decision: RevokeAcceptanceDecision): Promise; - makeSureOpened(): Promise; - openReplication(_setting: RemoteDBSettings, _keepAlive: boolean, showResult: boolean, _ignoreCleanLock: boolean): Promise; - tryConnectRemote(_setting: RemoteDBSettings, _showResult?: boolean): Promise; - replicateAllToServer(_setting: RemoteDBSettings, _showingNotice?: boolean, _sendChunksInBulkDisabled?: boolean): Promise; - selectPeer(settingPeerName: string, r: TrysteroReplicator, logLevel: LOG_LEVEL): Promise; - tryUntilSuccess(func: () => Promise, repeat: number, logLevel: LOG_LEVEL): Promise; - replicateAllFromServer(setting: RemoteDBSettings, showingNotice?: boolean): Promise; - tryResetRemoteDatabase(_setting: RemoteDBSettings): Promise; - tryCreateRemoteDatabase(_setting: RemoteDBSettings): Promise; - markRemoteLocked(_setting: RemoteDBSettings, _locked: boolean, _lockByClean: boolean): Promise; - markRemoteResolved(_setting: RemoteDBSettings): Promise; - resetRemoteTweakSettings(_setting: RemoteDBSettings): Promise; - setPreferredRemoteTweakSettings(_setting: RemoteDBSettings): Promise; - fetchRemoteChunks(_missingChunks: string[], _showResult: boolean): Promise; - getRemoteStatus(_setting: RemoteDBSettings): Promise; - getRemotePreferredTweakValues(_setting: RemoteDBSettings): Promise; - countCompromisedChunks(): Promise; - getConnectedDeviceList(_setting?: RemoteDBSettings): Promise; - accepted_nodes: string[]; - }>; - env: LiveSyncTrysteroReplicatorEnv; - constructor(env: LiveSyncTrysteroReplicatorEnv); -} diff --git a/_types/src/lib/src/replication/trystero/P2PLogCollector.d.ts b/_types/src/lib/src/replication/trystero/P2PLogCollector.d.ts deleted file mode 100644 index 9212edf9..00000000 --- a/_types/src/lib/src/replication/trystero/P2PLogCollector.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { P2PReplicationProgress } from "./TrysteroReplicator"; -export declare class P2PLogCollector { - constructor(); - p2pReplicationResult: Map; - updateP2PReplicationLine(): void; - p2pReplicationLine: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -} diff --git a/_types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts b/_types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts deleted file mode 100644 index 3eb6750f..00000000 --- a/_types/src/lib/src/replication/trystero/P2PReplicatorBase.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { P2PSyncSetting, EntryDoc } from "@lib/common/types"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; -export interface P2PReplicatorBase { - storeP2PStatusLine: ReactiveSource; - settings: P2PSyncSetting; - _log(msg: unknown, level?: LOG_LEVEL): void; - _notice(msg: unknown, key?: string): void; - getSettings(): P2PSyncSetting; - getDB: () => PouchDB.Database; - confirm: Confirm; - simpleStore(): SimpleStore; - handleReplicatedDocuments(docs: EntryDoc[]): Promise; - init(): Promise; - services: InjectableServiceHub; -} diff --git a/_types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts b/_types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts deleted file mode 100644 index 3414b1af..00000000 --- a/_types/src/lib/src/replication/trystero/P2PReplicatorCore.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { P2PPaneParams } from "./UseP2PReplicatorResult"; -export type P2PViewFactory = (leaf: unknown) => unknown; -/** - * ServiceFeature: P2P Replicator lifecycle management. - * Binds a LiveSyncTrysteroReplicator to the host's lifecycle events, - * following the same middleware style as useOfflineScanner. - * - * @param viewTypeAndFactory Optional [viewType, factory] pair for registering the P2P pane view. - * When provided, also registers commands and ribbon icon via services.API. - */ -export declare function useP2PReplicator(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, viewTypeAndFactory?: [viewType: string, factory: P2PViewFactory]): P2PPaneParams; diff --git a/_types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts b/_types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts deleted file mode 100644 index e4ec7234..00000000 --- a/_types/src/lib/src/replication/trystero/P2PReplicatorPaneCommon.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -export declare const EVENT_P2P_PEER_SHOW_EXTRA_MENU = "p2p-peer-show-extra-menu"; -export declare enum AcceptedStatus { - UNKNOWN = "Unknown", - ACCEPTED = "Accepted", - DENIED = "Denied", - ACCEPTED_IN_SESSION = "Accepted in session", - DENIED_IN_SESSION = "Denied in session" -} -export type PeerExtraMenuEvent = { - peer: PeerStatus; - event: MouseEvent; -}; -export declare enum ConnectionStatus { - CONNECTED = "Connected", - CONNECTED_LIVE = "Connected(live)", - DISCONNECTED = "Disconnected" -} -export type PeerStatus = { - name: string; - peerId: string; - syncOnConnect: boolean; - watchOnConnect: boolean; - syncOnReplicationCommand: boolean; - accepted: AcceptedStatus; - status: ConnectionStatus; - isFetching: boolean; - isSending: boolean; - isWatching: boolean; -}; -declare global { - interface LSEvents { - [EVENT_P2P_PEER_SHOW_EXTRA_MENU]: PeerExtraMenuEvent; - } -} -export interface PluginShim { - services: InjectableServiceHub; - core: { - services: InjectableServiceHub; - }; -} diff --git a/_types/src/lib/src/replication/trystero/ProxiedDB.d.ts b/_types/src/lib/src/replication/trystero/ProxiedDB.d.ts deleted file mode 100644 index baa175d7..00000000 --- a/_types/src/lib/src/replication/trystero/ProxiedDB.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ReplicatorHostEnv } from "./types"; -import type { EntryDoc } from "@lib/common/models/db.definition"; -export declare function createHostingDB(env: ReplicatorHostEnv): { - info: () => Promise; - changes: (options: PouchDB.Core.ChangesOptions) => PouchDB.Core.Changes; - revsDiff: (diff: PouchDB.Core.RevisionDiffOptions) => Promise; - bulkDocs: (docs: PouchDB.Core.PutDocument>[], options?: PouchDB.Core.BulkDocsOptions) => Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>; - bulkGet: (options: PouchDB.Core.BulkGetOptions) => Promise>; - put: (doc: PouchDB.Core.PutDocument>, options?: PouchDB.Core.PutOptions) => Promise; - get: (id: string, options?: PouchDB.Core.GetOptions) => Promise; - _stopHosting: () => void; -}; -export type HostingDB = ReturnType; diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts deleted file mode 100644 index 440a7dba..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicator.d.ts +++ /dev/null @@ -1,153 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EntryDoc, type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { type ProgressInfo } from "@lib/pouchdb/ReplicatorShim"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import { type Advertisement, type ReplicatorHostEnv } from "./types"; -import { EVENT_P2P_REPLICATOR_PROGRESS, EVENT_P2P_REPLICATOR_STATUS, P2PHost } from "./TrysteroReplicatorP2PServer"; -export type P2PReplicatorStatus = { - isBroadcasting: boolean; - replicatingTo: string[]; - replicatingFrom: string[]; - watchingPeers: string[]; -}; -export type P2PReplicationProgress = { - peerId: string; - peerName: string; - fetching: { - max: number; - current: number; - isActive: boolean; - }; - sending: { - max: number; - current: number; - isActive: boolean; - }; -}; -export type P2PReplicationReport = { - peerId: string; - peerName: string; -} & ({ - fetching: { - max: number; - current: number; - isActive: boolean; - }; -} | { - sending: { - max: number; - current: number; - isActive: boolean; - }; -}); -declare global { - interface LSEvents { - [EVENT_P2P_REPLICATOR_STATUS]: P2PReplicatorStatus; - [EVENT_P2P_REPLICATOR_PROGRESS]: P2PReplicationReport; - } -} -export type AllReplicationClientStatus = { - [peerId: string]: { - isReplicatingTo: boolean; - isReplicatingFrom: boolean; - isWatching: boolean; - stats: P2PReplicationProgress; - }; -}; -export declare class TrysteroReplicator { - _env: ReplicatorHostEnv; - server?: P2PHost; - replicationStatus(): {}; // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type - get settings(): import("@lib/common/types").P2PSyncSetting; - get db(): PouchDB.Database; - get deviceName(): string; - get platform(): string; - get confirm(): Confirm; - private runFiniteReplicationActivity; - constructor(env: ReplicatorHostEnv, server?: P2PHost); - close(): Promise; - open(): Promise; - makeSureOpened(): Promise; - get autoSyncPeers(): RegExp[]; - get autoWatchPeers(): RegExp[]; - onNewPeer(peer: Advertisement): Promise; - onPeerLeaved(peerId: string): void; - _onSetup: boolean; - setOnSetup(): void; - clearOnSetup(): void; - getTweakSettings(fromPeerId: string): Promise>; - getCommands(): { - reqSync: (fromPeerId: string) => Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - "!reqAuth": (fromPeerId: string) => Promise; - getTweakSettings: (fromPeerId: string) => Promise>; - onProgress: (fromPeerId: string) => Promise<{ - error: Error; - } | undefined>; - getAllConfig: (fromPeerId: string) => Promise; - onProgressAcknowledged: (fromPeerId: string, info: ProgressInfo) => Promise; - getIsBroadcasting: () => Promise; - requestBroadcasting: (peerId: string) => Promise; - }; - requestAuthenticate(peerId: string): Promise; - lastSeq: string | number; - requestSynchroniseToPeer(peerId: string): Promise["reqSync"]>>>; - requestSynchroniseToAllAvailablePeers(): Promise; - dispatchStatus(): void; - requestStatus(): void; - changes?: PouchDB.Core.Changes; - _isBroadcasting: boolean; - disableBroadcastChanges(): void; - enableBroadcastChanges(): void; - get knownAdvertisements(): Advertisement[]; - availableReplicationPairs: Set; - sync(remotePeer: string, showNotice?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - } | undefined>; - _replicateToPeers: Set; - _replicateFromPeers: Set; - dispatchReplicationProgress(peerId: string, info?: ProgressInfo): void; - onReplicationProgress(peerId: string, info?: ProgressInfo): boolean; - onProgressAcknowledged(peerId: string, info?: ProgressInfo): boolean; - acknowledgeProgress(remotePeerId: string, info?: ProgressInfo): void; - replicateFrom(remotePeer: string, showNotice?: boolean, fromStart?: boolean): Promise<{ - error: unknown; - ok?: undefined; - } | { - ok: boolean; - error?: undefined; - }>; - notifyProgress(excludePeerId?: string): Promise | undefined; - requestBroadcastChanges(peerId: string): Promise; - getRemoteIsBroadcasting(peerId: string): Promise; - _watchingPeers: Set; - watchPeer(peerId: string): void; - unwatchPeer(peerId: string): void; - onUpdateDatabase(fromPeerId: string): Promise; - getRemoteConfig(peerId: string): Promise; - checkTweakValues(peerId: string): Promise; - replicateFromCommand(showResult?: boolean): Promise; - disconnectFromServer(): void; - pauseServe(): Promise; - allowReconnection(): void; -} diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts deleted file mode 100644 index 693f9aac..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PClient.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { PouchDBShim, SomeDocument } from "@lib/pouchdb/ReplicatorShim"; -import type { TrysteroReplicatorP2PServer } from "./TrysteroReplicatorP2PServer"; -import { type BindableObject, type NonPrivateMethodKeys, type Response } from "./types"; -import type { JsonLike } from "@lib/rpc"; -export declare class TrysteroReplicatorP2PClient { - _server: TrysteroReplicatorP2PServer; - _connectedPeerId: string; - _remoteDB: PouchDBShim>; - get remoteDB(): PouchDBShim>; - constructor(server: TrysteroReplicatorP2PServer, connectedPeerId: string); - _bindRemoteDB(): PouchDBShim>; - _sendRPC(type: string, args: JsonLike[], timeout?: number): Promise; - __onResponse(_data: Response): void; - bindRemoteFunction(type: string, timeout?: number): (...args: T) => Promise; - invokeRemoteFunction(type: string, args: T, timeout?: number): Promise; - bindRemoteObjectFunctions, U extends keyof T>(key: U, timeout?: number): (...args: Parameters) => Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - invokeRemoteObjectFunction, U extends NonPrivateMethodKeys>(key: U, args: Parameters, timeout?: number): Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - close(): void; -} diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts deleted file mode 100644 index 482fca67..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PConnection.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TrysteroReplicatorP2PServer } from "./TrysteroReplicatorP2PServer"; -export { TrysteroReplicatorP2PServer as TrysteroConnection }; diff --git a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts b/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts deleted file mode 100644 index 25fd4b34..00000000 --- a/_types/src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ActionSender, type Room } from "@trystero-p2p/nostr"; -import { type P2PSyncSetting } from "@lib/common/types"; -import { type ReplicatorHostEnv, type FullFilledDeviceInfo, type Request, type Response, type Payload, type Advertisement, type BindableObject, type BindableFunction } from "./types"; -import { StoredMapLike } from "@lib/dataobject/StoredMap"; -import { TrysteroReplicatorP2PClient } from "./TrysteroReplicatorP2PClient"; -import { Computed } from "octagonal-wheels/dataobject/Computed"; -import { RpcRoom, type JsonLike } from "@lib/rpc"; -import { type DiagRTCStats } from "@lib/rpc/transports/DiagRTCPeerConnections.types"; -export type PeerInfo = Advertisement & { - isAccepted: boolean | undefined; - isTemporaryAccepted: boolean | undefined; -}; -export type AcceptanceDecision = { - peerId: string; - name: string; - decision: boolean; - isTemporary: boolean; -}; -export type RevokeAcceptanceDecision = { - peerId: string; - name: string; -}; -export type P2PServerInfo = { - isConnected: boolean; - knownAdvertisements: PeerInfo[]; - serverPeerId: string; - roomId: string; - diag: DiagRTCStats; -}; -export declare const EVENT_SERVER_STATUS = "p2p-server-status"; -export declare const EVENT_MAKE_DECISION = "make-decision-p2p-peer"; -export declare const EVENT_REVOKE_DECISION = "revoke-decision-p2p-peer"; -export declare const EVENT_ADVERTISEMENT_RECEIVED = "p2p-advertisement-received"; -export declare const EVENT_DEVICE_LEAVED = "p2p-device-leaved"; -export declare const EVENT_REQUEST_STATUS = "p2p-request-status"; -export declare const EVENT_P2P_REQUEST_FORCE_OPEN = "p2p-request-force-open"; -export declare const EVENT_P2P_CONNECTED = "p2p-connected"; -export declare const EVENT_P2P_DISCONNECTED = "p2p-disconnected"; -export declare const EVENT_P2P_REPLICATOR_STATUS = "p2p-replicator-status"; -export declare const EVENT_P2P_REPLICATOR_PROGRESS = "p2p-replicator-progress"; -declare global { - interface LSEvents { - [EVENT_SERVER_STATUS]: P2PServerInfo; - [EVENT_MAKE_DECISION]: AcceptanceDecision; - [EVENT_REVOKE_DECISION]: RevokeAcceptanceDecision; - [EVENT_ADVERTISEMENT_RECEIVED]: Advertisement; - [EVENT_DEVICE_LEAVED]: string; - [EVENT_REQUEST_STATUS]: undefined; - [EVENT_P2P_REQUEST_FORCE_OPEN]: undefined; - [EVENT_P2P_CONNECTED]: undefined; - [EVENT_P2P_DISCONNECTED]: undefined; - } -} -export declare class TrysteroReplicatorP2PServer { - _env: ReplicatorHostEnv; - _room?: Room; - _serverPeerId: string; - _activeRoomId: string; - ___send?: ActionSender; - assignedFunctions: Map; - clients: Map; - _bindingObjects: BindableObject[]; - _rpcRoom?: RpcRoom; - protected _peerStatusEventCleanup: (() => void) | undefined; - protected _peerFailureAnalysisCleanup: (() => void) | undefined; - protected _peerConnectionEventCleanup(): void; - _diagStats: DiagRTCStats; - get isDisposed(): boolean; - get isServing(): boolean; - ensureLeaved(): Promise; - setRoom(room: Room): Promise; - shutdown(): Promise; - dispatchConnectionStatus(): Promise; - constructor(env: ReplicatorHostEnv, _serverPeerId?: string); - makeDecision(decision: AcceptanceDecision): Promise; - revokeDecision(decision: RevokeAcceptanceDecision): Promise; - get room(): Room | undefined; - get serverPeerId(): string; - get db(): PouchDB.Database; - get confirm(): import("../../interfaces/Confirm").Confirm; - get settings(): P2PSyncSetting; - get isEnabled(): boolean; - get deviceInfo(): FullFilledDeviceInfo; - _sendAdvertisement?: ActionSender; - sendAdvertisement(peerId?: string): void; - _knownAdvertisements: Map; - get knownAdvertisements(): Advertisement[]; - onAdvertisement(data: Advertisement, peerId: string): void; - acceptedPeers: StoredMapLike; - temporaryAcceptedPeers: Map; - confirmUserToAccept(peerId: string): Promise; - _confirmUserToAccept(peerId: string): Promise; - _acceptablePeers: Computed<[settings: P2PSyncSetting], RegExp[]>; - _shouldDenyPeers: Computed<[settings: P2PSyncSetting], RegExp[]>; - isAcceptablePeer(peerId: string): Promise; - __send(data: Payload, peerId: string): Promise; - processArrivedRPC(data: Payload, peerId: string): Promise; - private _onPeerJoin; - private _onPeerLeave; - activePeer: Map; - onAfterJoinRoom(): void; - startService(bindings?: BindableObject[]): Promise; - start(bindings?: BindableObject[]): Promise; - /** - * @deprecated Use serveFunction or serveObject instead. This is only for backward compatibility and may be removed in the future. - * @param type - * @param func - */ - serveFunction(type: string, func: (peerId: string, ...args: T) => U | Promise): void; - serveObject(obj: BindableObject): void; - __onResponse(data: Response, peerId: string): void; - __onRequest(data: Request, peerId: string): Promise; - close(): Promise; - getConnection(peerId: string): TrysteroReplicatorP2PClient; - get rpcRoom(): RpcRoom | undefined; -} -export { TrysteroReplicatorP2PServer as P2PHost }; diff --git a/_types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts b/_types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts deleted file mode 100644 index 3fe87433..00000000 --- a/_types/src/lib/src/replication/trystero/UseP2PReplicatorResult.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator"; -import type { P2PLogCollector } from "./P2PLogCollector"; -export type UseP2PReplicatorResult = { - replicator: LiveSyncTrysteroReplicator; -}; -export type P2PPaneParams = { - replicator: LiveSyncTrysteroReplicator; - p2pLogCollector: P2PLogCollector; - storeP2PStatusLine: ReactiveSource; -}; diff --git a/_types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts b/_types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts deleted file mode 100644 index 7eab73e0..00000000 --- a/_types/src/lib/src/replication/trystero/addP2PEventHandlers.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator"; -import type { Advertisement } from "./types"; -/** - * Minimal interface that a P2P replicator instance should satisfy for addP2PEventHandlers to work. - */ -export interface P2PReplicatorLike { - onNewPeer(peer: Advertisement): Promise | void; - onPeerLeaved(peerId: string): void; - requestStatus(): void; - open(): Promise; - close(): Promise; - /** Indicates whether the room is currently active. */ - readonly isServing?: boolean; - /** Legacy: host object that may carry isServing (LiveSyncTrysteroReplicator). */ - readonly server?: { - isServing?: boolean; - }; -} -/** - * Add event handlers for P2P replication related events. - * @param instance P2PReplicatorLike instance - */ -export declare function addP2PEventHandlers(instance: P2PReplicatorLike): void; -/** - * open P2P replicator if not opened yet. - * @param instance - */ -export declare function openP2PReplicator(instance: P2PReplicatorLike): Promise; -/** - * close P2P replicator - * @param instance - */ -export declare function closeP2PReplicator(instance: P2PReplicatorLike): Promise; -export type { LiveSyncTrysteroReplicator }; diff --git a/_types/src/lib/src/replication/trystero/rpcCompat.d.ts b/_types/src/lib/src/replication/trystero/rpcCompat.d.ts deleted file mode 100644 index 1e3e69a0..00000000 --- a/_types/src/lib/src/replication/trystero/rpcCompat.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function toRpcMethodName(method: string): string; diff --git a/_types/src/lib/src/replication/trystero/types.d.ts b/_types/src/lib/src/replication/trystero/types.d.ts deleted file mode 100644 index 2efa7d8c..00000000 --- a/_types/src/lib/src/replication/trystero/types.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { JsonLike } from "@lib/rpc"; -import type { P2PSyncSetting, EntryDoc } from "@lib/common/types"; -import type { SimpleStore } from "@lib/common/utils"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { AsyncActivityRunner } from "@lib/interfaces/AsyncActivityRunner"; -export declare const DIRECTION_REQUEST = "request"; -export type DIRECTION_REQUEST = typeof DIRECTION_REQUEST; -export declare const DIRECTION_RESPONSE = "response"; -export type DIRECTION_RESPONSE = typeof DIRECTION_RESPONSE; -export declare const DEFAULT_RPC_TIMEOUT = 30000; -export declare const BULK_GET_RPC_TIMEOUT = 40000; -export type BindableFunction = (...args: any[]) => any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export type NonPrivateMethodKeys = { - [K in keyof T]: K extends `_${string}` ? never : K extends `constructor` ? never : T[K] extends BindableFunction ? K : never; -}[keyof T]; -export type BindableObject = { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - [k in NonPrivateMethodKeys]: T[k] extends BindableFunction ? T[k] : never; -}; -export type ConnectionInfo = { - relayURIs: string[]; - roomId: string; - password: string; - appId: string; -}; -export declare class ResponsePreventedError extends Error { - constructor(message: string); -} -export type Request = { - type: string; - direction: DIRECTION_REQUEST; - seq: number; - args: T; -}; -export type Response = { - type: string; - direction: DIRECTION_RESPONSE; - seq: number; - data?: T; - error?: JsonLike; -}; -export type DeviceInfo = { - currentPeerId: string; - name?: string; - version?: string; - platform?: string; - decision?: DeviceDecisions; -}; -export type DeviceInfoForRequest = { - currentPeerId: string; - name: string; -}; -export type FullFilledDeviceInfo = { - currentPeerId: string; - name: string; - version: string; - platform: string; - decision?: DeviceDecisions; -}; -export declare enum DeviceDecisions { - ACCEPT = "accepted", - REJECT = "rejected", - IGNORE = "ignore" -} -export declare const ID_P2PKnownDevices = "_local/P2PKnownDevices"; -export type KnownDevices = { - _id: typeof ID_P2PKnownDevices; - devices: { - [deviceName: string]: DeviceDecisions; - }; -}; -export type Payload = Request | Response; -export interface ReplicatorHost { - deviceName: string; - platform: string; - confirm: Confirm; -} -export interface ReplicatorHostEnv extends ReplicatorHost { - settings: P2PSyncSetting; - db: PouchDB.Database; - simpleStore: SimpleStore; - runFiniteReplicationActivity?: AsyncActivityRunner["run"]; - processReplicatedDocs(docs: Array>): void | Promise; -} -export type Advertisement = { - peerId: string; - name: string; - platform: string; -}; -export declare const KEY_DEVICE_DECISIONS = "p2p-device-decisions"; diff --git a/_types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts b/_types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts deleted file mode 100644 index 28df7f32..00000000 --- a/_types/src/lib/src/replication/trystero/useP2PReplicatorCommands.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { UseP2PReplicatorResult } from "./UseP2PReplicatorResult"; -/** - * ServiceFeature: Registers event handlers for P2P replication and manages the lifecycle of a LiveSyncTrysteroReplicator instance. - * @param host - */ -export declare function useP2PReplicatorCommands(host: NecessaryServices<"API" | "setting", never>, { replicator }: UseP2PReplicatorResult): void; diff --git a/_types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts b/_types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts deleted file mode 100644 index 9fb24c80..00000000 --- a/_types/src/lib/src/replication/trystero/useP2PReplicatorFeature.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { LiveSyncTrysteroReplicator } from "./LiveSyncTrysteroReplicator"; -import { type UseP2PReplicatorResult } from "./UseP2PReplicatorResult"; -/** - * Factory type: given a replicator instance, returns the openReplicationUI callback for that instance. - * Injected by the host platform (e.g. Obsidian). CLI/headless environments omit this. - */ -export type OpenReplicationUIFactory = (replicator: LiveSyncTrysteroReplicator) => (showResult: boolean) => Promise; -/** Same shape as OpenReplicationUIFactory, used for the rebuild/replicateAllFromServer flow. */ -export type OpenRebuildUIFactory = OpenReplicationUIFactory; -/** - * ServiceFeature: P2P Replicator integration and lifecycle management. - * Registers a LiveSyncTrysteroReplicator instance as the active replicator when P2P is enabled in settings, - * and binds it to lifecycle events for proper initialization and cleanup. - * @param host - */ -export declare function useP2PReplicatorFeature(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator" | "remote", never>, openReplicationUIFactory?: OpenReplicationUIFactory, openRebuildUIFactory?: OpenRebuildUIFactory): UseP2PReplicatorResult; diff --git a/_types/src/lib/src/rpc/RpcRoom.d.ts b/_types/src/lib/src/rpc/RpcRoom.d.ts deleted file mode 100644 index 2a64ed85..00000000 --- a/_types/src/lib/src/rpc/RpcRoom.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { RpcSession } from "./RpcSession"; -import { type JsonLike, type RpcMethodHandler, type RpcRegisterOptions, type RpcRoomOptions } from "./types"; -export declare class RpcRoom { - private options; - private pending; - private inboundCalls; - private methods; - private sessions; - private outgoingChunkMap; - private incomingChunkMap; - private incomingChunkTimers; - private peerVersion; - private disposer; - constructor(options: RpcRoomOptions); - close(): void; - session(peerId: string): RpcSession; - register(method: string, handler: RpcMethodHandler, options?: RpcRegisterOptions): void; - invoke(peerId: string, method: string, args: JsonLike[], timeoutMs?: number): Promise; - cancel(peerId: string, requestId: string): Promise; - private sendEnvelope; - private scheduleMissingAck; - private onWireMessage; - private onEnvelopePayload; -} diff --git a/_types/src/lib/src/rpc/RpcSession.d.ts b/_types/src/lib/src/rpc/RpcSession.d.ts deleted file mode 100644 index ceee44e0..00000000 --- a/_types/src/lib/src/rpc/RpcSession.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { JsonLike } from "./types"; -import type { RpcRoom } from "./RpcRoom"; -export declare class RpcSession { - readonly peerId: string; - private room; - constructor(room: RpcRoom, peerId: string); - call(method: string, args?: JsonLike[], timeoutMs?: number): Promise; - createProxy(namespace: string): T; -} diff --git a/_types/src/lib/src/rpc/chunking.d.ts b/_types/src/lib/src/rpc/chunking.d.ts deleted file mode 100644 index fbbc2b53..00000000 --- a/_types/src/lib/src/rpc/chunking.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function estimateBytes(text: string): number; -export declare function splitIntoChunks(payload: string, maxBytes: number): string[]; -export declare class IncomingChunkBuffer { - total: number; - parts: Map; - constructor(total: number); - add(index: number, payload: string): void; - missingIndices(): number[]; - isComplete(): boolean; - toPayload(): string; -} diff --git a/_types/src/lib/src/rpc/errors.d.ts b/_types/src/lib/src/rpc/errors.d.ts deleted file mode 100644 index 2673f539..00000000 --- a/_types/src/lib/src/rpc/errors.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { JsonLike, RpcErrorCode, RpcErrorShape } from "./types"; -export declare class RpcError extends Error { - code: RpcErrorCode; - details?: JsonLike; - constructor(code: RpcErrorCode, message: string, details?: JsonLike); - toShape(): RpcErrorShape; -} -export declare function asRpcErrorShape(ex: unknown): RpcErrorShape; diff --git a/_types/src/lib/src/rpc/index.d.ts b/_types/src/lib/src/rpc/index.d.ts deleted file mode 100644 index 6df2bb5a..00000000 --- a/_types/src/lib/src/rpc/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { RpcRoom } from "./RpcRoom"; -export { RpcSession } from "./RpcSession"; -export { RpcError } from "./errors"; -export { exposeDB } from "./pouchdb/RpcPouchDBServer"; -export { RpcPouchDBProxy } from "./pouchdb/RpcPouchDBProxy"; -export type { JsonLike, RpcEnvelope, RpcErrorCode, RpcErrorShape, RpcMethodHandler, RpcRegisterOptions, RpcRoomOptions, RpcWireMessage, TransportAdapter, } from "./types"; diff --git a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts b/_types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts deleted file mode 100644 index d1e2a0fd..00000000 --- a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBProxy.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RpcSession } from "@lib/rpc/RpcSession"; -/** - * A PouchDB-compatible proxy that forwards all database operations to a remote - * peer via an {@link RpcSession}. - * - * The proxy exposes the same public interface as a PouchDB instance and can be - * passed directly to `PouchDB.replicate()` or `PouchDB.sync()` as either the - * source or the target database. It can also be used with {@link replicateShim} - * from `ReplicatorShim.ts`. - * - * The remote side must have called {@link exposeDB} to register the matching - * RPC handlers. - * - * ### Changes feed - * `changes()` returns an object that satisfies both the EventEmitter interface - * required by `pouchdb-replication` and the Promise interface required by - * `replicateShim` (i.e. it can be `await`ed directly). - * - * ### Error propagation - * PouchDB-specific error properties (`status`, `name`, `reason`) are preserved - * across the RPC transport and reconstructed on the proxy side so that callers - * such as `pouchdb-checkpointer` can inspect `err.status === 404` correctly. - */ -export declare class RpcPouchDBProxy extends EventEmitter { - /** The logical name of the remote database. */ - readonly name: string; - /** - * Stub ActiveTasks object required by `pouchdb-replication`. All - * operations are no-ops; task state is not tracked across the RPC boundary. - */ - readonly activeTasks: { - add: (_task: object) => unknown; - get: (_id: unknown) => unknown; - update: (_id: unknown, _update: object) => void; - remove: (_id: unknown, _err?: Error) => void; - list: () => unknown[]; - }; - private readonly session; - private readonly ns; - constructor(session: RpcSession, name: string, ns?: string); - /** - * Invoke an RPC method and reconstruct PouchDB error shapes on the response. - * - * When the remote handler wraps a PouchDB error via {@link exposeDB}'s - * `runDB` helper, the `RpcError.details` object carries `status`, `name`, - * and `reason`. This method rebuilds a plain `Error` with those properties - * so that callers (e.g. pouchdb-checkpointer) can use `err.status` / - * `err.name` as expected. - */ - private callDB; - info(): Promise; - id(): Promise; - /** - * Returns a `Changes`-compatible object that is simultaneously: - * - An **EventEmitter** with `change`, `complete`, and `error` events, plus - * a `cancel()` method — satisfying the interface consumed by - * `PouchDB.replicate()` / `PouchDB.sync()`. - * - A **thenable** (`then` / `catch`) — allowing `await db.changes(opts)` - * as used by `replicateShim`. - * - * The remote changes feed is always fetched as a one-shot snapshot - * (`live: false`). - */ - changes(opts: PouchDB.Core.ChangesOptions): PouchDB.Core.Changes; - get(id: string, opts?: PouchDB.Core.GetOptions): Promise; - put(doc: PouchDB.Core.PutDocument, opts?: PouchDB.Core.PutOptions): Promise; - bulkGet(opts: PouchDB.Core.BulkGetOptions): Promise>; - bulkDocs(docs: PouchDB.Core.PostDocument[] | { - docs: PouchDB.Core.PostDocument[]; - new_edits?: boolean; - }, opts?: PouchDB.Core.BulkDocsOptions): Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]>; - revsDiff(diff: PouchDB.Core.RevisionDiffOptions): Promise; - allDocs(opts?: PouchDB.Core.AllDocsOptions): Promise>; -} - -class EventEmitter { - on(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - once(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - off(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - emit(event: string | symbol, ...args: any[]): boolean; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - addListener(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - removeListener(event: string | symbol, listener: (...args: any[]) => void): this; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - removeAllListeners(event: string | symbol): this; -} diff --git a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts b/_types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts deleted file mode 100644 index d4c617dd..00000000 --- a/_types/src/lib/src/rpc/pouchdb/RpcPouchDBServer.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RpcRoom } from "@lib/rpc/RpcRoom"; -/** - * Exposes a PouchDB database as a set of RPC methods registered on an - * {@link RpcRoom}. The remote peer can access the database via - * {@link RpcPouchDBProxy}. - * - * All methods are registered under the given namespace prefix `ns` (default: - * `'pdb'`). For example, with the default namespace the method names are - * `pdb.info`, `pdb.id`, `pdb.changes`, `pdb.get`, `pdb.put`, `pdb.bulkGet`, - * `pdb.bulkDocs`, `pdb.revsDiff`, and `pdb.allDocs`. - * - * @param room The {@link RpcRoom} on which to register handlers. - * @param db The PouchDB database instance to expose. - * @param ns Method namespace prefix (default: `'pdb'`). - */ -export declare function exposeDB(room: RpcRoom, db: PouchDB.Database, ns?: string): void; diff --git a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts b/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts deleted file mode 100644 index bbb19f4d..00000000 --- a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DiagRTCStats, DiagRTCFailureDiagnosis } from "./DiagRTCPeerConnections.types"; -/** - * Subscribes to connection status updates. The callback will be called with the latest connection statistics whenever there is a change in the connection status of any RTCPeerConnection instance. - * Returns an unsubscribe function to stop receiving updates. - * - * @param callback - The function to call with the latest connection statistics. - * @returns A function that can be called to unsubscribe from updates. - */ -export declare function subscribeConnectionStatus(callback: (status: DiagRTCStats) => void): () => void; -/** - * Subscribes to failure diagnosis updates. The callback will be called with the diagnosis information whenever a connection failure is detected in any RTCPeerConnection instance. - * Returns an unsubscribe function to stop receiving updates. - * @param callback - The function to call with the diagnosis information. - * @returns A function that can be called to unsubscribe from updates. - */ -export declare function subscribeFailureDiagnosis(callback: (diagnosis: DiagRTCFailureDiagnosis) => void): () => void; -export type DiagRTCPeerConnectionConstructor = typeof RTCPeerConnection; -/** - * A wrapper around RTCPeerConnection to collect statistics for diagnostics. - * It extends the native (or globally-polyfilled) RTCPeerConnection and overrides its constructor to add event listeners for connection state changes, - * ice connection state changes, ice gathering state changes, and signaling state changes. It maintains a history of these states and logs the progress. - * It also tracks the number of new connections, failed connections, successful connections, and closed connections, and dispatches this information to subscribers. - */ -export declare function createDiagRTCPeerConnectionConstructor(): DiagRTCPeerConnectionConstructor; diff --git a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts b/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts deleted file mode 100644 index 767d1238..00000000 --- a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.types.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type DiagRTCConnectionStatus = { - connectionState: RTCPeerConnection["connectionState"]; - iceConnectionState: RTCPeerConnection["iceConnectionState"]; -}; -export type DiagRTCStats = { - totalNewConnections: number; - totalFailedConnections: number; - totalSuccessfulConnections: number; - totalClosedConnections: number; - details: Record; -}; -export type DiagRTCPeerConnectionInternalStateHistory = { - connectionHistory: RTCPeerConnection["connectionState"][]; - iceConnectionHistory: RTCPeerConnection["iceConnectionState"][]; - iceGatheringHistory: RTCPeerConnection["iceGatheringState"][]; - signalingHistory: RTCPeerConnection["signalingState"][]; -}; -export declare const DiagRTCFailureReasonCodes: { - readonly ICE_GATHERING_NOT_COMPLETED: "ICE_GATHERING_NOT_COMPLETED"; - readonly ICE_CONNECTIVITY_FAILED: "ICE_CONNECTIVITY_FAILED"; - readonly STUN_REQUEST_TIMEOUT: "STUN_REQUEST_TIMEOUT"; - readonly SIGNALING_NOT_STABLE: "SIGNALING_NOT_STABLE"; - readonly CONNECTION_DROPPED_AFTER_ESTABLISHED: "CONNECTION_DROPPED_AFTER_ESTABLISHED"; - readonly NETWORK_INTERRUPTED: "NETWORK_INTERRUPTED"; - readonly UNKNOWN: "UNKNOWN"; -}; -export type DiagRTCFailureDiagnosis = { - reasonCode: keyof typeof DiagRTCFailureReasonCodes; - userMessage: string; -}; -export type DiagRTCFailureStats = { - diagnosis: DiagRTCFailureDiagnosis; - stats: { - reportsCount: number; - selectedPair: { - id: string; - state: string; - localCandidateId: string; - remoteCandidateId: string; - currentRoundTripTime: number | "unknown"; - totalRoundTripTime: number | "unknown"; - requestsSent: number | "unknown"; - responsesReceived: number | "unknown"; - packetsDiscardedOnSend: number | "unknown"; - bytesSent: number | "unknown"; - bytesReceived: number | "unknown"; - }; - }; -}; -export type DiagRTCPeerConnectionMetrics = { - selectedPair: Record | undefined; - selectedPairId: string; - state: string; - localCandidateId: string; - remoteCandidateId: string; - currentRoundTripTime: number | "unknown"; - totalRoundTripTime: number | "unknown"; - requestsSent: number | "unknown"; - responsesReceived: number | "unknown"; - packetsDiscardedOnSend: number | "unknown"; - bytesSent: number | "unknown"; - bytesReceived: number | "unknown"; - reports: unknown[]; -}; diff --git a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts b/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts deleted file mode 100644 index bd11f674..00000000 --- a/_types/src/lib/src/rpc/transports/DiagRTCPeerConnections.utils.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type DiagRTCPeerConnectionInternalStateHistory, type DiagRTCFailureDiagnosis, type DiagRTCPeerConnectionMetrics, type DiagRTCFailureStats } from "./DiagRTCPeerConnections.types"; -/** - * Diagnoses the failure reason of a failed RTCPeerConnection based on its internal state history and selected candidate pair information. - * @param internalStateHistory The internal state history of the RTCPeerConnection. - * @param selectedPair The selected candidate pair information. - * @returns The diagnosis of the RTC failure. - */ -export declare function diagnoseRtcFailure(internalStateHistory: DiagRTCPeerConnectionInternalStateHistory, selectedPair: Record | undefined): DiagRTCFailureDiagnosis; -/** - * Describes the current progress of the RTCPeerConnection based on its internal state history and selected candidate pair information. - * This is useful for providing user-friendly status messages during the connection process. - * @param internalStateHistory The internal state history of the RTCPeerConnection. - * @param selectedPair The selected candidate pair information. - * @returns A user-friendly description of the current connection progress. - */ -export declare function describeRTCProgress(internalStateHistory: DiagRTCPeerConnectionInternalStateHistory, selectedPair: Record | undefined): string; -/** - * Fetches the RTCPeerConnection statistics and returns a structured metrics object. - * This is useful for diagnosing connection issues and understanding the connection performance. - * @param instanceId An identifier for the RTCPeerConnection instance, used for logging. - * @param peer The RTCPeerConnection instance to fetch stats from. - * @returns A structured object containing the connection metrics, or undefined if fetching stats failed. - */ -export declare function getPeerConnectionStats(instanceId: string, peer: RTCPeerConnection): Promise; -/** - * Audits the RTCPeerConnection for failures and returns a structured failure report. - * This is useful for diagnosing connection issues and understanding the reasons for failure. - * @param instanceId An identifier for the RTCPeerConnection instance, used for logging. - * @param internalStateHistory The internal state history of the RTCPeerConnection. - * @param peer The RTCPeerConnection instance to audit. - * @returns A structured object containing the failure diagnosis and metrics, or undefined if auditing failed. - */ -export declare function auditRtcConnectionFailures(instanceId: string, internalStateHistory: DiagRTCPeerConnectionInternalStateHistory, peer: RTCPeerConnection): Promise; diff --git a/_types/src/lib/src/rpc/transports/TrysteroTransport.d.ts b/_types/src/lib/src/rpc/transports/TrysteroTransport.d.ts deleted file mode 100644 index c5896447..00000000 --- a/_types/src/lib/src/rpc/transports/TrysteroTransport.d.ts +++ /dev/null @@ -1,218 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type Room } from "@trystero-p2p/nostr"; -import { RpcRoom } from "@lib/rpc/RpcRoom"; -import { RpcPouchDBProxy } from "@lib/rpc/pouchdb/RpcPouchDBProxy"; -import type { RpcRoomOptions, TransportAdapter } from "@lib/rpc/types"; -import type { P2PConnectionInfo } from "@lib/common/models/setting.type"; -/** - * Lightweight presence record broadcast by each peer when it joins the room. - * The `peerId` field MUST match the Trystero sender ID to prevent spoofing. - */ -export type TrysteroAdvertisement = { - /** The Trystero `selfId` of the sending peer. */ - peerId: string; - /** Human-readable device / vault name. */ - name: string; - /** Optional platform tag (e.g. `"desktop"`, `"mobile"`). */ - platform: string; -}; -/** The handle returned by {@link attachAdvertisement}. */ -export type AdvertisementHandle = { - /** - * All currently known peers, keyed by `peerId`. - * Updated in real-time as advertisements arrive or peers leave. - */ - readonly peers: ReadonlyMap; - /** - * (Re-)broadcast your own advertisement. - * Pass a `toPeerId` to send only to that peer; omit to broadcast to all. - */ - sendAdvertisement(toPeerId?: string): Promise; - /** Stop listening for advertisements and clear the peer map. */ - stop(): void; -}; -/** - * Attach advertisement-based peer discovery to an already-joined Trystero room. - * - * - Sends your advertisement to every peer that joins after this call. - * - Stores incoming advertisements in `handle.peers`. - * - Removes entries when peers leave. - * - * @param room An active Trystero room. - * @param localPeerId Your own Trystero `selfId`. - * @param name Human-readable name broadcast to other peers. - * @param platform Optional platform string (default `"unknown"`). - */ -export declare function attachAdvertisement(room: Room, localPeerId: string, name: string, platform?: string): AdvertisementHandle; -/** - * Options forwarded to the `RpcRoom` constructor when creating a room through - * the high-level helpers (`serveTrysteroDB`, `connectTrysteroDBClient`, or by - * calling `joinTrysteroRoom` and then constructing `RpcRoom` manually). - * - * Trystero internally chunks at ~16 348 bytes (16 KiB minus a 36-byte header). - * Setting `maxWirePayloadBytes` above that threshold causes double-chunking. - * The Trystero-appropriate default is therefore **15 360 bytes** (15 KiB), - * which leaves ~1 KiB of headroom for the JSON wrapper overhead. - * - * Trystero uses WebRTC SCTP data channels which guarantee delivery and order, - * so missing-chunk retransmission virtually never triggers. `chunkMissingRetryMs` - * can safely be lowered from the generic default of 350 ms to **150 ms**. - */ -export type TrysteroRoomOptions = Pick & { - maxWirePayloadBytes?: number; - chunkMissingRetryMs?: number; -}; -/** Trystero-appropriate defaults for `RpcRoom`. */ -export declare const TRYSTERO_RPC_DEFAULTS: { - /** Stay below Trystero's internal ~16 348-byte chunk boundary. */ - readonly maxWirePayloadBytes: number; - /** SCTP guarantees delivery; retransmission is a last-resort safety net. */ - readonly chunkMissingRetryMs: 150; -}; -/** - * Wrap an already-joined Trystero `Room` as a `TransportAdapter` for `RpcRoom`. - * - * A dedicated Trystero action channel named `"rpc2"` is created on the room. - * Note: Trystero does not expose per-handler unsubscription for `onPeerJoin` - * / `onPeerLeave`, so the returned cleanup stubs are no-ops. Close the - * Trystero room via `leave()` when done. - * - * @param room An active Trystero `Room` returned by `joinRoom()`. - * @returns A `TransportAdapter` that can be passed to `new RpcRoom(...)`. - */ -export declare function wrapTrysteroRoom(room: Room): TransportAdapter; -/** The result of joining a Trystero room. */ -export type TrysteroRoomHandle = { - /** `TransportAdapter` ready to pass to `new RpcRoom(...)`. */ - transport: TransportAdapter; - /** This peer's own ID (Trystero `selfId`). */ - peerId: string; - /** Leave the Trystero room and release resources. */ - leave: () => Promise; - /** - * Attach advertisement-based peer discovery to this room. - * Call once after joining; returns a handle to read and re-broadcast. - */ - advertise: (name: string, platform?: string) => AdvertisementHandle; - /** - * Returns the current WebRTC peer connections keyed by peer ID. - * Equivalent to the Trystero `room.getPeers()` call. - */ - getPeers: () => Record; - /** - * The underlying Trystero `Room` instance. - * Use when you need raw access to Trystero APIs such as `makeAction`, - * `onPeerJoin`, or `onPeerLeave` beyond what the helpers expose. - */ - room: Room; -}; -/** - * Join a Trystero room from a {@link P2PConnectionInfo} and return a - * `TransportAdapter` together with this peer's own ID. - * - * The raw passphrase is hashed with `mixedHash` before being passed to - * Trystero, matching the convention used by `TrysteroReplicatorP2PServer`. - */ -export declare function joinTrysteroRoom(settings: P2PConnectionInfo): TrysteroRoomHandle; -/** - * Join a Trystero room from a `sls+p2p://` connection string and return a - * `TransportAdapter` together with this peer's own ID. - * - * The URL must be parseable by {@link ConnectionStringParser.parse} as type - * `"p2p"`, i.e. it must start with `sls+p2p://`. - * - * @example - * ```ts - * const handle = joinTrysteroRoomFromUrl( - * "sls+p2p://my-room?relays=wss://relay.example.com&appId=my-app" - * ); - * const rpcRoom = new RpcRoom({ transport: handle.transport }); - * ``` - */ -export declare function joinTrysteroRoomFromUrl(url: string): TrysteroRoomHandle; -/** The result of starting a DB server over Trystero. */ -export type TrysteroDBServerHandle = { - /** This peer's own ID. Share it with clients so they can call `session()`. */ - peerId: string; - /** The `RpcRoom` that hosts the DB methods. */ - rpcRoom: RpcRoom; - /** Stop serving and leave the room. */ - close: () => Promise; - /** - * Attach advertisement-based peer discovery so that clients can find this - * server without needing the peerId passed out-of-band. - */ - advertise: (name: string, platform?: string) => AdvertisementHandle; -}; -/** - * **Server side.** Join a Trystero room and expose `db` as a set of RPC - * methods. The caller's `peerId` is returned so that it can be shared with - * clients (e.g. via a separate signalling channel or advertisement). - * - * @param settings P2P connection settings (from `P2PConnectionInfo` or parsed - * from a `sls+p2p://` URL via {@link ConnectionStringParser}). - * @param db The PouchDB database to expose. - * @param ns Method namespace (default `"pdb"`). - * - * @example - * ```ts - * const server = serveTrysteroDB(settings, db); - * console.log("server peer ID:", server.peerId); - * // Share server.peerId with the client out-of-band. - * ``` - */ -export declare function serveTrysteroDB(settings: P2PConnectionInfo, db: PouchDB.Database, ns?: string, options?: TrysteroRoomOptions): TrysteroDBServerHandle; -/** The result of connecting to a DB server over Trystero. */ -export type TrysteroDBClientHandle = { - /** Proxy object usable with `PouchDB.replicate()` or `replicateShim()`. */ - proxy: RpcPouchDBProxy; - /** The `RpcRoom` used to communicate with the server. */ - rpcRoom: RpcRoom; - /** Disconnect and leave the room. */ - close: () => Promise; - /** - * Attach advertisement-based peer discovery so this client can locate the - * server peer without needing its peerId passed statically. - */ - advertise: (name: string, platform?: string) => AdvertisementHandle; -}; -/** - * **Client side.** Join a Trystero room and create an {@link RpcPouchDBProxy} - * pointing at `serverPeerId`. The proxy can be passed directly to - * `PouchDB.replicate()` or `replicateShim()`. - * - * @param settings P2P connection settings. - * @param serverPeerId The `peerId` of the server peer (returned by - * {@link serveTrysteroDB}). - * @param dbName Logical name for the remote database. - * @param ns Method namespace, must match the server's `ns` - * (default `"pdb"`). - * - * @example - * ```ts - * const client = connectTrysteroDBClient(settings, serverPeerId, "my-db"); - * await PouchDB.replicate(client.proxy, localDb, { live: false }); - * await client.close(); - * ``` - */ -export declare function connectTrysteroDBClient(settings: P2PConnectionInfo, serverPeerId: string, dbName: string, ns?: string, options?: TrysteroRoomOptions): TrysteroDBClientHandle; -/** - * Join a Trystero room, advertise as `name`, wait `timeoutMs`, collect all - * received peer advertisements, then leave the room and return the results. - * - * Useful for CLI-style peer discovery where you need a one-shot list of - * present peers before deciding how to connect. - * - * @param settings P2P connection settings. - * @param name Your local device / vault name advertised to others. - * @param timeoutMs How long to listen before returning (milliseconds). - * @param platform Optional platform tag (default `"unknown"`). - * - * @example - * ```ts - * const peers = await collectTrysteroAdvertisements(settings, "my-vault", 5000); - * console.log(peers.map(p => `${p.name} (${p.peerId})`)); - * ``` - */ -export declare function collectTrysteroAdvertisements(settings: P2PConnectionInfo, name: string, timeoutMs: number, platform?: string): Promise; diff --git a/_types/src/lib/src/rpc/transports/trysteroUtils.d.ts b/_types/src/lib/src/rpc/transports/trysteroUtils.d.ts deleted file mode 100644 index af185119..00000000 --- a/_types/src/lib/src/rpc/transports/trysteroUtils.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { BaseRoomConfig } from "@trystero-p2p/nostr"; -import type { P2PConnectionInfo } from "@lib/common/models/setting.type"; -export declare function generateJoinRoomOptions(settings: P2PConnectionInfo): BaseRoomConfig; diff --git a/_types/src/lib/src/rpc/types.d.ts b/_types/src/lib/src/rpc/types.d.ts deleted file mode 100644 index fac0f406..00000000 --- a/_types/src/lib/src/rpc/types.d.ts +++ /dev/null @@ -1,77 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const RPC_VERSION_MAJOR = 1; -export declare const RPC_VERSION_MINOR = 0; -export type JsonLike = null | boolean | number | string | JsonLike[] | { - [key: string]: JsonLike; -}; -export type RpcErrorCode = "TIMEOUT" | "NOT_CONNECTED" | "REMOTE_ERROR" | "CANCELLED" | "PROTOCOL_ERROR"; -export type RpcErrorShape = { - code: RpcErrorCode; - message: string; - details?: JsonLike; -}; -export type RpcRequestEnvelope = { - kind: "request"; - requestId: string; - method: string; - args: JsonLike[]; -}; -export type RpcResponseEnvelope = { - kind: "response"; - requestId: string; - ok: true; - data: JsonLike; -}; -export type RpcResponseErrorEnvelope = { - kind: "response"; - requestId: string; - ok: false; - error: RpcErrorShape; -}; -export type RpcCancelEnvelope = { - kind: "cancel"; - requestId: string; -}; -export type RpcHandshakeEnvelope = { - kind: "handshake"; - versionMajor: number; - versionMinor: number; -}; -export type RpcEnvelope = RpcRequestEnvelope | RpcResponseEnvelope | RpcResponseErrorEnvelope | RpcCancelEnvelope | RpcHandshakeEnvelope; -export type RpcWireMessageRaw = { - wire: "raw"; - payload: string; -}; -export type RpcWireMessageChunk = { - wire: "chunk"; - streamId: string; - index: number; - total: number; - payload: string; -}; -export type RpcWireMessageChunkAck = { - wire: "chunk-ack"; - streamId: string; - missing: number[]; -}; -export type RpcWireMessage = RpcWireMessageRaw | RpcWireMessageChunk | RpcWireMessageChunkAck; -export type TransportOnMessage = (message: RpcWireMessage, peerId: string) => void; -export type TransportOnPeer = (peerId: string) => void; -export interface TransportAdapter { - send(message: RpcWireMessage, peerId: string): void | Promise; - onMessage(handler: TransportOnMessage): () => void; - onPeerJoin?(handler: TransportOnPeer): () => void; - onPeerLeave?(handler: TransportOnPeer): () => void; -} -export type RpcRoomOptions = { - transport: TransportAdapter; - maxWirePayloadBytes?: number; - chunkMissingRetryMs?: number; - canAcceptRequest?: (peerId: string, method: string) => boolean | Promise; - onProtocolWarning?: (message: string, peerId?: string) => void; -}; -export type RpcMethodHandler = (peerId: string, ...args: T) => U | Promise; -export type RpcRegisterOptions = { - serial?: boolean; -}; diff --git a/_types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts b/_types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts deleted file mode 100644 index f694406f..00000000 --- a/_types/src/lib/src/serviceFeatures/checkRemoteSize.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { createInstanceLogFunction, type LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -/** - * Notify when checking remote storage size is not configured. - * @returns true if the check is passed or user has configured the notification, false to block subsequent processes. (always true). - */ -export declare function onNotifyRemoteSizeNotConfiguredFactory(host: NecessaryServices<"appLifecycle" | "API" | "setting", never>, log: ReturnType): () => Promise; -/** - * Notify when the remote storage size exceed the threshold. - * @returns true if the check is passed or user has chosen to ignore the warning, false to block subsequent processes. - * @param host - * @param log - * @returns - */ -export declare function onNotifyRemoteSizeExceedFactory(host: NecessaryServices<"API" | "setting" | "replicator", "rebuilder">, log: ReturnType): () => Promise; -/** - * Scan the remote storage size and notify if it is not configured or exceed the threshold. - * @param host The necessary services required for the operation. - * @param log The logging function to use for logging messages. - * @param resetThreshold Whether to reset the notification threshold before scanning. This is useful when you want to force the notification to show up again. - * @returns A promise that resolves to true if all checks pass or user has configured the notification. - */ -export declare function scanAllStat(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">, log: LogFunction, resetThreshold?: boolean): Promise; -/** - * Associate the remote storage size check feature with the app lifecycle events. - * @param host - */ -export declare function useCheckRemoteSize(host: NecessaryServices<"API" | "setting" | "replicator" | "appLifecycle", "rebuilder">): void; diff --git a/_types/src/lib/src/serviceFeatures/offlineScanner.d.ts b/_types/src/lib/src/serviceFeatures/offlineScanner.d.ts deleted file mode 100644 index e1af96ff..00000000 --- a/_types/src/lib/src/serviceFeatures/offlineScanner.d.ts +++ /dev/null @@ -1,130 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePathWithPrefix, type FilePathWithPrefixLC, type MetaEntry, type UXFileInfoStub, type ObsidianLiveSyncSettings, type LOG_LEVEL } from "@lib/common/types"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; -/** - * Collect deleted files that have expired according to retention policy. - * @param host Services container - * @param log Logging function - * @returns Array of expired deletion history - */ -export declare function collectDeletedFiles(host: NecessaryServices<"setting" | "database", never>, log: LogFunction): Promise; -/** - * Get the file path from a meta entry. - * This is a helper function to extract path from various document types. - * @param doc Meta entry document - * @returns Path string - */ -export declare function getPathFromEntry(host: NecessaryServices<"path", never>, doc: MetaEntry): FilePathWithPrefix; -/** - * Synchronise a single file between database and storage based on freshness comparison. - * @param host Services container - * @param log Logging function - * @param file Storage file information - * @param doc Database entry - */ -export declare function syncFileBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, doc: MetaEntry): Promise; -export declare function canProceedScan(host: NecessaryServices<"keyValueDB" | "setting", never>, errorManager: UnresolvedErrorManager, log: LogFunction, showingNotice?: boolean, ignoreSuspending?: boolean): boolean; -/** - * Convert file path to lower case if the settings indicate that filename case should be handled insensitively. - * @param settings - * @param path - * @returns - */ -export declare function convertCase(settings: ObsidianLiveSyncSettings, path: T): FilePathWithPrefixLC; -export declare function collectFilesOnStorage(host: NecessaryServices<"vault", "storageAccess">, settings: ObsidianLiveSyncSettings, log: LogFunction): Promise<{ - storageFileNameMap: { - [k: string]: UXFileInfoStub; - }; - storageFileNames: FilePathWithPrefix[]; - storageFileNameCI2CS: Record; -}>; -export declare function collectDatabaseFiles(host: NecessaryServices<"database" | "vault" | "path", never>, settings: ObsidianLiveSyncSettings, log: LogFunction, showingNotice: boolean): Promise<{ - databaseFileNameMap: { - [k: string]: MetaEntry; - }; - databaseFileNames: FilePathWithPrefix[]; - databaseFileNameCI2CS: Record; -}>; -export declare function updateToDatabase(host: NecessaryServices<"vault", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, file: UXFileInfoStub): Promise; -export declare function updateToStorage(host: NecessaryServices<"vault" | "path", "fileHandler">, log: LogFunction, logLevel: LOG_LEVEL, w: MetaEntry): Promise; -export declare function syncStorageAndDatabase(host: NecessaryServices<"setting" | "vault" | "path", "storageAccess" | "fileHandler">, log: LogFunction, file: UXFileInfoStub, logLevel: LOG_LEVEL, doc: MetaEntry): Promise; -export declare const FullScanModes: { - readonly DB_APPLY: "db-apply"; - readonly NEWER_WINS: "newer-wins"; -}; -export declare const ExtraOnRemote: { - /** - * Delete database entries if they are missing on storage. - */ - readonly DELETE_LOCAL_MISSING: "delete-local-missing"; -}; -export declare const ExtraOnLocal: { - /** - * Delete local files if they were deleted on database. - */ - readonly DELETE_DB_DELETED: "delete-db-deleted"; - /** - * Delete local files if they are missing on database or were deleted on database. - */ - readonly DELETE_DB_MISSING: "delete-db-missing"; - /** - * Merge local files to database - */ - readonly APPEND_STORAGE_ONLY: "append-storage-only"; -}; -export interface FullScanOptions { - mode: FullScanMode; - extraOnLocal?: (typeof ExtraOnLocal)[keyof typeof ExtraOnLocal]; - extraOnRemote?: (typeof ExtraOnRemote)[keyof typeof ExtraOnRemote]; - omitEvents?: boolean; - showingNotice?: boolean; - ignoreSuspending?: boolean; -} -export type FullScanMode = (typeof FullScanModes)[keyof typeof FullScanModes]; -type FilePair = { - file: UXFileInfoStub; - doc: MetaEntry; -} | { - file: undefined; - doc: MetaEntry; -} | { - file: UXFileInfoStub; - doc: undefined; -}; -type FilePairState = "storage-only" | "db-only" | "db-only-deleted" | "both" | "both-db-deleted"; -type FilePairAction = "update-db" | "update-storage" | "sync-newer" | "delete-local" | "delete-db" | "skip"; -export declare function getFilePairState(pair: FilePair): FilePairState; -/** - * Determine the action to be taken for a file pair based on its state and the selected scan options. - */ -export declare function resolveFilePairAction(state: FilePairState, options: FullScanOptions): FilePairAction; -/** - * Synchronise all files between database and storage based on the selected mode and options. - * @param host Core - * @param log Logging function - * @param errorManager Error manager - * @param options Full scan options - */ -export declare function synchroniseAllFilesBetweenDBandStorage(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options: FullScanOptions): Promise; -export declare function normaliseFullScanOptions(showingNoticeOrOptions: Partial | boolean | undefined, ignoreSuspending?: boolean): FullScanOptions; -/** - * Perform a full scan and synchronisation between database and storage. - * @param host Services container - * @param log Logging function - * @param errorManager Error manager - * @param showingNotice Whether to show notices during scanning - * @param ignoreSuspending Whether to ignore suspension settings - * @returns True if scan completed successfully - */ -export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, options?: Partial): Promise; -export declare function performFullScan(host: NecessaryServices<"setting" | "vault" | "path" | "fileProcessing" | "database" | "keyValueDB", "storageAccess" | "fileHandler">, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, ignoreSuspending?: boolean): Promise; -/** - * Associate the initialiser file feature with the app lifecycle events. - * This function binds initialization handlers to the appropriate lifecycle events. - * @param host Services container with required dependencies - */ -export declare function useOfflineScanner(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "keyValueDB" | "replicator", "storageAccess" | "fileHandler">): void; -export {}; diff --git a/_types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts b/_types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts deleted file mode 100644 index 5e3b84b7..00000000 --- a/_types/src/lib/src/serviceFeatures/prepareDatabaseForUse.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -/** - * Initialise the database and trigger a full vault scan. - * @param host Services container - * @param log Logging function - * @param errorManager Error manager - * @param showingNotice Whether to show notices during initialisation - * @param reopenDatabase Whether to reopen the database connection - * @param ignoreSuspending Whether to ignore suspension settings - * @returns True if initialisation succeeded - */ -export declare function prepareDatabaseForUse(host: NecessaryServices<"appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>, log: LogFunction, errorManager: UnresolvedErrorManager, showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean): Promise; -/** - * Associate the initialiser file feature with the app lifecycle events. - * This function binds initialization handlers to the appropriate lifecycle events. - * @param host Services container with required dependencies - */ -export declare function usePrepareDatabaseForUse(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "replicator", never>): void; diff --git a/_types/src/lib/src/serviceFeatures/remoteConfig.d.ts b/_types/src/lib/src/serviceFeatures/remoteConfig.d.ts deleted file mode 100644 index 5fa4781e..00000000 --- a/_types/src/lib/src/serviceFeatures/remoteConfig.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "@lib/common/logger"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -export type RemoteConfigHost = NecessaryServices<"setting" | "UI" | "replication" | "control" | "appLifecycle" | "API", never>; -export declare function migrateLegacyRemoteConfigurationsInPlace(settings: ObsidianLiveSyncSettings, log?: (message: string, level?: LOG_LEVEL) => void): boolean; -/** - * Generate a unique ID for a new remote configuration. - * @returns A unique string identifier. - */ -export declare function createRemoteConfigurationId(): string; -/** - * Keep compatibility for users who were already using P2P as their main active remote. - */ -export declare function migrateP2PActiveRemoteConfigurationIdInPlace(settings: ObsidianLiveSyncSettings): boolean; -/** - * SF:RemoteConfig - Service Feature for Remote Configuration Management - */ -/** - * Migrates existing flat settings to the new multiple remote configurations list. - */ -export declare function migrateToMultipleRemoteConfigurations(host: RemoteConfigHost): Promise; -/** - * Logic to switch the active configuration. - */ -export declare function activateRemoteConfiguration(settings: ObsidianLiveSyncSettings, id: string): ObsidianLiveSyncSettings | false; -/** - * Apply a dedicated P2P remote configuration onto runtime P2P-related fields, - * while keeping the current `remoteType` unchanged. - */ -export declare function activateP2PRemoteConfiguration(settings: ObsidianLiveSyncSettings, id: string): ObsidianLiveSyncSettings | false; -/** - * Command: Switch Active Remote - */ -export declare function commandSwitchActiveRemote(host: RemoteConfigHost): Promise; -/** - * Command: Replicate with specific remote - */ -export declare function commandReplicateWithSpecificRemote(host: RemoteConfigHost): Promise; -/** - * Migration feature to be used during initialisation. - */ -export declare function useRemoteConfigurationMigration(host: RemoteConfigHost): void; -/** - * Hook to set up remote configuration features (Commands). - */ -export declare function useRemoteConfiguration(host: RemoteConfigHost): boolean; diff --git a/_types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts b/_types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts deleted file mode 100644 index 2caa30c0..00000000 --- a/_types/src/lib/src/serviceFeatures/setupObsidian/qrCode.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { SetupFeatureHost } from "./types"; -export declare function encodeSetupSettingsAsQR(host: SetupFeatureHost): Promise; -export declare function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>): void; diff --git a/_types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts b/_types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts deleted file mode 100644 index af1aa8d8..00000000 --- a/_types/src/lib/src/serviceFeatures/setupObsidian/setupUri.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import type { SetupFeatureHost } from "./types"; -export declare function askEncryptingPassphrase(host: SetupFeatureHost): Promise; -export declare function copySetupURI(host: SetupFeatureHost, log: LogFunction, stripExtra?: boolean): Promise; -export declare function copySetupURIFull(host: SetupFeatureHost, log: LogFunction): Promise; -export declare function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>): void; diff --git a/_types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts b/_types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts deleted file mode 100644 index b45c29eb..00000000 --- a/_types/src/lib/src/serviceFeatures/setupObsidian/types.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -export type SetupFeatureHost = NecessaryServices<"API" | "UI" | "setting", never>; diff --git a/_types/src/lib/src/serviceFeatures/targetFilter.d.ts b/_types/src/lib/src/serviceFeatures/targetFilter.d.ts deleted file mode 100644 index 615f71df..00000000 --- a/_types/src/lib/src/serviceFeatures/targetFilter.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type UXFileInfoStub } from "@lib/common/types"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -/** - * This is a simple handler that accepts all files. - */ -export declare function isAcceptedAlwaysFactory(host: NecessaryServices, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -/** - * Check if a file is accepted based on filename duplication in the vault. - */ -export declare function isAcceptedInFilenameDuplicationFactory(host: NecessaryServices<"vault" | "fileProcessing", "storageAccess">, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -/** - * Check if a file is accepted by the local database (e.g., not rejected by the local DB's target file check). - * Local database responsible for non-internal files, syncOnlyRegEx, syncIgnoreRegEx - * This possibly should be separated. - */ -export declare function isAcceptedByLocalDBFactory(host: NecessaryServices<"database" | "databaseEvents", never>, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -/** - * Factory function to create the isAcceptedByIgnoreFiles handler. - * This handler checks if a file is ignored based on the ignore files specified in the settings. - * It also caches the ignore file contents for performance and listens to settings changes to invalidate the cache. - */ -export declare function isAcceptedByIgnoreFilesFactory(host: NecessaryServices<"setting" | "appLifecycle", "storageAccess">, log: LogFunction): (file: string | UXFileInfoStub) => Promise; -export declare function useTargetFilters(host: NecessaryServices<"API" | "vault" | "fileProcessing" | "setting" | "appLifecycle" | "database" | "databaseEvents", "storageAccess">): void; diff --git a/_types/src/lib/src/serviceModules/FileAccessBase.d.ts b/_types/src/lib/src/serviceModules/FileAccessBase.d.ts deleted file mode 100644 index 4cb02b36..00000000 --- a/_types/src/lib/src/serviceModules/FileAccessBase.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXDataWriteOptions, UXFileInfoStub, UXFolderInfo } from "@lib/common/types.ts"; -import type { IStorageAccessManager } from "@lib/interfaces/StorageAccess.ts"; -import type { IAPIService, IPathService, ISettingService, IVaultService } from "@lib/services/base/IService.ts"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils.ts"; -import type { FileWithFileStat } from "@lib/common/models/fileaccess.type"; -import type { IFileSystemAdapter } from "./adapters"; -export declare function toArrayBuffer(arr: Uint8Array | ArrayBuffer | DataView): ArrayBuffer; -export interface FileAccessBaseDependencies { - vaultService: IVaultService; - storageAccessManager: IStorageAccessManager; - settingService: ISettingService; - pathService: IPathService; - APIService: IAPIService; -} -/** - * Type helper to extract the abstract file type from a file system adapter - */ -export type ExtractAbstractFile = T extends IFileSystemAdapter ? A : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the file type from a file system adapter - */ -export type ExtractFile = T extends IFileSystemAdapter ? F : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the folder type from a file system adapter - */ -export type ExtractFolder = T extends IFileSystemAdapter ? D : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Type helper to extract the stat type from a file system adapter - */ -export type ExtractStat = T extends IFileSystemAdapter ? S : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * Base class for file access operations - * Uses adapter pattern for platform-specific implementations - * - * @template TAdapter - The file system adapter type, which determines all native file types - */ -export declare class FileAccessBase> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - protected storageAccessManager: IStorageAccessManager; - protected vaultService: IVaultService; - protected settingService: ISettingService; - protected APIService: IAPIService; - protected path: IPathService; - protected adapter: TAdapter; - _log: ReturnType; - constructor(adapter: TAdapter, dependencies: FileAccessBaseDependencies); - isFile(file: UXFileInfoStub | ExtractAbstractFile | FilePath | ExtractFolder | ExtractFile | null): file is ExtractFile; - isFolder(item: UXFileInfoStub | ExtractAbstractFile | FilePath | ExtractFolder | ExtractFile | null): item is ExtractFolder; - getPath(file: ExtractAbstractFile | string): FilePath; - nativeFileToUXFileInfoStub(file: ExtractFile): UXFileInfoStub; - nativeFolderToUXFolder(file: ExtractFolder): UXFolderInfo; - normalisePath(path: string): string; - protected _writeOp | string, U>(file: T, callback: (path: FilePath, file: T) => Promise): Promise; - protected _readOp | string, U>(file: T, callback: (path: FilePath, file: T) => Promise): Promise; - tryAdapterStat(file: ExtractFile | string): Promise; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - adapterStat(file: ExtractFile | string): Promise | null>; - adapterExists(file: ExtractFile | string): Promise; - adapterRemove(file: ExtractFile | string): Promise; - adapterRead(file: ExtractFile | string): Promise; - adapterReadBinary(file: ExtractFile | string): Promise; - adapterReadAuto(file: ExtractFile | string): Promise; - adapterWrite(file: ExtractFile | string, data: string | ArrayBuffer | Uint8Array, options?: UXDataWriteOptions): Promise; - adapterList(basePath: string): Promise<{ - files: string[]; - folders: string[]; - }>; - vaultCacheRead(file: ExtractFile): Promise; - vaultRead(file: ExtractFile): Promise; - vaultReadBinary(file: ExtractFile): Promise; - vaultReadAuto(file: ExtractFile): Promise; - vaultModify(file: ExtractFile, data: string | ArrayBuffer | Uint8Array, options?: UXDataWriteOptions): Promise; - vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: UXDataWriteOptions): Promise>; - vaultRename(file: ExtractFile, newPath: string): Promise>; - trigger(name: string, ...data: unknown[]): void; - reconcileInternalFile(path: string): Promise; - /** - * Append data to a file using the adapter's append method. This is useful for large files that cannot be read into memory. - * Please note that this method does not check concurrent modifications. - * @param normalizedPath - * @param data - * @param options - * @returns - */ - adapterAppend(normalizedPath: string, data: string, options?: UXDataWriteOptions): Promise; - delete(file: ExtractAbstractFile | ExtractFolder, force?: boolean): Promise; - trash(file: ExtractAbstractFile | ExtractFolder, force?: boolean): Promise; - protected isStorageInsensitive(): boolean; - getAbstractFileByPath(path: FilePath | string): Promise | null>; - getFiles(): Promise[]>; - ensureDirectory(fullPath: string): Promise; - touch(file: ExtractFile | FilePath): Promise; - recentlyTouched(file: ExtractFile | UXFileInfoStub | FileWithFileStat): boolean; - clearTouched(): void; -} diff --git a/_types/src/lib/src/serviceModules/Rebuilder.d.ts b/_types/src/lib/src/serviceModules/Rebuilder.d.ts deleted file mode 100644 index 30c8178f..00000000 --- a/_types/src/lib/src/serviceModules/Rebuilder.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { APIService } from "@lib/services/base/APIService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { DatabaseEventService } from "@lib/services/base/DatabaseEventService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService"; -import type { RemoteService } from "@lib/services/base/RemoteService"; -import type { ReplicationService } from "@lib/services/base/ReplicationService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import type { SettingService } from "@lib/services/base/SettingService"; -import type { VaultService } from "@lib/services/base/VaultService"; -import type { UIService } from "@lib/services/implements/base/UIService"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -import type { ControlService } from "@lib/services/base/ControlService"; -export interface ServiceRebuilderDependencies { - appLifecycle: AppLifecycleService; - API: APIService; - UI: UIService; - setting: SettingService; - remote: RemoteService; - databaseEvents: DatabaseEventService; - storageAccess: StorageAccess; - replicator: ReplicatorService; - vault: VaultService; - replication: ReplicationService; - database: DatabaseService; - fileHandler: IFileHandler; - control: ControlService; -} -export declare class ServiceRebuilder extends ServiceModuleBase implements Rebuilder { - private appLifecycle; - private API; - private UI; - private setting; - private remote; - private databaseEvents; - private storageAccess; - private replicator; - private vault; - private replication; - private database; - private fileHandler; - private control; - constructor(services: ServiceRebuilderDependencies); - $performRebuildDB(method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks"): Promise; - informOptionalFeatures(): Promise; - askUsingOptionalFeature(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }): Promise; - rebuildRemote(): Promise; - private performRemoteRebuild; - $rebuildRemote(): Promise; - rebuildEverything(): Promise; - private performRebuildEverything; - $rebuildEverything(): Promise; - $fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean): Promise; - $fetchLocalDBFast(autoResume: boolean): Promise; - scheduleRebuild(): Promise; - scheduleFetch(): Promise; - private _tryResetRemoteDatabase; - private _onResetLocalDatabase; - suspendAllSync(): Promise; - suspendReflectingDatabase(ignoreMinIO?: boolean): Promise; - resumeReflectingDatabase(ignoreMinIO?: boolean): Promise; - fetchLocal(makeLocalChunkBeforeSync?: boolean, preventMakeLocalFilesBeforeSync?: boolean, autoResume?: boolean): Promise; - private performFetchLocal; - fetchLocalDBFast(autoResume: boolean): Promise; - private performFetchLocalDBFast; - /** - * Finish rebuild process with resuming the reflection. - * - * @param ignoreMinIO Whether to ignore minio for resuming the reflection. - */ - finishRebuild(ignoreMinIO?: boolean): Promise; - /** - * Fetch local database with making all chunks. - * This is a wrapper for {@link fetchLocal} with makeLocalChunkBeforeSync = true. - * - * @returns - */ - fetchLocalWithRebuild(): Promise; - private _allSuspendAllSync; - resetLocalDatabase(): Promise; - private getFastFetchCheckpoint; - private saveFastFetchCheckpoint; - private clearFastFetchCheckpoint; -} diff --git a/_types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts b/_types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts deleted file mode 100644 index 9e1b60e2..00000000 --- a/_types/src/lib/src/serviceModules/ServiceDatabaseFileAccessBase.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfoStub, FilePathWithPrefix, UXFileInfo, MetaEntry, LoadedEntry, FilePath } from "@lib/common/types"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { APIService } from "@lib/services/base/APIService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService"; -import type { PathService } from "@lib/services/base/PathService"; -import type { VaultService } from "@lib/services/base/VaultService"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -export interface ServiceDatabaseFileAccessDependencies { - API: APIService; - vault: VaultService; - storageAccess: StorageAccess; - path: PathService; - database: DatabaseService; -} -export declare class ServiceDatabaseFileAccessBase extends ServiceModuleBase implements DatabaseFileAccess { - private vault; - private storageAccess; - private path; - private database; - constructor(services: ServiceDatabaseFileAccessDependencies); - checkIsTargetFile(file: UXFileInfoStub | FilePathWithPrefix): Promise; - delete(file: UXFileInfoStub | FilePathWithPrefix, rev?: string): Promise; - createChunks(file: UXFileInfo, force?: boolean, skipCheck?: boolean): Promise; - store(file: UXFileInfo, force?: boolean, skipCheck?: boolean): Promise; - storeAsConflictedRevision(file: UXFileInfo, currentRev: string, skipCheck?: boolean): Promise; - storeContent(path: FilePathWithPrefix, content: string): Promise; - private __store; - private getParentRev; - hasContentInRevisionHistory(file: UXFileInfoStub | FilePathWithPrefix, content: string | string[] | Blob | ArrayBuffer, currentRev?: string): Promise; - getConflictedRevs(file: UXFileInfoStub | FilePathWithPrefix): Promise; - fetch(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean): Promise; - fetchEntryMeta(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, skipCheck?: boolean): Promise; - fetchEntryFromMeta(meta: MetaEntry, waitForReady?: boolean, skipCheck?: boolean): Promise; - fetchEntry(file: UXFileInfoStub | FilePathWithPrefix, rev?: string, waitForReady?: boolean, skipCheck?: boolean): Promise; - deleteFromDBbyPath(fullPath: FilePath | FilePathWithPrefix, rev?: string): Promise; -} diff --git a/_types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts b/_types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts deleted file mode 100644 index 358ab54f..00000000 --- a/_types/src/lib/src/serviceModules/ServiceFileAccessBase.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, FilePathWithPrefix, UXDataWriteOptions, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXStat } from "@lib/common/types"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -import type { APIService } from "@lib/services/base/APIService"; -import type { IStorageAccessManager, StorageAccess } from "@lib/interfaces/StorageAccess.ts"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService"; -import { StorageEventManager } from "@lib/interfaces/StorageEventManager.ts"; -import { type CustomRegExp } from "@lib/common/utils"; -import type { VaultService } from "@lib/services/base/VaultService"; -import type { SettingService } from "@lib/services/base/SettingService"; -import type { FileAccessBase, ExtractFile, ExtractFolder } from "@lib/serviceModules/FileAccessBase"; -import type { IFileSystemAdapter } from "@lib/serviceModules/adapters"; -export interface StorageAccessBaseDependencies> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - API: APIService; - appLifecycle: AppLifecycleService; - fileProcessing: FileProcessingService; - vault: VaultService; - setting: SettingService; - storageEventManager: StorageEventManager; - storageAccessManager: IStorageAccessManager; - vaultAccess: FileAccessBase; -} -export declare class ServiceFileAccessBase> extends ServiceModuleBase> implements StorageAccess { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - private vaultAccess; - private vaultManager; - private vault; - private setting; - constructor(services: StorageAccessBaseDependencies); - restoreState(): Promise; - _everyOnFirstInitialize(): Promise; - _everyCommitPendingFileEvent(): Promise; - normalisePath(path: string): string; - writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - readFileAuto(path: string): Promise; - readFileText(path: string): Promise; - isExists(path: string): Promise; - renameFile(file: UXFileInfoStub | FilePathWithPrefix, newPath: FilePathWithPrefix): Promise; - writeHiddenFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise; - appendHiddenFile(path: string, data: string, opt?: UXDataWriteOptions): Promise; - stat(path: string): Promise; - statHidden(path: string): Promise; - removeHidden(path: string): Promise; - readHiddenFileAuto(path: string): Promise; - readHiddenFileText(path: string): Promise; - readHiddenFileBinary(path: string): Promise; - isExistsIncludeHidden(path: string): Promise; - ensureDir(path: string): Promise; - _triggerFileEvent(event: string, path: string): Promise; - triggerFileEvent(event: string, path: string): void; - triggerHiddenFile(path: string): Promise; - getFileStub(path: string): Promise; - readStubContent(stub: UXFileInfoStub): Promise; - getStub(path: string): Promise; - getFiles(): Promise; - getFileNames(): Promise; - getFilesIncludeHidden(basePath: string, includeFilter?: CustomRegExp[], excludeFilter?: CustomRegExp[], skipFolder?: string[]): Promise; - touched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - recentlyTouched(file: UXFileInfoStub | FilePathWithPrefix): Promise; - clearTouched(): void; - delete(file: FilePathWithPrefix | UXFileInfoStub | string, force: boolean): Promise; - trash(file: FilePathWithPrefix | UXFileInfoStub | string, system: boolean): Promise; - __deleteVaultItem(file: ExtractFile | ExtractFolder): Promise; - deleteVaultItem(fileSrc: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise; -} diff --git a/_types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts b/_types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts deleted file mode 100644 index f534285e..00000000 --- a/_types/src/lib/src/serviceModules/ServiceFileHandlerBase.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AnyEntry, FilePath, FilePathWithPrefix, MetaEntry, UXFileInfo, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -import type { IFileHandler } from "@lib/interfaces/FileHandler.ts"; -import { ServiceModuleBase } from "@lib/serviceModules/ServiceModuleBase"; -import type { APIService } from "@lib/services/base/APIService.ts"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess.ts"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService.ts"; -import type { ReplicationService } from "@lib/services/base/ReplicationService.ts"; -import type { ConflictService } from "@lib/services/base/ConflictService.ts"; -import type { PathService } from "@lib/services/base/PathService.ts"; -import type { SettingService } from "@lib/services/base/SettingService.ts"; -import type { VaultService } from "@lib/services/base/VaultService.ts"; -export interface ServiceFileHandlerDependencies { - API: APIService; - databaseFileAccess: DatabaseFileAccess; - storageAccess: StorageAccess; - fileProcessing: FileProcessingService; - replication: ReplicationService; - conflict: ConflictService; - path: PathService; - setting: SettingService; - vault: VaultService; -} -export declare abstract class ServiceFileHandlerBase extends ServiceModuleBase implements IFileHandler { - private databaseFileAccess; - private storageAccess; - private conflict; - private path; - private setting; - private vault; - constructor(services: ServiceFileHandlerDependencies); - get db(): DatabaseFileAccess; - get storage(): StorageAccess; - getPath(entry: AnyEntry): FilePathWithPrefix; - getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix; - readFileFromStub(file: UXFileInfoStub | UXFileInfo): Promise; - private infoToStub; - storeFileToDB(info: UXFileInfoStub | UXFileInfo | UXInternalFileInfoStub | FilePathWithPrefix, force?: boolean, onlyChunks?: boolean): Promise; - deleteFileFromDB(info: UXFileInfoStub | UXInternalFileInfoStub | FilePath): Promise; - renameFileInDB(info: UXFileInfoStub | UXFileInfo, oldPath: FilePath | FilePathWithPrefix): Promise; - deleteRevisionFromDB(info: UXFileInfoStub | FilePath | FilePathWithPrefix, rev: string): Promise; - resolveConflictedByDeletingRevision(info: UXFileInfoStub | FilePath, rev: string): Promise; - dbToStorageWithSpecificRev(info: UXFileInfoStub | UXFileInfo | FilePath | null, rev: string, force?: boolean): Promise; - dbToStorage(entryInfo: MetaEntry | FilePathWithPrefix, info: UXFileInfoStub | UXFileInfo | FilePath | null, force?: boolean): Promise; - private preserveUnsyncedStorageAsConflict; - private _anyHandlerProcessesFileEvent; - _anyProcessReplicatedDoc(entry: MetaEntry): Promise; - createAllChunks(showingNotice?: boolean): Promise; -} diff --git a/_types/src/lib/src/serviceModules/ServiceModuleBase.d.ts b/_types/src/lib/src/serviceModules/ServiceModuleBase.d.ts deleted file mode 100644 index 751a754b..00000000 --- a/_types/src/lib/src/serviceModules/ServiceModuleBase.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { APIService } from "@lib/services/base/APIService"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -export interface ServiceModuleBaseDependencies { - API: APIService; -} -export declare abstract class ServiceModuleBase { - _log: ReturnType; - get name(): string; - constructor(services: T); -} diff --git a/_types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts deleted file mode 100644 index bd30f936..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IConversionAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types.ts"; -/** - * Conversion adapter interface - * Converts between native file system types and universal types - */ -export interface IConversionAdapter { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Convert a native file object to a universal file info stub - */ - nativeFileToUXFileInfoStub(file: TNativeFile): UXFileInfoStub; - /** - * Convert a native folder object to a universal folder info - */ - nativeFolderToUXFolder(folder: TNativeFolder): UXFolderInfo; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts deleted file mode 100644 index cba85358..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IFileSystemAdapter.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXStat } from "@lib/common/types.ts"; -import type { IPathAdapter } from "./IPathAdapter.ts"; -import type { ITypeGuardAdapter } from "./ITypeGuardAdapter.ts"; -import type { IConversionAdapter } from "./IConversionAdapter.ts"; -import type { IStorageAdapter } from "./IStorageAdapter.ts"; -import type { IVaultAdapter } from "./IVaultAdapter.ts"; -/** - * Main file system adapter interface - * Composes all other adapters and provides platform-specific operations - */ -export interface IFileSystemAdapter { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** Path operations */ - readonly path: IPathAdapter; - /** Type guard operations */ - readonly typeGuard: ITypeGuardAdapter; - /** Conversion operations */ - readonly conversion: IConversionAdapter; - /** Storage operations */ - readonly storage: IStorageAdapter; - /** Vault operations */ - readonly vault: IVaultAdapter; - /** - * Get a file or folder by path (case-sensitive) - */ - getAbstractFileByPath(path: FilePath | string): Promise; - /** - * Get a file or folder by path (case-insensitive) - */ - getAbstractFileByPathInsensitive(path: FilePath | string): Promise; - /** - * Get all files in the vault - */ - getFiles(): Promise; - /** - * Rename a file and refresh any platform-specific caches - */ - renameFile(file: TNativeFile, newPath: string): Promise; - /** - * Get file statistics from a native file object - */ - statFromNative(file: TNativeFile): Promise; - /** - * Reconcile internal file state - * Platform-specific operation for syncing internal metadata - */ - reconcileInternalFile(path: string): Promise; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts deleted file mode 100644 index edfcb133..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IPathAdapter.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types.ts"; -/** - * Path operations adapter interface - * Handles path normalization and extraction - */ -export interface IPathAdapter { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Get the path from a file object or return the path string as-is - */ - getPath(file: TNativeAbstractFile | string): FilePath; - /** - * Normalize a path according to the platform's conventions - */ - normalisePath(path: string): string; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts deleted file mode 100644 index 22f68af3..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IStorageAdapter.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Focused compatibility views of the existing storage adapter. - * - * These interfaces allow internal consumers to depend on fewer operations while - * retaining the existing `UXStat`, `UXDataWriteOptions`, and method semantics. - * They are not a neutral or extraction-ready storage API. Their names, shared - * types, and behavioural contracts may change when the cross-platform contract - * is designed and stabilised. - * - * @packageDocumentation - */ -import type { UXDataWriteOptions, UXStat } from "@lib/common/types.ts"; -/** File and directory existence and metadata operations. */ -export interface IStorageMetadataAccess { - exists(path: string): Promise; - trystat(path: string): Promise; - stat(path: string): Promise; -} -/** Text-file read operation. */ -export interface IStorageTextReadAccess { - read(path: string): Promise; -} -/** Binary-file read operation. */ -export interface IStorageBinaryReadAccess { - readBinary(path: string): Promise; -} -/** Text-file write operation. */ -export interface IStorageTextWriteAccess { - write(path: string, data: string, options?: UXDataWriteOptions): Promise; -} -/** Binary-file write operation. */ -export interface IStorageBinaryWriteAccess { - writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; -} -/** Text-file append operation. */ -export interface IStorageTextAppendAccess { - append(path: string, data: string, options?: UXDataWriteOptions): Promise; -} -/** Directory creation and direct-child listing operations. */ -export interface IStorageDirectoryAccess { - mkdir(path: string): Promise; - list(basePath: string): Promise<{ - files: string[]; - folders: string[]; - }>; -} -/** File or directory removal operation. */ -export interface IStorageRemoveAccess { - remove(path: string): Promise; -} -/** - * Storage adapter interface - * Backwards-compatible aggregate of the focused storage capability views. - */ -export interface IStorageAdapter extends IStorageMetadataAccess, IStorageTextReadAccess, IStorageBinaryReadAccess, IStorageTextWriteAccess, IStorageBinaryWriteAccess, IStorageTextAppendAccess, IStorageDirectoryAccess, IStorageRemoveAccess { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} diff --git a/_types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts deleted file mode 100644 index 2195823f..00000000 --- a/_types/src/lib/src/serviceModules/adapters/ITypeGuardAdapter.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * Type guard adapter interface - * Provides runtime type checking for native file system objects - */ -export interface ITypeGuardAdapter { - /** - * Check if the given object is a file - */ - isFile(file: unknown): file is TNativeFile; - /** - * Check if the given object is a folder - */ - isFolder(item: unknown): item is TNativeFolder; -} diff --git a/_types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts b/_types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts deleted file mode 100644 index c6fd22b3..00000000 --- a/_types/src/lib/src/serviceModules/adapters/IVaultAdapter.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXDataWriteOptions } from "@lib/common/types.ts"; -/** - * Vault adapter interface - * High-level file operations that interact with the vault layer - */ -export interface IVaultAdapter { - /** - * Read a file as text - */ - read(file: TNativeFile): Promise; - /** - * Read a file using cached content if available - */ - cachedRead(file: TNativeFile): Promise; - /** - * Read a file as binary - */ - readBinary(file: TNativeFile): Promise; - /** - * Modify an existing file with text content - */ - modify(file: TNativeFile, data: string, options?: UXDataWriteOptions): Promise; - /** - * Modify an existing file with binary content - */ - modifyBinary(file: TNativeFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - /** - * Create a new file with text content - */ - create(path: string, data: string, options?: UXDataWriteOptions): Promise; - /** - * Create a new file with binary content - */ - createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - /** - * Rename or move an existing file - */ - rename(file: TNativeFile, newPath: string): Promise; - /** - * Delete a file or folder - */ - delete(file: TNativeFile | TNativeFolder, force?: boolean): Promise; - /** - * Move a file or folder to trash - */ - trash(file: TNativeFile | TNativeFolder, force?: boolean): Promise; - /** - * Trigger an event in the vault - */ - trigger(name: string, ...data: unknown[]): void; -} diff --git a/_types/src/lib/src/serviceModules/adapters/index.d.ts b/_types/src/lib/src/serviceModules/adapters/index.d.ts deleted file mode 100644 index 62d40d69..00000000 --- a/_types/src/lib/src/serviceModules/adapters/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export type { IPathAdapter } from "./IPathAdapter.ts"; -export type { ITypeGuardAdapter } from "./ITypeGuardAdapter.ts"; -export type { IConversionAdapter } from "./IConversionAdapter.ts"; -export type { IStorageAdapter, IStorageBinaryReadAccess, IStorageBinaryWriteAccess, IStorageDirectoryAccess, IStorageMetadataAccess, IStorageRemoveAccess, IStorageTextAppendAccess, IStorageTextReadAccess, IStorageTextWriteAccess, } from "./IStorageAdapter.ts"; -export type { IVaultAdapter } from "./IVaultAdapter.ts"; -export type { IFileSystemAdapter } from "./IFileSystemAdapter.ts"; diff --git a/_types/src/lib/src/services/BrowserServices.d.ts b/_types/src/lib/src/services/BrowserServices.d.ts deleted file mode 100644 index ed077cff..00000000 --- a/_types/src/lib/src/services/BrowserServices.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableVaultServiceCompat } from "@lib/services/implements/injectable/InjectableVaultService"; -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -export declare class BrowserServiceHub extends InjectableServiceHub { - get vault(): InjectableVaultServiceCompat; - constructor(); -} diff --git a/_types/src/lib/src/services/HeadlessServices.d.ts b/_types/src/lib/src/services/HeadlessServices.d.ts deleted file mode 100644 index 56f1f6dd..00000000 --- a/_types/src/lib/src/services/HeadlessServices.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -import type { Constructor } from "@lib/common/utils.type"; -export declare class HeadlessServiceHub extends InjectableServiceHub { - constructor(_context?: T, overrideServiceConstructor?: { - database?: Constructor>; - }); -} diff --git a/_types/src/lib/src/services/InjectableServices.d.ts b/_types/src/lib/src/services/InjectableServices.d.ts deleted file mode 100644 index 917671eb..00000000 --- a/_types/src/lib/src/services/InjectableServices.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; diff --git a/_types/src/lib/src/services/ServiceHub.d.ts b/_types/src/lib/src/services/ServiceHub.d.ts deleted file mode 100644 index 0f3dd195..00000000 --- a/_types/src/lib/src/services/ServiceHub.d.ts +++ /dev/null @@ -1,83 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UIService } from "./implements/base/UIService.ts"; -import type { ConfigService } from "@lib/services/base/ConfigService.ts"; -import type { TestService } from "@lib/services/base/TestService.ts"; -import type { VaultService } from "@lib/services/base/VaultService.ts"; -import type { TweakValueService } from "@lib/services/base/TweakValueService.ts"; -import type { SettingService } from "@lib/services/base/SettingService.ts"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService.ts"; -import type { ConflictService } from "@lib/services/base/ConflictService.ts"; -import type { RemoteService } from "@lib/services/base/RemoteService.ts"; -import type { ReplicationService } from "@lib/services/base/ReplicationService.ts"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService.ts"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService.ts"; -import type { DatabaseEventService } from "@lib/services/base/DatabaseEventService.ts"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -import type { PathService } from "@lib/services/base/PathService.ts"; -import type { APIService } from "@lib/services/base/APIService.ts"; -import type { ServiceContext } from "./base/ServiceBase.ts"; -import type { IServiceHub } from "./base/IService.ts"; -import type { KeyValueDBService } from "./base/KeyValueDBService.ts"; -import type { ControlService } from "./base/ControlService.ts"; -export type ServiceInstances = { - API?: APIService; - path?: PathService; - database?: DatabaseService; - databaseEvents?: DatabaseEventService; - replicator?: ReplicatorService; - fileProcessing?: FileProcessingService; - replication?: ReplicationService; - remote?: RemoteService; - conflict?: ConflictService; - appLifecycle?: AppLifecycleService; - setting?: SettingService; - tweakValue?: TweakValueService; - vault?: VaultService; - test?: TestService; - ui?: UIService; - config?: ConfigService; - keyValueDB?: KeyValueDBService; - control?: ControlService; -}; -export declare abstract class ServiceHub implements IServiceHub { - protected context: T; - protected abstract _api: APIService; - protected abstract _path: PathService; - protected abstract _database: DatabaseService; - protected abstract _databaseEvents: DatabaseEventService; - protected abstract _replicator: ReplicatorService; - protected abstract _fileProcessing: FileProcessingService; - protected abstract _replication: ReplicationService; - protected abstract _remote: RemoteService; - protected abstract _conflict: ConflictService; - protected abstract _appLifecycle: AppLifecycleService; - protected abstract _setting: SettingService; - protected abstract _tweakValue: TweakValueService; - protected abstract _vault: VaultService; - protected abstract _test: TestService; - protected abstract _ui: UIService; - protected abstract _config: ConfigService; - protected abstract _keyValueDB: KeyValueDBService; - protected abstract _control: ControlService; - protected _injected: ServiceInstances; - constructor(context: T, services?: ServiceInstances); - get API(): APIService; - get path(): PathService; - get database(): DatabaseService; - get databaseEvents(): DatabaseEventService; - get replicator(): ReplicatorService; - get fileProcessing(): FileProcessingService; - get replication(): ReplicationService; - get remote(): RemoteService; - get conflict(): ConflictService; - get appLifecycle(): AppLifecycleService; - get setting(): SettingService; - get tweakValue(): TweakValueService; - get vault(): VaultService; - get test(): TestService; - get UI(): UIService; - get config(): ConfigService; - get keyValueDB(): KeyValueDBService; - get control(): ControlService; -} diff --git a/_types/src/lib/src/services/base/APIService.d.ts b/_types/src/lib/src/services/base/APIService.d.ts deleted file mode 100644 index da01ac7c..00000000 --- a/_types/src/lib/src/services/base/APIService.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import type { LOG_LEVEL } from "@lib/common/logger"; -import type { IAPIService, ICommandCompat } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { Confirm } from "@lib/interfaces/Confirm"; -/** - * The APIService provides methods for interacting with the plug-in's API, - */ -export declare abstract class APIService extends ServiceBase implements IAPIService { - /** - * Get a custom fetch handler for making HTTP requests (e.g., S3 without CORS issues). - */ - abstract getCustomFetchHandler(): FetchHttpHandler; - /** - * Add a log entry to the log (Now not used). - * @param message The log message. - * @param level The log level. - * @param key The log key. - */ - abstract addLog(message: unknown, level: LOG_LEVEL, key: string): void; - /** - * Check if the app is running on a mobile device. - * @returns true if running on mobile, false otherwise. - */ - abstract isMobile(): boolean; - /** - * Show a window (or in Obsidian, a leaf). - * @param type The type of window to show. - */ - abstract showWindow(type: string): Promise; - /** - * Show a window on the right sidebar when supported. - * Platforms that do not support sidebars can fall back to showWindow. - */ - showWindowOnRight(type: string): Promise; - /** - * returns App ID. In Obsidian, it is vault ID. - */ - abstract getAppID(): string; - /** - * Returns the vaultName which system has identified, without any additional suffix. - */ - abstract getSystemVaultName(): string; - /** - * Check if the last POST request failed due to payload size. - */ - abstract getPlatform(): string; - abstract getAppVersion(): string; - abstract getPluginVersion(): string; - abstract getCrypto(): Crypto; - /** - * Register a command to the runtime. - * @param command - */ - abstract addCommand(command: TCommand): TCommand; - /** - * Register a window (or leaf) type to the runtime. - * @param type - * @param factory - */ - abstract registerWindow(type: string, factory: (leaf: T) => unknown): void; - /** - * Add a ribbon icon to the UI. - * @param icon - * @param title - * @param callback - */ - abstract addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement; - /** - * Register a protocol handler. - * @param action The action string for the protocol. - * @param handler The handler function for the protocol. - */ - abstract registerProtocolHandler(action: string, handler: (params: Record) => unknown): void; - /** - * Get the basic UI component for showing a confirmation dialog to the user. - */ - abstract get confirm(): Confirm; - requestCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - responseCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - get isOnline(): boolean; - webCompatFetch(req: string | Request, opts?: RequestInit): Promise; - nativeFetch(req: string | Request, opts?: RequestInit): Promise; - abstract addStatusBarItem(): HTMLElement | undefined; - setInterval(handler: () => void, timeout: number): number; - clearInterval(timerId: number): void; - /** - * Get the system configuration directory. - * This is used for storing configuration files in a consistent location across platforms. - * @returns - */ - getSystemConfigDir(): string; -} diff --git a/_types/src/lib/src/services/base/AppLifecycleService.d.ts b/_types/src/lib/src/services/base/AppLifecycleService.d.ts deleted file mode 100644 index 96f85bf7..00000000 --- a/_types/src/lib/src/services/base/AppLifecycleService.d.ts +++ /dev/null @@ -1,134 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IAppLifecycleService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -export interface AppLifecycleServiceDependencies { - settingService: ISettingService; -} -/** - * The AppLifecycleService provides methods for managing the plug-in's lifecycle events. - */ -export declare abstract class AppLifecycleService extends ServiceBase implements IAppLifecycleService { - protected readonly settingService: ISettingService; - constructor(context: T, dependencies: AppLifecycleServiceDependencies); - /** - * Event triggered when the plug-in's layout is ready. - * In Obsidian, it is after the workspace is ready. - */ - readonly onLayoutReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is being initialized for the first time. - * This is only called once per plug-in lifecycle. - */ - readonly onFirstInitialise: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is fully ready. - * This is called after all initialisation processes are complete. - */ - readonly onReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered to wire up necessary event listeners. - * This is typically called during the initialisation phase. - */ - readonly onWireUpEvents: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is being initialised. - */ - readonly onInitialise: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is loading. - * This is typically called during the quite early initialisation phase, before everything. - * In Obsidian, it is in the onload() method of the plugin. - */ - readonly onLoad: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in's settings have been loaded and applied. - */ - readonly onSettingLoaded: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in has fully loaded. - * This is typically called after all initialisation and loading processes are complete. - */ - readonly onLoaded: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Scan for any startup issues that may affect the plug-in's operation. - */ - readonly onScanningStartupIssues: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is unloading (e.g., during app shutdown or plug-in disable). - * This is typically called during the unload() method of the plugin. - * Entry point to unload everything. - */ - readonly onAppUnload: import("@lib/services/lib/HandlerUtils").CollectiveHandlerFunction<() => Promise, unknown>; - /** - * Event triggered before the plug-in is unloaded. - * This is typically used to perform any necessary cleanup or save state before the plug-in is unloaded. - */ - readonly onBeforeUnload: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the plug-in is being unloaded. - */ - readonly onUnload: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Perform an immediate restart of the application. - * Note that this is not graceful, and not only the plug-in. APPLICATION (means Obsidian) will be restarted. - */ - abstract performRestart(): void; - /** - * Ask the user for a restart. - * @param message Optional message to display to the user when asking for a restart. - */ - abstract askRestart(message?: string): void; - /** - * Schedule a restart of the application. - * After the current operation is done, the application will be restarted. - * Note that this is not graceful, and not only the plug-in. APPLICATION (means Obsidian) will be restarted. - */ - abstract scheduleRestart(): void; - /** - * Event triggered when the application is being suspended (e.g., system sleep). - */ - readonly onSuspending: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered when the application is resuming from a suspended state. - */ - readonly onResuming: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered after the application has resumed from a suspended state. - */ - readonly onResumed: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - private _isSuspended; - /** - * Check if the plug-in is currently suspended. - * Also consider the plug-in as suspended if it is not configured, to prevent any issues before configuration. - */ - isSuspended(): boolean; - /** - * Set the suspension state of the plug-in. - * @param suspend Set to true to suspend the plug-in, false to resume. - */ - setSuspended(suspend: boolean): void; - private _isReady; - /** - * Check if the plug-in is ready. - * A ready plug-in means it has been fully initialised and is operational. - * If not ready, most operations will be blocked. - */ - isReady(): boolean; - /** - * Mark the plug-in as ready. - */ - markIsReady(): void; - /** - * Reset the ready state of the plug-in. - */ - resetIsReady(): void; - /** - * Check if a restart has been scheduled. - */ - abstract isReloadingScheduled(): boolean; - /** - * Get unresolved error messages. - */ - readonly getUnresolvedMessages: import("@lib/services/lib/HandlerUtils").CollectiveHandlerFunction<() => Promise<(string | Error)[][]>, unknown>; -} diff --git a/_types/src/lib/src/services/base/ConfigService.d.ts b/_types/src/lib/src/services/base/ConfigService.d.ts deleted file mode 100644 index 258b2a85..00000000 --- a/_types/src/lib/src/services/base/ConfigService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IConfigService } from "@lib/services/base/IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -export declare abstract class ConfigService extends ServiceBase implements IConfigService { - abstract getSmallConfig(key: string): string | null; - abstract setSmallConfig(key: string, value: string): void; - abstract deleteSmallConfig(key: string): void; -} diff --git a/_types/src/lib/src/services/base/ConflictService.d.ts b/_types/src/lib/src/services/base/ConflictService.d.ts deleted file mode 100644 index fae199ca..00000000 --- a/_types/src/lib/src/services/base/ConflictService.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePathWithPrefix, MISSING_OR_ERROR, AUTO_MERGED } from "@lib/common/types"; -import type { IConflictService } from "@lib/services/base/IService"; -import { ServiceBase } from "@lib/services/base/ServiceBase"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -/** - * The ConflictService provides methods for handling file conflicts. - */ -export declare abstract class ConflictService extends ServiceBase implements IConflictService { - /** - * Get an optional conflict check method for a given file (virtual) path. - */ - readonly getOptionalConflictCheckMethod: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<(path: FilePathWithPrefix) => Promise, unknown>; - /** - * Queue a check for conflicts if the file is currently open in the editor. - * @param path The file (virtual) path to check for conflicts. - */ - abstract queueCheckForIfOpen(path: FilePathWithPrefix): Promise; - /** - * Queue a check for conflicts for a given file (virtual) path. - * @param path The file (virtual) path to check for conflicts. - */ - abstract queueCheckFor(path: FilePathWithPrefix): Promise; - /** - * Ensure all queued file conflict checks are processed. - */ - abstract ensureAllProcessed(): Promise; - /** - * Resolve a conflict by user interaction (e.g., showing a modal dialog). - * @param filename The file (virtual) path with conflict. - * @param conflictCheckResult The result of the conflict check. - * @returns A promise that resolves to true if the conflict was resolved, false if not, or undefined if no action was taken. - */ - readonly resolveByUserInteraction: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<(filename: FilePathWithPrefix, conflictCheckResult: import("@lib/common/types").diff_result) => Promise, unknown>; - /** - * Resolve a conflict by deleting a specific revision. - * @param path The file (virtual) path with conflict. - * @param deleteRevision The revision to delete. - * @param title The title of the conflict (for user display). - */ - abstract resolveByDeletingRevision(path: FilePathWithPrefix, deleteRevision: string, title: string): Promise; - /** - * Resolve a conflict as several possible strategies. - * It may involve user interaction (means raising resolveByUserInteraction). - * @param filename The file (virtual) path to resolve. - */ - abstract resolve(filename: FilePathWithPrefix): Promise; - /** - * Resolve a conflict by choosing the newest version. - * @param filename The file (virtual) path to resolve. - */ - abstract resolveByNewest(filename: FilePathWithPrefix): Promise; - abstract resolveAllConflictedFilesByNewerOnes(): Promise; - conflictProcessQueueCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -} diff --git a/_types/src/lib/src/services/base/ControlService.d.ts b/_types/src/lib/src/services/base/ControlService.d.ts deleted file mode 100644 index 8db82b31..00000000 --- a/_types/src/lib/src/services/base/ControlService.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -import type { APIService } from "./APIService"; -import type { DatabaseService } from "./DatabaseService"; -import type { IControlService, IFileProcessingService, IReplicatorService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { type PromiseWithResolvers } from "octagonal-wheels/promises"; -import type { AppLifecycleService } from "./AppLifecycleService"; -export interface ControlServiceDependencies { - appLifecycleService: AppLifecycleService; - replicatorService: IReplicatorService; - settingService: ISettingService; - databaseService: DatabaseService; - fileProcessingService: IFileProcessingService; - APIService: APIService; -} -/** - * The ControlService provides methods for controlling the overall behaviour of the plugin, such as applying settings or handling lifecycle events. - */ -export declare class ControlService extends ServiceBase implements IControlService { - protected services: ControlServiceDependencies; - protected _log: ReturnType; - protected _unloaded: boolean; - protected _activated: PromiseWithResolvers; - /** - * Check if the plug-in has been unloaded. - */ - hasUnloaded(): boolean; - constructor(context: T, dependencies: ControlServiceDependencies); - get activated(): Promise; - private onActivated; - /** - * Apply current settings to reflect the changes immediately. - * @returns - */ - applySettings(): Promise; - private _onLiveSyncUnload; - /** - * Called when the plugin is loaded. It will trigger the app lifecycle event onLoad. - * Main process should be called in onReady. - * @returns - */ - onLoad(): Promise; - /** - * Main entry point of the plugin. It will trigger the app lifecycle event onReady. - * Usually it should be called on `app.workspace.onLayoutReady` - * @returns - */ - onReady(): Promise; - /** - * On unload event of the plugin. It will trigger the app lifecycle event onUnload. - * @returns - */ - onUnload(): Promise; -} diff --git a/_types/src/lib/src/services/base/DatabaseEventService.d.ts b/_types/src/lib/src/services/base/DatabaseEventService.d.ts deleted file mode 100644 index 2141c0b0..00000000 --- a/_types/src/lib/src/services/base/DatabaseEventService.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IDatabaseEventService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * The DatabaseEventService provides methods for handling database lifecycle events. - */ -export declare abstract class DatabaseEventService extends ServiceBase implements IDatabaseEventService { - /** - * Event triggered when the database is about to be unloaded. - */ - readonly onUnloadDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database is about to be closed. - */ - readonly onCloseDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database is being initialized. - */ - readonly onDatabaseInitialisation: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database has been initialized. - */ - readonly onDatabaseInitialised: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showNotice: boolean) => Promise>; - /** - * Event triggered when the database is being reset. - */ - readonly onResetDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(db: import("../../pouchdb/LiveSyncLocalDB").LiveSyncLocalDB) => Promise>; - /** - * Event triggered when the database is ready for use. - */ - readonly onDatabaseHasReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Initialize the database. - * @param showingNotice Whether to show a notice to the user. - * @param reopenDatabase Whether to reopen the database if it is already open. - * @param ignoreSuspending Whether to ignore any suspending state. - */ - readonly initialiseDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean) => Promise>; -} diff --git a/_types/src/lib/src/services/base/DatabaseService.d.ts b/_types/src/lib/src/services/base/DatabaseService.d.ts deleted file mode 100644 index dee2b621..00000000 --- a/_types/src/lib/src/services/base/DatabaseService.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IDatabaseService, IPathService, IVaultService, openDatabaseParameters } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { LiveSyncLocalDB } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { SettingService } from "./SettingService"; -import type { APIService } from "./APIService"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -export type DatabaseServiceDependencies = { - path: IPathService; - vault: IVaultService; - setting: SettingService; - API: APIService; -}; -/** - * The DatabaseService provides methods for managing the local database. - * Please note that each event of database lifecycle is handled in DatabaseEventService. - */ -export declare abstract class DatabaseService extends ServiceBase implements IDatabaseService { - _log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void; - constructor(context: T, dependencies: DatabaseServiceDependencies); - protected _localDatabase: LiveSyncLocalDB | null; - protected services: DatabaseServiceDependencies; - onOpenDatabase: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(vaultName: string) => Promise>; - /** - * Called after the local database has been reset. - */ - onDatabaseReset: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - get localDatabase(): LiveSyncLocalDB; - get localDatabaseDirect(): LiveSyncLocalDB | null; - protected modifyDatabaseOptions(settings: ObsidianLiveSyncSettings, name: string, options: PouchDB.Configuration.DatabaseConfiguration): { - name: string; - options: PouchDB.Configuration.DatabaseConfiguration; - }; - createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database; - openDatabase(params: openDatabaseParameters): Promise; - isDatabaseReady(): boolean; - resetDatabase(): Promise; -} diff --git a/_types/src/lib/src/services/base/FileProcessingService.d.ts b/_types/src/lib/src/services/base/FileProcessingService.d.ts deleted file mode 100644 index c25b445e..00000000 --- a/_types/src/lib/src/services/base/FileProcessingService.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { IFileProcessingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * File processing service handles file events and processes them accordingly. - */ -export declare class FileProcessingService extends ServiceBase implements IFileProcessingService { - /** - * Process a file event item by the registered handlers. - */ - readonly processFileEvent: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(item: import("../../common/types").FileEventItem) => Promise>; - /** - * Process a file event item optionally, if any handler is registered. - * i.e., hidden files synchronisation or customisation sync. - */ - readonly processOptionalFileEvent: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(path: import("../../common/types").FilePath) => Promise>; - /** - * Commit any pending file events that have been queued for processing. - */ - readonly commitPendingFileEvents: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - batched: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - totalQueued: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - processing: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - totalStorageFileEventCount: number; - onStorageFileEvent(): void; -} diff --git a/_types/src/lib/src/services/base/IService.d.ts b/_types/src/lib/src/services/base/IService.d.ts deleted file mode 100644 index 3745feb1..00000000 --- a/_types/src/lib/src/services/base/IService.d.ts +++ /dev/null @@ -1,286 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import { type LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, TweakValues, UXFileInfo, UXFileInfoStub } from "@lib/common/types"; -import type { LiveSyncLocalDB } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -import type { ReplicationStatics } from "@lib/common/models/shared.definition"; -import type { ReplicatorService } from "./ReplicatorService"; -import type { DatabaseEventService } from "./DatabaseEventService"; -import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols"; -import type { AsyncActivityOptions } from "@lib/interfaces/AsyncActivityRunner.ts"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - DISABLE: "DISABLE"; - } -} -export interface ICommandCompat { - id: string; - name: string; - icon?: string; - callback?: () => any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - checkCallback?: (checking: boolean) => boolean | void; - editorCallback?: (editor: any, ctx: any) => any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - editorCheckCallback?: (checking: any, editor: any, ctx: any) => boolean | void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -} -export interface IAPIService { - getCustomFetchHandler(): FetchHttpHandler; - addStatusBarItem(): HTMLElement | undefined; - addLog(message: unknown, level: LOG_LEVEL, key?: string): void; - isMobile(): boolean; - showWindow(type: string): Promise; - showWindowOnRight?(type: string): Promise; - getAppID(): string; - getSystemVaultName(): string; - getPlatform(): string; - getAppVersion(): string; - getPluginVersion(): string; - addCommand(command: TCommand): TCommand; - registerWindow(type: string, factory: (leaf: T) => unknown): void; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement; - registerProtocolHandler(action: string, handler: (params: Record) => unknown): void; - confirm: Confirm; - responseCount: ReactiveSource; - requestCount: ReactiveSource; - isOnline: boolean; - webCompatFetch(url: string | Request, opts?: RequestInit): Promise; - nativeFetch(url: string | Request, opts?: RequestInit): Promise; - setInterval(handler: () => void, timeout: number): number; - clearInterval(timerId: number): void; - getSystemConfigDir(): string; -} -export interface IPathService { - id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix; - path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise; - getPath(entry: AnyEntry): FilePathWithPrefix; - markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; -} -export interface openDatabaseParameters { - replicator: ReplicatorService; - databaseEvents: DatabaseEventService; -} -export interface IDatabaseService { - localDatabase: LiveSyncLocalDB; - createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database; - openDatabase(params: openDatabaseParameters): Promise; - resetDatabase(): Promise; - onDatabaseReset: () => Promise; - onOpenDatabase: (vaultName: string) => Promise; - isDatabaseReady(): boolean; -} -export interface IDatabaseEventService { - onUnloadDatabase(db: LiveSyncLocalDB): Promise; - onCloseDatabase(db: LiveSyncLocalDB): Promise; - onDatabaseInitialisation(db: LiveSyncLocalDB): Promise; - onDatabaseInitialised(showNotice: boolean): Promise; - onDatabaseHasReady(): Promise; - onResetDatabase(db: LiveSyncLocalDB): Promise; - initialiseDatabase(showingNotice?: boolean, reopenDatabase?: boolean, ignoreSuspending?: boolean): Promise; -} -export interface IKeyValueDBService { - openSimpleStore(kind: string): SimpleStore; - simpleStore: SimpleStore; -} -export interface IFileProcessingService { - processFileEvent(item: FileEventItem): Promise; - processOptionalFileEvent(path: FilePath): Promise; - commitPendingFileEvents(): Promise; - batched: ReactiveSource; - processing: ReactiveSource; - totalQueued: ReactiveSource; - totalStorageFileEventCount: number; - onStorageFileEvent(): void; -} -export interface IReplicatorService { - onCloseActiveReplication(): Promise; - onReplicatorInitialised(): Promise; - getNewReplicator(settingOverride?: Partial): Promise; - getActiveReplicator(): LiveSyncAbstractReplicator | undefined; - replicationStatics: ReactiveSource; - /** Number of finite remote operations currently in progress. */ - boundedRemoteActivityCount: ReactiveSource; - /** Number of finite replication operations which can still deliver database documents. */ - finiteReplicationActivityCount: ReactiveSource; - /** Runs a finite remote operation within the host activity policy. */ - runBoundedRemoteActivity(task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; - /** Runs finite replication which may place documents in the local database. */ - runFiniteReplicationActivity(task: () => T | PromiseLike, options?: AsyncActivityOptions): Promise; -} -export interface IReplicationService { - processSynchroniseResult(doc: MetaEntry): Promise; - processOptionalSynchroniseResult(doc: LoadedEntry): Promise; - processVirtualDocument(docs: PouchDB.Core.ExistingDocument): Promise; - onBeforeReplicate(showMessage: boolean): Promise; - checkConnectionFailure(): Promise; - onCheckReplicationReady(showMessage: boolean): Promise; - isReplicationReady(showMessage: boolean): Promise; - performReplication(showMessage?: boolean): Promise; - replicate(showMessage?: boolean): Promise; - replicateByEvent(showMessage?: boolean): Promise; - onReplicationFailed(showMessage?: boolean): Promise; - parseSynchroniseResult(docs: Array>): Promise; - databaseQueueCount: ReactiveSource; - storageApplyingCount: ReactiveSource; - replicationResultCount: ReactiveSource; - replicateAllToRemote(showingNotice?: boolean, sendChunksInBulkDisabled?: boolean): Promise; - replicateAllFromRemote(showingNotice?: boolean): Promise; - markLocked(lockByClean?: boolean): Promise; - markUnlocked(): Promise; - markResolved(): Promise; -} -export interface IRemoteService { - connect(uri: string, auth: CouchDBCredentials, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean, customHeaders: Record, useRequestAPI: boolean, getPBKDF2Salt: () => Promise): Promise; - info: PouchDB.Core.DatabaseInfo; - }>; - /** - * State if the last POST request failed due to payload size. - */ - get hadLastPostFailedBySize(): boolean; -} -export interface IConflictService { - getOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise; - resolveByUserInteraction: (filename: FilePathWithPrefix, conflictCheckResult: diff_result) => Promise; - queueCheckForIfOpen(path: FilePathWithPrefix): Promise; - queueCheckFor(path: FilePathWithPrefix): Promise; - ensureAllProcessed(): Promise; - resolveByDeletingRevision(path: FilePathWithPrefix, deleteRevision: string, title: string): Promise; - resolve(filename: FilePathWithPrefix): Promise; - resolveByNewest(filename: FilePathWithPrefix): Promise; - resolveAllConflictedFilesByNewerOnes(): Promise; - conflictProcessQueueCount: ReactiveSource; -} -export interface IAppLifecycleService { - onLayoutReady(): Promise; - onFirstInitialise(): Promise; - onReady(): Promise; - onWireUpEvents(): Promise; - onInitialise(): Promise; - onLoad(): Promise; - onSettingLoaded(): Promise; - onLoaded(): Promise; - onScanningStartupIssues(): Promise; - onAppUnload(): Promise; - onBeforeUnload(): Promise; - onUnload(): Promise; - onSuspending(): Promise; - onResuming(): Promise; - onResumed(): Promise; - getUnresolvedMessages: () => Promise<(string | Error)[][]>; - performRestart(): void; - askRestart(message?: string): void; - scheduleRestart(): void; - isSuspended(): boolean; - setSuspended(suspend: boolean): void; - isReady(): boolean; - markIsReady(): void; - resetIsReady(): void; - isReloadingScheduled(): boolean; -} -export interface ISettingService { - onBeforeRealiseSetting(): Promise; - onSettingRealised(): Promise; - onRealiseSetting(): Promise; - suspendAllSync(): Promise; - suspendExtraSync(): Promise; - suggestOptionalFeatures(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }): Promise; - enableOptionalFeature(mode: keyof OPTIONAL_SYNC_FEATURES): Promise; - clearUsedPassphrase(): void; - decryptSettings(settings: ObsidianLiveSyncSettings): Promise; - adjustSettings(settings: ObsidianLiveSyncSettings): Promise; - loadSettings(): Promise; - getDeviceAndVaultName(): string; - setDeviceAndVaultName(name: string): void; - saveDeviceAndVaultName(): void; - onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial | void)[]>; - saveSettingData(): Promise; - currentSettings(): ObsidianLiveSyncSettings; - updateSettings(updateFn: (current: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings, saveImmediately?: boolean): Promise; - applyExternalSettings(partial: Partial, saveImmediately?: boolean): Promise; - applyPartial(partial: Partial, saveImmediately?: boolean): Promise; - onSettingLoaded(settings: ObsidianLiveSyncSettings): Promise; - onSettingChanged(settings: ObsidianLiveSyncSettings): Promise; - onSettingSaved(settings: ObsidianLiveSyncSettings): Promise; - getSmallConfig(key: string): string | null; - setSmallConfig(key: string, value: string): void; - deleteSmallConfig(key: string): void; -} -export interface ITweakValueService { - fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise; - checkAndAskResolvingMismatched(preferred: Partial): Promise<[TweakValues | boolean, boolean]>; - askResolvingMismatched(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE">; - checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - askUseRemoteConfiguration(trialSetting: RemoteDBSettings, preferred: TweakValues): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; -} -export interface IVaultService { - vaultName(): string; - getVaultName(): string; - scanVault(showingNotice?: boolean, ignoreSuspending?: boolean): Promise; - isIgnoredByIgnoreFile(file: string | UXFileInfoStub): Promise; - isTargetFile(file: string | UXFileInfoStub): Promise; - isTargetFileInExtra(file: string | UXFileInfoStub): Promise; - isFileSizeTooLarge(size: number): boolean; - getActiveFilePath(): FilePath | undefined; - isStorageInsensitive(): boolean; - shouldCheckCaseInsensitively(): boolean; - isValidPath(path: string): boolean; -} -export interface ITestService { - test(): Promise; - testMultiDevice(): Promise; - addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void; -} -export interface IUIService { - promptCopyToClipboard(title: string, value: string): Promise; - showMarkdownDialog(title: string, contentMD: string, buttons: T): Promise<(typeof buttons)[number] | false>; - get confirm(): Confirm; -} -export interface IConfigService { - getSmallConfig(key: string): string | null; - setSmallConfig(key: string, value: string): void; - deleteSmallConfig(key: string): void; -} -export interface IServiceHub { - API: IAPIService; - path: IPathService; - database: IDatabaseService; - databaseEvents: IDatabaseEventService; - replicator: IReplicatorService; - fileProcessing: IFileProcessingService; - replication: IReplicationService; - remote: IRemoteService; - conflict: IConflictService; - appLifecycle: IAppLifecycleService; - setting: ISettingService; - tweakValue: ITweakValueService; - vault: IVaultService; - test: ITestService; - UI: IUIService; - config: IConfigService; - keyValueDB: IKeyValueDBService; - control: IControlService; -} -export interface IControlService { - applySettings(): Promise; - onLoad(): Promise; - onReady(): Promise; - onUnload(): Promise; - hasUnloaded(): boolean; - activated: Promise; -} diff --git a/_types/src/lib/src/services/base/KeyValueDBService.d.ts b/_types/src/lib/src/services/base/KeyValueDBService.d.ts deleted file mode 100644 index 49d814fe..00000000 --- a/_types/src/lib/src/services/base/KeyValueDBService.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SimpleStore } from "@lib/common/utils"; -import type { IKeyValueDBService, IVaultService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { KeyValueDatabase } from "@lib/interfaces/KeyValueDatabase"; -import type { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService"; -import type { AppLifecycleServiceBase } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; -export interface KeyValueDBDependencies { - databaseEvents: InjectableDatabaseEventService; - vault: IVaultService; - appLifecycle: AppLifecycleServiceBase; -} -/** - * The KeyValueDBService provides methods for managing the local key-value database. - * Please note that each event of database lifecycle is handled in DatabaseEventService. - */ -export declare abstract class KeyValueDBService extends ServiceBase implements IKeyValueDBService { - private _kvDB; - private _simpleStore; - get simpleStore(): SimpleStore; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - get kvDB(): KeyValueDatabase; - private databaseEvents; - private vault; - private appLifecycle; - private _log; - private _everyOnResetDatabase; - private tryCloseKvDB; - private openKeyValueDB; - private _onOtherDatabaseUnload; - private _onOtherDatabaseClose; - private _everyOnInitializeDatabase; - private _everyOnloadAfterLoadSettings; - constructor(context: T, dependencies: KeyValueDBDependencies); - openSimpleStore(kind: string): { - get: (key: string) => Promise; - set: (key: string, value: unknown) => Promise; - delete: (key: string) => Promise; - keys: (from: string | undefined, to: string | undefined, count?: number) => Promise; - db: Promise; - }; -} diff --git a/_types/src/lib/src/services/base/PathService.d.ts b/_types/src/lib/src/services/base/PathService.d.ts deleted file mode 100644 index 275c2d3f..00000000 --- a/_types/src/lib/src/services/base/PathService.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DocumentID, EntryHasPath, FilePathWithPrefix, FilePath, AnyEntry, UXFileInfo, UXFileInfoStub } from "@lib/common/types"; -import type { IPathService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols"; -export interface PathServiceDependencies { - settingService: ISettingService; -} -/** - * The PathService provides methods for converting between file paths and document IDs. - * This class would be migrated to the new logic later. - */ -export declare abstract class PathService extends ServiceBase implements IPathService { - protected settingService: ISettingService; - protected abstract normalizePath(path: string): string; - get settings(): import("@lib/common/types").ObsidianLiveSyncSettings; - constructor(context: T, dependencies: PathServiceDependencies); - private _id2path; - private _path2id; - /** - * Convert a document ID or entry to a virtual file path. - * @param id A document ID. Nowadays, it is mostly not the same as the file path. - * If the document has `_` prefixed, saved as `/_`. - * @param entry An entry object. If provided, it can be used to get the path directly. - * @param stripPrefix Whether to strip the prefix from the path. - */ - id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix; - /** - * Convert a virtual file path to a document ID (with prefix if any). - * @param filename A file path with or without prefix. - * @param prefix The prefix to use for the document ID. - */ - path2id(filename: FilePathWithPrefix | FilePath, prefix?: string): Promise; - getPath(entry: AnyEntry): FilePathWithPrefix; - abstract markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - abstract unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - abstract compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - abstract isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; -} diff --git a/_types/src/lib/src/services/base/RemoteService.d.ts b/_types/src/lib/src/services/base/RemoteService.d.ts deleted file mode 100644 index 37b791b8..00000000 --- a/_types/src/lib/src/services/base/RemoteService.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { CouchDBCredentials, EntryDoc } from "@lib/common/types"; -import type { IRemoteService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { type LOG_LEVEL } from "@lib/common/types"; -import { AuthorizationHeaderGenerator } from "@lib/replication/httplib"; -import type { APIService } from "@lib/services/base/APIService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { SettingService } from "@lib/services/base/SettingService"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -export interface RemoteServiceDependencies { - APIService: APIService; - appLifecycle: AppLifecycleService; - setting: SettingService; -} -declare const FetchMethod: { - readonly webCompat: 0; - readonly native: 1; -}; -type FetchMethod = (typeof FetchMethod)[keyof typeof FetchMethod]; -/** - * The RemoteService provides methods for interacting with the remote database. - */ -export declare abstract class RemoteService extends ServiceBase implements IRemoteService { - /** - * Connect to the remote database with the provided settings. - * @param uri The URI of the remote database. - * @param auth The authentication credentials for the remote database. - * @param disableRequestURI Whether to disable the request URI. - * @param passphrase The passphrase for the remote database. - * @param useDynamicIterationCount Whether to use dynamic iteration count. - * @param performSetup Whether to perform setup. - * @param skipInfo Whether to skip information retrieval. - * @param compression Whether to enable compression. - * @param customHeaders Custom headers to include in the request. - * @param useRequestAPI Whether to use the request API. - * @param getPBKDF2Salt Function to retrieve the PBKDF2 salt. - * Note that this function is used for CouchDB and compatible only. - */ - protected _log: LogFunction; - protected _authHeader: AuthorizationHeaderGenerator; - protected _APIService: APIService; - protected _appLifecycleService: AppLifecycleService; - protected _settingService: SettingService; - protected _unresolvedErrors: UnresolvedErrorManager; - protected last_successful_post: boolean; - get hadLastPostFailedBySize(): boolean; - constructor(context: T, dependencies: RemoteServiceDependencies); - showError(msg: string, max_log_level?: LOG_LEVEL): void; - clearErrors(): void; - performFetch(req: string | Request, opts?: RequestInit, fetchMethod?: FetchMethod): Promise; - connect(uri: string, auth: CouchDBCredentials, disableRequestURI: boolean, passphrase: string | false, useDynamicIterationCount: boolean, performSetup: boolean, skipInfo: boolean, compression: boolean, customHeaders: Record, useRequestAPI: boolean, getPBKDF2Salt: () => Promise): Promise; - info: PouchDB.Core.DatabaseInfo; - }>; -} -export {}; diff --git a/_types/src/lib/src/services/base/ReplicationService.d.ts b/_types/src/lib/src/services/base/ReplicationService.d.ts deleted file mode 100644 index 1c7f70fb..00000000 --- a/_types/src/lib/src/services/base/ReplicationService.d.ts +++ /dev/null @@ -1,94 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "@lib/common/types"; -import type { IAPIService, IDatabaseService, IFileProcessingService, IReplicationService, IReplicatorService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { AppLifecycleService } from "./AppLifecycleService"; -export interface ReplicationServiceDependencies { - APIService: IAPIService; - settingService: ISettingService; - appLifecycleService: AppLifecycleService; - databaseService: IDatabaseService; - replicatorService: IReplicatorService; - fileProcessingService: IFileProcessingService; -} -/** - * The ReplicationService provides methods for managing replication processes. - */ -export declare abstract class ReplicationService extends ServiceBase implements IReplicationService { - private _unresolvedErrorManager; - showError(msg: string, max_log_level?: LOG_LEVEL): void; - clearErrors(): void; - _log: LogFunction; - settingService: ISettingService; - appLifecycleService: AppLifecycleService; - replicatorService: IReplicatorService; - APIService: IAPIService; - fileProcessing: IFileProcessingService; - databaseService: IDatabaseService; - constructor(context: T, dependencies: ReplicationServiceDependencies); - /** - * Process a synchronisation result document. - */ - readonly processSynchroniseResult: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(doc: import("@lib/common/types").MetaEntry) => Promise>; - /** - * Process a synchronisation result document for optional entries i.e., hidden files. - */ - readonly processOptionalSynchroniseResult: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(doc: import("@lib/common/types").LoadedEntry) => Promise>; - /** - * Process an array of synchronisation result documents. - * @param docs An array of documents to parse and handle. - */ - readonly parseSynchroniseResult: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(docs: Array>) => Promise>; - /** - * Process a virtual document (e.g., for customisation sync). - */ - readonly processVirtualDocument: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(docs: PouchDB.Core.ExistingDocument) => Promise>; - /** - * An event triggered before starting replication. - */ - readonly onBeforeReplicate: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise>; - /** - * - */ - readonly onCheckReplicationReady: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showMessage: boolean) => Promise>; - /** - * Check if the replication is ready to start. - * @param showMessage Whether to show messages to the user. - */ - isReplicationReady(showMessage?: boolean): Promise; - onReplicationFailed: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showMessage?: boolean) => Promise>; - private performReplicationRequest; - /** - * Perform replication and handle a failed result. - * @param showMessage Whether to show replication progress messages. - */ - performReplication(showMessage?: boolean): Promise; - /** - * Start the replication process. - * @param showMessage Whether to show messages to the user. - */ - replicate(showMessage?: boolean): Promise; - previousReplicated: number; - /** - * Start the replication process triggered by an event (e.g., file change). - * @param showMessage Whether to show messages to the user. - */ - replicateByEvent(showMessage?: boolean): Promise; - /** - * Check if there is a connection failure with the remote database. - */ - readonly checkConnectionFailure: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<() => Promise, unknown>; - databaseQueueCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - storageApplyingCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - replicationResultCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - getActiveReplicatorFor(usage: string): false | LiveSyncAbstractReplicator; - replicateAllToRemote(showingNotice?: boolean, sendChunksInBulkDisabled?: boolean): Promise; - replicateAllFromRemote(showingNotice?: boolean): Promise; - private _getReplicatorAndPerform; - markLocked(lockByClean?: boolean): Promise; - markUnlocked(): Promise; - markResolved(): Promise; -} diff --git a/_types/src/lib/src/services/base/ReplicatorService.d.ts b/_types/src/lib/src/services/base/ReplicatorService.d.ts deleted file mode 100644 index afe6dac9..00000000 --- a/_types/src/lib/src/services/base/ReplicatorService.d.ts +++ /dev/null @@ -1,68 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { IReplicatorService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import type { SettingService } from "./SettingService"; -import type { AppLifecycleService } from "./AppLifecycleService"; -import { UnresolvedErrorManager } from "./UnresolvedErrorManager"; -import type { DatabaseEventService } from "./DatabaseEventService"; -import { type AsyncActivityOptions, type AsyncActivityRunner } from "@lib/interfaces/AsyncActivityRunner.ts"; -export interface ReplicatorServiceDependencies { - settingService: SettingService; - appLifecycleService: AppLifecycleService; - databaseEventService: DatabaseEventService; - activityRunner?: AsyncActivityRunner; -} -/** - * The ReplicatorService provides methods for managing replication. - */ -export declare abstract class ReplicatorService extends ServiceBase implements IReplicatorService { - protected dependencies: ReplicatorServiceDependencies; - readonly boundedRemoteActivityCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - readonly finiteReplicationActivityCount: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - _log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void; - private settingService; - private databaseEventService; - private _activeReplicator; - private _replicatorType; - private appLifecycleService; - _unresolvedErrorManager: UnresolvedErrorManager; - constructor(context: T, dependencies: ReplicatorServiceDependencies); - /** - * Runs one finite remote operation while exposing its lifetime to host policy and status UI. - * Continuous replication must not use this boundary. - */ - runBoundedRemoteActivity(task: () => TValue | PromiseLike, options?: AsyncActivityOptions): Promise; - /** Runs one finite replication and exposes its document-delivery lifetime. */ - runFiniteReplicationActivity(task: () => TValue | PromiseLike, options?: AsyncActivityOptions): Promise; - private runRemoteActivity; - private suspendReplication; - private reinitialiseReplicator; - private disposeReplicator; - private _initialiseReplicator; - /** - * Close the active replication if any. - * Not used currently. - */ - readonly onCloseActiveReplication: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Get a new replicator instance based on the provided settings. - */ - readonly getNewReplicator: import("@lib/services/lib/HandlerUtils").MultipleHandlerFunction<(settingOverride?: Partial) => Promise, unknown>; - readonly onReplicatorInitialised: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Get the currently active replicator instance. - * If no active replicator, return undefined but that is the fatal situation (on Obsidian). - */ - getActiveReplicator(): LiveSyncAbstractReplicator | undefined; - replicationStatics: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource<{ - sent: number; - arrived: number; - maxPullSeq: number; - maxPushSeq: number; - lastSyncPullSeq: number; - lastSyncPushSeq: number; - syncStatus: import("@lib/common/types").DatabaseConnectingStatus; - }>; -} diff --git a/_types/src/lib/src/services/base/ServiceBase.d.ts b/_types/src/lib/src/services/base/ServiceBase.d.ts deleted file mode 100644 index d47f290e..00000000 --- a/_types/src/lib/src/services/base/ServiceBase.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare class ServiceContext { -} -export declare abstract class ServiceBase { - protected context: T; - constructor(context: T); -} diff --git a/_types/src/lib/src/services/base/SettingService.d.ts b/_types/src/lib/src/services/base/SettingService.d.ts deleted file mode 100644 index 017d3c94..00000000 --- a/_types/src/lib/src/services/base/SettingService.d.ts +++ /dev/null @@ -1,114 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import type { IAPIService, ISettingService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -export interface SettingServiceDependencies { - APIService: IAPIService; -} -export declare abstract class SettingService extends ServiceBase implements ISettingService { - deviceAndVaultName: string; - protected APIService: IAPIService; - protected abstract setItem(key: string, value: string): void; - protected abstract getItem(key: string): string; - protected abstract deleteItem(key: string): void; - _settings: ObsidianLiveSyncSettings; - get settings(): ObsidianLiveSyncSettings; - set settings(value: ObsidianLiveSyncSettings); - protected abstract saveData(setting: ObsidianLiveSyncSettings): Promise; - protected abstract loadData(): Promise; - private _lastPersistedSettings?; - _log: ReturnType; - constructor(context: T, dependencies: SettingServiceDependencies); - /** - * Adjust the given settings, e.g., migrate old settings to new format. - * @param settings The settings to adjust. - */ - adjustSettings(settings: ObsidianLiveSyncSettings): Promise; - /** - * Get the unique name for identify the device. - */ - getDeviceAndVaultName(): string; - /** - * Set the unique name for identify the device. - * @param name The unique name to set. - */ - setDeviceAndVaultName(name: string): void; - /** - * Save the current device and vault name to settings, aside from the main settings. - */ - saveDeviceAndVaultName(): void; - private additionalSuffixOfDatabaseName; - private getKey; - setSmallConfig(key: string, value: string): void; - getSmallConfig(key: string): string; - deleteSmallConfig(key: string): void; - /** - * Save the current settings to storage. - */ - saveSettingData(): Promise; - private encryptRemoteConfigurationUris; - private decryptRemoteConfigurationUris; - /** - * Event triggered before realising the settings. - * Handlers can return false to abort the realisation process. - */ - readonly onBeforeRealiseSetting: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered after the settings have been realised. - */ - readonly onSettingRealised: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Event triggered to realise the settings. - */ - readonly onRealiseSetting: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Suspend all synchronisation activities and save to the settings. - */ - readonly suspendAllSync: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Suspend extra synchronisation activities, e.g., hidden files sync. - */ - readonly suspendExtraSync: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Suggest enabling optional features to the user. - */ - readonly suggestOptionalFeatures: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(opt: { - enableFetch?: boolean; - enableOverwrite?: boolean; - }) => Promise>; - /** - * Enable an optional feature and save to the settings. - * It may also raised from `handleSuggestOptionalFeatures` if the user agrees. - * @param mode The optional feature to enable. - */ - readonly enableOptionalFeature: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(mode: keyof OPTIONAL_SYNC_FEATURES) => Promise>; - readonly onSettingLoaded: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(settings: ObsidianLiveSyncSettings) => Promise>; - readonly onSettingChanged: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(settings: ObsidianLiveSyncSettings) => Promise>; - readonly onSettingSaved: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(settings: ObsidianLiveSyncSettings) => Promise>; - readonly onBeforeSaveSettingData: import("@lib/services/lib/HandlerUtils").CollectiveHandlerFunction<(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings) => Promise<(Partial | void)[]>, unknown>; - /** - * Get the current settings. - */ - currentSettings(): ObsidianLiveSyncSettings; - updateSettings(updateFn: (settings: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings, saveImmediately?: boolean): Promise; - applyExternalSettings(partial: Partial, saveImmediately?: boolean): Promise; - applyPartial(partial: Partial, saveImmediately?: boolean): Promise; - getPassphrase(settings: ObsidianLiveSyncSettings): Promise; - private usedPassphrase; - /** - * Clear any used passphrase from memory. - */ - clearUsedPassphrase(): void; - decryptConfigurationItem(encrypted: string, passphrase: string): Promise; - encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings): Promise; - /** - * Decrypt the given settings. - * @param settings The settings to decrypt. - */ - decryptSettings(settings: ObsidianLiveSyncSettings): Promise; - loadSettings(): Promise; - private tryDecodeJson; - private cloneSettings; -} diff --git a/_types/src/lib/src/services/base/TestService.d.ts b/_types/src/lib/src/services/base/TestService.d.ts deleted file mode 100644 index 73655f04..00000000 --- a/_types/src/lib/src/services/base/TestService.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ITestService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * The TestService provides methods for adding and handling test results. - */ -export declare abstract class TestService extends ServiceBase implements ITestService { - /** - * Run the test suite to verify the plug-in's functionality. - * This is typically used for development and debugging purposes. - * It may involve user interaction (means raising resolveByUserInteraction). - */ - readonly test: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Run the multi-device test suite to verify the plug-in's functionality across multiple devices. - * This is typically used for development and debugging purposes. - * It may involve user interaction (means raising resolveByUserInteraction). - */ - readonly testMultiDevice: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<() => Promise>; - /** - * Add a test result to the test suite. - * @param name The name of the test case. - * @param key The key of the test result. - * @param result The result of the test (true for success, false for failure). - * @param summary A brief summary of the test result. - * @param message A detailed message about the test result. - */ - abstract addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void; -} diff --git a/_types/src/lib/src/services/base/TweakValueService.d.ts b/_types/src/lib/src/services/base/TweakValueService.d.ts deleted file mode 100644 index e446b038..00000000 --- a/_types/src/lib/src/services/base/TweakValueService.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { RemoteDBSettings, TweakValues } from "@lib/common/types"; -import type { ITweakValueService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -/** - * The TweakValueService provides methods for managing tweak values and resolving mismatches. - */ -export declare abstract class TweakValueService extends ServiceBase implements ITweakValueService { - /** - * Fetch and trial the remote database settings to determine if they are preferred. - * @param trialSetting The remote database settings to connect. - */ - abstract fetchRemotePreferred(trialSetting: RemoteDBSettings): Promise; - /** - * Check and ask the user to resolve any mismatched tweak values. - * @param preferred The preferred tweak values to check against. - */ - abstract checkAndAskResolvingMismatched(preferred: Partial): Promise<[TweakValues | boolean, boolean]>; - /** - * Ask the user to resolve any mismatched tweak values. - * @param preferredSource The preferred tweak values to resolve against. - */ - abstract askResolvingMismatched(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE">; - /** - * Check and ask the user to use the remote configuration. - * @param settings The remote database settings to connect. - */ - abstract checkAndAskUseRemoteConfiguration(settings: RemoteDBSettings): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - /** - * Ask the user to use the remote configuration. - * @param trialSetting The remote database settings to connect. - * @param preferred The preferred tweak values to use. - */ - abstract askUseRemoteConfiguration(trialSetting: RemoteDBSettings, preferred: TweakValues): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; -} diff --git a/_types/src/lib/src/services/base/UnresolvedErrorManager.d.ts b/_types/src/lib/src/services/base/UnresolvedErrorManager.d.ts deleted file mode 100644 index e792034b..00000000 --- a/_types/src/lib/src/services/base/UnresolvedErrorManager.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { AppLifecycleService } from "./AppLifecycleService"; -export declare class UnresolvedErrorManager { - private _log; - private appLifecycleService; - private _occurredErrors; - showError(msg: string, max_log_level?: LOG_LEVEL): void; - clearError(msg: string): void; - clearErrors(): void; - countErrors(needle: string): number; - private _reportUnresolvedMessages; - constructor(appLifecycleService: AppLifecycleService); -} diff --git a/_types/src/lib/src/services/base/VaultService.d.ts b/_types/src/lib/src/services/base/VaultService.d.ts deleted file mode 100644 index 3be8c816..00000000 --- a/_types/src/lib/src/services/base/VaultService.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types"; -import type { IAPIService, ISettingService, IVaultService } from "./IService"; -import { ServiceBase, type ServiceContext } from "./ServiceBase"; -export interface VaultServiceDependencies { - settingService: ISettingService; - APIService: IAPIService; -} -/** - * The VaultService provides methods for interacting with the vault (local file system). - */ -export declare abstract class VaultService extends ServiceBase implements IVaultService { - protected settingService: ISettingService; - protected APIService: IAPIService; - get settings(): import("@lib/common/types").ObsidianLiveSyncSettings; - constructor(context: T, dependencies: VaultServiceDependencies); - /** - * Get the vault name only. - */ - vaultName(): string; - /** - * Get the vault name with additional suffixes. - */ - getVaultName(): string; - /** - * Scan the vault for changes (especially for changes during the plug-in were not running). - * @param showingNotice Whether to show a notice to the user. - * @param ignoreSuspending Whether to ignore any suspending state. - */ - readonly scanVault: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(showingNotice?: boolean, ignoreSuspending?: boolean) => Promise>; - /** - * Check if a file is ignored by the ignore file (e.g., .gitignore, .obsidianignore). - * @param file The file path or file info stub to check. - */ - readonly isIgnoredByIgnoreFile: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(file: string | import("@lib/common/types").UXFileInfoStub) => Promise>; - /** - * Check if a file is a target file for synchronisation. - * @param file The file path or file info stub to check. - * @param keepFileCheckList Whether to keep the file in the check list. - */ - readonly isTargetFile: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(file: string | import("@lib/common/types").UXFileInfoStub) => Promise>; - /** - * Check if a file is a target file for some extra feature - */ - readonly isTargetFileInExtra: import("@lib/services/lib/HandlerUtils").BooleanMultipleHandlerFunction<(file: string | import("@lib/common/types").UXFileInfoStub) => Promise>; - /** - * Check if a filesize is too large against the current settings. - * @param size The file size to check. - */ - isFileSizeTooLarge(size: number): boolean; - /** - * Get the currently active file path in the editor, if any. - */ - abstract getActiveFilePath(): FilePath | undefined; - /** - * Check if the vault is on a case-insensitive file system. - * This is important for certain operating systems like Windows and macOS. - */ - abstract isStorageInsensitive(): boolean; - /** - * Check if the file system should be treated case-insensitively. - * This is important for certain operating systems like Windows and macOS. - */ - shouldCheckCaseInsensitively(): boolean; - /** - * Check if a given path is valid in the vault. - * @param path The file path to check. - */ - abstract isValidPath(path: string): boolean; -} diff --git a/_types/src/lib/src/services/implements/base/UIService.d.ts b/_types/src/lib/src/services/implements/base/UIService.d.ts deleted file mode 100644 index a3d273fc..00000000 --- a/_types/src/lib/src/services/implements/base/UIService.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ComponentHasResult, SvelteDialogManagerBase } from "@lib/UI/svelteDialog"; -import type { IAPIService, IUIService } from "@lib/services/base/IService"; -import { type AppLifecycleService } from "@lib/services/base/AppLifecycleService.ts"; -import { ServiceBase, type ServiceContext } from "@lib/services/base/ServiceBase"; -export type UIServiceDependencies = { - appLifecycle: AppLifecycleService; - dialogManager: SvelteDialogManagerBase; - APIService: IAPIService; -}; -type DialogResult = "ok" | "cancel"; -type DialogParams = { - title: string; - dataToCopy: string; -}; -export declare abstract class UIService extends ServiceBase implements IUIService { - private _dialogManager; - protected _APIService: IAPIService; - abstract get dialogToCopy(): ComponentHasResult; - constructor(context: T, dependents: UIServiceDependencies); - get dialogManager(): SvelteDialogManagerBase; - promptCopyToClipboard(title: string, value: string): Promise; - showMarkdownDialog(title: string, contentMD: string, buttons: T, defaultAction?: (typeof buttons)[number]): Promise<(typeof buttons)[number] | false>; - get confirm(): Confirm; -} -export {}; diff --git a/_types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts b/_types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts deleted file mode 100644 index c9e00df0..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserAPIService.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import type { ICommandCompat } from "@lib/services/base/IService"; -import type { Confirm } from "@lib/interfaces/Confirm"; -export declare const PACKAGE_VERSION: string; -export declare const MANIFEST_VERSION: string; -export declare class BrowserAPIService extends InjectableAPIService { - _confirmInstance: Confirm; - private commandBar; - private commandButtons; - private logPanel; - private logViewport; - private readonly maxLogLines; - private windowFactories; - private windowInstances; - private windowRoot; - private windowTabs; - private windowBody; - private windowPanels; - private activeWindowType; - constructor(context: T); - get confirm(): Confirm; - showWindow(type: string): Promise; - getCustomFetchHandler(): FetchHttpHandler; - isMobile(): boolean; - getAppID(): string; - getSystemVaultName: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => string, unknown>; - getAppVersion(): string; - getPluginVersion(): string; - getPlatform(): string; - getCrypto(): Crypto; - nativeFetch(req: string | Request, opts?: RequestInit): Promise; - private ensureLogPanel; - private formatLogLine; - private appendLog; - private ensureCommandBar; - private ensureWindowHost; - private ensureWindowTab; - private ensureWindowPanel; - private activateWindow; - private createLeafShim; - private evaluateEnabled; - private executeCommand; - private refreshCommandStates; - addCommand(command: TCommand): TCommand; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerWindow(type: string, factory: (leaf: any) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerProtocolHandler(action: string, handler: (params: Record) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - addStatusBarItem(): HTMLElement | undefined; -} diff --git a/_types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts b/_types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts deleted file mode 100644 index 262d7eaa..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserConfirm.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class BrowserConfirm implements Confirm { - _context: T; - constructor(context: T); - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} diff --git a/_types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts b/_types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts deleted file mode 100644 index 4502b721..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserDatabaseService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -export declare class BrowserDatabaseService extends DatabaseService { -} -export declare class BrowserKeyValueDBService extends KeyValueDBService { -} diff --git a/_types/src/lib/src/services/implements/browser/BrowserUIService.d.ts b/_types/src/lib/src/services/implements/browser/BrowserUIService.d.ts deleted file mode 100644 index 71e48c61..00000000 --- a/_types/src/lib/src/services/implements/browser/BrowserUIService.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { UIService } from "@lib/services/implements/base/UIService"; -import type { ConfigService } from "@lib/services/base/ConfigService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { IAPIService, IControlService } from "@lib/services/base/IService"; -export type BrowserUIServiceDependencies = { - appLifecycle: AppLifecycleService; - config: ConfigService; - replicator: ReplicatorService; - APIService: IAPIService; - control: IControlService; -}; -export declare class BrowserUIService extends UIService { - get dialogToCopy(): import("svelte/legacy").LegacyComponentType; - constructor(context: T, dependents: BrowserUIServiceDependencies); -} diff --git a/_types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts b/_types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts deleted file mode 100644 index 57c4a763..00000000 --- a/_types/src/lib/src/services/implements/browser/ConfigServiceBrowserCompat.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ConfigService } from "@lib/services/base/ConfigService"; -import type { IAPIService, ISettingService } from "@lib/services/base/IService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -export interface ConfigServiceBrowserCompatDependencies { - settingService: ISettingService; - APIService: IAPIService; -} -export declare class ConfigServiceBrowserCompat extends ConfigService { - private _settingService; - _log: ReturnType; - constructor(context: T, dependencies: ConfigServiceBrowserCompatDependencies); - getSmallConfig(key: string): string | null; - setSmallConfig(key: string, value: string): void; - deleteSmallConfig(key: string): void; -} diff --git a/_types/src/lib/src/services/implements/browser/Menu.d.ts b/_types/src/lib/src/services/implements/browser/Menu.d.ts deleted file mode 100644 index 1cc11aa5..00000000 --- a/_types/src/lib/src/services/implements/browser/Menu.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type PromiseWithResolvers } from "octagonal-wheels/promises"; -export declare class MenuItem { - type: string; - title: string; - handler?: () => void | Promise; - icon: string; - setTitle(title: string): this; - onClick(callback: () => void | Promise): this; - setIcon(icon: string | null): this; -} -export declare class MenuSeparator { - type: string; -} -export declare class Menu { - type: string; - items: (MenuItem | MenuSeparator)[]; - constructor(); - addItem(callback: (item: MenuItem) => void): this; - addSeparator(): this; - waitingForClose?: PromiseWithResolvers; - showAtPosition(pos: { - x: number; - y: number; - }): Promise; - hide(): void; -} diff --git a/_types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts b/_types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts deleted file mode 100644 index c9bd82bb..00000000 --- a/_types/src/lib/src/services/implements/browser/ui/renderMessageMarkdown.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function renderMessageMarkdown(message: string): string; diff --git a/_types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts b/_types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts deleted file mode 100644 index 5bbb6d2a..00000000 --- a/_types/src/lib/src/services/implements/headless/HeadlessAPIService.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { FetchHttpHandler } from "@smithy/fetch-http-handler"; -import type { ICommandCompat } from "@lib/services/base/IService"; -import type { Confirm } from "@lib/interfaces/Confirm"; -/** - * Headless implementation of Confirm that returns sensible defaults instead - * of throwing. Dialogs are logged to stderr so the prompts are visible in - * service logs, and the default/conservative action is taken automatically. - */ -export declare class HeadlessConfirm implements Confirm { - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} -export declare class HeadlessAPIService extends InjectableAPIService { - private _confirmInstance; - private _systemVaultName; - constructor(context: T); - get confirm(): Confirm; - showWindow(type: string): Promise; - getCustomFetchHandler(): FetchHttpHandler; - isMobile(): boolean; - getAppID(): string; - getAppVersion(): string; - getPluginVersion(): string; - getPlatform(): string; - getCrypto(): Crypto; - addCommand(command: TCommand): TCommand; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerWindow(type: string, factory: (leaf: any) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - registerProtocolHandler(action: string, handler: (params: Record) => any): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - addStatusBarItem(): HTMLElement | undefined; - private toSafeKeyPart; - private hash32; - private deriveSystemVaultName; - getSystemVaultName(): string; - get isOnline(): boolean; - nativeFetch(req: string | Request, opts?: RequestInit): Promise; -} diff --git a/_types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts b/_types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts deleted file mode 100644 index 0eb9c035..00000000 --- a/_types/src/lib/src/services/implements/headless/HeadlessDatabaseService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -export declare class HeadlessDatabaseService extends DatabaseService { -} -export declare class HeadlessKeyValueDBService extends KeyValueDBService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts deleted file mode 100644 index 72c7c24f..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableAPIService.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { APIService } from "@lib/services/base/APIService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare abstract class InjectableAPIService extends APIService { - addLog: import("@lib/services/lib/HandlerUtils").HandlerFunction<(message: unknown, level: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void, unknown>; - getPlatform(): string; - getCrypto(): Crypto; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts deleted file mode 100644 index 26a0bca1..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableAppLifecycleService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { IAppLifecycleService } from "@lib/services/base/IService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare abstract class AppLifecycleServiceBase extends AppLifecycleService { - askRestart: import("@lib/services/lib/HandlerUtils").HandlerFunction<(message?: string) => void, unknown>; - scheduleRestart: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => void, unknown>; - isReloadingScheduled: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => boolean, unknown>; -} -export declare abstract class InjectableAppLifecycleService extends AppLifecycleServiceBase implements IAppLifecycleService { - performRestart: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => void, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts deleted file mode 100644 index f9c6ad03..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableConflictService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ConflictService } from "@lib/services/base/ConflictService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableConflictService extends ConflictService { - queueCheckForIfOpen: import("@lib/services/lib/HandlerUtils").HandlerFunction<(path: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - queueCheckFor: import("@lib/services/lib/HandlerUtils").HandlerFunction<(path: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - ensureAllProcessed: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => Promise, unknown>; - resolveByDeletingRevision: import("@lib/services/lib/HandlerUtils").HandlerFunction<(path: import("../../../common/types").FilePathWithPrefix, deleteRevision: string, title: string) => Promise, unknown>; - resolve: import("@lib/services/lib/HandlerUtils").HandlerFunction<(filename: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - resolveByNewest: import("@lib/services/lib/HandlerUtils").HandlerFunction<(filename: import("../../../common/types").FilePathWithPrefix) => Promise, unknown>; - resolveAllConflictedFilesByNewerOnes: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => Promise, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts deleted file mode 100644 index a1162a55..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableDatabaseEventService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { DatabaseEventService } from "@lib/services/base/DatabaseEventService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableDatabaseEventService extends DatabaseEventService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts deleted file mode 100644 index 2fdf909e..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableFileProcessingService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { FileProcessingService } from "@lib/services/base/FileProcessingService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableFileProcessingService extends FileProcessingService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts deleted file mode 100644 index d16e609d..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectablePathService.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@lib/common/types"; -import { PathService } from "@lib/services/base/PathService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols"; -export declare function compareFileFreshnessGeneric(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; -export declare class PathServiceCompat extends PathService { - markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; - normalizePath(path: string): string; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts deleted file mode 100644 index b906494e..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableRemoteService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { RemoteService } from "@lib/services/base/RemoteService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableRemoteService extends RemoteService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts deleted file mode 100644 index 5118a276..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableReplicationService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ReplicationService } from "@lib/services/base/ReplicationService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableReplicationService extends ReplicationService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts deleted file mode 100644 index 267a973c..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableReplicatorService.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare class InjectableReplicatorService extends ReplicatorService { -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts deleted file mode 100644 index f5f7a38c..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableServiceHub.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ConfigService } from "@lib/services/base/ConfigService"; -import { ControlService } from "@lib/services/base/ControlService"; -import type { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { PathService } from "@lib/services/base/PathService"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { SettingService } from "@lib/services/base/SettingService"; -import { ServiceHub } from "@lib/services/ServiceHub"; -import type { UIService } from "@lib/services/implements/base/UIService"; -import { InjectableAPIService } from "./InjectableAPIService"; -import { type AppLifecycleServiceBase } from "./InjectableAppLifecycleService"; -import { InjectableConflictService } from "./InjectableConflictService"; -import { InjectableDatabaseEventService } from "./InjectableDatabaseEventService"; -import { InjectableFileProcessingService } from "./InjectableFileProcessingService"; -import { InjectableRemoteService } from "./InjectableRemoteService"; -import { InjectableReplicationService } from "./InjectableReplicationService"; -import { InjectableReplicatorService } from "./InjectableReplicatorService"; -import type { InjectableServiceInstances } from "./InjectableServices"; -import { InjectableTestService } from "./InjectableTestService"; -import { InjectableTweakValueService } from "./InjectableTweakValueService"; -import { InjectableVaultService } from "./InjectableVaultService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -export declare class InjectableServiceHub extends ServiceHub { - protected readonly _api: InjectableAPIService; - protected readonly _path: PathService; - protected readonly _database: DatabaseService; - protected readonly _databaseEvents: InjectableDatabaseEventService; - protected readonly _replicator: InjectableReplicatorService; - protected readonly _fileProcessing: InjectableFileProcessingService; - protected readonly _replication: InjectableReplicationService; - protected readonly _remote: InjectableRemoteService; - protected readonly _conflict: InjectableConflictService; - protected readonly _appLifecycle: AppLifecycleServiceBase; - protected readonly _setting: SettingService; - protected readonly _tweakValue: InjectableTweakValueService; - protected readonly _vault: InjectableVaultService; - protected readonly _test: InjectableTestService; - protected readonly _ui: UIService; - protected readonly _config: ConfigService; - protected readonly _keyValueDB: KeyValueDBService; - protected readonly _control: ControlService; - get API(): InjectableAPIService; - get path(): PathService; - get database(): DatabaseService; - get databaseEvents(): InjectableDatabaseEventService; - get replicator(): InjectableReplicatorService; - get fileProcessing(): InjectableFileProcessingService; - get replication(): InjectableReplicationService; - get remote(): InjectableRemoteService; - get conflict(): InjectableConflictService; - get appLifecycle(): AppLifecycleServiceBase; - get setting(): SettingService; - get tweakValue(): InjectableTweakValueService; - get vault(): InjectableVaultService; - get test(): InjectableTestService; - get control(): ControlService; - get keyValueDB(): KeyValueDBService; - get UI(): UIService; - get config(): ConfigService; - constructor(context: T, services: InjectableServiceInstances & { - setting: SettingService; - appLifecycle: AppLifecycleServiceBase; - path: PathService; - API: InjectableAPIService; - ui: UIService; - config: ConfigService; - database: DatabaseService; - vault: InjectableVaultService; - keyValueDB: KeyValueDBService; - replicator: InjectableReplicatorService; - }); -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableServices.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableServices.d.ts deleted file mode 100644 index 9fc7fd77..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableServices.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ServiceInstances } from "@lib/services/ServiceHub.ts"; -import type { UIService } from "@lib/services/implements/base/UIService.ts"; -import type { ConfigService } from "@lib/services/base/ConfigService.ts"; -import type { ServiceContext } from "@lib/services/base/ServiceBase.ts"; -import type { InjectableAPIService } from "./InjectableAPIService"; -import type { InjectableDatabaseEventService } from "./InjectableDatabaseEventService"; -import type { InjectableReplicatorService } from "./InjectableReplicatorService"; -import type { InjectableFileProcessingService } from "./InjectableFileProcessingService"; -import type { InjectableReplicationService } from "./InjectableReplicationService"; -import type { InjectableRemoteService } from "./InjectableRemoteService"; -import type { InjectableConflictService } from "./InjectableConflictService"; -import type { AppLifecycleServiceBase } from "./InjectableAppLifecycleService"; -import type { InjectableTweakValueService } from "./InjectableTweakValueService"; -import type { InjectableVaultService } from "./InjectableVaultService"; -import type { InjectableTestService } from "./InjectableTestService"; -import type { PathService } from "@lib/services/base/PathService"; -import type { DatabaseService } from "@lib/services/base/DatabaseService.ts"; -import type { SettingService } from "@lib/services/base/SettingService"; -export type InjectableServiceInstances = ServiceInstances & { - API?: InjectableAPIService; - path?: PathService; - database?: DatabaseService; - databaseEvents?: InjectableDatabaseEventService; - replicator?: InjectableReplicatorService; - fileProcessing?: InjectableFileProcessingService; - replication?: InjectableReplicationService; - remote?: InjectableRemoteService; - conflict?: InjectableConflictService; - appLifecycle?: AppLifecycleServiceBase; - setting?: SettingService; - tweakValue?: InjectableTweakValueService; - vault?: InjectableVaultService; - test?: InjectableTestService; - ui?: UIService; - config?: ConfigService; -}; diff --git a/_types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts deleted file mode 100644 index 9890e2e4..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableSettingService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import type { ObsidianLiveSyncSettings } from "@lib/common/types"; -export declare class InjectableSettingService extends SettingService { - constructor(context: T, dependencies: SettingServiceDependencies); - protected setItem(key: string, value: string): void; - protected getItem(key: string): string; - protected deleteItem(key: string): void; - saveData: import("@lib/services/lib/HandlerUtils").HandlerFunction<(data: ObsidianLiveSyncSettings) => Promise, unknown>; - loadData: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => Promise, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts deleted file mode 100644 index e44929af..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableTestService.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { TestService } from "@lib/services/base/TestService"; -export declare class InjectableTestService extends TestService { - addTestResult: import("@lib/services/lib/HandlerUtils").HandlerFunction<(name: string, key: string, result: boolean, summary?: string, message?: string) => void, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts deleted file mode 100644 index e53214c7..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableTweakValueService.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { TweakValueService } from "@lib/services/base/TweakValueService"; -export declare class InjectableTweakValueService extends TweakValueService { - fetchRemotePreferred: import("@lib/services/lib/HandlerUtils").HandlerFunction<(trialSetting: import("../../../common/types").RemoteDBSettings) => Promise, unknown>; - checkAndAskResolvingMismatched: import("@lib/services/lib/HandlerUtils").HandlerFunction<(preferred: Partial) => Promise<[import("../../../common/types").TweakValues | boolean, boolean]>, unknown>; - askResolvingMismatched: import("@lib/services/lib/HandlerUtils").HandlerFunction<(preferredSource: import("../../../common/types").TweakValues) => Promise<"OK" | "CHECKAGAIN" | "IGNORE">, unknown>; - checkAndAskUseRemoteConfiguration: import("@lib/services/lib/HandlerUtils").HandlerFunction<(settings: import("../../../common/types").RemoteDBSettings) => Promise<{ - result: false | import("../../../common/types").TweakValues; - requireFetch: boolean; - }>, unknown>; - askUseRemoteConfiguration: import("@lib/services/lib/HandlerUtils").HandlerFunction<(trialSetting: import("../../../common/types").RemoteDBSettings, preferred: import("../../../common/types").TweakValues) => Promise<{ - result: false | import("../../../common/types").TweakValues; - requireFetch: boolean; - }>, unknown>; -} diff --git a/_types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts b/_types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts deleted file mode 100644 index bac5c061..00000000 --- a/_types/src/lib/src/services/implements/injectable/InjectableVaultService.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import { VaultService } from "@lib/services/base/VaultService"; -export declare abstract class InjectableVaultService extends VaultService { -} -export declare class InjectableVaultServiceCompat extends InjectableVaultService { - isStorageInsensitive: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => boolean, unknown>; - getActiveFilePath: import("@lib/services/lib/HandlerUtils").HandlerFunction<() => import("../../../common/types").FilePath | undefined, unknown>; - isValidPath(path: string): boolean; -} diff --git a/_types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts b/_types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts deleted file mode 100644 index a52aaf9b..00000000 --- a/_types/src/lib/src/services/implements/obsidian/ObsidianServiceContext.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { App, Plugin } from "@/deps"; -export declare class ObsidianServiceContext extends ServiceContext { - app: App; - plugin: Plugin; - liveSyncPlugin: ObsidianLiveSyncPlugin; - constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin); -} diff --git a/_types/src/lib/src/services/lib/HandlerUtils.d.ts b/_types/src/lib/src/services/lib/HandlerUtils.d.ts deleted file mode 100644 index 28e53314..00000000 --- a/_types/src/lib/src/services/lib/HandlerUtils.d.ts +++ /dev/null @@ -1,376 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** - * A function type that can be used as a handler. - */ -type HandlerFunc = (...args: TArg) => TResult | Promise; -/** - * A function type that returns a boolean or a Promise of boolean. - */ -type BooleanHandlerFunc = (...args: TArg) => U | Promise; -/** - * An interface for invokable handlers that can add and remove handler functions. - */ -export interface InvokableHandler { - /** - * Invokes the handler with the provided arguments. - * @param args The arguments to pass to the handler. - * @returns A Promise that resolves to the result of the handler. - */ - invoke(...args: T): Promise; -} -/** - * An interface for invokable boolean handlers that can add and remove handler functions. - */ -type InvokableBooleanHandler = InvokableHandler; -/** - * A function type that can be used to unregister a handler. - */ -export type UnregisterFunction = () => void; -/** - * An interface for binder handlers that can assign a single handler function. - */ -export interface BinderHandler { - assign(callback: HandlerFunc, override?: boolean): UnregisterFunction; -} -/** - * An interface for multi-binder handlers that can add and remove handler functions. - */ -export interface MultiRegisterHandler { - /** - * Adds a handler function. - * Note: The same function only added once. - * If you want to prevent duplication, please remove the existing handler before adding it again. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler(callback: BooleanHandlerFunc): UnregisterFunction; - /** - * Removes a handler function. - * @param callback The handler function to remove. - */ - removeHandler(callback: BooleanHandlerFunc): void; - use(callback: BooleanHandlerFunc): UnregisterFunction; -} -/** - * An interface for dispatch handlers that can dispatch events to multiple handlers. - */ -export interface DispatcherHandler { - dispatch(...args: T): Promise<(Awaited | Error)[]>; -} -/** - * An interface for dispatch handlers that can add and remove handler functions. - */ -export interface DispatchHandler extends DispatcherHandler, MultiRegisterHandler { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -/** - * A binder that allows assigning and invoking a single handler function. - */ -export declare class Binder any> implements BinderHandler, ReturnType>, InvokableHandler, ReturnType> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - private _name; - /** - * Creates a new Binder instance. - * @param name The name of the handler. - * @param initialCallback An optional initial callback function to assign. - */ - constructor(name: string, initialCallback?: T); - private _callback; - /** - * Assigns a new handler function. - * @param callback The new handler function to assign. - */ - assign(callback: T, override?: boolean): () => void; - /** - * Invokes the assigned handler function with the provided arguments. - * @param args The arguments to pass to the handler function. - * @returns The result of the handler function. - */ - invoke(...args: Parameters): ReturnType; -} -/** - * A binder that allows assigning and invoking a single handler function asynchronously. - * The invocation will wait until a handler is assigned. - */ -export declare class LazyBinder any> implements BinderHandler, ReturnType>, InvokableHandler, Promise>>> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - private _name; - private _callbackPromise; - private _callback; - /** - * Creates a new LazyBinder instance. - * @param name The name of the handler. - * @param initialCallback An optional initial callback function to assign. - */ - constructor(name: string, initialCallback?: T); - assign(callback: T, override?: boolean): () => void; - /** - * Invokes the assigned handler function with the provided arguments. - * @param args The arguments to pass to the handler function. - * @returns The result of the handler function. - */ - invoke(...args: Parameters): Promise>>; -} -/** - * A multi-binder that allows adding and removing multiple handler functions. - */ -export declare class MultiBinder any> implements MultiRegisterHandler, ReturnType> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - protected _name: string; - /** - * Creates a new MultiBinder instance. - * @param name The name of the handler. - */ - constructor(name: string); - protected _callbackMap: Map; - protected _isCallbackDirty: boolean; - protected _maxUsedPriority: number; - /** - * Adds a handler function. - * Note: The same function is only added once. - * @param callback The handler function to add. - * @param priority The priority of the handler, Do not use floating numbers to prevent confusion. - * @returns A function to unregister the added handler. - * - */ - addHandler(callback: T, priority?: number, allowSwap?: boolean): UnregisterFunction; - /** - * Removes a handler function. - * @param callback The handler function to remove. - */ - removeHandler(callback: T): void; - /** - * Adds a handler function (alias of addHandler, but more semantic). - * @param callback - * @returns - */ - use(callback: T, priority?: number): UnregisterFunction; - _sortedCallbacks: T[]; - protected get _callbacks(): T[]; -} -/** - * A dispatcher that invokes all added handler functions sequentially and collects their results. - * */ -export declare class Dispatch extends MultiBinder> implements DispatcherHandler { - /** - * Dispatches the event to all registered handlers sequentially. - * @param args The arguments to pass to the handlers. - * @returns An array of results or errors from each handler. - */ - dispatch(...args: T): Promise<(Awaited | Error)[]>; -} -/** - * A dispatcher that invokes all added handler functions in parallel and collects their results. - */ -export declare class DispatchParallel extends MultiBinder> implements DispatcherHandler { - /** - * Dispatches the event to all registered handlers in parallel. - * @param args The arguments to pass to the handlers. - * @returns An array of results or errors from each handler. - */ - dispatch(...args: T): Promise<(Awaited | Error)[]>; -} -/** - * A base class for boolean handlers that can add and remove handler functions. - */ -export declare abstract class BooleanHandlerBase extends MultiBinder> implements InvokableBooleanHandler { - abstract invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions sequentially until one returns false. - */ -export declare class AllHandler extends BooleanHandlerBase { - /** - * Invoke all handlers sequentially until one returns false. - * @param args The arguments to pass to the handlers. - * @returns A Promise that resolves to true if all handlers return true, otherwise false. - */ - invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions in parallel and returns true only if all return true. - */ -export declare class ParallelAllHandler extends BooleanHandlerBase { - /** - * Invoke all handlers in parallel - * @param args The arguments to pass to the handlers. - * @returns True if all handlers return true, otherwise false. - */ - invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions sequentially until one returns true. - */ -export declare class AnySuccessHandler extends BooleanHandlerBase { - /** - * Invokes handlers sequentially until one returns true. - * @param args The arguments to pass to the handlers. - * @returns True if any handler returns true, otherwise false. - */ - invoke(...args: T): Promise; -} -/** - * A handler that invokes all added handler functions sequentially until one returns a non-falsy value. - */ -export declare class FirstResultHandler extends MultiBinder> { - /** - * Invokes handlers sequentially until one returns a non-falsy value. - * @param args The arguments to pass to the handlers. - * @returns The first non-falsy result from the handlers, or false if none found. - */ - invoke(...args: T): Promise; -} -/** - * A function type that can be used as a handler with assignable functionality. - */ -export interface HandlerFunction U | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Assigns a new handler function. - * @param callback The new handler function to assign. - * @param override Whether to override the existing handler if one is already assigned. - * @returns A function to unregister the assigned handler. - */ - setHandler: (callback: TFunc, override?: boolean) => void; -} -/** - * A function type that can be used as a handler with assignable functionality. - */ -export interface LazyHandlerFunction U | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): Promise>>; - /** - * Assigns a new handler function. - * @param callback The new handler function to assign. - * @param override Whether to override the existing handler if one is already assigned. - * @returns A function to unregister the assigned handler. - */ - setHandler: (callback: TFunc, override?: boolean) => void; -} -/** - * A function type that can be used as a multiple handler with add/remove functionality. - */ -export interface MultipleHandlerFunction U | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Adds a handler function. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler: (callback: TFunc) => () => void; - /** - * Removes a handler function. - * @param callback The handler function to remove. - * @returns - */ - removeHandler: (callback: TFunc) => void; -} -/** - * A function type that can be used as a value-collecting handler with add/remove functionality. - */ -export type CollectorFunction U | Promise, U = unknown> = (...args: Parameters) => Promise>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -/** - * A Handler function type that can have multiple handlers added or removed, and collects their results into an array. - */ -export interface CollectiveHandlerFunction U[] | Promise, U = unknown> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Adds a handler function. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler: (callback: CollectorFunction) => () => void; - /** - * Removes a handler function. - * @param callback The handler function to remove. - * @returns - */ - removeHandler: (callback: CollectorFunction) => void; -} -export interface BooleanMultipleHandlerFunction boolean | Promise> { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Invokes the handler function with the provided arguments. - */ - (...args: Parameters): ReturnType; - /** - * Adds a handler function. - * @param callback The handler function to add. - * @returns A function to remove the added handler. - */ - addHandler: (callback: TFunc, priority?: number) => () => void; - /** - * Removes a handler function. - * @param callback The handler function to remove. - * @returns - */ - removeHandler: (callback: TFunc) => void; -} -export interface MultiBinderInstance extends InvokableHandler, MultiRegisterHandler { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -export interface BooleanMultiBinderInstance extends InvokableBooleanHandler, MultiRegisterHandler { // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface -} -export declare function allFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function bailFirstFailureFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function allParallelFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function anySuccessFunction Promise>(name?: string): BooleanMultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function firstResultFunction Promise>(name?: string): MultipleHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function dispatchParallelFunction Promise>(name?: string): CollectiveHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function bindableFunction any>(name?: string): HandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -export declare function lazyBindableFunction any>(name?: string): LazyHandlerFunction; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -type FunctionKeys = Extract<{ - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -}[keyof T], string>; -export declare function handlers(): { - /** - * Create a handler that invokes all added handler functions sequentially until one returns false. - * @param name - * @returns - */ - all>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions in parallel and returns true only if all return true. - * @param name - * @returns - */ - allParallel>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions sequentially until one returns false. - * @param name - * @returns - */ - bailFirstFailure>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions sequentially until one returns true. - * @param name - * @returns - */ - anySuccess>(name: K): BooleanMultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions sequentially until one returns a non-falsy value. - * @param name - * @returns - */ - firstResult>(name: K): MultipleHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a handler that invokes all added handler functions in parallel. - * @param name - * @returns - */ - dispatchParallel>(name: K): CollectiveHandlerFunction Promise>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - /** - * Create a binder handler that can assign a single handler function. - * @param name - * @returns - */ - binder>(name: K): HandlerFunction any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - lazyBinder>(name: K): LazyHandlerFunction any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration -}; -export {}; diff --git a/_types/src/lib/src/services/lib/logUtils.d.ts b/_types/src/lib/src/services/lib/logUtils.d.ts deleted file mode 100644 index 1e8a40f0..00000000 --- a/_types/src/lib/src/services/lib/logUtils.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type LOG_LEVEL } from "octagonal-wheels/common/logger"; -import type { IAPIService } from "@lib/services/base/IService"; -export declare const MARK_LOG_SEPARATOR = "\u200A"; -export declare const MARK_LOG_NETWORK_ERROR = "\u200B"; -/** - * Creates a log function that prefixes messages with the service name and uses the provided APIService's addLog method if available. - * If APIService is not provided, it falls back to using the global Logger function. - * @param serviceName The name of the service to prefix log messages with. - * @param APIService An optional APIService instance to use for logging. - * @returns A log function that can be used to log messages with the specified service name and APIService. - */ -export declare function createInstanceLogFunction(serviceName: string, APIService?: IAPIService): (msg: unknown, level?: LOG_LEVEL, key?: string) => void; -export type LogFunction = ReturnType; diff --git a/_types/src/lib/src/services/lib/remoteActivity.d.ts b/_types/src/lib/src/services/lib/remoteActivity.d.ts deleted file mode 100644 index d6c69410..00000000 --- a/_types/src/lib/src/services/lib/remoteActivity.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -export type TrackedPhysicalRequestCounters = { - requestCount: ReactiveSource; - responseCount: ReactiveSource; -}; -/** - * Tracks one physical-request unit for status reporting. - * - * A unit may be an exact transport attempt or a higher-level SDK command. The - * resulting in-flight count is deliberately approximate and must not be used - * for protocol correctness, throttling, or completion decisions. - */ -export declare function runWithTrackedPhysicalRequest(counters: TrackedPhysicalRequestCounters, task: () => T | PromiseLike): Promise; diff --git a/_types/src/lib/src/string_and_binary/chunks.d.ts b/_types/src/lib/src/string_and_binary/chunks.d.ts deleted file mode 100644 index 6e9dcc3f..00000000 --- a/_types/src/lib/src/string_and_binary/chunks.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare function splitPiecesTextV2(dataSrc: string | string[], pieceSize: number, minimumChunkSize: number): () => Generator; -export declare function binaryTextSplit(data: string, pieceSize: number, minimumChunkSize: number): () => Generator; -export declare function splitPiecesText(dataSrc: string | string[], pieceSize: number, plainSplit: boolean, minimumChunkSize: number, useSegmenter: boolean): () => Generator; -export declare function splitPiecesTextV1(dataSrc: string | string[], pieceSize: number, plainSplit: boolean, minimumChunkSize: number): () => Generator; -export declare function collectGenAll(strGen: AsyncGenerator | Generator): Promise; -export declare function concatGeneratedAll(strGen: AsyncGenerator | Generator): Promise; -export declare function splitPieces2V2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPieces2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPiecesRabinKarp(dataSrc: Blob, absoluteMaxPieceSize: number, doPlainSplit: boolean, minimumChunkSize: number, _filename?: string, _useSegmenter?: boolean): Promise<() => AsyncGenerator>; -export declare function splitPiecesRabinKarpOld(dataSrc: Blob, absoluteMaxPieceSize: number, doPlainSplit: boolean, minimumChunkSize: number, _filename?: string, _useSegmenter?: boolean): Promise<() => AsyncGenerator>; diff --git a/_types/src/lib/src/string_and_binary/convert.d.ts b/_types/src/lib/src/string_and_binary/convert.d.ts deleted file mode 100644 index 34f8b9be..00000000 --- a/_types/src/lib/src/string_and_binary/convert.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { arrayBufferToBase64, base64ToArrayBuffer, base64ToArrayBufferInternalBrowser, readString, writeString, tryConvertBase64ToArrayBuffer } from "octagonal-wheels/binary"; -export { arrayBufferToBase64, base64ToArrayBuffer, base64ToArrayBufferInternalBrowser, readString, writeString, tryConvertBase64ToArrayBuffer, }; -export declare function arrayBufferToBase64Single(buffer: Uint8Array | ArrayBuffer): Promise; -export { uint8ArrayToHexString, hexStringToUint8Array } from "octagonal-wheels/binary/hex"; -export { encodeBinaryEach, decodeToArrayBuffer } from "octagonal-wheels/binary/encodedUTF16"; -export { decodeBinary, encodeBinary } from "octagonal-wheels/binary"; -export { escapeStringToHTML } from "octagonal-wheels/string"; -export declare function versionNumberString2Number(version: string): number; diff --git a/_types/src/lib/src/string_and_binary/hash.d.ts b/_types/src/lib/src/string_and_binary/hash.d.ts deleted file mode 100644 index 1fd9da95..00000000 --- a/_types/src/lib/src/string_and_binary/hash.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export * from "octagonal-wheels/hash/xxhash.js"; -export type * from "octagonal-wheels/hash/xxhash.js"; diff --git a/_types/src/lib/src/string_and_binary/path.d.ts b/_types/src/lib/src/string_and_binary/path.d.ts deleted file mode 100644 index fc15a7a9..00000000 --- a/_types/src/lib/src/string_and_binary/path.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "@lib/common/types.ts"; -export declare function isValidFilenameInWidows(filename: string): boolean; -export declare function isValidFilenameInDarwin(filename: string): boolean; -export declare function isValidFilenameInLinux(filename: string): boolean; -export declare function isValidFilenameInAndroid(filename: string): boolean; -export declare function isFilePath(path: FilePath | FilePathWithPrefix): path is FilePath; -export declare function stripAllPrefixes(prefixedPath: FilePathWithPrefix): FilePath; -export declare function addPrefix(path: FilePath | FilePathWithPrefix, prefix: string): FilePathWithPrefix; -export declare function expandFilePathPrefix(path: FilePathWithPrefix | FilePath): [string, FilePathWithPrefix]; -export declare function expandDocumentIDPrefix(id: DocumentID): [string, FilePathWithPrefix]; -export declare function path2id_base(filenameSrc: FilePathWithPrefix | FilePath, obfuscatePassphrase: string | false, caseInsensitive: boolean): Promise; -export declare function id2path_base(id: DocumentID, entry?: EntryHasPath): FilePathWithPrefix; -export declare function getPath(entry: AnyEntry): FilePathWithPrefix; -export declare function getPathWithoutPrefix(entry: AnyEntry): FilePath; -export declare function stripPrefix(prefixedPath: FilePathWithPrefix): FilePath; -export declare function shouldBeIgnored(filename: string): boolean; -export declare function isPlainText(filename: string): boolean; -export declare function shouldSplitAsPlainText(filename: string): boolean; -/** - * returns whether the given path is accepted (not ignored) by the `.gitignore`. - * @param path path of the file which is relative from `.gitignore` file - * @param ignore lines of `.gitignore` - * @returns true when accepted. - * false when not accepted. - * undefined when the path is not mentioned in the `.gitignore` file. - */ -export declare function isAccepted(path: string, ignore: string[]): boolean | undefined; -/** - * Checks whether the path is accepted by all ignored files. - * @param path path of target file - * @param ignoreFiles list of ignore files. i.e. [".gitignore", ".dockerignore"] - * @param getList function to retrieve the file. - * @returns true when accepted. false when should be ignored. - */ -export declare function isAcceptedAll(path: string, ignoreFiles: string[], getList: (path: string) => Promise): Promise; diff --git a/_types/src/lib/src/worker/bg.common.d.ts b/_types/src/lib/src/worker/bg.common.d.ts deleted file mode 100644 index f2aa078d..00000000 --- a/_types/src/lib/src/worker/bg.common.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { END_OF_DATA } from "./universalTypes.ts"; -export declare function postBack(key: number, seq: number, data: string | END_OF_DATA): void; diff --git a/_types/src/lib/src/worker/bg.worker.d.ts b/_types/src/lib/src/worker/bg.worker.d.ts deleted file mode 100644 index 467855c2..00000000 --- a/_types/src/lib/src/worker/bg.worker.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export {}; diff --git a/_types/src/lib/src/worker/bg.worker.encryption.d.ts b/_types/src/lib/src/worker/bg.worker.encryption.d.ts deleted file mode 100644 index ce827c47..00000000 --- a/_types/src/lib/src/worker/bg.worker.encryption.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EncryptHKDFArguments } from "./universalTypes.ts"; -import type { EncryptArguments } from "./universalTypes.ts"; -/** - * Processes the encryption of data. - * @param data The data to be encrypted or decrypted. - */ -export declare function processEncryption(data: EncryptArguments | EncryptHKDFArguments): Promise; diff --git a/_types/src/lib/src/worker/bg.worker.splitting.d.ts b/_types/src/lib/src/worker/bg.worker.splitting.d.ts deleted file mode 100644 index be153611..00000000 --- a/_types/src/lib/src/worker/bg.worker.splitting.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { SplitArguments } from "./universalTypes.ts"; -/** - * Processes the splitting of data into chunks. - * @param data The data to be split. - */ -export declare function processSplit(data: SplitArguments): Promise; diff --git a/_types/src/lib/src/worker/bgWorker.d.ts b/_types/src/lib/src/worker/bgWorker.d.ts deleted file mode 100644 index 26c02125..00000000 --- a/_types/src/lib/src/worker/bgWorker.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EncryptArguments, EncryptHKDFArguments, EncryptHKDFProcessItem, EncryptProcessItem, ProcessItem, SplitArguments, SplitProcessItem } from "./universalTypes.ts"; -export type WorkerInstance = { - worker: Worker; - processing: number; - /** Keys of tasks currently dispatched to this worker instance. */ - taskKeys: Set; -}; -export declare function splitPieces2Worker(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function splitPieces2WorkerV2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function splitPieces2WorkerRabinKarp(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function encryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function decryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function encryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare function decryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare const tasks: Map; -/** - * Remove a completed (or aborted) task from both the tasks map and its worker's taskKeys set. - */ -export declare function removeTask(key: number): void; -export declare function initialiseWorkerModule(): void; -export declare function startWorker(data: Omit): EncryptHKDFProcessItem; -export declare function startWorker(data: Omit): EncryptProcessItem; -export declare function startWorker(data: Omit): SplitProcessItem; -export declare function terminateWorker(): void; diff --git a/_types/src/lib/src/worker/bgWorker.encryption.d.ts b/_types/src/lib/src/worker/bgWorker.encryption.d.ts deleted file mode 100644 index 30afe2b1..00000000 --- a/_types/src/lib/src/worker/bgWorker.encryption.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type EncryptHKDFProcessItem, type ResultPayload } from "./universalTypes.ts"; -import { type EncryptProcessItem } from "./universalTypes.ts"; -import { type EncryptHKDFArguments } from "./universalTypes.ts"; -import { type EncryptArguments } from "./universalTypes.ts"; -/** - * Offloads encryption to a web worker. - * @param data The data to be encrypted. - * @returns A promise that resolves with the encryption result. - */ -export declare function encryptionOnWorker(data: Omit): Promise; -/** - * Offloads HKDF encryption to a web worker. - * @param data The data to be encrypted. - * @returns A promise that resolves with the encryption result. - */ -export declare function encryptionHKDFOnWorker(data: Omit): Promise; -/** - * Handles the encryption callbacks - * @param process The process item associated with the task. - * @param data The data to be processed. - */ -export declare function handleTaskEncrypt(process: EncryptProcessItem | EncryptHKDFProcessItem, data: ResultPayload): void; diff --git a/_types/src/lib/src/worker/bgWorker.mock.d.ts b/_types/src/lib/src/worker/bgWorker.mock.d.ts deleted file mode 100644 index 9aa57050..00000000 --- a/_types/src/lib/src/worker/bgWorker.mock.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { EncryptHKDFProcessItem, EncryptProcessItem, SplitProcessItem, ProcessItem } from "./universalTypes.ts"; -export type SplitArguments = { - key: number; - type: "split"; - dataSrc: Blob; - pieceSize: number; - plainSplit: boolean; - minimumChunkSize: number; - filename?: string; - useV2: boolean; - useSegmenter: boolean; -}; -export type EncryptArguments = { - key: number; - type: "encrypt" | "decrypt"; - input: string; - passphrase: string; - autoCalculateIterations: boolean; -}; -export type EncryptHKDFArguments = { - key: number; - type: "encryptHKDF" | "decryptHKDF"; - input: string; - passphrase: string; - pbkdf2Salt: Uint8Array; -}; -export declare function terminateWorker(): void; -export declare function splitPieces2Worker(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPieces2WorkerV2(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): Promise<(() => Generator) | (() => AsyncGenerator)>; -export declare function splitPieces2WorkerRabinKarp(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename?: string, useSegmenter?: boolean): () => AsyncGenerator; -export declare function encryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function decryptWorker(input: string, passphrase: string, autoCalculateIterations: boolean): Promise; -export declare function encryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare function decryptHKDFWorker(input: string, passphrase: string, pbkdf2Salt: Uint8Array): Promise; -export declare function startWorker(data: Omit): EncryptHKDFProcessItem; -export declare function startWorker(data: Omit): EncryptProcessItem; -export declare function startWorker(data: Omit): SplitProcessItem; -export declare const tasks: Map; -export declare function initialiseWorkerModule(): void; diff --git a/_types/src/lib/src/worker/bgWorker.splitting.d.ts b/_types/src/lib/src/worker/bgWorker.splitting.d.ts deleted file mode 100644 index b519756c..00000000 --- a/_types/src/lib/src/worker/bgWorker.splitting.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ResultPayloadWithSeq, type SplitProcessItem } from "./universalTypes"; -/** - * Splits data into pieces using a worker. - * @param dataSrc The source data to be split. - * @param pieceSize The size of each piece. - * @param plainSplit Whether to use plain splitting. - * @param minimumChunkSize The minimum size of each chunk. - * @param filename The name of the file being processed. - * @param splitVersion The version of the splitting algorithm to use. - * @param useSegmenter Whether to use a segmenter (only works on splitVersion:2) - * @returns A generator that yields the split pieces. - */ -export declare function _splitPieces2Worker(dataSrc: Blob, pieceSize: number, plainSplit: boolean, minimumChunkSize: number, filename: string | undefined, splitVersion: 1 | 2 | 3, useSegmenter: boolean): () => AsyncGenerator; -/** - * Aborts all in-flight split tasks identified by the given keys. - * Called when the background worker that owned these tasks has crashed, so the streams - * will never receive any more data and must be torn down to unblock callers. - * @param keys The task keys to abort. - * @param error The error to report to each stream. - */ -export declare function abortSplitTasks(keys: number[], error: Error): void; -/** - * Handles the splitting callback from the worker. - * @param process the splitting process item - * @param data the data received from the worker - */ -export declare function handleTaskSplit(process: SplitProcessItem, data: ResultPayloadWithSeq): void; diff --git a/_types/src/lib/src/worker/universalTypes.d.ts b/_types/src/lib/src/worker/universalTypes.d.ts deleted file mode 100644 index 6421b5a6..00000000 --- a/_types/src/lib/src/worker/universalTypes.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { PromiseWithResolvers } from "octagonal-wheels/promises"; -export type EncryptArguments = { - key: number; - type: "encrypt" | "decrypt"; - input: string; - passphrase: string; - autoCalculateIterations: boolean; -}; -export type EncryptHKDFArguments = { - key: number; - type: "encryptHKDF" | "decryptHKDF"; - input: string; - passphrase: string; - pbkdf2Salt: Uint8Array; -}; -export type SplitArguments = { - key: number; - type: "split"; - dataSrc: Blob; - pieceSize: number; - plainSplit: boolean; - minimumChunkSize: number; - filename?: string; - useSegmenter: boolean; - splitVersion: 1 | 2 | 3; -}; -export type SplitProcessItem = { - key: number; - type: SplitArguments["type"]; - finalize: () => void; -}; -export type EncryptProcessItem = { - key: number; - task: PromiseWithResolvers; - type: EncryptArguments["type"]; - finalize: () => void; -}; -export type EncryptHKDFProcessItem = { - key: number; - task: PromiseWithResolvers; - type: EncryptHKDFArguments["type"]; - finalize: () => void; -}; -export type ProcessItem = SplitProcessItem | EncryptProcessItem | EncryptHKDFProcessItem; -export declare const END_OF_DATA: null; -export type END_OF_DATA = typeof END_OF_DATA; -export type ResultPayloadBase = { - key: number; -}; -export type ResultPayloadWithResult = ResultPayloadBase & { - result: string; -}; -export type ResultPayloadWithError = ResultPayloadBase & { - error: unknown; -}; -export type ResultPayload = ResultPayloadWithResult | ResultPayloadWithError; -export type ResultPayloadWithSeqBase = ResultPayload & { - seq: number; -}; -export type ResultPayloadPiece = ResultPayloadWithSeqBase & { - result: string | END_OF_DATA; -}; -export type ResultPayloadWithSeq = ResultPayloadPiece | ResultPayloadWithError; diff --git a/_types/src/main.d.ts b/_types/src/main.d.ts deleted file mode 100644 index 0f2836f3..00000000 --- a/_types/src/main.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { Plugin, type App, type PluginManifest } from "./deps"; -import { LiveSyncCommands } from "./features/LiveSyncCommands.ts"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; -import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts"; -export type LiveSyncCore = LiveSyncBaseCore; -export default class ObsidianLiveSyncPlugin extends Plugin { - core: LiveSyncCore; - /** - * Initialise service modules. - */ - private initialiseServiceModules; - /** - * @obsolete Use services.setting.saveSettingData instead. Save the settings to the disk. This is usually called after changing the settings in the code, to persist the changes. - */ - saveSettings(): Promise; - constructor(app: App, manifest: PluginManifest); - private _startUp; - onload(): void; - onunload(): undefined; -} diff --git a/_types/src/managers/ObsidianStorageEventManagerAdapter.d.ts b/_types/src/managers/ObsidianStorageEventManagerAdapter.d.ts deleted file mode 100644 index 1a81fb03..00000000 --- a/_types/src/managers/ObsidianStorageEventManagerAdapter.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TFile, TFolder } from "@/deps"; -import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; -import type { FileEventItem } from "@lib/common/types"; -import type { IStorageEventManagerAdapter } from "@lib/managers/adapters"; -import type { IStorageEventTypeGuardAdapter, IStorageEventPersistenceAdapter, IStorageEventWatchAdapter, IStorageEventStatusAdapter, IStorageEventConverterAdapter, IStorageEventWatchHandlers } from "@lib/managers/adapters"; -import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { LiveSyncCore } from "@/main"; -import type { FileProcessingService } from "@lib/services/base/FileProcessingService"; -/** - * Obsidian-specific type guard adapter - */ -declare class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter { - isFile(file: unknown): file is TFile; - isFolder(item: unknown): item is TFolder; -} -/** - * Obsidian-specific persistence adapter - */ -declare class ObsidianPersistenceAdapter implements IStorageEventPersistenceAdapter { - private core; - constructor(core: LiveSyncCore); - saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise; - loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null>; -} -/** - * Obsidian-specific status adapter - */ -declare class ObsidianStatusAdapter implements IStorageEventStatusAdapter { - private fileProcessing; - constructor(fileProcessing: FileProcessingService); - updateStatus(status: { - batched: number; - processing: number; - totalQueued: number; - }): void; -} -/** - * Obsidian-specific converter adapter - */ -declare class ObsidianConverterAdapter implements IStorageEventConverterAdapter { - toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub; - toInternalFileInfo(path: FilePath): UXInternalFileInfoStub; -} -/** - * Obsidian-specific watch adapter - */ -declare class ObsidianWatchAdapter implements IStorageEventWatchAdapter { - private plugin; - constructor(plugin: ObsidianLiveSyncPlugin); - beginWatch(handlers: IStorageEventWatchHandlers): Promise; -} -/** - * Composite adapter for Obsidian StorageEventManager - */ -export declare class ObsidianStorageEventManagerAdapter implements IStorageEventManagerAdapter { - readonly typeGuard: ObsidianTypeGuardAdapter; - readonly persistence: ObsidianPersistenceAdapter; - readonly watch: ObsidianWatchAdapter; - readonly status: ObsidianStatusAdapter; - readonly converter: ObsidianConverterAdapter; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, fileProcessing: FileProcessingService); -} -export {}; diff --git a/_types/src/managers/StorageEventManagerObsidian.d.ts b/_types/src/managers/StorageEventManagerObsidian.d.ts deleted file mode 100644 index 6b0a6b18..00000000 --- a/_types/src/managers/StorageEventManagerObsidian.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath } from "@lib/common/types"; -import type ObsidianLiveSyncPlugin from "@/main"; -import type { LiveSyncCore } from "@/main"; -import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager"; -import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter"; -export declare class StorageEventManagerObsidian extends StorageEventManagerBase { - core: LiveSyncCore; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies); - /** - * Override _watchVaultRawEvents to add Obsidian-specific logic - */ - protected _watchVaultRawEvents(path: FilePath): Promise; -} diff --git a/_types/src/modules/AbstractModule.d.ts b/_types/src/modules/AbstractModule.d.ts deleted file mode 100644 index 7243de91..00000000 --- a/_types/src/modules/AbstractModule.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types"; -import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -export declare abstract class AbstractModule = LiveSyncBaseCore> { - core: T; - _log: (msg: unknown, level?: import("octagonal-wheels/common/logger").LOG_LEVEL, key?: string) => void; - get services(): import("../lib/src/services/InjectableServices").InjectableServiceHub; - addCommand: (command: TCommand) => TCommand; - registerView: (type: string, factory: (leaf: T_1) => unknown) => void; - addRibbonIcon: (icon: string, title: string, callback: (evt: MouseEvent) => unknown) => HTMLElement; - registerObsidianProtocolHandler: (action: string, handler: (params: Record) => unknown) => void; - get localDatabase(): import("../lib/src/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get settings(): import("@lib/common/types").ObsidianLiveSyncSettings; - set settings(value: import("@lib/common/types").ObsidianLiveSyncSettings); - getPath(entry: AnyEntry): FilePathWithPrefix; - getPathWithoutPrefix(entry: AnyEntry): FilePathWithPrefix; - onBindFunction(core: T, services: typeof core.services): void; - constructor(core: T); - saveSettings: () => Promise; - addTestResult(key: string, value: boolean, summary?: string, message?: string): void; - testDone(result?: boolean): Promise; - testFail(message: string): Promise; - isMainReady(): boolean; - isMainSuspended(): boolean; - isDatabaseReady(): boolean; -} diff --git a/_types/src/modules/AbstractObsidianModule.d.ts b/_types/src/modules/AbstractObsidianModule.d.ts deleted file mode 100644 index 7ff21c6a..00000000 --- a/_types/src/modules/AbstractObsidianModule.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncCore } from "@/main"; -import type ObsidianLiveSyncPlugin from "@/main"; -import { AbstractModule } from "./AbstractModule.ts"; -export declare abstract class AbstractObsidianModule extends AbstractModule { - plugin: ObsidianLiveSyncPlugin; - get app(): import("obsidian").App; - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore); - isThisModuleEnabled(): boolean; -} diff --git a/_types/src/modules/core/ModulePeriodicProcess.d.ts b/_types/src/modules/core/ModulePeriodicProcess.d.ts deleted file mode 100644 index 29ae3d91..00000000 --- a/_types/src/modules/core/ModulePeriodicProcess.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { PeriodicProcessor } from "@/common/PeriodicProcessor"; -import type { LiveSyncCore } from "@/main"; -import { AbstractModule } from "@/modules/AbstractModule"; -export declare class ModulePeriodicProcess extends AbstractModule { - periodicSyncProcessor: PeriodicProcessor; - disablePeriodic(): Promise; - resumePeriodic(): Promise; - private _allOnUnload; - private _everyBeforeRealizeSetting; - private _everyBeforeSuspendProcess; - private _everyAfterResumeProcess; - private _everyAfterRealizeSetting; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ModuleReplicator.d.ts b/_types/src/modules/core/ModuleReplicator.d.ts deleted file mode 100644 index aad4e833..00000000 --- a/_types/src/modules/core/ModuleReplicator.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule"; -import { type EntryDoc, type RemoteType } from "@lib/common/types"; -import type { LiveSyncCore } from "@/main"; -import { ReplicateResultProcessor } from "./ReplicateResultProcessor"; -export declare class ModuleReplicator extends AbstractModule { - _replicatorType?: RemoteType; - processor: ReplicateResultProcessor; - private _unresolvedErrorManager; - clearErrors(): void; - private _everyOnloadAfterLoadSettings; - _onReplicatorInitialised(): Promise; - _everyOnDatabaseInitialized(showNotice: boolean): Promise; - _everyBeforeReplicate(showMessage: boolean): Promise; - /** - * obsolete method. No longer maintained and will be removed in the future. - * @deprecated v0.24.17 - * @param showMessage If true, show message to the user. - */ - cleaned(showMessage: boolean): Promise; - private onReplicationFailed; - _parseReplicationResult(docs: Array>): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ModuleReplicatorCouchDB.d.ts b/_types/src/modules/core/ModuleReplicatorCouchDB.d.ts deleted file mode 100644 index 285810f4..00000000 --- a/_types/src/modules/core/ModuleReplicatorCouchDB.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings } from "@lib/common/types"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import { AbstractModule } from "@/modules/AbstractModule"; -import type { LiveSyncCore } from "@/main"; -export declare class ModuleReplicatorCouchDB extends AbstractModule { - _anyNewReplicator(settingOverride?: Partial): Promise; - _everyAfterResumeProcess(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ModuleReplicatorMinIO.d.ts b/_types/src/modules/core/ModuleReplicatorMinIO.d.ts deleted file mode 100644 index 46012eba..00000000 --- a/_types/src/modules/core/ModuleReplicatorMinIO.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type RemoteDBSettings } from "@lib/common/types"; -import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator"; -import type { LiveSyncCore } from "@/main"; -import { AbstractModule } from "@/modules/AbstractModule"; -export declare class ModuleReplicatorMinIO extends AbstractModule { - _anyNewReplicator(settingOverride?: Partial): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/core/ReplicateResultProcessor.d.ts b/_types/src/modules/core/ReplicateResultProcessor.d.ts deleted file mode 100644 index e4592b9e..00000000 --- a/_types/src/modules/core/ReplicateResultProcessor.d.ts +++ /dev/null @@ -1,116 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type AnyEntry, type EntryDoc, type LoadedEntry, type MetaEntry } from "@lib/common/types"; -import type { ModuleReplicator } from "./ModuleReplicator"; -import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; -export declare class ReplicateResultProcessor { - private log; - private logError; - private replicator; - constructor(replicator: ModuleReplicator); - get localDatabase(): import("../../lib/src/pouchdb/LiveSyncLocalDB").LiveSyncLocalDB; - get services(): import("../../lib/src/services/InjectableServices").InjectableServiceHub; - get core(): LiveSyncBaseCore; - getPath(entry: AnyEntry): string; - suspend(): void; - resume(): void; - private _suspended; - get isSuspended(): boolean; - /** - * Take a snapshot of the current processing state. - * This snapshot is stored in the KV database for recovery on restart. - */ - protected _takeSnapshot(): Promise; - /** - * Trigger taking a snapshot. - */ - protected _triggerTakeSnapshot(): void; - /** - * Throttled version of triggerTakeSnapshot. - */ - protected triggerTakeSnapshot: import("octagonal-wheels/function").ThrottledFunction<() => void>; - /** - * Restore from snapshot. - */ - restoreFromSnapshot(): Promise; - private _restoreFromSnapshot; - /** - * Restore from snapshot only once. - * @returns Promise that resolves when restoration is complete. - */ - restoreFromSnapshotOnce(): Promise; - /** - * Perform the given procedure while counting the concurrency. - * @param proc async procedure to perform - * @param countValue reactive source to count concurrency - * @returns result of the procedure - */ - withCounting(proc: () => Promise, countValue: ReactiveSource): Promise; - /** - * Report the current status. - */ - protected reportStatus(): void; - /** - * Enqueue all the given changes for processing. - * @param changes Changes to enqueue - */ - enqueueAll(changes: PouchDB.Core.ExistingDocument[]): void; - /** - * Process the change if it is not a document change. - * @param change Change to process - * @returns True if the change was processed; false otherwise - */ - protected processIfNonDocumentChange(change: PouchDB.Core.ExistingDocument): boolean; - /** - * Queue of changes to be processed. - */ - private _queuedChanges; - /** - * List of changes being processed. - */ - private _processingChanges; - /** - * Enqueue the given document change for processing. - * @param doc Document change to enqueue - * @returns - */ - protected enqueueChange(doc: PouchDB.Core.ExistingDocument): void; - /** - * Trigger processing of the queued changes. - */ - protected triggerProcessQueue(): void; - /** - * Semaphore to limit concurrent processing. - * This is the per-id semaphore + concurrency-control (max 10 concurrent = 10 documents being processed at the same time). - */ - private _semaphore; - /** - * Flag indicating whether the process queue is currently running. - */ - private _isRunningProcessQueue; - /** - * Process the queued changes. - */ - private runProcessQueue; - /** - * Parse the given document change. - * @param change - * @returns - */ - parseDocumentChange(change: PouchDB.Core.ExistingDocument): Promise; - protected applyToDatabase(doc: PouchDB.Core.ExistingDocument): Promise; - private _applyToDatabase; - /** - * Phase 3: Apply the given entry to storage. - * @param entry - * @returns - */ - protected applyToStorage(entry: MetaEntry): Promise; - /** - * Check whether processing is required for the given document. - * @param dbDoc Document to check - * @returns True if processing is required; false otherwise - */ - protected checkIsChangeRequiredForDatabaseProcessing(dbDoc: LoadedEntry): Promise; -} diff --git a/_types/src/modules/coreFeatures/ModuleConflictChecker.d.ts b/_types/src/modules/coreFeatures/ModuleConflictChecker.d.ts deleted file mode 100644 index ba36ebe7..00000000 --- a/_types/src/modules/coreFeatures/ModuleConflictChecker.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import { type FilePathWithPrefix } from "@lib/common/types"; -import { QueueProcessor } from "octagonal-wheels/concurrency/processor"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleConflictChecker extends AbstractModule { - _queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise; - _queueConflictCheck(file: FilePathWithPrefix): Promise; - _waitForAllConflictProcessed(): Promise; - conflictResolveQueue: QueueProcessor; - conflictCheckQueue: QueueProcessor; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/coreFeatures/ModuleConflictResolver.d.ts b/_types/src/modules/coreFeatures/ModuleConflictResolver.d.ts deleted file mode 100644 index 235e2c01..00000000 --- a/_types/src/modules/coreFeatures/ModuleConflictResolver.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import { type diff_check_result, type FilePathWithPrefix } from "@lib/common/types"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -declare global { - interface LSEvents { - "conflict-cancelled": FilePathWithPrefix; - } -} -export declare class ModuleConflictResolver extends AbstractModule { - private _resolveConflictByDeletingRev; - checkConflictAndPerformAutoMerge(path: FilePathWithPrefix): Promise; - private _resolveConflict; - private _anyResolveConflictByNewest; - private _resolveAllConflictedFilesByNewerOnes; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts b/_types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts deleted file mode 100644 index 7f147277..00000000 --- a/_types/src/modules/coreFeatures/ModuleResolveMismatchedTweaks.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type TweakValues, type ObsidianLiveSyncSettings, type RemoteDBSettings } from "@lib/common/types.ts"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleResolvingMismatchedTweaks extends AbstractModule { - private _hasNotifiedAutoAcceptCompatibleUndefined; - private _collectMismatchedTweakKeys; - private _selectNewerTweakSide; - private _shouldAutoAcceptCompatibleLossy; - /** - * Hook before saving settings, to check if there are changes in tweak values, and if so, - * update the tweakModified timestamp to current time. - * This allows other devices to know that the tweak values have been changed and decide whether to accept the new values based on the modification time. - * @param next - * @param previous - * @returns - */ - _onBeforeSaveSettingData(next: ObsidianLiveSyncSettings, previous: ObsidianLiveSyncSettings): Promise<{ - tweakModified: number; - } | undefined>; - _anyAfterConnectCheckFailed(): Promise; - _checkAndAskResolvingMismatchedTweaks(preferred: TweakValues): Promise<[TweakValues | boolean, boolean]>; - _askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE">; - _fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise; - _checkAndAskUseRemoteConfiguration(trialSetting: RemoteDBSettings): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - _askUseRemoteConfiguration(trialSetting: RemoteDBSettings, preferred: TweakValues): Promise<{ - result: false | TweakValues; - requireFetch: boolean; - }>; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/coreObsidian/UILib/dialogs.d.ts b/_types/src/modules/coreObsidian/UILib/dialogs.d.ts deleted file mode 100644 index 59e5fc7e..00000000 --- a/_types/src/modules/coreObsidian/UILib/dialogs.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ButtonComponent } from "@/deps.ts"; -import { App, FuzzySuggestModal, Modal, Plugin, Component } from "@/deps.ts"; -import { type CompatIntervalHandle } from "@lib/common/coreEnvFunctions.ts"; -declare class AutoClosableModal extends Modal { - _closeByUnload(): void; - constructor(app: App); - onClose(): void; -} -export declare class InputStringDialog extends AutoClosableModal { - result: string | false; - onSubmit: (result: string | false) => void; - title: string; - key: string; - placeholder: string; - isManuallyClosed: boolean; - isPassword: boolean; - constructor(app: App, title: string, key: string, placeholder: string, isPassword: boolean, onSubmit: (result: string | false) => void); - onOpen(): void; - onClose(): void; -} -export declare class PopoverSelectString extends FuzzySuggestModal { - _app: App; - callback: ((e: string) => void) | undefined; - getItemsFun: () => string[]; - constructor(app: App, note: string, placeholder: string | undefined, getItemsFun: (() => string[]) | undefined, callback: (e: string) => void); - getItems(): string[]; - getItemText(item: string): string; - onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void; - onClose(): void; -} -export declare class MessageBox extends AutoClosableModal { - plugin: Plugin; - title: string; - contentMd: string; - buttons: T; - result: string | false; - isManuallyClosed: boolean; - defaultAction: string | undefined; - timeout: number | undefined; - timer: CompatIntervalHandle | undefined; - defaultButtonComponent: ButtonComponent | undefined; - wideButton: boolean; - onSubmit: (result: string | false) => void; - component: Component; - constructor(plugin: Plugin, title: string, contentMd: string, buttons: T, defaultAction: T[number], timeout: number | undefined, wideButton: boolean, onSubmit: (result: T[number] | false) => void); - onOpen(): void; - onClose(): void; -} -export declare function confirmWithMessage(plugin: Plugin, title: string, contentMd: string, buttons: T, defaultAction: T[number], timeout?: number): Promise; -export declare function confirmWithMessageWithWideButton(plugin: Plugin, title: string, contentMd: string, buttons: T, defaultAction: T[number], timeout?: number): Promise; -export declare const askYesNo: (app: App, message: string) => Promise<"yes" | "no">; -export declare const askSelectString: (app: App, message: string, items: string[]) => Promise; -export declare const askString: (app: App, title: string, key: string, placeholder: string, isPassword?: boolean) => Promise; -export {}; diff --git a/_types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts b/_types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts deleted file mode 100644 index d072ad83..00000000 --- a/_types/src/modules/coreObsidian/storageLib/utilObsidian.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TFile, type TAbstractFile, type TFolder } from "@/deps.ts"; -import type { FilePathWithPrefix, UXFileInfo, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "@lib/common/types.ts"; -import type { LiveSyncCore } from "@/main.ts"; -import type { FileAccessObsidian } from "@/serviceModules/FileAccessObsidian.ts"; -export declare function TFileToUXFileInfo(core: LiveSyncCore, file: TFile, prefix?: string, deleted?: boolean): Promise; -export declare function InternalFileToUXFileInfo(fullPath: string, vaultAccess: FileAccessObsidian, prefix?: string): Promise; -export declare function TFileToUXFileInfoStub(file: TFile | TAbstractFile, deleted?: boolean): UXFileInfoStub; -export declare function InternalFileToUXFileInfoStub(filename: FilePathWithPrefix, deleted?: boolean): UXInternalFileInfoStub; -export declare function TFolderToUXFileInfoStub(file: TFolder): UXFolderInfo; diff --git a/_types/src/modules/essential/ModuleBasicMenu.d.ts b/_types/src/modules/essential/ModuleBasicMenu.d.ts deleted file mode 100644 index b56b684a..00000000 --- a/_types/src/modules/essential/ModuleBasicMenu.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncCore } from "@/main"; -import { AbstractModule } from "@/modules/AbstractModule"; -export declare class ModuleBasicMenu extends AbstractModule { - _everyOnloadStart(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/essential/ModuleMigration.d.ts b/_types/src/modules/essential/ModuleMigration.d.ts deleted file mode 100644 index 0820ab61..00000000 --- a/_types/src/modules/essential/ModuleMigration.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleMigration extends AbstractModule { - migrateUsingDoctor(skipRebuild?: boolean, activateReason?: string, forceRescan?: boolean): Promise; - migrateDisableBulkSend(): Promise; - initialMessage(): Promise; - askAgainForSetupURI(): Promise; - hasIncompleteDocs(force?: boolean): Promise; - hasCompromisedChunks(): Promise; - _everyOnFirstInitialize(): Promise; - _everyOnLayoutReady(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts b/_types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts deleted file mode 100644 index 2fed2e5d..00000000 --- a/_types/src/modules/essentialObsidian/APILib/ObsHttpHandler.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler"; -import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http"; -/** - * This is close to origin implementation of FetchHttpHandler - * https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts - * that is released under Apache 2 License. - * But this uses Obsidian requestUrl instead. - */ -export declare class ObsHttpHandler extends FetchHttpHandler { - requestTimeoutInMs: number | undefined; - reverseProxyNoSignUrl: string | undefined; - constructor(options?: FetchHttpHandlerOptions, reverseProxyNoSignUrl?: string); - handle(request: HttpRequest, { abortSignal }?: HttpHandlerOptions): Promise<{ - response: HttpResponse; - }>; -} diff --git a/_types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts b/_types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts deleted file mode 100644 index d35de4b0..00000000 --- a/_types/src/modules/essentialObsidian/ModuleObsidianEvents.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import type { TFile } from "@/deps.ts"; -import { type ReactiveSource } from "octagonal-wheels/dataobject/reactive"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleObsidianEvents extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - __performAppReload(): void; - initialCallback: (() => void) | undefined; - swapSaveCommand(): void; - registerWatchEvents(): void; - hasFocus: boolean; - isLastHidden: boolean; - private boundedRemoteActivityEndHandler?; - private deferredBoundedLifecycle?; - private keepReplicationActiveInBackground; - private applyDeferredBoundedActivityLifecycle; - private deferLifecycleUntilBoundedRemoteActivityEnds; - setHasFocus(hasFocus: boolean): void; - watchWindowVisibility(): void; - watchOnline(): void; - watchOnlineAsync(): Promise; - watchWindowVisibilityAsync(): Promise; - watchWorkspaceOpen(file: TFile | null): void; - watchWorkspaceOpenAsync(file: TFile): Promise; - _everyOnLayoutReady(): Promise; - private _askReload; - _totalProcessingCount?: ReactiveSource; - private _scheduleAppReload; - _isReloadingScheduled(): boolean; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts b/_types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts deleted file mode 100644 index c0a90cc8..00000000 --- a/_types/src/modules/essentialObsidian/ModuleObsidianMenu.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LiveSyncCore } from "@/main.ts"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -export declare class ModuleObsidianMenu extends AbstractModule { - _everyOnloadStart(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts b/_types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts deleted file mode 100644 index 782090b9..00000000 --- a/_types/src/modules/features/DocumentHistory/DocumentHistoryModal.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { TFile, Modal, App } from "@/deps.ts"; -import ObsidianLiveSyncPlugin from "@/main.ts"; -import { type DocumentID, type FilePathWithPrefix, type LoadedEntry } from "@lib/common/types.ts"; -import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -export declare class DocumentHistoryModal extends Modal { - plugin: ObsidianLiveSyncPlugin; - core: LiveSyncBaseCore; - get services(): import("../../../lib/src/services/InjectableServices").InjectableServiceHub; - range: HTMLInputElement; - contentView: HTMLDivElement; - info: HTMLDivElement; - fileInfo: HTMLDivElement; - showDiff: boolean; - diffOnly: boolean; - id?: DocumentID; - file: FilePathWithPrefix; - revs_info: PouchDB.Core.RevisionInfo[]; - currentDoc?: LoadedEntry; - currentText: string; - currentDeleted: boolean; - initialRev?: string; - currentDiffIndex: number; - diffNavContainer: HTMLDivElement; - diffNavIndicator: HTMLSpanElement; - diffOnlyLabel: HTMLLabelElement; - searchKeyword: string; - searchResults: { - rev: string; - index: number; - matchType: "Content" | "Diff"; - }[]; - currentSearchIndex: number; - searchResultIndicator: HTMLSpanElement; - searchProgressIndicator: HTMLSpanElement; - searchTimeout: number | null; - constructor(app: App, core: LiveSyncBaseCore, plugin: ObsidianLiveSyncPlugin, file: TFile | FilePathWithPrefix, id?: DocumentID, revision?: string); - loadFile(initialRev?: string): Promise; - loadRevs(initialRev?: string): Promise; - BlobURLs: Map; - revokeURL(key: string): void; - generateBlobURL(key: string, data: Uint8Array): string; - prepareContentView(usePreformatted?: boolean): void; - appendTextDiff(diff: [number, string][]): void; - appendSearchHighlightedText(container: HTMLElement, text: string): void; - appendImageDiff(baseSrc: string, overlaySrc?: string): void; - appendDeletedNotice(usePreformatted?: boolean): void; - showExactRev(rev: string): Promise; - /** - * Navigate to the previous or next diff block in the content view. - * Only effective when diff highlighting is enabled. - */ - navigateDiff(direction: "prev" | "next"): void; - /** - * Reset the diff navigation index and update the indicator. - */ - resetDiffNavigation(): void; - /** - * Show or hide the diff navigation buttons based on the showDiff state. - */ - updateDiffNavVisibility(): void; - /** - * Search through the last 100 revisions for the given keyword. - */ - performSearch(keyword: string): Promise; - updateSearchUI(): void; - navigateSearch(direction: "prev" | "next"): void; - onOpen(): void; - onClose(): void; -} diff --git a/_types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts b/_types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts deleted file mode 100644 index cb01653f..00000000 --- a/_types/src/modules/features/GlobalHistory/GlobalHistoryView.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { WorkspaceLeaf } from "@/deps.ts"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -export declare const VIEW_TYPE_GLOBAL_HISTORY = "global-history"; -export declare class GlobalHistoryView extends SvelteItemView { - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - plugin: ObsidianLiveSyncPlugin; - icon: string; - title: string; - navigation: boolean; - getIcon(): string; - constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin); - getViewType(): string; - getDisplayText(): string; -} diff --git a/_types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts b/_types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts deleted file mode 100644 index 1938be88..00000000 --- a/_types/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Modal } from "@/deps.ts"; -import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "@lib/common/types.ts"; -import { eventHub } from "@/common/events.ts"; -export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string; -declare global { - interface Slips extends LSSlips { - "conflict-resolved": typeof CANCELLED | MergeDialogResult; - } -} -export declare class ConflictResolveModal extends Modal { - result: diff_result; - filename: string; - response: MergeDialogResult; - isClosed: boolean; - consumed: boolean; - title: string; - pluginPickMode: boolean; - localName: string; - remoteName: string; - offEvent?: ReturnType; - currentDiffIndex: number; - diffView: HTMLDivElement; - diffNavIndicator: HTMLSpanElement; - constructor(app: App, filename: string, diff: diff_result, pluginPickMode?: boolean, remoteName?: string); - appendDiffFragment(container: HTMLDivElement, text: string, cls: string): void; - appendVersionInfo(container: HTMLDivElement, cls: string, name: string, date: string): void; - navigateDiff(direction: "prev" | "next"): void; - resetDiffNavigation(): void; - onOpen(): void; - sendResponse(result: MergeDialogResult): void; - onClose(): void; - waitForResult(): Promise; -} diff --git a/_types/src/modules/features/Log/LogPaneView.d.ts b/_types/src/modules/features/Log/LogPaneView.d.ts deleted file mode 100644 index 62a47d39..00000000 --- a/_types/src/modules/features/Log/LogPaneView.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { WorkspaceLeaf } from "@/deps.ts"; -import type ObsidianLiveSyncPlugin from "@/main.ts"; -import { SvelteItemView } from "@/common/SvelteItemView.ts"; -export declare const VIEW_TYPE_LOG = "log-log"; -export declare class LogPaneView extends SvelteItemView { - instantiateComponent(target: HTMLElement): { - $on?(type: string, callback: (e: any) => void): () => void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - $set?(props: Partial>): void; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - } & Record; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration - plugin: ObsidianLiveSyncPlugin; - icon: string; - title: string; - navigation: boolean; - getIcon(): string; - constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin); - getViewType(): string; - getDisplayText(): import("octagonal-wheels/common/types").TaggedType; -} diff --git a/_types/src/modules/features/ModuleGlobalHistory.d.ts b/_types/src/modules/features/ModuleGlobalHistory.d.ts deleted file mode 100644 index 96949683..00000000 --- a/_types/src/modules/features/ModuleGlobalHistory.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -export declare class ModuleObsidianGlobalHistory extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - showGlobalHistory(): void; - onBindFunction(core: typeof this.core, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleInteractiveConflictResolver.d.ts b/_types/src/modules/features/ModuleInteractiveConflictResolver.d.ts deleted file mode 100644 index cd5259a2..00000000 --- a/_types/src/modules/features/ModuleInteractiveConflictResolver.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type FilePathWithPrefix, type diff_result } from "@lib/common/types.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleInteractiveConflictResolver extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - _anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise; - allConflictCheck(): Promise; - pickFileForResolve(): Promise; - _allScanStat(): Promise; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleLog.d.ts b/_types/src/modules/features/ModuleLog.d.ts deleted file mode 100644 index d1e7a7ef..00000000 --- a/_types/src/modules/features/ModuleLog.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ReactiveValue } from "octagonal-wheels/dataobject/reactive"; -import { type LOG_LEVEL } from "@lib/common/types.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import { Notice } from "@/deps.ts"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector.ts"; -import type { LiveSyncCore } from "@/main.ts"; -import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; -export declare const MARK_DONE = "\u2009\u2009"; -export declare class ModuleLog extends AbstractObsidianModule { - statusBar?: HTMLElement; - statusDiv?: HTMLElement; - statusLine?: HTMLDivElement; - logMessage?: HTMLDivElement; - logHistory?: HTMLDivElement; - messageArea?: HTMLDivElement; - statusBarLabels: ReactiveValue<{ - message: string; - status: string; - }>; - statusLog: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - activeFileStatus: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; - notifies: { - [key: string]: { - notice: Notice; - count: number; - }; - }; - p2pLogCollector: P2PLogCollector; - observeForLogs(): void; - private _everyOnload; - adjustStatusDivPosition(): void; - getActiveFileStatus(): Promise; - setFileStatus(): Promise; - updateMessageArea(): Promise; - onActiveLeafChange(): void; - nextFrameQueue: ReturnType | undefined; - logLines: { - ttl: number; - message: string; - }[]; - applyStatusBarText(): void; - private _allStartOnUnload; - _everyOnloadStart(): Promise; - private _everyOnloadAfterLoadSettings; - writeLogToTheFile(now: Date, vaultName: string, newMessage: string): void; - __addLog(message: unknown, level?: LOG_LEVEL, key?: string): void; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleObsidianDocumentHistory.d.ts b/_types/src/modules/features/ModuleObsidianDocumentHistory.d.ts deleted file mode 100644 index 20a847ab..00000000 --- a/_types/src/modules/features/ModuleObsidianDocumentHistory.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type TFile } from "@/deps.ts"; -import type { FilePathWithPrefix, DocumentID } from "@lib/common/types.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -export declare class ModuleObsidianDocumentHistory extends AbstractObsidianModule { - _everyOnloadStart(): Promise; - showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID): void; - fileHistory(): Promise; - onBindFunction(core: typeof this.core, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts b/_types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts deleted file mode 100644 index 173cd987..00000000 --- a/_types/src/modules/features/ModuleObsidianSettingAsMarkdown.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { ServiceContext } from "@lib/services/base/ServiceBase.ts"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleObsidianSettingsAsMarkdown extends AbstractModule { - _everyOnloadStart(): Promise; - extractSettingFromWholeText(data: string): { - preamble: string; - body: string; - postscript: string; - }; - parseSettingFromMarkdown(filename: string, data?: string): Promise<{ - preamble: string; - body: string; - postscript: string; - }>; - checkAndApplySettingFromMarkdown(filename: string, automated?: boolean): Promise; - generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial; - saveSettingToMarkdown(filename: string): Promise; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/features/ModuleObsidianSettingTab.d.ts b/_types/src/modules/features/ModuleObsidianSettingTab.d.ts deleted file mode 100644 index 3e8f4b2c..00000000 --- a/_types/src/modules/features/ModuleObsidianSettingTab.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts"; -import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleObsidianSettingDialogue extends AbstractObsidianModule { - settingTab: ObsidianLiveSyncSettingTab; - _everyOnloadStart(): Promise; - openSetting(): void; - get appId(): string; - onBindFunction(core: LiveSyncCore, services: typeof core.services): void; -} diff --git a/_types/src/modules/features/RemoteActivityStatus.d.ts b/_types/src/modules/features/RemoteActivityStatus.d.ts deleted file mode 100644 index a1b47074..00000000 --- a/_types/src/modules/features/RemoteActivityStatus.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -/** Status icon for a finite remote operation whose lifetime is known. */ -export declare const REMOTE_OPERATION_ACTIVITY_ICON = "\uD83D\uDCF2"; -/** Status icon for approximate physical remote-request activity. */ -export declare const REMOTE_REQUEST_ACTIVITY_ICON = "\uD83C\uDF10"; -/** Avoids hiding very short remote requests before the status bar can render them. */ -export declare const REMOTE_REQUEST_ACTIVITY_MINIMUM_VISIBLE_MS = 150; -export type RemoteActivityStatus = { - remoteOperationCount: number; - trackedRequestCount: number; -}; -/** Returns the non-negative difference between tracked request starts and completions. */ -export declare function getTrackedRequestCount(requestCount: number, responseCount: number): number; -/** Formats the compact prefix shown before the replication status. */ -export declare function formatRemoteActivityStatusLabel(status: RemoteActivityStatus): string; diff --git a/_types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts b/_types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts deleted file mode 100644 index 855c980c..00000000 --- a/_types/src/modules/features/SettingDialogue/LiveSyncSetting.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { Setting, TextComponent, type ToggleComponent, type DropdownComponent, ButtonComponent, type TextAreaComponent, type ValueComponent } from "@/deps.ts"; -import { type ConfigurationItem } from "@lib/common/types.ts"; -import { type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import { type AllSettingItemKey, type AllSettings, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey } from "./settingConstants.ts"; -import { type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts"; -export declare class LiveSyncSetting extends Setting { - autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent; - applyButtonComponent?: ButtonComponent; - selfKey?: AllSettingItemKey; - watchDirtyKeys: AllSettingItemKey[]; - holdValue: boolean; - static env: ObsidianLiveSyncSettingTab; - descBuf: string | DocumentFragment; - nameBuf: string | DocumentFragment; - placeHolderBuf: string; - hasPassword: boolean; - invalidateValue?: () => void; - setValue?: (value: unknown) => void; - constructor(containerEl: HTMLElement); - setDesc(desc: string | DocumentFragment): this; - setName(name: string | DocumentFragment): this; - setAuto(key: AllSettingItemKey, opt?: AutoWireOption): this; - autoWireSetting(key: AllSettingItemKey, opt?: AutoWireOption): { - name: string; - desc?: string; - placeHolder?: string; - status?: "BETA" | "ALPHA" | "EXPERIMENTAL"; - obsolete?: boolean; - level?: import("@lib/common/types.ts").ConfigLevel; - isHidden?: boolean; - isAdvanced?: boolean; - } | undefined; - autoWireComponent(component: ValueComponent, conf?: ConfigurationItem, opt?: AutoWireOption): void; - commitValue(value: AllSettings[T]): Promise; - autoWireText(key: AllStringItemKey, opt?: AutoWireOption): this; - autoWireTextArea(key: AllStringItemKey, opt?: AutoWireOption): this; - autoWireNumeric(key: AllNumericItemKey, opt: AutoWireOption & { - clampMin?: number; - clampMax?: number; - acceptZero?: boolean; - }): this; - autoWireToggle(key: AllBooleanItemKey, opt?: AutoWireOption): this; - autoWireDropDown(key: AllStringItemKey, opt: AutoWireOption & { - options: Record; - }): this; - addApplyButton(keys: AllSettingItemKey[], text?: string): this; - addOnUpdate(func: () => OnUpdateResult): this; - updateHandlers: Set<() => OnUpdateResult>; - prevStatus: OnUpdateResult; - _getComputedStatus(): OnUpdateResult; - _applyOnUpdateHandlers(): void; - _onUpdate(): void; -} diff --git a/_types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts b/_types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts deleted file mode 100644 index a5ec9518..00000000 --- a/_types/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.d.ts +++ /dev/null @@ -1,104 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { App, Component, PluginSettingTab } from "@/deps.ts"; -import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts"; -import ObsidianLiveSyncPlugin from "@/main.ts"; -import { type AllSettingItemKey, type AllStringItemKey, type AllNumericItemKey, type AllBooleanItemKey, type AllSettings, OnDialogSettingsDefault, type OnDialogSettings } from "./settingConstants.ts"; -import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; -import { type OnSavedHandler, type OnSavedHandlerFunc, type OnUpdateFunc, type OnUpdateResult, type UpdateFunction } from "./SettingPane.ts"; -import { JournalSyncCore } from "@lib/replication/journal/JournalSyncCore.js"; -export declare class ObsidianLiveSyncSettingTab extends PluginSettingTab { - plugin: ObsidianLiveSyncPlugin; - private _lifetimeComponent; - get lifetimeComponent(): Component; - get core(): import("@/main.ts").LiveSyncCore; - get services(): import("../../../lib/src/services/InjectableServices.ts").InjectableServiceHub; - selectedScreen: string; - _editingSettings?: AllSettings; - get editingSettings(): AllSettings; - set editingSettings(v: AllSettings); - initialSettings?: typeof this.editingSettings; - /** - * Apply editing setting to the plug-in. - * @param keys setting keys for applying - */ - applySetting(keys: AllSettingItemKey[]): void; - applyAllSettings(): void; - saveLocalSetting(key: keyof typeof OnDialogSettingsDefault): Promise; - /** - * Apply and save setting to the plug-in. - * @param keys setting keys for applying - */ - saveSettings(keys: AllSettingItemKey[]): Promise; - /** - * Apply all editing setting to the plug-in. - * @param keys setting keys for applying - */ - saveAllDirtySettings(): Promise; - /** - * Invalidate buffered value and fetch the latest. - */ - requestUpdate(): void; - reloadAllLocalSettings(): { - configPassphrase: string; - preset: "" | "PERIODIC" | "LIVESYNC" | "DISABLE"; - syncMode: "ONEVENTS" | "PERIODIC" | "LIVESYNC"; - dummy: number; - deviceAndVaultName: string; - }; - computeAllLocalSettings(): Partial; - /** - * Reread all settings and request invalidate - */ - reloadAllSettings(skipUpdate?: boolean): void; - /** - * Reread each setting and request invalidate - */ - refreshSetting(key: AllSettingItemKey): void; - isDirty(key: AllSettingItemKey): boolean; - isSomeDirty(keys: AllSettingItemKey[]): boolean; - isConfiguredAs(key: AllStringItemKey, value: string): boolean; - isConfiguredAs(key: AllNumericItemKey, value: number): boolean; - isConfiguredAs(key: AllBooleanItemKey, value: boolean): boolean; - settingComponents: Setting[]; - controlledElementFunc: UpdateFunction[]; - onSavedHandlers: OnSavedHandler[]; - inWizard: boolean; - constructor(app: App, plugin: ObsidianLiveSyncPlugin); - testConnection(settingOverride?: Partial): Promise; - closeSetting(): void; - handleElement(element: HTMLElement, func: OnUpdateFunc): void; - createEl(el: HTMLElement, tag: T, o?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[T]) => void, func?: OnUpdateFunc): HTMLElementTagNameMap[T]; - addEl(el: HTMLElement, tag: T, o?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[T]) => void, func?: OnUpdateFunc): Promise>; - addOnSaved(key: T, func: OnSavedHandlerFunc): void; - resetEditingSettings(): void; - hide(): void; - isShown: boolean; - requestReload(): void; - manifestVersion: string; - lastVersion: number; - screenElements: { - [key: string]: HTMLElement[]; - }; - changeDisplay(screen: string): void; - enableMinimalSetup(): Promise; - menuEl?: HTMLElement; - addScreenElement(key: string, element: HTMLElement): void; - selectPane(event: Event): void; - isNeedRebuildLocal(): boolean; - isNeedRebuildRemote(): boolean; - isAnySyncEnabled(): boolean; - enableOnlySyncDisabled: OnUpdateFunc; - onlyOnP2POrCouchDB: () => OnUpdateResult; - onlyOnCouchDB: () => OnUpdateResult; - onlyOnMinIO: () => OnUpdateResult; - onlyOnOnlyP2P: () => OnUpdateResult; - onlyOnCouchDBOrMinIO: () => OnUpdateResult; - checkWorkingPassphrase: () => Promise; - isPassphraseValid: () => Promise; - rebuildDB: (method: "localOnly" | "remoteOnly" | "rebuildBothByThisDevice" | "localOnlyWithChunks") => Promise; - confirmRebuild(): Promise; - display(): void; - getMinioJournalSyncClient(): JournalSyncCore; - resetRemoteBucket(): Promise; -} diff --git a/_types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts b/_types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts deleted file mode 100644 index 144d32df..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneAdvanced.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts b/_types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts deleted file mode 100644 index 81c747e9..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneChangeLog.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -export declare function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts b/_types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts deleted file mode 100644 index 309f516f..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneCustomisationSync.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneCustomisationSync(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneGeneral.d.ts b/_types/src/modules/features/SettingDialogue/PaneGeneral.d.ts deleted file mode 100644 index 0b1f4a06..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneGeneral.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneGeneral(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneHatch.d.ts b/_types/src/modules/features/SettingDialogue/PaneHatch.d.ts deleted file mode 100644 index dc90c38b..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneHatch.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts b/_types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts deleted file mode 100644 index c431319b..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneMaintenance.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab"; -import { type PageFunctions } from "./SettingPane"; -export declare function paneMaintenance(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PanePatches.d.ts b/_types/src/modules/features/SettingDialogue/PanePatches.d.ts deleted file mode 100644 index 89b5973b..00000000 --- a/_types/src/modules/features/SettingDialogue/PanePatches.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts b/_types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts deleted file mode 100644 index e2c047ad..00000000 --- a/_types/src/modules/features/SettingDialogue/PanePowerUsers.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function panePowerUsers(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts b/_types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts deleted file mode 100644 index bee6bcc3..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneRemoteConfig.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneRemoteConfig(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneSelector.d.ts b/_types/src/modules/features/SettingDialogue/PaneSelector.d.ts deleted file mode 100644 index a33e6312..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneSelector.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneSetup.d.ts b/_types/src/modules/features/SettingDialogue/PaneSetup.d.ts deleted file mode 100644 index 206126da..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneSetup.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneSetup(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts b/_types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts deleted file mode 100644 index c710f270..00000000 --- a/_types/src/modules/features/SettingDialogue/PaneSyncSettings.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -import type { PageFunctions } from "./SettingPane.ts"; -export declare function paneSyncSettings(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel, addPane }: PageFunctions): void; diff --git a/_types/src/modules/features/SettingDialogue/SettingPane.d.ts b/_types/src/modules/features/SettingDialogue/SettingPane.d.ts deleted file mode 100644 index 0cf9dcef..00000000 --- a/_types/src/modules/features/SettingDialogue/SettingPane.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ConfigLevel } from "@lib/common/types"; -import type { AllSettingItemKey, AllSettings } from "./settingConstants"; -export declare const combineOnUpdate: (func1: OnUpdateFunc, func2: OnUpdateFunc) => OnUpdateFunc; -export declare const setLevelClass: (el: HTMLElement, level?: ConfigLevel) => void; -export declare function setStyle(el: HTMLElement, styleHead: string, condition: () => boolean): void; -export declare function visibleOnly(cond: () => boolean): OnUpdateFunc; -export declare function enableOnly(cond: () => boolean): OnUpdateFunc; -export type OnUpdateResult = { - visibility?: boolean; - disabled?: boolean; - classes?: string[]; - isCta?: boolean; - isWarning?: boolean; -}; -export type OnUpdateFunc = () => OnUpdateResult; -export type UpdateFunction = () => void; -export type OnSavedHandlerFunc = (value: AllSettings[T]) => Promise | void; -export type OnSavedHandler = { - key: T; - handler: OnSavedHandlerFunc; -}; -export declare function getLevelStr(level: ConfigLevel): "" | import("octagonal-wheels/common/types").TaggedType | import("octagonal-wheels/common/types").TaggedType | import("octagonal-wheels/common/types").TaggedType; -export type AutoWireOption = { - placeHolder?: string; - holdValue?: boolean; - isPassword?: boolean; - invert?: boolean; - onUpdate?: OnUpdateFunc; - obsolete?: boolean; -}; -export declare function findAttrFromParent(el: HTMLElement, attr: string): string; -export declare function wrapMemo(func: (arg: T) => void): (arg: T) => void; -export type PageFunctions = { - addPane: (parentEl: HTMLElement, title: string, icon: string, order: number, wizardHidden: boolean, level?: ConfigLevel) => Promise; - addPanel: (parentEl: HTMLElement, title: string, callback?: (el: HTMLDivElement) => void, func?: OnUpdateFunc, level?: ConfigLevel) => Promise; -}; diff --git a/_types/src/modules/features/SettingDialogue/SveltePanel.d.ts b/_types/src/modules/features/SettingDialogue/SveltePanel.d.ts deleted file mode 100644 index 9a581d41..00000000 --- a/_types/src/modules/features/SettingDialogue/SveltePanel.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type Component } from "svelte"; -import { type Writable } from "svelte/store"; -/** - * Props passed to Svelte panels, containing a writable port - * to communicate with the panel - */ -export type SveltePanelProps = { - port: Writable; -}; -/** - * A class to manage a Svelte panel within Obsidian - * Especially useful for settings panels - */ -export declare class SveltePanel { - private _mountedComponent; - private _componentValue; - /** - * Creates a Svelte panel instance - * @param component Component to mount - * @param mountTo HTMLElement to mount the component to - * @param valueStore Optional writable store to bind to the component's port, if not provided a new one will be created - * @returns The SveltePanel instance - */ - constructor(component: Component>, mountTo: HTMLElement, valueStore?: Writable); - /** - * Destroys the Svelte panel instance by unmounting the component - */ - destroy(): void; - /** - * Gets or sets the current value of the component's port - */ - get componentValue(): T | undefined; - set componentValue(value: T | undefined); -} diff --git a/_types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts b/_types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts deleted file mode 100644 index 16dbbf6f..00000000 --- a/_types/src/modules/features/SettingDialogue/remoteConfigBuffer.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianLiveSyncSettings } from "@lib/common/types.ts"; -export declare function syncActivatedRemoteSettings(target: Partial, source: ObsidianLiveSyncSettings): void; diff --git a/_types/src/modules/features/SettingDialogue/settingConstants.d.ts b/_types/src/modules/features/SettingDialogue/settingConstants.d.ts deleted file mode 100644 index edc2ff40..00000000 --- a/_types/src/modules/features/SettingDialogue/settingConstants.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export * from "@lib/common/settingConstants.ts"; diff --git a/_types/src/modules/features/SettingDialogue/settingUtils.d.ts b/_types/src/modules/features/SettingDialogue/settingUtils.d.ts deleted file mode 100644 index 99d52117..00000000 --- a/_types/src/modules/features/SettingDialogue/settingUtils.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -/** - * Generates a summary of P2P configuration settings - * @param setting Settings object - * @param additional Additional summary information to include - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getP2PConfigSummary(setting: ObsidianLiveSyncSettings, additional?: Record, showAdvanced?: boolean): { - [x: string]: string; -}; -/** - * Generates a summary of Object Storage configuration settings - * @param setting Settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getBucketConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced?: boolean): Record; -/** - * Generates a summary of CouchDB configuration settings - * @param setting Settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getCouchDBConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced?: boolean): Record; -/** - * Generates a summary of E2EE configuration settings - * @param setting Settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getE2EEConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced?: boolean): Record; -/** - * Converts partial settings into a summary object - * @param setting Partial settings object - * @param showAdvanced Whether to include advanced settings - * @returns Summary object - */ -export declare function getSummaryFromPartialSettings(setting: Partial, showAdvanced?: boolean): Record; -/** - * Copy document from one database to another for migration purposes - * @param docName document ID - * @param dbFrom source database - * @param dbTo destination database - * @returns - */ -export declare function copyMigrationDocs(docName: string, dbFrom: PouchDB.Database, dbTo: PouchDB.Database): Promise; -type PouchDBOpenFunction = () => Promise | PouchDB.Database; -/** - * Migrate databases from one to another - * @param operationName Name of the migration operation - * @param from source database - * @param openTo function to open destination database - * @returns True if migration succeeded - */ -export declare function migrateDatabases(operationName: string, from: PouchDB.Database, openTo: PouchDBOpenFunction): Promise; -export {}; diff --git a/_types/src/modules/features/SetupManager.d.ts b/_types/src/modules/features/SetupManager.d.ts deleted file mode 100644 index 711717a2..00000000 --- a/_types/src/modules/features/SetupManager.d.ts +++ /dev/null @@ -1,121 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts"; -import { AbstractModule } from "@/modules/AbstractModule.ts"; -/** - * User modes for onboarding and setup - */ -export declare const enum UserMode { - /** - * New User Mode - for users who are new to the plugin - */ - NewUser = "new-user", - /** - * Existing User Mode - for users who have used the plugin before, or just configuring again - */ - ExistingUser = "existing-user", - /** - * Unknown User Mode - for cases where the user mode is not determined - */ - Unknown = "unknown", - /** - * Update User Mode - for users who are updating configuration. May be `existing-user` as well, but possibly they want to treat it differently. - */ - Update = "unknown" // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value -} -/** - * Setup Manager to handle onboarding and configuration setup - */ -export declare class SetupManager extends AbstractModule { - get dialogManager(): import("../../lib/src/UI/svelteDialog.ts").SvelteDialogManagerBase; - /** - * Starts the onboarding process - * @returns Promise that resolves to true if onboarding completed successfully, false otherwise - */ - startOnBoarding(): Promise; - /** - * Handles the onboarding process based on user mode - * @param userMode - * @returns Promise that resolves to true if onboarding completed successfully, false otherwise - */ - onOnboard(userMode: UserMode): Promise; - /** - * Handles setup using a setup URI - * @param userMode - * @param setupURI - * @returns Promise that resolves to true if onboarding completed successfully, false otherwise - */ - onUseSetupURI(userMode: UserMode, setupURI?: string): Promise; - /** - * Handles manual setup for CouchDB - * @param userMode - * @param currentSetting - * @param activate Whether to activate the CouchDB as remote type - * @returns Promise that resolves to true if setup completed successfully, false otherwise - */ - onCouchDBManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise; - /** - * Handles manual setup for S3-compatible bucket - * @param userMode - * @param currentSetting - * @param activate Whether to activate the Bucket as remote type - * @returns Promise that resolves to true if setup completed successfully, false otherwise - */ - onBucketManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise; - /** - * Handles manual setup for P2P - * @param userMode - * @param currentSetting - * @param activate Whether to activate the P2P as remote type (as P2P Only setup) - * @returns Promise that resolves to true if setup completed successfully, false otherwise - */ - onP2PManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise; - /** - * Handles only E2EE configuration - * @param userMode - * @param currentSetting - * @returns - */ - onlyE2EEConfiguration(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings): Promise; - /** - * Handles manual configuration flow (E2EE + select server) - * @param originalSetting - * @param userMode - * @returns - */ - onConfigureManually(originalSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise; - /** - * Handles server selection during manual configuration - * @param currentSetting - * @param userMode - * @returns - */ - onSelectServer(currentSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise; - /** - * Confirms and applies settings obtained from the wizard - * @param newConf - * @param _userMode - * @param activate Whether to activate the remote type in the new settings - * @param extra Extra function to run before applying settings - * @returns Promise that resolves to true if settings applied successfully, false otherwise - */ - onConfirmApplySettingsFromWizard(newConf: ObsidianLiveSyncSettings, _userMode: UserMode, activate?: boolean, extra?: () => void): Promise; - /** - * Prompts the user with QR code scanning instructions - * @returns Promise that resolves to false as QR code instruction dialog does not yield settings directly - */ - onPromptQRCodeInstruction(): Promise; - /** - * Decodes settings from a QR code string and applies them - * @param qr QR code string containing encoded settings - * @returns Promise that resolves to true if settings applied successfully, false otherwise - */ - decodeQR(qr: string): Promise; - /** - * Applies the new settings to the core settings and saves them - * @param newConf - * @param userMode - * @returns Promise that resolves to true if settings applied successfully, false otherwise - */ - applySetting(newConf: ObsidianLiveSyncSettings, userMode: UserMode): Promise; -} diff --git a/_types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts b/_types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts deleted file mode 100644 index 995c6bf9..00000000 --- a/_types/src/modules/features/SetupWizard/dialogs/setupDialogTypes.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { BucketSyncSetting, CouchDBConnection, EncryptionSettings, ObsidianLiveSyncSettings, P2PConnectionInfo } from "@lib/common/models/setting.type"; -export declare const TYPE_IDENTICAL = "identical"; -export declare const TYPE_INDEPENDENT = "independent"; -export declare const TYPE_UNBALANCED = "unbalanced"; -export declare const TYPE_CANCEL = "cancelled"; -export declare const TYPE_BACKUP_DONE = "backup_done"; -export declare const TYPE_BACKUP_SKIPPED = "backup_skipped"; -export declare const TYPE_UNABLE_TO_BACKUP = "unable_to_backup"; -export declare const TYPE_NEW_USER = "new-user"; -export declare const TYPE_EXISTING_USER = "existing-user"; -export declare const TYPE_CANCELLED = "cancelled"; -export declare const TYPE_EXISTING = "existing-user"; -export declare const TYPE_NEW = "new-user"; -export declare const TYPE_COMPATIBLE_EXISTING = "compatible-existing-user"; -export declare const TYPE_APPLY = "apply"; -export declare const TYPE_USE_SETUP_URI = "use-setup-uri"; -export declare const TYPE_SCAN_QR_CODE = "scan-qr-code"; -export declare const TYPE_CONFIGURE_MANUALLY = "configure-manually"; -export declare const TYPE_CLOSE = "close"; -export declare const TYPE_COUCHDB = "couchdb"; -export declare const TYPE_BUCKET = "bucket"; -export declare const TYPE_P2P = "p2p"; -export type ResultTypeVault = typeof TYPE_IDENTICAL | typeof TYPE_INDEPENDENT | typeof TYPE_UNBALANCED | typeof TYPE_CANCEL; -export type ResultTypeBackup = typeof TYPE_BACKUP_DONE | typeof TYPE_BACKUP_SKIPPED | typeof TYPE_UNABLE_TO_BACKUP | typeof TYPE_CANCEL; -export type ResultTypeExtra = { - preventFetchingConfig: boolean; -}; -export type FetchEverythingResult = { - vault: ResultTypeVault; - backup: ResultTypeBackup; - extra: ResultTypeExtra; -} | typeof TYPE_CANCEL; -export type RebuildEverythingResult = { - backup: ResultTypeBackup; - extra: ResultTypeExtra; -} | typeof TYPE_CANCEL; -export type IntroResultType = typeof TYPE_NEW_USER | typeof TYPE_EXISTING_USER | typeof TYPE_CANCELLED; -export type OutroAskUserModeResultType = typeof TYPE_EXISTING | typeof TYPE_NEW | typeof TYPE_COMPATIBLE_EXISTING | typeof TYPE_CANCELLED; -export type OutroExistingUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED; -export type OutroNewUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED; -export type SelectMethodNewUserResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED; -export type SelectMethodExistingResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_SCAN_QR_CODE | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED; -export type SetupRemoteResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | typeof TYPE_P2P | typeof TYPE_CANCELLED; -export type UseSetupURIResultType = typeof TYPE_CANCELLED | ObsidianLiveSyncSettings; -export type SetupRemoteE2EEResultType = typeof TYPE_CANCELLED | EncryptionSettings; -export type SetupRemoteBucketResultType = typeof TYPE_CANCELLED | BucketSyncSetting; -export type SetupRemoteCouchDBResultType = typeof TYPE_CANCELLED | CouchDBConnection; -export type SetupRemoteP2PResultType = typeof TYPE_CANCELLED | P2PConnectionInfo; -export type ScanQRCodeResultType = typeof TYPE_CLOSE; diff --git a/_types/src/modules/features/StatusBarDisplay.d.ts b/_types/src/modules/features/StatusBarDisplay.d.ts deleted file mode 100644 index 4a0714bb..00000000 --- a/_types/src/modules/features/StatusBarDisplay.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ReactiveValue } from "octagonal-wheels/dataobject/reactive"; -export declare const STATUS_COUNTER_INACTIVE_LINGER_MS = 3000; -export type DisposableReactiveValue = ReactiveValue & { - dispose(): void; -}; -/** - * Mirrors an activity count while keeping each visible period on screen for a - * minimum total lifetime. The delay applies only when the source becomes zero. - */ -export declare function createMinimumVisibleActivityCount(source: ReactiveValue, minimumVisibleMs: number): DisposableReactiveValue; -/** - * Formats a counter with a stable width and briefly retains its zero value so - * that the completion of queued work remains visible. - */ -export declare function createPaddedCounterLabel(source: ReactiveValue, mark: string, inactiveLingerMs?: number): DisposableReactiveValue; diff --git a/_types/src/modules/main/ModuleLiveSyncMain.d.ts b/_types/src/modules/main/ModuleLiveSyncMain.d.ts deleted file mode 100644 index e49f2958..00000000 --- a/_types/src/modules/main/ModuleLiveSyncMain.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AbstractModule } from "@/modules/AbstractModule.ts"; -import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; -import type { LiveSyncCore } from "@/main.ts"; -export declare class ModuleLiveSyncMain extends AbstractModule { - _onLiveSyncReady(): Promise; - _wireUpEvents(): Promise; - _onLiveSyncLoad(): Promise; - onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void; -} diff --git a/_types/src/modules/services/ObsidianAPIService.d.ts b/_types/src/modules/services/ObsidianAPIService.d.ts deleted file mode 100644 index 8de3744d..00000000 --- a/_types/src/modules/services/ObsidianAPIService.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { type Command } from "@/deps.ts"; -import { ObsHttpHandler } from "@/modules/essentialObsidian/APILib/ObsHttpHandler"; -import type { Confirm } from "@lib/interfaces/Confirm"; -declare module "obsidian" { - interface App { - appId?: string; - isMobile?: boolean; - } -} -export declare class ObsidianAPIService extends InjectableAPIService { - _customHandler: ObsHttpHandler | undefined; - _confirmInstance: Confirm; - constructor(context: ObsidianServiceContext); - getCustomFetchHandler(): ObsHttpHandler; - showWindow(viewType: string): Promise; - showWindowOnRight(viewType: string): Promise; - private get app(); - getPlatform(): string; - isMobile(): boolean; - getAppID(): string; - getSystemVaultName(): string; - getAppVersion(): string; - getPluginVersion(): string; - get confirm(): Confirm; - addCommand(command: TCommand): TCommand; - registerWindow(type: string, factory: (leaf: T) => unknown): void; - addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => unknown): HTMLElement; - registerProtocolHandler(action: string, handler: (params: Record) => unknown): void; - /** - * In Obsidian, we will use the native `requestUrl` function as the default fetch handler, - * to address unavoidable CORS issues. - */ - nativeFetch(req: string | Request, opts?: RequestInit): Promise; - addStatusBarItem(): HTMLElement | undefined; - setInterval(handler: () => void, timeout: number): number; - getSystemConfigDir(): string; -} diff --git a/_types/src/modules/services/ObsidianAppLifecycleService.d.ts b/_types/src/modules/services/ObsidianAppLifecycleService.d.ts deleted file mode 100644 index 362af798..00000000 --- a/_types/src/modules/services/ObsidianAppLifecycleService.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { AppLifecycleServiceBase } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -declare module "obsidian" { - interface App { - commands: { - executeCommandById: (id: string) => Promise; - }; - } -} -export declare class ObsidianAppLifecycleService extends AppLifecycleServiceBase { - performRestart(): void; -} diff --git a/_types/src/modules/services/ObsidianConfirm.d.ts b/_types/src/modules/services/ObsidianConfirm.d.ts deleted file mode 100644 index 01c28cd1..00000000 --- a/_types/src/modules/services/ObsidianConfirm.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type App, type Plugin } from "@/deps"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -export declare class ObsidianConfirm implements Confirm { - private _context; - get _app(): App; - get _plugin(): Plugin; - constructor(context: T); - askYesNo(message: string): Promise<"yes" | "no">; - askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise; - askYesNoDialog(message: string, opt?: { - title?: string; - defaultOption?: "Yes" | "No"; - timeout?: number; - }): Promise<"yes" | "no">; - askSelectString(message: string, items: string[]): Promise; - askSelectStringDialogue(message: string, buttons: T, opt: { - title?: string; - defaultAction: T[number]; - timeout?: number; - }): Promise; - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void; - confirmWithMessage(title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], timeout?: number): Promise<(typeof buttons)[number] | false>; -} diff --git a/_types/src/modules/services/ObsidianDatabaseService.d.ts b/_types/src/modules/services/ObsidianDatabaseService.d.ts deleted file mode 100644 index ef6d3bf9..00000000 --- a/_types/src/modules/services/ObsidianDatabaseService.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { DatabaseService, type DatabaseServiceDependencies } from "@lib/services/base/DatabaseService.ts"; -export declare class ObsidianDatabaseService extends DatabaseService { - private __onOpenDatabase; - constructor(context: T, dependencies: DatabaseServiceDependencies); -} diff --git a/_types/src/modules/services/ObsidianPathService.d.ts b/_types/src/modules/services/ObsidianPathService.d.ts deleted file mode 100644 index ee27eaee..00000000 --- a/_types/src/modules/services/ObsidianPathService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { PathService } from "@lib/services/base/PathService"; -import { type BASE_IS_NEW, type TARGET_IS_NEW, type EVEN } from "@/common/utils"; -import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@lib/common/types"; -export declare class ObsidianPathService extends PathService { - markChangesAreSame(old: UXFileInfo | AnyEntry | FilePathWithPrefix, newMtime: number, oldMtime: number): boolean | undefined; - unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void; - compareFileFreshness(baseFile: UXFileInfoStub | AnyEntry | undefined, checkTarget: UXFileInfo | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN; - isMarkedAsSameChanges(file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, mtimes: number[]): undefined | typeof EVEN; - protected normalizePath(path: string): string; -} diff --git a/_types/src/modules/services/ObsidianServiceHub.d.ts b/_types/src/modules/services/ObsidianServiceHub.d.ts deleted file mode 100644 index 1c8cf0bc..00000000 --- a/_types/src/modules/services/ObsidianServiceHub.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type ObsidianLiveSyncPlugin from "@/main"; -export declare class ObsidianServiceHub extends InjectableServiceHub { - constructor(plugin: ObsidianLiveSyncPlugin); -} diff --git a/_types/src/modules/services/ObsidianServices.d.ts b/_types/src/modules/services/ObsidianServices.d.ts deleted file mode 100644 index 3b333228..00000000 --- a/_types/src/modules/services/ObsidianServices.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService"; -import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService"; -import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService"; -import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService"; -import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService"; -import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService"; -import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService"; -import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService"; -import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { ControlService } from "@lib/services/base/ControlService"; -export declare class ObsidianDatabaseEventService extends InjectableDatabaseEventService { -} -export declare class ObsidianReplicatorService extends InjectableReplicatorService { -} -export declare class ObsidianFileProcessingService extends InjectableFileProcessingService { -} -export declare class ObsidianReplicationService extends InjectableReplicationService { -} -export declare class ObsidianRemoteService extends InjectableRemoteService { -} -export declare class ObsidianConflictService extends InjectableConflictService { -} -export declare class ObsidianTweakValueService extends InjectableTweakValueService { -} -export declare class ObsidianTestService extends InjectableTestService { -} -export declare class ObsidianConfigService extends ConfigServiceBrowserCompat { -} -export declare class ObsidianKeyValueDBService extends KeyValueDBService { -} -export declare class ObsidianControlService extends ControlService { -} diff --git a/_types/src/modules/services/ObsidianSettingService.d.ts b/_types/src/modules/services/ObsidianSettingService.d.ts deleted file mode 100644 index bd143f22..00000000 --- a/_types/src/modules/services/ObsidianSettingService.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -export declare class ObsidianSettingService extends SettingService { - constructor(context: T, dependencies: SettingServiceDependencies); - protected setItem(key: string, value: string): void; - protected getItem(key: string): string; - protected deleteItem(key: string): void; - protected saveData(data: ObsidianLiveSyncSettings): Promise; - protected loadData(): Promise; -} diff --git a/_types/src/modules/services/ObsidianUIService.d.ts b/_types/src/modules/services/ObsidianUIService.d.ts deleted file mode 100644 index 383c840a..00000000 --- a/_types/src/modules/services/ObsidianUIService.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ConfigService } from "@lib/services/base/ConfigService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import { UIService } from "@lib/services/implements/base/UIService"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { IAPIService, IControlService } from "@lib/services/base/IService"; -export type ObsidianUIServiceDependencies = { - appLifecycle: AppLifecycleService; - config: ConfigService; - replicator: ReplicatorService; - APIService: IAPIService; - control: IControlService; -}; -export declare class ObsidianUIService extends UIService { - get dialogToCopy(): import("svelte/legacy").LegacyComponentType; - constructor(context: ObsidianServiceContext, dependents: ObsidianUIServiceDependencies); -} diff --git a/_types/src/modules/services/ObsidianVaultService.d.ts b/_types/src/modules/services/ObsidianVaultService.d.ts deleted file mode 100644 index 5a732866..00000000 --- a/_types/src/modules/services/ObsidianVaultService.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { FilePath } from "@lib/common/types"; -declare module "obsidian" { - interface DataAdapter { - insensitive?: boolean; - } -} -export declare class ObsidianVaultService extends InjectableVaultService { - vaultName(): string; - getActiveFilePath(): FilePath | undefined; - isStorageInsensitive(): boolean; - shouldCheckCaseInsensitively(): boolean; - isValidPath(path: string): boolean; -} diff --git a/_types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts b/_types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts deleted file mode 100644 index b45ae5a3..00000000 --- a/_types/src/serviceFeatures/onLayoutReady/enablei18n.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -export declare const enableI18nFeature: import("@lib/interfaces/ServiceModule").ServiceFeatureFunction, keyof import("@lib/interfaces/ServiceModule").ServiceModules, Promise>; diff --git a/_types/src/serviceFeatures/redFlag.d.ts b/_types/src/serviceFeatures/redFlag.d.ts deleted file mode 100644 index 1543900a..00000000 --- a/_types/src/serviceFeatures/redFlag.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -/** - * Flag file handler interface, similar to target filter pattern. - */ -interface FlagFileHandler { - priority: number; - check: () => Promise; - handle: () => Promise; -} -export declare function isFlagFileExist(host: NecessaryServices, path: string): Promise; -export declare function deleteFlagFile(host: NecessaryServices, log: LogFunction, path: string): Promise; -/** - * Factory function to create a fetch all flag handler. - * All logic related to fetch all flag is encapsulated here. - */ -export declare function createFetchAllFlagHandler(host: NecessaryServices<"vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle" | "path" | "keyValueDB" | "database", "storageAccess" | "rebuilder" | "fileHandler">, log: LogFunction): FlagFileHandler; -/** - * Adjust setting to remote configuration. - * @param config current configuration to retrieve remote preferred config - * @returns updated configuration if applied, otherwise null. - */ -export declare function adjustSettingToRemote(host: NecessaryServices<"tweakValue" | "UI" | "setting", never>, log: LogFunction, config: ObsidianLiveSyncSettings): Promise; -/** - * Adjust setting to remote if needed. - * @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything) - * @param config current configuration to retrieve remote preferred config - */ -export declare function adjustSettingToRemoteIfNeeded(host: NecessaryServices<"tweakValue" | "UI" | "setting", never>, log: LogFunction, extra: { - preventFetchingConfig: boolean; -}, config: ObsidianLiveSyncSettings): Promise; -/** - * Process vault initialisation with suspending file watching and sync. - * @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process. - * @param keepSuspending whether to keep suspending file watching after the process. - * @returns result of the process, or false if error occurs. - */ -export declare function processVaultInitialisation(host: NecessaryServices<"setting", never>, log: LogFunction, proc: () => Promise, keepSuspending?: boolean): Promise; -export declare function verifyAndUnlockSuspension(host: NecessaryServices<"setting" | "appLifecycle" | "UI", never>, log: LogFunction): Promise; -/** - * Factory function to create a rebuild flag handler. - * All logic related to rebuild flag is encapsulated here. - */ -export declare function createRebuildFlagHandler(host: NecessaryServices<"setting" | "appLifecycle" | "UI" | "tweakValue", "storageAccess" | "rebuilder">, log: LogFunction): { - priority: number; - check: () => Promise; - handle: () => Promise; -}; -/** - * Factory function to create a suspend all flag handler. - * All logic related to suspend flag is encapsulated here. - */ -export declare function createSuspendFlagHandler(host: NecessaryServices<"setting", "storageAccess">, log: LogFunction): FlagFileHandler; -export declare function flagHandlerToEventHandler(flagHandler: FlagFileHandler): () => Promise; -export declare function useRedFlagFeatures(host: NecessaryServices<"API" | "appLifecycle" | "UI" | "setting" | "tweakValue" | "fileProcessing" | "vault" | "path" | "keyValueDB" | "database", "storageAccess" | "rebuilder" | "fileHandler">): void; -export {}; diff --git a/_types/src/serviceFeatures/redFlag.simpleFetch.d.ts b/_types/src/serviceFeatures/redFlag.simpleFetch.d.ts deleted file mode 100644 index 339ebdc1..00000000 --- a/_types/src/serviceFeatures/redFlag.simpleFetch.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import { type FullScanOptions } from "@lib/serviceFeatures/offlineScanner"; -export declare const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files"; -export declare const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer"; -export declare const SIMPLE_FETCH_STAGE1_LEGACY = "Use the detailed flow"; -export declare const SIMPLE_FETCH_STAGE1_CANCEL = "Cancel"; -export declare const SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE = "Keep local files even if not on remote"; -export declare const SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL = "Delete local files if not on remote"; -export declare const SIMPLE_FETCH_STAGE2_NEWER_CLEANUP = "Delete local files if deleted on remote"; -export declare const SIMPLE_FETCH_STAGE2_NEWER_SYNC_ALL = "Keep local files even if deleted on remote"; -export declare const STAGE2_ABORT = "Cancel all and reboot"; -export declare function askSimpleFetchMode(host: NecessaryServices<"UI" | "setting", never>): Promise<{ - mode: string; - options: Partial; -} | "cancelled" | "aborted">; -export declare function askAndPerformFastSetupOnScheduledFetchAll(host: NecessaryServices<"vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle" | "path" | "keyValueDB" | "database", "storageAccess" | "rebuilder" | "fileHandler">, log: LogFunction, cleanupFlag: () => Promise): Promise; diff --git a/_types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts b/_types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts deleted file mode 100644 index ea8d7c7b..00000000 --- a/_types/src/serviceFeatures/setupObsidian/setupManagerHandlers.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type SetupManager } from "@/modules/features/SetupManager"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -export declare function openSetupURI(setupManager: SetupManager): Promise; -export declare function openP2PSettings(host: SetupFeatureHost, setupManager: SetupManager): Promise; -export declare function useSetupManagerHandlersFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>, setupManager: SetupManager): void; diff --git a/_types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts b/_types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts deleted file mode 100644 index f597168c..00000000 --- a/_types/src/serviceFeatures/setupObsidian/setupProtocol.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { LogFunction } from "@lib/services/lib/logUtils"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type SetupManager } from "@/modules/features/SetupManager"; -export declare function registerSetupProtocolHandler(host: SetupFeatureHost, log: LogFunction, setupManager: SetupManager): void; -export declare function useSetupProtocolFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>, setupManager: SetupManager): void; diff --git a/_types/src/serviceFeatures/useP2PReplicatorUI.d.ts b/_types/src/serviceFeatures/useP2PReplicatorUI.d.ts deleted file mode 100644 index 4e838b3e..00000000 --- a/_types/src/serviceFeatures/useP2PReplicatorUI.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type UseP2PReplicatorResult } from "@lib/replication/trystero/UseP2PReplicatorResult"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector"; -import type { LiveSyncCore } from "@/main"; -/** - * ServiceFeature: P2P Replicator lifecycle management. - * Binds a LiveSyncTrysteroReplicator to the host's lifecycle events, - * following the same middleware style as useOfflineScanner. - * - * @param viewTypeAndFactory Optional [viewType, factory] pair for registering the P2P pane view. - * When provided, also registers commands and ribbon icon via services.API. - */ -export declare function useP2PReplicatorUI(host: NecessaryServices<"API" | "appLifecycle" | "setting" | "vault" | "database" | "databaseEvents" | "keyValueDB" | "replication" | "config" | "UI" | "replicator", never>, core: LiveSyncCore, replicator: UseP2PReplicatorResult): { - replicator: import("../lib/src/replication/trystero/LiveSyncTrysteroReplicator").LiveSyncTrysteroReplicator; - p2pLogCollector: P2PLogCollector; - storeP2PStatusLine: import("octagonal-wheels/dataobject/reactive_v2").ReactiveSource; -}; diff --git a/_types/src/serviceModules/DatabaseFileAccess.d.ts b/_types/src/serviceModules/DatabaseFileAccess.d.ts deleted file mode 100644 index 2cfd564f..00000000 --- a/_types/src/serviceModules/DatabaseFileAccess.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; -import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase"; -export declare class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess { -} diff --git a/_types/src/serviceModules/FileAccessObsidian.d.ts b/_types/src/serviceModules/FileAccessObsidian.d.ts deleted file mode 100644 index a7a12a86..00000000 --- a/_types/src/serviceModules/FileAccessObsidian.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type App } from "@/deps"; -import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts"; -import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; -/** - * Obsidian-specific implementation of FileAccessBase - * Uses ObsidianFileSystemAdapter for platform-specific operations - */ -export declare class FileAccessObsidian extends FileAccessBase { - constructor(app: App, dependencies: FileAccessBaseDependencies); -} diff --git a/_types/src/serviceModules/FileHandler.d.ts b/_types/src/serviceModules/FileHandler.d.ts deleted file mode 100644 index 843572c7..00000000 --- a/_types/src/serviceModules/FileHandler.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase"; -export declare class ServiceFileHandler extends ServiceFileHandlerBase { -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts deleted file mode 100644 index 6896bd23..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types"; -import type { IConversionAdapter } from "@lib/serviceModules/adapters"; -import type { TFile, TFolder } from "obsidian"; -/** - * Conversion adapter implementation for Obsidian - */ -export declare class ObsidianConversionAdapter implements IConversionAdapter { - nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub; - nativeFolderToUXFolder(folder: TFolder): UXFolderInfo; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts deleted file mode 100644 index c0dfe204..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { FilePath, UXStat } from "@lib/common/types"; -import type { IFileSystemAdapter, IPathAdapter, ITypeGuardAdapter, IConversionAdapter, IStorageAdapter, IVaultAdapter } from "@lib/serviceModules/adapters"; -import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian"; -declare module "obsidian" { - interface Vault { - getAbstractFileByPathInsensitive(path: string): TAbstractFile | null; - } - interface DataAdapter { - reconcileInternalFile?(path: string): Promise; - } -} -/** - * Complete file system adapter implementation for Obsidian - */ -export declare class ObsidianFileSystemAdapter implements IFileSystemAdapter { - private app; - readonly path: IPathAdapter; - readonly typeGuard: ITypeGuardAdapter; - readonly conversion: IConversionAdapter; - readonly storage: IStorageAdapter; - readonly vault: IVaultAdapter; - constructor(app: App); - getAbstractFileByPath(path: FilePath | string): Promise; - getAbstractFileByPathInsensitive(path: FilePath | string): Promise; - getFiles(): Promise; - renameFile(file: TFile, newPath: string): Promise; - statFromNative(file: TFile): Promise; - reconcileInternalFile(path: string): Promise; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts deleted file mode 100644 index 43421db9..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { type TAbstractFile } from "@/deps"; -import type { FilePath } from "@lib/common/types"; -import type { IPathAdapter } from "@lib/serviceModules/adapters"; -/** - * Path adapter implementation for Obsidian - */ -export declare class ObsidianPathAdapter implements IPathAdapter { - getPath(file: string | TAbstractFile): FilePath; - normalisePath(path: string): string; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts deleted file mode 100644 index ef874d13..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IStorageAdapter } from "@lib/serviceModules/adapters"; -import type { Stat, App } from "obsidian"; -/** - * Storage adapter implementation for Obsidian - */ -export declare class ObsidianStorageAdapter implements IStorageAdapter { - private app; - constructor(app: App); - exists(path: string): Promise; - trystat(path: string): Promise; - stat(path: string): Promise; - mkdir(path: string): Promise; - remove(path: string): Promise; - read(path: string): Promise; - readBinary(path: string): Promise; - write(path: string, data: string, options?: UXDataWriteOptions): Promise; - writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - append(path: string, data: string, options?: UXDataWriteOptions): Promise; - list(basePath: string): Promise<{ - files: string[]; - folders: string[]; - }>; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts deleted file mode 100644 index 768a5721..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters"; -import { TFile, TFolder } from "obsidian"; -/** - * Type guard adapter implementation for Obsidian - */ -export declare class ObsidianTypeGuardAdapter implements ITypeGuardAdapter { - isFile(file: unknown): file is TFile; - isFolder(item: unknown): item is TFolder; -} diff --git a/_types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts b/_types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts deleted file mode 100644 index 6a2eccea..00000000 --- a/_types/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IVaultAdapter } from "@lib/serviceModules/adapters"; -import type { TFile, App, TFolder } from "obsidian"; -/** - * Vault adapter implementation for Obsidian - */ -export declare class ObsidianVaultAdapter implements IVaultAdapter { - private app; - constructor(app: App); - read(file: TFile): Promise; - cachedRead(file: TFile): Promise; - readBinary(file: TFile): Promise; - modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise; - modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - create(path: string, data: string, options?: UXDataWriteOptions): Promise; - createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise; - rename(file: TFile, newPath: string): Promise; - delete(file: TFile | TFolder, force?: boolean): Promise; - trash(file: TFile | TFolder, force?: boolean): Promise; - trigger(name: string, ...data: unknown[]): void; -} diff --git a/_types/src/serviceModules/ServiceFileAccessImpl.d.ts b/_types/src/serviceModules/ServiceFileAccessImpl.d.ts deleted file mode 100644 index dd046dd2..00000000 --- a/_types/src/serviceModules/ServiceFileAccessImpl.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase"; -import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; -export declare class ServiceFileAccessObsidian extends ServiceFileAccessBase { -} diff --git a/_types/src/types.d.ts b/_types/src/types.d.ts deleted file mode 100644 index e80355b5..00000000 --- a/_types/src/types.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-nocheck -// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: bbf2539 -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { IServiceHub } from "@lib/services/base/IService"; -export interface ServiceModules { - storageAccess: StorageAccess; - /** - * Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc. - */ - databaseFileAccess: DatabaseFileAccess; - /** - * File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc. - */ - fileHandler: IFileHandler; - /** - * Rebuilder for handling database rebuilding operations. - */ - rebuilder: Rebuilder; -} -export interface LiveSyncHost { - services: IServiceHub; - serviceModules: ServiceModules; -} diff --git a/devs.md b/devs.md index 38432cf7..a6acd9c6 100644 --- a/devs.md +++ b/devs.md @@ -10,22 +10,19 @@ Self-hosted LiveSync is an Obsidian plugin for synchronising vaults across devic #### First-time Setup -This repository uses submodules by convention. Therefore, you must use the `--recursive` flag when cloning it. - ```bash -git clone --recursive https://github.com/vrtmrz/obsidian-livesync +git clone https://github.com/vrtmrz/obsidian-livesync +cd obsidian-livesync npm ci npm run build ``` -Note: if you already cloned without submodules, run: `git submodule update --init --recursive` - #### Branch switching -When switching branches, please make sure to update submodules as well, since they may be updated in the new branch. +When switching branches, reinstall dependencies when the lockfile changes. ```bash -git checkout --recurse-submodules 0.25.70-patch1 # tag or branch name +git checkout 0.25.70-patch1 # tag or branch name npm ci npm run build ``` @@ -38,14 +35,15 @@ npm run check # TypeScript and svelte type checking npm run dev # Development build with auto-rebuild (uses .env for test vault paths) npm run build # Production build npm run buildDev # Development build (one-time) -npm run bakei18n # Pre-build step: compile i18n resources (YAML → JSON → TS) -npm run test:unit # Run unit tests only (no Docker services required) -npm test # Run Harness based vitest tests (requires Docker services), not recommended, unstable. +npm run test:integration # Run CouchDB-backed integration tests +npm run test:setup-tools # Check provisioning and Setup URI package contracts +npm run test:e2e:cli:p2p # Run canonical P2P validation in Compose +npm run test:e2e:obsidian:local-suite # Run the real Obsidian local suite ``` ### Tips -Use CLI E2E tests or real Obsidian E2E scripts instead of `npm test` when the behaviour can be verified outside the browser harness. +Select the narrowest unit, integration, CLI E2E, or real Obsidian E2E command that owns the behaviour being changed. The obsolete mocked browser Harness has been retired. ### Unreleased change notes @@ -62,22 +60,23 @@ To facilitate development and testing, the build process can automatically copy ### Testing Infrastructure -- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~ - - This is now obsolete, migrated to vitest. - **Vitest**: - **Unit Tests** (`vitest.config.unit.ts`): Unit tests run in Node.js (excluding harnesses and integration tests). Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`). Executed via `npm run test:unit`. - **Integration Tests** (`vitest.config.integration.ts`): Tests run in Node.js against a real CouchDB instance. Integration tests should be `*.integration.spec.ts` or `*.integration.test.ts` and placed alongside the implementation file (e.g., `StreamingFetch.integration.spec.ts`). Executed via `npm run test:integration`. - - If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you strongly expected to write an integration test to verify the behaviour against a real CouchDB server. - - **Browser Harness Tests** (`vitest.config.ts`): Transitional browser-based harness tests using Playwright/Chromium. Executed via `npm run test`. This layer is no longer the preferred destination for new broad E2E coverage because `test/harness` can drift from real Obsidian behaviour. - - **P2P Tests** (`vitest.config.p2p.ts`): Browser-based Peer-to-Peer replication tests. Executed via `npm run test:p2p`. - - **RPC Unit Tests** (`vitest.config.rpc-unit.ts`): RPC-specific unit tests with coverage thresholds. + - If you add a feature that interacts with the remote database (e.g., replication changes, custom changes feed parameters, or custom HTTP queries), you are strongly expected to write an integration test to verify the behaviour against a real CouchDB server. + - **Commonlib Tests**: Commonlib owns unit and package tests for shared RPC, storage, replication, and platform contracts. LiveSync CI verifies the exact packed dependency as a downstream consumer. + +Regression tests remain beside the implementation which owns their contract. Prefix a case or group with `compatibility:` when it protects a persisted input or state which current releases still accept, and with `retirement guard:` when it prevents a removed setting, control, or notification from returning. Remove or replace a compatibility case only when the corresponding input is no longer accepted or an equivalent maintained case preserves the contract. Remove a retirement guard only when another current contract makes the old behaviour unreachable. Do not preserve a disconnected historical test as an executable specification when no maintained runner invokes it; Git history is the reference for retired test infrastructure. + +- **CLI E2E** (`src/apps/cli/testdeno/`): Host-independent consumer workflows. The canonical Compose P2P suite covers ordinary two-peer synchronisation, replacement of the current replicator followed by transfer with the same peer, and explicit relay disconnection followed by paused and resumed reconnection. Its lifecycle entry point is included only in the Docker test build and does not add a public CLI command. Run `npm run test:e2e:cli` for the ordinary suite or `npm run test:e2e:cli:p2p` for P2P validation. +- **Self-hosted setup tools** (`utils/couchdb/`, `utils/setup/`, and `utils/flyio/`): Deno contract tests consume the exact locked Commonlib registry package, verify current CouchDB, Object Storage, and random-room P2P Setup URI defaults and remote profiles, and keep CouchDB administration separate from package-owned LiveSync database-version negotiation. `unit-ci` also provisions a real temporary CouchDB database and verifies its version document against the installed Commonlib package. Run `npm run test:setup-tools` for the local contract gate. - **Real Obsidian E2E** (`test/e2e-obsidian/`): Local-first scripts that launch real Obsidian with temporary vaults and the built Self-hosted LiveSync plug-in. Use these for boot-up sequence, vault reflection, RedFlag flows, Fast Setup (Simple Fetch), settings dialogues, restart-sensitive workflows, Object Storage regressions, and other behaviour that depends on Obsidian itself. Run focused scripts such as `npm run test:e2e:obsidian:two-vault-sync`, or use `npm run test:e2e:obsidian:local-suite:services` to run the broader local suite with CouchDB and MinIO fixtures managed by the wrapper. -- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services: +- **Docker Services**: Service-backed tests use CouchDB and MinIO (S3). Canonical P2P validation owns its relay through the CLI Compose runner: ```bash npm run test:docker-all:start # Start all test services - npm run test:full # Run tests with coverage + npm run test:integration # Run the relevant service-backed suite npm run test:docker-all:stop # Stop services ``` @@ -86,35 +85,28 @@ To facilitate development and testing, the build process can automatically copy - **Test Structure**: - `test/e2e-obsidian/` - Real Obsidian E2E scripts for local verification - - `test/suite/` - Transitional browser harness tests for sync operations - - `test/unit/` - Unit tests (via vitest, as harness is browser-based) - - `test/harness/` - Transitional mock implementations (e.g., `obsidian-mock.ts`). Avoid adding broad new E2E coverage here unless it is explicitly a compatibility bridge. + - co-located `*.unit.spec.ts` files - Node-based unit tests + - co-located `*.integration.spec.ts` files - service-backed integration tests + - `src/apps/webapp/obsidianMock.ts` - Webapp-only Obsidian compatibility adapter; it is not an E2E Harness ### Import Path Normalisation -The codebase uses `@/` and `@lib/` path aliases to keep import structures clean. To normalise imports and exports across files, use the following utility script: +The codebase uses the `@/` alias for source owned by this repository. Commonlib imports use explicit `@vrtmrz/livesync-commonlib` package subpaths. To normalise LiveSync-owned imports and exports, use the following utility script: ```bash npm run pretty:importpath ``` -Under the hood, this runs Deno with the script [utilsdeno/normalise-imports.ts](file:///p:/plant25/obsidian/projects/obsidian-livesync/utilsdeno/normalise-imports.ts). You can pass additional flags to this script if required (by running it via Deno directly from the `utilsdeno` directory): +Under the hood, this runs Deno with the script [utilsdeno/normalise-imports.ts](utilsdeno/normalise-imports.ts). You can pass additional flags to this script if required (by running it via Deno directly from the `utilsdeno` directory): - `--run`: Applies the changes (the script runs in dry-run mode by default). - `--all-alias`: Normalises sibling/child relative imports starting with `./` to use aliases. -### Type Generation +### Commonlib dependency -To generate fallback type definitions for the shared library and add appropriate Deno ignore comments (which suppresses Deno compilation warnings and linting warnings inside the `_types` directory), run: +Shared synchronisation code is compiled and typed by the `@vrtmrz/livesync-commonlib` package. `npm ci` installs the exact artefact recorded by the lockfile; this repository does not compile Commonlib source or commit fallback declarations. -```bash -npm run build:lib:types -``` - -This script executes: - -1. TypeScript compilation (`tsconfig.types.json`) to output definitions to the `_types` directory. -2. The Deno script [utilsdeno/types-add-ignore.ts](file:///p:/plant25/obsidian/projects/obsidian-livesync/utilsdeno/types-add-ignore.ts) to prepend Deno ignore comments to the generated type files. +Changes spanning both repositories must first produce a packed Commonlib artefact which passes its standalone package checks. Install that exact artefact in LiveSync, then run the LiveSync type checks, unit tests, application builds, CLI E2E, and any focused real-Obsidian E2E required by the changed boundary. Replace the temporary artefact reference with the reviewed immutable package version before release. ## Architecture @@ -149,10 +141,12 @@ Hence, the new feature should be implemented as follows: ### Key Architectural Components -- **LiveSyncLocalDB** (`src/lib/src/pouchdb/`): Local PouchDB database wrapper -- **Replicators** (`src/lib/src/replication/`): CouchDB, Journal, and MinIO sync engines +- **LiveSyncLocalDB** (`@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB`): Local PouchDB database wrapper +- **Replicators** (`@vrtmrz/livesync-commonlib/compat/replication/*`): CouchDB, Journal, and P2P synchronisation engines - **Service Hub** (`src/modules/services/`): Central service registry using dependency injection -- **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools +- **Common Library** (`@vrtmrz/livesync-commonlib`): Platform-independent synchronisation logic, shared with the CLI, Webapp, WebPeer, and external tools + +Commonlib owns the P2P replicator and Trystero transport lifecycle. Host commands, event handlers, and views must retain the Commonlib service-feature result and resolve its current `replicator` at the point of use. They must not snapshot an instance which can be replaced when settings or the local database change, close Trystero-owned raw peers, or install another Trystero transport generation at the application root. ### Conflict Merge Policy @@ -160,6 +154,10 @@ Markdown conflict auto-merge should behave like a conservative three-way merge. When in doubt, prefer the safer outcome: preserve data, keep the conflict visible, and ask the user rather than silently discarding content or choosing one side. +The detailed contract is documented in [Conflict resolution and revision provenance](docs/specs_conflict_resolution.md). Determine the merge base by intersecting the exact `available` revision IDs from both leaf histories and selecting the nearest shared revision. Do not infer ancestry from revision generation numbers. When a remote resolution reaches a Vault which still contains the exact content of a deleted losing branch, treat that content as known synchronised history so the resolution can be reflected without recreating the conflict. + +File operations made while a conflict is active must use the device-local file-reflection provenance injected into `ServiceFileHandlerBase`. Treat its exact revision as authoritative; use byte equality only to reconstruct a missing record when exactly one available revision matches. If branch identity remains unknown, preserve data and leave the conflict visible. Do not hide key-value database readiness behind an implicit wait: maintained hosts open it through the sequential settings lifecycle before file events or replication begin. + - If one side deletes a line and the other side leaves that same line unchanged, treat it as a safe deletion. The deleted line must not be reintroduced into the merged result. - If one side inserts new content in a different region while the other side deletes an unchanged old region, preserve the insertion and the deletion. - If one side deletes a line and the other side modifies that same line, keep the conflict for user resolution. @@ -172,15 +170,15 @@ This policy is intentionally aligned with the conflict checkboxes and compatibil - **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild) - **Development code**: Use `.dev.ts` suffix (replaced with `.prod.ts` in production) -- **Path aliases**: `@/*` maps to `src/*`, `@lib/*` maps to `src/lib/src/*` +- **Path aliases**: `@/*` maps to `src/*`; Commonlib uses package exports rather than a source alias ## Code Conventions ### Internationalisation (i18n) - **Translation Workflow**: - 1. Edit YAML files in `src/lib/src/common/messagesYAML/` (human-editable) - 2. Run `npm run bakei18n` to compile: YAML → JSON → TypeScript constants + 1. Edit the human-readable YAML files in this repository under `src/common/messagesYAML/` + 2. Run `npm run i18n:bake` to compile YAML → JSON → TypeScript constants 3. Use `$t()`, `$msg()` functions for translations You can also use `$f` for formatted messages with Tagged Template Literals. - **Usage**: @@ -189,13 +187,15 @@ This policy is intentionally aligned with the conflict checkboxes and compatibil $t("Some message"); // Direct translation $f`Hello, ${userName}`; // Formatted message ``` -- **Supported languages**: `def` (English), `de`, `es`, `ja`, `ko`, `ru`, `zh`, `zh-tw` +- **Supported languages**: `def` (English), `de`, `es`, `fr`, `he`, `ja`, `ko`, `ru`, `zh`, `zh-tw` + +Commonlib owns the typed English fallback for messages requested by its services. LiveSync owns the multilingual application catalogue and injects its translator into the Obsidian, CLI, and browser service compositions. Adding a Commonlib message therefore requires its canonical English definition in Commonlib; LiveSync may provide translations here, while an untranslated key falls back to Commonlib English. Importing a Commonlib language catalogue is not part of the boundary. ### File Path Handling - Use tagged types from `types.ts`: `FilePath`, `FilePathWithPrefix`, `DocumentID` - Prefix constants: `CHeader` (chunks), `ICHeader`/`ICHeaderEnd` (internal data) -- Path utilities in `src/lib/src/string_and_binary/path.ts`: `addPrefix()`, `stripAllPrefixes()`, `shouldBeIgnored()` +- Path utilities are supplied by the focused Commonlib compatibility path `@vrtmrz/livesync-commonlib/compat/string_and_binary/path` ### Logging & Debugging @@ -224,8 +224,8 @@ export class ModuleExample extends AbstractObsidianModule { ### Settings Management -- Settings defined in `src/lib/src/common/types.ts` (`ObsidianLiveSyncSettings`) -- Configuration metadata in `src/lib/src/common/settingConstants.ts` +- Settings are defined by Commonlib (`ObsidianLiveSyncSettings`) +- Configuration metadata is supplied by the Commonlib settings exports - Use `this.services.setting.saveSettingData()` instead of using plugin methods directly ### Database Operations @@ -239,24 +239,15 @@ export class ModuleExample extends AbstractObsidianModule { - [esbuild.config.mjs](esbuild.config.mjs) - Build configuration with platform/dev file replacement - [package.json](package.json) - Scripts reference and dependencies -## Beta Policy +## Pre-release Policy -- Beta versions are denoted by appending `-patchedN` to the base version number. - - `The base version` mostly corresponds to the stable release version. - - e.g., v0.25.41-patched1 is equivalent to v0.25.42-beta1. - - This notation is due to SemVer incompatibility of Obsidian's plugin system. - - Hence, this release is `0.25.41-patched1`. -- Each beta version may include larger changes, but bug fixes will often not be included. - - I think that in most cases, bug fixes will cause the stable releases. - - They will not be released per branch or backported; they will simply be released. - - Bug fixes for previous versions will be applied to the latest beta version. - This means, if xx.yy.02-patched1 exists and there is a defect in xx.yy.01, a fix is applied to xx.yy.02-patched1 and yields xx.yy.02-patched2. - If the fix is required immediately, it is released as xx.yy.02 (with xx.yy.01-patched1). - - This procedure remains unchanged from the current one. -- At the very least, I am using the latest beta. - - However, I will not be using a beta continuously for a week after it has been released. It is probably closer to an RC in nature. - -In short, the situation remains unchanged for me, but it means you all become a little safer. Thank you for your understanding! +- Use SemVer beta identifiers such as `1.0.0-beta.0` for immutable integration previews. Increment the beta number when a published preview needs a correction. Reserve `1.0.0-rc.0` for the first feature- and contract-frozen release candidate. Historical `-patchedN` releases remain unchanged in the release history. +- Publish a pre-release from an immutable reviewed tag, mark its GitHub Release as a pre-release, and do not replace the latest stable release. +- A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag. +- Keep 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. +- If validation fails, leave every published tag unchanged and prepare the next pre-release or patch version. ## Release Notes @@ -272,43 +263,48 @@ In short, the situation remains unchanged for me, but it means you all become a This workflow is for maintainers. Contributors should update `## Unreleased` for user-facing feature or fix PRs, but do not need to run the release workflows. The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `release` GitHub Environment. Configure Environment protection in the repository settings so tag creation and release publication require maintainer approval. -- Run the `Prepare Release PR` workflow with the target version. It creates the release branch, updates versions, regenerates the `_types` fallback definitions used by the community plug-in scan, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. +- Run the `Prepare Release PR` workflow with the target version and selected base branch. It creates the release branch, updates versions, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. The base branch may already select the target development version; the workflow still runs the version lifecycle so that release-only metadata such as `versions.json` is recorded in the release commit. - Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`. -- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that both the plug-in tag (for example, `0.25.81`) and the CLI tag (for example, `0.25.81-cli`) point to that commit, and explicitly dispatches the plug-in and CLI publishing workflows. The workflow can be retried when existing tags already point to the reviewed commit, but stops if either tag points elsewhere. -- The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. The CLI Docker workflow retains its documented branch, tag, and manual triggers. -- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. Confirm that the CLI workflow has published the fixed version tag, the major-minor moving tag (for example, `0.25-cli`), `latest`, and the SHA-qualified tag. -- Publish the draft GitHub Release as the latest stable release while keeping the release PR in draft and leaving `main` unchanged. Record the state in the PR with: 'Release `` has been published as the latest stable release. This pull request intentionally remains in draft, and `main` has not yet been updated. Merge is on hold until BRAT validation is complete.' +- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, ensures that the plug-in tag points to that commit, optionally creates the corresponding CLI tag, and explicitly dispatches the selected plug-in and CLI release workflows. The finalisation workflow can be retried when existing tags already point to the reviewed commit, but stops if a selected tag points elsewhere. +- The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. When CLI publication is selected, finalisation dispatches the CLI Docker workflow against the reviewed CLI tag instead of relying on a tag created by `GITHUB_TOKEN` to start another workflow. +- For a hyphenated pre-release, run finalisation with `prerelease=true`; CLI publication remains optional. For a stable version awaiting BRAT validation, use `prerelease=true` and `publish_cli=false`. +- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. When a hyphenated pre-release includes the CLI, confirm that it received only its immutable version and SHA-qualified image tags. +- 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 BRAT validation succeeds, mark the release PR ready and merge it with a merge commit. This keeps the tagged release commit in the `main` history. -- If BRAT validation fails, keep the release PR in draft. Do not move published tags; prepare and publish a new patch release instead. -- If a pre-release is needed, run the `Release Obsidian Plugin` workflow manually with the target tag, `draft=false`, and `prerelease=true`. +- 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. +- 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 :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`. ### Release Cheat Sheet 1. Before starting, add user-facing notes under `## Unreleased` in `updates.md`. 2. Run `Prepare Release PR` from GitHub Actions. - `version`: the target version, for example `0.25.81`. - - `base_branch`: normally `main`. + - `base_branch`: normally `main`, or the reviewed integration branch for an integration preview. - `release_branch`: leave blank to use the default branch name, for example `0_25_81`. - `release_date`: use an ordinal date such as `14th July, 2026`, or leave blank to use the current UTC date. - `allow_empty_updates`: leave disabled unless the release intentionally has no user-facing notes. 3. Review the generated draft PR. - Polish `updates.md`. - - Confirm `package.json`, `manifest.json`, `versions.json`, workspace package versions, and the generated `_types` definitions. + - Confirm `package.json`, `manifest.json`, `versions.json`, workspace package versions, and the locked Commonlib package version. - Confirm that `manifest.json` has the intended `minAppVersion`. - Wait for the necessary CI checks. 4. When the PR head is fixed, run `Finalise Release Tags`. - `version`: the same target version. - `release_branch`: leave blank unless the release branch used a custom name. - `expected_head_sha`: the full head commit SHA reviewed in the release PR. + - `prerelease`: enable for a version such as `1.0.0-rc.0`, and also when staging a stable version for BRAT. + - `publish_cli`: optional for a hyphenated pre-release, but disable it when staging a stable version. 5. Approve the `Release Obsidian Plugin` workflow for the `release` environment, then check the generated draft GitHub Release. -6. Confirm that the explicitly dispatched CLI Docker workflow has published the expected image tags. -7. Publish the draft GitHub Release as the latest stable release, but keep the release PR in draft and leave `main` unchanged. -8. Update the PR state message to say that the release is now the latest stable release and that merging is intentionally on hold until BRAT validation is complete. +6. If a hyphenated pre-release includes the CLI, confirm that the explicitly dispatched CLI workflow published only immutable version and SHA-qualified image tags. +7. Publish the draft as a GitHub pre-release without replacing the latest stable release, but keep the release PR in draft and leave its base branch unchanged. +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 BRAT validation succeeds, mark the release PR ready and merge it into `main` with a merge commit. -11. If validation fails, leave the PR in draft and prepare a new patch release without moving the published tags. -12. If the release should be a pre-release instead of a draft release, run `Release Obsidian Plugin` manually with the target `tag`, `draft=false`, and `prerelease=true`. +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. +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 diff --git a/docs/adding_translations.md b/docs/adding_translations.md index 4aa595e2..78732358 100644 --- a/docs/adding_translations.md +++ b/docs/adding_translations.md @@ -1,34 +1,49 @@ # How to add translations +Self-hosted LiveSync owns its multilingual catalogue. Commonlib supplies only the typed English messages which its services request; the plug-in combines those keys with its application messages and injects the selected translator at each service composition root. + ## Getting ready -1. Clone this repository recursively. -```sh -git clone --recursive https://github.com/vrtmrz/obsidian-livesync -``` -2. Make `ls-debug` folder under your vault's `.obsidian` folder (as like `.../dev/.obsidian/ls-debug`). +1. Clone this repository. -## Add translations for already defined terms + ```sh + git clone https://github.com/vrtmrz/obsidian-livesync + cd obsidian-livesync + npm ci + ``` -1. Install dependencies, and build the plug-in as dev. build. -```sh -cd obsidian-livesync -npm i -D -npm run buildDev -``` +2. Create an `ls-debug` directory below the test Vault's `.obsidian` directory, for example `.obsidian/ls-debug`. -2. Copy the `main.js` to `.obsidian/plugins/obsidian-livesync` folder of your vault, and run Obsidian-Self-hosted LiveSync. -3. You will get the `missing-translation-yyyy-mm-dd.jsonl`, please fill in new translations. -4. Build the plug-in again, and confirm that displayed things were expected. -5. Merge them into `rosetta.ts`, and make the PR to `https://github.com/vrtmrz/livesync-commonlib`. +## Add translations for existing messages -## Make messages to be translated +1. Edit the human-readable YAML files under `src/common/messagesYAML/`. +2. Regenerate the JSON and TypeScript resources. -1. Find the message that you want to be translated. -2. Change the literal to use `$tf`, like below. -```diff -- Logger("Could not determine passphrase to save data.json! You probably make the configuration sure again!", LOG_LEVEL_URGENT); -+ Logger($tf('someKeyForPassphraseError'), LOG_LEVEL_URGENT); -``` -3. Make the PR to `https://github.com/vrtmrz/obsidian-livesync`. -4. Follow the steps of "Add translations for already defined terms" to add the translations. \ No newline at end of file + ```sh + npm run i18n:bake + ``` + +3. Build the plug-in in development mode, install it in the test Vault, and run Self-hosted LiveSync. +4. Review any `missing-translation-yyyy-mm-dd.jsonl` file written below `.obsidian/ls-debug`, and add the required translations to the YAML catalogue. +5. Bake and build again, then confirm the displayed text and placeholder substitution in the relevant workflow. + +Commit the edited YAML and all regenerated JSON and TypeScript resources together. + +## Make a message translatable + +LiveSync-owned messages may first be added to +`src/common/messages/LiveSyncProvisionalMessages.ts` while their wording is being +exercised. This keeps an application-only message out of Commonlib and provides a +typed English fallback without requiring contributors to update every language. + +When the wording is ready for translation: + +1. Move its canonical English entry from + `src/common/messages/LiveSyncProvisionalMessages.ts` to + `src/common/messagesYAML/en.yaml`. Remove the provisional entry in the same + change. Translations in the other LiveSync YAML files may follow as contributor + updates. +2. Replace the source literal with `$msg()` or another existing translation helper, using the English catalogue key as the typed contract. +3. Run `npm run i18n:bake`, build the plug-in, and verify the affected workflow. + +When a new message belongs to Commonlib rather than the application, add its canonical English definition and key type in Commonlib first. Add any available translations to LiveSync when consuming that package; untranslated languages use Commonlib's canonical English fallback. diff --git a/docs/adr/2026_06_real_obsidian_e2e.md b/docs/adr/2026_06_real_obsidian_e2e.md index 863b7568..cade7701 100644 --- a/docs/adr/2026_06_real_obsidian_e2e.md +++ b/docs/adr/2026_06_real_obsidian_e2e.md @@ -2,15 +2,15 @@ ## Status -Proposed / Spike Implemented +Accepted / Implemented ## Release -Not yet. Planned after the serviceFeature refactoring branch is reviewed. +Accepted — implemented as the maintained local E2E layer for the 1.0 release preparation. ## Context -The current end-to-end tests run through Vitest browser mode and a mocked Obsidian environment in `test/harness`. This has been useful for exercising synchronisation flows without launching Obsidian, but it is no longer a reliable final signal for plug-in behaviour. +When this decision was proposed, the end-to-end tests ran through Vitest browser mode and a mocked Obsidian environment in `test/harness`. This was useful for exercising synchronisation flows without launching Obsidian, but it was no longer a reliable final signal for plug-in behaviour. The main issues are: @@ -31,7 +31,7 @@ The long-term test pyramid should be: 2. Integration tests for CouchDB, Object Storage, P2P services, database operations, and replication protocols. 3. Real Obsidian E2E tests for boot-up sequence, vault reflection, command registration, settings dialogues, restart scheduling, and user-visible workflows. -The existing `test/harness` should be demoted to a transitional compatibility layer. It may remain temporarily while the real Obsidian runner reaches parity for critical flows, but new high-level E2E coverage should target the real runner. +Retain the existing browser Harness only as a transitional compatibility layer while the real Obsidian runner reaches parity for critical flows, then remove it. New high-level E2E coverage targets the real runner. ## Non-Goals @@ -143,19 +143,19 @@ Current verification: - `npm run test:e2e:obsidian:couchdb-upload` configures a unique CouchDB database, creates a note through Obsidian, commits it into the local database, runs one-shot synchronisation, and verifies that CouchDB contains the metadata document and all referenced chunk documents. - `npm run test:e2e:obsidian:minio-upload` configures a unique Object Storage bucket prefix, creates a note through Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written to the S3-compatible bucket. - `npm run test:e2e:obsidian:startup-scan` verifies that a file written while Obsidian is stopped is picked up during the next real Obsidian boot and uploaded to CouchDB after one-shot synchronisation. -- `npm run test:e2e:obsidian:two-vault-sync` verifies two-vault note synchronisation: creation, update, rename, deletion, per-device target-filter differences, and a separate encrypted round-trip with Path Obfuscation enabled. The experimental Markdown conflict automatic merge check is available with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true` but is not part of the default local suite. +- `npm run test:e2e:obsidian:two-vault-sync` verifies two-vault note synchronisation: creation, update, rename, deletion, per-device target-filter differences, and a separate encrypted round-trip with Path Obfuscation enabled. The optional Markdown conflict check creates divergent branches in two real Vaults, conservatively merges them, edits the merged result again, and verifies that the Vault holding the deleted losing revision accepts the propagated result without recreating a conflict. Enable it with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`. A separate `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` scope keeps four conflicts active while a Vault edits, deletes, case-renames, and cross-path-renames files, then verifies exact parent revisions and replicated trees. Neither scope is part of the default local suite. - `npm run test:e2e:obsidian:hidden-file-snippet-sync` verifies hidden file synchronisation as a two-vault round-trip: creation, deletion, automatic JSON conflict merging with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target-pattern differences. - `npm run test:e2e:obsidian:customisation-sync` verifies a two-vault Customisation Sync workflow: scan a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronise them through CouchDB, apply them on the second vault, assert the resulting `.obsidian` files, propagate a snippet update, and verify deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy. - `npm run test:e2e:obsidian:setting-markdown-export` verifies that setting Markdown export creates a vault file and omits credentials when credential export is disabled. - `npm run test:e2e:obsidian:install-appimage` reuses the existing AppImage and extracted binary when they are already present. -- `npm run test:e2e:obsidian:local-suite` runs the local verification sequence for the real Obsidian runner after CouchDB and MinIO have been started. -- `npm run test:e2e:obsidian:local-suite:services` stops leftover CouchDB and MinIO fixtures, starts fresh fixtures, runs the local suite, and stops the fixtures again. -- `npm run test:e2e:obsidian:local-suite:services` has been verified locally with real Obsidian, CouchDB, and MinIO. The run completed discovery, smoke, vault reflection, CouchDB upload, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export. The build step still emits existing Svelte warnings. +- `npm run test:e2e:obsidian:local-suite` runs the local verification sequence for the real Obsidian runner after CouchDB, MinIO, and the P2P relay have been started. +- `npm run test:e2e:obsidian:local-suite:services` stops leftover CouchDB, MinIO, and P2P relay fixtures, starts fresh fixtures, runs the local suite, and stops the fixtures again. +- `npm run test:e2e:obsidian:local-suite:services` has been verified locally with real Obsidian and all three fixtures. In addition to the core launch and synchronisation scenarios, the maintained suite covers CouchDB, Object Storage, and P2P Setup URI workflows in which the working first device generates the URI imported by the second device. Known limits: - The smoke runner currently proves only one-vault launch and plug-in load readiness. Broader workflows are covered by separate real Obsidian scripts, including CouchDB upload, startup scan, two-vault note synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export. -- Cross-platform support is still discovery-level. The working path has been validated on Linux ARM64. macOS and Windows should be validated in their own environments as follow-up work. +- The working path has been validated on Linux ARM64 and on macOS with Obsidian 1.12.7. Windows remains unverified. - CI wiring is intentionally not implemented. The runner depends on a licensed desktop application and is treated as a local verification tool. ### Phase 2: First Real Workflow @@ -186,7 +186,7 @@ Current implementation status: Current implementation status: -- `test:e2e:obsidian:two-vault-sync` covers creation, update, rename, deletion, and per-device target-filter behaviour for a non-encrypted CouchDB configuration. Markdown conflict automatic merging remains an optional check because it needs a dedicated, less timing-sensitive fixture. +- `test:e2e:obsidian:two-vault-sync` covers creation, update, rename, deletion, and per-device target-filter behaviour for a non-encrypted CouchDB configuration. Its optional conflict fixtures cover real two-Vault divergence, conservative merge, post-resolution editing, propagation to a Vault whose file still contains the deleted losing revision, and edit, logical deletion, case-only rename, and cross-path rename while conflicts remain active. - The same script creates a separate temporary CouchDB database and temporary vault pair for an encrypted two-vault round-trip with Path Obfuscation enabled. ### Phase 4: Harness Retirement @@ -199,17 +199,18 @@ Current implementation status: Current implementation status: -- `test/harness` is now documented as a transitional compatibility layer. -- New broad E2E work should target `test/e2e-obsidian/` when real Obsidian behaviour is the risk being tested. -- The next high-value scenarios are RedFlag variants and Fast Setup (Simple Fetch) variants, not a line-by-line migration of `test/suite`. +- The mocked Vitest browser suites, their P2P runner, their root-level relay helpers, and the manual `harness-ci` workflow have been removed after maintained suites covered the critical flows. +- Headless CouchDB and Object Storage combinations, with and without encryption, remain owned by the CLI two-Vault matrix. P2P transport replacement and relay lifecycle remain owned by the CLI Compose E2E suite. Real Obsidian owns the visible CouchDB, Object Storage, and P2P Setup URI workflows, including URI generation on the first device, import on the second device, and two-way Vault synchronisation. +- The Obsidian compatibility implementation still needed by the Webapp has moved to `src/apps/webapp/obsidianMock.ts`; it is not a retained browser E2E Harness. +- Remaining high-value scenarios, including RedFlag and Fast Setup (Simple Fetch) variants, should be added according to their owning integration boundary rather than copied line by line from the retired suite. ## Local Verification Strategy Real Obsidian E2E is a local verification layer. It should not be wired into the default CI gate. - Keep the scripts individually runnable for focused local debugging. -- Provide `test:e2e:obsidian:local-suite` for a broader local pass after the CouchDB and MinIO fixtures have been started. -- Provide `test:e2e:obsidian:local-suite:services` for a broader local pass that manages the CouchDB and MinIO fixtures itself. +- Provide `test:e2e:obsidian:local-suite` for a broader local pass after the CouchDB, MinIO, and P2P relay fixtures have been started. +- Provide `test:e2e:obsidian:local-suite:services` for a broader local pass that manages all three fixtures itself. - Use `OBSIDIAN_BINARY` when testing against an installed desktop application. - Use `test:e2e:obsidian:install-appimage` on Linux when a local AppImage copy is needed, and reuse the extracted `_testdata/obsidian/squashfs-root` directory between local runs. - Capture Obsidian logs, plug-in logs, vault snapshots, and service logs manually when investigating failures. @@ -240,12 +241,12 @@ Real Obsidian E2E is a local verification layer. It should not be wired into the 4. Add `test:e2e:obsidian:smoke` for one-vault plug-in load. 5. Document required local environment variables, especially `OBSIDIAN_BINARY`. 6. Port one CouchDB-backed workflow after the smoke test is stable. -7. Mark `test/harness` as transitional and block new broad E2E work from targeting it. +7. Retire the browser Harness after critical workflows have replacement coverage. 8. Add the local suite script for broader local verification. ## Consequences - Real Obsidian E2E becomes the source of truth for plug-in lifecycle and vault integration. - Unit and integration tests remain the primary fast feedback loops. -- The old browser harness can be deleted once the new runner covers the critical workflows. +- The old browser Harness has been deleted now that the replacement coverage owns its critical workflows. - The project will gain slower but higher-confidence tests for the behaviours most likely to differ between mocks and Obsidian itself. diff --git a/docs/adr/2026_07_bounded_remote_activity.md b/docs/adr/2026_07_bounded_remote_activity.md index ed328860..3f8498d3 100644 --- a/docs/adr/2026_07_bounded_remote_activity.md +++ b/docs/adr/2026_07_bounded_remote_activity.md @@ -37,6 +37,10 @@ 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. + +This separation also keeps the activity indicators truthful: `📲` describes a finite remote operation and must not remain active solely because local Vault writes are pending. The existing replication-result count continues to describe that local queue. If a future feature offers screen-awake protection while applying downloaded documents, it must use a separately typed local-application activity or power-policy boundary, preserve the current behaviour by default, and avoid incrementing either remote-activity count. + Manual P2P commands which bypass `ReplicationService` enter the broad boundary. Direct P2P pull and push entry points are therefore both protected as finite remote work, covering the Obsidian panes, CLI, and Webapp. A pull or bidirectional synchronisation also enters the narrower finite-replication boundary because it can place documents in the local database. A push-only request remains broad-only: it cannot satisfy a local missing-chunk read and must not present itself as a delivery source. Automatic synchronisation on peer discovery, a pull requested by a remote peer, and a watched pull following a peer progress notification enter both boundaries because each can deliver local documents. A normal P2P peer-selection dialogue represents one broad finite session: it remains inside the boundary while waiting for a peer and while the person may perform repeated synchronisations, then settles when the dialogue closes and any in-flight synchronisation has finished. Closing without synchronising returns a failed result and releases the boundary. The 'Start Sync & Close' action completes its synchronisation before closing. This deliberately protects peer discovery and selection, because display sleep can interrupt discovery or connection establishment and require the person to start detection again. It may therefore retain a Wake Lock longer than the network transfer alone. A transfer performed inside that session temporarily adds a nested activity; the count remains a logical-operation count rather than a connection total. `ChunkFetcher` enters the broad boundary synchronously when it accepts newly missing chunk identifiers, but it does not increment the finite-replication count. A typed per-identifier claim keeps the broad boundary active through queue waiting, interval throttling, `fetchRemoteChunks`, validation, local persistence, and terminal event delivery. Duplicate requests share the existing claim. Explicit absence, failure, cancellation, or a conservative five-minute period without fetcher-observable progress settles the affected claim. The five-minute value is only a last-resort leak fuse: it prevents a never-settling integration from retaining the per-identifier claim and waiter indefinitely and, once the activity runner has entered the claim task, lets the associated Wake Lock, lifecycle deferral, and indicator finish. It is not an arrival estimate, proof of remote absence, or a transport deadline. This scope is defined in detail by the chunk-arrival-quiescence ADR. @@ -81,10 +85,12 @@ 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 local storage reflection in this boundary. It is re-entrant and offline-capable, and needs a separate decision if activity reporting or power policy is added later. +- Do not include offline scans, unrelated local storage reflection, or the durable replication-result queue in this boundary. They are offline-capable and require a separate decision if activity reporting or power policy is added later. ## Verification +Before changing the transfer/application separation, add a deterministic regression scenario which leaves downloaded documents queued after finite transfer settles, verifies that remote activity has ended, persists the queued state, and resumes Vault application after suspension or restart. An optional local-application Wake Lock feature requires its own enabled and disabled cases; existing remote-operation E2E is not evidence for that separate policy. + Unit tests cover: - overlapping bounded activities and their reactive count transitions; @@ -119,4 +125,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. - Users can now distinguish the lifetime of a finite remote operation from approximate request activity within it. diff --git a/docs/adr/2026_07_common_library_package_boundary.md b/docs/adr/2026_07_common_library_package_boundary.md new file mode 100644 index 00000000..43be51da --- /dev/null +++ b/docs/adr/2026_07_common_library_package_boundary.md @@ -0,0 +1,347 @@ +# Architectural Decision Record: Package the Common Library Behind Explicit Host Boundaries + +## Status + +Accepted — the package boundary is implemented, published pre-release artefacts have been verified as exact downstream dependencies, and the Community directory scanner preview confirms the intended source boundary. Each later release candidate still requires immutable artefact and downstream validation. + +## Context + +Self-hosted LiveSync currently consumes `livesync-commonlib` as the `src/lib` Git submodule. TypeScript, Vite, Vitest, the CLI, Webapp, and WebPeer resolve `@lib/*` directly to the submodule's TypeScript source. The common-library repository has no package manifest, standalone build, export map, or self-contained test command. Its tests and dependency versions are consequently supplied by the Self-hosted LiveSync repository. + +Source archives do not populate a Git submodule. Self-hosted LiveSync therefore also commits generated declarations under `_types` and resolves `@lib/*` to those declarations as a fallback. This makes one logical dependency appear in the plug-in repository twice: once as a submodule checkout and once as generated declarations. Release preparation must regenerate and commit the fallback, and repository scanners can report generated lint directives as if they were maintained plug-in source. + +The Obsidian community scanner currently inspects repository source beyond the plug-in entry point. Reports include `src/apps/cli`, `src/apps/webapp`, `src/apps/webpeer`, and generated `_types`. Moving the common library from `src/lib` to another source directory or a workspace package inside the Self-hosted LiveSync repository would therefore preserve the scan surface. It could remove the submodule fallback, but it would not create the same dependency boundary as consuming a published npm package. + +The common library is already a substantial shared domain layer. At the time of this decision it contains 270 non-test TypeScript or Svelte source files. At Self-hosted LiveSync revision `e114f66fb2b7c6f3fec2d53701f6638d2557e606`, source outside `src/lib` uses 180 distinct raw `@lib/*` specifiers across plug-in, application, and test source. Normalising optional TypeScript source suffixes leaves 140 migration paths. The plug-in contributes most of those imports, but the CLI, Webapp, and WebPeer also depend on the library. Treating every existing deep path as a permanent public API would make future refactoring impractical. + +The intended external use is not new: + +- [commonlib issue #1](https://github.com/vrtmrz/livesync-commonlib/issues/1) has requested an npm package and an established API since 2022; +- [Self-hosted LiveSync issue #87](https://github.com/vrtmrz/obsidian-livesync/issues/87) asks for a client with list, get, submit, and delete operations; +- [commonlib issue #10](https://github.com/vrtmrz/livesync-commonlib/issues/10) asks for an API which lets another application add Markdown files to the CouchDB data model; and +- [commonlib issue #13](https://github.com/vrtmrz/livesync-commonlib/issues/13) records the integration cost of consuming a Git submodule and compiling its source. + +Existing external consumers use custom TypeScript aliases, loaders, and host stubs to reach `DirectFileManipulator` and other deep modules. The present source shape therefore imposes real integration work without supplying a stable contract. + +The library is close to being self-contained, but it crosses its boundary in a small number of important places: + +- four files depend on Self-hosted LiveSync's event hub or event identifiers; +- `KeyValueDBService` imports the host's concrete database-opening function; +- `ObsidianServiceContext` imports the plug-in class and Obsidian types from the parent repository; and +- `coreEnvFunctions` imports an Obsidian function type even though the module is intended to be host-neutral. + +The library also mixes domain logic with host presentation: + +- Svelte dialogue mounting depends on Svelte, LiveSync service context, application-lifecycle events, translation, and cancellation policy in one implementation; +- Obsidian context and setup helpers remain below the common-library directory; +- browser dialogue shims and LiveSync Svelte components are shipped alongside headless replication code; and +- the translation implementation statically imports the complete generated catalogue. The generated message modules and JSON catalogues account for approximately 1.5 MB of source and generated data, even when a headless consumer does not require them. + +The root `src/index.ts` currently exports only `DirectFileManipulator` and its options. `DirectFileManipulator` is useful evidence for a future SDK, but it is not yet a sufficient stable façade: initialisation starts from its constructor, enumeration remains unfinished, watch ownership and failure semantics are not documented, and conflict and concurrency semantics are not defined as a public guarantee. + +The present process model also assumes one main LiveSync instance. Event dispatch, translation state, environment configuration, offline-scan state, worker pools, synchronisation-parameter handlers, diagnostic counters, and some compatibility caches are module-scoped. Some of these are safe process-wide facilities, while others can allow two client instances to influence each other. Publishing a package makes concurrent clients in one process a supported possibility, so this state must be classified rather than carried across accidentally. + +## Decision + +Maintain `livesync-commonlib` as the authoritative independent repository and publish its compiled output as the scoped, pre-1.0 npm package `@vrtmrz/livesync-commonlib`. + +Self-hosted LiveSync, its CLI, Webapp, WebPeer, and external tools will consume the compiled package and declarations through package export maps. They must not compile the common library's source through a path alias. The package lock records the exact resolved artefact used to build a plug-in release. + +The common-library repository may become a small workspace if independently useful artefacts emerge, but it does not move into the Fancy Kit repository or the Self-hosted LiveSync plug-in repository. This preserves its domain ownership, existing history, issues, and independent release cadence while making the plug-in repository a package consumer. + +The first package release is an infrastructure and compatibility release, not a declaration that every internal service is stable. It exposes: + +- a small documented root API; +- named, task-oriented public subpaths; +- explicitly marked compatibility subpaths required to migrate current Self-hosted LiveSync imports; and +- no unrestricted source-directory wildcard as a permanent contract. + +Compatibility subpaths may be exported during the migration, but they remain pre-1.0 and are documented as internal. New external integrations should use the high-level client façade rather than reproduce Self-hosted LiveSync's internal Service Hub composition. + +### Domain ownership + +The common library continues to own behaviour which defines the Self-hosted LiveSync data and synchronisation model: + +- document, metadata, chunk, setting, and protocol types; +- path and identifier encoding; +- chunk splitting, hashing, compression, encryption, and content reconstruction; +- PouchDB-facing data access and replication primitives; +- CouchDB, Object Storage, and P2P replication domain logic; +- conflict, chunk-delivery, storage-event, and replication managers; +- platform-neutral storage and service contracts; +- headless composition; and +- the future high-level client façade. + +The Self-hosted LiveSync repository owns plug-in and product integration: + +- `ObsidianServiceContext` and every reference to the plug-in class, `App`, `Plugin`, or Obsidian lifecycle; +- Setup Wizard components and LiveSync-specific presentation policy; +- Obsidian menus, notices, settings panes, and dialogue composition; +- plug-in event wiring which is not part of the replication protocol; and +- the concrete initialisation of injected environment, translation, storage, key-value database, and UI capabilities. + +Browser-only implementations may remain in the common-library repository only when they implement a documented host-neutral contract used by more than one browser consumer. LiveSync-specific Webapp and WebPeer composition remains application code even if it later moves out of the plug-in repository. + +Neutral utilities which do not express LiveSync data, storage, replication, or UI policy may move to `octagonal-wheels` after more than one consumer proves the abstraction. Reusable Obsidian adapters may move to `@vrtmrz/obsidian-plugin-kit`. Neither package becomes an owner of LiveSync domain behaviour. + +### Dependency inversion + +The package must contain no `@/` import and no direct Obsidian import. The current reverse dependencies are removed as follows: + +- common protocol and service events live with their common-library contracts; +- host-only event reactions are registered by Self-hosted LiveSync at its composition root; +- `KeyValueDBService` receives an `openKeyValueDatabase` factory through constructor or service dependencies; +- `ObsidianServiceContext` moves to Self-hosted LiveSync; +- the language getter uses a local function type rather than importing the Obsidian declaration; and +- browser globals, fetch, crypto, timers, storage, and document access are obtained through explicit host capabilities where behaviour varies by runtime. + +Temporary package-level configuration is acceptable where converting every call site in one change would be unsafe, but configuration must be instance-scoped wherever multiple clients can coexist. Importing the package must not patch `HTMLElement`, `SVGElement`, or another global prototype. Webapp compatibility patches belong in Webapp bootstrap code. + +Every mutable module-level value is classified as one of: + +- immutable or safely shared process infrastructure; +- an explicitly keyed cache with bounded ownership and disposal; or +- client state which moves behind an instance-owned service or dependency. + +In particular, event subscriptions, translation selection, offline-scan maps and timers, synchronisation-parameter handlers, and database transformation policy must not leak between independent client instances. A high-level client owns an explicit asynchronous disposal path which removes subscriptions, cancels work it owns, and releases its instance state. + +### Translation resources + +Translation lookup is separated from the application catalogue. Commonlib owns a typed canonical English definition for the messages requested by its services and uses it when a host omits the optional translator capability. This ensures that another consumer receives meaningful English rather than symbolic keys. + +Self-hosted LiveSync owns the complete multilingual catalogue, generation tools, selected-language state, and translator implementation. It injects that translator into the Obsidian, CLI, and browser composition roots. Commonlib does not publish `compat/common/i18n`, `compat/common/rosetta`, generated language modules, or JSON catalogues. + +The canonical Commonlib key type and English fallback change with Commonlib. LiveSync may add translations for those keys without duplicating every English definition; its translator delegates absent keys to Commonlib's canonical English fallback. A separate language package remains possible only if independent consumers and release cadence later justify it; core must never depend on an application catalogue. + +LiveSync-owned wording may remain in a typed, application-local provisional English map while it is being exercised. The LiveSync translator composes those keys with the generated application catalogue and the Commonlib key type. Moving a stable message into LiveSync's YAML catalogue makes it available for translation without changing Commonlib. + +### Svelte dialogue hosting + +The present Svelte dialogue implementation is split into three responsibilities: + +1. an Obsidian `Modal` host which owns content, title, close, one-shot settlement, and disposal; +2. a Svelte adapter which mounts and unmounts a component in that host; and +3. Self-hosted LiveSync policy which supplies service context, reacts to plug-in unload, requires explicit cancellation in selected workflows, translates messages, and styles Setup Wizard content. + +Fancy Kit is an appropriate owner for the first responsibility when the contract is useful beyond this migration. The Kit API should be framework-neutral: it accepts a mount callback, exposes typed `resolve`, `cancel`, `close`, and `setTitle` controls, and accepts a disposer returned by the callback. It returns a `Promise` which settles once, and it owns safe-area, viewport, focus, and Obsidian Modal lifecycle guarantees. + +Fancy Kit does not add Svelte to its core dependencies for this migration. Self-hosted LiveSync initially owns the small Svelte adapter which calls Svelte's `mount` and `unmount` through the framework-neutral host. The browser dialogue host also remains outside the Obsidian plug-in kit. If a second Obsidian plug-in needs the same Svelte adapter, it can be promoted to an optional Kit subpath with an optional Svelte peer dependency, or to a separate `@vrtmrz/obsidian-svelte-kit` package. That promotion must preserve the existing `sideEffects: false` and focused-import guarantees for consumers which do not use Svelte. + +Self-hosted LiveSync's `openWithExplicitCancel` retry policy, application service context, and Setup Wizard components do not move to Fancy Kit. + +Arbitrary component mounting is not added to the neutral `UiInteractions` contract. A component instance is a framework and host integration detail rather than a portable interaction request which a generic driver can serialise or answer. A LiveSync workflow which needs a hosted component defines and receives its own narrow, typed dialogue capability; Self-hosted LiveSync composes that capability from the Kit Modal host and its local Svelte adapter. + +### Package artefacts + +The common-library package publishes compiled ESM and generated declaration files. Consumers do not receive raw TypeScript as the runtime entry point. The package must provide: + +- an explicit `exports` map; +- declaration maps where they remain useful for debugging; +- browser and Node entry points where implementations differ; +- accurate `sideEffects` metadata; +- explicit runtime, peer, and optional dependencies; +- no unresolved `@lib/*`, `@/*`, Vite query, or source `.ts` import in published JavaScript or declarations; +- an `npm pack` contents check; and +- clean-install consumer fixtures for Node, a browser bundle, and Self-hosted LiveSync. + +Svelte source, worker query imports, AWS SDK adapters, PouchDB adapters, and Node-only crypto must not leak through the root entry point. Optional feature subpaths may carry their own heavier dependency surface. + +### Platform host entries + +Platform-specific host access is explicit rather than inferred. `@vrtmrz/livesync-commonlib/node` supplies Node-only capabilities and `createNodeStorage({ rootPath })`. `@vrtmrz/livesync-commonlib/browser` supplies `createFileSystemAccessStorage({ rootHandle })` for the browser File System Access API. Browser bundles cannot resolve the Node implementation through the browser entry. + +Both storage factories receive an existing root from their host. Commonlib does not choose a process directory, present a browser directory picker, request browser permission, or persist a `FileSystemDirectoryHandle`. The CLI owns path selection and configuration. The Webapp owns user activation, permission, handle persistence, and re-authorisation. The adapters own only path containment and storage operations below the injected root. + +Here, the browser capability means the browser File System Access API, not Node's `fs` API. The package proof deliberately moves the rooted `IStorageAdapter` implementation first. The Webapp retains its LiveSync-specific `IFileSystemAdapter` composition, file-object cache, Vault semantics, picker flow, and storage-event policy. Those responsibilities can move later only with their own documented contracts; they are not implied by the low-level browser entry point. + +A later composition may place the constructed storage contract on a host-specific `ServiceContext` subtype, so services depend only on the injected capability while each platform owns context initialisation. This proof does not add storage or raw platform objects to the base `ServiceContext`: doing so would make an optional application capability mandatory for Obsidian, CLI, Webapp, WebPeer, and test contexts at the same time. A future change should inject the narrow `IStorageAdapter` contract rather than expose `FileSystemDirectoryHandle`, Node `fs`, or environment detection through the shared context. + +The paired adapters run against the same contract suite for metadata, text and binary access, append, listing, removal, missing paths, parent creation, empty-root handling, and traversal rejection. Timestamp fidelity remains platform-specific because the File System Access API does not provide the same creation-time and timestamp-setting facilities as Node. + +The Node adapter rejects symbolic-link components which it observes in adapter paths before an operation and opens file entries without following the final link where the platform supports that flag. The CLI delegates its Vault reads, writes, discovery, deletion, and atomic rename to this rooted adapter rather than maintaining a second direct `fs` path. This rejects traversal paths and symbolic links which already exist when a remote-derived operation begins, while retaining case-only and cross-directory rename behaviour. + +This adapter is not an operating-system filesystem sandbox. The CLI assumes that the host-selected Vault root and its local filesystem are trusted and are not concurrently rewritten by an untrusted local process. A privileged CLI must therefore not use a Vault root which is writable by a less-trusted local actor. + +The Node entry also centralises direct Node built-in access needed by trusted headless application code. This is a package and scanner boundary, not an assertion that Node and browser APIs are interchangeable. Cross-platform behaviour belongs in a shared contract with separate implementations, as demonstrated by rooted storage. + +### Standard input and output + +Commonlib's `context` entry exposes a narrow `StandardIo` contract for command-line composition. It reads UTF-8 standard input, asks one line-oriented question, and writes text or binary chunks to standard output and standard error without adding delimiters. Commonlib's `node` entry supplies `createNodeStandardIo()`, which binds that contract to host-selected Node streams and defaults to the current process streams. + +Self-hosted LiveSync constructs the Node implementation at the CLI composition root and places the exact instance on `NodeServiceContext`. `NodeServiceHub` must receive that Context rather than silently constructing one. Command handlers reach input, prompts, and protocol output through the Context, so unit tests can inject memory I/O without replacing process globals. Obsidian and browser Contexts do not acquire a fictitious terminal capability, and the base `ServiceContextContract` remains limited to capabilities required by every composition. + +Standard I/O does not own command-line arguments, exit codes, signals, stream lifecycle, log levels, or log persistence. Diagnostic logging remains the responsibility of the existing API and Logger composition. CLI adapters receive a narrow diagnostic callback wired to the service logging API instead of calling `console` directly. A CLI host may render selected logs on standard error, but Logger is not added to `StandardIo` or made mandatory on the base Context. + +Commonlib verifies memory injection, split UTF-8 decoding, object-mode rejection, text and binary output, and prompting against injected Node streams. The downstream CLI verifies Context identity and exact injected I/O in unit tests, then runs the built Node artefact through its Deno command E2E. + +### Context and result compatibility + +`ServiceContextContract` is the minimum host-neutral composition contract. It supplies an event channel and message translator selected by the host. Obsidian and CLI contexts extend the default implementation with their own capabilities; the Webapp currently uses the default implementation. Every Service Hub and every service in one composition must retain the exact Context object supplied by that host. + +Compatibility is established through observable results, not only structural TypeScript compatibility. A shared probe verifies event delivery, translation results, and Context identity across the public Service Hub surface. Commonlib separately verifies that default contexts isolate their event channels and translators. Real Obsidian runs the same invariants against the loaded plug-in through `obsidian-cli eval`; the CLI runs its composition contract in unit tests and then exercises the built Node artefact through Deno E2E; and the Webapp composition runs the shared contract directly without depending on its currently stale Playwright workflow. + +The same rule applies as more APIs move: define the shared result set first, run the same cases against each implementation, and document platform-specific behaviour outside that result set. Matching method names or return types alone does not establish behavioural compatibility. The contract runner remains test support during this proof rather than becoming a new public package API. + +### Barrels and export surfaces + +The migration does not prohibit every barrel. A small root entry point or task-oriented subpath entry point is an intentional package contract when it has one documented responsibility, uses explicit named exports, and does not load unrelated implementations or optional dependencies. The root client entry point, a focused RPC entry point, and type-only storage-adapter entry points are examples which may remain after review. + +An existing barrel or forwarding façade is removed when the migration provides a clearer import and the barrel: + +- aggregates unrelated domains or exposes implementation layout as API; +- hides a platform, UI framework, worker, database adapter, or another optional dependency; +- makes side effects or tree-shaking behaviour difficult to determine; +- merely re-exports another package without adding a LiveSync-owned contract; or +- exists only to preserve the present `@lib/*` source alias or Service Hub composition. + +In particular, the broad `common/types` barrel is retained only as a temporary compatibility path while focused settings, model, protocol, and path contracts are extracted. The `InjectableServices` forwarding barrel is removed in favour of explicit compatibility imports. Forwarding exports for `octagonal-wheels` facilities are removed where consumers can import the owning package directly; mixed modules such as conversion and utility modules are split when this prevents neutral dependencies from being presented as LiveSync-owned API. + +Every retained barrel must correspond to an explicit `exports` entry, list named exports rather than use an unrestricted wildcard, and have a packed-consumer or bundle test proving that unrelated optional code is not loaded. Removing a barrel is not sufficient reason to expose every underlying file as a public subpath. + +## Migration Plan + +### Phase 0: Record and enforce the boundary + +- Add a package-boundary check which rejects `@/` and direct Obsidian imports in common-library production source. +- Record the current Self-hosted LiveSync `@lib/*` import inventory and classify each path as public, compatibility-only, host-owned, or obsolete. +- Inventory mutable module-level state and add a two-client isolation test before promising a public client API. +- Add standalone test and type-check configuration to the common-library repository while retaining downstream Self-hosted LiveSync CI. +- Add a packed-consumer fixture before changing Self-hosted LiveSync resolution. + +### Phase 1: Remove host leaks + +- Move `ObsidianServiceContext` and `setupObsidian` presentation code to Self-hosted LiveSync. +- Inject the key-value database factory and host event reactions. +- Replace the Obsidian language type import with a local contract. +- Move global DOM compatibility mutation to the browser application bootstrap. +- Introduce translator injection and detach core imports from the full catalogue. +- Split the generic dialogue host, Svelte adapter, and LiveSync policy without changing visible behaviour. + +### Phase 2: Build the package + +- Add the package manifest, compiled ESM build, declarations, export map, and package documentation. +- Keep the root export deliberately small. +- Remove accidental and forwarding barrels where focused imports are clearer; retain only reviewed package entry points with explicit named exports. +- Add temporary compatibility subpaths required by the classified 140-path migration inventory. +- Test Node, browser, worker, PouchDB, Object Storage, P2P, and headless entry points independently. +- Verify that installing a core entry does not pull Svelte UI or language catalogue code into a representative bundle. + +### Phase 3: Publish and validate a pre-release + +- Publish an immutable pre-1.0 version to a pre-release npm dist-tag. +- Verify its package name, provenance, checksum, export map, and packed files. +- Run common-library tests against the packed artefact rather than the source checkout. +- Run downstream Self-hosted LiveSync type checks, unit tests, integration tests, CLI tests, Webapp tests, WebPeer tests, production builds, and focused real-Obsidian E2E against the exact package version. +- Validate at least one external consumer which currently uses a submodule or custom loader. + +### Phase 4: Convert Self-hosted LiveSync + +- Replace `@lib/*` source aliases with package imports. +- Remove the `src/lib` submodule, `_types`, `tsconfig.types.json`, `generate-types.mjs`, and release-workflow steps which regenerate fallback declarations. +- Move the multilingual catalogue and its generation tooling into Self-hosted LiveSync, while retaining only Commonlib's typed English fallback in the package. +- Keep application-owned source under `src/apps` until a separate decision moves an application. +- Run the community scanner's branch or commit preview before releasing the converted plug-in. + +### Phase 5: Stabilise the SDK + +- Design a high-level asynchronous client around explicit `create`, `list`, `get`, `put`, `delete`, `watch`, and `close` lifecycles. +- Specify path normalisation, encryption negotiation, conflict handling, conditional writes, deletion, history, and resource disposal before declaring the façade stable. +- Adapt `DirectFileManipulator` behind that façade or deprecate it; do not treat its current constructor and deep dependencies as the final API. +- Narrow or remove compatibility-only export paths as Self-hosted LiveSync imports migrate to documented package modules. + +## Implementation Proof + +The package proof builds Commonlib as one compiled ESM package with a small root, `context`, `settings`, `remote-configurations`, `browser`, `node`, and `rpc` entries, plus only the explicit compatibility exports required by the reviewed downstream inventory. It publishes neither raw TypeScript nor Svelte source, uses no unrestricted export wildcard, and can be installed into a clean consumer, imported in Node, type-checked from declarations, and bundled independently for browser context, browser storage, browser services, and workers. Release validation records and compares the immutable registry version and checksum. + +The compatibility inventory is regenerated from the maintained Self-hosted LiveSync consumer. Focused entries replace compatibility imports when their contracts are proven; obsolete paths are removed from the next candidate rather than retained merely because an earlier immutable release contained them. The multilingual `common/i18n` and `common/rosetta` paths have been removed in this way after ownership moved to the host. + +The proof found and fixed three boundary defects which source-alias consumption had hidden: compiled JSON imports required explicit output extensions, precompiled Svelte output could not safely be treated as source by the downstream Svelte pipeline, and Vite's default client conditions selected Commonlib's browser worker while building the Node CLI. Packed-consumer regressions cover the first two. The CLI now uses Vite's server conditions and treats every Node built-in reported by Commonlib's Node entry as external; the built CLI is exercised through Deno E2E. Importing root or context also no longer patches DOM prototypes, translator injection prevents the context entry from loading the complete language catalogue, and standard input and protocol output are supplied by the package-owned host contract rather than direct stream access in command handlers. + +Self-hosted LiveSync, its CLI, Webapp, WebPeer, plug-in source, and tests compile against that exact registry artefact without `@lib/*`. Focused downstream storage tests pass against the package-owned Node and File System Access API implementations. Commonlib also owns the Trystero implementation and version; the host retains no direct Trystero dependency, preventing two transport generations from entering one application graph. The old `src/lib` Git submodule, generated `_types` fallback, type-generation scripts, and source aliases are removed by the proof branch. + +Commonlib's contract and complete suites cover Context results, both platform storage implementations, standard I/O, replication, settings, and package consumption. Self-hosted LiveSync runs the same host-composition result contract for Obsidian, CLI, and browser compositions against the exact packed artefact, followed by its unit, integration, application-build, CLI E2E, and focused real-Obsidian gates. + +The package-owned Trystero transport also completes the canonical Compose P2P synchronisation workflow with a local relay and two isolated CLI peers. In real Obsidian, the plug-in starts with one consistent `ObsidianServiceContext`, the representative server-selection and Setup URI Svelte dialogues mount and close through their normal controls, their mobile variants satisfy viewport, safe-area, and touch-target assertions, and the settings pane exposes only the effective deletion controls. These runtime checks complement the package tests without making Webapp maintenance the primary release gate. + +The Community directory scanner preview completes with no source-code errors. The former findings attributed to generated `_types`, raw `src/lib` source, Node built-ins, forbidden rule suppressions, unsupported Obsidian APIs, and undescribed directive comments are absent. This confirms that the registry dependency is recognised as a package boundary. The preview identified behaviour-neutral redundant CLI candidate types and Webapp File System Access API assertions; these are corrected in the host source rather than carried as known warnings. + +The remaining source warnings belong to application code. Browser dialogue visibility now uses DOM state instead of inline static styling, so the earlier styling warning is absent. Direct diagnostic output was resolved at its existing ownership boundaries: Webapp components use an injected log function backed by `BrowserAPIService`, WebPeer retains output in its Svelte log store, Obsidian modules use the established Logger path, and duplicate console emission was removed from `ModuleLog`. The later Webapp and WebPeer recomposition around maintained Context and serviceFeature APIs should preserve these explicit output paths. + +The final Community lint inventory for this boundary has no errors and 126 warnings: 67 sentence-case findings, 58 deprecated-API findings, and one declarative setting-definition suggestion. The sentence-case strings and deprecated interfaces are retained deliberately to avoid an unrelated localisation and host-lifecycle change. Declarative definitions would migrate the complete Obsidian setting tab into the 1.13 settings-search model; that is a separate visible UI project after LiveSync 1.0, not a hidden package-boundary release gate. Revisit each category through focused UI and compatibility work rather than suppressing the rules or treating the warning count as zero. + +WebPeer's production build still reports that Vite externalises the guarded Node `crypto` fallback reached through a compatibility path. Browser execution selects `globalThis.crypto`, and the focused root, `context`, and `browser` bundle checks do not include the Node fallback, so this is not a leak in the reviewed public browser entries. Removing the compatibility-build warning requires a focused crypto-capability contract or a platform-specific implementation split and remains part of compatibility-surface narrowing. + +The dependency preview also reports `uuid`, but the installed and locked graph resolves PouchDB's UUID dependency to the patched `uuid@11.1.1` through the repository override, and `npm audit` does not report the UUID advisory. The scanner appears to infer the older declared PouchDB range rather than the resolved override, so this warning is treated as a scanner false positive unless a packed-artefact inspection shows otherwise. + +The 1.0 dependency review found newly disclosed parser and denial-of-service advisories with compatible fixes. All locked `brace-expansion` generations now use their patched releases, including the production generation reached by Commonlib path matching and the CLI's user-configured ignore patterns. The development-only ESLint and Istanbul `js-yaml` generations likewise use patched releases. The production Markdown parser uses the patched `linkify-it` release to avoid quadratic processing of maliciously structured `mailto:` links, while the development-only JSON Schema toolchain uses the patched `fast-uri` release for unambiguous hostname parsing. A clean install and both complete and production-only `npm audit` checks no longer report these packages. + +The remaining audit report is the existing `werift` and `werift-ice` dependency on `ip`, for which npm offers no patched version. The advisory concerns `ip.isPublic()` misclassifying unusual loopback representations. The locked werift implementation uses `ip` for address encoding, decoding, format detection, and loopback filtering, but does not call `isPublic()` or `isPrivate()`. LiveSync reaches werift only through the Node CLI's injected `RTCPeerConnection`; the Obsidian plug-in and browser applications use their platform WebRTC implementation, and the plug-in artefact does not contain werift. The package-level finding is therefore accepted for the 1.0 integration preview as a non-reachable advisory in the reviewed call path, not as a general waiver. Revisit it when werift or `ip` publishes a replacement, or before any change which delegates address trust, routing, or URL access decisions to that dependency. + +The local real-Obsidian suite verifies the actual loaded `ObsidianServiceContext`, all 18 services, Vault reflection, CouchDB and Object Storage transfer, remote-activity accounting, CLI-to-Obsidian encrypted synchronisation, startup scanning, two-Vault create, update, delete, ordinary rename, case-only rename, target mismatch, Hidden File Sync, Customisation Sync, setting Markdown export, and two-device CouchDB, Object Storage, and P2P Setup URI workflows. These checks establish observable results and host composition rather than relying on declaration compatibility alone. + +The obsolete mocked browser Harness and its `harness-ci` workflow have been removed. Webapp production builds and the direct browser composition contract remain part of the package-boundary proof; future Webapp browser automation is a Webapp-owned workflow rather than a substitute for real Obsidian E2E. + +## Scanner and Repository Consequences + +Consuming the npm package removes the common-library source and generated `_types` from the plug-in repository's Community directory scan, because package dependencies are treated as dependencies rather than maintained plug-in source. The branch preview confirms this expected boundary. + +The change does not hide or resolve warnings in Self-hosted LiveSync-owned source. The scanner will continue to inspect the plug-in, CLI, Webapp, and WebPeer while those applications remain in the repository. Moving those applications to a LiveSync-family application repository may be considered after the package boundary is stable, but it is not part of this decision because the CLI and Webapp still import shared plug-in composition code. + +Package extraction also does not resolve release-asset attestation verification, unsupported release assets, declarative settings migration, or other scanner findings which are independent of source ownership. + +## Alternatives Rejected + +### Move the common library into the Self-hosted LiveSync monorepo + +This would permit atomic source changes and remove the Git submodule, but the community scanner already examines non-plug-in application source. A source workspace would remain in scope, and every library directive and platform dependency would be attributed to the plug-in repository. It would also make independent consumers depend on the plug-in repository's release cadence. + +### Move the common library into Fancy Kit + +Fancy Kit owns reusable framework-neutral interactions, Obsidian adapters, test infrastructure, and neutral utilities. Replication protocols, chunk and metadata formats, PouchDB composition, conflict rules, and LiveSync storage policy are a different domain. Moving them would obscure ownership and make general plug-in tooling carry Self-hosted LiveSync release concerns. + +### Publish the current source tree without changing boundaries + +This would expose accidental deep imports, global mutations, Svelte and browser code, Node-only modules, and parent-repository imports. Consumers would still need source aliases and bundler-specific behaviour, while maintainers would be unable to distinguish public API from implementation. + +### Split every concern into a separate npm package immediately + +Core, language, UI, browser, Node, P2P, Object Storage, and SDK packages could make dependency graphs precise, but the present source has not yet proven those release boundaries. Starting with one compiled package and explicit optional subpaths allows measurement without creating a coordinated release matrix. Additional packages remain possible after their contracts and consumers are demonstrated. + +## Verification + +The package-boundary conversion is ready for acceptance only when: + +- common-library production source has no parent-repository or Obsidian dependency; +- the package builds and tests from a standalone clean checkout; +- `npm pack` contains only intended compiled artefacts and documentation; +- packed Node and browser consumers resolve only exported paths; +- a headless client does not bundle Svelte or the full language catalogue; +- retained entry-point barrels do not load unrelated optional implementations, and no removed barrel is replaced by unrestricted deep exports; +- Self-hosted LiveSync verifies its local Svelte adapter and workflow policy through injected tests and a focused composition smoke test; +- Self-hosted LiveSync no longer has `src/lib`, `_types`, or `@lib/*` source aliases; +- the Commonlib owner and packed-consumer suites, Self-hosted LiveSync unit and integration tests, Deno CLI E2E, plug-in and application production builds, and focused real-Obsidian E2E pass against the reviewed package artefact; +- common-library downstream CI records and tests the exact Self-hosted LiveSync ref; +- a Community directory scanner preview confirms the expected change in source findings; and +- compatibility exports are explicitly enumerated from a reviewed downstream inventory rather than exposed through a wildcard. + +The following are later SDK-stabilisation gates, not blockers for accepting the package boundary: + +- two clients with different database, encryption, language, and lifecycle settings can coexist without sharing mutable client state; +- an external consumer can replace its submodule or custom loader with the documented high-level package API; and +- if the framework-neutral Modal host is promoted to Fancy Kit, Kit-owned lifecycle, viewport, safe-area, and touch-target guarantees replace duplicated device tests in LiveSync. + +## Consequences + +- The common library gains an independently consumable and testable artefact while retaining its domain ownership and history. +- Self-hosted LiveSync release archives no longer need generated fallback declarations for an absent submodule. +- Community directory scanning distinguishes plug-in source from the reviewed dependency. +- Changes which span the package and plug-in require coordinated package and downstream validation rather than one atomic source commit. +- Temporary compatibility exports increase the first package surface, but their pre-1.0 status and explicit classification prevent them from becoming silent permanent contracts. +- Application translation, Svelte, and platform dependencies become optional composition concerns rather than root-package side effects; Commonlib's small English fallback remains safe by default. +- Fancy Kit may gain a generally useful typed Obsidian Modal host without acquiring LiveSync policy or a mandatory Svelte dependency. + +## Later Open Questions + +- Define the minimum conflict and conditional-write guarantees required by the first public high-level client. +- Continue narrowing the broad model, service-composition, and replication compatibility paths as focused result contracts become available. +- Confirm whether another Fancy Kit consumer needs the framework-neutral Modal host before it is added to the Kit, or whether LiveSync should pilot the contract first. diff --git a/docs/adr/2026_07_feature_maturity_for_1_0.md b/docs/adr/2026_07_feature_maturity_for_1_0.md new file mode 100644 index 00000000..c1668947 --- /dev/null +++ b/docs/adr/2026_07_feature_maturity_for_1_0.md @@ -0,0 +1,49 @@ +# Feature maturity for 1.0 + +## Status + +Proposed for the 1.0 integration branch. + +## Context + +Self-hosted LiveSync accumulated several labels such as 'experimental', 'Beta', 'obsolete', and 'sunset' over the 0.x line. Those labels did not consistently describe the current implementation. Some formerly experimental features now have maintained unit, Compose, and real-Obsidian coverage, while some old database-format options remain executable only because existing data must still be opened safely. + +Version 1.0 needs to distinguish supported opt-in features from previews and from compatibility paths. Removing a label does not make a network environment reliable, and retaining a setting key does not make it a recommendation. + +## Decision + +### Supported opt-in features + +- Peer-to-Peer Synchronisation is supported. Commonlib owns the transport lifecycle, the Compose suite covers transfer, replacement, disconnect, and reconnect behaviour, and the Obsidian host verifies its current pane boundary. Documentation must describe environmental WebRTC limitations without calling the feature experimental. +- Hidden File Sync is supported as an advanced, separately initialised feature. It remains disabled during ordinary Setup URI initialisation and has its own two-Vault, conflict, filtering, and notification acceptance workflow. +- Customisation Sync is supported as an advanced opt-in feature. Its maintained two-Vault real-Obsidian workflow covers snippets, configuration files, and plug-in files, including updates and deletion of source synchronisation data. It remains separate from Hidden File Sync and must not manage the same files concurrently. +- Data Compression is maintained as an advanced opt-in storage and bandwidth trade-off. It remains disabled by default. The three-repeat CLI and CouchDB benchmark reduced stored chunk data and upload bodies by about 9% for the mixed fixture, but processing and worker-memory costs remained substantial. The [Data Compression specification](../specs_data_compression.md) records the contract, measurements, compatibility behaviour, execution model, and reproduction path for future default-setting decisions. +- The real-Obsidian E2E runner is maintained release infrastructure rather than an experimental Harness. + +### Retained previews + +- JWT authentication remains experimental because it depends on specialised CouchDB server configuration and does not yet have a maintained server-backed authentication matrix. The current implementation, Setup URI transport, focused unit coverage, and reported ES512 use justify retaining it. +- Ignore files remain Beta. They have focused target-filter tests, but nested rules, hidden-file expectations, and open user reports still require review. +- Automatic newer-file conflict resolution remains Beta and disabled by default because it can deliberately overwrite one side of a conflict. +- Garbage Collection V3 remains Beta and explicitly initiated. Its algorithm and safety protocol are outside this classification change and require a separate decision. + +### Compatibility-only and sunset paths + +- E2EE V1 and its dynamic iteration-count setting remain for existing encrypted databases. E2EE V2 is the new-Vault contract. +- The old IndexedDB adapter remains only with its migration path back to IDB. +- `xxhash64` is the current hash contract. Other hash algorithms remain available for existing databases and edge-case recovery, not as experimental alternatives for new Vaults. +- Eden chunks remain accepted at runtime and in transported settings, but are not offered in the settings interface. +- `doNotUseFixedRevisionForChunks` remains an inert compatibility input. Chunk revisions are always content-derived. +- The deprecated cleaned-database reconciliation callback remains internal while an old IndexedDB client may still encounter that remote state. It is not a user-selectable maintenance action and is omitted from the settings reference. + +### Already removed + +The obsolete mocked browser Harness, automatic bulk chunk pre-send, legacy trash toggle, and fixed-revision control have no supported 1.0 UI path. Their compatibility data, where required, remains accepted independently of their removed controls. + +## Consequences + +- Supported opt-in features retain focused release gates and user documentation. +- Preview features remain off by default and keep explicit maturity labels. +- Compatibility-only settings must not silently change existing data formats. New configuration should not expose retired formats merely because their decoders remain available. +- Commonlib setting types and Setup URI decoding remain broad enough to read existing configurations. Removing those package contracts requires a separately versioned compatibility decision. +- Deprecated host accessors and Community directory API warnings are a separate refactoring boundary. This decision does not authorise removing broadly used internal access paths. diff --git a/docs/adr/2026_07_multiple_remote_onboarding.md b/docs/adr/2026_07_multiple_remote_onboarding.md new file mode 100644 index 00000000..ff07cdec --- /dev/null +++ b/docs/adr/2026_07_multiple_remote_onboarding.md @@ -0,0 +1,88 @@ +# Architectural Decision Record: Make Onboarding Profile-Aware + +## Status + +Accepted — implemented and verified against the locked Commonlib package. + +## Context + +Self-hosted LiveSync stores multiple remote connections in `remoteConfigurations` and selects the ordinary replication target with `activeConfigurationId`. P2P features have a separate `P2P_ActiveRemoteConfigurationId`. Existing replication implementations still consume the older flat CouchDB, Object Storage, and P2P fields, so selecting a profile projects its connection settings onto those compatibility fields. + +The settings pane already creates and edits profiles directly. The Setup Wizard was inconsistent: + +- modern Setup URI and QR payloads retained a supplied profile map; +- legacy imports relied on `SettingService` to migrate flat connection fields into `legacy-*` profiles; +- manual CouchDB and Object Storage setup wrote only the flat fields and relied on that same migration; and +- manual P2P setup partly updated the profile map itself. + +Legacy migration deliberately runs only when the profile map is empty. If a Vault already had one or more profiles, manually configuring another CouchDB or Object Storage connection changed the flat fields but did not add a profile. The selected stored profile could subsequently project its older values back onto those fields. Fresh P2P onboarding could also finish without a profile when no P2P profile ID existed beforehand. + +## Decision + +The profile map is authoritative persisted state for every newly configured remote. The compatibility fields remain the runtime projection of a selected profile and the input accepted from an older settings payload. + +### Manual onboarding + +A successful manual CouchDB, Object Storage, or P2P setup creates or deliberately updates a profile through Commonlib's focused `@vrtmrz/livesync-commonlib/remote-configurations` entry. + +- Existing profiles are preserved. +- A newly created profile receives an opaque generated ID. +- CouchDB and Object Storage setup select the new profile as the main remote. +- P2P setup selects the profile through `P2P_ActiveRemoteConfigurationId`. +- P2P setup also selects it as the main remote when P2P is being configured as the main remote. +- Configuring P2P alongside another main remote changes only the P2P selection. +- A known profile ID is supplied only when an existing profile is deliberately being updated; omitting the ID creates another profile. + +The onboarding dialogue does not add a naming step. Commonlib proposes a concise type-specific display name and adds a numeric suffix when necessary. The settings pane can rename it later. + +### Identity and naming + +Profile names are presentation only. They are neither unique identity nor a marker for the selected profile. No entry receives a special `default` ID or name. Opaque IDs establish identity, `activeConfigurationId` establishes the main selection, and `P2P_ActiveRemoteConfigurationId` establishes the P2P selection. + +Generated names describe the connection without exposing credentials, for example `CouchDB couch.example`, `S3 notes`, or `P2P team-room`. + +### Setup URI and QR import + +A modern payload preserves its profile IDs, display names, profile URIs, main selection, and P2P selection. Setup does not rename or recreate those profiles. + +A legacy payload containing only flat connection fields remains supported. `SettingService` migrates it into clearly labelled `legacy-couchdb`, `legacy-s3`, or `legacy-p2p` profiles only when no modern profile map exists. This is the only onboarding path which intentionally relies on the compatibility migration. + +### Persistence and restart ordering + +Profile construction happens before the settings are submitted to the onboarding completion boundary. Existing-device and new-device setup reserve Fetch or Rebuild respectively before enabling and saving the settings. Profile awareness does not change that initialisation ownership or restart ordering. + +Commonlib produces an in-memory plaintext profile URI. The standard setting service applies its configured at-rest encryption during persistence. + +## Alternatives rejected + +### Keep relying on legacy migration + +This works only while the profile map is empty. It silently fails to register a newly configured connection once multiple-remote settings are already in use and leaves P2P with a separate implementation. + +### Create a special `default` profile + +The special meaning would duplicate `activeConfigurationId`, make a user-visible name carry identity, and become ambiguous as soon as the user selects another profile. Selection IDs already express the required state. + +### Add profile naming and full list editing to onboarding + +That would make the first-run path longer and duplicate the established Saved connections interface. Automatic descriptive names and later renaming keep this change limited to data integrity and consistent selection. + +### Replace the compatibility fields immediately + +Replication, diagnostics, and older settings paths still consume the projected fields. Removing them belongs to a broader runtime migration and is not required to make onboarding correctly profile-aware. + +## Verification + +Commonlib unit tests cover preserving existing profiles, opaque-ID insertion, generated display names, duplicate-name suffixes, main activation, independent P2P selection, and URI serialisation. Its packed-consumer test imports the focused entry point from the generated package. + +Self-hosted LiveSync unit tests cover preserving modern Setup URI profiles and their active selection, retaining legacy Setup URI and QR migration, adding CouchDB and Object Storage profiles beside an existing profile, independent P2P selection, fresh P2P selection as both main and P2P remote, and cancellation without mutation. + +The real-Obsidian onboarding E2E owns the invitation, dialogue presentation, safe-area and touch-target checks, cancellation, and reopening from the Setup pane. It does not contact a remote or submit credentials. Remote connection correctness remains owned by the CouchDB, Object Storage, P2P, and two-Vault suites. The end-to-end Setup URI and provisioning acceptance workflow remains a separate release gate. + +## Consequences + +- Manual onboarding and the Saved connections list share one Commonlib profile contract. +- Existing profiles survive reconfiguration, and a newly configured connection becomes explicitly selectable. +- Modern imports retain user-assigned profile identity and names. +- Legacy Setup URIs continue to work through an isolated compatibility boundary. +- Runtime consumers may keep using projected flat fields while the persisted model and new APIs use the 1.0 multiple-remote contract. diff --git a/docs/adr/2026_07_p2p_transport_lifecycle.md b/docs/adr/2026_07_p2p_transport_lifecycle.md new file mode 100644 index 00000000..e7f4b671 --- /dev/null +++ b/docs/adr/2026_07_p2p_transport_lifecycle.md @@ -0,0 +1,91 @@ +# Architectural Decision Record: P2P Room and Transport Lifecycle + +## Status + +Accepted — implemented and verified through Commonlib owner tests, the Compose transport suite, and the real-Obsidian setup workflow. + +## Context + +Self-hosted LiveSync uses Trystero's Nostr strategy for P2P discovery, signalling, and WebRTC transport. Three related resources have different owners and lifetimes: + +- a LiveSync P2P service instance owns its commands, RPC sessions, advertisements, and room membership; +- Trystero owns the underlying WebRTC peers and may share one physical peer across more than one room; and +- Trystero's Nostr relay manager owns WebSocket clients shared by relay URL. + +Closing every `RTCPeerConnection` returned by `room.getPeers()` bypasses Trystero's shared-peer manager. The manager may then retain a stale shared peer and prevent a replacement LiveSync replicator from discovering the same remote peer again. + +Room departure and physical transport destruction are not equivalent. `room.leave()` sends the room-leave action, removes that room's actions and callbacks, and detaches its shared-peer binding. Trystero may retain a healthy physical WebRTC peer for later reuse after the last room binding has gone. The retained peer cannot carry actions for the room which has been left. + +Relay WebSockets have a separate lifecycle. LiveSync's explicit disconnect operation must close them and prevent automatic reconnection. A later explicit connect must allow reconnection before joining the room again. + +## Decision + +Normal P2P shutdown delegates physical peer ownership to Trystero: + +1. Stop LiveSync broadcast, replication, watch, client, and RPC state. +2. Leave the active Trystero room through `room.leave()`. +3. Remove LiveSync's room, advertisement, diagnostic-listener, and active-instance references. +4. Pause Trystero relay reconnection and close the current relay WebSockets. + +LiveSync does not call `close()` on the `RTCPeerConnection` values returned by `room.getPeers()` during normal shutdown or from a peer-leave callback. A peer-leave callback removes LiveSync-owned advertisement and client state only. Trystero remains responsible for deciding whether an underlying shared peer is reusable, stale, or ready for idle destruction. + +The explicit disconnect operation therefore has the following contract: + +| Resource | State after the operation | +| --------------------------------- | ------------------------------------------------------------------------------------------------- | +| LiveSync P2P service and RPC room | Closed immediately. | +| Trystero room membership | Left; room actions and advertisements are no longer available. | +| Nostr relay WebSockets | Closed, with automatic reconnection paused. | +| Underlying WebRTC peer | May remain idle under Trystero ownership for reuse, but cannot carry the departed room's traffic. | + +This operation is a logical LiveSync disconnection and a physical signalling-server disconnection. It does not promise that every browser-owned WebRTC object has been destroyed synchronously. + +An explicit connect resumes relay reconnection before opening a new room. Settings application and database lifecycle replacement close the current LiveSync replicator, discard it, construct a new instance from the current settings, and open that current instance when the configured policy requires it. Commands, event handlers, and panes resolve the current service-feature result at the point of use rather than retaining an obsolete replicator. + +Lifecycle operations on one `LiveSyncTrysteroReplicator` are serialised. A close requested while an open is in progress must leave no orphan room serving, and repeated opens must not create parallel rooms. No fixed delay is inserted between close and open: readiness is determined by the actual lifecycle operation and peer discovery. + +Relay sockets retain their Trystero-provided close handlers. LiveSync pauses relay reconnection, closes the sockets, and later resumes reconnection through Trystero's public functions. It does not replace `socket.onclose`, because Trystero uses that handler to retire and recreate shared relay clients correctly. + +P2P setup follows the transport's actual ownership model. Initialising the first device resets and scans the local database, but does not attempt to lock, reset, or upload to a non-existent central remote database. Its confirmation dialogues therefore describe preparing this device and do not present warnings about overwriting a central server or an option to fetch its configuration. An additional device selects a peer once, performs Fetch once, then resumes database and Vault reflection. The generic second convergence pass remains reserved for central remote types because repeating it for P2P would ask the user to select the same peer twice. + +## Ownership + +Commonlib owns the LiveSync-specific P2P service, RPC, command, and lifecycle composition. Trystero owns WebRTC peer creation, sharing, reuse, stale detection, and destruction, as well as relay-client reconstruction. The Self-hosted LiveSync host owns the current Commonlib service-feature result and supplies the platform services used by its current replicator. + +Self-hosted LiveSync does not add a separate root Trystero dependency. Tests which must observe relay sockets resolve the exact Trystero generation owned by the locked Commonlib package, avoiding two independent transport singletons in one process. + +## Alternatives rejected + +### Close every value returned by `room.getPeers()` + +This bypasses Trystero's shared-peer manager and can prevent a replacement replicator from rediscovering the same peer. + +### Add a fixed close-to-open delay + +A timing guess does not repair stale ownership and would make ordinary settings application slower. + +### Keep raw close behaviour behind a force command + +Changing the command name does not make the lifecycle safe. A force command which cannot reconnect predictably has no reliable operational value. + +### Override relay `onclose` to suppress reconnection + +This interferes with Trystero's shared relay clients. The public pause and resume functions provide the intended control boundary. + +## Verification + +Commonlib unit tests prove that normal P2P host closure calls `room.leave()` without directly closing Trystero-owned peer connections. Additional package tests cover the action API, replaceable peer-event subscriptions, multiple RPC transport disposers, serialised open and close operations, initialisation of the first device without a central remote, and Fetch running once for an additional device. + +Self-hosted LiveSync unit tests prove that settings and database replacement leave panes on the current replicator, and that an explicit P2P rebuild bypasses the policy intended for ordinary replication. + +The canonical Compose P2P suite uses a real local Nostr relay and WebRTC implementation. It covers ordinary two-peer synchronisation, replacement of the active LiveSync replicator followed by discovery and transfer with the same peer, and explicit relay disconnection followed by paused and resumed reconnection. The lifecycle scenario is exposed only through a Docker test build and an injected CLI command runner; it is not part of the public CLI command surface. + +The real-Obsidian P2P Setup URI workflow creates the first device, generates the second-device URI from it, accepts each peer visibly, and verifies a two-way note round-trip through a local relay. A separate focused pane test covers the principal connection control and teardown without requiring a remote peer. Transport replacement and relay-socket lifecycle remain owned by the package and Compose tests rather than being duplicated in Obsidian. + +## Consequences + +- Replacing a P2P replicator no longer leaves host views or commands bound to an obsolete instance. +- Explicit signalling-server disconnection has a testable socket-level meaning without claiming immediate destruction of idle WebRTC objects. +- Settings which change the relay, room, passphrase, or TURN configuration can replace the whole LiveSync room safely. +- Trystero may reuse healthy peers across room lifecycles, reducing unnecessary renegotiation. +- Strict physical WebRTC teardown remains unavailable until Trystero exposes a safe ownership-aware operation. diff --git a/docs/adr/2026_07_release_notes_and_database_compatibility.md b/docs/adr/2026_07_release_notes_and_database_compatibility.md new file mode 100644 index 00000000..a444e40d --- /dev/null +++ b/docs/adr/2026_07_release_notes_and_database_compatibility.md @@ -0,0 +1,103 @@ +# Release notes and database compatibility gates + +## Status + +Accepted for the 1.0 release line. + +## Context + +Self-hosted LiveSync historically used several unrelated kinds of version and settings state during start-up. + +The plug-in SemVer was converted into a numeric major/minor value and stored in `lastReadUpdates`. The settings dialogue used that value to open the change log automatically, and offered a button which marked the release line as read. Patch versions were intentionally ignored. Pre-release identifiers containing an additional dot did not fit this numeric representation and could be interpreted as a much larger release line. + +Separately, the internal database compatibility constant `VER` is recorded in device-local storage under a Vault-scoped key. Crossing this internal version used to set `versionUpFlash` and permanently change several automatic synchronisation settings to `false`. Replication services already reject work while `versionUpFlash` is non-empty, so changing the user's saved choices duplicated the runtime safety gate and required manual reconstruction after acknowledgement. + +The remote `obsydian_livesync_version` document also carries the internal database version. It is a protocol and data-compatibility mechanism, not a copy of the plug-in SemVer. + +Commonlib's `settingVersion` describes the stored settings shape, while `DEFAULT_SETTINGS` was historically used both to complete missing values in an existing document and to initialise a new Vault. Those operations require different defaults: an existing Vault needs conservative completion, while a genuinely new Vault can use current recommendations without changing an established configuration. + +## Decision + +### Release notes + +- Keep the change-log pane and render the current release history whenever it is opened. +- Remove automatic unread-version tracking, the acknowledgement button for ordinary release notes, and automatic navigation to the change-log pane. +- Do not derive data-compatibility behaviour from the plug-in's major, minor, patch, or pre-release identifiers. +- Retain the saved `lastReadUpdates` field in the settings schema for backwards compatibility, but do not use it in the plug-in. It can be removed through a separately reviewed settings-schema migration if retaining it later becomes burdensome. + +### Settings schema and initial settings + +- Keep the Commonlib settings schema version independent of the plug-in SemVer and the internal database version `VER`. Increment it only for an ordered change to the stored settings shape. +- Use Commonlib's conservative schema defaults to complete an existing settings document. Explicit stored values take precedence, and an ordinary migration does not disable or replace the person's synchronisation choices. +- Use `createNewVaultSettings()` only for a store which has never held Self-hosted LiveSync settings, explicit new-user onboarding, a factory reset, or CLI settings creation. Setup URI, QR, Markdown, and other existing-setting imports retain conservative completion semantics. +- Apply remote-specific preferred values only when that remote is explicitly selected during setup. Do not infer and merge new recommendations into an existing configuration. +- Keep settings saved by a future Commonlib schema fail-closed and do not persist an apparent downgrade migration. + +The current new-Vault base selects a 50 MB maximum synchronised file size, Rabin–Karp chunk splitting, Plug-in Sync V2, case-insensitive file-name handling, and E2EE V2. It does not enable synchronisation, encryption, or a remote connection without user action. Chunk revisions are always content-derived; `doNotUseFixedRevisionForChunks` remains only as deprecated compatibility input and is not a recommendation or review setting. + +Relative to the conservative existing-setting fallbacks, only Plug-in Sync V2 and the explicit case-insensitive value differ for a new Vault. The 50 MB limit, Rabin–Karp splitter, and E2EE V2 already match the legacy fallback values. Data Compression, Eden, V1 dynamic iteration, the legacy IndexedDB adapter, Hidden File Sync, and automatic synchronisation remain disabled when absent from an existing settings document. + +An existing settings document without an explicit `handleFilenameCaseSensitive` choice is normalised to `false` and saved. This preserves the effective case-insensitive branch used by earlier releases when the value was absent, and does not require a review or rebuild. An explicit `true` or `false` choice remains unchanged. + +Keep configured-state inference separate from new-Vault initialisation. If an existing legacy document has no `isConfigured` value, repeat the pre-1.0 comparison with the conservative defaults: a default-equivalent document remains unconfigured, while a non-default stored value is evidence that it was configured. Persist that inferred boolean so a migration cannot turn an unconfigured Vault into an irreversible configured state merely because its settings document was non-empty. + +### Database compatibility + +- Continue to use the internal database version `VER` for changes which require explicit compatibility review. Changing the plug-in SemVer alone does not increment `VER`. +- Store the last acknowledged internal database version through Commonlib's device-local small-configuration contract under `database-compatibility-version`. Copy the legacy raw local-storage value into that contract once, then remove the legacy key after the copy has completed. +- Initialise the marker to the current `VER` only when Commonlib identifies a genuinely new Vault with no pending review. A configured existing Vault with a missing or invalid marker requires review instead of being silently accepted. +- Defer database compatibility evaluation for an existing unconfigured Vault. It cannot replicate, so do not persist a misleading pause or acknowledge its missing marker while onboarding is still pending. Keep the marker absent so a later configured start evaluates the same state before ordinary synchronisation. +- Treat a missing marker on a configured Vault as an ambiguous device transition. Copying or restoring a Vault, or opening it with a new Obsidian profile, can preserve settings and database files without preserving device-local storage. Do not infer acknowledgement from an empty local database: a recovery operation, partial copy, or remote-first setup can also produce that state. Explain these cases and require an explicit decision in the compatibility dialogue. +- Derive one structured pause from the acknowledged database version, Commonlib's settings-migration state, and any persisted legacy review message. Persist the generic `versionUpFlash` message without changing any automatic synchronisation setting, because Commonlib already treats that field as a replication gate. +- Treat non-empty `versionUpFlash` as a runtime replication gate. Standard and one-shot replication must stop before remote work begins. +- Apply the same ordinary replication policy to P2P pull, push, and peer-requested synchronisation. An explicitly confirmed Fetch or Rebuild may bypass the ordinary policy because it is the operation selected to construct or recover the local state. +- Present the reason in a dedicated dialogue after the Obsidian layout is ready. The details view is explanatory only and returns to the summary before any decision can be made. The safe default and closing either dialogue keep synchronisation paused. A persistent Notice and a command allow the dialogue to be reopened without using the settings pane. +- Let the person read focused compatibility details without presenting the whole release history as a safety instruction. The Change Log remains a manually opened release-history pane and contains no compatibility acknowledgement control. +- Offer an explicit resume action only when every reason is recoverable in the running implementation. An upgrade, a missing or invalid marker on an existing Vault, and a reviewed migration from an older settings schema are resumable after all devices have been updated. A downgrade from a newer acknowledged `VER`, or settings saved by a future schema, cannot be acknowledged by the older installation. +- On resume, clear `versionUpFlash` and persist that fail-closed change before recording the current `VER` as acknowledged. If saving fails, restore the gate. Reapply settings only after the marker has advanced so that the previously configured synchronisation behaviour can resume without reconstruction. +- Preserve the original legacy review message as a structured reason when no more specific database or settings-schema reason is available. Escape it before including it in Markdown UI. +- Continue to reject a remote version document which is newer than the running implementation. That receiver-side check is independent of the local upgrade review. + +### Onboarding activation and initialisation + +- Keep an unconfigured Vault outside database initialisation, offline scanning, and configured-only checks. Offer setup through the long-lived onboarding Notice, and allow the wizard to be reopened from the Setup pane instead of opening a competing dialogue automatically. +- For new-device onboarding, reserve Rebuild before enabling and saving the accepted settings. +- For an unconfigured existing device, reserve Fetch before enabling and saving imported or manually confirmed settings. +- Suspend the current runtime after the flag has been written, apply the accepted settings through the scheduler's preparation callback, and request restart only after that callback succeeds. +- If the flag cannot be written, do not enable the settings. If preparation fails, remove the flag, resume the current runtime, and leave the transition incomplete. +- Applying compatible settings to an already configured device remains an ordinary edit and does not schedule Fetch automatically. + +`isConfigured`, the Fetch and Rebuild flags, and in-memory suspension therefore retain separate meanings. `isConfigured` controls participation in ordinary processing, a flag selects a one-shot operation for the next start, and suspension prevents the old process from observing newly enabled settings before that selected restart. + +### Flag-file recovery order + +- For a configured Vault, evaluate and persist the compatibility gate after settings load, before Obsidian layout-ready recovery begins. This blocks ordinary and one-shot replication even while the review dialogue has not yet opened. An existing unconfigured Vault follows the deferred rule above instead. +- Preserve the existing ordered flag-file recovery handlers: SCRAM at priority 5, fetch-all at priority 10, and rebuild-all at priority 20. These files express an explicit recovery instruction and may invoke their focused storage or rebuild service while ordinary replication remains gated. +- Present the compatibility review at priority 30, after any selected recovery operation. A recovery handler which cancels start-up, keeps SCRAM active, or schedules a restart returns `false`, so the current process does not open a competing compatibility dialogue. If recovery completes and start-up continues, the dialogue opens before normal synchronisation is allowed to resume. +- Keep database preparation independent of an unanswered compatibility dialogue, because the compatibility gate already blocks replication. Before Config Doctor begins its interactive checks, await the active initial review so that the two update dialogues cannot overlap. +- Never mark compatibility as acknowledged merely because fetch, rebuild, or local database reset completed. The person must still use the explicit resume action. This keeps destructive recovery intent separate from protocol and settings compatibility acknowledgement. + +## Consequences + +- Ordinary releases no longer force the settings dialogue to show release notes. Important operational instructions must be clear in the published release notes and any explicit migration notice. +- SemVer pre-releases such as `1.0.0-rc.0` no longer require a special numeric encoding inside plug-in settings. +- An internal compatibility change remains fail-closed for replication, but it no longer destroys the person's synchronisation preferences. +- A new installation has no previous internal-version marker and therefore does not show an upgrade review. Its initial settings and onboarding remain responsible for keeping replication disabled until configuration is complete. +- An existing unconfigured installation also remains on onboarding without a compatibility warning. Unlike a genuinely new Vault, it does not receive an acknowledgement marker; activation leaves the compatibility decision for its next configured start. +- A copied or restored configured Vault can show a one-time compatibility review on its new device or profile. This is intentional even when its local database appears empty, because emptiness does not prove how the Vault was produced. +- A genuinely new Vault receives current recommendations without applying them as fallbacks to an existing configuration. It remains inert until onboarding is accepted. +- Accepted new-device and existing-device setup cannot enable ordinary processing before the selected Rebuild or Fetch has been reserved. +- An older installation cannot dismiss evidence that a newer implementation or settings schema has already been used on the device. +- The Obsidian-specific dialogue depends only on a host-neutral compatibility result and the injected confirmation capability. Commonlib remains responsible for settings migration, device-local storage, and the replication gate. +- A future incompatible database change must increment `VER`, provide an actionable review message, verify the remote version negotiation, and test both the pending and acknowledged states. A major SemVer increase without those changes has no database-compatibility effect. + +## Verification + +- Unit tests verify new-Vault initialisation, upgrades, missing and invalid markers, downgrades, future settings schemas, legacy marker migration, acknowledgement ordering, and save-failure recovery while retaining automatic synchronisation choices. +- Commonlib package tests verify conservative stored-setting completion, independently mutable new-Vault settings, legacy file-name case normalisation, future-schema protection, and the focused settings entry from a clean consumer. +- Host unit tests verify new-Vault factory use, conservative import paths, the unconfigured start-up gate, deferred compatibility evaluation and later re-evaluation, flag-before-settings ordering, rollback when the flag cannot be reserved, ordinary configured edits, and compatibility acknowledgement persistence. +- Unit tests verify that a pending review is honoured by the packaged Commonlib replication service before remote activity begins. +- Unit and Compose tests verify that ordinary P2P replication observes the policy, explicit P2P rebuild uses the setup bypass, and replacement leaves host actions on the current replicator. +- A real-Obsidian settings test verifies the dedicated summary and details dialogues, captures representative screenshots, confirms that the acknowledged internal version advances only after explicit resume, and confirms that the Change Log contains no acknowledgement control. +- The real-Obsidian CouchDB workflow starts from configured plug-in data without a device-local marker, verifies the copied-or-restored Vault explanation, resumes through the actual dialogue, and then completes remote metadata, chunk, and activity checks. The two-Vault workflow performs the same review once per isolated Vault before reusing the acknowledged device state for later process launches. +- Unit tests fix the layout-ready priority after the three flag-file recovery priorities, so a recovery which stops start-up cannot race the compatibility dialogue. diff --git a/docs/datastructure.md b/docs/datastructure.md index c6de91ae..e923ab5e 100644 --- a/docs/datastructure.md +++ b/docs/datastructure.md @@ -1,4 +1,5 @@ # Data Structures of Self-Hosted LiveSync + ## Overview Self-hosted LiveSync uses the following types of documents: @@ -30,7 +31,7 @@ export interface DatabaseEntry { This document stores version information for Self-hosted LiveSync. The ID is fixed as `obsydian_livesync_version` [VERSIONING_DOCID]. Yes, the typo has become a curse. When Self-hosted LiveSync detects changes to this document via Replication, it reads the version information and checks compatibility. -In that case, if there are major changes, synchronisation may be stopped. +This internal database version is independent of the plug-in's SemVer version. The last version explicitly acknowledged on a device is stored through Commonlib's device-local configuration contract. When that version differs, or when a settings migration requires review, Self-hosted LiveSync presents a dedicated compatibility dialogue and blocks replication without changing the user's automatic synchronisation choices. A supported upgrade can resume only after explicit review. A downgrade from a newer acknowledged database version, or settings written by a future schema, remains blocked until a compatible plug-in is installed. Please refer to negotiation.ts. ### Synchronise Information Document diff --git a/docs/p2p.md b/docs/p2p.md new file mode 100644 index 00000000..ff47468a --- /dev/null +++ b/docs/p2p.md @@ -0,0 +1,98 @@ +# How peer-to-peer synchronisation works + +Peer-to-peer (P2P) synchronisation transfers Vault data between LiveSync devices through WebRTC. It does not require a central database containing a copy of the Vault. It does require a signalling relay so that devices can discover one another and establish a connection. + +For the procedure for the first and additional devices, see [Set up peer-to-peer synchronisation](setup_p2p.md). For connection problems, see [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md). + +## Connection model + +```mermaid +flowchart LR + A["Device A"] <-->|"Discovery and connection signalling"| S["Signalling relay"] + S <-->|"Discovery and connection signalling"| B["Device B"] + A <-->|"Encrypted Vault synchronisation"| B + A -.->|"Fallback encrypted WebRTC traffic"| T["TURN server"] + T -.-> B +``` + +The signalling relay and TURN server have different roles: + +- The **signalling relay** is required for peer discovery and connection negotiation. LiveSync uses Nostr-compatible WebSocket relays for this role. The relay does not store or transfer Vault contents. +- A **TURN server** is an optional fallback. WebRTC uses it to relay the encrypted peer connection only when the devices cannot establish a direct path through their networks. + +## The project's public signalling relay + +The project author operates a public signalling relay as a best-effort convenience. Selecting **Use the project's public signalling relay** means that no signalling server needs to be provisioned for an ordinary setup. + +The public relay: + +- is not a Vault storage service; +- may observe signalling metadata, such as connection timing and network addresses; +- has no availability or log-retention guarantee; and +- can be replaced with another compatible relay at any time by updating every device in the P2P group. + +Use a signalling relay which is acceptable for your privacy and availability requirements. A controlled deployment may use its own Nostr-compatible relay. + +## Signalling relay and TURN server + +Both settings contain server addresses, but they are not interchangeable. + +| Setting | Required | Carries Vault contents | Purpose | +| --- | --- | --- | --- | +| **Signalling relay URLs** | Yes | No | Finds peers and exchanges the information needed to establish WebRTC connections. | +| **TURN server URLs** | Only when direct WebRTC connectivity fails | Encrypted WebRTC traffic | Relays traffic between peers when NAT or firewall rules prevent a direct path. | + +A TURN provider cannot read LiveSync's encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust. The project does not operate an official TURN service. + +## P2P Status + +The **P2P Status** pane is the current Obsidian interface for P2P connections. + +- After a P2P configuration exists, the command **Self-hosted LiveSync: P2P Sync : Open P2P Status** is available from the command palette. +- The P2P ribbon icon appears only after a P2P configuration exists. +- LiveSync does not open the pane merely because Obsidian has started. If the pane was already part of the saved Obsidian workspace, Obsidian may restore it. +- Workspaces containing the retired P2P pane are migrated to the current status pane. The retired command is no longer exposed. + +The active P2P remote is selected independently from the main CouchDB or Object Storage remote. Devices can therefore use P2P alongside their main remote without replacing it. + +![P2P Status on desktop](../images/p2p-setup/p2p-status-pane.png) + +![P2P Status in a mobile layout](../images/p2p-setup/p2p-status-pane-mobile.png) + +**Open connection** joins the signalling room and makes the device available for discovery. **Disconnect** leaves the LiveSync room, stops its P2P replication service, and closes the signalling connections. It does not delete the saved P2P profile. + +Every participating device must use the same signalling relay set, Group ID, and P2P passphrase. Each device should have a distinct device name. A peer which joins after another device is already connected is advertised to that device; use **Refresh**, or reconnect the device which should be discovered, if a peer is not yet listed. + +## Manual and automatic data movement + +**Replicate now** performs an explicit bidirectional synchronisation with the selected peer. This is the clearest option when proving a new configuration. + +**Announce changes** and **Follow changes** provide a more continuous experience: + +- The source device must enable **Announce changes** before it dispatches change notifications. +- A receiving device must enable **Follow changes** for that peer before it fetches in response to those notifications. +- A notification contains no Vault data. It only asks the following peer to fetch through the encrypted P2P connection. +- Missing a notification does not make an explicit later synchronisation unsafe; **Replicate now** still compares the available data. + +The peer's **More actions** menu can save these choices for that device: + +- **Synchronise when this device connects** runs one synchronisation when that named peer is discovered. +- **Follow whenever this device connects** restores following for that named peer. +- **Include in the P2P synchronisation command** includes that peer when the command for registered targets is run. + +![Persistent actions for a detected peer](../images/p2p-setup/guide-p2p-setup-peer-actions-menu.png) + +Configure these only after a manual round trip has succeeded. Device names used by persistent rules should remain unique and stable. + +## Approval and privacy + +A device must approve a peer before serving its data. Permanent approval is stored; session approval lasts only for the current Obsidian session. Check the displayed device name before approving a request. + +The encrypted Setup URI contains the shared P2P configuration but deliberately omits the device-specific name. Store the Setup URI and its passphrase separately, and generate a Setup URI for another device from a first device which has completed setup. + +## Operational limits + +- At least one device which already has the required data must be online while another device fetches it. +- P2P does not provide the continuously available central copy offered by CouchDB or Object Storage. Keep independent backups. +- Mobile operating systems may pause Obsidian in the background. Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large synchronisation. +- Changing from CouchDB to P2P is not a repair operation for a stopped CouchDB setup. Diagnose the existing transport first. diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md index 563a6d2d..529d816f 100644 --- a/docs/p2p_sync_updates_2026.md +++ b/docs/p2p_sync_updates_2026.md @@ -1,62 +1,7 @@ -# User Guide: Peer-to-Peer Synchronisation (2026 Edition) +# Peer-to-peer synchronisation -Peer-to-Peer (P2P) synchronisation has evolved significantly. This guide covers the essential setup and the new features introduced in the 2026 updates. +This address is retained for links to an earlier P2P guide. The time-specific interface description has been replaced by stable documentation: -## 1. Core Concept: Server-less Freedom -P2P synchronisation allows your devices to talk directly to each other using WebRTC. A central server is not required for data storage, ensuring maximum privacy and "freedom." - -## 2. Setting Up via P2P Status Pane -You no longer need to navigate through complex menus. Simply open the **P2P Status** (via the ribbon icon or command palette) and click the **⚙ (Cog)** icon. - -This opens the **P2P Setup** dialogue where you can configure the essentials: -- **Room ID:** A unique identifier for your synchronisation group. -- **Passphrase:** Your encryption key. Ensure all your devices use the exact same passphrase. -- **Device Name:** A recognisable name for the current device (e.g., `iphone-16`). - -Once you have saved the settings, return to the **P2P Status Pane** and click the **Connect** button to join the network. - -*Tip: You can also toggle **Auto Connect** in the setup dialogue to automatically join the network whenever Obsidian starts.* - -## 3. Real-time Control -The status pane in the right sidebar provides granular control over your synchronisation: - -- **Active P2P Remote (new):** P2P now has its own active remote selection, separate from the normal active remote for database replication. Use the combo box next to the cog icon to choose which P2P remote configuration is active for P2P features. -- **Create P2P Remote (new):** Use the **+** button to open the P2P setup dialogue and create a dedicated P2P remote configuration. This is recommended when no P2P active remote has been selected yet. -- **Selection required (new):** If no P2P active remote is selected, the pane asks for selection before P2P target-related changes are saved. - -- **Signalling Status:** Shows if you are connected to the relay (🟢 Online). -- **Live-push (Broadcast):** Toggle "Broadcast changes" to notify other peers whenever you make an edit. -- **Replicate now (🔄):** Start immediate bidirectional replication with a visible peer (Pull, then Push). -- **Watch (🔔/🔕):** Enable "Watch" on specific peers to automatically pull changes when they broadcast. This creates a "LiveSync-like" experience. -- **Sync target (🔗/⛓️‍💥):** Mark specific peers as **sync targets**. Peers marked here will be included when you run the **"P2P: Sync with targets"** command (see section 5). Click the button next to a peer to toggle it on (🔗, highlighted) or off (⛓️‍💥). This setting is persisted in your configuration. - -## 4. Replication Dialogue -If you want to synchronise with a specific peer manually, use the **Replication** command or button. This opens the **Replication Dialogue** listing available devices. - -Inside the dialogue, the **Server Status** card at the top confirms you are still connected while performing the sync. -The status card now shows a stable **Room ID suffix** above **Peer ID**. The Room ID suffix is better for identifying your P2P group, while Peer ID may change between connections. - -Two actions are available per peer: - -- **Sync** — Starts a bidirectional synchronisation (Pull then Push) and keeps the dialogue open so you can monitor progress or sync with additional peers. -- **Start Sync & Close** — Runs the same bidirectional synchronisation, waits for it to settle, then closes the dialogue. After a successful synchronisation, it also closes the signalling connection. - -On supported mobile and desktop devices, LiveSync keeps the screen awake while this peer-selection dialogue is open and while its synchronisations finish. This is intentional: display sleep can interrupt peer discovery or connection establishment and require detection to start again. Wake Lock support remains best effort, does not keep a hidden application running in the background, and does not override operating-system sleep or suspension. - -During a P2P rebuild, peer discovery and selection remain protected after the rebuild has started. Keep Obsidian visible until a peer is selected and the transfer finishes, because mobile platform restrictions can still pause or terminate a hidden application. - -## 5. Syncing with Registered Targets via Command Palette - -You can now trigger a synchronisation with all your pre-registered target peers in one step, without opening any UI. - -1. Open the **Command Palette** (`Ctrl/Cmd + P`). -2. Run **"P2P: Sync with targets"**. - -This command synchronises with every peer whose **SYNC** toggle is enabled in the **Detected Peers** list. If no targets are registered, or if the P2P server is not running, the command will notify you accordingly. - -*Tip: Pair this command with a hotkey for a quick, keyboard-driven sync workflow.* - -## 6. Technical Improvements in 2026 -- **Decoupled Architecture:** The UI is now strictly separated from the core logic, making the plug-in more stable across different platforms (Mobile, Desktop, and Web). -- **Svelte 5 UI:** The interface has been rebuilt for better responsiveness and clearer status indicators. -- **Security:** All data remains end-to-end encrypted. Even the signalling relay never sees your actual notes. +- [Set up peer-to-peer synchronisation](setup_p2p.md) for configuring the first device, generating a Setup URI for another device, approving the connection, and verifying synchronisation in both directions. +- [How peer-to-peer synchronisation works](p2p.md) for signalling, TURN, privacy, the P2P Status pane, and automatic behaviour. +- [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md) for connection troubleshooting. diff --git a/docs/quick_setup.md b/docs/quick_setup.md index f56f575a..a77b3834 100644 --- a/docs/quick_setup.md +++ b/docs/quick_setup.md @@ -2,132 +2,139 @@ [Japanese docs](./quick_setup_ja.md) - [Chinese docs](./quick_setup_cn.md). -The plug-in has so many configuration options to deal with different circumstances. However, only a few settings are required in the normal cases. Therefore, `The Setup wizard` has been implemented to simplify the setup. +This guide establishes ordinary note synchronisation on the first device and then adds another device. Optional features are configured only after this basic path works. -![](../images/quick_setup_1.png) +Before starting: -There are three methods to set up Self-hosted LiveSync. +- back up every Vault involved; +- disable Obsidian Sync, iCloud synchronisation, and any other service which writes to the same Vault; +- prepare the remote service and a Setup URI; and +- keep the Setup URI and its passphrase separate from each other. -1. [Using setup URIs](#1-using-setup-uris) *(Recommended)* -2. [Minimal setup](#2-minimal-setup) -3. [Fully manual setup and enabling on this dialogue](#3-manually-setup) +This walkthrough covers the recommended provisioned CouchDB path. Follow [Set up a CouchDB server](./setup_own_server.md) to prepare the server and Setup URI. -## At the first device +## What a Setup URI contains -### 1. Using setup URIs +A Setup URI starts with `obsidian://setuplivesync?settings=`. It contains encrypted connection settings, including credentials, and must be protected even though it is encrypted. -> [!TIP] -> What is the setup URI? Why is it required? -> The setup URI is the encrypted representation of Self-hosted LiveSync configuration as a URI. This starts `obsidian://setuplivesync?settings=`. This is encrypted with a passphrase, so that it can be shared relatively securely between devices. It is a bit long, but it is one line. This allows a series of settings to be set at once without any inconsistencies. -> -> If you have configured the remote database by [Automated setup on Fly.io](./setup_flyio.md#a-very-automated-setup) or [set up your server with the tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server), **you should have one of them** +The Setup URI passphrase decrypts the URI. It is different from the Vault encryption passphrase which protects synchronised data. Store both securely, and do not send the Setup URI and its passphrase through the same channel. -In this procedure, [this video](https://youtu.be/7sa_I1832Xc?t=146) may help us. +## Set up the first device -1. Click the `Use` button (or launch the `Use the copied setup URI (Formerly Open setup URI)` command from the command palette). -2. Paste the Setup URI into the dialogue -3. Type the passphrase of the Setup URI -4. Answer `yes` for `Importing LiveSync's conf, OK?`. -5. Answer `Set it up as secondary or subsequent device` for `How would you like to set it up?`. -6. Initialisation will begin, please hold a while. -7. You will asked about the hidden file synchronisation, answer as you like. - 1. If you are new to Self-hosted LiveSync, we can configure it later so leave it once. -8. Synchronisation has been started! `Reload app without saving` is recommended after the indicators of Self-hosted LiveSync disappear. +Use this path only when the remote database is new, or when this device is intentionally the source of truth for a full server rebuild. -OK, we can proceed the [next step](#). +1. Install and enable Self-hosted LiveSync in the intended Vault. +2. Select the `Welcome to Self-hosted LiveSync` Notice to open onboarding. +3. Select `I am setting this up for the first time`, then confirm that you want to set up a new synchronisation. +4. On `Connection Method`, select `Use a Setup URI (Recommended)`. +5. Paste the Setup URI, enter its Setup URI passphrase, and select `Test Settings and Continue`. -### 2. Minimal setup + ![Encrypted Setup URI and masked passphrase](../images/quick-setup/guide-quick-setup-first-setup-uri.png) -If you do not have any setup URI, Press the `start` button. The setting dialogue turns into the wizard mode and will display only minimal items. +6. Review `Setup Complete: Preparing to Initialise Server`, then select `Restart and Initialise Server`. ->[!TIP] -> We can generate the setup URI with the tool in any time. Please use [this tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server). + ![First-device server initialisation confirmation](../images/quick-setup/guide-quick-setup-first-initialise.png) -![](../images/quick_setup_2.png) +7. Read the final overwrite warning carefully. Select `I Understand, Overwrite Server` only after checking that backups exist and that replacing the remote data is intended. + ![Final server overwrite warning](../images/quick-setup/guide-quick-setup-first-rebuild-confirmation.png) -#### Select the remote type +8. A newly provisioned database may show `Fetch Remote Configuration Failed` because it does not contain a saved preferred configuration yet. If this is a genuinely new setup, select `Skip and proceed`. Otherwise, stop and investigate before continuing. -1. Select the Remote Type from dropdown list. -We now have a choice between CouchDB (and its compatibles) and object storage (MinIO, S3, R2). CouchDB is the first choice and is also recommended. And supporting Object Storage is an experimental feature. + ![Expected missing remote configuration choice for a new database](../images/quick-setup/guide-quick-setup-missing-remote-configuration.png) -#### Remote configuration +9. Acknowledge `All optional features are disabled`. Optional features remain off until the ordinary synchronisation path has been verified. +10. Allow initialisation and any requested restart to finish. Keep Obsidian open until the LiveSync progress indicators have cleared. -##### CouchDB +Create an ordinary test note and allow it to upload before adding another device. -Enter the information for the database we have set up. +## Create a Setup URI for another device -![](../images/quick_setup_3.png) +Generate a Setup URI for another device from the working first device. This captures the settings which that device is actually using, rather than asking another device to reuse the Setup URI produced during server provisioning. -##### Object Storage +1. Open the Obsidian command palette on the first device. +2. Run `Self-hosted LiveSync: Copy settings as a new Setup URI`. +3. Enter a new passphrase which will protect this Setup URI, then select `OK`. -1. Enter the information for the S3 API and bucket. + ![Masked passphrase for a new Setup URI for another device](../images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png) -![](../images/quick_setup_3b.png) +4. Copy the resulting Setup URI, then select `OK`. -Note 1: if you use S3, you can leave the Endpoint URL empty. -Note 2: if your Object Storage cannot configure the CORS setting fully, you may able to connect to the server by enabling the `Use Custom HTTP Handler` toggle. + ![Setup URI generated by the working first device](../images/quick-setup/guide-quick-setup-copy-setup-uri-result.png) -2. Press `Test` of `Test Connection` once and ensure you can connect to the Object Storage. +Store the new Setup URI and its passphrase separately. The URI is encrypted, but it contains credentials and Vault settings, so continue to protect it. -#### Only CouchDB: Test database connection and Check database configuration +## Add another device -We can check the connectivity to the database, and the database settings. +Start with a new or separately backed-up Vault. Do not use a production Vault containing unsynchronised notes unless you have reviewed the [Fast Setup choices](./tips/fast-setup.md). -![](../images/quick_setup_5.png) +1. Install and enable Self-hosted LiveSync. +2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice. +3. Select `I am adding a device to an existing synchronisation setup`, then confirm that you want to add the device. +4. On `Device Setup Method`, select `Use a Setup URI (Recommended)`. +5. Paste the new Setup URI generated by the first device, enter its Setup URI passphrase, and select `Test Settings and Continue`. +6. Review `Setup Complete: Preparing to Fetch Synchronisation Data`, then select `Restart and Fetch Data`. -#### Only CouchDB: Check and Fix database configuration + ![Fetch confirmation on the additional device](../images/quick-setup/guide-quick-setup-second-fetch.png) -Check the database settings and fix any problems on the spot. +7. For a new or empty Vault, select `Overwrite all with remote files`. For a Vault with local work, stop and choose the appropriate strategy from the [Fast Setup guide](./tips/fast-setup.md). -![](../images/quick_setup_6.png) + ![Fast Setup data retrieval choices](../images/quick-setup/guide-quick-setup-retrieval-method.png) -This item may vary depending on the connection. In the above case, press all three Fix buttons. -If the Fix buttons disappear and all become check marks, we are done. +8. When asked how to handle extra local files, the conservative choice is `Keep local files even if not on remote`. Select the delete option only when the local Vault is disposable and an exact remote copy is intended. -#### Confidentiality configuration (Optional but very preferred) + ![Local file policy on the additional device](../images/quick-setup/guide-quick-setup-local-file-policy.png) -![](../images/quick_setup_4.png) +9. Allow retrieval, file reflection, and any requested restart to finish. Keep Obsidian open until the LiveSync progress indicators have cleared. -Enable End-to-end encryption and the contents of your notes will be encrypted at the moment it leaves the device. We strongly recommend enabling it. And `Path Obfuscation` also obfuscates filenames. Now stable and recommended. +Confirm that the ordinary test note from the first device appears unchanged. Then edit or create a second ordinary note on the new device, and confirm that it reaches the first device. -These setting can be disabled if you are inside a closed network and it is clear that you will not be accessed by third parties. +![Ordinary note received through the provisioned Setup URI](../images/quick-setup/guide-quick-setup-synchronised-note.png) -> [!TIP] -> Encryption is based on 256-bit AES-GCM. +## After ordinary synchronisation works -We should proceed to the Next step. +Add optional features separately so that their ownership and initialisation direction are explicit: -#### Sync Settings -Finally, finish the wizard by selecting a preset for synchronisation. +- [Hidden File Sync](./tips/hidden-file-sync.md) for selected hidden files and folders; or +- [Customisation Sync](./settings.md#6-customisation-sync-advanced) for managed Obsidian customisations. -Note: If you are going to use Object Storage, you cannot select `LiveSync`. +Do not enable both features for the same files. -![](../images/quick_setup_9_1.png) +## Configure CouchDB manually on the first device -Select any synchronisation methods we want to use and `Apply`. If database initialisation is required, it will be performed at this time. When `All done!` is displayed, we are ready to synchronise. +Use this path when CouchDB is ready but a Setup URI is unavailable. It configures one first device through the visible onboarding dialogue; it does not provision or repair the CouchDB server. Add later devices with a Setup URI generated by this working first device instead of entering the credentials again. -The dialogue of `Copy current settings as a new setup URI` will open automatically. Please input a passphrase to encrypt the new `Setup URI`. (This passphrase is to encrypt the setup URI, not the vault). +1. Install and enable Self-hosted LiveSync in the intended Vault. +2. Select the `Welcome to Self-hosted LiveSync` Notice, choose `I am setting this up for the first time`, then confirm that you want to set up a new synchronisation. +3. On `Connection Method`, select `Configure a remote manually`, then select `Proceed with manual configuration`. -![](../images/quick_setup_10.png) + ![Manual remote configuration option during onboarding](../images/couchdb-manual/guide-couchdb-manual-connection-method.png) -The Setup URI will be copied to the clipboard, please make a note(Not in Obsidian) of this. +4. On `End-to-End Encryption`, decide how the synchronised data will be protected. + - For an ordinary new Vault, enable `End-to-End Encryption` and enter a strong Vault encryption passphrase. + - Enable `Obfuscate Properties` if remote document properties should also be concealed. + - Store the Vault encryption passphrase securely. It is separate from the passphrase used to protect a Setup URI. ->[!TIP] -We can copy this at any time by running the "Copy settings as a new setup URI" command from the command palette (or clicking the "Copy the current settings to a Setup URI" button in the settings UI). + ![CouchDB Vault encryption settings with the passphrase masked](../images/couchdb-manual/guide-couchdb-manual-encryption.png) -### 3. Manually setup +5. On `Choose a synchronisation remote`, select `CouchDB`, then select `Continue to CouchDB setup`. -It is strongly recommended to perform a "minimal set-up" first and set up the other contents after making sure has been synchronised. + ![CouchDB option in the list of synchronisation remotes](../images/couchdb-manual/guide-couchdb-manual-remote-selection.png) -However, if you have some specific reasons to configure it manually, please click the `Enable` button of `Enable LiveSync on this device as the set-up was completed manually`. -And, please copy the setup URI by running the "Copy settings as a new setup URI" command (or using the "Copy the current settings to a Setup URI" button) and make a note(Not in Obsidian) of this. +6. Enter the complete CouchDB URL, username, password, and database name. + - Obsidian Mobile requires HTTPS. Plain HTTP is suitable only for a trusted local connection from a desktop device. + - Use credentials which are allowed to connect to the selected database and, when configuring the first device, create it if it does not exist. -## At the subsequent device -After installing Self-hosted LiveSync on the first device, we should have a setup URI. **The first choice is to use it**. Please share it with the device you want to setup. + ![Manual CouchDB connection fields with the password masked](../images/couchdb-manual/guide-couchdb-manual-connection-details.png) -It is completely same as [Using setup URIs on the first device](#1-using-setup-uris). Please refer it. +7. `Check server requirements` is optional. It sends the displayed credentials to the configured server through Obsidian's internal request API, and some checks require CouchDB administrator access. The initial check is read-only. If it offers a server change, review and confirm that individual change separately. -> [!TIP] -> **Fast Setup (Simple Fetch)** -> In recent versions, when you import a Setup URI or trigger a Fetch All, the plug-in boots in scheduled fetch mode and runs a simplified **Fast Setup** process. This allows you to choose your sync strategy with a single dialogue and performs initial synchronisation in one step. Refer to the [Fast Setup Guide](./tips/fast-setup.md) for more details. + ![Successful optional CouchDB server requirements check](../images/couchdb-manual/guide-couchdb-manual-server-requirements.png) + +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. +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. diff --git a/docs/recovery.md b/docs/recovery.md new file mode 100644 index 00000000..cb23d6fe --- /dev/null +++ b/docs/recovery.md @@ -0,0 +1,120 @@ +# Recovery and flag files + +This guide covers emergency suspension, local database recovery, and deliberate remote reconstruction. These operations are not ordinary synchronisation. + +> [!IMPORTANT] +> Back up every available Vault before recovery. If a central remote is involved, back up that database or bucket as well. Stop or suspend other LiveSync devices until you have chosen the authoritative copy. + +If Obsidian will not start normally, do not give up. Flag files can be created or removed with the operating system's file manager while Obsidian is closed. They are the only supported way to intervene before the ordinary LiveSync boot-up sequence reaches its database and synchronisation work. + +## First choose the authoritative copy + +Use the least destructive operation which matches the evidence: + +- If the correct data is uncertain, suspend all work with `redflag.md`, preserve every copy, and inspect them before proceeding. +- If the central remote is healthy and should win, use **Reset Synchronisation on This Device** or `flag_fetch.md`. +- If this device's Vault is healthy and should replace a damaged or unwanted central remote, use **Overwrite Server Data with This Device's Files** or `flag_rebuild.md`. +- If both the Vault and local database are healthy and the only concern is unused storage, Garbage Collection may be appropriate. It does not repair a damaged database. + +Do not switch transport, enable P2P, or run Garbage Collection as a substitute for diagnosing a stopped CouchDB or Object Storage setup. + +## Suspend before diagnosis + +Close Obsidian completely, then create an empty file or directory named `redflag.md` at the root of the Vault. On the next start, LiveSync enters its emergency suspension state before ordinary database, file-watching, and synchronisation work continues. + +While suspended: + +1. Back up the Vault and any available remote data. +2. Check which device or remote contains the intended files. +3. Correct only the identified configuration or storage problem. +4. Remove `redflag.md`. +5. Start Obsidian and review the remaining suspension controls under `Hatch` -> `Scram Switches`. + +The flag deliberately enables file logging, which may affect performance. Remove it after the emergency has been understood. + +## Recover a conflicted or mismatched file + +Use this workflow when one file, or a small number of known files, has conflicts, missing chunks, or a difference between the current Vault file and the local LiveSync database. The inspection is device-local: it does not query a remote database or prove that another device has the same chunks. + +The `Hatch` recovery controls are ordered by escalation. Running **Recreate chunks for current Vault files** again with unchanged chunk settings and file contents produces the same chunks, and does not alter the revision tree. **Inspect conflicts and file/database differences** then provides actions for exact revisions. **Resolve All conflicted files by the newer one** is last because it applies a modification-time policy in bulk and logically deletes every other live version. + +1. Stop editing the affected file, pause replication on the participating devices, and keep a separate copy of every readable version. +2. If another device or backup has the intended content, preserve that copy before changing any revision. +3. If the current Vault file is readable, select **Recreate current chunks**. This can restore only chunks derived from the current Vault contents; it cannot reconstruct unique bytes from an unavailable historical or conflict revision. +4. Select **Inspect conflicts and file/database differences** → **Begin inspection**. +5. Review the database winner, every conflict revision, and any unavailable shared ancestor separately. Revision identifiers, `Δsize`, `Δtime`, and chunk availability are diagnostic evidence; they do not decide which content is correct. +6. Use the wrench menu on the exact revision: + - **Compare with Vault** opens a read-only comparison for readable text. + - **Apply this revision to Vault** replaces the Vault file with that readable database revision. + - **Mark this revision as the Vault version** records an exact byte-for-byte match without creating a child revision. + - **Store Vault file as a child of this revision** preserves the current Vault bytes on that selected branch. + - **Retry reading revision** retries configured chunk retrieval without changing the revision tree. + - **Apply logical deletion to Vault**, **Discard this branch**, and **Discard unreadable revision** are destructive decisions. Use them only after preserving every version which may still be needed. +7. Synchronise the healthy source if chunks were restored, scan again, and confirm that the expected conflict or difference has disappeared before resuming ordinary editing. + +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). + +## 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. + +The readable flag is `flag_fetch.md`; the legacy name `redflag3.md` remains accepted. + +On the next start, LiveSync: + +1. pauses ordinary start-up work; +2. asks which remote to use when more than one remote profile exists; +3. asks how to treat existing Vault files; +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. + +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. + +## Overwrite server data with this device's files + +Use this only when this device's Vault is the authoritative copy and the central remote should be reconstructed from it. + +The readable flag is `flag_rebuild.md`; the legacy name `redflag2.md` remains accepted. + +For CouchDB and Object Storage, this is destructive to the selected remote state. Other devices may still contain revisions or files which are not present in the authoritative Vault, so keep them stopped until the new remote has been verified and then reset them from that remote. + +For a P2P-only setup, there is no central remote database to overwrite. Preparing the first device instead rebuilds its local LiveSync database from its Vault. + +## Garbage Collection is not Rebuild + +Garbage Collection removes unreferenced chunks while preserving the current database and its revision model. Use it only when: + +- the Vault is healthy; +- the local LiveSync database is healthy; +- all relevant devices have synchronised; and +- the remaining historical and deletion state is understood. + +Deleted documents, tombstones, live conflicts, and retained metadata are not free. Live conflict branches keep the chunks needed for review, while an ordinary superseded linear revision does not protect its former chunks. Garbage Collection can therefore make old content unreadable and cannot promise the smallest possible remote. Review the [Garbage Collection V3 specification](specs_garbage_collection.md) before using it. + +Rebuild is a different operation. It reconstructs the database from a chosen authoritative state and is the more certain way to remove unwanted history or repair a damaged remote, but it is also more disruptive and can discard changes which exist only elsewhere. + +## Flag-file reference + +Create only the flag required for the chosen operation. + +| File at the Vault root | Effect | +| --- | --- | +| `redflag.md` | Suspend ordinary LiveSync work for diagnosis. It remains until removed manually. | +| `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. + +## When the warning continues + +If LiveSync still reports emergency suspension after a recovery dialogue has closed: + +1. close Obsidian completely; +2. inspect the Vault root for every name in the table above; +3. remove only flags whose intended operation has finished or been abandoned; +4. restart Obsidian; and +5. check `Hatch` -> `Scram Switches` for remaining suspended file watching or database reflection. + +If the intended authoritative copy is still uncertain, leave synchronisation suspended and collect a [full report](troubleshooting.md#collect-a-report) before changing the databases again. diff --git a/docs/releases/0.25.md b/docs/releases/0.25.md new file mode 100644 index 00000000..4b01dff0 --- /dev/null +++ b/docs/releases/0.25.md @@ -0,0 +1,1826 @@ +# 0.25 release history + +This is the 0.25 release line of the [Self-hosted LiveSync release history](../../updates.md). Earlier releases continue in the [legacy history](legacy.md). + +## 0.25.83 + +16th July, 2026 + +Our plug-in continues to improve every day thanks to all of your contributions. We have finally resolved an issue first reported in 2023. Thank you for everything you contribute. + +### Fixed + +- Fixed the 📲 remote-activity indicator remaining visible after CouchDB requests had completed. +- Fixed missing chunks being reported unavailable while an in-progress on-demand fetch or finite replication could still deliver them. Reads now follow the actual delivery lifecycle and recheck the local database when it finishes. +- Fixed an issue where changing only the letter case of a file name within the same directory could delete it on other devices when 'Handle files as Case-Sensitive' was disabled. Directory case changes remain unsupported (#198, PR #1014; [commonlib PR #68](https://github.com/vrtmrz/livesync-commonlib/pull/68)). Thank you to @metrovoc for the fix! +- Fixed **Resolve All conflicted files by the newer one** displaying a separate success notice for every resolved file and updating its progress notice for nine out of every ten checked files. Successful per-file results are now logged, progress updates every ten files, and errors and non-bulk success notices remain visible (#1016, PR #1017). Thank you to @apple-ouyang for the fix! + +### Improved + +- Split the status-bar remote activity display into `📲` for a finite remote operation and `🌐N` for the approximate number of tracked CouchDB or Object Storage requests currently in progress. + +## 0.25.82 + +15th July, 2026 + +Recently, I created a repository called Fancy Kit and have been trying to build some proper infrastructure around it. Does any of this look like cumbersome bureaucracy? No need to worry: you can carry on as usual. Codex is simply tidying up my usual rambling prose, terrible pull requests, and disjointed remarks. + +### Fixed + +- Refreshed the remote Security Seed before each replication, preventing a client that remained open during a remote database rebuild from uploading data encrypted with the previous seed (#1018, PR #1019). Thank you to @apple-ouyang for the fix! +- The P2P **Start Sync & Close** action now waits for synchronisation to settle before closing the dialogue, avoiding premature release of screen-awake protection while work remains in flight. + +### Improved + +- On supported mobile and desktop devices, one-shot replication, P2P peer discovery and selection, database rebuild and fetch operations, and remote chunk fetching now keep the screen awake for the duration of these finite remote operations. LiveSync also postpones its normal visibility suspension until the operation finishes, although platform restrictions may still pause or terminate background work. +- The 📲 status-bar indicator now covers finite remote work as well as HTTP requests. It reports broad remote activity, such as P2P peer selection and remote chunk fetching, rather than an exact physical connection count. + +### Improved (CLI) + +- CLI container images are now published for both AMD64 and ARM64 platforms. + +### Testing + +- Added a local real-Obsidian compatibility test that writes an encrypted, path-obfuscated note through the LiveSync CLI and verifies that the plug-in materialises the same content. + +## 0.25.81 + +14th July, 2026 + +I am releasing this update to make the chunk-boundary fix available before the next broader release. Thank you to everyone who helped reproduce, trace, and verify the issue. + +### Fixed + +- Fixed an issue where a U+FEFF character at a Rabin–Karp chunk boundary could be lost, changing the reconstructed file and causing repeated size-mismatch conflicts (#1000). + +### Improved + +- Improved vault scanning and CLI file filtering by reusing compiled ignore patterns, reducing processing overhead for vaults with many files or ignore rules (#1006, #1007, and #1008). + +### Improved (CLI and Webapp) + +- Rooted storage adapters now reject absolute, drive-qualified, backslash-separated, and traversal paths. They also prevent file writes, appends, and removal from targeting the configured root itself. +- File System Access API storage can now create files below previously missing parent directories, matching the existing Node behaviour. + +### Testing + +- Added shared Node and File System Access API storage contract coverage for metadata, text and binary operations, append, listing, removal, path containment, and empty-root handling. +- Added a Compose-based P2P end-to-end smoke test and repeatable network benchmark cases for local performance investigations. + +### Miscellaneous + +- Split the internal storage adapter contract into focused capability views without changing existing runtime behaviour. + +## 0.25.80 + +7th July, 2026 + +### Fixed + +- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993). + - Behaviour change: + - When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result. + - When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side. +- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994). + - Behaviour change: + - Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text. +- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992). + - Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled. + +## 0.25.79 + +29th June, 2026 + +### Fixed + +- Fast Fetch now retries transient stream interruptions and resumes from the latest persisted checkpoint, instead of starting over after ordinary network or platform interruptions (#977, PR #978; commonlib PR #59). Thank you so much for @apple-ouyang for the fix! +- Simple Fetch now remembers the selected setup choices while an interrupted Fetch All operation is still pending, so users are not asked the same questions again on retry (#977, PR #978). Thank you so much for @apple-ouyang for the fix! +- No longer hidden storage events, such as `.git` paths, reach the normal target-file filter when internal file synchronisation is disabled. This avoids noisy non-target logs before those files are skipped (commonlib PR #60). Thank you so much for @apple-ouyang for the fix! +- Fixed an issue where a file deleted from storage could be resurrected by the offline scanner because the database tombstone was not written when the storage file was already gone (commonlib PR #56). Thank you so much for @cosmic-fire-eng for the fix! + +### Improved + +- Local database maintenance commands now ask before applying the required chunk settings, and can apply those prerequisites before continuing (#980, PR #981). Thank you so much for @apple-ouyang for the improvement! +- Improved CouchDB replication event handling by using the new `StreamInbox` helper from `octagonal-wheels` (commonlib PR #62). + +### Documentation + +- Added `nginx` to the setup documentation table of contents (PR #976). Thank you so much for @kiraventom for the improvement! + +### Miscellaneous + +- Updated `octagonal-wheels` to `0.1.47` across the plug-in and workspace packages to use the newly published helper modules. + +## 0.25.78 + +23rd June, 2026 + +### Fixed +- No longer fast synchronisation (a.k.a. Fast Fetch) causes a rewind and re-fetch of the entire database when some errors occur during the process (#972, PR #973). Thank you so much for @apple-ouyang for the fix! + +### Improved + +- Overhauled the Object Storage (e.g., MinIO and S3) replication engine ('Journal Replicator 2nd Edition'). + - It now leverages the standard Web Streams API for a resilient, backpressure-aware architecture, reducing memory footprints/temporary storage usage on large vaults. + - Decoupled the physical storage logic to make it easier to add new storage backends in the future. + - Stricter compliance with CouchDB's replication protocol (proper `_revisions` transfers with `new_edits: false`) when using Object Storage. + +### Testing + - Added comprehensive unit tests for the new `JournalSyncCore` engine, covering streams, backpressure, and `new_edits: false` validation. + - Improved integration test workflows in the CI pipeline to run MinIO tests automatically using standard environment variables. + +## 0.25.77 + +19th June, 2026 + +This update is mostly meaningless for users. But for maintainers, not, I hope. I wonder if I were done well in the start, there would be no hassles. It really was a great opportunity. + +Also, this update is a very large one, even if we had a lot of time, and we had CI tests, and mostly only fixing the types. Please let me know if you find any issues! + +### Improved + +- File deletion now respects the user's deletion preferences (by utilising the `FileManager.trashFile` API) on Obsidian v1.7.2 or newer, regardless of the plug-in's internal trashbin setting. + +### Miscellaneous +- Typings of the library are now included +- Many typing errors have been improved. +- Import paths have been normalised to be relative to the root and to the `lib/src` directory, to avoid breaking the boundary between the library and the plug-in. +- Subprojects, such as the CLI and the webapp, are now in the workspace. + +## 0.25.76 + +15th June, 2026 + +### Fixed + +- Now the S3 connection with custom headers works properly (#875). + - Previously, custom headers injected for proxy authentication were incorrectly included in the AWS Signature v4 calculation. This led to a '400 Bad Request' error (such as 'signed header is not present') on strict S3 backends (for example, Garage), or when reverse proxies modified, renamed, or stripped these headers before they reached the storage service. +- No longer connection information of the P2P synchronisation is broken on the specific platform (#956). + +## 0.25.75 + +13th June, 2026 + +### Fixed + +- Fixed an issue where using fast synchronisation caused a TypeError in some environments (#953). + +### New features +- Now we can configure to keep replication active in the background on desktop platforms (#939, PR #949). Thank you so much for @migsferro! + +### Fixed (CLI, automated) + +- Fixed an issue where the mirror command could fail to apply updates when conflict preservation checks prevented overwriting unsynchronised local changes, even when the `force` parameter or `writeDocumentsIfConflicted` setting was enabled. + +### Improved + +- (CLI) Ported the remaining bash regression tests (`test-daemon-linux.sh`, `test-decoupled-vault-linux.sh`, and `test-remote-commands-linux.sh`) to Deno for cross-platform validation. + +### Miscellaneous +- Some dependencies have been updated. +- Now we check the compatibility with iOS 15 in the CI tests to ensure the plugin continues to work on older iOS versions even after we upgrade some dependencies. + +## 0.25.74 + +8th June, 2026 + +### Fixed + +- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan (#941). +- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911). +- The startup sequence now saves the state more effectively (Thank you so much for @bmcyver)! + +## Only CLI + +8th June, 2026 + +I should also consider the version numbering for the CLI... + +### Improved + +- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`. +- --vault option is now available for daemon and mirror commands! (Thank you so much for @starskyzheng)! +- Decoupled the database directory path from the actual vault directory path using the `--vault` (or `-V`) option. + +### Fixed (preventive) + +- Validated that the specified vault path exists and is indeed a directory before starting the CLI. +- Integrated path resolution and validations for one-off commands (such as `'push'`, `'pull'`, `'cat'`, `'rm'`, `'info'`, and `'resolve'`) against the decoupled vault path instead of the database path. + +## 0.25.73 + +4th June, 2026 + +### Fixed + +- Adjust CouchDB's database name checking to its specification (#926). +- `Reset Syncronisation on This Device` for minio and P2P is now working properly. + +## ~~0.25.71~~ 0.25.72 + +0.25.71 was cancelled due to the fixes needed (Object Storage related) + +3rd June, 2026 + +### Improved + +- Database fetching (a.k.a. Reset Synchronisation on This Device) on the initialisation now supports streaming and is faster (CouchDB only) +- The database fetching process has been streamlined, and database operations are now suspended until it has been completed +- The initial synchronisation process has been simplified, making it easier to synchronise files with the remote server +- We can select the remote database to fetch from during the initialisation, when there are multiple remote databases configured (e.g. multiple CouchDBs or S3 remotes) +- Hebrew (he) Translation has been added (Thank you so much, @MusiCode1)! +- Translation loading time has been reduced (Thank you so much, @bmcyver)! + +### Fixed + +- No longer does the status element break other plugins' interaction (#930). +- No longer does file events occured during initial database fetching using Object Storage. + +### Refactored + +To support the new Community automated tests, we fixed numerous lint warnings. This may have also resolved potential issues. + +## 0.25.70 + +25th May, 2026 + +### New features +- Diff dialogue now has great tools to navigate and understand the differences, including: + - A checkbox to toggle the visibility of collapsed identical sections, making it easier to focus on the actual differences (PR #889). + - A search feature to find specific text in past revisions, and navigate revisions with search results highlighted in the dialogue (PR #890). + +- Conflict resolution dialogue now has a navigation feature to jump between conflicts (PR #891). + +Thank you so much to @SeleiXi for implementing these features! + +### Improved + +- More diagnostic information for P2P connections is now shown, including why a connection failure occurred and the current connection status. + +## 0.25.69 + +22nd May, 2026 + +### Fixed +- No longer does the P2P passphrase mismatch cause a server shutdown. +- Settings related to P2P synchronisation are now correctly applied on start-up and no longer reverted. + +### New features +- Diagnostic P2P connection stats are now available. + - These stats indicate the number of connection trials, successes, and failures. + +## 0.25.68 + +22nd May, 2026 + +### Improved + +- P2P connections have improved slightly + - Upgrade to `trystero` v0.24.0, and fixes event handler assignment. This should fix some edge cases where P2P connections fail to establish or messages are not properly handled. + - Weaken terser options to avoid potential issues with minification that could cause runtime errors in some environments. + +## ~~0.25.66~~ 0.25.67 + +20th May, 2026 + +0.25.66 had a bug that the auto-accept logic for compatible but lossy mismatches was not working as intended. + +### New features +- Implement an auto-accept compatible tweak setting and enhance the mismatch resolution logic. + +### Improved +- Many messages related to tweak mismatch resolution have been updated for clarity. + +## 0.25.65 + +19th May, 2026 + +### Fixed +- Fix an issue about resuming from background on iOS (#888). +- Now Chunk Splitter: `V3: Fine Deduplication` is working fine again (#866). + - It has some drawbacks, such as fewer chunks are generated. However, it makes less transfer and storage when the files are modified but not completely changed. +- Unsynchronised local changes (which means changes that have not been sent) are now correctly preserved as a conflict (Thank you so much for @SeleiXi!). +- Avoid creating a new revision when the current and conflicted revisions have identical content (Thank you so much for @daichi-629). + +### Improved +- Improved the error verbosity on concurrent processing during the start-up process. +- Now the `report` includes recent logs (of verbosity `verbose` even settings is not set to `verbose`). +- Updating logs is now debounced to avoid excessive updates during rapid log generation. +- Added a `Generate full report for opening the issue with debug info` command to the command palette, which generates a report without opening the settings dialogue. + +## 0.25.64 + +17th May, 2026 + +### P2P Status Pane + +- Added active P2P remote selector (combo box) and `+` action to create/select a P2P remote from the P2P setup dialogue. +- Added per-peer immediate replication action on accepted peers. +- Updated status control icons for clarity: + - Replicate now: `🔄` (`⏳` while running) + - Watch: `🔔` / `🔕` + - Sync target: `🔗` / `⛓️‍💥` +- Added warning state when no active P2P remote is selected. + +### P2P Status Card + +- Added stable Room ID suffix display and placed it above Peer ID for better identification. + +### Non behavioural internal changes + +#### P2P + +- Added `P2P_ActiveRemoteConfigurationId` as a dedicated active remote selection for P2P features, separate from the normal active remote. +- Added activation logic for P2P dedicated remote configuration that reflects P2P settings while keeping `remoteType` unchanged. +- Added migration support to carry over P2P active remote selection when appropriate. +- Added shared Room ID utility functions and applied them across P2P setup and P2P panes. + +#### Tests + +- Added/updated unit test coverage around settings load behaviour for P2P active remote application. + +## 0.25.63 + +17th May, 2026 + +### Fixed +- The issue which cannot synchronise in Only-P2P mode has been fixed. +- Fixed an issue where "Failed to connect to the remote server" was shown during the redFlag rebuild flow when P2P was the primary remote type. Remote configuration fetch is now skipped for P2P. + +### P2P Replication UI Improvements +- Brand-new P2P Server Status pane has been added to provide real-time visibility into your connection status and peer network. + - For detailed instructions on using the new P2P features, please refer to the updated [User Guide: Peer-to-Peer Synchronisation (2026 Edition)](./docs/p2p_sync_updates_2026.md). +- Now `Replicate` button or ribbon icon opens a redesigned interactive replication dialogue that performs smart bidirectional sync with a single click. +- The vault rebuild flow (`replicateAllFromServer`) now opens the redesigned P2P Replication modal instead of a plain text selection dialogue, providing a consistent UI experience. + +## 0.25.62 + +14th May, 2026 + +### Fixed + +- Fixed an issue where a connection could not be established when attempting to connect to a brand-new remote database without going through the set-up wizard or configuration checking (#660). + +## 0.25.61 + +13th May, 2026 + +Reviews have started on the Obsidian Community, haven't they? It was quite a struggle, what with having to fix the outdated ESLint. +I am a bit nervous, but it is far better than just plodding along aimlessly, so let us get on with it. If you spot any issues, please let me know straight away. + +From now on, I am avoiding committing directly to the main branch. This is because you lots have all been sending so much PRs. I wanted to keep things harmonious. +That said, I am still not used to rebasing, so there are some parts where the commit history is a right mess. I will work on improving that. + +### Improved + +- P2P synchronisation has been made more robust + Now the foundation for P2P synchronisation has been rewritten, and the unit tests have been added. The foundation has been separated into the transport layer, signalling-and-connection layer, and, an RPC layers. And each layer has been unit-tested. As the result, the P2P synchronisation now uses the robust shim that uses RPC-ed PouchDB synchronisation in contrast to previous implementation. +This P2P synchronisation is not compatible with previous versions in terms of connectivity. All devices must be updated. + +### Fixed + +- No longer baffling errors occur when setting-update is triggered during the early stage of initialisation. +- Network error notice pop-ups are now suppressed when 'NetworkWarningStyle' is set to 'Hidden'. (Thank you so much @SeleiXi!) + +### New features + +- Diff navigation buttons have been added to the diff view, making it easier to move between differences. (Thank you so much @SeleiXi! #871) + +### Translations + +- Chinese (Simplified) translations for settings and the Setup Wizard have been added. (Thank you so much @zombiek731!) +- Common UI controls and signal words are now localised into Chinese (Simplified). (Thank you so much @zombiek731!) +- i18n runtime behaviour and locale coverage have been improved. (Thank you so much @52sanmao!) + +### CLI + +#### New features + +- Daemon synchronisation is now supported. (Thank you so much @andrewleech! #843) +- `HeadlessConfirm` has been implemented with sensible defaults, enabling unattended operation in headless environments. (Thank you so much @andrewleech!) +- The CLI onboarding experience has been improved. (Thank you so much @OriBoharon! #872) + +#### Fixed + +- Sub-millisecond CLI mtimes are now truncated to prevent mobile crash. (Thank you so much @brian-spackman! #893) + +## 0.25.60 + +29th April, 2026 + +### Fixed + +- Now larger settings can be exported and imported via QR code without issues. (#595) + - When the settings data exceeds the QR code capacity, it is now split into multiple QR codes. + - These QR codes are reassembled by the aggregator page, which collects the split data and reconstructs the original settings. + - Aggregator page is available at `https://vrtmrz.github.io/obsidian-livesync/aggregator.html`, and this file is also included in the repository. + - We will not send the settings data to any server. The QR code data is generated and processed entirely on the client side, ensuring that your settings remain private and secure. HOWEVER, please be careful your network environment. +- Fixed some errors during serialisation and deserialisation of the settings, which caused issues in some cases when importing/exporting settings via QR code. + +### Fixed (CLI) + +- `ls` and `mirror` commands now provide informative feedback when no documents are found or filters skip all files, resolving the issue where they would exit silently (#860). + - Improved the clarity of CLI command logs by including the total count of processed items. +- The command-line argument `vault` has been renamed to a more appropriate name, `databaseDir`. +- The `mirror` command now accepts a `vault` directory, which specifies the location where the actual files are stored. For compatibility reasons, the previous behaviour is still supported. + +## 0.25.59 + +### Fixed + +- No longer Setup-wizard drops username and password silently. (#865) + - Thank you so much for @koteitan ! +- Setup URI is now correctly imported (#859). + - Also thank you so much for @koteitan ! + +### Improved + +- now French translation is added by @foXaCe ! Thank you so much! + +## 0.25.58 + +### Fixed + +- No longer credentials are broken during object storage configuration (related: #852). +- Fixed a worker-side recursion issue that could raise `Maximum call stack size exceeded` during chunk splitting (related: #855). +- Improved background worker crash cleanup so pending split/encryption tasks are released cleanly instead of being left in a waiting state (related: #855). +- On start-up, the selected remote configuration is now applied to runtime connection fields as well, reducing intermittent authentication failures caused by stale runtime settings (related: #855). +- Issue report generation now redacts `remoteConfigurations` connection strings and keeps only the scheme (e.g. `sls+https://`), so credentials are not exposed in reports. +- Hidden file JSON conflicts no longer keep re-opening and dismissing the merge dialogue before we can act, which fixes persistent unresolvable `data.json` conflicts in plug-in settings sync (related: #850). + +## 0.25.57 + +9th April, 2026 + +- Packing a batch during the journal sync now continues even if the batch contains no items to upload. +- No unexpected error (about a replicator) during the early stage of initialisation. +- Now error messages are kept hidden if the show status inside the editor is disabled (related: #829). +- Fixed an issue where devices could no longer upload after another device performed 'Fresh Start Wipe' and 'Overwrite remote' in Object Storage mode (#848). + - Each device's local deduplication caches (`knownIDs`, `sentIDs`, `receivedFiles`, `sentFiles`) now track the remote journal epoch (derived from the encryption parameters stored on the remote). + - When the epoch changes, the plugin verifies whether the device's last uploaded file still exists on the remote. If the file is gone, it confirms a remote wipe and automatically clears the stale caches. If the file is still present (e.g. a protocol upgrade without a wipe), the caches are preserved, and only the epoch is updated. This means normal upgrades never cause unnecessary re-processing. + +### Translations + +- Russian translation has been added! Thank you so much for the contribution, @vipka1n! (#845) + +### New features + +- Now we can configure multiple Remote Databases of the same type, e.g, multiple CouchDBs or S3 remotes. + - A user interface for managing multiple remote databases has been added to the settings dialogue. I think no explanation is needed for the UI, but please let me know if you have any questions. +- We can switch between multiple Remote Databases in the settings dialogue. + +### CLI + +#### Fixed + +- Replication progress is now correctly saved and restored in the CLI (related: #846). + +## ~~0.25.55~~ 0.25.56 + +30th March, 2026 + +### Fixed + +- No longer `Peer-to-Peer Sync is not enabled. We cannot open a new connection.` error occurs when we have not enabled P2P sync and are not expected to use it (#830). + +### CLI + +- Fixed incomplete localStorage support in the CLI (#831). Thank you so much @rewse ! +- Fixed the issue where the CLI could not be connected to the remote which had been locked once (#833), also thanks to @rewse ! + +## 0.25.54 + +18th March, 2026 + +### Fixed + +- Remote storage size check now works correctly again (#818). +- Some buttons on the settings dialogue now respond correctly again (#827). + +### Refactored + +- P2P replicator has been refactored to be a little more robust and easier to understand. +- Delete items which are no longer used that might cause potential problems + +### CLI + +- Fixed the corrupted display of the help message. +- Remove some unnecessary code. + +### WebApp + +- Fixed the issue where the detail level was not being applied in the log pane. +- Pop-ups are now shown. +- Add coverage for the test. +- Pop-ups are now shown in the web app as well. + +## 0.25.53 + +17th March, 2026 + +I did wonder whether I should have released a minor version update, but when I actually tested it, compatibility seemed to be intact, so I didn’t. Hmm. + +### Fixed + +#### P2P Synchronisation + +- Fixed flaky timing issues in P2P synchronisation. +- No longer unexpected `Unhandled Rejections` during P2P operations (waiting for acceptance). + +#### Journal Sync + +- Fixed an issue where some conflicts cannot be resolved in Journal Sync. +- Many minor fixes have been made for better stability and reliability. + +### Tests + +- Rewrite P2P end-to-end tests to use the CLI as a host. + +### CLI + +We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details. +- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version. +- P2P sync and Object Storage are also supported in the CLI. + - Yes, we have finally managed to 'get one file'. + - Also, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can do it. + +- Now binary files are also supported in the CLI. + +### Refactored or internal changes + +- ServiceFileAccessBase now correctly handles the reading of binary files. +- HeadlessAPIService now correctly provides the online status (always online) to the plug-in. +- Non-worker version of bgWorker now correctly handles some functions. +- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`. +- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`. +- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`. +- Offline change scanner and the local database preparation have been separated. +- Set default priority for processFileEvent and processSynchroniseResult for the place to add hooks. +- ControlService now provides the readiness for processing operations. +- DatabaseService is now able to modify database opening options on derived classes. +- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`. +- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported. +- Journal Replicator now yields true after the replication is done. + +### R&D + +- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing. +- We can see the code in `src/apps/webapp` for the browser version. + + +## 0.25.52-patched-3 + +16th March, 2026 + +### Fixed + +- Fixed flaky timing issues in P2P synchronisation. +- Fixed more binary file handling issues in CLI. + +### Tests + +- Rewrite P2P end-to-end tests to use the CLI as host. + + +## 0.25.52-patched-2 + +14th March, 2026 + +### Fixed + +- No longer unexpected `Unhandled Rejections` during P2P operations (waiting acceptance). +- Fixed an issue where conflicts cannot be resolved in Journal Sync + +### CLI new features + +- `mirror` command has been added to the CLI. This command is intended to mirror the storage to the local database. +- `p2p-sync`, `p2p-peers`, and `p2p-host` commands have been added to the CLI. These commands are intended for P2P synchronisation. + - Yes, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can handle it by itself. + +## 0.25.52-patched-1 + +12th March, 2026 + +### Fixed + +- Fixed Journal Sync had not been working on some timing, due to a compatibility issue (for a long time). +- ServiceFileAccessBase now correctly handles the reading of binary files. +- HeadlessAPIService now correctly provides the online status (always online) to the plug-in. +- Non-worker version of bgWorker now correctly handles some functions. + +### Refactored + +- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`. +- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`. +- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`. +- Offline change scanner and the local database preparation have been separated. +- Set default priority for processFileEvent and processSynchroniseResult for the place to add hooks. +- ControlService now provides the readiness for processing operations. +- DatabaseService is now able to modify database opening options on derived classes. +- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`. + +### Internal API changes + +- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported. +- Journal Replicator now yields true after the replication is done. + +### CLI + +We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details. + +- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version. +- Add more tests. +- Object Storage support has also been confirmed (and fixed) in CLI. + - Yes, we have finally managed to 'get one file'. +- Now binary files are also supported in the CLI. + +### R&D + +- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing. +- We can see the code in `src/apps/webapp` for the browser version. + + +## 0.25.52 + +9th March, 2026 + +Excuses: Too much `I`. +Whilst I had a fever, I could not figure it out at all, but once I felt better, I spotted the problem in about thirty seconds. I apologise for causing you concern. I am grateful for your patience. +I would like to devise a mechanism for running simple test scenarios. Now that we have got the Obsidian CLI up and running, it seems the perfect opportunity. + +To improve the bus factor, we really need to organise the source code more thoroughly. Your cooperation and contributions would be greatly appreciated. + +### Fixed + +- No longer unexpected deletion-propagation occurs when the parent directory is not empty (#813). + +### Revert reversions + +- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature. + +## 0.25.51 + +7th March, 2026 + +### Reverted + +- Reverted to ModuleRedFlag and ModuleInitializerFile to the previous version because of some unexpected issues. (#813) + - I will re-implement them in the future with better design and tests. + +## 0.25.50 + +3rd March, 2026 + +Note: 0.25.49 has been skipped because of too verbose logging (credentials are logged in verbose level, but I realised that could lead to unexpected exposure on issue reporting). Please bump to 0.25.50 to get the fix if you are on 0.25.49. (No expected behaviour changes except the logging). + +### Fixed + +- No longer deleted files are not clickable in the Global History pane. +- Diff view now uses more specific classes (#803). +- A message of configuration mismatching slightly added for better understanding. + - Now it says `When replication is initiated manually via the command palette or ribbon, a dialogue box will open to address this.` to make it clear that the user can fix the issue by themselves. + +### Refactored + +- `ModuleRedFlag` has been refactored to `serviceFeatures/redFlag` and also tested. +- `ModuleInitializerFile` has been refactored to `lib/serviceFeatures/offlineScanner` and also tested. + +## 0.25.48 + +2nd March, 2026 + +No behavioural changes except unidentified faults. Please report if you find any unexpected behaviour after this update. + +### Refactored + +- Many storage-related functions have been refactored for better maintainability and testability. + - Now all platform-specific logics are supplied as adapters, and the core logic has become platform-agnostic. + - Quite a number of tests have been added for the core logic, and the platform-specific logics are also tested with mocked adapters. + +## 0.25.47 + +27th February, 2026 + +Phew, the financial year is still not over yet, but I have got some time to work on the plug-in again! + +### Fixed and refactored + +- Fixed the inexplicable behaviour when retrieving chunks from the network. + - The chunk manager has been layered to be responsible for its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`, and `ArrivalWaitLayer`. + - All layers have been tested now! + - `LayeredChunkManager` has been implemented to manage these layers. Also tested. + - `EntryManager` has been mostly rewritten and also tested. + +- Now we can configure `Never warn` for remote storage size notification again. + +### Tests + +- The following test has been added: + - `ConflictManager`. + +## 0.25.46 + +26th February, 2026 + +### Fixed + +- Unexpected errors no longer occurred when the plug-in was unloaded. +- Hidden File Sync now respects selectors. +- Registering protocol-handlers now works safely without causing unexpected errors. + +### Refactored + +- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and tests have also been added. +- Some unnecessary things have been removed. +- LiveSyncManagers has now explicit dependencies. +- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies. + - This is to avoid circular dependencies and clarify the ownership of the managers. +- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly. +- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage. + +## 0.25.45 + +25th February, 2026 + +As a result of recent refactoring, we are able to write tests more easily now! + +### Refactored + +- `ModuleTargetFilter`, which was responsible for checking if a file is a target file, has been ported to a serviceFeature. + - And also tests have been added. The middleware-style-power. +- `ModuleObsidianAPI` has been removed and implemented in `APIService` and `RemoteService`. +- Now `APIService` is responsible for the network-online-status, not `databaseService.managers.networkManager`. + +## 0.25.44 + +24th February, 2026 + +This release represents a significant architectural overhaul of the plug-in, focusing on modularity, testability, and stability. While many changes are internal, they pave the way for more robust features and easier maintenance. +However, as this update is very substantial, please do feel free to let me know if you encounter any issues. + +### Fixed + +- Ignore files (e.g., `.ignore`) are now handled efficiently. +- Replication & Database: + - Replication statistics are now correctly reset after switching replicators. +- Fixed `File already exists` for .md files has been merged (PR #802) So thanks @waspeer for the contribution! + +### Improved + +- Now we can configure network-error banners as icons, or hide them completely with the new `Network Warning Style` setting in the `General` pane of the settings dialogue. (#770, PR #804) + - Thanks so much to @A-wry! + +### Refactored + +#### Architectural Overhaul: + +- A major transition from Class-based Modules to a Service/Middleware architecture has begun. + - Many modules (for example, `ModulePouchDB`, `ModuleLocalDatabaseObsidian`, `ModuleKeyValueDB`) have been removed or integrated into specific Services (`database`, `keyValueDB`, etc.). + - Reduced reliance on dynamic binding and inverted dependencies; dependencies are now explicit. + - `ObsidianLiveSyncPlugin` properties (`replicator`, `localDatabase`, `storageAccess`, etc.) have been moved to their respective services for better separation of concerns. + - In this refactoring, the Service will henceforth, as a rule, cease to use setHandler, that is to say, simple lazy binding. + - They will be implemented directly in the service. + - However, not everything will be middlewarised. Modules that maintain state or make decisions based on the results of multiple handlers are permitted. +- Lifecycle: + - Application LifeCycle now starts in `Main` rather than `ServiceHub` or `ObsidianMenuModule`, ensuring smoother startup coordination. + +#### New Services & Utilities: + +- Added a `control` service to orchestrate other services (for example, handling stop/start logic during settings realisation). +- Added `UnresolvedErrorManager` to handle and display unresolved errors in a unified way. +- Added `logUtils` to unify logging injection and formatting. +- `VaultService.isTargetFile` now uses multiple, distinct checkers for better extensibility. + +#### Code Separation: + +- Separated Obsidian-specific logic from base logic for `StorageEventManager` and `FileAccess` modules. +- Moved reactive state values and statistics from the main plug-in instance to the services responsible for them. + +#### Internal Cleanups: + +- Many functions have been renamed for clarity (for example, `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`). +- Added `override` keywords to overridden items and removed dynamic binding for clearer code inheritance. +- Moved common functions to the common library. + +#### Dependencies: + +- Bumped dependencies simply to a point where they can be considered problem-free (by human-powered-artefacts-diff). + - Svelte, terser, and more something will be bumped later. They have a significant impact on the diff and paint it totally. + - You may be surprised, but when I bump the library, I am actually checking for any unintended code. + +## 0.25.43-patched-9 a.k.a. 0.25.44-rc1 + +We are finally ready for release. I think I will go ahead and release it after using it for a few days. + +### Fixed + +- Hidden file synchronisation now works! +- Now Hidden file synchronisation respects `.ignore` files. +- Replicator initialisation during rebuilding now works correctly. + +### Refactored + +- Some methods naming have been changed for better clarity, i.e., `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`. + +### Follow-up tasks memo (After 0.25.44) + +Going forward, functionality that does not span multiple events is expected to be implemented as middleware-style functions rather than modules based on classes. + +Consequently, the existing modules will likely be gradually dismantled. +For reference, `ModuleReplicator.ts` has extracted several functionalities as functions. + +However, this does not negate object-oriented design. Where lifecycles and state are present, and the Liskov Substitution Principle can be upheld, we design using classes. After all, a visible state is preferable to a hidden state. In other words, the handler still accepts both functions and member methods, so formally there is no change. + +As undertaking this for everything would be a bit longer task, I intend to release it at this stage. + +Note: I left using `setHandler`s that as a mark of `need to be refactored`. Basically, they should be implemented in the service itself. That is because it is just a mis-designed, separated implementation. + +## 0.25.43-patched-8 + +I really must thank you all. You know that it seems we have just a little more to do. +Note: This version is not fully tested yet. Be careful to use this. Very dogfood-y one. + +### Fixed + +- Now the device name is saved correctly. + +### Refactored + +- Add `override` keyword to all overridden items. +- More dynamic binding has been removed. +- The number of inverted dependencies has decreased much more. +- Some check-logic; i.e., like pre-replication check is now separated into check functions and added to the service as handlers, layered. + - This may help with better testing and better maintainability. + + +## 0.25.43-patched-7 + +19th February, 2026 + +Right then, let us make a decision already. + +Last time, since I found a bug, I ended up doing a few other things as well, but next time I intend to release it with just the bug fix. It is quite substantial, after all. + +Customisation Sync has mostly been verified. Hidden file synchronisation has not been done yet. + +Vite's build system is not in the production. However, I possibly migrate to it in the future. + +And, the `daily-progress` will be tidied on releasing 0.25.44. Do not worry! + +### Fixed + +- Fixed an issue where the StorageEventManager was not correctly loading the settings. +- Replication statistics are now correctly reset after switching replicators. + +### Refactored + +- Now, many reactive values which keep the state or statistics of the plugin are moved to the services which have the responsibility for these states. +- `serviceFeatures` are now able to be added to the services; this is not a class module, but a function which accepts dependencies and returns an addHandler-able function. This is for better separation of concerns, better maintainability, and testability. +- `control` service; is a meta-service which is responsible for orchestrating services has been added. + - Don't you think stopping replication or something occurs during `settingService.realiseSetting` is quite weird? It may be done by the control service, which can orchestrate the setting service and the replicator service. + - +- Some functions on services have been moved. e.g., `getSystemVaultName` is now on the API service. +- Setting Service is now responsible for the setting, no longer using dynamic binding for the modules. + +## 0.25.43-patched-6 + +18th February, 2026 + +Let me confess that I have lied about `now all ambiguous properties`... I have found some more implicit calling. + +Note: I have not checked hidden file sync and customisation sync yet. Please report if you find any unexpected behaviour in these features. + +### Fixed + +- Now ReplicatorService responds to database reset and database initialisation events to dispose of the active replicator. + - Fixes some unlocking issues during rebuilding. + +### Refactored + +- Now `StorageEventManagerBase` is separated from `StorageEventManagerObsidian` following their concerns. + - No longer using `ObsidianFileAccess` indirectly during checking duplicated-file events. + - Last event memorisation is now moved into the StorageAccessManager, just like the file processing interlocking. + - These methods, i.e., `ObsidianFileAccess.touch`. `StorageEventManager.recentlyTouched`, and `StorageEventManager.touch` are still available, but simply call the StorageAccessManager's methods. +- Now `FileAccessBase` is separated from `FileAccessObsidian` following their concerns. + +## 0.25.43-patched-5 + +17th February, 2026 + +Yes, we mostly have got refactored! + +### Refactored + +- Following properties of `ObsidianLiveSyncPlugin` are now initialised more explicitly: + + - property : what is responsible + - `storageAccess` : `ServiceFileAccessObsidian` + - `databaseFileAccess` : `ServiceDatabaseFileAccess` + - `fileHandler` : `ServiceFileHandler` + - `rebuilder` : `ServiceRebuilder` + - Not so long from now, ServiceFileAccessObsidian might be abstracted to a more general FileAccessService, and make more testable and maintainable. + - These properties are initialised in `initialiseServiceModules` on `ObsidianLiveSyncPlugin`. + - They are `ServiceModule`s. + - Which means they do not use dynamic binding themselves, but they use bound services. + - ServiceModules are in src/lib/src/serviceModules for common implementations, and src/serviceModules for Obsidian-specific implementations. + - Hence, now all ambiguous properties of `ObsidianLiveSyncPlugin` are initialised explicitly. We can proceed to testing. + - Well, I will release v0.25.44 after testing this. + +- Conflict service is now responsible for `resolveAllConflictedFilesByNewerOnes` function, which has been in the rebuilder. +- New functions `updateSettings`, and `applyPartial` have been added to the setting service. We should use these functions instead of directly writing the settings on `ObsidianLiveSyncPlugin.setting`. +- Some interfaces for services have been moved to src/lib/src/interfaces. +- `RemoteService.tryResetDatabase` and `tryCreateDatabase` are now moved to the replicator service. + - You know that these functions are surely performed by the replicator. + - Probably, most of the functions in `RemoteService` should be moved to the replicator service, but for now, these two functions are moved as they are the most related ones, to rewrite the rebuilder service. +- Common functions are gradually moved to the common library. +- Now, binding functions on modules have been delayed until the services and service modules are initialised, to avoid fragile behaviour. + +## 0.25.43-patched-4 + +16th February, 2026 + +I have been working on it little by little in my spare time. Sorry for the delayed response for issues! ! However, thanks for your patience, we seems the `revert to 0.25.43` is not necessary, and I will keep going with this version. + +### Refactored + +- No longer `DatabaseService` is an injectable service. It is now actually a service which has its own handlers. No dynamic binding for necessary functions. +- Now the following properties of `ObsidianLiveSyncPlugin` belong to each service: + - `replicator` : `services.replicator` (still we can access `ObsidianLiveSyncPlugin.replicator` for the active replicator) +- A Handy class `UnresolvedErrorManager` has been added, which is responsible for managing unresolved errors and their handlers (we will see `unresolved errors` on a red-background-banner in the editor when they occur). + - This manager can be used to handle unresolved errors in a unified way, and it can also be used to display notifications or something when unresolved errors occur. + +## 0.25.43-patched-3 + +16th February, 2026 + +### Refactored + +- Now following properties of `ObsidianLiveSyncPlugin` belong to each service: + - property : service (still we can access these properties from `ObsidianLiveSyncPlugin` for better usability, but probably we should access these from services to clarify the dependencies) + - `localDatabase` : `services.database` + - `managers` : `services.database` + - `simpleStore` : `services.keyValueDB` + - `kvDB`: `services.keyValueDB` +- Initialising modules, addOns, and services are now explicitly separated in the `_startUp` function of the main plug-in class. +- LiveSyncLocalDB now depends more explicitly on specified services, not the whole `ServiceHub`. +- New service `keyValueDB` has been added. This had been separated from the `database` service. +- Non-trivial modules, such as `ModuleExtraSyncObsidian` (which only holds deviceAndVaultName), are simply implemented in the service. +- Add `logUtils` for unifying logging method injection and formatting. This utility is able to accept the API service for log writing. +- `ModuleKeyValueDB` has been removed, and its functionality is now implemented in the `keyValueDB` service. +- `ModulePouchDB` and `ModuleLocalDatabaseObsidian` have been removed, and their functionality is now implemented in the `database` service. + - Please be aware that you have overridden createPouchDBInstance or something by dynamic binding; you should now override the createPouchDBInstance in the database service instead of using the module. + - You can refer to the `DirectFileManipulatorV2` for an example of how to override the createPouchDBInstance function in the database service. + +## 0.25.43-patched-2 + +14th February, 2026 + +### Fixed + +- Application LifeCycle has now started in Main, not ServiceHub. + - Indeed, ServiceHub cannot be known other things in main have got ready, so it is quite natural to start the lifecycle in main. + +## 0.25.43-patched-1 + +13th February, 2026 + +**NOTE: Hidden File Sync and Customisation Sync may not work in this version.** + +Just a heads-up: this is a patch version, which is essentially a beta release. Do not worry about the following memos, as they are indeed freaking us out. I trust that you have thought this was too large; you're right. + +If this cannot be stable, I will revert to 0.24.43 and try again. + +### Refactored + +- Now resolving unexpected and inexplicable dependency order issues... +- The function which is able to implement to the service is now moved to each service. + - AppLifecycleService.performRestart +- VaultService.isTargetFile is now using multiple checkers instead of a single function. + - This change allows better separation of concerns and easier extension in the future. +- Application LifeCycle has now started in ServiceHub, not ObsidianMenuModule. + + - It was in a QUITE unexpected place..., isn't it? + - Instead of, we should call `await this.services.appLifecycle.onReady()` in other platforms. + - As in the browser platform, it will be called at `DOMContentLoaded` event. + +- ModuleTargetFilter, which is responsible for parsing ignore files, has been refined. + - This should be separated to a TargetFilter and an IgnoreFileFilter for better maintainability. +- Using `API.addCommand` or some Obsidian API and shimmer APIs, Many modules have been refactored to be derived to AbstractModule from AbstractObsidianModule, to clarify the dependencies. (we should make `app` usage clearer...) +- Fixed initialising `storageAccess` too late in `FileAccessObsidian` module (I am still wondering why it worked before...). +- Remove some redundant overrides in modules. + +### Planned + +- Some services have an ambiguous name, such as `Injectable`. These will be renamed in the future for better clarity. +- Following properties of `ObsidianLiveSyncPlugin` should be initialised more explicitly: + - property : where it is initialised currently + - `localDatabase` : `ModuleLocalDatabaseObsidian` + - `managers` : `ModuleLocalDatabaseObsidian` + - `replicator` : `ModuleReplicator` + - `simpleStore` : `ModuleKeyValueDB` + - `storageAccess` : `ModuleFileAccessObsidian` + - `databaseFileAccess` : `ModuleDatabaseFileAccess` + - `fileHandler` : `ModuleFileHandler` + - `rebuilder` : `ModuleRebuilder` + - `kvDB`: `ModuleKeyValueDB` + - And I think that having a feature in modules directly is not good for maintainability, these should be separated to some module (loader) and implementation (not only service, but also independent something). +- Plug-in statuses such as requestCount, responseCount... should be moved to a status service or somewhere for better separation of concerns. + +## 0.25.43 + +5th, February, 2026 + +### Fixed + +- Encryption/decryption issues when using Object Storage as remote have been fixed. + - Now the plug-in falls back to V1 encryption/decryption when V2 fails (if not configured as ForceV1). + - This may fix the issue reported in #772. + +### Notice + +Quite a few packages have been updated in this release. Please report if you find any unexpected behaviour after this update. + +## 0.25.42 + +2nd, February, 2026 + +This release is identical to 0.25.41-patched-3, except for the version number. + +### Refactored + +- Now the service context is `protected` instead of `private` in `ServiceBase`. + - This change allows derived classes to access the context directly. +- Some dynamically bound services have been moved to services for better dependency management. +- `WebPeer` has been moved to the main repository from the sub repository `livesync-commonlib` for correct dependency management. +- Migrated from the outdated, unstable platform abstraction layer to services. + - A bit more services will be added in the future for better maintainability. + +## 0.25.41 + +24th January, 2026 + +### Fixed + +- No longer `No available splitter for settings!!` errors occur after fetching old remote settings while rebuilding local database. (#748) + +### Improved + +- Boot sequence warning is now kept in the in-editor notification area. + +### New feature + +- We can now set the maximum modified time for reflect events in the settings. (for #754) + - This setting can be configured from `Patches` -> `Remediation` in the settings dialogue. + - Enabling this setting will restrict the propagation from the database to storage to only those changes made before the specified date and time. + - This feature is primarily intended for recovery purposes. After placing `redflag.md` in an empty vault and importing the Self-hosted LiveSync configuration, please perform this configuration, and then fetch the local database from the remote. + - This feature is useful when we want to prevent recent unwanted changes from being reflected in the local storage. + +### Refactored + +- Module to service refactoring has been started for better maintainability: + - UI module has been moved to UI service. + +### Behaviour change + +- Default chunk splitter version has been changed to `Rabin-Karp` for new installations. + +## 0.25.40 + +23rd January, 2026 + +### Fixed + +- Fixed an issue where some events were not triggered correctly after the refactoring in 0.25.39. + +## 0.25.39 + +23rd January, 2026 + +Also no behaviour changes or fixes in this release. Just refactoring for better maintainability. Thank you for your patience! I will address some of the reported issues soon. +However, this is not a minor refactoring, so please be careful. Let me know if you find any unexpected behaviour after this update. + +### Refactored + +- Rewrite the service's binding/handler assignment systems +- Removed loopholes that allowed traversal between services to clarify dependencies. +- Consolidated the hidden state-related state, the handler, and the addition of bindings to the handler into a single object. + - Currently, functions that can have handlers added implement either addHandler or setHandler directly on the function itself. + I understand there are differing opinions on this, but for now, this is how it stands. +- Services now possess a Context. Please ensure each platform has a class that inherits from ServiceContext. +- To permit services to be dynamically bound, the services themselves are now defined by interfaces. + +## 0.25.38 + +17th January, 2026 + +### Fixed + +- Fixed an issue where indexedDB would not close correctly on some environments, causing unexpected errors during database operations. + +## 0.25.37 + +15th January, 2026 + +Thank you for your patience until my return! + +This release contains minor changes discovered and fixed during test implementation. +There are no changes affecting usage. + +### Refactored + +- Logging system has been slightly refactored to improve maintainability. +- Some import statements have been unified. + +## 0.25.36 + +25th December, 2025 + +### Improved + +- Now the garbage collector (V3) has been implemented. (Beta) + - This garbage collector ensures that all devices are synchronised to the latest progress to prevent inconsistencies. + - In other words, it makes sure that no new conflicts would have arisen. + - This feature requires additional information (via node information), but it should be more reliable. + - This feature requires all devices have v0.25.36 or later. + - After the garbage collector runs, the database size may be reduced (Compaction will be run automatically after GC). + - We should have an administrative privilege on the remote database to run this garbage collector. +- Now the plug-in and device information is stored in the remote database. + - This information is used for the garbage collector (V3). + - Some additional features may be added in the future using this information. + +## 0.25.35 + +24th December, 2025 + +Sorry for a small release! I would like to keep things moving along like this if possible. After all, the holidays seem to be starting soon. I will be doubled by my business until the 27th though, indeed. + +### Fixed + +- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764). + +## 0.25.34 + +10th December, 2025 + +### Behaviour change + +- The plug-in automatically fetches the missing chunks even if `Fetch chunks on demand` is disabled. + - This change is to avoid loss of data when receiving a bulk of revisions. + - This can be prevented by enabling `Use Only Local Chunks` in the settings. +- Storage application now saved during each event and restored on startup. +- Synchronisation result application is also now saved during each event and restored on startup. + - These may avoid some unexpected loss of data when the editor crashes. + +### Fixed + +- Now the plug-in waits for the application of pended batch changes before the synchronisation starts. + - This may avoid some unexpected loss or unexpected conflicts. + Plug-in sends custom headers correctly when RequestAPI is used. +- No longer causing unexpected chunk creation during `Reset synchronisation on This Device` with bucket sync. + +### Refactored + +- Synchronisation result application process has been refactored. +- Storage application process has been refactored. + - Please report if you find any unexpected behaviour after this update. A bit of large refactoring. + +## 0.25.33 + +05th December, 2025 + +### New feature + +- We can analyse the local database with the `Analyse database usage` command. + - This command makes a TSV-style report of the database usage, which can be pasted into spreadsheet applications. + - The report contains the number of unique chunks and shared chunks for each document revision. + - Unique chunks indicate the actual consumption. + - Shared chunks indicate the reference counts from other chunks with no consumption. + - We can find which notes or files are using large amounts of storage in the database. Or which notes cannot share chunks effectively. + - This command is useful when optimising the database size or investigating an unexpectedly large database size. +- We can reset the notification threshold and check the remote usage at once with the `Reset notification threshold and check the remote database usage` command. +- Commands are available from the Command Palette, or `Hatch` pane in the settings dialogue. + +### Fixed + +- Now the plug-in resets the remote size notification threshold after rebuild. + +## 0.25.32 + +02nd December, 2025 + +Now I am back from a short (?) break! Thank you all for your patience. (It is nothing major, but the first half of the year has finally come to an end). +Anyway, I will release the things a bit by bit. I think that we need a rehabilitation or getting gears in again. + +### Improved + +- Now the plugin warns when we are in several file-related situations that may cause unexpected behaviour (#300). + - These errors are displayed alongside issues such as file size exceeding limits. + - Such situations include: + - When the document has a name which is not supported by some file systems. + - When the vault has the same file names with different letter cases. + +## 0.25.31 + +18th November, 2025 + +### Fixed + +- Now fetching configuration from the server can handle the empty remote correctly (reported on #756). +- No longer asking to switch adapters during rebuilding. + +## 0.25.0 through 0.25.30 + +(0.25.0 through 0.25.30) + +Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) + +After reading Issue #668, I conducted another self-review of the E2EE-related code. In retrospect, it was clearly written by someone inexperienced, which is understandable, but it is still rather embarrassing. Three years is certainly enough time for growth. + +I have now rewritten the E2EE code to be more robust and easier to understand. It is significantly more readable and should be easier to maintain in the future. The performance issue, previously considered a concern, has been addressed by introducing a master key and deriving keys using HKDF. This approach is both fast and robust, and it provides protection against rainbow table attacks. (In addition, this implementation has been [a dedicated package on the npm registry](https://github.com/vrtmrz/octagonal-wheels), and tested in 100% branch-coverage). + +As a result, this is the first time in a while that forward compatibility has been broken. We have also taken the opportunity to change all metadata to use encryption rather than obfuscation. Furthermore, the `Dynamic Iteration Count` setting is now redundant and has been moved to the `Patches` pane in the settings. Thanks to Rabin-Karp, the eden setting is also no longer necessary and has been relocated accordingly. Therefore, v0.25.0 represents a legitimate and correct evolution. + +--- + +## 0.25.30 + +17th November, 2025 + +So sorry for the quick follow-up release, due to a humble mistake in a quick causing a matter. + +### Fixed + +- Now we can save settings correctly again (#756). + +## ~~0.25.28~~ 0.25.29 + +(0.25.28 was skipped due to a packaging issue.) + +17th November, 2025 + +### New feature + +- We can now configure hidden file synchronisation to always overwrite with the latest version (#579). + +### Fixed + +- Timing dependency issues during initialisation have been mitigated (#714) + +### Improved + +- Error logs now contain stack-traces for better inspection. + +## 0.25.27 + +12th November, 2025 + +### Improved + +- Now we can switch the database adapter between IndexedDB and IDB without rebuilding (#747). + - Just a local migration will be required, but faster than a full rebuild. +- No longer checking for the adapter by `Doctor`. + +### Changes + +- The default adapter is reverted to IDB to avoid memory leaks (#747). + +### Fixed (?) + +- Reverted QR code library to v1.4.4 (To make sure #752). + +## 0.25.26 + +07th November, 2025 + +### Improved + +- Some JWT notes have been added to the setting dialogue (#742). + +### Fixed + +- No longer wrong values encoded into the QR code. +- We can acknowledge why the QR codes have not been generated. + - Probably too large a dataset to encode. When this happens, please consider using Setup-URI via text instead of QR code, or reduce the settings temporarily. + +### Refactored + +- Some dependencies have been updated. +- Internal functions have been modularised into `octagonal-wheels` packages and are well tested. + - `dataobject/Computed` for caching computed values. + - `encodeAnyArray/decodeAnyArray` for encoding and decoding any array-like data into compact strings (#729). +- Fixed importing from the parent project in library codes. (#729). + +## 0.25.25 + +06th November, 2025 + +### Fixed + +#### JWT Authentication + +- Now we can use JWT Authentication ES512 correctly (#742). +- Several misdirections in the Setting dialogues have been fixed (i.e., seconds and minutes confusion...). +- The key area in the Setting dialogue has been enlarged and accepts newlines correctly. +- Caching of JWT tokens now works correctly + - Tokens are now cached and reused until they expire. + - They will be kept until 10% of the expiration duration is remaining or 10 seconds, whichever is longer (but at a maximum of 1 minute). +- JWT settings are now correctly displayed on the Setting dialogue. + +And, tips about JWT Authentication on CouchDB have been added to the documentation (docs/tips/jwt-on-couchdb.md). + +#### Other fixes + +- Receiving non-latest revisions no longer causes unexpected overwrites. + - On receiving revisions that made conflicting changes, we are still able to handle them. + +### Improved + +- No longer duplicated message notifications are shown when a connection to the remote server fails. + - Instead, a single notification is shown, and it will be kept on the notification area inside the editor until the situation is resolved. +- The notification area is no longer imposing, distracting, and overwhelming. + - With a pale background, but bordered and with icons. + +## 0.25.24 + +04th November, 2025 + +(Beta release notes have been consolidated to this note). + +### Guidance and UI improvements! + +Since several issues were pointed out, our setup procedure had been quite `system-oriented`. This is not good for users. Therefore, I have changed the procedure to be more `goal-oriented`. I have made extensive use of Svelte, resulting in a very straightforward setup. +While I would like to accelerate documentation and i18n adoption, I do not want to confuse everyone who's already working on it. Therefore, I have decided to release a Beta version at this stage. Significant changes are not expected from this point onward, so I will proceed to stabilise the codebase. (However, this is significant). + +### TURN server support and important notice + +TURN server settings are only necessary if you are behind a strict NAT or firewall that prevents direct P2P +connections. In most cases, you do not need to set up a TURN server. + +Using public TURN servers may have privacy implications, as your data will be relayed through third-party +servers. Even if your data are encrypted, your existence may be known to them. Please ensure you trust the TURN +server provider before using their services. Also your `network administrator` too. You should consider setting +up your own TURN server for your FQDN, if possible. + +### New features + +- We can use the TURN server for P2P connections now. + +### Fixed + +- P2P Replication got more robust and stable. + - Update [Trystero](https://github.com/dmotz/trystero) to the official v0.22.0! + - Fixed a bug that caused P2P connections to drop or (unwanted reconnection to the relay server) unexpectedly in some environments. + - Now, the connection status is more accurately reported. + - While in the background, the connection to the signalling server is now disconnected to save resources. + - When returning to the foreground, it will not reconnect automatically for safety. Please reconnect manually. +- All connection configurations should be edited in each dedicated dialogue now. +- No longer will larger files create chunks during preparing `Reset Synchronisation on This Device`. +- Now hidden file synchronisation respects the filters correctly (#631, #735) + - And `ignore-files` settings are also respected and surely read during the start-up. + +### Behaviour changes + +- The setup wizard is now more `goal-oriented`. Brand-new screens are introduced. +- `Fetch everything` and `Rebuild everything` are now `Reset Synchronisation on This Device` and `Overwrite Server Data with This Device's Files`. +- Remote configuration and E2EE settings are now separated into each modal dialogue. + - Remote configuration is now more straightforward. And if we need the rebuild (No... `Overwrite Server Data with This Device's Files`), it is now clearly indicated. +- Peer-to-Peer settings are also separated into their own modal dialogue (still in progress, and we need to open a P2P pane, still). +- Setup-URI, and Report for the Issue are now not copied to the clipboard automatically. Instead, there are copy-dialogue and buttons to copy them explicitly. + - This is to avoid confusion for users who do not want to use these features. +- No longer optional features are introduced during the setup, or `Reset Synchronisation on This Device`, `Overwrite Server Data with This Device's Files`. + - This is to avoid confusion for users who do not want to use these features. Instead, we will be informed that optional features are available after the setup is completed. +- We cannot perform `Fetch everything` and `Rebuild everything` (Removed, so the old name) without restarting Obsidian now. + +### Miscellaneous + +- Setup QR Code generation is separated into a src/lib/src/API/processSetting.ts file. Please use it as a subrepository if you want to generate QR codes in your own application. +- Setup-URI is also separated into a src/lib/src/API/processSetting.ts +- Some direct access to web APIs is now wrapped into the services layer. + +### Dependency updates + +- Many dependencies are updated. Please see `package.json`. + - This is the hardest part of this update. I read most of the changes in the dependencies. If you find any extra information, please let me know. +- As upgrading TypeScript, Fixed many UInt8Array and Uint8Array type mismatches. +- + +### Breaking changes + +- Sending configuration via Peer-to-Peer connection is not compatible with older versions. + - Please upgrade all devices to v0.25.24.beta1 or later to use this feature again. + - This is due to security improvements in the encryption scheme. + +## 0.25.23 + +26th October, 2025 + +The next version we are preparing (you know that as 0.25.23.beta1) is now still on beta, resulting in this rather unfortunate versioning situation. Apologies for the confusion. The next v0.25.23.beta2 will be v0.25.24.beta1. In other words, this is a v0.25.22.patch-1 actually, but possibly not allowed by Obsidian's rule. +(Perhaps we ought to declare 1.0.0 with a little more confidence. The current minor part has been effectively a major one for a long time. If it were 1.22.1 and 1.23.0.beta1, no confusion ). + +### Fixed + +- We are now able to enable optional features correctly again (#732). +- No longer oversized files have been processed, furthermore. + + - Before creating a chunk, the file is verified as the target. + - The behaviour upon receiving replication has been changed as follows: + - If the remote file is oversized, it is ignored. + - If not, but while the local file is oversized, it is also ignored. + +- We are now able to enable optional features correctly again (#732). +- No longer oversized files have been processed, furthermore. + - Before creating a chunk, the file is verified as the target. + - The behaviour upon receiving replication has been changed as follows: + - If the remote file is oversized, it is ignored. + - If not, but while the local file is oversized, it is also ignored. + +## 0.25.22 + +15th October, 2025 + +### Fixed + +- Fixed a bug that caused wrong event bindings and flag inversion (#727) + - This caused following issues: + - In some cases, settings changes were not applied or saved correctly. + - Automatic synchronisation did not begin correctly. + +### Improved + +- Too large diffs are not shown in the file comparison view, due to performance reasons. + +### Notes + +- The checking algorithm implemented in 0.25.20 is also raised as PR (#237). And completely I merged it manually. + - Sorry for lacking merging this PR, and let me say thanks to the great contribution, @bioluks ! +- Known issues: + - Sync on Editor save seems not to work correctly in some cases. + - I am investigating this issue. If you have any information, please let me know. + +## 0.25.21 + +13th October, 2025 + +This release including 0.25.21.beta1 and 0.25.21.beta2. + +Apologies for taking a little time. I was seriously tackling this. +(Of course, being caught up in an unfamiliar structure due to personnel changes on my workplace played a part, but fortunately I have returned to a place where I can do research and development rather than production. Completely beside the point, though). +Now then, this time, moving away from 'convention over configuration', I have changed to a mechanism for manually binding events. This makes it much easier to leverage IDE assistance. +And, also, we are ready to separate `Features` and `APIs` from `Module`. Features are still in the module, but APIs will be moved to a Service layer. This will make it easier to maintain and extend the codebase in the future. + +If you have found any issues, please let me know. I am now on the following: + +- GitHub [Issues](https://github.com/vrtmrz/obsidian-livesync/issues) Excellent! May the other contributors will help you too. +- Twitter [@vorotamoroz](https://twitter.com/vorotamoroz) Quickest! +- Matrix [@vrtmrz:matrix.org](https://matrix.to/#/@vrtmrz:matrix.org) Also quick, and if you need to keep it private! + I am creating rooms too, but I'm struggling to figure out how to use them effectively because I cannot tell the difference of use-case between them and discussions. However, if you want to use Discord, this is a answer; We should on E2E encrypted platform. + +## 0.25.21.beta2 + +8th October, 2025 + +### Fixed + +- Fixed wrong event type bindings (which caused some events not to be handled correctly). +- Fixed detected a timing issue in StorageEventManager + - When multiple events for the same file are fired in quick succession, metadata has been kept older information. This induces unexpected wrong notifications and write prevention. + +## 0.25.21.beta1 + +6th October, 2025 + +### Refactored + +- Event handling now does not rely on 'convention over configuration'. + - Services.ts now have a proper event handler registration system. + +## 0.25.20 + +26th September, 2025 + +### Fixed + +- Chunk fetching no longer reports errors when the fetched chunk could not be saved (#710). + - Just using the fetched chunk temporarily. +- Chunk fetching reports errors when the fetched chunk is surely corrupted (#710, #712). +- It no longer detects files that the plug-in has modified. + - It may reduce unnecessary file comparisons and unexpected file states. + +### Improved + +- Now checking the remote database configuration respecting the CouchDB version (#714). + +## 0.25.19 + +18th September, 2025 + +### Improved + +- Now encoding/decoding for chunk data and encryption/decryption are performed in native functions (if they were available). + - This uses Uint8Array.fromBase64 and Uint8Array.toBase64, which are natively available in iOS 18.2+ and Android with Chrome 140+. + - In Android, WebView is by default updated with Chrome, so it should be available in most cases. + - Note that this is not available in Desktop yet (due to being based on Electron). We are staying tuned for future updates. + - This realised by an external(?) package [octagonal-wheels](https://github.com/vrtmrz/octagonal-wheels). Therefore, this update only updates the dependency. + +## 0.25.18 + +17th September, 2025 + +### Fixed + +- Property encryption detection now works correctly (On Self-hosted LiveSync, it was not broken, but as a library, it was not working correctly). +- Initialising the chunk splitter is now surely performed. +- DirectFileManipulator now works fine (as a library) + - Old `DirectFileManipulatorV1` is now removed. + +### Refactored + +- Removed some unnecessary intermediate files. + +## 0.25.17 + +16th September, 2025 + +### Fixed + +- No longer information-level logs have produced during toggling `Show only notifications` in the settings (#708). +- Ignoring filters for Hidden file sync now works correctly (#709). + +### Refactored + +- Removed some unnecessary intermediate files. + +## 0.25.16 + +4th September, 2025 + +### Improved + +- Improved connectivity for P2P connections +- The connection to the signalling server can now be disconnected while in the background or when explicitly disconnected. + - These features use a patch that has not been incorporated upstream. + - This patch is available at [vrtmrz/trystero](https://github.com/vrtmrz/trystero). + +## 0.25.15 + +3rd September, 2025 + +### Improved + +- Now we can configure `forcePathStyle` for bucket synchronisation (#707). + +## 0.25.14 + +2nd September, 2025 + +### Fixed + +- Opening IndexedDB handling has been ensured. +- Migration check of corrupted files detection has been fixed. + - Now informs us about conflicted files as non-recoverable, but noted so. + - No longer errors on not-found files. + +## 0.25.13 + +1st September, 2025 + +### Fixed + +- Conflict resolving dialogue now properly displays the changeset name instead of A or B (#691). + +## 0.25.12 + +29th August, 2025 + +### Fixed + +- Fixed an issue with automatic synchronisation starting (#702). + +## 0.25.11 + +28th August, 2025 + +### Fixed + +- Automatic translation detection on the first launch now works correctly (#630). +- No errors are shown during synchronisations in offline (if not explicitly requested) (#699). +- Missing some checking during automatic-synchronisation now works correctly. + +## 0.25.10 + +26th August, 2025 + +### New experimental feature + +- We can perform Garbage Collection (Beta2) without rebuilding the entire database, and also fetch the database. + - Note that this feature is very experimental and should be used with caution. + - This feature requires disabling `Fetch chunks on demand`. + +### Fixed + +- Resetting the bucket now properly clears all uploaded files. + +### Refactored + +- Some files have been moved to better reflect their purpose and improve maintainability. +- The extensive LiveSyncLocalDB has been split into separate files for each role. + +### Fixed + +- Unexpected `Failed to obtain PBKDF2 salt` or similar errors during bucket-synchronisation no longer occur. +- Unexpected long delays for chunk-missing documents when using bucket-synchronisation have been resolved. +- Fetched remote chunks are now properly stored in the local database if `Fetch chunks on demand` is enabled. +- The 'fetch' dialogue's message has been refined. +- No longer overwriting any corrupted documents to the storage on boot-sequence. + +### Refactored + +- Type errors have been corrected. + +## 0.25.9 + +20th August, 2025 + +### Fixed + +- CORS Checking messages now use replacements. +- Configuring CORS setting via the UI now respects the existing rules. +- Now startup-checking works correctly again, performs migration check serially and then it will also fix starting LiveSync or start-up sync. (#696) +- Statusline in editor now supported 'Bases'. + +## 0.25.8 + +18th August, 2025 + +### New feature + +- Insecure chunk detection has been implemented. + - A notification dialogue will be shown if any insecure chunks are detected; these may have been created by v0.25.6 due to its issue. If this dialogue appears, please ensure you rebuild the database after backing it up. + +## 0.25.7 + +15th August, 2025 + +**Since the release of 0.25.6, there are two large problem. Please update immediately.** + +- We may have corrupted some documents during the migration process. **Please check your documents on the wizard.** +- Due to a chunk ID assignment issue, some data has not been encrypted. **Please rebuild the database using Rebuild Everything** if you have enabled E2EE. + +**_So, If you have enabled E2EE, please perform `Rebuild everything`. If not, please check your documents on the wizard._** + +In next version, insecure chunk detection will be implemented. + +### Fixed + +- Off-loaded chunking have been fixed to ensure proper functionality (#693). +- Chunk document ID assignment has been fixed. +- Replication prevention message during version up detection has been improved (#686). +- `Keep A` and `Keep B` on Conflict resolving dialogue has been renamed to `Use Base` and `Use Conflicted` (#691). + +### Improved + +- Metadata and content-size unmatched documents are now detected and reported, prevented to be applied to the storage. + - This behaviour can be configured in `Patch` -> `Edge case addressing (Behaviour)` -> `Process files even if seems to be corrupted` + - Note: this toggle is for the direct-database-manipulation users. + +### New Features + +- `Scan for Broken files` has been implemented on `Hatch` -> `TroubleShooting`. + +### Refactored + +- Off-loaded processes have been refactored for the better maintainability. + - Files prefixed `bg.worker` are now work on the worker threads. + - Files prefixed `bgWorker.` are now also controls these worker threads. (I know what you want to say... I will rename them). +- Removed unused code. + +## ~~0.25.5~~ 0.25.6 + +(0.25.5 has been withdrawn due to a bug in the `Fetch chunks on demand` feature). + +9th August, 2025 + +### Fixed + +- Storage scanning no longer occurs when `Suspend file watching` is enabled (including boot-sequence). + - This change improves safety when troubleshooting or fetching the remote database. +- `Fetch chunks on demand` is now working again (if you installed 0.25.5, other versions are not affected). + +### Improved + +- Saving notes and files now consumes less memory. + - Data is no longer fully buffered in memory and written at once; instead, it is now written in each over-2MB increments. +- Chunk caching is now more efficient. + - Chunks are now managed solely by their count (still maintained as LRU). If memory usage becomes excessive, they will be automatically released by the system-runtime. + - Reverse-indexing is also no longer used. It is performed as scanning caches and act also as a WeakRef thinning. +- Both of them (may) are effective for #692, #680, and some more. + +### Changed + +- `Incubate Chunks in Document` (also known as `Eden`) is now fully sunset. + - Existing chunks can still be read, but new ones will no longer be created. +- The `Compute revisions for chunks` setting has also been removed. + - This feature is now always enabled and is no longer configurable (restoring the original behaviour). +- As mentioned, `Memory cache size (by total characters)` has been removed. + - The `Memory cache size (by total items)` setting is now the only option available (but it has 10x ratio compared to the previous version). + +### Refactored + +- A significant refactoring of the core codebase is underway. + - This is part of our ongoing efforts to improve code maintainability, readability, and to unify interfaces. + - Previously, complex files posed a risk due to a low bus factor. Fortunately, as our devices have become faster and more capable, we can now write code that is clearer and more maintainable (And not so much costs on performance). + - Hashing functions have been refactored into the `HashManager` class and its derived classes. + - Chunk splitting functions have been refactored into the `ContentSplitterCore` class and its derived classes. + - Change tracking functions have been refactored into the `ChangeManager` class. + - Chunk read/write functions have been refactored into the `ChunkManager` class. + - Fetching chunks on demand is now handled separately from the `ChunkManager` and chunk reading functions. Chunks are queued by the `ChunkManager` and then processed by the `ChunkFetcher`, simplifying the process and reducing unnecessary complexity. + - Then, local database access via `LiveSyncLocalDB` has been refactored to use the new classes. +- References to external sources from `commonlib` have been corrected. +- Type definitions in `types.ts` have been refined. +- Unit tests are being added incrementally. + - I am using `Deno` for testing, to simplify testing and coverage reporting. + - While this is not identical to the Obsidian environment, `jest` may also have limitations. It is certainly better than having no tests. + - In other words, recent manual scenario testing has highlighted some shortcomings. + - `pouchdb-test`, used for testing PouchDB with Deno, has been added, utilising the `memory` adapter. + +Side note: Although class-oriented programming is sometimes considered an outdated style, However, I have come to re-evaluate it as valuable from the perspectives of maintainability and readability. + +## 0.25.4 + +29th July, 2025 + +### Fixed + +- The PBKDF2Salt is no longer corrupted when attempting replication while the device is offline. (#686) + - If this issue has already occurred, please use `Maintenance` -> `Rebuilding Operations (Remote Only)` -> `Overwrite Remote` and `Send` to resolve it. + - Please perform this operation on the device that is most reliable. + - I am so sorry for the inconvenience; there are no patching workarounds. The rebuilding operation is the only solution. + - This issue only affects the encryption of the remote database and does not impact the local databases on any devices. + - (Preventing synchronisation is by design and expected behaviour, even if it is sometimes inconvenient. This is also why we should avoid using workarounds; it is, admittedly, an excuse). + - In any case, we can unlock the remote from the warning dialogue on receiving devices. We are performing replication, instead of simple synchronisation at the expense of a little complexity (I would love to express thank you again for your every effort to manage and maintain the settings! Your all understanding saves our notes). + - This process may require considerable time and bandwidth (as usual), so please wait patiently and ensure a stable network connection. + +### Side note + +The PBKDF2Salt will be referred to as the `Security Seed`, and it is used to derive the encryption key for replication. Therefore, it should be stored on the server prior to synchronisation. We apologise for the lack of explanation in previous updates! + +## 0.25.3 + +22nd July, 2025 + +### Fixed + +- Now the `Doctor` at migration will save the configuration. + +## 0.25.2 ~~0.25.1~~ + +(0.25.1 was missed due to a mistake in the versioning process). +19th July, 2025 + +### Refined and New Features + +- Fetching the remote database on `RedFlag` now also retrieves remote configurations optionally. + - This is beneficial if we have already set up another device and wish to use the same configuration. We will see a much less frequent `Unmatched` dialogue. +- The setup wizard using Set-up URI and QR code has been improved. + - The message is now more user-friendly. + - The obsolete method (manual setting application) has been removed. + - The `Cancel` button has been added to the setup wizard. + - We can now fetch the remote configuration from the server if it exists, which is useful for adding new devices. + - Mostly same as a `RedFlag` fetching remote configuration. + - We can also use the `Doctor` to check and fix the imported (and fetched) configuration before applying it. + +### Changes + +- The Set-up URI is now encrypted with a new encryption algorithm (mostly the same as `V2`). + - The new Set-up URI is not compatible with version 0.24.x or earlier. + +## 0.25.0 + +### Fixed + +- The encryption algorithm now uses HKDF with a master key. + - This is more robust and faster than the previous implementation. + - It is now more secure against rainbow table attacks. + - The previous implementation can still be used via `Patches` -> `End-to-end encryption algorithm` -> `Force V1`. + - Note that `V1: Legacy` can decrypt V2, but produces V1 output. +- `Fetch everything from the remote` now works correctly. + - It no longer creates local database entries before synchronisation. +- Extra log messages during QR code decoding have been removed. + +### Changed + +- The following settings have been moved to the `Patches` pane: + - `Remote Database Tweak` + - `Incubate Chunks in Document` + - `Data Compression` + +### Behavioural and API Changes + +- `DirectFileManipulatorV2` now requires new settings (as you may already know, E2EEAlgorithm). +- The database version has been increased to `12` from `10`. + - If an older version is detected, we will be notified and synchronisation will be paused until the update is acknowledged. (It has been a long time since this behaviour was last encountered; we always err on the side of caution, even if it is less convenient.) + +### Refactored + +- `couchdb_utils.ts` has been separated into several explicitly named files. +- Some missing functions in `bgWorker.mock.ts` have been added. diff --git a/docs/releases/1.0-previews.md b/docs/releases/1.0-previews.md new file mode 100644 index 00000000..d4940dc2 --- /dev/null +++ b/docs/releases/1.0-previews.md @@ -0,0 +1,161 @@ +# 1.0 preview release history + +This document records the opt-in beta and release-candidate builds published before 1.0.0. Most users upgrading from 0.25.83 only need the consolidated [1.0.0 release notes](../../updates.md). + +The prepared `1.0.0-rc.0` tag was not published as a plug-in release and is therefore omitted. + +## 1.0.0-rc.1 + +27th July, 2026 + +The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). + +### Important + +- This candidate retains the plug-in behaviour prepared for rc.0. The version was advanced because release tags are immutable; rc.0 was stopped during CLI validation before a plug-in release was published. +- This remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. The exact rc.1 plug-in and CLI artefacts will be validated separately after publication. + +### CLI and release validation + +- CLI release validation now generates Setup URIs through the supported ESM package interface, allowing the Docker test to reach the CLI container instead of stopping during test preparation. +- The CLI Docker image now assigns its entrypoint permissions explicitly, so non-root execution does not depend on permissions inherited from the source checkout. +- Release finalisation now explicitly dispatches the CLI container workflow when CLI publication is selected, rather than relying on a workflow-created tag to start another workflow. +- Focused regression tests guard the ESM execution mode and deterministic container entrypoint permissions, while the existing release-workflow tests now require explicit CLI dispatch with non-dry-run, immutable-tag inputs. + +### Testing + +- The native CLI setup, put, cat, list, information, deletion, conflict-resolution, and revision-retrieval scenario completed with the packaged Commonlib dependency. +- The same scenario completed through the rebuilt non-root Docker image. +- The focused CLI and release-workflow unit tests passed after first demonstrating all three regressions against the unmodified implementation. + +## 1.0.0-beta.5 + +26th July, 2026 + +### Improved + +- **Inspect conflicts and file/database differences** now compares the current Vault file with the database winner and every live conflict revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain. +- Each reported file and live revision now has a compact wrench menu. Its available actions can compare readable text, apply the selected revision to the Vault, record an exact byte match, store the Vault content as a child of the selected branch, retry retrieving missing chunks without changing the revision tree, or discard only the selected live branch after confirmation. + +### Fixed + +- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent. + +### Testing + +- Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions. + +## 1.0.0-beta.4 + +25th July, 2026 + +### Improved + +- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation. +- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**. +- Text in setup and review dialogues can now be selected for copying or translation. +- When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue. + +### Fixed + +- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device. +- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message. + +### Testing + +- Added regressions for revision repair, command availability, selectable dialogues, conflict-aware chunk reachability, device-progress safeguards, and compaction timeouts. +- Added a real CouchDB integration test for logical chunk deletion, shared and conflict chunk retention, compaction completion, downstream replication, and content-addressed chunk recreation. +- Added a real Obsidian encrypted reconnect scenario which replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation refreshes it without restoring the old value, and proves a bidirectional encrypted round-trip. + +## 1.0.0-beta.3 + +24th July, 2026 + +### Improved + +- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices. +- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits. +- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`. +- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation. +- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review. + +### Fixed + +- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings. +- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart. + +### Testing + +- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues. + +## 1.0.0-beta.2 + +23rd July, 2026 + +### Improved + +- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved. + +### Fixed + +- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred. + +### Testing + +- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages. + +## 1.0.0-beta.1 + +22nd July, 2026 + +### Important + +- This corrected opt-in integration preview follows `1.0.0-beta.0` and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault. + +### Fixed + +- Conflict resolutions made on another device no longer recreate the same conflict when the receiving Vault still contains the exact content of the deleted losing revision. Automatic text and structured-data merge now uses the nearest revision actually shared by both branches instead of inferring ancestry from revision generation numbers. +- Edits, deletions, and renames made while a file is conflicted now extend the exact revision displayed on that device. If LiveSync cannot prove the displayed branch, it preserves the affected branches for review instead of silently applying the operation to the database winner. + +### Testing + +- Added revision-tree regressions and focused real-Obsidian scenarios for propagated resolutions and file operations performed while a conflict remains active. + +## 1.0.0-beta.0 + +22nd July, 2026 + +### Important + +- This is an opt-in 1.0 integration preview for BRAT and testing with existing Vaults. It does not replace the latest stable release. Use it with a current backup, and update every participating device before resuming synchronisation. +- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. The review preserves the existing automatic synchronisation choices and resumes them only after the decision has been saved successfully. + +### Improved + +- An unconfigured installation now waits for you to start setup. A long-lived Notice offers the setup action, and **Open onboarding wizard** remains available from the command palette instead of the dialogue opening automatically. +- The setup wizard now creates named remote profiles for CouchDB, Object Storage, and P2P. Current Setup URIs preserve their profile names and selections, and the wizard reserves Rebuild or Fetch before the ordinary start-up scan begins. +- Peer-to-Peer Synchronisation (P2P) and Hidden File Sync are supported opt-in features. JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 remain previews. Customisation Sync remains a supported advanced workflow. +- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the Data Compression specification. +- Compatibility review now runs before Config Doctor without overlapping it. Existing Vaults retain their automatic synchronisation choices and explicit file-name case setting. For installations created by earlier releases, LiveSync preserves whether setup had been completed and saves a missing legacy case setting as case-insensitive. +- P2P connections now restart reliably after settings are reapplied or the local database is reset. Setup on an additional device asks you to select the source device once. Disconnecting leaves the LiveSync room and closes its signalling relay connections so that reconnecting can establish a new room. +- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in issue #555. +- Warnings about estimated remote storage size now appear as long-lived clickable Notices instead of timed dialogues. Initial uploads and Rebuild operations no longer prompt to send every chunk in advance; ordinary replication completes the transfer. +- Removed the obsolete **Use the trash bin** control and the setting for fixed chunk revisions. Remote deletion still follows Obsidian's preference, and chunk revisions remain content-derived. The Change Log remains available but no longer opens automatically or tracks unread versions. + +### Fixed + +- The optional Custom HTTP Handler used by Object Storage now sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. +- When selectors, ignore files, size limits, modification-time limits, or file-name case settings are broadened, LiveSync now rechecks previously received files without requiring another remote update. +- P2P setup on the first device no longer displays reset or upload steps for a central database, and Config Doctor now offers its chunk size recommendation for CouchDB only when a CouchDB remote profile is selected. + +### Security + +- Fly.io setup now generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates prevent excessive CPU use from specially crafted path patterns and `mailto:` links. The CLI rejects path traversal and symbolic-link components detected before Vault operations. + +### Miscellaneous + +- Self-hosted LiveSync now owns its translation catalogue. Commonlib provides English messages to other applications, and translation contributions can be made directly to the Self-hosted LiveSync repository. + +### Testing + +- Expanded automated testing in Obsidian for upgrades, synchronisation between two devices, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, and clean-up after failures. diff --git a/docs/releases/legacy.md b/docs/releases/legacy.md new file mode 100644 index 00000000..e238bff8 --- /dev/null +++ b/docs/releases/legacy.md @@ -0,0 +1,1794 @@ +# Legacy release history + +This history covers releases before 0.25. Later releases are recorded in the [0.25 history](0.25.md) and the [current release history](../../updates.md). + +## 0.24 + +I know that we have been waiting for a long time. It is finally released! + +Over the past three years since the inception of the plugin, various features have been implemented to address diverse user needs. This is truly honourable, and I am grateful for your years of support. However, this process has led to an increasingly disorganised codebase, with features becoming entangled. Consequently, this has led to a situation where bugs can go unnoticed and resolving one issue may inadvertently introduce another. + +In 0.24.0, I reorganised the previously jumbled main codebase into clearly defined modules. Although I had assumed that the total size of the code would not increase, I discovered that it has in fact increased. While the complexity is still considerable, the refactoring has improved the clarity of the code's structure. Additionally, while testing the release candidates, we still found many bugs to fix, which helped to make this plug-in robust and stable. Therefore, we are now ready to use the updated plug-in, and in addition to that, proceed to the next step. + +This is also the first step towards a fully-fledged-fancy LiveSync, not just a plug-in from Obsidian. Of course, it will still be a plug-in primarily and foremost, but this development marks a significant step towards the self-hosting concept. + +Finally, I would like to once again express my respect and gratitude to all of you. My gratitude extends to all of the dev testers! Your contributions have certainly made the plug-in robust and stable! + +Thank you, and I hope your troubles will be resolved! + +--- + +## 0.24.31 + +10th July, 2025 + +### Fixed + +- The description of `Enable Developers' Debug Tools.` has been refined. + - Now performance impact is more clearly stated. +- Automatic conflict checking and resolution has been improved. + - It now works parallelly for each other file, instead of sequentially. It makes significantly faster on first synchronisation when with local files information. +- Resolving conflicts dialogue will not be shown for the multiple files at once. + - It will be shown for each file, one by one. + +## 0.24.30 + +9th July, 2025 + +### New Feature + +- New chunking algorithm `V3: Fine deduplication` has been added, and will be recommended after updates. + - The Rabin-Karp algorithm is used for efficient chunking. + - This will be the default in the new installations. + - It is more robust and faster than the previous one. + - We can change it in the `Advanced` pane of the settings. +- New language `ko` (Korean) has been added. + - Thank you for your contribution, [@ellixspace](https://x.com/ellixspace)! + - Any contributions are welcome, from any route. Please let me know if I seem to be unaware of this. It is often the case that I am not really aware of it. +- Chinese (Simplified) translation has been updated. + - Thank you for your contribution, [@52sanmao](https://github.com/52sanmao)! + +### Fixed + +- Numeric settings are now never lost the focus during value changing. +- Doctor now redacts more sensitive information on error reports. + +### Improved + +- All translations have been rewritten into YAML format, to easier to manage and contribute. + - We can write them with comments, newlines, and other YAML features. +- Doctor recommendations are now shown in a user-friendly notation. + - We can now see the recommended as `V3: Fine deduplication` instead of `v3-rabin-karp`. + +### Refactored + +- Never-ending `ObsidianLiveSyncSettingTab.ts` has finally been separated into each pane's file. +- Some commented-out code has been removed. + +### Acknowledgement + +- Jun Murakami, Shun Ishiguro, and Yoshihiro Oyama. 2012. Implementation and Evaluation of a Cache Deduplication Mechanism with Content-Defined Chunking. In _IPSJ SIG Technical Report_, Vol.2012-ARC-202, No.4. Information Processing Society of Japan, 1-7. + +## 0.24.29 + +20th June, 2025 + +### Fixed + +- Synchronisation with buckets now works correctly, regardless of whether a prefix is set or the bucket has been (re-) initialised (#664). +- An information message is now displayed again, during any automatic synchronisation is enabled (#662). + +### Tidied up + +- Importing paths have been tidied up. + +## 0.24.28 + +15th June, 2025 + +### Fixed + +- Batch Update is no longer available in LiveSync mode to avoid unexpected behaviour. (#653) +- Now compatible with Cloudflare R2 again for bucket synchronisation. + - @edo-bari-ikutsu, thank you for [your contribution](https://github.com/vrtmrz/livesync-commonlib/pull/12)! +- Prevention of broken behaviour due to database connection failures added (#649). + +## 0.24.27 + +10th June, 2025 + +### Improved + +- We can use prefix for path for the Bucket synchronisation. + - For example, if you set the `vaultName/` as a prefix for the bucket in the root directory, all data will be transferred to the bucket under the `vaultName/` directory. +- The "Use Request API to avoid `inevitable` CORS problem" option is now promoted to the normal setting, not a niche patch. + +### Fixed + +- Now switching replicators applied immediately, without the need to restart Obsidian. + +### Tidied up + +- Some dependencies have been updated to the latest version. + +## 0.24.26 + +14th May, 2025 + +This update introduces an option to circumvent Cross-Origin Resource Sharing +(CORS) constraints for CouchDB requests, by leveraging Obsidian's native request +API. The implementation of such a feature had previously been deferred due to +significant security considerations. + +CORS is a vital security mechanism, enabling servers like CouchDB -- which +functions as a sophisticated REST API -- to control access from different +origins, thereby ensuring secure communication across trust boundaries. I had +long hesitated to offer a CORS circumvention method, as it deviates from +security best practices; My preference was for users to configure CORS correctly +on the server-side. + +However, this policy has shifted due to specific reports of intractable +CORS-related configuration issues, particularly within enterprise proxy +environments where proxy servers can unpredictably alter or block +communications. Given that a primary objective of the "Self-hosted LiveSync" +plugin is to facilitate secure Obsidian usage within stringent corporate +settings, addressing these 'unavoidable' user-reported problems became +essential. Mostly raison d'être of this plugin. + +Consequently, the option "Use Request API to avoid `inevitable` CORS problem" +has been implemented. Users are strongly advised to enable this _only_ when +operating within a trusted environment. We can enable this option in the `Patch` pane. + +However, just to whisper, this is tremendously fast. + +### New Features + +- Automatic display-language changing according to the Obsidian language + setting. + - We will be asked on the migration or first startup. + - **Note: Please revert to the default language if you report any issues.** + - Not all messages are translated yet. We welcome your contribution! +- Now we can limit files to be synchronised even in the hidden files. +- "Use Request API to avoid `inevitable` CORS problem" has been implemented. + - Less secure, please use it only if you are sure that you are in the trusted + environment and be able to ignore the CORS. No `Web viewer` or similar tools + are recommended. (To avoid the origin forged attack). If you are able to + configure the server setting, always that is recommended. +- `Show status icon instead of file warnings banner` has been implemented. + - If enabled, the ⛔ icon will be shown inside the status instead of the file + warnings banner. No details will be shown. + +### Improved + +- All regular expressions can be inverted by prefixing `!!` now. + +### Fixed + +- No longer unexpected files will be gathered during hidden file sync. +- No longer broken `\n` and new-line characters during the bucket + synchronisation. +- We can purge the remote bucket again if we using MinIO instead of AWS S3 or + Cloudflare R2. +- Purging the remote bucket is now more reliable. + - 100 files are purged at a time. +- Some wrong messages have been fixed. + +### Behaviour changed + +- Entering into the deeper directories to gather the hidden files is now limited + by `/` or `\/` prefixed ignore filters. (It means that directories are scanned + deeper than before). + - However, inside the these directories, the files are still limited by the + ignore filters. + +### Etcetera + +- Some code has been tidied up. +- Trying less warning-suppressing and be more safer-coding. +- Dependent libraries have been updated to the latest version. +- Some build processes have been separated to `pre` and `post` processes. + +## 0.24.25 + +22nd April, 2025 + +### Improved + +- Peer-to-peer synchronisation has been got more robust. + +### Fixed + +- No longer broken falsy values in settings during set-up by the QR code + generation. + +### Refactored + +- Some `window` references now have pointed to `globalThis`. +- Some sloppy-import has been fixed. +- A server side implementation `Synchromesh` has been suffixed with `deno` + instead of `server` now. + +## 0.24.24 + +15th April, 2025 + +### Fixed + +- No longer broken JSON files including `\n`, during the bucket synchronisation. + (#623) +- Custom headers and JWT tokens are now correctly sent to the server during + configuration checking. (#624) + +### Improved + +- Bucket synchronisation has been enhanced for better performance and + reliability. + - Now less duplicated chunks are sent to the server. Note: If you have + encountered about too less chunks, please let me know. However, you can send + it to the server by `Overwrite remote`. + - Fetching conflicted files from the server is now more reliable. + - Dependent libraries have been updated to the latest version. + - Also, let me know if you have encountered any issues with this update. + Especially you are using a device that has been in use for a little + longer. + +## 0.24.23 + +10th April, 2025 + +### New Feature + +- Now, we can send custom headers to the server. + - They can be sent to either CouchDB or Object Storage. +- Authentication with JWT in CouchDB is now supported. + - I will describe steps later, but please refer to the + [CouchDB document](https://docs.couchdb.org/en/stable/config/auth.html#authentication-configuration). + - A JWT keypair for testing can be generated in the setting dialogue. + +### Improved + +- The QR Code for set-up can be shown also from the setting dialogue now. +- Conflict checking for preventing unexpected overwriting on the boot-up process + has been quite faster. + +### Fixed + +- Some bugs on Dev and Testing modules have been fixed. + +## 0.24.22 ~~0.24.21~~ + +1st April, 2025 + +(Really sorry for the confusion. I have got a miss at releasing...). + +### Fixed + +- No longer conflicted files are handled in the boot-up process. No more + unexpected overwriting. + - It ignores `Always overwrite with a newer file`, and always be prevented for + the safety. Please pick it manually or open the file. +- Some log messages on conflict resolution has been corrected. +- Automatic merge notifications, displayed on the grounds of `same`, have been + degraded to logs. + +### Improved + +- Now we can fetch the remote database with keeping local files completely + intact. + - In new option, all files are stored into the local database before the + fetching, and will be merged automatically or detected as conflicts. +- The dialogue presenting options when performing `Fetch` are now more + informative. + +### Refactored + +- Some class methods have been fixed its arguments to be more consistent. +- Types have been defined for some conditional results. + +## 0.24.20 + +24th March, 2025 + +### Improved + +- Now we can see the detail of `TypeError` using Obsidian API during remote + database access. + +### Behaviour and default changed + +- **NOW INDEED AND ACTUALLY** `Compute revisions for chunks` are backed into + enabled again. it is necessary for garbage collection of chunks. + - As far as existing users are concerned, this will not automatically change, + but the Doctor will inform us. + +## 0.24.19 + +5th March, 2025 + +### New Feature + +- Now we can generate a QR Code for transferring the configuration to another device. + - This QR Code can be scanned by the camera app or something QR Code Reader of another device, and via Obsidian URL, the configuration will be transferred. + - Note: This QR Code is not encrypted. So, please be careful when transferring the configuration. + +## 0.24.18 + +28th February, 2025 + +### Fixed + +- Now no chunk creation errors will be raised after switching `Compute revisions for chunks`. +- Some invisible file can be handled correctly (e.g., `writing-goals-history.csv`). +- Fetching configuration from the server is now saves the configuration immediately (if we are not in the wizard). + +### Improved + +- Mismatched configuration dialogue is now more informative, and rewritten to more user-friendly. +- Applying configuration mismatch is now without rebuilding (at our own risks). +- Now, rebuilding is decided more fine grained. + +### Improved internally + +- Translations can be nested. i.e., task:`Some procedure`, check: `%{task} checking`, checkfailed: `%{check} failed` produces `Some procedure checking failed`. + - Max to 10 levels of nesting + +## 0.24.17 + +27th February, 2025 + +Confession. I got the default values wrong. So scary and sorry. + +## 0.24.16 + +### Improved + +#### Peer-to-Peer + +- Now peer-to-peer synchronisation checks the settings are compatible with each other. + - No longer unexpected database broken, phew. +- Peer-to-peer synchronisation now handles the platform and detects pseudo-clients. + - Pseudo clients will not decrypt/encrypt anything, just relay the data. Hence, always settings are not compatible. Therefore, we have to accept the incompatibility for pseudo clients. + +#### General + +- New migration method has been implemented, that called `Doctor`. + + - `Doctor` checks the difference between the ideal and actual values and encourages corrective action. To facilitate our decision, the reasons for this and the recommendations are also presented. + - This can be used not only during migration. We can invoke the doctor from the settings for trouble-shooting. + +- The minimum interval for replication to be caused when an event occurs can now be configurable. +- Some detail note has been added and change nuance about the `Report` in the setting dialogue, which had less informative. + +### Behaviour and default changed + +- `Compute revisions for chunks` are backed into enabled again. it is necessary for garbage collection of chunks. + - As far as existing users are concerned, this will not automatically change, but the Doctor will inform us. + +### Refactored + +- Platform specific codes are more separated. No longer `node` modules were used in the browser and Obsidian. + +## 0.24.15 + +### Fixed + +- Now, even without WeakRef, Polyfill is used and the whole thing works without error. However, if you can switch WebView Engine, it is recommended to switch to a WebView Engine that supports WeakRef. + +## 0.24.14 + +### Fixed + +- Resolving conflicts of JSON files (and sensibly merging them) is now working fine, again! + - And, failure logs are more informative. +- More robust to release the event listeners on unwatching the local database. + +### Refactored + +- JSON file conflict resolution dialogue has been rewritten into svelte v5. +- Upgrade eslint. +- Remove unnecessary pragma comments for eslint. + +## 0.24.13 + +Sorry for the lack of replies. The ones that were not good are popping up, so I am just going to go ahead and get this one... However, they realised that refactoring and restructuring is about clarifying the problem. Your patience and understanding is much appreciated. + +### Fixed + +#### General Replication + +- No longer unexpected errors occur when the replication is stopped during for some reason (e.g., network disconnection). + +#### Peer-to-Peer Synchronisation + +- Set-up process will not receive data from unexpected sources. +- No longer resource leaks while enabling the `broadcasting changes` +- Logs are less verbose. +- Received data is now correctly dispatched to other devices. +- `Timeout` error now more informative. +- No longer timeout error occurs for reporting the progress to other devices. +- Decision dialogues for the same thing are not shown multiply at the same time anymore. +- Disconnection of the peer-to-peer synchronisation is now more robust and less error-prone. + +#### Webpeer + +- Now we can toggle Peers' configuration. + +### Refactored + +- Cross-platform compatibility layer has been improved. +- Common events are moved to the common library. +- Displaying replication status of the peer-to-peer synchronisation is separated from the main-log-logic. +- Some file names have been changed to be more consistent. + +## 0.24.12 + +I created a SPA called [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) (well, right... I will think of a name again), which replaces the server when using Peer-to-Peer synchronisation. This is a pseudo-client that appears to other devices as if it were one of the clients. . As with the client, it receives and sends data without storing it as a file. +And, this is just a single web page, without any server-side code. It is a static web page that can be hosted on any static web server, such as GitHub Pages, Netlify, or Vercel. All you have to do is to open the page and enter several items, and leave it open. + +### Fixed + +- No longer unnecessary acknowledgements are sent when starting peer-to-peer synchronisation. + +### Refactored + +- Platform impedance-matching-layer has been improved. + - And you can see the actual usage of this on [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) that a pseudo client for peer-to-peer synchronisation. +- Some UIs have been got isomorphic among Obsidian and web applications (for `webpeer`). + +## 0.24.11 + +Peer-to-peer synchronisation has been implemented! + +Until now, I have not provided a synchronisation server. More people may not +even know that I have shut down the test server. I confess that this is a bit +repetitive, but I confess it is a cautionary tale. This is out of a sense of +self-discipline that someone has occurred who could see your data. Even if the +'someone' is me. I should not be unaware of its superiority, even though +well-meaning and am a servant of all. (Half joking, but also serious). However, +now I can provide you with a signalling server. Because, to the best of my +knowledge, it is only the network that is connected to your device. Also, this +signalling server is just a Nostr relay, not my implementation. You can run your +implementation, which you consider trustworthy, on a trustworthy server. You do +not even have to trust me. Mate, it is great, isn't it? For your information, +strfry is running on my signalling server. + +Nevertheless, that being said, to be more honest, I still have not decided what +to do with this signalling server if too much traffic comes in. + +Note: Already you have noticed this, but let me mention it again, this is a +significantly large update. If you have noticed anything, please let me know. I +will try to fix it as soon as possible (Some address is on my +[profile](https://github.com/vrtmrz)). + +### Improved + +- New Translation: `es` (Spanish) by @zeedif (Thank you so much)! +- Now all of messages can be selectable and copyable, also on the iPhone, iPad, and Android devices. Now we can copy or share the messages easily. + +### New Feature + +- Peer-to-Peer Synchronisation has been implemented! + - This feature is still in early beta, and it is recommended to use it with caution. + - However, it is a significant step towards the self-hosting concept. It is now possible to synchronise your data without using any remote database or storage. It is a direct connection between your devices. + - Note: We should keep the device online to synchronise the data. It is not a background synchronisation. Also it needs a signalling server to establish the connection. But, the signalling server is used only for establishing the connection, and it does not store any data. + +### Fixed + +- No longer memory or resource leaks when the plug-in is disabled. +- Now deleted chunks are correctly detected on conflict resolution, and we are guided to resurrect them. +- Hanging issue during the initial synchronisation has been fixed. +- Some unnecessary logs have been removed. +- Now all modal dialogues are correctly closed when the plug-in is disabled. + +### Refactor + +- Several interfaces have been moved to the separated library. +- Translations have been moved to each language file, and during the build, they are merged into one file. +- Non-mobile friendly code has been removed and replaced with the safer code. + - (Now a days, mostly server-side engine can use webcrypto, so it will be rewritten in the future more). +- Started writing Platform impedance-matching-layer. +- Svelte has been updated to v5. +- Some function have got more robust type definitions. +- Terser optimisation has slightly improved. +- During the build, analysis meta-file of the bundled codes will be generated. + +## 0.24.10 + +### Fixed + +- Fixed the issue which the filename is shown as `undefined`. +- Fixed the issue where files transferred at short intervals were not reflected. + +### Improved + +- Add more translations: `ja-JP` (Japanese) by @kohki-shikata (Thank you so much)! + +### Internal + +- Some files have been prettified. + +## 0.24.9 + +Skipped. + +## 0.24.8 + +### Fixed + +- Some parallel-processing tasks are now performed more safely. +- Some error messages has been fixed. + +### Improved + +- Synchronisation is now more efficient and faster. +- Saving chunks is a bit more robust. + +### New Feature + +- We can remove orphaned chunks again, now! + - Without rebuilding the database! + - Note: Please synchronise devices completely before removing orphaned chunks. + - Note2: Deleted files are using chunks, if you want to remove them, please commit the deletion first. (`Commit File Deletion`) + - Note3: If you lost some chunks, do not worry. They will be resurrected if not so much time has passed. Try `Resurrect deleted chunks`. + - Note4: This feature is still beta. Please report any issues you encounter. + - Note5: Please disable `On demand chunk fetching`, and enable `Compute revisions for each chunk` before using this feature. + - These settings is going to be default in the future. + +## 0.24.7 + +### Fixed (Security) + +- Assigning IDs to chunks has been corrected for more safety. + - Before version 0.24.6, there were possibilities in End-to-End encryption where a brute-force attack could be carried out against an E2EE passphrase via a chunk ID if a zero-byte file was present. Now the chunk ID should be assigned more safely, and not all of passphrases are used for generating the chunk ID. + - This is a security fix, and it is recommended to update and rebuild database to this version as soon as possible. + - Note: It keeps the compatibility with the previous versions, but the chunk ID will be changed for the new files and modified files. Hence, deduplication will not work for the files which are modified after the update. It is recommended to rebuild the database to avoid the potential issues, and reduce the database size. + - Note2: This fix is only for with E2EE. Plain synchronisation is not affected by this issue. + +### Fixed + +- Now the conflict resolving dialogue is automatically closed after the conflict has been resolved (and transferred from other devices; or written by some other resolution). +- Resolving conflicts by timestamp is now working correctly. + - It also fixes customisation sync. + +### Improved + +- Notifications can be suppressed for the hidden files update now. +- No longer uses the old-xxhash and sha1 for generating the chunk ID. Chunk ID is now generated with the new algorithm (Pure JavaScript hash implementation; which is using Murmur3Hash and FNV-1a now used). + +## 0.24.6 + +### Fixed (Quick Fix) + +- Fixed the issue of log is not displayed on the log pane if the pane has not been shown on startup. + - This release is only for it. However, fixing this had been necessary to report any other issues. + +## 0.24.5 + +### Fixed + +- Fixed incorrect behaviour when comparing objects with undefined as a property value. + +### Improved + +- The status line and the log summary are now displayed more smoothly and efficiently. + - This improvement has also been applied to the logs displayed in the log pane. + +## 0.24.4 + +### Fixed + +- Fixed so many inefficient and buggy modules inherited from the past. + +### Improved + +- Tasks are now executed in an efficient asynchronous library. +- On-demand chunk fetching is now more efficient and keeps the interval between requests. + - This will reduce the load on the server and the network. + - And, safe for the Cloudant. + +## 0.24.3 + +### Improved + +- Many messages have been improved for better understanding as thanks to the fine works of @Volkor3-16! Thank you so much! +- Documentations also have been updated to reflect the changes in the messages. +- Now the style of In-Editor Status has been solid for some Android devices. + +## 0.24.2 + +### Rewritten + +- Hidden File Sync is now respects the file changes on the storage. Not simply comparing modified times. + - This makes hidden file sync more robust and reliable. + +### Fixed + +- `Scan hidden files before replication` is now configurable again. +- Some unexpected errors are now handled more gracefully. +- Meaningless event passing during boot sequence is now prevented. +- Error handling for non-existing files has been fixed. +- Hidden files will not be batched to avoid the potential error. + - This behaviour had been causing the error in the previous versions in specific situations. +- The log which checking automatic conflict resolution is now in verbose level. +- Replication log (skipping non-targetting files) shows the correct information. +- The dialogue that asking enabling optional feature during `Rebuild Everything` now prevents to show the `overwrite` option. + - The rebuilding device is the first, meaningless. +- Files with different modified time but identical content are no longer processed repeatedly. +- Some unexpected errors which caused after terminating plug-in are now avoided. +- + +### Improved + +- JSON files are now more transferred efficiently. + - Now the JSON files are transferred in more fine chunks, which makes the transfer more efficient. + +## 0.24.1 + +### Fixed + +- Vault History can show the correct information of match-or-not for each file and database even if it is a binary file. +- `Sync settings via markdown` is now hidden during the setup wizard. +- Verify and Fix will ignore the hidden files if the hidden file sync is disabled. + +#### New feature + +- Now we can fetch the tweaks from the remote database while the setting dialogue and wizard are processing. + +### Improved + +- More things are moved to the modules. + - Includes the Main codebase. Now `main.ts` is almost stub. +- EventHub is now more robust and typesafe. + +## 0.24.0 + +### Improved + +- The welcome message is now more simple to encourage the use of the Setup-URI. + - The secondary message is also simpler to guide users to Minimal Setup. + - But Setup-URI will be recommended again, due to its importance. + - These dialogues contain a link to the documentation which can be clicked. +- The minimal setup is more minimal now. And, the setup is more user-friendly. + - Now the Configuration of the remote database is checked more robustly, but we can ignore the warning and proceed with the setup. +- Before we are asked about each feature, we are asked if we want to use optional features in the first place. + - This is to prevent the user from being overwhelmed by the features. + - And made it clear that it is not recommended for new users. +- Many messages have been improved for better understanding. + - Ridiculous messages have been (carefully) refined. + - Dialogues are more informative and friendly. + - A lot of messages have been mostly rewritten, leveraging Markdown. + - Especially auto-closing dialogues are now explicitly labelled: `To stop the countdown, tap anywhere on the dialogue`. +- Now if the is plugin configured to ignore some events, we will get a chance to fix it, in addition to the warning. + - And why that has happened is also explained in the dialogue. +- A note relating to device names has been added to Customisation Sync on the setting dialogue. +- We can verify and resolve also the hidden files now. + +### Fixed + +- We can resolve the conflict of the JSON file correctly now. +- Verifying files between the local database and storage is now working correctly. +- While restarting the plug-in, the shown dialogues will be automatically closed to avoid unexpected behaviour. +- Replicated documents that the local device has configured to ignore are now correctly ignored. +- The chunks of the document on the local device during the first transfer will be created correctly. + - And why we should create them is now explained in the dialogue. +- If optional features have been enabled in the wizard, `Enable advanced features` will be toggled correctly. + The hidden file sync is now working correctly. - Now the deletion of hidden files is correctly synchronised. +- Customisation Sync is now working correctly together with hidden file sync. +- No longer database suffix is stored in the setting sharing markdown. +- A fair number of bugs have been fixed. + +### Changed + +- Some default settings have been changed for an easier new user experience. + - Preventing the meaningless migration of the settings. + +### Tiding + +- The codebase has been reorganised into clearly defined modules. +- Commented-out codes have been gradually removed. + +### 0.23.0 + +Incredibly new features! + +Now, we can use object storage (MinIO, S3, R2 or anything you like) for synchronising! Moreover, despite that, we can use all the features as if we were using CouchDB. +Note: As this is a pretty experimental feature, hence we have some limitations. + +- This is built on the append-only architecture. It will not shrink used storage if we do not perform a rebuild. +- A bit fragile. However, our version x.yy.0 is always so. +- When the first synchronisation, the entire history to date is transferred. For this reason, it is preferable to do this under the WiFi network. +- Do not worry, from the second synchronisation, we always transfer only differences. + +I hope this feature empowers users to maintain independence and self-host their data, offering an alternative for those who prefer to manage their own storage solutions and avoid being stuck on the right side of a sudden change in business model. + +Of course, I use Self-hosted MinIO for testing and recommend this. It is for the same reason as using CouchDB. -- open, controllable, auditable and indeed already audited by numerous eyes. + +Let me write one more acknowledgement. + +I have a lot of respect for that plugin, even though it is sometimes treated as if it is a competitor, remotely-save. I think it is a great architecture that embodies a different approach to my approach of recreating history. This time, with all due respect, I have used some of its code as a reference. +Hooray for open source, and generous licences, and the sharing of knowledge by experts. + +#### Version history + +- 0.23.23: + - Refined: + - Setting dialogue very slightly refined. + - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes. + - Now we have new panes for: + - `Selector` + - `Advanced` + - `Power users` + - `Patches (Edge case)` + - Applying the settings will now be more informative. + - The header bar will be shown for applying the settings which needs a database rebuild. + - Applying methods are now more clearly navigated. + - Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed. + - New features: + - Word-segmented chunk building on users language + - Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line. + - This feature is enabled by default through `Use Segmented-splitter`. + - (Default: Disabled, Please be relived, I have learnt). + - Fixed: + - Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error. + - `Send chunk in bulk` is back to default disabled. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically"). + - Merging conflicts of JSON files are now works fine even if it contains `null`. + - Development: + - Implemented the logic for automatically generating the stub of document for the setting dialogue. +- 0.23.22: + - Fixed: + - Case-insensitive file handling + - Full-lower-case files are no longer created during database checking. + - Bulk chunk transfer + - The default value will automatically adjust to an acceptable size when using IBM Cloudant. +- 0.23.21: + - New Features: + - Case-insensitive file handling + - Files can now be handled case-insensitively. + - This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour). + - Improved chunk revision fixing + - Revisions for chunks can now be fixed for faster chunk creation. + - This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour). + - Bulk chunk transfer + - Chunks can now be transferred in bulk during uploads. + - This feature is enabled by default through `Send chunks in bulk`. + - Creation of missing chunks without + - Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation. + - Improvements: + - File status scanning on the startup + - Quite significant performance improvements. + - No more missing scans of some files. + - Status in editor enhancements + - Significant performance improvements in the status display within the editor. + - Notifications for files that will not be synchronised will now be properly communicated. + - Encryption and Decryption + - These processes are now performed in background threads to ensure fast and stable transfers. + - Verify and repair all files + - Got faster through parallel checking. + - Migration on update + - Migration messages and wizards have become more helpful. + - Behavioural changes: + - Chunk size adjustments + - Large chunks will no longer be created for older, stable files, addressing storage consumption issues. + - Flag file automation + - Confirmation will be shown and we can cancel it. + - Fixed: + - Database File Scanning + - All files in the database will now be enumerated correctly. + - Miscellaneous + - Dependency updated. + - Now, tree shaking is left to terser, from esbuild. +- 0.23.20: + - Fixed: + - Customisation Sync now checks the difference while storing or applying the configuration. + - No longer storing the same configuration multiple times. + - Time difference in the dialogue has been fixed. + - Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved. + - Improved: + - The Enlarging button on the enlarging threshold dialogue now displays the new value. +- 0.23.19: + - Not released. +- 0.23.18: + - New feature: + - Per-file-saved customization sync has been shipped. + - We can synchronise plug-igs etc., more smoothly. + - Default: disabled. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost compatibility with old versions. + - Customisation sync has got beta3. + - We can set `Flag` to each item to select the newest, automatically. + - This configuration is per device. + - Improved: + - Start-up speed has been improved. + - Fixed: + - On the customisation sync dialogue, buttons are kept within the screen. + - No more unnecessary entries on `data.json` for customisation sync. + - Selections are no longer lost while updating customisation items. + - Tidied on source codes: + - Many typos have been fixed. + - Some unnecessary type casting removed. +- 0.23.17: + - Improved: + - Overall performance has been improved by using PouchDB 9.0.0. + - Configuration mismatch detection is refined. We can resolve mismatches more smoothly and naturally. + More detail is on `troubleshooting.md` on the repository. + - Fixed: + - Customisation Sync will be disabled when a corrupted configuration is detected. + Therefore, the Device Name can be changed even in the event of a configuration mismatch. + - New feature: + - We can get a notification about the storage usage of the remote database. + - Default: We will be asked. + - If the remote storage usage approaches the configured value, we will be asked whether we want to Rebuild or increase the limit. +- 0.23.16: + - Maintenance Update: + - Library refining (Phase 1 - step 2). There are no significant changes on the user side. + - Including the following fixes of potentially problems: + - the problem which the path had been obfuscating twice has been resolved. + - Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons. +- 0.23.15: + - Maintenance Update: + - Library refining (Phase 1). There are no significant changes on the user side. +- 0.23.14: + - Fixed: + - No longer batch-saving ignores editor inputs. + - The file-watching and serialisation processes have been changed to the one which is similar to previous implementations. + - We can configure the settings (Especially about text-boxes) even if we have configured the device name. + - Improved: + - We can configure the delay of batch-saving. + - Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct). + - Also, we can configure the limit of delaying batch-saving. + - The performance of showing status indicators has been improved. +- 0.23.13: + - Fixed: + - No longer files have been trimmed even delimiters have been continuous. + - Fixed the toggle title to `Do not split chunks in the background` from `Do not split chunks in the foreground`. + - Non-configured item mismatches are no longer detected. +- 0.23.12: + - Improved: + - Now notes will be split into chunks in the background thread to improve smoothness. + - Default enabled, to disable, toggle `Do not split chunks in the foreground` on `Hatch` -> `Compatibility`. + - If you want to process very small notes in the foreground, please enable `Process small files in the foreground` on `Hatch` -> `Compatibility`. + - We can use a `splitting-limit-capped chunk splitter`; which performs more simple and make less amount of chunks. + - Default disabled, to enable, toggle `Use splitting-limit-capped chunk splitter` on `Sync settings` -> `Performance tweaks` + - Tidied + - Some files have been separated into multiple files to make them more explicit in what they are responsible for. +- 0.23.11: + - Fixed: + - Now we _surely_ can set the device name and enable customised synchronisation. + - Unnecessary dialogue update processes have been eliminated. + - Customisation sync no longer stores half-collected files. + - No longer hangs up when removing or renaming files with the `Sync on Save` toggle enabled. + - Improved: + - Customisation sync now performs data deserialization more smoothly. + - New translations have been merged. +- 0.23.10 + - Fixed: + - No longer configurations have been locked in the minimal setup. +- 0.23.9 + - Fixed: + - No longer unexpected parallel replication is performed. + - Now we can set the device name and enable customised synchronisation again. +- 0.23.8 + - New feature: + - Now we are ready for i18n. + - Patch or PR of `rosetta.ts` are welcome! + - The setting dialogue has been refined. Very controllable, clearly displayed disabled items, and ready to i18n. + - Fixed: + - Many memory leaks have been rescued. + - Chunk caches now work well. + - Many trivial but potential bugs are fixed. + - No longer error messages will be shown on retrieving checkpoint or server information. + - Now we can check and correct tweak mismatch during the setup + - Improved: + - Customisation synchronisation has got more smoother. + - Tidied + - Practically unused functions have been removed or are being prepared for removal. + - Many of the type-errors and lint errors have been corrected. + - Unused files have been removed. + - Note: + - From this version, some test files have been included. However, they are not enabled and released in the release build. + - To try them, please run Self-hosted LiveSync in the dev build. +- 0.23.7 + - Fixed: + - No longer missing tasks which have queued as the same key (e.g., for the same operation to the same file). + - This occurs, for example, with hidden files that have been changed multiple times in a very short period of time, such as `appearance.json`. Thanks for the report! + - Some trivial issues have been fixed. + - New feature: + - Reloading Obsidian can be scheduled until that file and database operations are stable. +- 0.23.6: + - Fixed: + - Now the remote chunks could be decrypted even if we are using `Incubate chunks in Document`. (The note of 0.23.6 has been fixed). + - Chunk retrieving with `Incubate chunks in document` got more efficiently. + - No longer task processor misses the completed tasks. + - Replication is no longer started automatically during changes in window visibility (e.g., task switching on the desktop) when off-focused. +- 0.23.5: + - New feature: + - Now we can check configuration mismatching between clients before synchronisation. + - Default: enabled / Preferred: enabled / We can disable this by the `Do not check configuration mismatch before replication` toggle in the `Hatch` pane. + - It detects configuration mismatches and prevents synchronisation failures and wasted storage. + - Now we can perform remote database compaction from the `Maintenance` pane. + - Fixed: + - We can detect the bucket could not be reachable. + - Note: + - Known inexplicable behaviour: Recently, (Maybe while enabling `Incubate chunks in Document` and `Fetch chunks on demand` or some more toggles), our customisation sync data is sometimes corrupted. It will be addressed by the next release. +- 0.23.4 + - Fixed: + - No longer experimental configuration is shown on the Minimal Setup. + - New feature: + - We can now use `Incubate Chunks in Document` to reduce non-well-formed chunks. + - Default: disabled / Preferred: enabled in all devices. + - When we enabled this toggle, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised. + - The [design document](https://github.com/vrtmrz/obsidian-livesync/blob/3925052f9290b3579e45a4b716b3679c833d8ca0/docs/design_docs_of_keep_newborn_chunks.md) has been also available.. +- 0.23.3 + - Fixed: No longer unwanted `\f` in journal sync. +- 0.23.2 + - Sorry for all the fixes to experimental features. (These things were also critical for dogfooding). The next release would be the main fixes! Thank you for your patience and understanding! + - Fixed: + - Journal Sync will not hang up during big replication, especially the initial one. + - All changes which have been replicated while rebuilding will not be postponed (Previous behaviour). + - Improved: + - Now Journal Sync works efficiently in download and parse, or pack and upload. + - Less server storage and faster packing/unpacking usage by the new chunk format. +- 0.23.1 + + - Fixed: + - Now journal synchronisation considers untransferred each from sent and received. + - Journal sync now handles retrying. + - Journal synchronisation no longer considers the synchronisation of chunks as revision updates (Simply ignored). + - Journal sync now splits the journal pack to prevent mobile device rebooting. + - Maintenance menus which had been on the command palette are now back in the maintain pane on the setting dialogue. + - Improved: + - Now all changes which have been replicated while rebuilding will be postponed. + +- 0.23.0 + - New feature: + - Now we can use Object Storage. + +### 0.22.0 + +A few years passed since Self-hosted LiveSync was born, and our codebase had been very complicated. This could be patient now, but it should be a tremendous hurt. +Therefore at v0.22.0, for future maintainability, I refined task scheduling logic totally. + +Of course, I think this would be our suffering in some cases. However, I would love to ask you for your cooperation and contribution. + +Sorry for being absent so much long. And thank you for your patience! + +Note: we got a very performance improvement. +Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please. + +#### Version history + +- 0.22.19 + - Fixed: + - No longer data corrupting due to false BASE64 detections. + - Improved: + - A bit more efficient in Automatic data compression. +- 0.22.18 + - New feature (Very Experimental): + - Now we can use `Automatic data compression` to reduce amount of traffic and the usage of remote database. + - Please make sure all devices are updated to v0.22.18 before trying this feature. + - If you are using some other utilities which connected to your vault, please make sure that they have compatibilities. + - Note: Setting `File Compression` on the remote database works for shrink the size of remote database. Please refer the [Doc](https://docs.couchdb.org/en/stable/config/couchdb.html#couchdb/file_compression). +- 0.22.17: + - Fixed: + - Error handling on booting now works fine. + - Replication is now started automatically in LiveSync mode. + - Batch database update is now disabled in LiveSync mode. + - No longer automatically reconnection while off-focused. + - Status saves are thinned out. + - Now Self-hosted LiveSync waits for all files between the local database and storage to be surely checked. + - Improved: + - The job scheduler is now more robust and stable. + - The status indicator no longer flickers and keeps zero for a while. + - No longer meaningless frequent updates of status indicators. + - Now we can configure regular expression filters in handy UI. Thank you so much, @eth-p! + - `Fetch` or `Rebuild everything` is now more safely performed. + - Minor things + - Some utility function has been added. + - Customisation sync now less wrong messages. + - Digging the weeds for eradication of type errors. +- 0.22.16: + - Fixed: + - Fixed the issue that binary files were sometimes corrupted. + - Fixed customisation sync data could be corrupted. + - Improved: + - Now the remote database costs lower memory. + - This release requires a brief wait on the first synchronisation, to track the latest changeset again. + - Description added for the `Device name`. + - Refactored: + - Many type-errors have been resolved. + - Obsolete file has been deleted. +- 0.22.15: + - Improved: - Faster start-up by removing too many logs which indicates normality - By streamlined scanning of customised synchronisation extra phases have been deleted. +- 0.22.14: + - New feature: + - We can disable the status bar in the setting dialogue. + - Improved: + - Now some files are handled as correct data type. + - Customisation sync now uses the digest of each file for better performance. + - The status in the Editor now works performant. + - Refactored: + - Common functions have been ready and the codebase has been organised. + - Stricter type checking following TypeScript updates. + - Remove old iOS workaround for simplicity and performance. +- 0.22.13: + - Improved: + - Now using HTTP for the remote database URI warns of an error (on mobile) or notice (on desktop). + - Refactored: + - Dependencies have been polished. +- 0.22.12: + - Changed: + - The default settings has been changed. + - Improved: + - Default and preferred settings are applied on completion of the wizard. + - Fixed: + - Now Initialisation `Fetch` will be performed smoothly and there will be fewer conflicts. + - No longer stuck while Handling transferred or initialised documents. +- 0.22.11: + - Fixed: + - `Verify and repair all files` is no longer broken. + - New feature: + - Now `Verify and repair all files` is able to... + - Restore if the file only in the local database. + - Show the history. + - Improved: + - Performance improved. +- 0.22.10 + - Fixed: + - No longer unchanged hidden files and customisations are saved and transferred now. + - File integrity of vault history indicates the integrity correctly. + - Improved: + - In the report, the schema of the remote database URI is now printed. +- 0.22.9 + - Fixed: + - Fixed a bug on `fetch chunks on demand` that could not fetch the chunks on demand. + - Improved: + - `fetch chunks on demand` works more smoothly. + - Initialisation `Fetch` is now more efficient. + - Tidied: + - Removed some meaningless codes. +- 0.22.8 + - Fixed: + - Now fetch and unlock the locked remote database works well again. + - No longer crash on symbolic links inside hidden folders. + - Improved: + - Chunks are now created more efficiently. + - Splitting old notes into a larger chunk. + - Better performance in saving notes. + - Network activities are indicated as an icon. + - Less memory used for binary processing. + - Tidied: + - Cleaned unused functions up. + - Sorting out the codes that have become nonsense. + - Changed: + - Now no longer `fetch chunks on demand` needs `Pacing replication` + - The setting `Do not pace synchronization` has been deleted. +- 0.22.7 + - Fixed: + - No longer deleted hidden files were ignored. + - The document history dialogue is now able to process the deleted revisions. + - Deletion of a hidden file is now surely performed even if the file is already conflicted. +- 0.22.6 + - Fixed: + - Fixed a problem with synchronisation taking a long time to start in some cases. + - The first synchronisation after update might take a bit longer. + - Now we can disable E2EE encryption. + - Improved: + - `Setup Wizard` is now more clear. + - `Minimal Setup` is now more simple. + - Self-hosted LiveSync now be able to use even if there are vaults with the same name. + - Database suffix will automatically added. + - Now Self-hosted LiveSync waits until set-up is complete. + - Show reload prompts when possibly recommended while settings. + - New feature: + - A guidance dialogue prompting for settings will be shown after the installation. + - Changed + - `Open setup URI` is now `Use the copied setup URI` + - `Copy setup URI` is now `Copy current settings as a new setup URI` + - `Setup Wizard` is now `Minimal Setup` + - `Check database configuration` is now `Check and Fix database configuration` +- 0.22.5 + - Fixed: + - Some description of settings have been refined + - New feature: + - TroubleShooting is now shown in the setting dialogue. +- 0.22.4 + - Fixed: + - Now the result of conflict resolution could be surely written into the storage. + - Deleted files can be handled correctly again in the history dialogue and conflict dialogue. + - Some wrong log messages were fixed. + - Change handling now has become more stable. + - Some event handling became to be safer. + - Improved: + - Dumping document information shows conflicts and revisions. + - The timestamp-only differences can be surely cached. + - Timestamp difference detection can be rounded by two seconds. + - Refactored: + - A bit of organisation to write the test. +- 0.22.3 + - Fixed: + - No longer detects storage changes which have been caused by Self-hosted LiveSync itself. + - Setting sync file will be detected only if it has been configured now. + - And its log will be shown only while the verbose log is enabled. + - Customisation file enumeration has got less blingy. + - Deletion of files is now reliably synchronised. + - Fixed and improved: + - In-editor-status is now shown in the following areas: + - Note editing pane (Source mode and live-preview mode). + - New tab pane. + - Canvas pane. +- 0.22.2 + - Fixed: + - Now the results of resolving conflicts are surely synchronised. + - Modified: + - Some setting items got new clear names. (`Sync Settings` -> `Targets`). + - New feature: + - We can limit the synchronising files by their size. (`Sync Settings` -> `Targets` -> `Maximum file size`). + - It depends on the size of the newer one. + - At Obsidian 1.5.3 on mobile, we should set this to around 50MB to avoid restarting Obsidian. + - Now the settings could be stored in a specific markdown file to synchronise or switch it (`General Setting` -> `Share settings via markdown`). + - [Screwdriver](https://github.com/vrtmrz/obsidian-screwdriver) is quite good, but mostly we only need this. + - Customisation of the obsoleted device is now able to be deleted at once. + - We have to put the maintenance mode in at the Customisation sync dialogue. +- 0.22.1 + - New feature: + - We can perform automatic conflict resolution for inactive files, and postpone only manual ones by `Postpone manual resolution of inactive files`. + - Now we can see the image in the document history dialogue. + - We can see the difference of the image, in the document history dialogue. + - And also we can highlight differences. + - Improved: + - Hidden file sync has been stabilised. + - Now automatically reloads the conflict-resolution dialogue when new conflicted revisions have arrived. + - Fixed: + - No longer periodic process runs after unloading the plug-in. + - Now the modification of binary files is surely stored in the storage. +- 0.22.0 + - Refined: + - Task scheduling logics has been rewritten. + - Screen updates are also now efficient. + - Possibly many bugs and fragile behaviour has been fixed. + - Status updates and logging have been thinned out to display. + - Fixed: + - Remote-chunk-fetching now works with keeping request intervals + - New feature: + - We can show only the icons in the editor. + - Progress indicators have been more meaningful: + - 📥 Unprocessed transferred items + - 📄 Working database operation + - 💾 Working write storage processes + - ⏳ Working read storage processes + - 🛫 Pending read storage processes + - ⚙️ Working or pending storage processes of hidden files + - 🧩 Waiting chunks + - 🔌 Working Customisation items (Configuration, snippets and plug-ins) + +### 0.21.0 + +The E2EE encryption V2 format has been reverted. That was probably the cause of the glitch. +Instead, to maintain efficiency, files are treated with Blob until just before saving. Along with this, the old-fashioned encryption format has also been discontinued. +There are both forward and backwards compatibilities, with recent versions. However, unfortunately, we lost compatibility with filesystem-livesync or some. +It will be addressed soon. Please be patient if you are using filesystem-livesync with E2EE. + +- 0.21.5 + - Improved: + - Now all revisions will be shown only its first a few letters. + - Now ID of the documents is shown in the log with the first 8 letters. + - Fixed: + - Check before modifying files has been implemented. + - Content change detection has been improved. +- 0.21.4 + - This release had been skipped. +- 0.21.3 + - Implemented: + - Now we can use SHA1 for hash function as fallback. +- 0.21.2 + - IMPORTANT NOTICE: **0.21.1 CONTAINS A BUG WHILE REBUILDING THE DATABASE. IF YOU HAVE BEEN REBUILT, PLEASE MAKE SURE THAT ALL FILES ARE SANE.** + - This has been fixed in this version. + - Fixed: + - No longer files are broken while rebuilding. + - Now, Large binary files can be written correctly on a mobile platform. + - Any decoding errors now make zero-byte files. + - Modified: + - All files are processed sequentially for each. +- 0.21.1 + - Fixed: + - No more infinity loops on larger files. + - Show message on decode error. + - Refactored: + - Fixed to avoid obsolete global variables. +- 0.21.0 + - Changes and performance improvements: + - Now the saving files are processed by Blob. + - The V2-Format has been reverted. + - New encoding format has been enabled in default. + - WARNING: Since this version, the compatibilities with older Filesystem LiveSync have been lost. + +## 0.20.0 + +At 0.20.0, Self-hosted LiveSync has changed the binary file format and encrypting format, for efficient synchronisation. +The dialogue will be shown and asks us to decide whether to keep v1 or use v2. Once we have enabled v2, all subsequent edits will be saved in v2. Therefore, devices running 0.19 or below cannot understand this and they might say that decryption error. Please update all devices. +Then we will have an impressive performance. + +Of course, these are very impactful changes. If you have any questions or troubled things, please feel free to open an issue and mention me. + +Note: if you want to roll it back to v1, please enable `Use binary and encryption version 1` on the `Hatch` pane and perform the `rebuild everything` once. + +Extra but notable information: + +This format change gives us the ability to detect some `marks` in the binary files as same as text files. Therefore, we can split binary files and some specific sort of them (i.e., PDF files) at the specific character. It means that editing the middle of files could be detected with marks. + +Now only a few chunks are transferred, even if we add a comment to the PDF or put new files into the ZIP archives. + +- 0.20.7 + - Fixed + - To better replication, path obfuscation is now deterministic even if with E2EE. + Note: Compatible with previous database without any conversion. Only new files will be obfuscated in deterministic. +- 0.20.6 + - Fixed + - Now empty file could be decoded. + - Local files are no longer pre-saved before fetching from a remote database. + - No longer deadlock while applying customisation sync. + - Configuration with multiple files is now able to be applied correctly. + - Deleting folder propagation now works without enabling the use of a trash bin. +- 0.20.5 + - Fixed + - Now the files which having digit or character prefixes in the path will not be ignored. +- 0.20.4 + - Fixed + - The text-input-dialogue is no longer broken. + - Finally, we can use the Setup URI again on mobile. +- 0.20.3 + - New feature: + - We can launch Customization sync from the Ribbon if we enabled it. + - Fixed: + - Setup URI is now back to the previous spec; be encrypted by V1. + - It may avoid the trouble with iOS 17. + - The Settings dialogue is now registered at the beginning of the start-up process. + - We can change the configuration even though LiveSync could not be launched in normal. + - Improved: + - Enumerating documents has been faster. +- 0.20.2 + - New feature: + - We can delete all data of customization sync from the `Delete all customization sync data` on the `Hatch` pane. + - Fixed: + - Prevent keep restarting on iOS by yielding microtasks. +- 0.20.1 + - Fixed: + - No more UI freezing and keep restarting on iOS. + - Diff of Non-markdown documents are now shown correctly. + - Improved: + - Performance has been a bit improved. + - Customization sync has gotten faster. + - However, We lost forward compatibility again (only for this feature). Please update all devices. + - Misc + - Terser configuration has been more aggressive. +- 0.20.0 + - Improved: + - A New binary file handling implemented + - A new encrypted format has been implemented + - Now the chunk sizes will be adjusted for efficient sync + - Fixed: + - levels of exception in some logs have been fixed + - Tidied: + - Some Lint warnings have been suppressed. + +### 0.19.0 + +#### Customization sync + +Since `Plugin and their settings` have been broken, so I tried to fix it, not just fix it, but fix it the way it should be. + +Now, we have `Customization sync`. + +It is a real shame that the compatibility between these features has been broken. However, this new feature is surely useful and I believe that worth getting over the pain. +We can use the new feature with the same configuration. Only the menu on the command palette has been changed. The dialog can be opened by `Show customization sync dialog`. + +I hope you will give it a try. + +#### Minors + +- 0.19.1 + - Fixed: Fixed hidden file handling on Linux + - Improved: Now customization sync works more smoothly. +- 0.19.2 + - Fixed: + - Fixed garbage collection error while unreferenced chunks exist many. + - Fixed filename validation on Linux. + - Improved: + - Showing status is now thinned for performance. + - Enhance caching while collecting chunks. +- 0.19.3 + - Improved: + - Now replication will be paced by collecting chunks. If synchronisation has been deadlocked, please enable `Do not pace synchronization` once. +- 0.19.4 + - Improved: + - Reduced remote database checking to improve speed and reduce bandwidth. + - Fixed: + - Chunks which previously misinterpreted are now interpreted correctly. + - No more missing chunks which not be found forever, except if it has been actually missing. + - Deleted file detection on hidden file synchronising now works fine. + - Now the Customisation sync is surely quiet while it has been disabled. +- 0.19.5 + - Fixed: + - Now hidden file synchronisation would not be hanged, even if so many files exist. + - Improved: + - Customisation sync works more smoothly. + - Note: Concurrent processing has been rollbacked into the original implementation. As a result, the total number of processes is no longer shown next to the hourglass icon. However, only the processes that are running concurrently are shown. +- 0.19.6 + - Fixed: + - Logging has been tweaked. + - No more too many planes and rockets. + - The batch database update now surely only works in non-live mode. + - Internal things: + - Some frameworks has been upgraded. + - Import declaration has been fixed. + - Improved: + - The plug-in now asks to enable a new adaptor, when rebuilding, if it is not enabled yet. + - The setting dialogue refined. + - Configurations for compatibilities have been moved under the hatch. + - Made it clear that disabled is the default. + - Ambiguous names configuration have been renamed. + - Items that have no meaning in the settings are no longer displayed. + - Some items have been reordered for clarity. + - Each configuration has been grouped. +- 0.19.7 + - Fixed: + - The initial pane of Setting dialogue is now changed to General Settings. + - The Setup Wizard is now able to flush existing settings and get into the mode again. +- 0.19.8 + - New feature: + - Vault history: A tab has been implemented to give a birds-eye view of the changes that have occurred in the vault. + - Improved: + - Now the passphrases on the dialogue masked out. Thank you @antoKeinanen! + - Log dialogue is now shown as one of tabs. + - Fixed: + - Some minor issues has been fixed. +- 0.19.9 + - New feature (For fixing a problem): + - We can fix the database obfuscated and plain paths that have been mixed up. + - Improvements + - Customisation Sync performance has been improved. +- 0.19.10 + - Fixed + - Fixed the issue about fixing the database. +- 0.19.11 + - Improvements: + - Hashing ChunkID has been improved. + - Logging keeps 400 lines now. + - Refactored: + - Import statement has been fixed about types. +- 0.19.12 + - Improved: + - Boot-up performance has been improved. + - Customisation sync performance has been improved. + - Synchronising performance has been improved. +- 0.19.13 + - Implemented: + - Database clean-up is now in beta 2! + We can shrink the remote database by deleting unused chunks, with keeping history. + Note: Local database is not cleaned up totally. We have to `Fetch` again to let it done. + **Note2**: Still in beta. Please back your vault up anything before. + - Fixed: + - The log updates are not thinned out now. +- 0.19.14 + - Fixed: + - Internal documents are now ignored. + - Merge dialogue now respond immediately to button pressing. + - Periodic processing now works fine. + - The checking interval of detecting conflicted has got shorter. + - Replication is now cancelled while cleaning up. + - The database locking by the cleaning up is now carefully unlocked. + - Missing chunks message is correctly reported. + - New feature: + - Suspend database reflecting has been implemented. + - This can be disabled by `Fetch database with previous behaviour`. + - Now fetch suspends the reflecting database and storage changes temporarily to improve the performance. + - We can choose the action when the remote database has been cleaned + - Merge dialogue now show `↲` before the new line. + - Improved: + - Now progress is reported while the cleaning up and fetch process. + - Cancelled replication is now detected. +- 0.19.15 + - Fixed: + - Now storing files after cleaning up is correct works. + - Improved: + - Cleaning the local database up got incredibly fastened. + Now we can clean instead of fetching again when synchronising with the remote which has been cleaned up. +- 0.19.16 + - Many upgrades on this release. I have tried not to let that happen, if something got corrupted, please feel free to notify me. + - New feature: + - (Beta) ignore files handling + We can use `.gitignore`, `.dockerignore`, and anything you like to filter the synchronising files. + - Fixed: + - Buttons on lock-detected-dialogue now can be shown in narrow-width devices. + - Improved: + - Some constant has been flattened to be evaluated. + - The usage of the deprecated API of obsidian has been reduced. + - Now the indexedDB adapter will be enabled while the importing configuration. + - Misc: + - Compiler, framework, and dependencies have been upgraded. + - Due to standing for these impacts (especially in esbuild and svelte,) terser has been introduced. + Feel free to notify your opinion to me! I do not like to obfuscate the code too. +- 0.19.17 + - Fixed: + - Now nested ignore files could be parsed correctly. + - The unexpected deletion of hidden files in some cases has been corrected. + - Hidden file change is no longer reflected on the device which has made the change itself. + - Behaviour changed: + - From this version, the file which has `:` in its name should be ignored even if on Linux devices. +- 0.19.18 + - Fixed: + - Now the empty (or deleted) file could be conflict-resolved. +- 0.19.19 + - Fixed: + - Resolving conflicted revision has become more robust. + - LiveSync now try to keep local changes when fetching from the rebuilt remote database. + Local changes now have been kept as a revision and fetched things will be new revisions. + - Now, all files will be restored after performing `fetch` immediately. +- 0.19.20 + - New feature: + - `Sync on Editor save` has been implemented + - We can start synchronisation when we save from the Obsidian explicitly. + - Now we can use the `Hidden file sync` and the `Customization sync` cooperatively. + - We can exclude files from `Hidden file sync` which is already handled in Customization sync. + - We can ignore specific plugins in Customization sync. + - Now the message of leftover conflicted files accepts our click. + - We can open `Resolve all conflicted files` in an instant. + - Refactored: + - Parallelism functions made more explicit. + - Type errors have been reduced. + - Fixed: + - Now documents would not be overwritten if they are conflicted. + It will be saved as a new conflicted revision. + - Some error messages have been fixed. + - Missing dialogue titles have been shown now. + - We can click close buttons on mobile now. + - Conflicted Customisation sync files will be resolved automatically by their modified time. +- 0.19.21 + - Fixed: + - Hidden files are no longer handled in the initial replication. + - Report from `Making report` fixed + - No longer contains customisation sync information. + - Version of LiveSync has been added. +- 0.19.22 + - Fixed: + - Now the synchronisation will begin without our interaction. + - No longer puts the configuration of the remote database into the log while checking configuration. + - Some outdated description notes have been removed. + - Options that are meaningless depending on other settings configured are now hidden. + - Scan for hidden files before replication + - Scan customization periodically +- 0.19.23 + -Improved: + - We can open the log pane also from the command palette now. + - Now, the hidden file scanning interval could be configured to 0. + - `Check database configuration` now points out that we do not have administrator permission. + +### 0.18.0 + +#### Now, paths of files in the database can now be obfuscated. (Experimental Feature) + +At before v0.18.0, Self-hosted LiveSync used the path of files, to detect and resolve conflicts. In naive. The ID of the document stored in the CouchDB was naturally the filename. +However, it means a sort of lacking confidentiality. If the credentials of the database have been leaked, the attacker (or an innocent bystander) can read the path of files. So we could not use confidential things in the filename in some environments. +Since v0.18.0, they can be obfuscated. so it is no longer possible to decipher the path from the ID. Instead of that, it costs a bit CPU load than before, and the data structure has been changed a bit. + +We can configure the `Path Obfuscation` in the `Remote database configuration` pane. +Note: **When changing this configuration, we need to rebuild both of the local and the remote databases**. + +#### Minors + +- 0.18.1 + - Fixed: + - Some messages are fixed (Typo) + - File type detection now works fine! +- 0.18.2 + - Improved: + - The setting pane has been refined. + - We can enable `hidden files sync` with several initial behaviours; `Merge`, `Fetch` remote, and `Overwrite` remote. + - No longer `Touch hidden files`. +- 0.18.3 + - Fixed Pop-up is now correctly shown after hidden file synchronisation. +- 0.18.4 + - Fixed: + - `Fetch` and `Rebuild database` will work more safely. + - Case-sensitive renaming now works fine. + Revoked the logic which was made at #130, however, looks fine now. +- 0.18.5 + + - Improved: + - Actions for maintaining databases moved to the `🎛️Maintain databases`. + - Clean-up of unreferenced chunks has been implemented on an **experimental**. + - This feature requires enabling `Use new adapter`. + - Be sure to fully all devices synchronised before perform it. + - After cleaning up the remote, all devices will be locked out. If we are sure had it be synchronised, we can perform only cleaning-up locally. If not, we have to perform `Fetch`. + +- 0.18.6 + - New features: + - Now remote database cleaning-up will be detected automatically. + - A solution selection dialogue will be shown if synchronisation is rejected after cleaning or rebuilding the remote database. + - During fetching or rebuilding, we can configure `Hidden file synchronisation` on the spot. + - It let us free from conflict resolution on initial synchronising. + +### 0.17.0 + +- 0.17.0 has no surfaced changes but the design of saving chunks has been changed. They have compatibility but changing files after upgrading makes different chunks than before 0.16.x. + Please rebuild databases once if you have been worried about storage usage. + + - Improved: + + - Splitting markdown + - Saving chunks + + - Changed: + - Chunk ID numbering rules + +#### Minors + +- 0.17.1 + + - Fixed: Now we can verify and repair the database. + - Refactored inside. + +- 0.17.2 + + - New feature + - We can merge conflicted documents automatically if sensible. + - Fixed + - Writing to the storage will be pended while they have conflicts after replication. + +- 0.17.3 + + - Now we supported canvas! And conflicted JSON files are also synchronised with merging its content if they are obvious. + +- 0.17.4 + + - Canvases are now treated as a sort of plain text file. now we transfer only the metadata and chunks that have differences. + +- 0.17.5 Now `read chunks online` had been fixed, and a new feature: `Use dynamic iteration count` to reduce the load on encryption/decryption. + Note: `Use dynamic iteration count` is not compatible with earlier versions. +- 0.17.6 Now our renamed/deleted files have been surely deleted again. +- 0.17.7 + - Fixed: + - Fixed merging issues. + - Fixed button styling. + - Changed: + - Conflict checking on synchronising has been enabled for every note in default. +- 0.17.8 + - Improved: Performance improved. Prebuilt PouchDB is no longer used. + - Fixed: Merging hidden files is also fixed. + - New Feature: Now we can synchronise automatically after merging conflicts. +- 0.17.9 + - Fixed: Conflict merge of internal files is no longer broken. + - Improved: Smoother status display inside the editor. +- 0.17.10 + - Fixed: Large file synchronising has been now addressed! + Note: When synchronising large files, we have to set `Chunk size` to lower than 50, disable `Read chunks online`, `Batch size` should be set 50-100, and `Batch limit` could be around 20. +- 0.17.11 + - Fixed: + - Performance improvement + - Now `Chunk size` can be set to under one hundred. + - New feature: + - The number of transfers required before replication stabilises is now displayed. +- 0.17.12: Skipped. +- 0.17.13 + - Fixed: Document history is now displayed again. + - Reorganised: Many files have been refactored. +- 0.17.14: Skipped. +- 0.17.15 + - Improved: + - Confidential information has no longer stored in data.json as is. + - Synchronising progress has been shown in the notification. + - We can commit passphrases with a keyboard. + - Configuration which had not been saved yet is marked now. + - Now the filename is shown on the Conflict resolving dialog + - Fixed: + - Hidden files have been synchronised again. + - Rename of files has been fixed again. + And, minor changes have been included. +- 0.17.16: + - Improved: + - Plugins and their settings no longer need scanning if changes are monitored. + - Now synchronising plugins and their settings are performed parallelly and faster. + - We can place `redflag2.md` to rebuild the database automatically while the boot sequence. + - Experimental: + - We can use a new adapter on PouchDB. This will make us smoother. + - Note: Not compatible with the older version. + - Fixed: + - The default batch size is smaller again. + - Plugins and their setting can be synchronised again. + - Hidden files and plugins are correctly scanned while rebuilding. + - Files with the name started `_` are also being performed conflict-checking. +- 0.17.17 + - Fixed: Now we can merge JSON files even if we failed to compare items like null. +- 0.17.18 + - Fixed: Fixed lack of error handling. +- 0.17.19 + - Fixed: Error reporting has been ensured. +- 0.17.20 + - Improved: Changes of hidden files will be notified to Obsidian. +- 0.17.21 + - Fixed: Skip patterns now handle capital letters. + - Improved + - New configuration to avoid exceeding throttle capacity. + - We have been grateful to @karasevm! + - The conflicted `data.json` is no longer merged automatically. + - This behaviour is not configurable, unlike the `Use newer file if conflicted` of normal files. +- 0.17.22 + - Fixed: + - Now hidden files will not be synchronised while we are not configured. + - Some processes could start without waiting for synchronisation to complete, but now they will wait for. + - Improved + - Now, by placing `redflag3.md`, we can discard the local database and fetch again. + - The document has been updated! Thanks to @hilsonp! +- 0.17.23 + - Improved: + - Now we can preserve the logs into the file. + - Note: This option will be enabled automatically also when we flagging a red flag. + - File names can now be made platform-appropriate. + - Refactored: + - Some redundant implementations have been sorted out. +- 0.17.24 + - New feature: + - If any conflicted files have been left, they will be reported. + - Fixed: + - Now the name of the conflicting file is shown on the conflict-resolving dialogue. + - Hidden files are now able to be merged again. + - No longer error caused at plug-in being loaded. + - Improved: + - Caching chunks are now limited in total size of cached chunks. +- 0.17.25 + - Fixed: + - Now reading error will be reported. +- 0.17.26 + - Fixed(Urgent): + - The modified document will be reflected in the storage now. +- 0.17.27 + - Improved: + - Now, the filename of the conflicted settings will be shown on the merging dialogue + - The plugin data can be resolved when conflicted. + - The semaphore status display has been changed to count only. + - Applying to the storage will be concurrent with a few files. +- 0.17.28 + -Fixed: + - Some messages have been refined. + - Boot sequence has been speeded up. + - Opening the local database multiple times in a short duration has been suppressed. + - Older migration logic. + - Note: If you have used 0.10.0 or lower and have not upgraded, you will need to run 0.17.27 or earlier once or reinstall Obsidian. +- 0.17.29 + - Fixed: + - Requests of reading chunks online are now split into a reasonable(and configurable) size. + - No longer error message will be shown on Linux devices with hidden file synchronisation. + - Improved: + - The interval of reading chunks online is now configurable. + - Boot sequence has been speeded up, more. + - Misc: + - Messages on the boot sequence will now be more detailed. If you want to see them, please enable the verbose log. + - Logs became be kept for 1000 lines while the verbose log is enabled. +- 0.17.30 + - Implemented: + - `Resolve all conflicted files` has been implemented. + - Fixed: + - Fixed a problem about reading chunks online when a file has more chunks than the concurrency limit. + - Rollbacked: + - Logs are kept only for 100 lines, again. +- 0.17.31 + - Fixed: + - Now `redflag3` can be run surely. + - Synchronisation can now be aborted. + - Note: The synchronisation flow has been rewritten drastically. Please do not haste to inform me if you have noticed anything. +- 0.17.32 + - Fixed: + - Now periodic internal file scanning works well. + - The handler of Window-visibility-changed has been fixed. + - And minor fixes possibly included. + - Refactored: + - Unused logic has been removed. + - Some utility functions have been moved into suitable files. + - Function names have been renamed. +- 0.17.33 + - Maintenance update: Refactored; the responsibilities that `LocalDatabase` had were shared. (Hoping) No changes in behaviour. +- 0.17.34 + - Fixed: The `Fetch` that was broken at 0.17.33 has been fixed. + - Refactored again: Internal file sync, plug-in sync and Set up URI have been moved into each file. + +### 0.16.0 + +- Now hidden files need not be scanned. Changes will be detected automatically. + - If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`. + - Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once. + +#### Minors + +- 0.16.1 Added missing log updates. +- 0.16.2 Fixed many problems caused by combinations of `Sync On Save` and the tracking logic that changed at 0.15.6. +- 0.16.3 + - Fixed detection of IBM Cloudant (And if there are some issues, be fixed automatically). + - A configuration information reporting tool has been implemented. +- 0.16.4 Fixed detection failure. Please set the `Chunk size` again when using a self-hosted database. +- 0.16.5 + - Fixed + - Conflict detection and merging now be able to treat deleted files. + - Logs while the boot-up sequence has been tidied up. + - Fixed incorrect log entries. + - New Feature + - The feature of automatically deleting old expired metadata has been implemented. + We can configure it in `Delete old metadata of deleted files on start-up` in the `General Settings` pane. +- 0.16.6 + - Fixed + - Automatic (temporary) batch size adjustment has been restored to work correctly. + - Chunk splitting has been backed to the previous behaviour for saving them correctly. + - Improved + - Corrupted chunks will be detected automatically. + - Now on the case-insensitive system, `aaa.md` and `AAA.md` will be treated as the same file or path at applying changesets. +- 0.16.7 Nothing has been changed except toolsets, framework library, and as like them. Please inform me if something had been getting strange! +- 0.16.8 Now we can synchronise without `bad_request:invalid UTF-8 JSON` even while end-to-end encryption has been disabled. + +Note: +Before 0.16.5, LiveSync had some issues making chunks. In this case, synchronisation had became been always failing after a corrupted one should be made. After 0.16.6, the corrupted chunk is automatically detected. Sorry for troubling you but please do `rebuild everything` when this plug-in notified so. + +### 0.15.0 + +- Outdated configuration items have been removed. +- Setup wizard has been implemented! + +I appreciate for reviewing and giving me advice @Pouhon158! + +#### Minors + +- 0.15.1 Missed the stylesheet. +- 0.15.2 The wizard has been improved and documented! +- 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard. +- 0.15.4 Fixed issues about asynchronous processing (e.g., Conflict check or hidden file detection) +- 0.15.5 Add new features for setting Self-hosted LiveSync up more easier. +- 0.15.6 File tracking logic has been refined. +- 0.15.7 Fixed bug about renaming file. +- 0.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices. +- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests. +- 0.15.10 Fixed: + - The boot sequence has been corrected and now boots smoothly. + - Auto applying of batch save will be processed earlier than before. + +### 0.14.1 + +- The target selecting filter was implemented. + Now we can set what files are synchronised by regular expression. +- We can configure the size of chunks. + We can use larger chunks to improve performance. + (This feature can not be used with IBM Cloudant) +- Read chunks online. + Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication. +- Added this note. +- Use local chunks in preference to remote them if present, + +#### Recommended configuration for Self-hosted CouchDB + +- Set chunk size to around 100 to 250 (10MB - 25MB per chunk) +- _Set batch size to 100 and batch limit to 20 (0.14.2)_ +- Be sure to `Read chunks online` checked. + +#### Minors + +- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed +- 0.14.3 New test items have been added to `Check database configuration`. +- 0.14.4 Fixed issue of importing configurations. +- 0.14.5 Auto chunk size adjusting implemented. +- 0.14.6 Change Target to ES2018 +- 0.14.7 Refactor and fix typos. +- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any. + +### 0.13.0 + +- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded. +- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync. +- We can see the history of deleted files. +- `Pick file to show` was renamed to `Pick a file to show. +- Files in the `Pick a file to show` are now ordered by their modified date descent. +- Update information became to be shown on the major upgrade. + +#### Minors + +- 0.13.1 Fixed on conflict resolution. +- 0.13.2 Fixed file deletion failures. +- 0.13.4 + - Now, we can synchronise hidden files that conflicted on each devices. + - We can search for conflicting docs. + - Pending processes can now be run at any time. + - Performance improved on synchronising large numbers of files at once. diff --git a/docs/review_harness.md b/docs/review_harness.md new file mode 100644 index 00000000..f94c670f --- /dev/null +++ b/docs/review_harness.md @@ -0,0 +1,67 @@ +# Review Harness + +The Review Harness is an opt-in, real-device review tool for Self-hosted LiveSync maintainers. It replaces the disabled legacy Test Pane with fixed, auditable scenarios, a device-local one-shot continuation, and a Markdown report which can be pasted into a pull request. + +It supplies supporting evidence rather than a release gate by itself. Unit, integration, Compose, CLI E2E, and real-Obsidian E2E remain authoritative for the boundaries they own. + +## Enabling the Harness + +1. Use a dedicated test Vault. +2. Enable **Power users → Enable Developers' Debug Tools**. +3. Restart Obsidian. +4. Run **Self-hosted LiveSync: Open review harness** from the command palette. + +The command and view are registered only when developer tools are enabled. Disabling the setting and restarting removes the entry point from an ordinary user session. + +## Scenarios and access + +| Scenario | Mode | Access | Purpose | +| --- | --- | --- | --- | +| Settings lifecycle | Automatic | Read-only | Exposes the seven synchronisation choices as typed observations and, for a genuinely new Vault, compares the selected recommendations with Commonlib's new-Vault contract. Existing-Vault setting preservation remains an automated migration and `settings-ui` E2E responsibility. | +| Compatibility review boundary | Guided | Device-local state | Observes the current dedicated compatibility controller and opens its actual review action. The Harness does not restore Change Log acknowledgement, `lastReadUpdates`, or a separate manual Pass or Fail result. | +| P2P composition | Automatic | Read-only | Confirms that the live P2P result resolves one replicator bound to the active Obsidian services. Peer discovery, replacement, reconnection, and transfer remain unit, CLI, and Compose E2E responsibilities. | +| Vault fixture round trip | Automatic, explicit | Dedicated Vault fixtures | After explicit confirmation, creates, reads, modifies, renames, and removes one owned fixture tree. General Vault reflection remains covered by its separate real-Obsidian E2E workflow. | + +**Automatic** runs only the two read-only local observations. **Full review** also starts the guided compatibility observation and asks before the Vault fixture scenario writes anything. A scenario can also be run individually. + +These observations deliberately do not repeat stronger automated workflows. They exist to record which contracts the current real-device composition exposes while a maintainer reviews an immutable artefact. + +### Vault fixture boundary + +The Vault scenario owns only `__self-hosted-livesync-review-harness__`. It refuses to run if that path already exists, so it cannot assume ownership of a user's existing file or folder. Once it creates the root, it removes the complete owned tree from a `finally` block whether the round trip passes or fails. + +The Harness accepts no path, command, code, remote configuration, or credential through plug-in data or its continuation state. New write scenarios must use a distinct fixed fixture root, describe their side effects in the interface, require confirmation, and clean up in `finally`. + +## Restart continuation + +The restart action writes a small device-local record under `review-harness-v1`, then asks Obsidian to reload. The record permits only: + +- the fixed `compatibility-review` scenario; +- the fixed `awaiting-restart` stage; +- a canonical ISO request time; and +- a request identifier derived exactly as `compatibility-review-`. + +On the next settings load, the Harness deletes the record before parsing and acting on it. A valid record reopens the Harness after layout is ready and leaves the compatibility observation waiting for the reviewer. Invalid state is removed and reported as a failed continuation. The record is not stored in `data.json`, copied through a Setup URI, or synchronised to another device. + +The compatibility controller does not require an Obsidian restart to acknowledge a pause. This continuation belongs to the review tool and proves its one-shot reload boundary; it is not a second compatibility lifecycle. + +## Reports and privacy + +**Copy Markdown report** includes: + +- the plug-in and Obsidian versions; +- the platform, user agent, and viewport; +- each scenario's status and bounded summary; and +- a bounded event transcript. + +The formatter has no inputs for Vault identifiers, paths, file names, file contents, remote configuration, or secrets. The report is copied locally and is never transmitted by the plug-in. Review the environment information before posting it because a user agent and viewport may identify a device or operating system. + +Unexpected runtime errors are written to the local LiveSync log, while copied reports retain only a generic failure summary. This prevents an adapter error from copying a local path or file name into a pull request by accident. + +## Automated real-Obsidian coverage + +External automation drives the stable `data-testid` attributes beginning with `review-harness-`. The dedicated workflow checks only Harness-owned behaviour: debug-only registration, consume-before-use continuation handling, fixed Vault fixture clean-up, report privacy, and mobile layout. + +Compatibility dialogue behaviour and persistence belong to `test:e2e:obsidian:settings-ui`. Real P2P transport belongs to the Compose P2P suite. General Vault reflection belongs to `test:e2e:obsidian:vault-reflection`. Keeping those responsibilities separate prevents the review tool from becoming a second, weaker copy of the acceptance suite. + +The mobile checks use `app.emulateMobile(true)`, a representative viewport, and safe-area and touch-target assertions. They do not claim to reproduce native operating-system overlays. diff --git a/docs/settings.md b/docs/settings.md index 995aed28..1ac07517 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -4,6 +4,17 @@ NOTE: This document not completed. I'll improve this doc in a while. but your co There are many settings in Self-hosted LiveSync. This document describes each setting in detail (not how-to). Configuration and settings are divided into several categories and indicated by icons. The icon is as follows: +## Feature maturity for 1.0 + +The following status applies to optional and compatibility features in the 1.0 line: + +| Status | Features | 1.0 policy | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Supported, opt-in | Peer-to-Peer Synchronisation, Hidden File Sync, and Customisation Sync | Maintained and covered by focused real-runtime tests. Enable them only where their separate setup and operational constraints are acceptable. | +| Maintained, advanced | Data Compression | Available as an explicit storage and bandwidth trade-off. It remains disabled by default because the measured processing and memory costs outweigh the mixed-dataset saving. | +| Beta or experimental | JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 for CouchDB | Retained for explicit testing and specialised use. They remain disabled by default and are not part of the minimum supported setup. | +| Compatibility only | V1 dynamic iteration counts, the old IndexedDB adapter, non-current hash algorithms, Eden chunks, and the stored `doNotUseFixedRevisionForChunks` key | Existing settings and data remain readable. New Vaults use the current defaults, and compatibility controls are shown only where a migration or recovery path still needs them. | + | Icon | Description | | :--: | ------------------------------------------------------------------ | | 💬 | [0. Change Log](#0-change-log) | @@ -21,12 +32,18 @@ There are many settings in Self-hosted LiveSync. This document describes each se ## 0. Change Log -This pane shows version up information. You can check what has been changed in recent versions. +This pane always shows the current release history. It does not track whether a particular plug-in version has been read and does not open automatically after an ordinary update. + +Internal database or settings compatibility reviews use a separate safety dialogue, not this pane. The dialogue explains why remote synchronisation has been paused and preserves the automatic synchronisation choices which were configured before the update. A configured Vault which was copied, restored, or opened in a new Obsidian profile can require this review because its device-local acknowledgement is not part of the Vault data. An empty local database is not accepted as evidence that it is safe to continue. An existing unconfigured Vault remains in onboarding without this synchronisation warning; its missing acknowledgement is not filled in automatically, so it is evaluated if the Vault is configured later. Closing the dialogue keeps synchronisation paused. When the detected state can be handled by the running version, the explicit resume action records the current internal database version and restores the configured behaviour. A persistent Notice and the `Review why synchronisation is paused` command reopen the review. An older installation cannot dismiss a pause caused by a newer database or settings version. ## 1. Setup This pane is used for setting up Self-hosted LiveSync. There are several options to set up Self-hosted LiveSync. +An unconfigured installation does not open the onboarding dialogue automatically or scan the Vault into the local database. A long-lived Notice offers the onboarding action. If the Notice is dismissed, open **Self-hosted LiveSync settings** → **Setup** → **Rerun Onboarding Wizard**. + +Choose the new-device path when this device owns the files which should initialise synchronisation. Choose the existing-device path when it should receive an established remote state. The wizard reserves Rebuild or Fetch respectively before enabling the settings and requesting a restart, so the selected initialisation runs before the ordinary start-up scan. + ### 1. Quick Setup Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted LiveSync with a few clicks. @@ -35,10 +52,14 @@ Most preferred method to setup Self-hosted LiveSync. You can setup Self-hosted L Setup the Self-hosted LiveSync with the `setup URI` which is [copied from another device](#copy-current-settings-as-a-new-setup-uri) or the setup script. +A current Setup URI retains its remote profiles, display names, and separate main and P2P selections. Older Setup URIs containing only flat connection settings remain supported and are migrated to a remote profile when applied. + #### Manual setup Step-by-step setup for Self-hosted LiveSync. You can setup Self-hosted LiveSync manually with Minimal setting items. +Completing manual CouchDB, Object Storage, or P2P setup creates the corresponding remote profile without replacing profiles which are already saved. CouchDB and Object Storage setup select the new profile as the main remote. P2P setup selects it for P2P use and, when the wizard is enabling LiveSync, also selects it as the main remote. A descriptive display name is generated and can be changed later. + #### Enable LiveSync This button only appears when the setup was not completed. If you have completed the setup manually, you can enable LiveSync on this device by this button. @@ -148,9 +169,11 @@ Show verbose log. Please enable when you report the logs ## 3. Remote Configuration -### 1. Remote Server +### 1. Connection settings -Self-hosted LiveSync supports multiple remote connection profiles under **Remote Server** -> **Remote Databases**. This allows you to save and switch between multiple databases or bucket configurations in a single vault. +Self-hosted LiveSync stores multiple remote connection profiles under **Connection settings** → **Saved connections**. Each profile represents a CouchDB database, an Object Storage connection, or a P2P configuration, and several profiles can be kept in one Vault. + +Each profile has an opaque identifier and a presentation name. The name does not need to be unique and is not used to select the profile. The main remote and the P2P remote are selected independently, so code and settings imports must preserve both selections rather than relying on a special identifier such as `default`. - **➕ Add new connection**: Create a new connection profile by launching the setup dialogue. - **📥 Import connection**: Paste a connection string (e.g., `sls+https://...`, `sls+s3://...`, `sls+p2p://...`) to import a remote configuration profile. @@ -162,7 +185,7 @@ Self-hosted LiveSync supports multiple remote connection profiles under **Remote Setting key: remoteType -The active remote server type. This is automatically projected to the legacy configuration when you activate a connection profile. +The active connection type. This is automatically projected to the legacy configuration when you activate a connection profile. ### 2. Notification @@ -170,7 +193,7 @@ The active remote server type. This is automatically projected to the legacy con Setting key: notifyThresholdOfRemoteStorageSize -MB (0 to disable). We can get a notification when the estimated remote storage size exceeds this value. +MB (0 to disable). At startup, Self-hosted LiveSync shows a long-lived, clickable notice when this value has not been configured or the estimated remote storage size exceeds it. Select **Review options** to open the detailed, untimed dialogue. Running the remote-size check explicitly opens that dialogue directly. ### 3. Privacy & Encryption @@ -197,14 +220,15 @@ In default, the path of the file is not obfuscated to improve the performance. I Setting key: E2EEAlgorithm The encryption algorithm version used for end-to-end encryption. + - `v2` (V2: AES-256-GCM With HKDF): Recommended and default version. - `forceV1` or `""` (V1: Legacy): Older legacy encryption. Only use this if you have an existing vault encrypted in the legacy format. -#### Use dynamic iteration count (Experimental) +#### Use dynamic iteration count (legacy V1 compatibility) Setting key: useDynamicIterationCount -This is an experimental feature and not recommended. If you enable this, the iteration count of the encryption will be dynamically determined. This is useful when you want to improve the performance. +This setting applies only to legacy V1 encryption data. Keep the saved value when opening an existing V1 database. New Vaults use E2EE V2 and do not use this setting. --- @@ -290,7 +314,7 @@ These settings are configured within the CouchDB Setup dialogue when adding (` Setting key: couchDB_URI The URI of the CouchDB server. -Note: Only Secure (HTTPS) connections can be used on Obsidian Mobile. The URI must not end with a trailing slash. +Only secure HTTPS connections can be used on Obsidian Mobile. The setup dialogue accepts a complete HTTP or HTTPS URL and normalises it when the settings are applied. #### Username @@ -308,14 +332,13 @@ The password used to authenticate with CouchDB. Setting key: couchDB_DBNAME -The name of the database. -Note: The database name cannot contain capital letters, spaces, or special characters other than `_$()+/-`, and cannot start with an underscore (`_`). +The name of the database. It must not be empty. CouchDB validates the name when the connection is attempted; the setup dialogue does not apply a narrower client-side naming rule. #### Use Request API to avoid inevitable CORS problem Setting key: useRequestAPI -This option is labeled **Use Internal API** in the setup dialogue. If enabled, Obsidian's internal request API will be used to bypass CORS restrictions. This is a workaround that may not be compliant with web standards and is less secure. Note that this might break in future Obsidian versions. +This option is labelled **Use Internal API** in the setup dialogue. If enabled, Obsidian's internal request API is used to bypass CORS restrictions. It sends the configured credentials to the CouchDB server through an Obsidian-owned API, so use it only with a server you trust. Configure CouchDB CORS correctly where possible; this compatibility workaround may change in future Obsidian versions. #### Custom Headers @@ -359,13 +382,20 @@ Setting key: jwtSub The subject (`sub`) claim of the JWT, which should match your CouchDB username. -#### Test Database Connection +#### Connection and save actions -Open database connection. If the remote database is not found and you have permission to create a database, the database will be created. +The action depends on why the dialogue was opened: -#### Validate Database Configuration +- Onboarding for the first device uses **Create or connect to database and continue**. It may create the database when it does not exist and the supplied account has permission. +- Onboarding for an additional device uses **Connect to existing database and continue**. It does not create a missing database. +- Adding or editing a saved remote profile uses **Test connection and save**. It does not create a missing database. +- Settings mode also offers **Save without connecting**. The existing profile is updated, but automatic synchronisation may fail until the connection is corrected. -Checks and fixes any potential issues with the database config. +Onboarding requires a successful connection. It does not expose an unverified continuation action. + +#### Check server requirements + +This optional check reads the CouchDB server configuration through Obsidian's internal request API and sends the configured credentials to that server. Administrator access may be required. The initial check is read-only. Each offered fix names the exact CouchDB setting and proposed value, and requires separate confirmation before making that change. #### Apply Settings @@ -377,11 +407,11 @@ Setting key: P2P_Enabled Enable direct peer-to-peer synchronisation via WebRTC. -#### Relay URL +#### Signalling relay URLs Setting key: P2P_relays -The WebSocket relay server URL(s) used for coordinating P2P connections via WebRTC. Multiple URLs can be separated by commas. +The Nostr-compatible WebSocket relay URL or URLs used for peer discovery and WebRTC connection negotiation. Multiple URLs can be separated by commas. A signalling relay does not store or transfer Vault contents. See [How peer-to-peer synchronisation works](p2p.md). #### Group ID @@ -407,17 +437,21 @@ Setting key: P2P_AutoStart This option is labeled **Auto Start P2P Connection** in the setup dialogue. If enabled, the P2P connection will start automatically when the plug-in launches. -#### Automatically broadcast changes to connected peers +#### Connect and disconnect + +Closing a P2P connection leaves the LiveSync P2P room, stops its replication service, closes the signalling relay sockets, and pauses their automatic reconnection. An idle WebRTC connection may remain temporarily under the transport's ownership so that it can be reused, but it cannot carry traffic for the room which has been left. Connecting again resumes relay reconnection and joins a new LiveSync room. + +#### Announce changes automatically after connecting Setting key: P2P_AutoBroadcast -This option is labeled **Auto Broadcast Changes** in the setup dialogue. If enabled, changes will be automatically broadcasted to connected peers, requesting them to fetch the changes. +When enabled, this device notifies connected peers after a local change. The notification contains no Vault data. A receiving peer fetches the change only when it follows this device. #### TURN Server URLs (comma-separated) Setting key: P2P_turnServers -A comma-separated list of TURN/STUN server URLs. Used to relay P2P connections when direct WebRTC connection fails due to strict NAT or firewalls. In most cases, these can be left blank. +A comma-separated list of TURN server URLs. TURN is an optional fallback which relays encrypted WebRTC traffic when strict NAT or firewall rules prevent a direct peer connection. It is distinct from the required signalling relay. In most environments, this field can remain blank. #### TURN Username @@ -447,6 +481,7 @@ Apply preset configuration Setting key: syncMode The trigger mechanism for synchronisation. + - **LiveSync** (`LIVESYNC`): Real-time, continuous, bidirectional synchronisation. Note: This requires a CouchDB or WebRTC P2P remote server. It is not supported for S3-compatible Object Storage. - **Periodic Sync** (`PERIODIC`): Synchronisation is performed at regular intervals specified by the **Periodic Sync interval** setting. @@ -514,10 +549,10 @@ Saving will be performed forcefully after this number of seconds. ### 4. Deletion Propagation (Advanced) -#### Use the trash bin +#### Legacy trash setting Setting key: trashInsteadDelete -Move remotely deleted files to the trash, instead of deleting. On Obsidian v1.7.2 or newer, file deletion respects the user's deletion preferences (by utilising the `FileManager.trashFile` API), regardless of this setting. +This key remains accepted for settings imports, Setup URIs, and compatibility with earlier versions, but it is no longer shown in the settings interface. Remote file deletion follows the user's Obsidian deletion preferences through the `FileManager.trashFile` API, regardless of this legacy value. #### Keep empty folder @@ -526,10 +561,12 @@ Should we keep folders that do not have any files inside? ### 5. Conflict resolution (Advanced) +Conflict resolution preserves unknown local content and automatically merges only when the available revision history supplies a safe shared base. See [Conflict resolution and revision provenance](specs_conflict_resolution.md) for the revision-tree rules, stale and concurrent resolutions, binary-file limitation, and the device-local provenance used for operations while a conflict is live. + #### (BETA) Always overwrite with a newer file Setting key: resolveConflictsByNewerFile -Testing only - Resolve file conflicts by syncing newer copies of the file, this can overwrite modified files. Be Warned. +Testing only. Resolve file conflicts by selecting the copy with the newer modification time. This can overwrite modified files and cannot establish which revision reflects the user's intent. #### Delay conflict resolution of inactive files @@ -559,6 +596,8 @@ Setting key: notifyAllSettingSyncFile ### 7. Hidden Files (Advanced) +See the [Hidden File Sync guide](./tips/hidden-file-sync.md) before enabling this feature. Rebuild and Fetch setup operations deliberately leave optional features disabled; establish ordinary note synchronisation first, then initialise Hidden File Sync independently on each device. + #### Enable Hidden files sync Setting key: syncInternalFiles @@ -606,6 +645,8 @@ If this is set, changes to local files which are matched by the ignore files wil Setting key: ignoreFiles Comma separated `.gitignore, .dockerignore` +When a saved setting changes whether a normal file can be reflected, LiveSync re-evaluates the normal-file metadata already held in the local database. This covers selector expressions, ignore-file settings, maximum-size and modification-time limits, and file-name case handling. A remote revision which was received and checkpointed while excluded can therefore be reflected after the criteria are broadened, without rewinding the remote checkpoint. Narrowing the criteria does not delete files which have already been reflected. + ### 2. Hidden Files (Advanced) #### Ignore patterns @@ -614,6 +655,8 @@ Comma separated `.gitignore, .dockerignore` ## 6. Customisation sync (Advanced) +Customisation Sync is a supported, advanced opt-in feature. Its current per-file implementation is covered by a two-Vault real-Obsidian workflow for snippets, configuration files, and plug-in files. Hidden File Sync is a separate feature with different setup, selection, and conflict behaviour; do not use both features to manage the same files. + ### 1. Customisation Sync #### Device name @@ -655,6 +698,12 @@ Open the dialogue #### Make report to inform the issue +#### Copy database information for a file + +Select a file to copy its local database information. The command **Copy database information for the active file** performs the same inspection for the file open in the editor. + +The report includes the Vault-relative path, document and chunk identifiers, local database revisions, conflicts, and local chunk availability. It does not query the remote or include file contents. Paths and identifiers can still be private metadata, so review the report before sharing it. + #### Write logs into the file Setting key: writeLogToTheFile @@ -678,17 +727,19 @@ Stop reflecting database changes to storage files. ### 3. Recovery and Repair -#### Recreate missing chunks for all files +#### Recreate chunks for current Vault files -This will recreate chunks for all files. If there were missing chunks, this may fix the errors. +Recreate chunks from files currently present in the Vault. This can repair missing chunks for those exact current contents after they have been confirmed as authoritative. It cannot reconstruct unavailable historical or conflict content. + +#### Inspect conflicts and file/database differences + +Compare each Vault file with every current live revision in the local database. Each winner and conflict revision is shown separately with its exact revision identifier, local chunk availability, and relationship to the current Vault file. Unavailable shared ancestors are reported separately because they prevent conservative three-way merging but are not live revisions which can be discarded. + +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. #### Resolve All conflicted files by the newer one -Resolve all conflicted files by the newer one. Caution: This will overwrite the older one, and cannot resurrect the overwritten one. - -#### Verify and repair all files - -Compare the content of files between on local database and storage. If not matched, you will be asked which one you want to keep. +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. #### Check and convert non-path-obfuscated files @@ -748,20 +799,20 @@ Setting key: concurrencyOfReadChunksOnline Setting key: minimumIntervalOfReadChunksOnline -#### Maximum size of chunks to send in one request +#### Maximum request size for manually resending chunks Setting key: sendChunksBulkMaxSize -Limit the maximum size of chunks to send in a single bulk request (MB). +Limit the maximum size of chunks sent in one request by the explicit **Resend all chunks** maintenance operation (MB). Ordinary and initial replication do not use this setting. ## 9. Power users (Power User) ### 1. Remote Database Tweak -#### Incubate Chunks in Document (Beta) +#### Incubate Chunks in Document (sunset compatibility) Setting key: useEden -If enabled, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised. +This setting is no longer offered for new configuration. Existing saved values remain accepted so that established databases can be opened and migrated without silently changing their structure. #### Maximum Incubating Chunks @@ -778,10 +829,14 @@ The maximum total size of chunks that can be incubated within the document. Chun Setting key: maxAgeInEden The maximum duration for which chunks can be incubated within the document. Chunks exceeding this period will graduate to independent chunks. -#### Data Compression (Experimental) +#### Data Compression (advanced opt-in) Setting key: enableCompression +Data Compression applies fflate level 8 to each chunk before E2EE. A chunk is left uncompressed when compression would not make it smaller, and readers continue to accept both representations. Changing the setting does not require a rebuild for compatibility; existing and new representations can coexist. + +The setting remains disabled by default because the measured storage and transfer benefit comes with workload-dependent processing and memory costs. See the [Data Compression specification](specs_data_compression.md) for the contract, evidence, execution model, and reproduction command. + ### 2. CouchDB Connection Tweak #### Batch size @@ -849,10 +904,10 @@ Enable this option to automatically apply the most recent change to documents ev Setting key: useIndexedDBAdapter Before v0.17.16, we used an old adapter for the local database. Now the new adapter is preferred. However, it needs local database rebuilding. Please disable this toggle when you have enough time. If leave it enabled, also while fetching from the remote database, you will be asked to disable this. -#### Compute revisions for chunks (Previous behaviour) +#### Content-derived chunk revisions (obsolete setting) Setting key: doNotUseFixedRevisionForChunks -If this enabled, all chunks will be stored with the revision made from its content. (Previous behaviour) +Chunk revisions are always derived from their content. This key remains accepted in stored settings and Setup URIs for compatibility, but its value no longer changes behaviour and it is not a maintenance prerequisite. #### Handle files as Case-Sensitive @@ -861,6 +916,8 @@ If this enabled, All files are handled as case-Sensitive (Previous behaviour). When this setting is disabled, changing only the letter case of a file name within the same directory is synchronised as a rename. Changing the letter case of a directory name is not supported by this handling. +New Vaults use case-insensitive handling for cross-platform compatibility. Existing settings with an explicit value preserve that choice. Earlier releases also followed the case-insensitive branch when this value was absent, so 1.0 saves a missing legacy value as `false` without requiring a compatibility review or database rebuild. + ### 4. Compatibility (Internal API Usage) #### Scan changes on customisation sync @@ -875,10 +932,12 @@ Do not use internal API Setting key: additionalSuffixOfDatabaseName LiveSync could not handle multiple vaults which have same name without different prefix, This should be automatically configured. -#### The Hash algorithm for chunk IDs (Experimental) +#### The Hash algorithm for chunk IDs (compatibility) Setting key: hashAlg +`xxhash64` is the supported current value. Older algorithms remain selectable only as an edge-case compatibility path for existing databases. Changing the algorithm can reduce chunk reuse between devices and requires the normal tweak review. + ### 6. Edge case addressing (Behaviour) #### Fetch database with previous behaviour @@ -908,6 +967,12 @@ If disabled(toggled), chunks will be split on the UI thread (Previous behaviour) Setting key: processSmallFilesInUIThread If enabled, the file under 1kb will be processed in the UI thread. +#### Automatically align compatible chunk settings + +Setting key: autoAcceptCompatibleTweak + +Current releases enable this by default when the differences are limited to compatible chunk settings. The side with the newer recorded modification time is used for the chunk hash algorithm, chunk size, or splitter version; the remote value is used when neither side has a recorded time or the times are equal. No dialogue or database reconstruction is required. Existing content remains readable, but changing these values can reduce chunk reuse. Turn this off to review compatible differences manually. Any difference which also involves an incompatible setting always requires an explicit decision. + ### 8. Compatibility (Trouble addressed) #### Do not check configuration mismatch before replication @@ -938,7 +1003,7 @@ Disables all synchronisation and restart. #### Resend -Resend all chunks to the remote. +Explicitly resend all locally available chunks to the remote. This is a recovery and maintenance operation; ordinary replication does not pre-send every chunk. #### Reset journal received history @@ -982,11 +1047,11 @@ Purge all download/upload cache. Delete all data on the remote server. -### 6. Deprecated +### 6. Garbage Collection V3 (CouchDB only) -#### Run database cleanup +Garbage Collection V3 identifies chunk documents which are not reachable from any current file or live conflict branch, creates logical deletions for those chunks locally, propagates the deletions to CouchDB, and requests remote compaction. -Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Overwrite Server Data with This Device's Files' under Reset Synchronisation information. +Use it only when the Vault, local database, and remote are healthy, and every relevant device has synchronised. It can make an ordinary superseded file revision unreadable when no live state still needs its chunks. It does not repair corruption or replace a deliberate rebuild. See the [Garbage Collection V3 specification](specs_garbage_collection.md). ### 7. Reset diff --git a/docs/setup_object_storage.md b/docs/setup_object_storage.md new file mode 100644 index 00000000..f29f60f1 --- /dev/null +++ b/docs/setup_object_storage.md @@ -0,0 +1,108 @@ +# Set up Object Storage + +This guide establishes Object Storage synchronisation on a first device, generates a Setup URI for another device from that working device, and verifies synchronisation in both directions. + +Object Storage uses the S3-compatible API. Prepare the following before starting: + +- an HTTPS endpoint reachable by every device; +- an access key and secret key with access to the selected bucket; +- a bucket name and region; +- a unique bucket prefix when the bucket is shared; and +- separate passphrases for Vault encryption and Setup URI encryption. + +Back up every Vault involved, and do not use Obsidian Sync, iCloud synchronisation, or another synchronisation service on the same Vault. + +## Generate the initial Setup URI + +The public generator applies the Object Storage preset and records the connection as the selected remote profile. Run it from a trusted terminal: + +```sh +export remote_type=s3 +export endpoint=https://objects.example.com +export access_key= +export secret_key= +export bucket=vault-data +export region=auto +export bucket_prefix=my-vault +export passphrase= +export uri_passphrase= +deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts +``` + +For providers which require them, set `force_path_style`, `use_custom_request_handler`, or `bucket_custom_headers` as described in the [setup utility reference](../utils/readme.md#object-storage). + +Store the generated Setup URI and Setup URI passphrase separately. The URI is encrypted, but it contains the Object Storage credentials. + +## Set up the first device + +Use a new bucket prefix, or a prefix whose contents you deliberately intend to replace. + +1. Install and enable Self-hosted LiveSync in the intended Vault. +2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice. +3. Select `I am setting this up for the first time`, then choose the recommended Setup URI method. +4. Paste the initial Setup URI, enter its passphrase, and select `Test Settings and Continue`. + + ![Object Storage Setup URI on the first device](../images/object-storage-setup/guide-object-storage-setup-first-setup-uri.png) + +5. Select `Restart and Initialise Server`, then read and accept the final overwrite confirmation only when this Vault is the intended source of truth. + + ![Object Storage initialisation on the first device](../images/object-storage-setup/guide-object-storage-setup-first-initialise.png) + + ![Final Object Storage overwrite confirmation](../images/object-storage-setup/guide-object-storage-setup-first-rebuild-confirmation.png) + +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. + + ![Expected missing remote configuration for a new Object Storage prefix](../images/object-storage-setup/guide-object-storage-setup-missing-remote-configuration.png) + +7. Keep optional features disabled until ordinary note synchronisation works. +8. Create an ordinary test note, and keep Obsidian open until the LiveSync progress indicators have cleared. + +## Generate the second-device Setup URI + +Generate a fresh Setup URI from the working first device: + +1. Run `Self-hosted LiveSync: Copy settings as a new Setup URI` from the command palette. +2. Enter a new Setup URI passphrase. + + ![Masked passphrase for the Object Storage Setup URI for another device](../images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-passphrase.png) + +3. Copy the resulting URI. + + ![Object Storage Setup URI generated by the working first device](../images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-result.png) + +Store this URI and its passphrase separately. + +## Add another device + +Start with a new or separately backed-up Vault. + +1. Install and enable Self-hosted LiveSync. +2. Open onboarding, select `I am adding a device to an existing synchronisation setup`, and choose the recommended Setup URI method. +3. Enter the URI generated by the first device and its passphrase. + + ![Object Storage Setup URI from the first device entered on the second device](../images/object-storage-setup/guide-object-storage-setup-second-setup-uri.png) + +4. Select `Restart and Fetch Data`. + + ![Object Storage Fetch confirmation on the second device](../images/object-storage-setup/guide-object-storage-setup-second-fetch.png) + +5. For a new or empty Vault, choose `Overwrite all with remote files`, then `Keep local files even if not on remote`. Review the [Fast Setup guide](./tips/fast-setup.md) before choosing a different policy for a Vault containing local work. + + ![Object Storage retrieval method](../images/object-storage-setup/guide-object-storage-setup-retrieval-method.png) + + ![Object Storage local-file policy](../images/object-storage-setup/guide-object-storage-setup-local-file-policy.png) + +6. Keep Obsidian open until retrieval and file reflection finish. + +Confirm that the first device's test note appears unchanged. Create a second ordinary note on the new device, wait for its journal synchronisation to finish, and confirm that it reaches the first device. Configure optional features only after this two-way check passes. + +![Note from the first device received by the second Object Storage device](../images/object-storage-setup/guide-object-storage-setup-first-to-second.png) + +![Second-device note received by the first Object Storage device](../images/object-storage-setup/guide-object-storage-setup-second-to-first.png) + +## Safety notes + +- Treat the endpoint, bucket, prefix, access key, secret key, Vault passphrase, Setup URI, and Setup URI passphrase as sensitive. +- Use a distinct prefix per synchronisation set unless shared data is explicitly intended. +- Do not initialise the first device against an existing prefix unless replacing its contents is deliberate. +- Object Storage is not a Vault backup. Keep independent backups and test restoration separately. diff --git a/docs/setup_own_server.md b/docs/setup_own_server.md index 9db4a594..5ac2b753 100644 --- a/docs/setup_own_server.md +++ b/docs/setup_own_server.md @@ -33,7 +33,7 @@ ```bash # Adding environment variables. -export hostname=localhost:5984 +export hostname=http://localhost:5984 export username=goojdasjdas #Please change as you like. export password=kpkdasdosakpdsa #Please change as you like @@ -115,31 +115,27 @@ Congrats, move on to [step 2](#2-run-couchdb-initsh-for-initialise) Please refer to the [official document](https://docs.couchdb.org/en/stable/install/index.html). However, we do not have to configure it fully. Just the administrator needs to be configured. ## 2. Run couchdb-init.sh for initialise + +Deno 2 is required. Export the CouchDB connection and database details, then run the provisioning wrapper: + ``` +export hostname=http://localhost:5984 +export username= +export password= +export database=obsidiannotes curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | bash ``` If it results like the following: ``` --- Configuring CouchDB by REST APIs... --> -{"ok":true} -"" -"" -"" -"" -"" -"" -"" -"" -"" -<-- Configuring CouchDB by REST APIs Done! +CouchDB provisioning completed. ``` -Your CouchDB has been initialised successfully. If you want this manually, please read the script. +The wrapper runs the exact registry-pinned Commonlib consumer. When `database` is supplied, it creates the database and initialises its LiveSync database-version document through Commonlib. Without `database`, it configures only the CouchDB server. If you are using Docker Compose and the above command does not work or displays `ERROR: Hostname missing`, you can try running the following command, replacing the placeholders with your own values: ``` -curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://:5984 username= password= bash +curl -s https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/couchdb-init.sh | hostname=http://:5984 username= password= database=obsidiannotes bash ``` ## 3. Expose CouchDB to the Internet @@ -171,43 +167,42 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv ## 4. Client Setup > [!TIP] -> Now manual configuration is not recommended for some reasons. However, if you want to do so, please use `Setup wizard`. The recommended extra configurations will be also set. +> A generated Setup URI is the recommended path because it carries the current defaults for a new Vault and the selected remote profile. If a Setup URI cannot be generated, follow [Configure CouchDB manually on the first device](./quick_setup.md#configure-couchdb-manually-on-the-first-device), then generate a new Setup URI from that working device for every additional device. ### 1. Generate the setup URI on a desktop device or server ```bash -export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com #Point to your vault -export database=obsidiannotes #Please change as you like -export passphrase=dfsapkdjaskdjasdas #Please change as you like +export hostname=https://tiles-photograph-routine-groundwater.trycloudflare.com +export database=obsidiannotes export username=johndoe -export password=abc123 -deno run -A https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/flyio/generate_setupuri.ts +export password= +export passphrase= +export uri_passphrase= # Optional +deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts ``` > [!TIP] -> What is the `passphrase`? Is it different from `uri_passphrase`? -> Yes, the `passphrase` we have exported now is for an End-to-End Encryption passphrase. -> And, `uri_passphrase` that used in the `generate_setupuri.ts` is a different one; for decrypting Set-up URI at using that. -> Why: I (vorotamoroz) think that the passphrase of the Setup-URI should be different from the E2EE passphrase to prevent exposure caused by operational errors or the possibility of evil in our environment. On top of that, I believe that it is desirable for the Setup-URI to be random. Setup-URI is inevitably long, so it goes through the clipboard. I think that its passphrase should not go through the same path, so it should essentially be typed manually. -> Hence, if we keep empty for uri_passphrase, generate_setupuri.ts generates an adjective-noun-randomnumber passphrase so that we can remember it without going through the clipboard. +> `passphrase` protects the synchronised Vault data with end-to-end encryption. `uri_passphrase` protects only the Setup URI. Use different values, store both securely, and do not send the Setup URI and its passphrase through the same channel. +> +> If `uri_passphrase` is omitted, the generator creates a cryptographically random value and prints it once. + +The generator consumes the exact registry-pinned Commonlib release used by the provisioning utility. It creates a configured CouchDB remote profile, applies the current defaults for a new Vault, and encodes them with Commonlib's Setup URI contract. You will then get the following output: ```bash +Generated couchdb Setup URI. +Your passphrase for the Setup URI is: H7vX...a-random-32-character-value +This passphrase is never shown again, so store it safely. obsidian://setuplivesync?settings=%5B%22tm2DpsOE74nJAryprZO2M93wF%2Fvg.......4b26ed33230729%22%5D - -Your passphrase of Setup-URI is: patient-haze -This passphrase is never shown again, so please note it in a safe place. ``` -Please keep your passphrase of Setup-URI. +Store the Setup URI and its passphrase separately. ### 2. Setup Self-hosted LiveSync to Obsidian -[This video](https://youtu.be/7sa_I1832Xc?t=146) may help us. -1. Install Self-hosted LiveSync -2. Choose `Use the copied setup URI` from the command palette and paste the setup URI. (obsidian://setuplivesync?settings=.....). -3. Type the previously displayed passphrase (`patient-haze`) for setup-uri passphrase. -4. Answer `yes` and `Set it up...`, and finish the first dialogue with `Keep them disabled`. -5. `Reload app without save` once. + +Follow [Quick setup](./quick_setup.md#set-up-the-first-device) for the first device. It covers the current onboarding Notice, Setup URI import, server initialisation, and the safety prompts shown for a newly provisioned database. + +After ordinary note synchronisation works, [generate a new Setup URI on that first device](./quick_setup.md#create-a-setup-uri-for-another-device), then follow [Add another device](./quick_setup.md#add-another-device). Do not make the second device depend on retaining the initial Setup URI produced during provisioning. Configure optional features only after the normal path is verified; [Hidden File Sync has its own guide](./tips/hidden-file-sync.md). --- diff --git a/docs/setup_own_server_cn.md b/docs/setup_own_server_cn.md index 1c88ed61..809c02e3 100644 --- a/docs/setup_own_server_cn.md +++ b/docs/setup_own_server_cn.md @@ -1,152 +1,152 @@ -# 在你自己的服务器上设置 CouchDB - -## 目录 -- [配置 CouchDB](#配置-CouchDB) -- [运行 CouchDB](#运行-CouchDB) - - [Docker CLI](#docker-cli) - - [Docker Compose](#docker-compose) -- [创建数据库](#创建数据库) -- [从移动设备访问](#从移动设备访问) - - [移动设备测试](#移动设备测试) - - [设置你的域名](#设置你的域名) ---- - -> 注:提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。推荐直接使用该 docker-compose 配置进行搭建。(若使用,请查阅链接中的文档,而不是这个文档) - -## 配置 CouchDB - -设置 CouchDB 的最简单方法是使用 [CouchDB docker image]((https://hub.docker.com/_/couchdb)). - -需要修改一些 `local.ini` 中的配置,以让它可以用于 Self-hosted LiveSync,如下: - -``` -[couchdb] -single_node=true -max_document_size = 50000000 - -[chttpd] -require_valid_user = true -max_http_request_size = 4294967296 - -[chttpd_auth] -require_valid_user = true -authentication_redirect = /_utils/session.html - -[httpd] -WWW-Authenticate = Basic realm="couchdb" -enable_cors = true - -[cors] -origins = app://obsidian.md,capacitor://localhost,http://localhost -credentials = true -headers = accept, authorization, content-type, origin, referer -methods = GET, PUT, POST, HEAD, DELETE -max_age = 3600 -``` - -## 运行 CouchDB - -### Docker CLI - -你可以通过指定 `local.ini` 配置运行 CouchDB: - -``` -$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb -``` -*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* - -后台运行: -``` -$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb -``` -*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* - -### Docker Compose -创建一个文件夹, 将你的 `local.ini` 放在文件夹内, 然后在文件夹内创建 `docker-compose.yml`. 请确保对 `local.ini` 有读写权限并且确保在容器运行后能创建 `data` 文件夹. 文件夹结构大概如下: -``` -obsidian-livesync -├── docker-compose.yml -└── local.ini -``` - -可以参照以下内容编辑 `docker-compose.yml`: -```yaml -services: - couchdb: - image: couchdb - container_name: obsidian-livesync - user: 1000:1000 - environment: - - COUCHDB_USER=admin - - COUCHDB_PASSWORD=password - volumes: - - ./data:/opt/couchdb/data - - ./local.ini:/opt/couchdb/etc/local.ini - ports: - - 5984:5984 - restart: unless-stopped -``` - -最后, 创建并启动容器: -``` -# -d will launch detached so the container runs in background -docker-compose up -d -``` - -## 创建数据库 - -CouchDB 部署成功后, 需要手动创建一个数据库, 方便插件连接并同步. - -1. 访问 `http://localhost:5984/_utils`, 输入帐号密码后进入管理页面 -2. 点击 Create Database, 然后根据个人喜好创建数据库 - -## 从移动设备访问 -如果你想要从移动设备访问 Self-hosted LiveSync,你需要一个合法的 SSL 证书。 - -### 移动设备测试 -测试时,[localhost.run](http://localhost.run/) 这一类的反向隧道服务很实用。(非必须,只是用于终端设备不方便 ssh 的时候的备选方案) - -``` -$ ssh -R 80:localhost:5984 nokey@localhost.run -Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts. - -=============================================================================== -Welcome to localhost.run! - -Follow your favourite reverse tunnel at [https://twitter.com/localhost_run]. - -**You need a SSH key to access this service.** -If you get a permission denied follow Gitlab's most excellent howto: -https://docs.gitlab.com/ee/ssh/ -*Only rsa and ed25519 keys are supported* - -To set up and manage custom domains go to https://admin.localhost.run/ - -More details on custom domains (and how to enable subdomains of your custom -domain) at https://localhost.run/docs/custom-domains - -To explore using localhost.run visit the documentation site: -https://localhost.run/docs/ - -=============================================================================== - - -** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. ** - -xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run -Connection to localhost.run closed by remote host. -Connection to localhost.run closed. -``` - -https://xxxxxxxx.localhost.run 即为临时服务器地址。 - -### 设置你的域名 - -设置一个指向你服务器的 A 记录,并根据需要设置反向代理。 - -Note: 不推荐将 CouchDB 挂载到根目录 -可以使用 Caddy 很方便的给服务器加上 SSL 功能 - -提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。 - -注意检查服务器日志,当心恶意访问。 +# 在你自己的服务器上设置 CouchDB + +## 目录 +- [配置 CouchDB](#配置-CouchDB) +- [运行 CouchDB](#运行-CouchDB) + - [Docker CLI](#docker-cli) + - [Docker Compose](#docker-compose) +- [创建数据库](#创建数据库) +- [从移动设备访问](#从移动设备访问) + - [移动设备测试](#移动设备测试) + - [设置你的域名](#设置你的域名) +--- + +> 注:提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。推荐直接使用该 docker-compose 配置进行搭建。(若使用,请查阅链接中的文档,而不是这个文档) + +## 配置 CouchDB + +设置 CouchDB 的最简单方法是使用 [CouchDB docker image]((https://hub.docker.com/_/couchdb)). + +需要修改一些 `local.ini` 中的配置,以让它可以用于 Self-hosted LiveSync,如下: + +``` +[couchdb] +single_node=true +max_document_size = 50000000 + +[chttpd] +require_valid_user = true +max_http_request_size = 4294967296 + +[chttpd_auth] +require_valid_user = true +authentication_redirect = /_utils/session.html + +[httpd] +WWW-Authenticate = Basic realm="couchdb" +enable_cors = true + +[cors] +origins = app://obsidian.md,capacitor://localhost,http://localhost +credentials = true +headers = accept, authorization, content-type, origin, referer +methods = GET, PUT, POST, HEAD, DELETE +max_age = 3600 +``` + +## 运行 CouchDB + +### Docker CLI + +你可以通过指定 `local.ini` 配置运行 CouchDB: + +``` +$ docker run --rm -it -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb +``` +*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* + +后台运行: +``` +$ docker run -d --restart always -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -v /path/to/local.ini:/opt/couchdb/etc/local.ini -p 5984:5984 couchdb +``` +*记得将上述命令中的 local.ini 挂载路径替换成实际的存放路径* + +### Docker Compose +创建一个文件夹, 将你的 `local.ini` 放在文件夹内, 然后在文件夹内创建 `docker-compose.yml`. 请确保对 `local.ini` 有读写权限并且确保在容器运行后能创建 `data` 文件夹. 文件夹结构大概如下: +``` +obsidian-livesync +├── docker-compose.yml +└── local.ini +``` + +可以参照以下内容编辑 `docker-compose.yml`: +```yaml +services: + couchdb: + image: couchdb + container_name: obsidian-livesync + user: 1000:1000 + environment: + - COUCHDB_USER=admin + - COUCHDB_PASSWORD=password + volumes: + - ./data:/opt/couchdb/data + - ./local.ini:/opt/couchdb/etc/local.ini + ports: + - 5984:5984 + restart: unless-stopped +``` + +最后, 创建并启动容器: +``` +# -d will launch detached so the container runs in background +docker-compose up -d +``` + +## 创建数据库 + +CouchDB 部署成功后, 需要手动创建一个数据库, 方便插件连接并同步. + +1. 访问 `http://localhost:5984/_utils`, 输入帐号密码后进入管理页面 +2. 点击 Create Database, 然后根据个人喜好创建数据库 + +## 从移动设备访问 +如果你想要从移动设备访问 Self-hosted LiveSync,你需要一个合法的 SSL 证书。 + +### 移动设备测试 +测试时,[localhost.run](http://localhost.run/) 这一类的反向隧道服务很实用。(非必须,只是用于终端设备不方便 ssh 的时候的备选方案) + +``` +$ ssh -R 80:localhost:5984 nokey@localhost.run +Warning: Permanently added the RSA host key for IP address '35.171.254.69' to the list of known hosts. + +=============================================================================== +Welcome to localhost.run! + +Follow your favourite reverse tunnel at [https://twitter.com/localhost_run]. + +**You need a SSH key to access this service.** +If you get a permission denied follow Gitlab's most excellent howto: +https://docs.gitlab.com/ee/ssh/ +*Only rsa and ed25519 keys are supported* + +To set up and manage custom domains go to https://admin.localhost.run/ + +More details on custom domains (and how to enable subdomains of your custom +domain) at https://localhost.run/docs/custom-domains + +To explore using localhost.run visit the documentation site: +https://localhost.run/docs/ + +=============================================================================== + + +** your connection id is xxxxxxxxxxxxxxxxxxxxxxxxxxxx, please mention it if you send me a message about an issue. ** + +xxxxxxxx.localhost.run tunneled with tls termination, https://xxxxxxxx.localhost.run +Connection to localhost.run closed by remote host. +Connection to localhost.run closed. +``` + +https://xxxxxxxx.localhost.run 即为临时服务器地址。 + +### 设置你的域名 + +设置一个指向你服务器的 A 记录,并根据需要设置反向代理。 + +Note: 不推荐将 CouchDB 挂载到根目录 +可以使用 Caddy 很方便的给服务器加上 SSL 功能 + +提供了 [docker-compose.yml 和 ini 文件](https://github.com/vrtmrz/self-hosted-livesync-server) 可以同时启动 Caddy 和 CouchDB。 + +注意检查服务器日志,当心恶意访问。 diff --git a/docs/setup_p2p.md b/docs/setup_p2p.md new file mode 100644 index 00000000..fb13a9c1 --- /dev/null +++ b/docs/setup_p2p.md @@ -0,0 +1,138 @@ +# Set up peer-to-peer synchronisation + +This guide configures a working first device through the ordinary user interface, generates a Setup URI for an additional device, and verifies synchronisation in both directions with explicit peer approval. + +Peer-to-peer synchronisation has no central data-storage server containing a copy of the Vault. A signalling relay is still required for peer discovery. The project's public signalling relay avoids the need to provision one for an ordinary setup; a controlled setup can use another Nostr-compatible relay. Vault data travels through the encrypted peer connection, not through the signalling relay. + +See [How peer-to-peer synchronisation works](p2p.md) for the communication model, the public relay policy, and the distinction between signalling and TURN. + +Before starting: + +- back up both Vaults; +- decide whether to use the project's public signalling relay or another relay reachable by both devices; +- ensure the networks permit a WebRTC connection, or review the [P2P troubleshooting guidance](./tips/p2p-sync-tips.md); +- disable every other synchronisation service for these Vaults; and +- keep both devices awake and Obsidian open during the initial transfer. + +## Set up the first device + +1. Install and enable Self-hosted LiveSync in the intended Vault. +2. Open onboarding from the `Welcome to Self-hosted LiveSync` Notice. +3. Select `I am setting this up for the first time`, choose manual configuration, then select `Peer-to-Peer only`. +4. In `P2P Configuration`: + - enable P2P; + - select `Use the project's public signalling relay`, or enter your own signalling relay URLs; + - generate or enter a private Group ID; + - enter a strong P2P passphrase; + - enter a unique name for this device; and + - leave automatic start and automatic announcements disabled until the manual round trip succeeds. +5. Select `Test Settings and Continue`. The test joins the signalling relay; it does not require another peer to be online. +6. Complete the initialisation and final confirmation on the first device. This initialises the local LiveSync database; P2P has no central remote database to erase. + + ![P2P local initialisation on the first device](../images/p2p-setup/guide-p2p-setup-first-initialise.png) + + ![P2P local database confirmation on the first device](../images/p2p-setup/guide-p2p-setup-first-rebuild-confirmation.png) + +7. Keep optional features disabled until ordinary note synchronisation works. +8. After saving the P2P profile, open `Self-hosted LiveSync: P2P Sync : Open P2P Status` from the command palette. The P2P ribbon icon provides the same destination. Select `Open connection` if signalling is disconnected. + + ![First P2P device connected to the signalling relay](../images/p2p-setup/guide-p2p-setup-first-device-connected.png) + +9. Create an ordinary test note and wait for the local LiveSync progress indicators to clear. + +## Generate the second-device Setup URI + +On the working first device: + +1. Run `Self-hosted LiveSync: Copy settings as a new Setup URI` from the command palette. +2. Enter a new Setup URI passphrase. + + ![Masked passphrase for the P2P Setup URI for another device](../images/p2p-setup/guide-p2p-setup-copy-setup-uri-passphrase.png) + +3. Copy the resulting URI. + + ![P2P Setup URI generated by the working first device](../images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png) + +Keep the first device online. Store the new URI and its passphrase separately. + +## Add the second device + +1. Install and enable Self-hosted LiveSync in a new or separately backed-up Vault. +2. Open onboarding, select `I am adding a device to an existing synchronisation setup`, and choose the recommended Setup URI method. +3. Enter the Setup URI generated by the first device and its passphrase. + + ![P2P Setup URI from the first device entered on the second device](../images/p2p-setup/guide-p2p-setup-second-setup-uri.png) + +4. Select `Restart and Fetch Data`. + + ![P2P Fetch confirmation on the second device](../images/p2p-setup/guide-p2p-setup-second-fetch.png) + +5. For a new or empty Vault, choose `Overwrite all with remote files`, then `Keep local files even if not on remote`. Review the [Fast Setup guide](./tips/fast-setup.md) before using a Vault which contains local work. + + ![P2P retrieval method](../images/p2p-setup/guide-p2p-setup-retrieval-method.png) + + ![P2P local-file policy](../images/p2p-setup/guide-p2p-setup-local-file-policy.png) + +6. In `P2P Rebuild`, confirm that the expected name of the first device is shown, then select `Sync`. + + ![Selecting the first device for P2P Rebuild](../images/p2p-setup/guide-p2p-setup-select-first-device.png) + +7. On the first device, verify the requesting device name and select `Accept`. Use `Accept Temporarily` instead when approval should last only for this Obsidian session. + + ![Explicit P2P connection request on the first device](../images/p2p-setup/guide-p2p-setup-connection-request-1.png) + +8. Keep both devices open until the test note appears on the second device. + + ![Note from the first device received by the second P2P device](../images/p2p-setup/guide-p2p-setup-first-to-second.png) + +## Verify the return journey + +Create a second ordinary note on the second device. Keep automatic announcements disabled, then run and verify the next synchronisation explicitly: + +1. Open `P2P Status` on both devices. +2. If a peer no longer appears, select `Disconnect` and then `Open connection` on the first device, followed by the second device. The device which joins last is advertised to devices which are already in the room. +3. On the first device, select `Refresh`, verify the second-device name, then select `Replicate now`. +4. On the second device, verify the name of the requesting first device and select `Accept` or `Accept Temporarily`. + + ![Explicit return-journey connection request on the second device](../images/p2p-setup/guide-p2p-setup-connection-request-2.png) + +5. Confirm that the second-device note appears unchanged on the first device. + + ![Second-device note received by the first P2P device](../images/p2p-setup/guide-p2p-setup-second-to-first.png) + +The two devices are now proven to share the same room, encryption settings, and data format in both directions. + +After this manual path works, configure automatic behaviour deliberately: + +- `Announce changes` on a source device dispatches change notifications while it is connected. +- `Follow changes` on the receiving device fetches after notifications from that peer. +- The peer's `More actions` menu can synchronise or follow whenever that named device connects, or include it in the P2P synchronisation command. + +An announcement contains no Vault data and does not transfer a change by itself. The source must announce, the receiver must follow, and both devices must be connected. + +## If a peer does not appear + +- Confirm that both panes show `Connected` and the same Room ID suffix. +- Select `Refresh` after the other device joins. +- Reconnect the device which should be discovered last. +- Check that the Setup URI came from the working first device and that neither device copied a peer name manually. +- Check signalling relay reachability separately from WebRTC connectivity. +- Review VPN and TURN options in [Peer-to-Peer Synchronisation Tips](./tips/p2p-sync-tips.md). + +## Controlled or self-hosted setup + +The ordinary route above starts in the plug-in UI and can use the project's public signalling relay. For a controlled deployment, prepare your own Nostr-compatible relay and enter it in `Signalling relay URLs` on every device. + +The public Setup URI generator is also available when configuration must be created outside Obsidian. Run it from a trusted terminal: + +```sh +export remote_type=p2p +export p2p_relays=wss://relay.example.com +export p2p_room_id= # Optional; generated when omitted +export p2p_passphrase= # Optional; generated when omitted +export passphrase= +export uri_passphrase= +deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts +``` + +The generated Setup URI contains the encrypted room, relay, and Vault settings. It deliberately omits the device-specific name. Store the URI and its passphrase separately. After importing it on the first device, continue from the initialisation step above, then generate a fresh Setup URI for an additional device from that working device. diff --git a/docs/specs_conflict_resolution.md b/docs/specs_conflict_resolution.md new file mode 100644 index 00000000..e476f75c --- /dev/null +++ b/docs/specs_conflict_resolution.md @@ -0,0 +1,258 @@ +# Conflict resolution and revision provenance + +This document describes the conflict-resolution and file-reflection guarantees used by Self-hosted LiveSync 1.0, together with the cases which still require user judgement. The underlying revision-tree operations and injectable provenance contract are owned by `@vrtmrz/livesync-commonlib`; LiveSync owns persistent device-local composition, Vault reflection, settings, and dialogue policy. + +## Revision-tree model + +PouchDB stores a document as a revision tree. It selects one live leaf as the deterministic winner and reports the other live leaves as conflicts. That winner is not proof that its content is newer, safer, or the version currently shown in the Vault. + +For example: + +```text +A1 +├── B1 ── C1 ── D1 +└── B2 ── C2 +``` + +The two live leaves are `D1` and `C2`. Their nearest shared ancestor is `A1`; neither `B1` nor `B2` is shared. A conservative three-way merge therefore compares the changes from `A1` to each leaf. Matching generation numbers, or selecting the first older revision from one branch, does not prove shared ancestry. + +Resolving a conflict writes the selected or merged result on one observed branch and deletes the other observed live leaf. A stale device may still have the deleted leaf's content in its Vault when it receives the resolution. + +## Implemented 1.0 guarantees + +- Automatic text and structured-data merge uses the nearest `available` revision ID which is present in both leaf histories. +- Missing or compacted history stops conservative automatic merge instead of guessing a base. +- A receiving Vault file which exactly matches any available revision in the document tree is treated as previously synchronised content. This includes an ancestor below a deleted losing leaf. +- A receiving Vault file whose bytes do not match any available revision is preserved as an unsynchronised local change. +- File bytes, rather than path, size, modification time, or revision generation, determine whether content is known. +- Three or more live versions are reviewed one pair at a time in a deterministic order, with each completed pair committed before the next live pair is read. +- Each device records the exact revision most recently reflected in each Vault file. An edit, deletion, or case-only rename made while a conflict is active extends that displayed branch rather than the deterministic database winner. +- A cross-path rename stores the target before logically deleting only the displayed source branch. + +The all-branch history check prevents a resolved conflict from being recreated merely because the receiving Vault still contains the known losing version. If the user has edited that version again, its bytes differ and the overwrite guard preserves it. + +## Resolution patterns + +| State | Safe action | +| -------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Both leaves contain identical bytes | Collapse the duplicate leaf. | +| Text or structured data has an available shared base and non-overlapping changes | Perform a conservative three-way merge. | +| One side deletes content which the other leaves unchanged | Preserve the deletion. | +| One side deletes content which the other modifies | Ask the user. | +| A receiving file matches a revision available anywhere in the tree | Apply the propagated database result. | +| A receiving file matches no available revision | Preserve it and ask the user. | +| A required body or shared ancestor is missing or compacted | Ask the user. | +| Binary contents differ | Prefer an explicit user selection; semantic merge is unavailable. | + +The compatibility implementation currently selects the newer modification time for differing binary conflicts even when the general **Always overwrite with a newer file** option is disabled. This is existing behaviour, not a new 1.0 guarantee. Changing it to explicit selection only is a separate compatibility decision. + +## Unreadable revisions and repair + +A document revision can remain in the PouchDB tree while one or more chunks needed to reconstruct its content are unavailable. Missing content is not evidence that the revision is obsolete. LiveSync therefore leaves an unreadable winner or conflict revision in the tree instead of deleting it during automatic conflict processing. + +**Hatch** → **Inspect conflicts and file/database differences** inspects the current winner, every current conflict revision, and the nearest shared ancestor for each conflict. A logical-deletion winner and an absent Vault file already agree, so that state is not reported unless another live branch still requires attention. When the Vault already matches the winner but conflict branches remain, the card shows the compact status `✅ Vault matches winner · ⚠️ Conflicts: N`; matching the winner does not mean that the conflict has been resolved. + +Each reported live revision has a compact wrench menu. The available actions depend on the exact revision and current Vault state: + +- **Compare with Vault** opens the existing difference dialogue in read-only mode for differing text files. +- **Apply this revision to Vault** writes the selected readable revision, even when it is not the database winner. Replacing an existing file requires confirmation. +- **Mark this revision as the Vault version** is offered when the bytes already match. It records exact device-local provenance without creating a child revision, and refuses the operation if the file changed after inspection. +- **Store Vault file as a child of this revision** preserves the current Vault bytes on the explicitly selected live branch. +- **Apply logical deletion to Vault** removes an existing Vault file after confirmation. An absent file needs no retained deletion provenance. +- **Retry reading revision** attempts the configured chunk-retrieval path again. It does not change the revision tree. +- **Discard this branch** is available for each exact live revision while at least one other live branch remains. It requires confirmation and creates a logical deletion on only the selected branch without changing the current Vault file. +- **Discard unreadable revision** remains available as a recovery action when an unreadable revision is the only live leaf. It requires confirmation because no other database branch remains. + +Every mutating action rechecks that the selected revision is still a current live leaf. If another operation resolved or replaced it, the action fails and the card is refreshed instead of extending an obsolete branch. + +The card uses compact, mobile-friendly diagnostic rows with an emoji and a text label. `🧩 Missing chunks: N` identifies an unreadable revision. In the database row, `Δsize` is decoded size minus recorded size; in the Vault row, `Δsize vs DB` is Vault size minus decoded database size. `Δtime` is Vault modification time minus database modification time. The ordinary two-second comparison window still labels which side is newer. These values help diagnose a mismatch; path, size, and modification time do not prove revision identity or decide which content should win. + +A shared ancestor is informational. An ancestor which is no longer a live revision cannot be discarded independently through this workflow. If its body is unavailable, conservative three-way merge remains disabled, although readable live revisions can still be selected manually. + +Logical deletion does not recreate missing bytes, purge the document history, or prove that the deleted version was unimportant. Another replica or backup may still contain the missing chunks. Recover from that source before discarding a revision whenever possible. + +**Recreate chunks for current Vault files** can recreate chunks only from files which are readable in the current Vault. It cannot reconstruct unique bytes from an unavailable historical or conflict revision. + +Garbage Collection V3 treats every live conflict revision and its nearest available shared ancestor as reachable. Their locally available chunks are retained until the conflict is resolved. After resolution, chunks used only by the discarded branch or no-longer-needed merge ancestry can become eligible for collection. See the [Garbage Collection V3 specification](specs_garbage_collection.md). + +A generation-one revision has no parent. When its body is unavailable, LiveSync cannot preserve a changed Vault file as a sibling branch without inventing ancestry. It leaves the operation unresolved. Recover the missing chunks from another replica or backup, or explicitly discard that live revision. If the current Vault file is the intended replacement, it can be stored after the unreadable revision has been logically deleted. + +### Two devices independently create the same path + +If two devices create the same full synchronised path before either device has +received the other creation, the two generation-one leaves have no shared +revision. Files with the same name in different directories remain separate +paths and do not form this conflict. + +When the independently created files contain identical bytes, LiveSync deletes +one duplicate leaf without synthesising merged content. A device which still +records the deleted duplicate as its displayed revision already has the same +bytes as the surviving revision, so it does not recreate the conflict. It +rebinds its device-local provenance to the surviving revision. + +When the independently created files contain different bytes, conservative +three-way merge has no valid base. LiveSync therefore leaves the two versions +for manual selection; it does not guess an empty base or concatenate unrelated +files. If both versions instead descend from a revision which the devices had +previously synchronised, they are ordinary divergent branches: LiveSync may +merge non-overlapping text or structured-data changes from that shared base, +and otherwise asks the user. + +## Stale and concurrent resolutions + +A device can resolve only the leaves which it has observed. If another device has already extended a branch, later replication can reveal another live leaf and require another resolution. Two devices can also produce different resolutions concurrently, leaving multiple live leaves after their trees meet. + +A higher revision generation or modification time does not make either result authoritative. The resolver must examine every current live leaf again until one result remains or user action is required. This is continued conflict processing, not a reset of the synchronisation checkpoint. + +## More than two live versions + +When three or more versions remain, LiveSync compares the current PouchDB winner with one conflict leaf at a time. Commonlib orders the remaining candidates by revision generation ascending, original leaf modification time ascending, then the complete revision ID in code-unit lexical order. A missing or non-finite modification time is ordered before a finite value. Modification time makes pair selection reproducible here; it does not decide which content wins. + +For each pair, LiveSync first collapses identical content, then attempts a conservative sensible merge, and finally asks the user when neither automatic action is safe. A completed action is written to the ordinary revision tree and its losing observed leaf is deleted before LiveSync reads the remaining live leaves again. There is no separate persistent merge accumulator. + +**Concat both** writes the concatenated result as a new child of the displayed PouchDB winner, then deletes only the other leaf shown in that dialogue. With two live versions, that action resolves the conflict. With three or more, the new child remains live against every untouched leaf and becomes part of the next pairwise review; it does not create an unrelated root or consume an unseen branch. + +Consequently, choosing **Not now** or closing Obsidian cannot undo a completed pair. After restart, LiveSync reconstructs the next pair from the live tree. If replication changes either revision while a dialogue is open, LiveSync discards the stale selection, refreshes the live count, and rechecks the path rather than deleting a revision which was not the one shown. + +## Device-local file provenance + +LiveSync composes Commonlib's injected `FileReflectionProvenance` with its local key-value database. Each device stores: + +```text +path -> { revision, observedStorageMtime? } +``` + +`revision` identifies the exact database revision which most recently produced the displayed Vault file. `observedStorageMtime` is the raw local modification time observed after reflection. It is not rounded, combined with another device's value, or used as proof of branch identity. No content hash is persisted. + +The record changes only after a successful database-to-Vault reflection or Vault-to-database write. Reading a file does not change it. The recorded revision remains authoritative even if the user edits the file to bytes which equal another branch; otherwise content equality could silently move the edit to a branch which was not displayed. + +LiveSync creates the namespaced store handle during service composition, before the key-value database is open. The sequential `onSettingLoaded` lifecycle opens that database before Vault scanning, watching, or replication starts. Store operations do not wait for implicit readiness: a lifecycle violation fails promptly, avoiding an indefinite or self-referential initialisation wait. Local database reset is a transient unavailable boundary, after which scanning reconstructs derived state. + +When no record exists, LiveSync may reconstruct the displayed revision only if the current Vault bytes match exactly one available revision body. No match, or identical content in multiple revisions, cannot prove branch identity. + +## Operations while a conflict exists + +- Editing a file writes a child of its recorded or uniquely reconstructed displayed revision. +- Deleting a file writes a logical-deletion child of that revision. It uses LiveSync's `deleted` marker, rather than a PouchDB `_deleted` tombstone, so the deletion remains a live branch which can replicate and be resolved against the other branch. +- A case-only rename writes the new path as a child in the same document tree. +- A cross-path rename stores the target document first, then writes a logical-deletion child on the displayed source branch. + +If an edit's base cannot be proved, LiveSync keeps the bytes as another manual-resolution branch instead of attaching them silently to the database winner. If a deletion's displayed branch cannot be proved after the file body has gone, LiveSync preserves every branch and requests conflict review. For an unproven cross-path rename, the new target remains stored and every source branch is preserved for review. These fallbacks can leave a temporary duplicate or unresolved source, but they do not discard an unproven branch. + +## Interactive dialogue policy + +Choosing **Not now** postpones repeated merge dialogues for the same +uninterrupted conflict episode in the current plug-in session. Ordinary file +checks and replication do not reopen the dialogue while at least one conflict +leaf remains. If the in-editor status display is enabled, the active file shows +**This file has 3 unresolved versions. They will be reviewed one pair at a +time.** for three or more live versions, using the current count, and **This +file has unresolved conflicts.** for two. Postponement therefore does not make +the conflict invisible. + +The command **Resolve if conflicted.**, and selecting a file through **Pick a +file to resolve conflict**, explicitly clear the postponement and request the +dialogue again. Cancellation caused by another conflict dialogue does not count +as **Not now**. Once the document has no remaining conflicts, the episode ends; +a later conflict at the same path prompts normally. The postponement is not +persisted across a plug-in reload. Completed pairwise resolutions are persisted +in the ordinary revision tree, so a reload forgets only the postponement and +does not repeat an already committed stage. + +When synchronisation supplies a resolved document, the existing incoming-file +processing event closes an open conflict dialogue for that path. The same event +rechecks the local revision tree: if no conflict leaf remains, it ends any +postponed episode and removes the active-file warning. If conflict leaves still +exist, the stale dialogue closes and the warning changes to the current live +version count. A postponed episode stays postponed; otherwise, subsequent +conflict processing may open a fresh dialogue for the current revision tree. +Each dialogue owns its completion result, so a prompt which is answered or +closed immediately still completes the waiting conflict operation; the result +does not depend on a later global listener being ready. +The end of an automatic repeat is silent. An explicit **Pick a file to resolve +conflict** request which starts with no conflicts may show one confirmation +Notice. + +## Example device scenarios + +### A user edits the branch shown on one device + +Mac and Android have produced two branches of `shared.md`. Mac's local database selects revision `C1` as its deterministic winner, but the file currently shown in the Android Vault came from revision `C2`: + +```text +A1 +├── B1 ── C1 database winner +└── B2 ── C2 displayed on Android +``` + +Android recorded `C2` when it wrote that revision into the Vault. If the user edits the file on Android, the new revision extends `C2`: + +```text +A1 +├── B1 ── C1 +└── B2 ── C2 ── D2 Android edit +``` + +After synchronisation, both devices receive `C1` and `D2` as the live branches. The edit is not moved silently onto `C1`, and ordinary conflict resolution can compare the real descendants. + +### A user deletes the branch shown on one device + +If Android deletes the file while it still displays `C2`, LiveSync writes a logical-deletion revision below `C2`: + +```text +A1 +├── B1 ── C1 +└── B2 ── C2 ── D2 (deleted: true) +``` + +The deletion remains one side of the live conflict. The user can still choose between the content at `C1` and deleting the file. LiveSync does not delete `C1` merely because PouchDB selected it as the winner. + +### A user renames a conflicted file + +If the user changes only the spelling case, such as `Note.md` to `note.md`, LiveSync keeps the rename in the same revision tree and extends the revision displayed on that device. + +If the user renames `draft.md` to `published.md`, LiveSync stores `published.md` before it marks the displayed `draft.md` branch as logically deleted. If an interruption occurs between those operations, the recoverable result is a duplicate which can be reviewed, rather than loss of the only copy. Any other live branch of `draft.md` remains available for conflict resolution. + +### A remote resolution reaches a device which still shows the losing content + +Android may resolve a conflict and continue editing while Mac still shows the losing revision. When Mac receives the resolved tree, LiveSync searches every available branch and recognises Mac's unchanged bytes as content which was already synchronised below the deleted losing leaf. It can apply Android's resolution without asking Mac to resolve the same unchanged conflict again. + +If the user edited the file on Mac before the resolution arrived, the bytes no longer match that historical revision. LiveSync preserves the Mac edit as an unsynchronised conflict instead of overwriting it. + +### A three-version review is interrupted + +Mac receives three live versions of `shared.md`. The active-file status reports three unresolved versions, and the first dialogue compares the deterministic winner with the first ordered conflict leaf. The user completes that pair, leaving two live versions, then chooses **Not now** on the next dialogue and closes Obsidian. + +The first decision has already changed the ordinary revision tree. On restart, LiveSync reads the two surviving versions and presents only that remaining pair; it does not reconstruct the original three-version state. If another device resolves the remaining pair before or while the dialogue is open, the warning disappears and the stale dialogue closes. + +### The device-local record is missing + +A local-database reset removes revision provenance. On the next scan, if the Vault file matches exactly one available revision, LiveSync can reconstruct which branch was displayed and continue from it. If the bytes match multiple revisions, or no available revision, the branch remains unproved. + +In that unproved state, an edit is retained as another manual-resolution branch. A deletion leaves all existing branches intact. A cross-path rename stores the target but leaves every source branch for review. The result can require an extra decision, but it does not discard data by guessing the winner. + +### Start-up or reset overlaps a provenance operation + +LiveSync creates the provenance handle during composition, then opens its backing store during the sequential settings lifecycle before starting scans, watchers, or replication. If the store cannot open, start-up stops rather than leaving file processing waiting indefinitely. + +During reset, the store can be temporarily unavailable. A racing provenance lookup fails promptly and follows the same conservative missing-record behaviour. After reopen, scanning can reconstruct a record when one exact revision body matches the Vault file. + +## Unsafe shortcuts + +Do not: + +- infer a common ancestor from generation numbers alone; +- assume that the PouchDB winner is the version currently displayed in the Vault; +- replace recorded displayed provenance merely because current bytes match another branch; +- discard local content when revision-history lookup fails; +- infer revision identity from path, size, modification time, or content hash without a revision ID; +- select the newest modification time unless the user has explicitly chosen that destructive policy; or +- merge overlapping text edits or unrelated binary contents automatically. + +## Verification + +Commonlib's real-PouchDB and injected-boundary unit tests cover unequal branch lengths, exact shared ancestry, deterministic ordering of multiple live leaves, a sensible stage followed by reconstruction of a manual pair, content below a deleted losing leaf, recorded and reconstructed branch identity, ambiguous matches, conflict-time editing, missing-body preservation when parent metadata is available, refusal to invent a parent for a generation-one revision, logical deletion, case-only rename, cross-path rename, and safe unproven fallbacks. + +LiveSync's optional real-Obsidian two-Vault checks have two scopes. `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true` resolves and edits a Markdown conflict, propagates it to a Vault which still displays the deleted losing content, and requires one live result to remain. `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` edits, deletes, case-renames, and cross-path-renames files while conflicts remain active; it verifies the parent revision of each resulting branch, replicates those exact trees, and confirms that the other live branches remain intact. + +The focused `test:e2e:obsidian:conflict-dialog-policy` scenario creates three live versions in one real Obsidian Vault. It verifies the count warning, commits a concatenated child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the isolated Obsidian profile, and confirms that only the live pair is reconstructed. It also verifies that an incoming resolution closes a stale dialogue, completes the waiting conflict operation, and clears the warning. The repair scenario removes a referenced local chunk, confirms that the exact unreadable live revision remains in the tree, and exercises explicit retry and discard controls without deleting another live revision. diff --git a/docs/specs_data_compression.md b/docs/specs_data_compression.md new file mode 100644 index 00000000..ac6e2845 --- /dev/null +++ b/docs/specs_data_compression.md @@ -0,0 +1,116 @@ +# Data Compression specification + +## Status and decision for 1.0 + +Data Compression is a maintained, advanced opt-in feature for CouchDB-compatible remote databases. It remains disabled by default in the 1.0 line. + +The feature provides a modest, measurable reduction in mixed-workload storage and transfer volume. Its value depends heavily on the chunk contents, while the current implementation adds substantial processing and worker-memory costs. Users with slow, metered, or storage-constrained connections may still find that trade-off worthwhile. + +## Stored-data behaviour + +The `enableCompression` setting affects chunk documents written through the CouchDB remote connection. It does not compress the local database, and it is separate from the journal format used by Object Storage synchronisation. + +For each document containing string `data`, the writer: + +1. detects canonical Base64 and decodes it to bytes, or encodes text as UTF-8; +2. applies raw DEFLATE through fflate at level 8; +3. encodes the result as Base64 and adds the LiveSync compressed-data marker; and +4. stores the compressed representation only when its string representation is shorter than the original. + +Already compressed data and data which does not become smaller are therefore retained unchanged. Readers always recognise and expand the compressed-data marker, including when their own `enableCompression` setting is off. Compressed and uncompressed chunks can coexist in the same remote database. + +When E2EE is enabled, compression is applied before E2EE V2 encryption. The encrypted remote representation does not expose the compression marker. + +Changing the setting does not require a rebuild for compatibility: new writes adopt the selected policy, while existing chunks remain readable. All devices are still asked to agree on the remote tweak value so that future writes use one consistent policy. A deliberate remote rebuild can normalise existing storage, but it is optional rather than a prerequisite for synchronisation. + +## Execution model + +The compression and decompression algorithms are not executed synchronously on Obsidian's UI thread. Browser builds call fflate's asynchronous `deflate` and `inflate` APIs, which create a Web Worker for an operation and terminate it after the callback. The CLI uses the corresponding Node worker-thread path. + +The PouchDB transform hook, Base64 detection and conversion, UTF-8 conversion, worker creation, result conversion, and document mutation still run in the calling JavaScript context. The current implementation also creates a separate fflate worker for each attempted chunk rather than reusing LiveSync's persistent splitting and encryption worker pool. `transform-pouch` applies the incoming transform to a bulk batch with `Promise.all`, so a batch can start many of these workers concurrently. It can therefore consume significant total CPU and memory, and worker churn may still affect responsiveness or mobile process limits even though the DEFLATE calculation itself is off the UI thread. + +The current benchmark measures the Node CLI process. It does not establish Obsidian UI event-loop latency, Electron renderer responsiveness, mobile WebView memory behaviour, battery use, thermal throttling, or platform watchdog thresholds. Those remain real-runtime validation gaps. + +## Reproducible benchmark + +The benchmark is implemented under `src/apps/cli/testdeno` and packaged by `test/bench-network`. It uses the real CLI mirror and synchronisation path, Commonlib 0.1.0-rc.4, CouchDB 3.5.0, the V3 Rabin–Karp splitter, optional Data Compression, and E2EE V2. It runs each of these conditions three times in rotating order: + +- E2EE off, compression off; +- E2EE off, compression on; +- E2EE on, compression off; and +- E2EE on, compression on. + +The 623,553-byte deterministic fixture contains three Markdown files, two generated JPEG files, two repository PNG files, two JSON files, two TypeScript files, one gzip file, and one high-entropy binary file. Every run materialises and byte-compares all 13 files after synchronisation. + +Run it from the repository root: + +```bash +BENCH_COMMAND=compression \ +BENCH_COMPRESSION_REPEAT_COUNT=3 \ +BENCH_COUCHDB_RTT_MS=1 \ +docker compose -f test/bench-network/compose.yml run --build --rm bench-runner +``` + +The latest local three-repeat result was generated on 21st July, 2026. The percentages below compare medians with compression off and on. + +| Measurement | E2EE off | E2EE on | +| ------------------------------------------------ | -------: | ------: | +| Stored chunk-data reduction | 9.12% | 9.01% | +| CouchDB external-size reduction | 9.03% | 8.92% | +| CouchDB file-size reduction | 2.57% | 7.62% | +| Upload request-body reduction | 8.58% | 8.61% | +| Complete materialisation response-body reduction | 6.56% | 4.84% | +| Upload wall-time increase | 197.10% | 199.29% | +| Upload CPU-time increase | 650.43% | 581.54% | +| Complete materialisation wall-time increase | 19.58% | 19.75% | +| Complete materialisation CPU-time increase | 45.11% | 43.98% | + +With E2EE on, median upload wall time rose from 1.49 seconds to 4.45 seconds, CPU time rose from 1.30 seconds to 8.86 seconds, and upload maximum resident memory rose from about 160 MiB to 403 MiB. The full materialisation workflow starts a CLI process for each file and produced a much higher compressed-path peak of about 983 MiB; treat that figure as evidence about the current CLI workflow rather than a browser decompression lower bound. + +The E2EE upload saved 99,034 decoded HTTP body bytes while adding about 2.96 seconds of local processing wall time. A simple serial-transfer estimate puts the wall-time break-even near 0.27 Mbit/s. This estimate excludes headers, contention, request overlap, radio energy, data charges, and server behaviour. The benchmark does not emulate a bandwidth-limited mobile link, so transfer-volume reduction may still be valuable where elapsed time is not the only cost. + +## Results by file kind + +The E2EE stored chunk-data reductions were: + +| Fixture kind | Reduction | +| ------------------- | --------: | +| Markdown | 16.30% | +| JPEG | 4.72% | +| PNG | 6.16% | +| JSON | 72.80% | +| TypeScript | 74.11% | +| gzip | 0% | +| high-entropy binary | 0% | + +These results describe payload and chunk characteristics, not a reliable file-extension policy. The JSON and TypeScript fixtures were repetitive and mapped to relatively large chunks. The Markdown files were split into 151 referenced chunks, so small-chunk overhead and limited repetition windows reduced their benefit. JPEG, PNG, and gzip inputs had already undergone format-level compression, while the deterministic binary input was intentionally difficult to compress. + +The remote transform sees a content-addressed chunk, not a trustworthy original file type. A chunk can also be deduplicated across files with different extensions. Enabling compression only for selected extensions would therefore either require carrying new provenance into the chunk format or make the representation depend on whichever file first produced a shared chunk. Neither is suitable for the 1.0 format. + +## Follow-up optimisation candidates + +The first optimisation should remove unbounded per-chunk worker creation: + +1. add compression and decompression tasks to a reusable worker pool; +2. run synchronous fflate inside those workers so that it does not create a nested worker for each task; +3. put a bounded scheduler or semaphore before dispatch, rather than submitting the complete `Promise.all` batch at once; +4. transfer derived input and output `ArrayBuffer` objects explicitly in each `postMessage` transfer list; and +5. decide that a result is not smaller inside the worker, so an unhelpful compressed buffer does not need to be returned. + +LiveSync's current `bgWorker` is a useful starting point because it creates a fixed pool of approximately half the reported hardware concurrency and selects workers in round-robin order. It is not sufficient unchanged: its `processing` count does not control selection, and it does not limit the number of tasks posted to each worker. Its foreground modules also form a circular dependency: `bgWorker.ts` imports the splitting and encryption adapters, while those adapters import task dispatch and removal from `bgWorker.ts`. Compression must not add another branch to that cycle. + +Before adding compression, the Worker code should be separated into a dependency-bottom pool, task registry, scheduler, and transfer transport, with splitting, encryption, and compression implemented as task adapters above it. The shared scheduler should own concurrency limits, cancellation, crash propagation, fairness, and task clean-up. Compression then needs either a separate bounded lane or scheduling which prevents a long level-8 task from starving splitting and encryption work. + +The `TransformStream` currently used by `bgWorker.splitting` is local to the calling context; it is not transferred to the Worker. Moving a stream endpoint across contexts could provide real end-to-end back-pressure, but it is not the first memory optimisation for Data Compression. The [Streams Standard transfer algorithm](https://streams.spec.whatwg.org/#transferable-streams) transfers the stream endpoint and posts each chunk through an internal `MessagePort` with an empty transfer list. Binary chunks can therefore still be cloned. Explicitly transferring each `ArrayBuffer` is the clearer zero-copy boundary for independent chunk tasks. + +Cross-context transferable streams also cannot yet be required by the supported mobile baseline. WebKit lists `ReadableStream`, `WritableStream`, and `TransformStream` transfer via `postMessage()` as a [Safari 27 beta addition](https://webkit.org/blog/17967/news-from-wwdc26-webkit-in-safari-27-beta/#readablestream-improvements). A future general `bgWorker` redesign may use transferable streams behind capability detection, particularly where back-pressure matters more than copying, but it needs a message-based fallback. + +After bounding worker use, adaptive per-chunk compression can reduce unnecessary work without depending on the source filename: + +1. skip very small chunks where the marker and worker start-up dominate; +2. sample decoded bytes and estimate entropy or repetition before starting level-8 DEFLATE; +3. require both a minimum byte saving and a minimum percentage saving; +4. compare lower DEFLATE levels against level 8; and +5. replace one-worker-per-chunk operation with a bounded, persistent compression worker pool and back-pressure. + +Any optimisation must preserve the current wire contract: content may remain uncompressed, the compressed marker must stay readable, compression must precede E2EE, and mixed representations must interoperate. It should be evaluated with the same four-condition benchmark, a bandwidth-shaped case, UI event-loop latency, and at least one real mobile Obsidian run before reconsidering the default. diff --git a/docs/specs_garbage_collection.md b/docs/specs_garbage_collection.md new file mode 100644 index 00000000..67ca4a30 --- /dev/null +++ b/docs/specs_garbage_collection.md @@ -0,0 +1,65 @@ +# Garbage Collection V3 + +Garbage Collection V3 is a beta maintenance operation for CouchDB remotes. It removes chunk documents which are no longer required by the current local revision tree, propagates those logical deletions to CouchDB, and then requests remote compaction. + +It is not a repair operation. Use it only when the Vault, the local LiveSync database, and the CouchDB remote are healthy, every relevant device has synchronised, and recoverable backups exist. + +## Supported scope + +Garbage Collection V3 is available in Edge Case mode for CouchDB. It is not offered for Object Storage or P2P: + +- Object Storage has a different journal and object lifecycle. +- P2P has no central database to compact and cannot provide the accepted-device progress information required by this workflow. + +The operation requires **Fetch chunks on demand** to be off so that its local reachability result is not confused with chunks which exist only on the remote. + +## Workflow + +After the user starts Garbage Collection V3, LiveSync: + +1. completes a one-shot bidirectional CouchDB synchronisation; +2. reads the accepted-device list and current progress recorded on the remote; +3. requires parseable progress information, warns when an accepted device has no current information or device progress differs, then requires explicit confirmation; +4. computes the chunks reachable from the local PouchDB revision tree; +5. creates a logical deletion for each locally present chunk which is not reachable; +6. completes a push-only replication so that those deletions reach CouchDB; +7. requests CouchDB compaction and waits for it for up to two minutes; and +8. clears the local chunk caches. + +If the initial synchronisation, device inspection, or confirmation fails, the workflow stops before collection. Missing or invalid device progress is treated as a failed inspection rather than offered as a confirmation override. If push-only replication fails, the local logical deletions have already been created, but remote compaction is not started; synchronise again before retrying or using another device. A compaction failure or completion timeout is reported separately and is not also reported as a successful completion. + +## Reachability rules + +A chunk remains reachable when it is referenced by any of the following: + +- the current database winner for a file; +- any other live conflict revision for that file; +- an available revision on either side of a live conflict which is required to describe the divergence; or +- the nearest available revision shared by both live conflict branches. + +Chunk identifiers are content-derived and shared between files. Reachability is therefore collected into one set across the database. A chunk used by two or more current files remains protected even when one file is updated or deleted. + +An ordinary superseded linear revision does not protect its former chunks. Once no current file or live conflict branch references a chunk, it can be collected. After a conflict is resolved, chunks unique to the discarded branch and to no-longer-needed merge ancestry can also become eligible. + +## Consequences + +Garbage Collection deliberately trades historical recoverability for storage. A metadata revision may remain in the revision tree after a chunk which only that superseded revision used has been collected, so that historical body can become unreadable. Remote compaction can then discard old CouchDB revision bodies. Tombstones and retained metadata also consume storage, so the operation does not promise the smallest possible database. + +Writing the same bytes again produces the same content-derived chunk identifier. If that chunk was collected previously, the normal chunk-writing path creates a new live revision for it, and ordinary replication can transfer it again. This does not recover an older file revision automatically; it only makes the newly written content available. + +Garbage Collection does not reconstruct a chunk which is already missing, determine whether an unreadable revision is important, or repair a damaged local database. Use **Inspect conflicts and file/database differences**, another healthy replica, or a backup for those cases. Use **Overwrite Server Data with This Device's Files** only when a chosen Vault is authoritative and a deliberate remote rebuild is required. + +## Verification + +Commonlib tests use real in-memory PouchDB revision trees to verify: + +- collection eligibility after a normal file update; +- protection of chunks shared by multiple current files; +- protection of all live conflict branches and their nearest available shared ancestor; +- eligibility of losing-branch and ancestor-only chunks after conflict resolution; +- propagation of chunk deletion to another PouchDB database; and +- recreation and propagation when the same content is written again. + +Self-hosted LiveSync unit tests verify that Garbage Collection V3 uses Commonlib's revision-aware result, deletes only unreachable chunks, performs the initial bidirectional and final push-only replications in order, and requests remote compaction. + +A disposable real-CouchDB integration test verifies logical deletion on the server, retention of shared and conflict chunks, compaction completion, replication after collection, and recreation of a content-addressed chunk. CouchDB's choice and timing of physical byte reclamation remain an external database boundary, so the test does not assert a particular reduction in database size. diff --git a/docs/terms.md b/docs/terms.md index c8809c85..98cdfcbe 100644 --- a/docs/terms.md +++ b/docs/terms.md @@ -27,7 +27,7 @@ All guidelines and conventions listed below are disclosed and maintained solely - Boot-up sequence (boot-sequence) - The initialisation process of the plug-in when Obsidian starts. It starts with the loading of the plug-in, setting up core services, loading saved settings, and opening the local database. Once the layout is ready, the plug-in checks for the presence of flag files, runs configuration diagnostics, connects to the remote database, and begins file watching. The sequence finishes once the plug-in is fully ready and operational. - Broken files (Size mismatch) - - A state where a file's metadata and the actual content stored in its chunks do not match, causing file retrieval or synchronisation failures. These mismatches can be detected and resolved by running validation tools such as `Verify and repair all files` on the Hatch pane. + - A state where a file's metadata and the actual content stored in its chunks do not match, causing file retrieval or synchronisation failures. These mismatches can be inspected with `Inspect conflicts and file/database differences` on the Hatch pane, then handled one exact revision at a time. - Chunk / Chunks - Divided units of data stored in the database or object storage to facilitate efficient synchronisation. - Compaction @@ -74,8 +74,8 @@ All guidelines and conventions listed below are disclosed and maintained solely - A privacy option that encrypts file paths and folder names on the remote server. - plug-in - We use the hyphenated form `plug-in` in user-facing messages and general documentation, while `plugin` may appear in codebase files, configuration settings, or technical contexts. -- Relay Server (P2P relays) - - A WebSocket-based coordination server used to establish direct WebRTC peer-to-peer connections. The default relay is provided by the plug-in author. +- Signalling relay (P2P) + - A Nostr-compatible WebSocket relay used for peer discovery and WebRTC connection negotiation. It does not store or transfer Vault contents. The project author operates a public relay as a best-effort convenience, and users can provide another compatible relay. - Remediation (maxMTimeForReflectEvents) - A recovery setting that restricts the propagation of changes from the database to local storage, ignoring any file events (such as accidental mass deletions) that occurred after a specified date and time. - Reset Synchronisation on This Device @@ -95,7 +95,7 @@ All guidelines and conventions listed below are disclosed and maintained solely - Sync Mode - The replication trigger mechanism. Users can select from `On Events` (synchronising on local file changes), `Periodic and Events` (synchronising at fixed intervals as well as on events), or `LiveSync` (continuous, real-time synchronisation). - TURN Server (WebRTC P2P) - - A server type (Traversal Using Relays around NAT) used as a fallback to relay traffic when direct WebRTC peer-to-peer connection is blocked by strict NAT or firewalls. + - A Traversal Using Relays around NAT server used as an optional fallback to relay encrypted WebRTC traffic when strict NAT or firewall rules block a direct peer connection. It is distinct from the signalling relay. - Update Thinning (Batch database update) - An optimisation that groups multiple local file edits together over a short delay before committing them to the local database, reducing the number of database write operations. - WebRTC P2P (Peer-to-Peer) diff --git a/docs/tips/hidden-file-sync.md b/docs/tips/hidden-file-sync.md new file mode 100644 index 00000000..4fd89a09 --- /dev/null +++ b/docs/tips/hidden-file-sync.md @@ -0,0 +1,73 @@ +# Hidden File Sync + +Hidden File Sync is an advanced, optional feature for synchronising hidden files and folders, including selected content below `.obsidian`. It is separate from ordinary note synchronisation and is deliberately left disabled during Rebuild and Fetch setup operations. + +Enable it only after ordinary notes synchronise correctly in both directions on every device. + +> [!WARNING] +> Hidden files can control active Obsidian settings, themes, snippets, and plug-ins. Back up every Vault before enabling this feature. Do not use Hidden File Sync and Customisation Sync to manage the same files. + +## Choose the initial source + +Enabling Hidden File Sync requires an initialisation direction: + +- `Merge` compares the local and remote hidden-file state without declaring either side authoritative. Use this when both sides contain changes which must be retained, and review any conflicts. +- `Fetch` treats the remote hidden-file state as authoritative and applies it locally. Use this on an additional device after the desired files have been uploaded. +- `Overwrite` treats this device's hidden files as authoritative and writes them to the remote database. Use this first on the device whose hidden files you intend to distribute. + +`Fetch` and `Overwrite` can replace files on one side. Check the direction and the backup before continuing. + +## Review the file selection + +1. Open Self-hosted LiveSync settings. +2. Open `Setup`, find `Enable extra and advanced features`, and enable `Advanced features`. + + ![Advanced features enabled](../../images/hidden-file-sync/guide-hidden-file-advanced-features.png) + +3. Open `Selector`, then review the `Hidden Files` section. +4. Use `Target patterns` to limit the feature to the hidden files you intend to synchronise. An empty target list includes every otherwise eligible hidden file. +5. Review `Ignore patterns`. The default excludes `node_modules`, `.git`, and Self-hosted LiveSync's own plug-in data. `Add default patterns` offers a `Cross-platform` set which also excludes Obsidian workspace files. + + ![Hidden File target and ignore selectors](../../images/hidden-file-sync/guide-hidden-file-selector.png) + +Prefer a narrow target list. Device-specific workspace state and another plug-in's credentials are usually poor candidates for cross-device synchronisation. + +Target patterns also control directory traversal. A pattern must therefore match each parent directory as well as the intended files. For example, the following pattern admits the `.obsidian` parent and only its `snippets` subtree: + +```text +^\.obsidian(?:$|/snippets(?:/|$)) +``` + +A pattern containing only `snippets` does not admit the `.obsidian` parent, so the scan cannot reach that directory. + +## Enable the first device + +1. Open `Sync Settings`, then find the advanced `Hidden Files` section. + + ![Hidden File Sync initialisation choices](../../images/hidden-file-sync/guide-hidden-file-enable.png) + +2. Under `Enable Hidden File Sync`, select the initialisation direction chosen above. +3. Keep Obsidian open while the initial scan and synchronisation finish. A progress Notice appears when preparation begins and remains visible until the initial scan has finished. + + ![Hidden File Sync initial scan progress Notice](../../images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png) + +4. Restart Obsidian when the completion Notice recommends it. +5. Confirm that the expected hidden files, and only those files, are present in the remote synchronisation state. + +For the common source-of-truth rollout, select `Overwrite` on the authoritative device first. + +## Enable each additional device + +Hidden File Sync must be enabled independently after ordinary setup on each device. + +1. Back up the additional device's Vault. +2. Apply the same target and ignore patterns. +3. Select `Fetch` if the remote hidden files are authoritative. Select `Merge` only when local hidden-file changes must also be retained. +4. Wait for the initial scan and file application to finish, then restart Obsidian if requested. +5. Verify representative files before enabling the feature on the next device. + +If you later run Rebuild or Fetch as a recovery operation, expect optional features to be disabled again. Complete ordinary recovery first, then repeat this guide deliberately. + +## Related settings + +The [settings reference](../settings.md#7-hidden-files-advanced) describes periodic scanning, scanning before replication, notification suppression, selectors, and overwrite patterns. Change those controls only after the basic Hidden File Sync path is working. diff --git a/docs/tips/p2p-sync-tips.md b/docs/tips/p2p-sync-tips.md index 4337bc43..533935ed 100644 --- a/docs/tips/p2p-sync-tips.md +++ b/docs/tips/p2p-sync-tips.md @@ -10,20 +10,55 @@ authors: # Peer-to-Peer Synchronisation Tips +For the first device, Setup URI, additional device, and two-way verification procedure, see [Set up peer-to-peer synchronisation](../setup_p2p.md). For the communication and privacy model, see [How peer-to-peer synchronisation works](../p2p.md). + > [!IMPORTANT] -> Peer-to-peer synchronisation is still an experimental feature. Although we have made every effort to ensure its reliability, it may not function correctly in all environments. +> P2P is a supported opt-in feature, but WebRTC connectivity still depends on the networks available to every device. A direct connection cannot be guaranteed in every environment. -## Difficulties with Peer-to-Peer Synchronisation +## A peer does not appear -It is often the case that peer-to-peer connections do not function correctly, for instance, when using mobile data services. -In such circumstances, we recommend connecting all devices to a single Virtual Private Network (VPN). It is advisable to select a service, such as Tailscale, which facilitates direct communication between peers wherever possible. -Should one be in an environment where even Tailscale is unable to connect, or where it cannot be lawfully installed, please continue reading. +Check discovery before changing any Vault settings: -## A More Detailed Explanation +1. Confirm that both devices use the same **Signalling relay URLs**, Group ID, and P2P passphrase. +2. Confirm that each device has a distinct device name. +3. Open `P2P Status` on both devices and confirm that each shows `Connected`. +4. Select `Refresh` after the other device joins. +5. If the peer remains absent, select `Disconnect`, then `Open connection` on the device which should be advertised again. -The failure of a Peer-to-Peer connection via WebRTC can be attributed to several factors. These may include an unsuccessful UDP hole-punching attempt, or an intermediary gateway intentionally terminating the connection. Troubleshooting this matter is not a simple undertaking. Furthermore, and rather unfortunately, gateway administrators are typically aware of this type of network behaviour. Whilst a legitimate purpose for such traffic can be cited, such as for web conferencing, this is often insufficient to prevent it from being blocked. +The signalling relay discovers peers; it does not prove that the networks can carry a WebRTC data connection. -This situation, however, is the primary reason that our project does not provide a TURN server. Although it is said that a TURN server within WebRTC does not decrypt communications, the project holds the view that the risk of a malicious party impersonating a TURN server must be avoided. Consequently, configuring a TURN server for relay communication is not currently possible through the user interface. Furthermore, there is no official project TURN server, which is to say, one that could be monitored by a third party. +## A peer appears but synchronisation cannot connect -We request that you provide your own server, using your own Fully Qualified Domain Name (FQDN), and subsequently enter its details into the advanced settings. -For testing purposes, Cloudflare's Real-Time TURN Service is exceedingly convenient and offers a generous amount of free data. However, it must be noted that because it is a well-known destination, such traffic is highly conspicuous. There is also a significant possibility that it may be blocked by default. We advise proceeding with caution. +WebRTC may fail when UDP hole punching is blocked by carrier-grade NAT, a firewall, a VPN policy, or an intermediary gateway. + +Try these in order: + +1. Put both devices on the same ordinary network and retry. +2. Remove a VPN temporarily if it blocks peer traffic, or use a trusted VPN such as Tailscale when it provides a reachable path between the devices. +3. In `P2P Configuration` -> `Advanced Settings`, configure a trusted TURN service. + +TURN is a fallback for encrypted WebRTC traffic. It is different from the required signalling relay. The project does not operate an official TURN service. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume. + +## A connected peer does not receive later edits + +An open signalling connection does not automatically move every change. + +- Use `Replicate now` to prove an explicit bidirectional round trip. +- Enable `Announce changes` on the source device before it dispatches notifications. +- Enable `Follow changes` for that source on the receiving device before it fetches in response. +- Use the peer's `More actions` menu only after the manual round trip works. + +If the device was asleep, Obsidian was in the background, or the peer disconnected, run an explicit synchronisation after both devices are visible and connected. + +## Mobile limitations + +Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large synchronisation. Wake Lock support is best effort and cannot prevent the operating system from suspending or terminating a background application. + +## Collect evidence + +If the same room works on one network but not another, include both network types in the report. Run `Generate full report for opening the issue with debug info`, remove credentials and private relay details, and state whether: + +- both devices reached `Connected`; +- each device appeared in `Detected Peers`; +- a connection request appeared; and +- a TURN server or VPN was in use. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 36d11688..0b7c283e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,452 +1,172 @@ -# Tips and Troubleshooting -- [Tips and Troubleshooting](#tips-and-troubleshooting) - - [Tips](#tips) - - [CORS avoidance](#cors-avoidance) - - [CORS configuration with reverse proxy](#cors-configuration-with-reverse-proxy) - - [Nginx](#nginx) - - [Nginx and subdirectory](#nginx-and-subdirectory) - - [Caddy](#caddy) - - [Caddy and subdirectory](#caddy-and-subdirectory) - - [Apache](#apache) - - [Show all setting panes](#show-all-setting-panes) - - [How to resolve `Tweaks Mismatched of Changed`](#how-to-resolve-tweaks-mismatched-of-changed) - - [Notable bugs and fixes](#notable-bugs-and-fixes) - - [Binary files get bigger on iOS](#binary-files-get-bigger-on-ios) - - [Some setting name has been changed](#some-setting-name-has-been-changed) - - [Questions and Answers](#questions-and-answers) - - [How should I share the settings between multiple devices?](#how-should-i-share-the-settings-between-multiple-devices) - - [What should I enter for the passphrase of Setup-URI?](#what-should-i-enter-for-the-passphrase-of-setup-uri) - - [Why the settings of Self-hosted LiveSync itself is disabled in default?](#why-the-settings-of-self-hosted-livesync-itself-is-disabled-in-default) - - [The plug-in says `something went wrong`.](#the-plug-in-says-something-went-wrong) - - [A large number of files were deleted, and were synchronised!](#a-large-number-of-files-were-deleted-and-were-synchronised) - - [Why `Use an old adapter for compatibility` is somehow enabled in my vault?](#why-use-an-old-adapter-for-compatibility-is-somehow-enabled-in-my-vault) - - [ZIP (or any extensions) files were not synchronised. Why?](#zip-or-any-extensions-files-were-not-synchronised-why) - - [I hope to report the issue, but you said you needs `Report`. How to make it?](#i-hope-to-report-the-issue-but-you-said-you-needs-report-how-to-make-it) - - [Where can I check the log?](#where-can-i-check-the-log) - - [Why are the logs volatile and ephemeral?](#why-are-the-logs-volatile-and-ephemeral) - - [Some network logs are not written into the file.](#some-network-logs-are-not-written-into-the-file) - - [If a file were deleted or trimmed, the capacity of the database should be reduced, right?](#if-a-file-were-deleted-or-trimmed-the-capacity-of-the-database-should-be-reduced-right) - - [How to launch the DevTools](#how-to-launch-the-devtools) - - [On Desktop Devices](#on-desktop-devices) - - [On Android](#on-android) - - [On iOS, iPadOS devices](#on-ios-ipados-devices) - - [How can I use the DevTools?](#how-can-i-use-the-devtools) - - [Checking the network log](#checking-the-network-log) - - [Troubleshooting](#troubleshooting) - - [While using Cloudflare Tunnels, often Obsidian API fallback and `524` error occurs.](#while-using-cloudflare-tunnels-often-obsidian-api-fallback-and-524-error-occurs) - - [On the mobile device, cannot synchronise on the local network!](#on-the-mobile-device-cannot-synchronise-on-the-local-network) - - [I think that something bad happening on the vault...](#i-think-that-something-bad-happening-on-the-vault) - - [Flag Files](#flag-files) - - [Old tips](#old-tips) +# Troubleshooting - - -## Tips - -### CORS avoidance - -If we are unable to configure CORS properly for any reason (for example, if we cannot configure non-administered network devices), we may choose to ignore CORS. -To use the Obsidian API (also known as the Non-Native API) to bypass CORS, we can enable the toggle ``Use Request API to avoid `inevitable` CORS problem``. - - - -### CORS configuration with reverse proxy - -- IMPORTANT: CouchDB handles CORS by itself. Do not process CORS on the reverse - proxy. - - Do not process `Option` requests on the reverse proxy! - - Make sure `host` and `X-Forwarded-For` headers are forwarded to the CouchDB. - - If you are using a subdirectory, make sure to handle it properly. More - detailed information is in the - [CouchDB documentation](https://docs.couchdb.org/en/stable/best-practices/reverse-proxies.html). - -Minimal configurations are as follows: - -#### Nginx - -```nginx -location / { - proxy_pass http://localhost:5984; - proxy_redirect off; - proxy_buffering off; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -} -``` - -#### Nginx and subdirectory - -```nginx -location /couchdb { - rewrite ^ $request_uri; - rewrite ^/couchdb/(.*) /$1 break; - proxy_pass http://localhost:5984$uri; - proxy_redirect off; - proxy_buffering off; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -} - -location /_session { - proxy_pass http://localhost:5984/_session; - proxy_redirect off; - proxy_buffering off; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -} -``` - -#### Caddy - -```caddyfile -domain.com { - reverse_proxy localhost:5984 -} -``` - -#### Caddy and subdirectory - -```caddyfile -domain.com { - reverse_proxy /couchdb/* localhost:5984 - reverse_proxy /_session/* localhost:5984/_session -} -``` - -#### Apache - -Sorry, Apache is not recommended for CouchDB. Omit the configuration from here. -Please refer to the -[Official documentation](https://docs.couchdb.org/en/stable/best-practices/reverse-proxies.html#reverse-proxying-with-apache-http-server). - -### Show all setting panes - -Full pane is not shown by default. To show all panes, please toggle all in -`🧙‍♂️ Wizard` -> `Enable extra and advanced features`. - -For your information, the all panes are as follows: -![All Panes](all_toggles.png) - -### How to resolve `Tweaks Mismatched of Changed` - -(Since v0.23.17) - -If you have changed some configurations or tweaks which should be unified -between the devices, you will be asked how to reflect (or not) other devices at -the next synchronisation. It also occurs on the device itself, where changes are -made, to prevent unexpected configuration changes from unwanted propagation.\ -(We may thank this behaviour if we have synchronised or backed up and restored -Self-hosted LiveSync. At least, for me so). - -Following dialogue will be shown: ![Dialogue](tweak_mismatch_dialogue.png) - -- If we want to propagate the setting of the device, we should choose - `Update with mine`. -- On other devices, we should choose `Use configured` to accept and use the - configured configuration. -- `Dismiss` can postpone a decision. However, we cannot synchronise until we - have decided. - -Rest assured that in most cases we can choose `Use configured`. (Unless you are -certain that you have not changed the configuration). - -If we see it for the first time, it reflects the settings of the device that has -been synchronised with the remote for the first time since the upgrade. -Probably, we can accept that. - - - -## Notable bugs and fixes - -### Binary files get bigger on iOS - -- Reported at: v0.20.x -- Fixed at: v0.21.2 (Fixed but not reviewed) -- Required action: larger files will not be fixed automatically, please perform - `Verify and repair all files`. If our local database and storage are not - matched, we will be asked to apply which one. - -### Some setting name has been changed - -- Fixed at: v0.22.6 - -| Previous name | New name | -| ---------------------------- | ---------------------------------------- | -| Open setup URI | Use the copied setup URI | -| Copy setup URI | Copy current settings as a new setup URI | -| Setup Wizard | Minimal Setup | -| Check database configuration | Check and Fix database configuration | - -## Questions and Answers - -### How should I share the settings between multiple devices? - -- Device setup: - - Using `Setup URI` is the most straightforward way. -- Setting changes during use: - - Use `Sync settings via Markdown files` on the `🔄️ Sync settings` pane. - -### What should I enter for the passphrase of Setup-URI? - -- Anything you like is OK. However, the recommendation is as follows: - - Include the vault (group) information. - - Include the date of operation. - - Anything random for your security. - - For example, `MyVault-20240901-r4nd0mStr1ng`. -- Why? - - The Setup-URI is encoded; that means it cannot indicate the actual settings. Hence, if you use the same passphrase for multiple vaults, you may accidentally mix up vaults. - -### Why the settings of Self-hosted LiveSync itself is disabled in default? - -Basically, if we configure all `additionalSuffixOfDatabaseName` the same, we can synchronise this file between multiple devices. -(`additionalSuffixOfDatabaseName` should be unique in each device, not in the synchronised vaults). -However, if we synchronise the settings of Self-hosted LiveSync itself, we may encounter some unexpected behaviours. -For example, if a setting that 'let Self-hosted LiveSync setting be excluded' is synced, it is very unlikely that things will recover automatically after this, and there is little chance we will even notice this. Even if we change our minds and change the settings back on other devices. It could get even worse if incompatible changes are automatically reflected; everything will break. - -### The plug-in says `something went wrong`. - -There are many cases where this is really unclear. One possibility is that the chunk fetch did not go well. - -1. Restarting Obsidian sometimes helps (fetch-order problem). -2. If actually there are no chunks, please perform `Recreate missing chunks for all files` on the `🧰 Hatch` pane at the other devices. And synchronise again. (also restart Obsidian may effect). -3. If the problem persists, please perform `Verify and repair all files` on the `🧰 Hatch` pane. If our local database and storage are not matched, we will be asked to apply which one. - -### A large number of files were deleted, and were synchronised! - -1. Backup everything important. - - Your local vault. - - Your CouchDB database (this can be done by replicating to another database). -2. Prepare the empty vault -3. Place `redflag.md` at the top of the vault. -4. Apply the settings **BUT DO NOT PROCEED TO RESTORE YET**. - - You can use `Setup URI`, QR Code, or manually apply the settings. -5. Set `Maximum file modification time for reflected file events` in `Remediation` on the `🩹 Patches` pane. - - If you know when the files were deleted, set the time a bit before that. - - If not, bisecting may help us. -6. Delete `redflag.md`. -7. Perform `Reset Synchronisation on This Device` on the `🎛️ Maintenance` pane. - -This mode is very fragile. Please be careful. - -### Why `Use an old adapter for compatibility` is somehow enabled in my vault? - -Because you are a compassionate and experienced user. Before v0.17.16, we used -an old adapter for the local database. At that time, current default adapter has -not been stable. The new adapter has better performance and has a new feature -like purging. Therefore, we should use new adapters and current default is so. - -However, when switching from an old adapter to a new adapter, some converting or -local database rebuilding is required, and it takes some time. It was a long -time ago now, but we once inconvenienced everyone in a hurry when we changed the -format of our database. For these reasons, this toggle is automatically on if we -have upgraded from vault which using an old adapter. - -When you overwrite server data with this device's files or reset synchronisation on this device again, you will be asked to -switch this. - -Therefore, experienced users (especially those stable enough not to have to -overwrite server data) may have this toggle enabled in their Vault. Please -disable it when you have enough time. - -### ZIP (or any extensions) files were not synchronised. Why? - -It depends on Obsidian detects. May toggling `Detect all extensions` of -`File and links` (setting of Obsidian) will help us. - -### I hope to report the issue, but you said you needs `Report`. How to make it? - -We can copy the report to the clipboard, by performing -`Generate full report for opening the issue with debug info` command! - -### Where can I check the log? - -We can launch the log pane by `Show log` on the command palette. And if you have -troubled something, please enable the `Verbose Log` on the `General Setting` -pane. -`Generate full report for opening the issue with debug info` command also contains -the recent 1000 log lines, which is very helpful for debugging. Full-report is -already set to the verbose level, so it contains all the logs without enabling the -`Verbose Log` toggle. - -Let me note that please be sure to remove any sensitive information before sharing the report. - -However, the logs would not be kept so long and cleared when restarted. If you -want to check the logs, please enable `Write logs into the file` temporarily. - -![ScreenShot](../images/write_logs_into_the_file.png) +Start with the symptom which is visible now. Do not reset a database, change transport, or enable P2P merely to see whether the problem disappears. > [!IMPORTANT] -> -> - Writing logs into the file will impact the performance. -> - Please make sure that you have erased all your confidential information -> before reporting issue. +> If Obsidian will not start, do not give up. Close it, create `redflag.md` at the Vault root with the operating system's file manager, then follow [Recovery and flag files](recovery.md). This is the supported route for intervening before ordinary LiveSync start-up work. -### Why are the logs volatile and ephemeral? +Before changing settings: -To avoid unexpected exposure to our confidential things. +1. Back up the affected Vaults and, where possible, the remote database or bucket. +2. Stop editing on other devices. +3. Confirm that every participating device uses the intended plug-in version. +4. Identify whether the active main remote is CouchDB, Object Storage, or P2P. +5. Open `Show log` and note the first error, rather than only the final summary. -### Some network logs are not written into the file. +For a report, run `Generate full report for opening the issue with debug info`, remove credentials and private server details, and include the steps which caused the symptom. -Especially the CORS error will be reported as a general error to the plug-in for -security reasons. So we cannot detect and log it. We are only able to -investigate them by [Checking the network log](#checking-the-network-log). +## CouchDB does not connect -### If a file were deleted or trimmed, the capacity of the database should be reduced, right? +Check the connection in this order: -No, even though if files were deleted, chunks were not deleted. Self-hosted -LiveSync splits the files into multiple chunks and transfers only newly created. -This behaviour enables us to less traffic. And, the chunks will be shared -between the files to reduce the total usage of the database. +1. Confirm that the URL is complete and points to the intended server. +2. On mobile, use HTTPS with a certificate trusted by the operating system. Plain HTTP and self-signed certificates are not supported. +3. Confirm the username, password, database name, and any custom headers. +4. Confirm that the server responds outside the plug-in and that the database exists on additional devices. +5. Use the setup dialogue's connection test. +6. If basic access works, run **Check server requirements**. Its initial check is read-only. Each offered server change requires separate confirmation. -And one more thing, we can handle the conflicts on any device even though it has -happened on other devices. This means that conflicts will happen in the past, -after the time we have synchronised. Hence we cannot collect and delete the -unused chunks even though if we are not currently referenced. +Configure CouchDB CORS first. Reverse-proxy examples belong in [Set up your own CouchDB server](setup_own_server.md), alongside the rest of the server configuration. -To shrink the database size, `Overwrite Server Data with This Device's Files` is the only reliable and effective way. -But do not worry, if we have synchronised well. We have the actual and real -files. Only it takes a bit of time and traffic. +`Use Internal API` is a compatibility workaround for a trusted server. It sends the configured credentials through Obsidian's internal request API. Enable it only after checking the destination, and do not treat a fallback through that API as proof that the server or proxy is correctly configured. -### How to launch the DevTools +A Cloudflare `524` response means that Cloudflare timed out while waiting for the origin. The response may also lack the CORS headers which would have been present on an ordinary CouchDB response. Correct the long-running server or proxy request first. The advanced CouchDB option `Use timeouts instead of heartbeats` may help only when the underlying operation is otherwise healthy. -#### On Desktop Devices +For JWT-specific setup and key-format errors, see [JWT Authentication on CouchDB](tips/jwt-on-couchdb.md). -We can launch the DevTools by pressing `ctrl`+`shift`+`i` (`Command`+`shift`+`i` on Mac). +## CouchDB was working but synchronisation stopped -#### On Android +Do not switch to P2P or reset the database as the first response. Check: -Please refer to [Remote debug Android devices](https://developer.chrome.com/docs/devtools/remote-debugging/). -Once the DevTools have been launched, everything operates the same as on a PC. +1. the active remote profile and connection state; +2. the plug-in version on every device; +3. the CouchDB response and server logs; +4. pending LiveSync progress indicators; +5. `Check server requirements`; and +6. the LiveSync log and full report. -#### On iOS, iPadOS devices +If the remote is healthy but one device's local database is not, use [Reset Synchronisation on This Device](recovery.md#reset-synchronisation-on-this-device) only after backing up unsynchronised local files. -If we have a Mac, we can inspect from Safari on the Mac. Please refer to [Inspecting iOS and iPadOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios). +## Files are missing or excluded -### How can I use the DevTools? +Check Obsidian's `Detect all file extensions`, LiveSync selectors, ignore files, file-size limits, modification-time limits, and Hidden File Sync rules. A filtered file is different from a file which reached the database but could not be reconstructed from its chunks. -#### Checking the network log +If the log reports missing chunks or a size mismatch: + +1. stop editing the affected file and keep a separate copy of any readable content; +2. restart Obsidian once to rule out an interrupted fetch; +3. synchronise a device or restore a backup which still has the correct content; +4. on that healthy device, run `Recreate chunks for current Vault files`, then synchronise; +5. follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file); run `Inspect conflicts and file/database differences` from `Hatch`, then use each revision's wrench menu to review and act on that exact branch; and +6. use `Discard this branch` only after confirming that the exact live branch is no longer wanted. Use the separate `Discard unreadable revision` recovery action only when an unreadable revision is the sole live leaf. + +The repair card uses compact diagnostic rows which remain readable in a narrow mobile settings pane. `🧩 Missing chunks: N` marks an unreadable revision. In the database row, `Δsize` means decoded size minus recorded size; `Δsize vs DB` means Vault size minus decoded database size; and `Δtime` means Vault modification time minus database modification time. These are diagnostic values, not a rule for deciding which revision is correct. `✅ Vault matches winner · ⚠️ Conflicts: N` means that the current Vault bytes agree with the database winner while other live branches still need a decision. Every mutating action rechecks that its selected revision is still live. Applying a logical deletion to an existing Vault file requires confirmation; a logical-deletion winner with no Vault file already agrees and is omitted. + +`Retry reading revision` does not change the revision tree. `Discard this branch` creates a logical deletion on one exact live revision while another live branch remains and leaves the current Vault file unchanged. If the discarded revision was recorded as the Vault's exact source, that stale device-local provenance is removed. `Discard unreadable revision` provides the corresponding explicit escape hatch for a sole unreadable live leaf. Neither action purges history or reconstructs missing content. An unavailable non-live ancestor cannot be deleted through this workflow; it disables conservative three-way merge but does not prevent explicit selection between readable live revisions. + +`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 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. + +Current releases automatically align compatible settings which control how new chunks are created, by default and where possible. This applies to the chunk hash algorithm, chunk size, and splitter version. Existing content remains readable across these choices, although using different choices can reduce chunk reuse and increase storage or transfer work. An explicit opt-out retains the manual review. A mismatch involving encryption, path obfuscation, file-name case handling, or any combination which includes one of those settings always remains a manual decision. + +The available actions depend on when the mismatch is found: + +- While checking a remote profile, `Use configured settings` accepts the shared values already stored in that remote. `Dismiss` leaves this device's settings unchanged. +- For a mismatch found before synchronisation, `Apply settings to this device` accepts the remote values. Choose `Update remote database settings` only when this device's values are intended to become the shared values. +- When the change requires local or remote reconstruction, the action itself states that Fetch or Rebuild will follow. Make sure that the intended authoritative copy is available before choosing it. +- `Dismiss` postpones a mismatch found before synchronisation. Synchronisation remains paused until the mismatch is resolved. + +![Configuration mismatch dialogue](tweak_mismatch_dialogue.png) + +Historic defect notices and renamed controls are retained in the [0.25 release history](releases/0.25.md) and [legacy release history](releases/legacy.md), rather than in the current troubleshooting path. + +## Setup and settings questions + +### Share a configuration with another device + +Generate an encrypted Setup URI from a working device. This preserves the intended remote profiles and selections while allowing the additional device to keep its own device-specific name. Store the URI and its passphrase separately. + +For deliberate setting changes during normal use, use `Sync Settings via Markdown` under `Sync settings`. + +### Choose a Setup URI passphrase + +Use a strong passphrase which is distinct from the Vault encryption passphrase. Record enough context outside the encrypted URI to identify the intended Vault and date, but do not rely on a reused human-readable pattern alone. + +### Why synchronising LiveSync's own settings is disabled by default + +An automatically propagated transport, database, or exclusion setting can disable the mechanism needed to reverse it. LiveSync therefore keeps its own settings out of Customisation Sync by default. Enable that advanced behaviour only with an independent recovery path and device-specific database suffixes. + +### The plug-in reports that something went wrong + +Use the first specific error in `Show log` to choose the relevant section. When it names chunks or a size mismatch, follow [Files are missing or excluded](#files-are-missing-or-excluded). Do not rebuild solely from the generic final message. + +### A large deletion propagated + +Stop every device, preserve the available copies, and follow [Recovery and flag files](recovery.md). If the deletion time is known, `Maximum file modification time for reflected file events` under `Remediation` can limit which remote events are applied while recovering into a separate, backed-up Vault. Treat that as a forensic recovery constraint, not as an ordinary synchronisation setting. + +### An old database adapter is still selected + +Very old Vaults may retain the compatibility adapter until a deliberate local database migration or reset. Do not toggle it merely to troubleshoot an unrelated current failure. The history and migration notes are in the [legacy release history](releases/legacy.md). + +### ZIP or another extension is not synchronised + +Enable Obsidian's `Detect all file extensions`, then check LiveSync selectors, ignore rules, and size limits as described in [Files are missing or excluded](#files-are-missing-or-excluded). + +## Collect a report + +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. + +Use `Show log` for live inspection. Logs are intentionally kept in memory for a limited time to reduce accidental disclosure. Enable `Write logs into the file` only while reproducing a problem, then disable it and remove the file after review because persistent logging affects performance and may contain private data. + +![Write logs into the file](../images/write_logs_into_the_file.png) + +Browser security errors, particularly CORS failures, may reach the plug-in only as a general network error. Use the network inspector when the ordinary log cannot show the rejected response. + +## The database remains large after files are deleted + +LiveSync stores file metadata, chunks, revision history, conflicts, deletions, and tombstones. Deleting or shortening a file therefore does not immediately remove every object which once represented it. + +Garbage Collection V3 can remove unreferenced chunks from a healthy CouchDB setup, but it is appropriate only when the Vault and local database are healthy and all relevant devices have synchronised. Current files and live conflict branches protect their required chunks; an ordinary superseded revision does not. Tombstones and retained metadata are not free, so Garbage Collection does not guarantee a minimal database. Review the [Garbage Collection V3 specification](specs_garbage_collection.md) before using it. + +`Overwrite Server Data with This Device's Files` is a separate rebuild operation and is the more certain way to reconstruct a central remote from a chosen authoritative Vault. It is also destructive and may discard changes which exist only on another device. Review [Recovery and flag files](recovery.md#garbage-collection-is-not-rebuild) before choosing between them. + +## Inspect a network failure + +### Desktop + +Open Developer Tools with `Ctrl`+`Shift`+`I`, or `Command`+`Option`+`I` on macOS. + +### Android + +Follow Chrome's [Remote debug Android devices](https://developer.chrome.com/docs/devtools/remote-debugging/) guide. + +### iOS and iPadOS + +Use Safari on a Mac and follow Apple's [Inspecting iOS and iPadOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios) guide. + +### Network evidence 1. Open the network pane. -2. Find the requests marked in red.\ +2. Reproduce the failure and select the request marked in red. ![Errored](../images/devtools1.png) -3. Capture the `Headers`, `Payload`, and, `Response`. **Please be sure to keep - important information confidential**. If the `Response` contains secrets, you - can omitted that. Note: Headers contains a some credentials. **The path of - the request URL, Remote Address, authority, and authorization must be - concealed.**\ +3. Record the status, timing, and a sanitised version of the headers, payload, and response. +4. Remove the request path, remote address, authority, authorisation, cookies, credentials, and response secrets before sharing. + ![Concealed sample](../images/devtools2.png) -## Troubleshooting +## P2P does not connect or transfer changes - +Use [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md). Check signalling discovery separately from the WebRTC data path, and confirm which devices announce and follow changes. P2P is not a repair step for another transport. -### While using Cloudflare Tunnels, often Obsidian API fallback and `524` error occurs. +## Obsidian or LiveSync remains suspended -A `524` error occurs when the request to the server is not completed within a -`specified time`. This is a timeout error from Cloudflare. From the reported -issue, it seems to be 100 seconds. (#627). +Follow [Recovery and flag files](recovery.md). A `redflag.md` emergency stop remains active until it is removed outside Obsidian. Fetch and rebuild flags have different, potentially destructive meanings; do not create them merely to clear a warning. -Therefore, this error returns from Cloudflare, not from the server. Hence, the -result contains no CORS field. It means that this response makes the Obsidian -API fallback. +## Further technical context -However, even if the Obsidian API fallback occurs, the request is still not -completed within the `specified time`, 100 seconds. - -To solve this issue, we need to configure the timeout settings. - -Please enable the toggle in `💪 Power users` -> `CouchDB Connection Tweak` -> -`Use timeouts instead of heartbeats`. - -### On the mobile device, cannot synchronise on the local network! - -Obsidian mobile is not able to connect to the non-secure end-point, such as -starting with `http://`. Make sure your URI of CouchDB. Also not able to use a -self-signed certificate. - -### I think that something bad happening on the vault... - -Place the [flag file](#flag-files) on top of the vault, and restart Obsidian. The most simple -way is to create a new note and rename it to `redflag`. Of course, we can put it -without Obsidian. - -For example, if there is `redflag.md`, Self-hosted LiveSync suspends all database and storage -processes. - -### Scram State and Flag Files (SCRAM Warning Loop) - -The plug-in uses a **Scram state** (emergency suspension of all synchronisation processes) to prevent database corruption when severe errors or conflicts are detected. This state is often triggered or persisted by **flag files** placed at the root of the vault. - -If you encounter a warning saying **"Scram detected, all sync operations are suspended per SCRAM"** or get caught in an infinite loop where the warning persists even after clicking "Resume", it is likely due to a flag file in your vault. - -#### Flag Files - -A flag file is a simple Markdown file located at the root of your vault. Its very existence is significant; it may be left blank or contain any text. These files are used so they can be easily placed or deleted from outside Obsidian (e.g., when Obsidian fails to launch). - -| Filename | Human-Friendly Name | Description | -| ------------- | ------------------- | --------------------------------------------------------------------------------------- | -| `redflag.md` | - | Suspends all processes (activates Scram). | -| `redflag2.md` | `flag_rebuild.md` | Suspends all processes, and overwrites server data with this device's files. | -| `redflag3.md` | `flag_fetch.md` | Suspends all processes, discards the local database, and resets synchronisation on this device. | - -When resetting synchronisation on this device or overwriting server data, restarting Obsidian is performed once for safety reasons. At that time, Self-hosted LiveSync uses these files to determine whether the process should be carried out. (This mechanism is especially useful on mobile devices to force cancellation if the database rebuilding fails). These files are not subject to synchronisation. - -#### How to Resolve the Scram Loop - -If you cannot disable Scram, please follow these steps: -1. Close Obsidian completely. -2. Open your system's file manager and check the root directory of your vault. -3. Locate and delete any flag files (such as `redflag.md`, `redflag2.md`, or `redflag3.md`). -4. Launch Obsidian. -5. Go to the settings dialogue -> **Hatch** -> **Scram Switches**, and manually toggle **Suspend file watching** and **Suspend database reflecting** to `false` (disabled) if they have not been reset automatically. - -> [!TIP] -> This is the reason why flag files are standard `.md` files: it allows you to manage them externally. On mobile devices, you can use system file manager applications (such as the native **Files** app on iOS/iPadOS or **Files by Google** on Android) to find and delete these files to resolve a lock, or conversely, create/place a new `redflag.md` file (or directory) at the root of your vault to force-suspend synchronisation and stop Obsidian's boot-up sequence if you need to fix a database issue. - -### JWT Authentication Errors - -#### DataError when configuring JWT authentication - -If you encounter a `DataError:` with no additional information in the logs when configuring JWT authentication, this usually indicates a private key formatting issue. - -Self-hosted LiveSync requires the private key (for ES256/ES512 algorithms) to be in the **PKCS#8 PEM** format. Standard SEC1 EC private keys (which begin with `-----BEGIN EC PRIVATE KEY-----`) will trigger this error. - -To resolve this, convert your private key to PKCS#8 format using the following `openssl` command: -```bash -openssl pkcs8 -topk8 -inform PEM -nocrypt -in private.key -out pkcs8.key -``` -Then paste the contents of `pkcs8.key` (which begins with `-----BEGIN PRIVATE KEY-----`) into the JWT Key field. - -### Old tips - -- Rarely, a file in the database could be corrupted. The plug-in will not write - to local storage when a file looks corrupted. If a local version of the file - is on your device, the corruption could be fixed by editing the local file and - synchronising it. But if the file does not exist on any of your devices, then - it can not be rescued. In this case, you can delete these items from the - settings dialogue. -- To stop the boot-up sequence (eg. for fixing problems on databases), you can - put a `redflag.md` file (or directory) at the root of your vault. Tip for iOS: - a redflag directory can be created at the root of the vault using the File - application. -- Also, with `redflag2.md` placed, we can automatically overwrite server data with this device's files during the boot-up sequence. With `redflag3.md`, we - can discard only the local database and reset synchronisation on this device. -- Q: The database is growing, how can I shrink it down? A: each of the docs is - saved with their past 100 revisions for detecting and resolving conflicts. - Picturing that one device has been offline for a while, and comes online - again. The device has to compare its notes with the remotely saved ones. If - there exists a historic revision in which the note used to be identical, it - could be updated safely (like git fast-forward). Even if that is not in - revision histories, we only have to check the differences after the revision - that both devices commonly have. This is like git's conflict-resolving method. - So, We have to make the database again like an enlarged git repo if you want - to solve the root of the problem. -- And more technical Information is in the [Technical Information](tech_info.md) -- If you want to synchronise files without obsidian, you can use - [filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync). -- WebClipper is also available on Chrome Web - Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) - -Repo is here: -[obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip). -(Docs are a work in progress.) +See [Technical Information](tech_info.md) for database and synchronisation internals. Current behaviour belongs in this guide; instructions for older defects remain in the release histories. diff --git a/docs/tweak_mismatch_dialogue.png b/docs/tweak_mismatch_dialogue.png index 073a999b..d25566ca 100644 Binary files a/docs/tweak_mismatch_dialogue.png and b/docs/tweak_mismatch_dialogue.png differ diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 23b8fe9b..e2e16e8e 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -73,8 +73,12 @@ const moduleAliasPlugin = { const removePragmaCommentsPlugin = { name: "remove-pragma-comments", setup(build) { + const sourceRoot = `${path.resolve("src")}${path.sep}`; // Filter target extensions (e.g., JavaScript and TypeScript) build.onLoad({ filter: /\.[jt]s?$/ }, async (args) => { + // Dependencies are already compiled and may contain comment-like text inside strings. + // Only maintained plug-in source needs its local suppression directives removed. + if (!args.path.startsWith(sourceRoot)) return; const source = await fs.promises.readFile(args.path, "utf8"); // Regex targeting both single-line and multi-line comments diff --git a/eslint.community.config.mjs b/eslint.community.config.mjs new file mode 100644 index 00000000..4944a5f8 --- /dev/null +++ b/eslint.community.config.mjs @@ -0,0 +1,79 @@ +import obsidianmd from "eslint-plugin-obsidianmd"; +import globals from "globals"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig( + globalIgnores([ + "node_modules", + "**/dist", + "build", + "coverage", + "main.js", + "main_org.js", + "pouchdb-browser.js", + "package.json", + "package-lock.json", + "versions.json", + // Svelte is covered by the project lint and svelte-check; the directory report currently analyses TypeScript. + "**/*.svelte", + "**/svelte.config.js", + "**/*.unit.spec.ts", + "**/test/**", + "src/apps/_test/**", + "src/apps/cli/testdeno/**", + ]), + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + MANIFEST_VERSION: "readonly", + PACKAGE_VERSION: "readonly", + UPDATE_INFO: "readonly", + hostPlatform: "readonly", + }, + parserOptions: { + project: [ + "./tsconfig.json", + "./src/apps/browser/tsconfig.json", + "./src/apps/cli/tsconfig.json", + "./src/apps/webapp/tsconfig.json", + "./src/apps/webpeer/tsconfig.app.json", + "./src/apps/webpeer/tsconfig.node.json", + ], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + ...obsidianmd.configs.recommended, + { + rules: { + // The directory review reports console usage as guidance rather than a release blocker. + "obsidianmd/rule-custom-message": "off", + "no-console": "warn", + "obsidianmd/no-unsupported-api": "error", + // Keep legacy type-safety debt visible while reserving errors for directory-review blockers. + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/no-base-to-string": "warn", + "@typescript-eslint/no-redundant-type-constituents": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "warn", + }, + }, + { + files: ["src/apps/**/*.{ts,js,mjs}"], + rules: { + // These applications are inspected by the directory review but accept external command and file data. + "@typescript-eslint/restrict-template-expressions": "warn", + }, + }, + { + files: ["src/apps/browser/**/*.ts", "src/apps/webapp/**/*.ts"], + rules: { + "obsidianmd/prefer-active-doc": "off", + }, + } +); diff --git a/eslint.config.common.mjs b/eslint.config.common.mjs index e9e36021..bdecfb05 100644 --- a/eslint.config.common.mjs +++ b/eslint.config.common.mjs @@ -91,8 +91,8 @@ export const baseRules = { */ export const obsidianRules = { // -- Obsidian rules - // obsidianmd/no-unsupported-api: usually this project checks for API support at runtime, so this rule is not critical but can be helpful to catch potential issues. - "obsidianmd/no-unsupported-api": warnWhileDev, + // Runtime fallbacks must use requireApiVersion guards recognised by the official rule. + "obsidianmd/no-unsupported-api": "error", // -- Plugin specific overrides "obsidianmd/rule-custom-message": "off", @@ -112,7 +112,6 @@ export const ImportAliasRules = (base) => ({ aliasForSubpaths: true, alias: { "@": `${base}/src`, - "@lib": `${base}/src/lib/src`, }, }, ], diff --git a/eslint.config.mjs b/eslint.config.mjs index 69384d2e..b436c9e3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,10 +7,19 @@ import svelteParser from "svelte-eslint-parser"; import importAlias from "@dword-design/eslint-plugin-import-alias"; import { baseRules, ImportAliasRules, obsidianRules } from "./eslint.config.common.mjs"; const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled. +const lintProjects = [ + "./tsconfig.json", + "./src/apps/browser/tsconfig.json", + "./src/apps/cli/tsconfig.json", + "./src/apps/webapp/tsconfig.json", + "./src/apps/webpeer/tsconfig.app.json", + "./src/apps/webpeer/tsconfig.node.json", +]; export default defineConfig([ globalIgnores([ // Build outputs and legacy files "**/build", + "**/dist/**", "coverage", "**/main.js", "main_org.js", @@ -22,21 +31,9 @@ export default defineConfig([ // Files from linked dependencies (those files should not exist for most people). "modules/octagonal-wheels/dist", - // Sub-projects (Exclude from root linting as they have different environments) - "src/apps", + // Sub-project tooling with its own environment "utils", - // Specific exclusions from common library (src/lib) - "src/lib/coverage", - "src/lib/browsertest", - "src/lib/test", - "src/lib/_tools", - "src/lib/src/patches/pouchdb-utils", - "src/lib/src/cli", - "src/lib/src/services/implements/browser/**", - "src/lib/src/services/implements/headless/**", - "src/lib/src/API", - // Config files and build scripts "**/jest.config.js", "**/rollup.config.js", @@ -49,6 +46,9 @@ export default defineConfig([ "vitest.*", // Testing files (Simplified patterns) "test/**", + "**/test/**", + "src/apps/_test/**", + "src/apps/cli/testdeno/**", "**/*.test.ts", "**/*.unit.spec.ts", "**/test.ts", @@ -59,13 +59,12 @@ export default defineConfig([ importAlias.configs.recommended, { files: ["**/*.ts"], - // ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules). languageOptions: { globals: { ...globals.browser, PouchDB: "readonly" }, parser: tsParser, parserOptions: { - project: "./tsconfig.json", - rootDir: "./", + project: lintProjects, + tsconfigRootDir: import.meta.dirname, }, }, linterOptions: { @@ -85,6 +84,8 @@ export default defineConfig([ parser: svelteParser, parserOptions: { parser: tsParser, + project: lintProjects, + tsconfigRootDir: import.meta.dirname, extraFileExtensions: [".svelte"], }, }, @@ -98,4 +99,23 @@ export default defineConfig([ ...ImportAliasRules("."), }, }, + { + files: ["src/apps/**/*.ts"], + rules: { + // Platform adapters implement asynchronous contracts even when a local operation is synchronous. + "@typescript-eslint/require-await": "off", + // Keep existing application code visible without making gradual type tightening a release blocker. + "@typescript-eslint/no-base-to-string": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "warn", + "@typescript-eslint/restrict-template-expressions": "warn", + }, + }, + { + files: ["src/apps/browser/**/*.{ts,svelte}", "src/apps/webapp/**/*.ts"], + rules: { + // Browser applications use the DOM rather than Obsidian's DOM extensions. + "obsidianmd/prefer-create-el": "off", + "obsidianmd/prefer-active-doc": "off", + }, + }, ]); diff --git a/generate-types.mjs b/generate-types.mjs deleted file mode 100644 index 868e6fae..00000000 --- a/generate-types.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { execSync } from "node:child_process"; -import fs from "node:fs"; -try { - fs.rmSync("./_types", { recursive: true, force: true }); - fs.mkdirSync("./_types"); - console.log("[Postbuild] Generating type definitions for fallback..."); - execSync("npx tsc -p tsconfig.types.json", { stdio: "inherit" }); - - console.log("[Postbuild] Type definitions generated successfully."); -} catch (error) { - // Ignore compiler errors from tsc so that pre-existing type errors in the submodule - // do not block the build from succeeding. - console.warn("[Postbuild] Type definitions generated with some compilation warnings."); - // process.exit(-1); -} - -try { - console.log("[Postbuild] Type definitions generated successfully. Adding ignore comments..."); - execSync("deno run -A ./utilsdeno/types-add-ignore.ts", { stdio: "inherit" }); - console.log("[Postbuild] Ignore comments added successfully."); -} catch (error) { - console.warn("[Postbuild] Failed to add ignore comments to type definitions."); - process.exit(-1); -} diff --git a/images/couchdb-manual/guide-couchdb-manual-connection-details.png b/images/couchdb-manual/guide-couchdb-manual-connection-details.png new file mode 100644 index 00000000..27a3e54e Binary files /dev/null and b/images/couchdb-manual/guide-couchdb-manual-connection-details.png differ diff --git a/images/couchdb-manual/guide-couchdb-manual-connection-method.png b/images/couchdb-manual/guide-couchdb-manual-connection-method.png new file mode 100644 index 00000000..259ca162 Binary files /dev/null and b/images/couchdb-manual/guide-couchdb-manual-connection-method.png differ diff --git a/images/couchdb-manual/guide-couchdb-manual-encryption.png b/images/couchdb-manual/guide-couchdb-manual-encryption.png new file mode 100644 index 00000000..fa0aa27d Binary files /dev/null and b/images/couchdb-manual/guide-couchdb-manual-encryption.png differ diff --git a/images/couchdb-manual/guide-couchdb-manual-remote-selection.png b/images/couchdb-manual/guide-couchdb-manual-remote-selection.png new file mode 100644 index 00000000..21c02829 Binary files /dev/null and b/images/couchdb-manual/guide-couchdb-manual-remote-selection.png differ diff --git a/images/couchdb-manual/guide-couchdb-manual-server-requirements.png b/images/couchdb-manual/guide-couchdb-manual-server-requirements.png new file mode 100644 index 00000000..a08541aa Binary files /dev/null and b/images/couchdb-manual/guide-couchdb-manual-server-requirements.png differ diff --git a/images/hidden-file-sync/guide-hidden-file-advanced-features.png b/images/hidden-file-sync/guide-hidden-file-advanced-features.png new file mode 100644 index 00000000..f62fb555 Binary files /dev/null and b/images/hidden-file-sync/guide-hidden-file-advanced-features.png differ diff --git a/images/hidden-file-sync/guide-hidden-file-enable.png b/images/hidden-file-sync/guide-hidden-file-enable.png new file mode 100644 index 00000000..ab54fc48 Binary files /dev/null and b/images/hidden-file-sync/guide-hidden-file-enable.png differ diff --git a/images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png b/images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png new file mode 100644 index 00000000..b9ddff7d Binary files /dev/null and b/images/hidden-file-sync/guide-hidden-file-initial-scan-progress.png differ diff --git a/images/hidden-file-sync/guide-hidden-file-selector.png b/images/hidden-file-sync/guide-hidden-file-selector.png new file mode 100644 index 00000000..42c22eed Binary files /dev/null and b/images/hidden-file-sync/guide-hidden-file-selector.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-passphrase.png b/images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-passphrase.png new file mode 100644 index 00000000..e8745a39 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-passphrase.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-result.png b/images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-result.png new file mode 100644 index 00000000..998dcb95 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-copy-setup-uri-result.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-first-initialise.png b/images/object-storage-setup/guide-object-storage-setup-first-initialise.png new file mode 100644 index 00000000..597d52e1 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-first-initialise.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-first-rebuild-confirmation.png b/images/object-storage-setup/guide-object-storage-setup-first-rebuild-confirmation.png new file mode 100644 index 00000000..152b034d Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-first-rebuild-confirmation.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-first-setup-uri.png b/images/object-storage-setup/guide-object-storage-setup-first-setup-uri.png new file mode 100644 index 00000000..52242817 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-first-setup-uri.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-first-to-second.png b/images/object-storage-setup/guide-object-storage-setup-first-to-second.png new file mode 100644 index 00000000..a2af3b5b Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-first-to-second.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-local-file-policy.png b/images/object-storage-setup/guide-object-storage-setup-local-file-policy.png new file mode 100644 index 00000000..423a92fb Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-local-file-policy.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-missing-remote-configuration.png b/images/object-storage-setup/guide-object-storage-setup-missing-remote-configuration.png new file mode 100644 index 00000000..79f205fc Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-missing-remote-configuration.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-retrieval-method.png b/images/object-storage-setup/guide-object-storage-setup-retrieval-method.png new file mode 100644 index 00000000..7a48bf73 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-retrieval-method.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-second-fetch.png b/images/object-storage-setup/guide-object-storage-setup-second-fetch.png new file mode 100644 index 00000000..84c7d411 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-second-fetch.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-second-setup-uri.png b/images/object-storage-setup/guide-object-storage-setup-second-setup-uri.png new file mode 100644 index 00000000..7b5b0e37 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-second-setup-uri.png differ diff --git a/images/object-storage-setup/guide-object-storage-setup-second-to-first.png b/images/object-storage-setup/guide-object-storage-setup-second-to-first.png new file mode 100644 index 00000000..412cd359 Binary files /dev/null and b/images/object-storage-setup/guide-object-storage-setup-second-to-first.png differ diff --git a/images/p2p-setup/guide-p2p-setup-connection-request-1.png b/images/p2p-setup/guide-p2p-setup-connection-request-1.png new file mode 100644 index 00000000..26c638f7 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-connection-request-1.png differ diff --git a/images/p2p-setup/guide-p2p-setup-connection-request-2.png b/images/p2p-setup/guide-p2p-setup-connection-request-2.png new file mode 100644 index 00000000..a5da0e9f Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-connection-request-2.png differ diff --git a/images/p2p-setup/guide-p2p-setup-copy-setup-uri-passphrase.png b/images/p2p-setup/guide-p2p-setup-copy-setup-uri-passphrase.png new file mode 100644 index 00000000..815068d4 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-copy-setup-uri-passphrase.png differ diff --git a/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png b/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png new file mode 100644 index 00000000..57acd4e3 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png differ diff --git a/images/p2p-setup/guide-p2p-setup-first-device-connected.png b/images/p2p-setup/guide-p2p-setup-first-device-connected.png new file mode 100644 index 00000000..72dea764 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-first-device-connected.png differ diff --git a/images/p2p-setup/guide-p2p-setup-first-initialise.png b/images/p2p-setup/guide-p2p-setup-first-initialise.png new file mode 100644 index 00000000..9cf9a206 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-first-initialise.png differ diff --git a/images/p2p-setup/guide-p2p-setup-first-rebuild-confirmation.png b/images/p2p-setup/guide-p2p-setup-first-rebuild-confirmation.png new file mode 100644 index 00000000..9940c589 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-first-rebuild-confirmation.png differ diff --git a/images/p2p-setup/guide-p2p-setup-first-setup-uri.png b/images/p2p-setup/guide-p2p-setup-first-setup-uri.png new file mode 100644 index 00000000..c57cddf9 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-first-setup-uri.png differ diff --git a/images/p2p-setup/guide-p2p-setup-first-to-second.png b/images/p2p-setup/guide-p2p-setup-first-to-second.png new file mode 100644 index 00000000..ba2909c1 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-first-to-second.png differ diff --git a/images/p2p-setup/guide-p2p-setup-local-file-policy.png b/images/p2p-setup/guide-p2p-setup-local-file-policy.png new file mode 100644 index 00000000..423a92fb Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-local-file-policy.png differ diff --git a/images/p2p-setup/guide-p2p-setup-peer-actions-menu.png b/images/p2p-setup/guide-p2p-setup-peer-actions-menu.png new file mode 100644 index 00000000..ffc3392f Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-peer-actions-menu.png differ diff --git a/images/p2p-setup/guide-p2p-setup-retrieval-method.png b/images/p2p-setup/guide-p2p-setup-retrieval-method.png new file mode 100644 index 00000000..b3b8f520 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-retrieval-method.png differ diff --git a/images/p2p-setup/guide-p2p-setup-second-fetch.png b/images/p2p-setup/guide-p2p-setup-second-fetch.png new file mode 100644 index 00000000..2c65ca95 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-second-fetch.png differ diff --git a/images/p2p-setup/guide-p2p-setup-second-setup-uri.png b/images/p2p-setup/guide-p2p-setup-second-setup-uri.png new file mode 100644 index 00000000..c4c37951 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-second-setup-uri.png differ diff --git a/images/p2p-setup/guide-p2p-setup-second-to-first.png b/images/p2p-setup/guide-p2p-setup-second-to-first.png new file mode 100644 index 00000000..3baab2b7 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-second-to-first.png differ diff --git a/images/p2p-setup/guide-p2p-setup-select-first-device.png b/images/p2p-setup/guide-p2p-setup-select-first-device.png new file mode 100644 index 00000000..c7f39d42 Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-select-first-device.png differ diff --git a/images/p2p-setup/p2p-status-pane-mobile.png b/images/p2p-setup/p2p-status-pane-mobile.png new file mode 100644 index 00000000..961b8485 Binary files /dev/null and b/images/p2p-setup/p2p-status-pane-mobile.png differ diff --git a/images/p2p-setup/p2p-status-pane.png b/images/p2p-setup/p2p-status-pane.png new file mode 100644 index 00000000..b0cfac88 Binary files /dev/null and b/images/p2p-setup/p2p-status-pane.png differ diff --git a/images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png b/images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png new file mode 100644 index 00000000..815068d4 Binary files /dev/null and b/images/quick-setup/guide-quick-setup-copy-setup-uri-passphrase.png differ diff --git a/images/quick-setup/guide-quick-setup-copy-setup-uri-result.png b/images/quick-setup/guide-quick-setup-copy-setup-uri-result.png new file mode 100644 index 00000000..01773d1e Binary files /dev/null and b/images/quick-setup/guide-quick-setup-copy-setup-uri-result.png differ diff --git a/images/quick-setup/guide-quick-setup-first-initialise.png b/images/quick-setup/guide-quick-setup-first-initialise.png new file mode 100644 index 00000000..597d52e1 Binary files /dev/null and b/images/quick-setup/guide-quick-setup-first-initialise.png differ diff --git a/images/quick-setup/guide-quick-setup-first-rebuild-confirmation.png b/images/quick-setup/guide-quick-setup-first-rebuild-confirmation.png new file mode 100644 index 00000000..152b034d Binary files /dev/null and b/images/quick-setup/guide-quick-setup-first-rebuild-confirmation.png differ diff --git a/images/quick-setup/guide-quick-setup-first-setup-uri.png b/images/quick-setup/guide-quick-setup-first-setup-uri.png new file mode 100644 index 00000000..2942512a Binary files /dev/null and b/images/quick-setup/guide-quick-setup-first-setup-uri.png differ diff --git a/images/quick-setup/guide-quick-setup-local-file-policy.png b/images/quick-setup/guide-quick-setup-local-file-policy.png new file mode 100644 index 00000000..423a92fb Binary files /dev/null and b/images/quick-setup/guide-quick-setup-local-file-policy.png differ diff --git a/images/quick-setup/guide-quick-setup-missing-remote-configuration.png b/images/quick-setup/guide-quick-setup-missing-remote-configuration.png new file mode 100644 index 00000000..79f205fc Binary files /dev/null and b/images/quick-setup/guide-quick-setup-missing-remote-configuration.png differ diff --git a/images/quick-setup/guide-quick-setup-retrieval-method.png b/images/quick-setup/guide-quick-setup-retrieval-method.png new file mode 100644 index 00000000..7a48bf73 Binary files /dev/null and b/images/quick-setup/guide-quick-setup-retrieval-method.png differ diff --git a/images/quick-setup/guide-quick-setup-second-fetch.png b/images/quick-setup/guide-quick-setup-second-fetch.png new file mode 100644 index 00000000..84c7d411 Binary files /dev/null and b/images/quick-setup/guide-quick-setup-second-fetch.png differ diff --git a/images/quick-setup/guide-quick-setup-synchronised-note.png b/images/quick-setup/guide-quick-setup-synchronised-note.png new file mode 100644 index 00000000..8e6a3433 Binary files /dev/null and b/images/quick-setup/guide-quick-setup-synchronised-note.png differ diff --git a/manifest.json b/manifest.json index 7325bc46..cf0ec3d1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.25.83", + "version": "1.0.0", "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", diff --git a/package-lock.json b/package-lock.json index 56640f4f..98b450ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.25.83", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.25.83", + "version": "1.0.0", "license": "MIT", "workspaces": [ "src/apps/cli", @@ -22,7 +22,8 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@trystero-p2p/nostr": "^0.24.0", + "@vrtmrz/livesync-commonlib": "0.1.0", + "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", @@ -38,7 +39,6 @@ "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@eslint/js": "^9.39.3", - "@playwright/test": "^1.58.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tsconfig/svelte": "^5.0.8", "@types/deno": "^2.5.0", @@ -55,16 +55,14 @@ "@types/pouchdb-replication": "^6.4.7", "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", - "@vitest/browser": "^4.1.8", - "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.1.0", + "@vrtmrz/obsidian-test-session": "0.2.6", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", "esbuild-svelte": "^0.9.4", "eslint": "^9.39.3", - "eslint-plugin-obsidianmd": "^0.3.0", + "eslint-plugin-obsidianmd": "^0.4.1", "eslint-plugin-svelte": "^3.19.0", "events": "^3.3.0", "globals": "^14.0.0", @@ -1260,13 +1258,6 @@ "node": ">=18" } }, - "node_modules/@blazediff/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", - "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", - "dev": true, - "license": "MIT" - }, "node_modules/@codemirror/state": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", @@ -1812,6 +1803,36 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.7.2.tgz", + "integrity": "sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "ignore": "^7.0.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1864,9 +1885,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1945,9 +1966,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2179,128 +2200,11 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2311,7 +2215,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2322,7 +2226,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2343,14 +2247,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2710,29 +2614,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@playwright/test": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", - "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.61.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, "node_modules/@promptbook/utils": { "version": "0.69.5", "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", @@ -3877,7 +3758,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -3920,22 +3801,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@trystero-p2p/core": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@trystero-p2p/core/-/core-0.24.0.tgz", - "integrity": "sha512-W5ATiflgzZLE21fN2VA3YsK2yBJEzCvhmJ/9q2Vm3QT/gcdqDpcBxsO0DYCy/wE1PBEwoB+A75eBNtGIGAPdxw==", - "license": "MIT" - }, - "node_modules/@trystero-p2p/nostr": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@trystero-p2p/nostr/-/nostr-0.24.0.tgz", - "integrity": "sha512-LmsJSicsFU/rhmOWYaP/OxFl3rwGieX+q0eh0pAWUQM7IXbMu6tLC5+aAimtHitikPv9r6sck6EUTWMin8dBAw==", - "license": "MIT", - "dependencies": { - "@noble/secp256k1": "^3.1.0", - "@trystero-p2p/core": "0.24.0" - } - }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", @@ -4413,7 +4278,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/which": { @@ -4753,54 +4618,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitest/browser": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.8.tgz", - "integrity": "sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pngjs": "^7.0.0", - "sirv": "^3.0.2", - "tinyrainbow": "^3.1.0", - "ws": "^8.19.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.1.8" - } - }, - "node_modules/@vitest/browser-playwright": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.8.tgz", - "integrity": "sha512-SR7FqgegaexEg73xvf3ArtygXegagMdXnL0EZMpxrWvvhQxvicD/E8p0ib0J91riPRtQUViyh67Xjw3NqvyhVg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@vitest/browser": "4.1.8", - "@vitest/mocker": "4.1.8", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "playwright": "*", - "vitest": "4.1.8" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": false - } - } - }, "node_modules/@vitest/coverage-v8": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", @@ -4946,10 +4763,85 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vrtmrz/obsidian-test-session": { + "node_modules/@vrtmrz/livesync-commonlib": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.1.0.tgz", - "integrity": "sha512-asBOIRTc3xK5GF5ds5mkxN6vsO4RE8o7puvVjoJiGYSlxoFa9jzr8FwAO13CyoOriHF05pOZfTB+eQmL1aNb/A==", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.1.0.tgz", + "integrity": "sha512-rdzEubzLStioanE67pps2XSGZ2UGyMIyeEoKsdPztQLLW8wM05PWApOPbJujIydHcDr2hFanbDFe89Lk7Mn4XQ==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-s3": "^3.808.0", + "@smithy/fetch-http-handler": "^5.3.10", + "@smithy/md5-js": "^4.2.9", + "@smithy/middleware-apply-body-checksum": "^4.3.9", + "@smithy/types": "^4.14.3", + "@smithy/util-retry": "^4.4.5", + "@trystero-p2p/nostr": "0.25.3", + "diff-match-patch": "^1.0.5", + "events": "^3.3.0", + "fflate": "^0.8.2", + "idb": "^8.0.3", + "markdown-it": "^14.2.0", + "minimatch": "^10.2.5", + "octagonal-wheels": "^0.1.51", + "pouchdb-adapter-http": "^9.0.0", + "pouchdb-adapter-idb": "^9.0.0", + "pouchdb-adapter-indexeddb": "^9.0.0", + "pouchdb-adapter-memory": "^9.0.0", + "pouchdb-core": "^9.0.0", + "pouchdb-errors": "^9.0.0", + "pouchdb-find": "^9.0.0", + "pouchdb-mapreduce": "^9.0.0", + "pouchdb-merge": "^9.0.0", + "pouchdb-replication": "^9.0.0", + "pouchdb-utils": "^9.0.0", + "qrcode-generator": "^1.4.4", + "transform-pouch": "^2.0.0", + "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "svelte": "5.56.3" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/@vrtmrz/livesync-commonlib/node_modules/@trystero-p2p/core": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@trystero-p2p/core/-/core-0.25.3.tgz", + "integrity": "sha512-lQKNq/ha+vF6kQZrpaXJGzlzxLF/Fhizoy5dwJUYQlkqRrbPup/Jov2EpPX/CPr2X+f79HxVzonkeni3MxmCuQ==", + "license": "MIT" + }, + "node_modules/@vrtmrz/livesync-commonlib/node_modules/@trystero-p2p/nostr": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@trystero-p2p/nostr/-/nostr-0.25.3.tgz", + "integrity": "sha512-nZV9Fl/GXuhIkJSQ+wIkdMoRLO1oSTUL/+vZorukaojeAdOpT/61e8b9Vzbt8L1ktMTaGPEJHw2BG/B5BCwBuw==", + "license": "MIT", + "dependencies": { + "@noble/secp256k1": "^3.1.0", + "@trystero-p2p/core": "0.25.3" + } + }, + "node_modules/@vrtmrz/obsidian-plugin-kit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.2.tgz", + "integrity": "sha512-LNNV8QCaN6FvRA+of96GiqI8atAbnMKQvLbqpi3nsepEe6tN8SmXO3P7pqBl3axNXEHhQfU5ggj5HAq9GJPgwQ==", + "license": "MIT", + "dependencies": { + "@vrtmrz/ui-interactions": "0.1.1" + }, + "peerDependencies": { + "obsidian": ">=1.8.7" + } + }, + "node_modules/@vrtmrz/obsidian-test-session": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-test-session/-/obsidian-test-session-0.2.6.tgz", + "integrity": "sha512-7xDTmTW3igBxwQGgbbpoRZU0JvOBpGgdRT4qF2WVrKz03OtKMJdkfJILY04/HZ+n4tvn9Ze8phJBr0dx3wewcQ==", "dev": true, "license": "MIT", "engines": { @@ -4960,6 +4852,12 @@ "playwright": ">=1.50.0" } }, + "node_modules/@vrtmrz/ui-interactions": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@vrtmrz/ui-interactions/-/ui-interactions-0.1.1.tgz", + "integrity": "sha512-XpO5fQzC7jyOW3xVF7YtFHI7X1BPQ7hJW56uw8atx8mDR7C9ejG2YSn0XcZ1H5FOCPgJbmkh/FLQRKLqSK8jkw==", + "license": "MIT" + }, "node_modules/@wdio/config": { "version": "9.27.0", "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.27.0.tgz", @@ -4987,9 +4885,9 @@ "license": "MIT" }, "node_modules/@wdio/config/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -5227,7 +5125,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "bin": { @@ -5355,9 +5253,9 @@ "license": "MIT" }, "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -5520,7 +5418,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -5782,7 +5680,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -5825,9 +5723,9 @@ "license": "MIT" }, "node_modules/babel-plugin-module-resolver/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -6077,15 +5975,15 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -6236,16 +6134,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", @@ -6404,7 +6292,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6892,7 +6780,7 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/diff-match-patch": { @@ -7774,9 +7662,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7871,9 +7759,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -7937,9 +7825,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -7986,12 +7874,13 @@ } }, "node_modules/eslint-plugin-obsidianmd": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.3.0.tgz", - "integrity": "sha512-QvGDI6B2nxJBrsZKGTg31da2A/fEJNlnwN+fRZkaoPIu1QL3fYXUdpP7ThyMdr/0iTYQxifb9lt2X9cpydQx1w==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.4.1.tgz", + "integrity": "sha512-Nv3593kVsFOS8kir/HWaJ5CR3xcyiPWEPyXst5Pib8mxRYYuLYPpJS2ALMoZXHulHyiGwoYW8AaxvLRc4rhrRg==", "dev": true, "license": "MIT", "dependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", "@eslint/config-helpers": "^0.4.2", "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", @@ -8000,7 +7889,7 @@ "@types/node": "20.12.12", "@typescript-eslint/types": "^8.33.1", "@typescript-eslint/utils": "^8.33.1", - "eslint": ">=9.0.0", + "eslint": ">=9.19.0", "eslint-plugin-depend": "1.3.1", "eslint-plugin-import": "^2.31.0", "eslint-plugin-json-schema-validator": "5.1.0", @@ -8021,7 +7910,7 @@ "peerDependencies": { "@eslint/js": "^9.30.1", "@eslint/json": "0.14.0", - "eslint": ">=9.0.0", + "eslint": ">=9.19.0", "obsidian": "1.8.7", "typescript-eslint": "^8.35.1" } @@ -8113,9 +8002,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8315,9 +8204,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -8355,7 +8244,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/espree": { @@ -8420,7 +8309,7 @@ "version": "2.2.11", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -8491,7 +8380,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -8596,9 +8484,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { @@ -8895,7 +8783,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true, "license": "MIT" }, "node_modules/functions-have-names": { @@ -8994,16 +8881,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/get-port": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", @@ -9093,24 +8970,6 @@ "node": ">= 14" } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -9182,9 +9041,9 @@ "license": "MIT" }, "node_modules/globby/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9884,7 +9743,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -10081,23 +9940,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -10196,9 +10038,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -11003,9 +10845,9 @@ } }, "node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "funding": [ { "type": "github", @@ -11051,7 +10893,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/locate-path": { @@ -11145,16 +10987,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/ltgt": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", @@ -11165,7 +10997,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -11257,7 +11089,6 @@ "resolved": "https://registry.npmjs.org/memdown/-/memdown-1.4.1.tgz", "integrity": "sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w==", "deprecated": "Superseded by memory-level (https://github.com/Level/community#faq)", - "dev": true, "license": "MIT", "dependencies": { "abstract-leveldown": "~2.7.1", @@ -11273,7 +11104,6 @@ "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.7.2.tgz", "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", "deprecated": "Superseded by abstract-level (https://github.com/Level/community#faq)", - "dev": true, "license": "MIT", "dependencies": { "xtend": "~4.0.0" @@ -11387,16 +11217,6 @@ "node": ">=4" } }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11679,6 +11499,7 @@ "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.1.tgz", "integrity": "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==", "license": "MIT", + "peer": true, "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" @@ -11971,23 +11792,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -12213,16 +12017,6 @@ "node": ">=18" } }, - "node_modules/pngjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", - "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.19.0" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12362,7 +12156,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/pouchdb-adapter-idb/-/pouchdb-adapter-idb-9.0.0.tgz", "integrity": "sha512-2oLlgwMyOQwdKuzrEmOv8T7jFVgX7JgT4Cr81zX3eiiRClp7xXGgjv41ZRdVCAbM530sIN8BudafaQRVFKRVmA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "pouchdb-adapter-utils": "9.0.0", @@ -12377,7 +12170,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/pouchdb-adapter-indexeddb/-/pouchdb-adapter-indexeddb-9.0.0.tgz", "integrity": "sha512-/mcCbnVR0VKwtVZWKf8lVSdADLD0yApjFudu4d+0jeLWAeBSGZBRKYlogz2PGs4uTA7GVc2TXjVCNGUdkCM9ZQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "pouchdb-adapter-utils": "9.0.0", @@ -12426,7 +12218,6 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/pouchdb-adapter-memory/-/pouchdb-adapter-memory-9.0.0.tgz", "integrity": "sha512-XbCwJ5f5U9dGdkiDikzYjTebdPHuA6Ghylx1Pq0lDe4y6l8R9xhjDSUy56pJ8G2F4Z+8QdB5FBY9EQoFlFSXWQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "memdown": "1.4.1", @@ -12866,9 +12657,9 @@ "license": "MIT" }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -13208,7 +12999,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/safe-push-apply": { @@ -13513,21 +13303,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13643,13 +13418,6 @@ "node": ">= 10.x" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -14040,7 +13808,7 @@ "version": "5.56.3", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -14295,21 +14063,6 @@ "node": ">=10" } }, - "node_modules/test-exclude": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", - "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^13.0.6", - "minimatch": "^10.2.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -14441,16 +14194,6 @@ "url": "https://github.com/sponsors/ota-meshi" } }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -15083,12 +14826,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/vite": { @@ -15170,67 +14917,6 @@ } } }, - "node_modules/vite-plugin-istanbul": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vite-plugin-istanbul/-/vite-plugin-istanbul-9.0.1.tgz", - "integrity": "sha512-zgcdcqa4r3urX+xqhMQG2uLR2s6IdklQW+acBEtLw6fvUWhuvXswYqxjHAZZ5HCfHEgRs7RH7qIZCklTLRsKLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/generator": "^7.29.7", - "@istanbuljs/load-nyc-config": "^1.1.0", - "@types/babel__generator": "7.27.0", - "espree": "^11.2.0", - "istanbul-lib-instrument": "^6.0.3", - "picocolors": "^1.1.1", - "source-map": "^0.7.6", - "test-exclude": "^8.0.0" - }, - "peerDependencies": { - "vite": ">=7" - } - }, - "node_modules/vite-plugin-istanbul/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/vite-plugin-istanbul/node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/vite-plugin-istanbul/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -15285,6 +14971,7 @@ "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.8", "@vitest/mocker": "4.1.8", @@ -16164,7 +15851,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/zip-stream": { @@ -16226,7 +15913,7 @@ }, "src/apps/cli": { "name": "self-hosted-livesync-cli", - "version": "0.25.83-cli", + "version": "1.0.0-cli", "dependencies": { "chokidar": "^4.0.0", "minimatch": "^10.2.5", @@ -16244,7 +15931,6 @@ "werift": "^0.23.0" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^7.1.2", "typescript": "5.9.3", "vite": "^8.0.16", "vitest": "^4.1.8" @@ -16252,22 +15938,19 @@ }, "src/apps/webapp": { "name": "livesync-webapp", - "version": "0.25.83-webapp", + "version": "1.0.0-webapp", "dependencies": { "octagonal-wheels": "^0.1.51" }, "devDependencies": { - "@playwright/test": "^1.58.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", - "playwright": "^1.58.2", "svelte": "5.56.3", "typescript": "5.9.3", - "vite": "^8.0.16", - "vite-plugin-istanbul": "^9.0.1" + "vite": "^8.0.16" } }, "src/apps/webpeer": { - "version": "0.25.83-webpeer", + "version": "1.0.0-webpeer", "dependencies": { "octagonal-wheels": "^0.1.51" }, diff --git a/package.json b/package.json index f738ae5c..3d66a4b5 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,79 @@ { "name": "obsidian-livesync", - "version": "0.25.83", + "version": "1.0.0", "description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "main": "main.js", "type": "module", "scripts": { - "bakei18n": "npm run i18n:yaml2json && npm run i18n:bakejson", - "i18n:bakejson": "npx tsx ./src/lib/_tools/bakei18n.ts", - "i18n:yaml2json": "npx tsx ./src/lib/_tools/yaml2json.ts", - "i18n:json2yaml": "npx tsx ./src/lib/_tools/json2yaml.ts", - "prettyjson": "prettier --config ./.prettierrc.mjs ./src/lib/src/common/messagesJson/*.json --write --log-level error", - "postbakei18n": "prettier --config ./.prettierrc.mjs ./src/lib/src/common/messages/*.ts --write --log-level error", - "posti18n:yaml2json": "npm run prettyjson", - "predev": "npm run bakei18n", "dev": "node --env-file=.env esbuild.config.mjs", - "prebuild": "npm run bakei18n", "build": "node esbuild.config.mjs production", - "build:lib:types": "node generate-types.mjs", "buildVite": "npx dotenv-cli -e .env -- vite build --mode production", "buildViteOriginal": "npx dotenv-cli -e .env -- vite build --mode original", "buildDev": "node esbuild.config.mjs dev", - "lint": "eslint --cache --concurrency auto src", - "svelte-check": "svelte-check --tsconfig ./tsconfig.json", + "lint": "eslint --cache --cache-strategy content --concurrency off src", + "lint:community": "eslint --config eslint.community.config.mjs --concurrency off src", + "lint:community:tools": "eslint --config eslint.community.config.mjs --concurrency off --max-warnings 0 _tools", + "svelte-check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", "tsc-check": "tsc --noEmit", + "tsc-check:apps": "tsc --noEmit -p src/apps/browser/tsconfig.json && tsc --noEmit -p src/apps/cli/tsconfig.json && tsc --noEmit -p src/apps/webapp/tsconfig.json && tsc --noEmit -p src/apps/webpeer/tsconfig.app.json && tsc --noEmit -p src/apps/webpeer/tsconfig.node.json", "pretty:importpath": "cd utilsdeno && deno run -A ./normalise-imports.ts", "pretty:json": "prettier --config ./.prettierrc.mjs \"**/*.json\" --write --log-level error", "pretty": "npm run prettyNoWrite -- --write --log-level error", "prettyCheck": "npm run prettyNoWrite -- --check", "prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ", + "precheck:compatibility": "npm run build", "check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15", - "precheck": "npm run build:lib:types", - "check": "npm run tsc-check && npm run lint && npm run svelte-check && npm run check:compatibility", - "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", - "test": "vitest run", + "check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run lint:community:tools && npm run svelte-check && npm run check:compatibility", + "i18n:bake": "npm run i18n:yaml2json && npm run i18n:bakejson && npm run i18n:format", + "i18n:bakejson": "tsx _tools/bakei18n.ts", + "i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'", + "i18n:json2yaml": "tsx _tools/json2yaml.ts", + "i18n:yaml2json": "tsx _tools/yaml2json.ts", "test:unit": "vitest run --config vitest.config.unit.ts", + "inspect:troubleshooting": "tsx _tools/inspect-troubleshooting-docs.ts", + "test:setup-tools": "bash utils/flyio/setenv.test.sh && deno test -A --config=utils/flyio/deno.jsonc --frozen --lock=utils/flyio/deno.lock utils/livesync-commonlib-version.test.ts utils/couchdb/provision.test.ts utils/setup/generate_setup_uri.test.ts utils/flyio/generate_setupuri.test.ts", + "test:contract:contexts": "vitest run --config vitest.config.unit.ts src/modules/services/ObsidianServiceContext.unit.spec.ts src/apps/cli/services/NodeServiceContext.unit.spec.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", + "test:contract:context:webapp": "vitest run --config vitest.config.unit.ts src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts", + "test:contract:context:cli": "npm run build --workspace self-hosted-livesync-cli && deno task --cwd src/apps/cli/testdeno test:setup-put-cat", + "test:contract:context:obsidian": "npm run build && npm run test:e2e:obsidian:smoke", + "test:e2e:cli": "npm run test:e2e:ci --workspace self-hosted-livesync-cli", + "test:e2e:cli:p2p": "npm run test:e2e:p2p --workspace self-hosted-livesync-cli", + "test:e2e:cli:all": "npm run test:e2e:all --workspace self-hosted-livesync-cli", "test:integration": "npx dotenv-cli -e .env -e .test.env -- vitest run --config vitest.config.integration.ts", "test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage", - "test:install-playwright": "npx playwright install chromium", - "test:install-dependencies": "npm run test:install-playwright", "test:e2e:obsidian:install-appimage": "tsx test/e2e-obsidian/scripts/install-appimage.ts", + "test:e2e:obsidian:runner": "vitest run --config vitest.config.e2e-runner.ts", "test:e2e:obsidian:discover": "tsx test/e2e-obsidian/scripts/discover.ts", "test:e2e:obsidian:cli-help": "tsx test/e2e-obsidian/scripts/cli-help.ts", "test:e2e:obsidian:debug-ui": "tsx test/e2e-obsidian/scripts/debug-ui.ts", + "test:e2e:obsidian:focused": "tsx test/e2e-obsidian/scripts/run-focused.ts", "test:e2e:obsidian:smoke": "tsx test/e2e-obsidian/scripts/smoke.ts", + "test:e2e:obsidian:onboarding-invitation": "tsx test/e2e-obsidian/scripts/onboarding-invitation.ts", + "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: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", "test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts", "test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts", + "test:e2e:obsidian:couchdb-manual-setup-workflow": "tsx test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts", "test:e2e:obsidian:cli-to-obsidian-sync": "tsx test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts", "test:e2e:obsidian:minio-upload": "tsx test/e2e-obsidian/scripts/minio-upload.ts", + "test:e2e:obsidian: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", "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", + "test:e2e:obsidian:security-seed-reconnect": "tsx test/e2e-obsidian/scripts/security-seed-reconnect.ts", "test:e2e:obsidian:hidden-file-snippet-sync": "tsx test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts", "test:e2e:obsidian:customisation-sync": "tsx test/e2e-obsidian/scripts/customisation-sync.ts", "test:e2e:obsidian:setting-markdown-export": "tsx test/e2e-obsidian/scripts/setting-markdown-export.ts", + "test:e2e:obsidian:upgrade-from-stable": "tsx test/e2e-obsidian/scripts/upgrade-from-stable.ts", "test:e2e:obsidian:local-suite": "tsx test/e2e-obsidian/scripts/local-suite.ts", "test:e2e:obsidian:local-suite:services": "tsx test/e2e-obsidian/scripts/local-suite.ts --manage-services", - "test:coverage": "vitest run --coverage", + "test:docker-p2p:start": "docker compose -f test/fixtures/p2p-relay/compose.yml up -d", + "test:docker-p2p:stop": "docker compose -f test/fixtures/p2p-relay/compose.yml down --volumes", "test:docker-couchdb:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-start.sh", "test:docker-couchdb:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-init.sh", "test:docker-couchdb:start": "npm run test:docker-couchdb:up && sleep 5 && npm run test:docker-couchdb:init", @@ -65,18 +84,11 @@ "test:docker-s3:start": "npm run test:docker-s3:up && sleep 3 && npm run test:docker-s3:init", "test:docker-s3:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-stop.sh", "test:docker-s3:stop": "npm run test:docker-s3:down", - "test:docker-p2p:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-start.sh", - "test:docker-p2p:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-init.sh", - "test:docker-p2p:start": "npm run test:docker-p2p:up && sleep 3 && npm run test:docker-p2p:init", - "test:docker-p2p:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-stop.sh", - "test:docker-p2p:stop": "npm run test:docker-p2p:down", - "test:docker-all:up": "npm run test:docker-couchdb:up ; npm run test:docker-s3:up ; npm run test:docker-p2p:up", - "test:docker-all:init": "npm run test:docker-couchdb:init ; npm run test:docker-s3:init ; npm run test:docker-p2p:init", - "test:docker-all:down": "npm run test:docker-couchdb:down ; npm run test:docker-s3:down ; npm run test:docker-p2p:down", + "test:docker-all:up": "npm run test:docker-couchdb:up ; npm run test:docker-s3:up", + "test:docker-all:init": "npm run test:docker-couchdb:init ; npm run test:docker-s3:init", + "test:docker-all:down": "npm run test:docker-couchdb:down ; npm run test:docker-s3:down", "test:docker-all:start": "npm run test:docker-all:up && sleep 5 && npm run test:docker-all:init", "test:docker-all:stop": "npm run test:docker-all:down", - "test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop", - "test:p2p": "bash test/suitep2p/run-p2p-tests.sh", "update-workspaces": "node update-workspaces.mjs", "version": "node version-bump.mjs && node update-workspaces.mjs && npm run pretty:json && git add manifest.json versions.json src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json" }, @@ -85,8 +97,9 @@ "license": "MIT", "devDependencies": { "@dword-design/eslint-plugin-import-alias": "^8.1.8", + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", "@eslint/js": "^9.39.3", - "@playwright/test": "^1.58.2", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tsconfig/svelte": "^5.0.8", "@types/deno": "^2.5.0", @@ -103,16 +116,14 @@ "@types/pouchdb-replication": "^6.4.7", "@types/transform-pouch": "^1.0.6", "@typescript-eslint/parser": "8.56.1", - "@vitest/browser": "^4.1.8", - "@vitest/browser-playwright": "^4.1.8", "@vitest/coverage-v8": "^4.1.8", - "@vrtmrz/obsidian-test-session": "0.1.0", + "@vrtmrz/obsidian-test-session": "0.2.6", "dotenv-cli": "^11.0.0", "esbuild": "0.28.1", "esbuild-plugin-inline-worker": "^0.1.1", "esbuild-svelte": "^0.9.4", "eslint": "^9.39.3", - "eslint-plugin-obsidianmd": "^0.3.0", + "eslint-plugin-obsidianmd": "^0.4.1", "eslint-plugin-svelte": "^3.19.0", "events": "^3.3.0", "globals": "^14.0.0", @@ -144,9 +155,7 @@ "vite": "^8.0.16", "vitest": "^4.1.8", "webdriverio": "^9.27.0", - "yaml": "^2.8.2", - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1" + "yaml": "^2.8.2" }, "dependencies": { "@aws-sdk/client-s3": "^3.808.0", @@ -157,7 +166,8 @@ "@smithy/querystring-builder": "^4.2.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@trystero-p2p/nostr": "^0.24.0", + "@vrtmrz/livesync-commonlib": "0.1.0", + "@vrtmrz/obsidian-plugin-kit": "0.1.2", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", @@ -168,6 +178,14 @@ "qrcode-generator": "^1.4.4", "xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2" }, + "overrides": { + "pouchdb-core": { + "uuid": "11.1.1" + }, + "pouchdb-utils": { + "uuid": "11.1.1" + } + }, "workspaces": [ "src/apps/cli", "src/apps/webpeer", diff --git a/src/LiveSyncBaseCore.ts b/src/LiveSyncBaseCore.ts index ca4f6a2a..116d7b06 100644 --- a/src/LiveSyncBaseCore.ts +++ b/src/LiveSyncBaseCore.ts @@ -1,22 +1,22 @@ 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 "@lib/common/types"; -import { __$checkInstanceBinding } from "@lib/dev/checks"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { LiveSyncLocalDBEnv } from "@lib/pouchdb/LiveSyncLocalDB"; -import type { LiveSyncCouchDBReplicatorEnv } from "@lib/replication/couchdb/LiveSyncReplicator"; -import type { CheckPointInfo } from "@lib/replication/journal/JournalSyncTypes"; -import type { LiveSyncJournalReplicatorEnv } from "@lib/replication/journal/LiveSyncJournalReplicatorEnv"; -import type { LiveSyncReplicatorEnv } from "@lib/replication/LiveSyncAbstractReplicator"; -import { useTargetFilters } from "@lib/serviceFeatures/targetFilter"; -import { useRemoteConfigurationMigration } from "@lib/serviceFeatures/remoteConfig"; -import type { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; +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"; @@ -26,10 +26,10 @@ import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChec import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver"; import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks"; import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain"; -import type { ServiceModules } from "@lib/interfaces/ServiceModule"; +import type { ServiceModules } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu"; -import { usePrepareDatabaseForUse } from "@lib/serviceFeatures/prepareDatabaseForUse"; -import type { Constructor } from "@lib/common/utils.type"; +import { usePrepareDatabaseForUse } from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/prepareDatabaseForUse"; +import type { Constructor } from "@vrtmrz/livesync-commonlib/compat/common/utils.type"; export class LiveSyncBaseCore< T extends ServiceContext = ServiceContext, diff --git a/src/apps/_test/storageAdapterContract.ts b/src/apps/_test/storageAdapterContract.ts index 5693e198..0d325c7e 100644 --- a/src/apps/_test/storageAdapterContract.ts +++ b/src/apps/_test/storageAdapterContract.ts @@ -1,4 +1,4 @@ -import type { IStorageAdapter } from "@lib/serviceModules/adapters"; +import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; /** One platform-neutral storage adapter contract case. */ export interface StorageAdapterContractCase { diff --git a/src/apps/browser/BrowserConfirm.ts b/src/apps/browser/BrowserConfirm.ts new file mode 100644 index 00000000..3963cb2d --- /dev/null +++ b/src/apps/browser/BrowserConfirm.ts @@ -0,0 +1,136 @@ +import type { Confirm, ConfirmActionLayout } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; +import { createNativeElement } from "@/apps/browserDom"; + +import MessageBox from "./ui/MessageBox.svelte"; +import TextInputBox from "./ui/TextInputBox.svelte"; + +import { mount } from "svelte"; +import { promiseWithResolvers } from "octagonal-wheels/promises"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { _activeDocument, compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; + +function displayMessageBox( + message: string, + buttons: U, + title: string, + commit: (ret: U[number]) => T, + actionLayout: ConfirmActionLayout = "vertical" +): Promise { + const el = createNativeElement(_activeDocument, "div"); + const p = promiseWithResolvers(); + mount(MessageBox, { + target: el, + props: { + message, + buttons: buttons as string[], + title: title, + actionLayout, + commit: (action: U[number]) => { + const ret = commit(action); + p.resolve(ret); + }, + }, + }); + _activeDocument.body.appendChild(el); + void p.promise.finally(() => { + el.remove(); + }); + return p.promise; +} +function promptForInput( + title: string, + key: string, + placeholder: string, + isPassword?: boolean +): Promise { + const el = createNativeElement(_activeDocument, "div"); + const p = promiseWithResolvers(); + mount(TextInputBox, { + target: el, + props: { + title, + message: key, + placeholder, + isPassword, + commit: (text: string | false) => { + p.resolve(text); + }, + }, + }); + _activeDocument.body.appendChild(el); + void p.promise.finally(() => { + el.remove(); + }); + return p.promise; +} + +export class BrowserConfirm implements Confirm { + _context: T; + constructor(context: T) { + this._context = context; + } + askYesNo(message: string): Promise<"yes" | "no"> { + return displayMessageBox(message, ["Yes", "No"] as const, "Confirm", (action) => + action == "Yes" ? "yes" : "no" + ); + } + askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise { + return promptForInput(title, key, placeholder, isPassword); + } + askYesNoDialog( + message: string, + opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } + ): Promise<"yes" | "no"> { + return displayMessageBox(message, ["Yes", "No"] as const, opt.title ?? "Confirm", (action) => + action == "Yes" ? "yes" : "no" + ); + } + askSelectString(message: string, items: string[]): Promise { + return displayMessageBox(message, [...items] as const, "Confirm", (action) => action); + } + askSelectStringDialogue( + message: string, + buttons: T, + opt: { title?: string; defaultAction: T[number]; timeout?: number } + ): Promise { + return displayMessageBox(message, [...buttons] as const, opt.title ?? "Confirm", (action) => action); + } + askInPopup( + key: string, + dialogText: string, + anchorCallback: (anchor: HTMLAnchorElement) => void, + durationMs: number = 20000 + ): void { + const existing = _activeDocument.querySelector(`[data-livesync-popup="${CSS.escape(key)}"]`); + existing?.remove(); + + const notice = createNativeElement(_activeDocument, "div"); + notice.className = "livesync-browser-notice"; + notice.dataset.livesyncPopup = key; + const [beforeText, afterText] = dialogText.split("{HERE}", 2); + notice.append(beforeText); + const anchor = createNativeElement(_activeDocument, "a"); + anchor.href = "#"; + anchorCallback(anchor); + anchor.addEventListener("click", () => notice.remove()); + notice.append(anchor, afterText ?? ""); + _activeDocument.body.appendChild(notice); + compatGlobal.setTimeout(() => notice.remove(), durationMs); + } + confirmWithMessage( + title: string, + contentMd: string, + buttons: string[], + defaultAction: (typeof buttons)[number], + timeout?: number, + actionLayout?: ConfirmActionLayout + ): Promise<(typeof buttons)[number] | false> { + return displayMessageBox( + contentMd, + [...buttons] as const, + title ?? "Confirm", + (action) => action, + actionLayout + ); + } +} diff --git a/src/apps/browser/BrowserMenu.ts b/src/apps/browser/BrowserMenu.ts new file mode 100644 index 00000000..55af8921 --- /dev/null +++ b/src/apps/browser/BrowserMenu.ts @@ -0,0 +1,72 @@ +import { promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises"; +import { createNativeElement } from "@/apps/browserDom"; +import { mount } from "svelte"; +import MenuView from "./ui/MenuView.svelte"; +import { _activeDocument } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; + +export class MenuItem { + type = "item"; + title = ""; + handler?: () => void | Promise; + icon: string = ""; + setTitle(title: string) { + this.title = title; + return this; + } + onClick(callback: () => void | Promise) { + this.handler = callback; + return this; + } + setIcon(icon: string | null) { + this.icon = icon || ""; + return this; + } +} +export class MenuSeparator { + type = "separator"; +} +export class Menu { + type = "menu"; + items: (MenuItem | MenuSeparator)[] = []; + + constructor() {} + addItem(callback: (item: MenuItem) => void) { + const item = new MenuItem(); + callback(item); + this.items.push(item); + return this; + } + addSeparator() { + this.items.push(new MenuSeparator()); + return this; + } + waitingForClose?: PromiseWithResolvers; + showAtPosition(pos: { x: number; y: number }) { + const el = createNativeElement(_activeDocument, "div"); + if (this.waitingForClose) { + this.waitingForClose.resolve(); + } + this.waitingForClose = promiseWithResolvers(); + mount(MenuView, { + target: el, + props: { + items: this.items, + closeMenu: () => { + this.waitingForClose?.resolve(); + this.waitingForClose = undefined; + }, + x: pos.x, + y: pos.y, + }, + }); + _activeDocument.body.appendChild(el); + void this.waitingForClose.promise.finally(() => { + el.remove(); + }); + return this.waitingForClose.promise; + } + hide() { + this.waitingForClose?.resolve(); + this.waitingForClose = undefined; + } +} diff --git a/src/apps/browser/BrowserSvelteDialogManager.ts b/src/apps/browser/BrowserSvelteDialogManager.ts new file mode 100644 index 00000000..ebb10ed0 --- /dev/null +++ b/src/apps/browser/BrowserSvelteDialogManager.ts @@ -0,0 +1,80 @@ +import { + type ComponentHasResult, + SvelteDialogManagerBase, + SvelteDialogMixIn, +} 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"; +import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte"; + +export class ShimModal { + contentEl: HTMLElement; + titleEl: HTMLElement; + modalEl: HTMLElement; + isOpen: boolean = false; + baseEl: HTMLElement; + constructor() { + const baseEl = createNativeElement(_activeDocument, "popup"); + this.baseEl = baseEl; + this.contentEl = createNativeElement(_activeDocument, "div"); + this.contentEl.className = "modal-content"; + this.titleEl = createNativeElement(_activeDocument, "div"); + this.titleEl.className = "modal-title"; + this.modalEl = createNativeElement(_activeDocument, "div"); + this.modalEl.className = "modal"; + this.modalEl.hidden = true; + this.modalEl.appendChild(this.titleEl); + this.modalEl.appendChild(this.contentEl); + this.baseEl.appendChild(this.modalEl); + } + open() { + this.isOpen = true; + this.modalEl.hidden = false; + if (!this.baseEl.parentElement) { + _activeDocument.body.appendChild(this.baseEl); + } + this.onOpen(); + } + close() { + this.isOpen = false; + this.modalEl.hidden = true; + this.baseEl.remove(); + this.onClose(); + } + onOpen() {} + onClose() {} + setPlaceholder(p: string) {} + setTitle(t: string) { + this.titleEl.textContent = t; + } +} + +const BrowserSvelteDialogBase = SvelteDialogMixIn(ShimModal, DialogHost); + +export class LiveSyncBrowserDialog extends BrowserSvelteDialogBase< + T, + U, + C +> { + constructor( + context: C, + dependents: SvelteDialogManagerDependencies, + component: ComponentHasResult, + initialData?: U + ) { + super(); + this.initDialog(context, dependents, component, initialData); + } +} +export class BrowserSvelteDialogManager extends SvelteDialogManagerBase { + override async openSvelteDialog( + component: ComponentHasResult, + initialData?: TU + ): Promise { + const dialog = new LiveSyncBrowserDialog(this.context, this.dependents, component, initialData); + dialog.open(); + return await dialog.waitForClose(); + } +} diff --git a/src/apps/browser/LiveSyncBrowserUIService.ts b/src/apps/browser/LiveSyncBrowserUIService.ts new file mode 100644 index 00000000..79db6d23 --- /dev/null +++ b/src/apps/browser/LiveSyncBrowserUIService.ts @@ -0,0 +1,25 @@ +import type { BrowserServiceHostDependencies } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices"; +import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService"; +import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte"; +import { BrowserSvelteDialogManager } from "./BrowserSvelteDialogManager"; + +export class LiveSyncBrowserUIService extends UIService { + override get dialogToCopy() { + return DialogToCopy; + } + constructor(context: T, dependents: BrowserServiceHostDependencies) { + const browserConfirm = dependents.API.confirm; + const obsidianSvelteDialogManager = new BrowserSvelteDialogManager(context, { + appLifecycle: dependents.appLifecycle, + config: dependents.config, + replicator: dependents.replicator, + confirm: browserConfirm, + control: dependents.control, + }); + super(context, { + dialogManager: obsidianSvelteDialogManager, + APIService: dependents.API, + }); + } +} diff --git a/src/apps/browser/createLiveSyncBrowserServiceHub.ts b/src/apps/browser/createLiveSyncBrowserServiceHub.ts new file mode 100644 index 00000000..f7cd13b0 --- /dev/null +++ b/src/apps/browser/createLiveSyncBrowserServiceHub.ts @@ -0,0 +1,37 @@ +import { BrowserServiceHub, type BrowserServiceHost } from "@vrtmrz/livesync-commonlib/compat/services/BrowserServices"; +import type { KeyValueDatabaseFactory } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase"; +import { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService"; +import { BrowserConfirm } from "./BrowserConfirm"; +import { LiveSyncBrowserUIService } from "./LiveSyncBrowserUIService"; +import { setLang, translateLiveSyncMessage } from "@/common/translation"; + +export type LiveSyncBrowserServiceHubOptions = { + context?: T; + openKeyValueDatabase?: KeyValueDatabaseFactory; +}; + +function createLiveSyncBrowserHost(): BrowserServiceHost { + return { + createAPI(context) { + return new BrowserAPIService(context, { + confirm: new BrowserConfirm(context), + }); + }, + createUI(context, dependencies) { + return new LiveSyncBrowserUIService(context, dependencies); + }, + }; +} + +export function createLiveSyncBrowserServiceHub( + options: LiveSyncBrowserServiceHubOptions = {} +): BrowserServiceHub { + const context = options.context ?? (new ServiceContext({ translate: translateLiveSyncMessage }) as T); + return new BrowserServiceHub({ + ...options, + context, + onDisplayLanguageChanged: setLang, + host: createLiveSyncBrowserHost(), + }); +} diff --git a/src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts b/src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts new file mode 100644 index 00000000..1e0c3dbe --- /dev/null +++ b/src/apps/browser/createLiveSyncBrowserServiceHub.unit.spec.ts @@ -0,0 +1,28 @@ +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { describe, expect, it } from "vitest"; + +import { + observeServiceComposition, + observeServiceContext, + SERVICE_CONTEXT_MEMBERS, +} from "../../../test/contracts/serviceContext"; +import { createLiveSyncBrowserServiceHub } from "./createLiveSyncBrowserServiceHub"; + +describe("LiveSync browser service context contract", () => { + it("preserves one injected context and its API results throughout the Webapp composition", () => { + const context = createServiceContext({ + translate: (key) => `webapp:${key}`, + }); + const hub = createLiveSyncBrowserServiceHub({ context }); + + expect(observeServiceContext(context, "moduleLocalDatabase.logWaitingForReady")).toEqual({ + translation: "webapp:moduleLocalDatabase.logWaitingForReady", + receivedEvents: ["context-contract-event"], + }); + const composition = observeServiceComposition(hub, context); + expect(composition.hubUsesExpectedContext).toBe(true); + expect(SERVICE_CONTEXT_MEMBERS.filter((member) => !composition.servicesUsingExpectedContext[member])).toEqual( + [] + ); + }); +}); diff --git a/src/apps/browser/tsconfig.json b/src/apps/browser/tsconfig.json new file mode 100644 index 00000000..896bf6eb --- /dev/null +++ b/src/apps/browser/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "paths": { + "@/*": ["../../*"] + } + }, + "include": ["**/*.ts", "**/*.svelte"], + "exclude": ["**/*.unit.spec.ts"] +} diff --git a/src/apps/browser/ui/MenuItemView.svelte b/src/apps/browser/ui/MenuItemView.svelte new file mode 100644 index 00000000..1f7be09f --- /dev/null +++ b/src/apps/browser/ui/MenuItemView.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/apps/browser/ui/MenuSeparatorView.svelte b/src/apps/browser/ui/MenuSeparatorView.svelte new file mode 100644 index 00000000..a254a8d9 --- /dev/null +++ b/src/apps/browser/ui/MenuSeparatorView.svelte @@ -0,0 +1,10 @@ + + +
diff --git a/src/apps/browser/ui/MenuView.svelte b/src/apps/browser/ui/MenuView.svelte new file mode 100644 index 00000000..d2dc352f --- /dev/null +++ b/src/apps/browser/ui/MenuView.svelte @@ -0,0 +1,89 @@ + + + + + +
closeMenu()} onkeydown={handleKey} role="none">
+ + diff --git a/src/apps/browser/ui/MessageBox.svelte b/src/apps/browser/ui/MessageBox.svelte new file mode 100644 index 00000000..64fd066b --- /dev/null +++ b/src/apps/browser/ui/MessageBox.svelte @@ -0,0 +1,137 @@ + + + +
{title}
+
{@html renderedMessage}
+
+ {#each buttons as button} + + {/each} +
+
+
commit("")} onkeydown={handleEsc} role="none">
+ + diff --git a/src/apps/browser/ui/TextInputBox.svelte b/src/apps/browser/ui/TextInputBox.svelte new file mode 100644 index 00000000..8066da4b --- /dev/null +++ b/src/apps/browser/ui/TextInputBox.svelte @@ -0,0 +1,126 @@ + + + +
{title}
+
+
{message}
+
+ +
+
+ +
+ + +
+
+
+ + diff --git a/src/apps/browser/ui/renderMessageMarkdown.ts b/src/apps/browser/ui/renderMessageMarkdown.ts new file mode 100644 index 00000000..8e937561 --- /dev/null +++ b/src/apps/browser/ui/renderMessageMarkdown.ts @@ -0,0 +1,21 @@ +import MarkdownIt from "markdown-it"; + +const markdownRenderer = new MarkdownIt({ + html: false, + breaks: true, + linkify: true, +}); + +const defaultLinkOpenRenderer = + markdownRenderer.renderer.rules.link_open ?? + ((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options)); + +markdownRenderer.renderer.rules.link_open = (tokens, idx, options, env, self) => { + tokens[idx].attrSet("target", "_blank"); + tokens[idx].attrSet("rel", "noopener noreferrer"); + return defaultLinkOpenRenderer(tokens, idx, options, env, self); +}; + +export function renderMessageMarkdown(message: string): string { + return markdownRenderer.render(message); +} diff --git a/src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts b/src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts new file mode 100644 index 00000000..cb5a53a6 --- /dev/null +++ b/src/apps/browser/ui/renderMessageMarkdown.unit.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; + +import { renderMessageMarkdown } from "./renderMessageMarkdown"; + +describe("renderMessageMarkdown", () => { + it("renders basic markdown features used by browser dialogues", () => { + const html = renderMessageMarkdown("# Title\n\n| left | right |\n| --- | --- |\n| a | b |\n"); + + expect(html).toContain("

Title

"); + expect(html).toContain(""); + expect(html).toContain(""); + }); + + it("escapes inline HTML instead of rendering it", () => { + const html = renderMessageMarkdown("BeforeAfter"); + + expect(html).not.toContain(" - - diff --git a/src/apps/webapp/test/e2e.spec.ts b/src/apps/webapp/test/e2e.spec.ts deleted file mode 100644 index 72c8343d..00000000 --- a/src/apps/webapp/test/e2e.spec.ts +++ /dev/null @@ -1,294 +0,0 @@ -/** - * WebApp E2E tests – two-vault scenarios. - * - * Each vault (A and B) runs in its own browser context so that JavaScript - * global state (including Trystero's global signalling tables) is fully - * isolated. The two vaults communicate only through the shared remote - * CouchDB database. - * - * Vault storage is OPFS-backed – no file-picker interaction needed. - * - * Prerequisites: - * - A reachable CouchDB instance whose connection details are in .test.env - * (read automatically by playwright.config.ts). - * - * How to run: - * cd src/apps/webapp && npm run test:e2e - */ - -import { test, expect, type BrowserContext, type Page, type TestInfo } from "@playwright/test"; -import type { LiveSyncTestAPI } from "@/apps/webapp/test-entry"; -import { mkdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// --------------------------------------------------------------------------- -// Settings helpers -// --------------------------------------------------------------------------- - -function requireEnv(name: string): string { - const v = process.env[name]; - if (!v) throw new Error(`Missing required env variable: ${name}`); - return v; -} - -async function ensureCouchDbDatabase(uri: string, user: string, pass: string, dbName: string): Promise { - const base = uri.replace(/\/+$/, ""); - const dbUrl = `${base}/${encodeURIComponent(dbName)}`; - const auth = Buffer.from(`${user}:${pass}`, "utf-8").toString("base64"); - const response = await fetch(dbUrl, { - method: "PUT", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - - // 201: created, 202: accepted, 412: already exists - if (response.status === 201 || response.status === 202 || response.status === 412) { - return; - } - - const body = await response.text().catch(() => ""); - throw new Error(`Failed to ensure CouchDB database (${response.status}): ${body}`); -} - -function buildSettings(dbName: string): Record { - return { - // Remote database (shared between A and B – this is the replication target) - couchDB_URI: requireEnv("hostname").replace(/\/+$/, ""), - couchDB_USER: process.env["username"] ?? "", - couchDB_PASSWORD: process.env["password"] ?? "", - couchDB_DBNAME: dbName, - - // Core behaviour - isConfigured: true, - liveSync: false, - syncOnSave: false, - syncOnStart: false, - periodicReplication: false, - gcDelay: 0, - savingDelay: 0, - notifyThresholdOfRemoteStorageSize: 0, - - // Encryption off for test simplicity - encrypt: false, - - // Disable plugin/hidden-file sync (not needed in webapp) - usePluginSync: false, - autoSweepPlugins: false, - autoSweepPluginsPeriodic: false, - - //Auto accept perr - P2P_AutoAcceptingPeers: "~.*", - }; -} - -// --------------------------------------------------------------------------- -// Test-page helpers -// --------------------------------------------------------------------------- - -/** Navigate to the test entry page and wait for `window.livesyncTest`. */ -async function openTestPage(ctx: BrowserContext): Promise { - const page = await ctx.newPage(); - await page.goto("/test.html"); - await page.waitForFunction(() => !!(window as any).livesyncTest, { timeout: 20_000 }); - return page; -} - -/** Type-safe wrapper – calls `window.livesyncTest.(...args)` in the page. */ -async function call( - page: Page, - method: M, - ...args: Parameters -): Promise>> { - const invoke = () => - page.evaluate(([m, a]) => (window as any).livesyncTest[m](...a), [method, args] as [ - string, - unknown[], - ]) as Promise>>; - - try { - return await invoke(); - } catch (ex: any) { - const message = String(ex?.message ?? ex); - // Some startup flows may trigger one page reload; recover once. - if ( - message.includes("Execution context was destroyed") || - message.includes("Most likely the page has been closed") - ) { - await page.waitForFunction(() => !!(window as any).livesyncTest, { timeout: 20_000 }); - return await invoke(); - } - throw ex; - } -} - -async function dumpCoverage(page: Page | undefined, label: string, testInfo: TestInfo): Promise { - if (!process.env.PW_COVERAGE || !page || page.isClosed()) { - return; - } - const cov = await page - .evaluate(() => { - const data = (window as any).__coverage__; - if (!data) return null; - // Reset between tests to avoid runaway accumulation. - (window as any).__coverage__ = {}; - return data; - }) - .catch(() => null!); - if (!cov) return; - if (typeof cov === "object" && Object.keys(cov as Record).length === 0) { - return; - } - - const outDir = path.resolve(__dirname, "../.nyc_output"); - mkdirSync(outDir, { recursive: true }); - const name = `${testInfo.testId.replace(/[^a-zA-Z0-9_-]/g, "_")}-${label}.json`; - writeFileSync(path.join(outDir, name), JSON.stringify(cov), "utf-8"); -} - -// --------------------------------------------------------------------------- -// Two-vault E2E suite -// --------------------------------------------------------------------------- - -test.describe("WebApp two-vault E2E", () => { - let ctxA: BrowserContext; - let ctxB: BrowserContext; - let pageA: Page; - let pageB: Page; - - const DB_SUFFIX = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const dbName = `${requireEnv("dbname")}-${DB_SUFFIX}`; - const settings = buildSettings(dbName); - - test.beforeAll(async ({ browser }) => { - await ensureCouchDbDatabase( - String(settings.couchDB_URI ?? ""), - String(settings.couchDB_USER ?? ""), - String(settings.couchDB_PASSWORD ?? ""), - dbName - ); - - // Open Vault A and Vault B in completely separate browser contexts. - // Each context has its own JS runtime, IndexedDB and OPFS root, so - // Trystero global state and PouchDB instance names cannot collide. - ctxA = await browser.newContext(); - ctxB = await browser.newContext(); - - pageA = await openTestPage(ctxA); - pageB = await openTestPage(ctxB); - - await call(pageA, "init", "testvault_a", settings as any); - await call(pageB, "init", "testvault_b", settings as any); - }); - - test.afterAll(async () => { - await call(pageA, "shutdown").catch(() => {}); - await call(pageB, "shutdown").catch(() => {}); - await ctxA.close(); - await ctxB.close(); - }); - - test.afterEach(async ({}, testInfo) => { - await dumpCoverage(pageA, "vaultA", testInfo); - await dumpCoverage(pageB, "vaultB", testInfo); - }); - - // ----------------------------------------------------------------------- - // Case 1: Vault A writes a file and can read its metadata back from the - // local database (no replication yet). - // ----------------------------------------------------------------------- - test("Case 1: A writes a file and can get its info", async () => { - const FILE = "e2e/case1-a-only.md"; - const CONTENT = "hello from vault A"; - - const ok = await call(pageA, "putFile", FILE, CONTENT); - expect(ok).toBe(true); - - const info = await call(pageA, "getInfo", FILE); - expect(info).not.toBeNull(); - expect(info!.path).toBe(FILE); - expect(info!.revision).toBeTruthy(); - expect(info!.conflicts).toHaveLength(0); - }); - - // ----------------------------------------------------------------------- - // Case 2: Vault A writes a file, both vaults replicate, and Vault B ends - // up with the file in its local database. - // ----------------------------------------------------------------------- - test("Case 2: A writes a file, both replicate, B receives the file", async () => { - const FILE = "e2e/case2-sync.md"; - const CONTENT = "content from A – should appear in B"; - - await call(pageA, "putFile", FILE, CONTENT); - - // A pushes to remote, B pulls from remote. - await call(pageA, "replicate"); - await call(pageB, "replicate"); - - const infoB = await call(pageB, "getInfo", FILE); - expect(infoB).not.toBeNull(); - expect(infoB!.path).toBe(FILE); - }); - - // ----------------------------------------------------------------------- - // Case 3: Vault A deletes the file it synced in case 2. After both - // vaults replicate, Vault B no longer sees the file. - // ----------------------------------------------------------------------- - test("Case 3: A deletes the file, both replicate, B no longer sees it", async () => { - // This test depends on Case 2 having put e2e/case2-sync.md into both vaults. - const FILE = "e2e/case2-sync.md"; - - await call(pageA, "deleteFile", FILE); - - await call(pageA, "replicate"); - await call(pageB, "replicate"); - - const infoB = await call(pageB, "getInfo", FILE); - // The file should be gone (null means not found or deleted). - expect(infoB).toBeNull(); - }); - - // ----------------------------------------------------------------------- - // Case 4: A and B each independently edit the same file that was already - // synced. After both vaults replicate the editing cycle, both - // vaults report a conflict on that file. - // ----------------------------------------------------------------------- - test("Case 4: concurrent edits from A and B produce a conflict on both sides", async () => { - const FILE = "e2e/case4-conflict.md"; - - // 1) Write a baseline and synchronise so both vaults start from the - // same revision. - await call(pageA, "putFile", FILE, "base content"); - await call(pageA, "replicate"); - await call(pageB, "replicate"); - - // Confirm B has the base file with no conflicts yet. - const baseInfoB = await call(pageB, "getInfo", FILE); - expect(baseInfoB).not.toBeNull(); - expect(baseInfoB!.conflicts).toHaveLength(0); - - // 2) Both vaults write diverging content without syncing in between – - // this creates two competing revisions. - await call(pageA, "putFile", FILE, "content from A (conflict side)"); - await call(pageB, "putFile", FILE, "content from B (conflict side)"); - - // 3) Run replication on both sides. The order mirrors the pattern - // from the CLI two-vault tests (A → remote → B → remote → A). - await call(pageA, "replicate"); - await call(pageB, "replicate"); - await call(pageA, "replicate"); // re-check from A to pick up B's revision - - // 4) At least one side must report a conflict. - const hasConflictA = await call(pageA, "hasConflict", FILE); - const hasConflictB = await call(pageB, "hasConflict", FILE); - - expect( - hasConflictA || hasConflictB, - "Expected a conflict to appear on vault A or vault B after diverging edits" - ).toBe(true); - }); -}); diff --git a/src/apps/webapp/tsconfig.json b/src/apps/webapp/tsconfig.json index e2198719..ab83008a 100644 --- a/src/apps/webapp/tsconfig.json +++ b/src/apps/webapp/tsconfig.json @@ -23,8 +23,7 @@ /* Path mapping */ // "baseUrl": ".", "paths": { - "@/*": ["../../*"], - "@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"] + "@/*": ["../../*"] } }, "include": ["*.ts", "**/*.ts", "**/*.tsx", "**/*.svelte"], diff --git a/src/apps/webapp/vaultSelector.ts b/src/apps/webapp/vaultSelector.ts index 35fc4f4f..7c2d2321 100644 --- a/src/apps/webapp/vaultSelector.ts +++ b/src/apps/webapp/vaultSelector.ts @@ -1,4 +1,4 @@ -import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; +import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; const HANDLE_DB_NAME = "livesync-webapp-handles"; const HANDLE_STORE_NAME = "handles"; @@ -15,6 +15,18 @@ export type VaultHistoryItem = { type VaultHistoryValue = VaultHistoryItem; +function isVaultHistoryValue(value: unknown): value is VaultHistoryValue { + if (typeof value !== "object" || value === null) return false; + const candidate = value as Partial; + return ( + typeof candidate.id === "string" && + typeof candidate.name === "string" && + typeof candidate.handle === "object" && + candidate.handle !== null && + typeof candidate.lastUsedAt === "number" + ); +} + function makeVaultKey(id: string): string { return `${VAULT_KEY_PREFIX}${id}`; } @@ -87,7 +99,7 @@ export class VaultHistoryStore { async getLastUsedVaultId(): Promise { return this.withStore("readonly", async (store) => { - const value = await this.requestAsPromise(store.get(LAST_USED_KEY)); + const value: unknown = await this.requestAsPromise(store.get(LAST_USED_KEY)); return typeof value === "string" ? value : null; }); } @@ -95,20 +107,21 @@ export class VaultHistoryStore { async getVaultHistory(): Promise { return this.withStore("readonly", async (store) => { const keys = await this.requestAsPromise(store.getAllKeys()); - const values = (await this.requestAsPromise(store.getAll())) as unknown[]; + const values = await this.requestAsPromise(store.getAll() as IDBRequest); const items: VaultHistoryItem[] = []; for (let i = 0; i < keys.length; i++) { - const key = String(keys[i]); + const key = keys[i]; + if (typeof key !== "string") continue; const id = parseVaultId(key); - const value = values[i] as Partial | undefined; - if (!id || !value || !value.handle || !value.name) { + const value = values[i]; + if (!id || !isVaultHistoryValue(value)) { continue; } items.push({ id, - name: String(value.name), + name: value.name, handle: value.handle, - lastUsedAt: Number(value.lastUsedAt || 0), + lastUsedAt: value.lastUsedAt, }); } items.sort((a, b) => b.lastUsedAt - a.lastUsedAt); diff --git a/src/apps/webapp/vite.config.ts b/src/apps/webapp/vite.config.ts index a82a1484..8857acf9 100644 --- a/src/apps/webapp/vite.config.ts +++ b/src/apps/webapp/vite.config.ts @@ -1,47 +1,26 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; -import istanbul from "vite-plugin-istanbul"; -import path from "node:path"; -import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, "../../.."); -const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8")); -const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8")); -const enableCoverage = process.env.PW_COVERAGE === "1"; + +function readVersion(filePath: string): string | undefined { + const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8")); + if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) { + return undefined; + } + return typeof parsed.version === "string" ? parsed.version : undefined; +} + +const packageVersion = readVersion(path.resolve(repoRoot, "package.json")); +const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json")); // https://vite.dev/config/ export default defineConfig({ - plugins: [ - svelte(), - ...(enableCoverage - ? [ - istanbul({ - cwd: repoRoot, - include: ["src/**/*.ts", "src/**/*.svelte"], - exclude: [ - "node_modules", - "dist", - "test", - "coverage", - "src/apps/webapp/test/**", - "playwright.config.ts", - "vite.config.ts", - "**/*.spec.ts", - "**/*.test.ts", - ], - extension: [".js", ".ts", ".svelte"], - requireEnv: false, - cypress: false, - checkProd: false, - }), - ] - : []), - ], + plugins: [svelte()], resolve: { alias: { "@": path.resolve(__dirname, "../../"), - "@lib": path.resolve(__dirname, "../../lib/src"), - obsidian: path.resolve(__dirname, "../../../test/harness/obsidian-mock.ts"), + obsidian: path.resolve(__dirname, "./obsidianMock.ts"), }, }, base: "./", @@ -49,19 +28,16 @@ export default defineConfig({ outDir: "dist", emptyOutDir: true, rollupOptions: { - // test.html is used by the Playwright dev-server; include it here - // so the production build doesn't emit warnings about unused inputs. input: { index: path.resolve(__dirname, "index.html"), webapp: path.resolve(__dirname, "webapp.html"), - test: path.resolve(__dirname, "test.html"), }, external: ["crypto"], }, }, define: { - MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"), - PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"), + MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"), + PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"), global: "globalThis", hostPlatform: JSON.stringify(process.platform || "linux"), }, diff --git a/src/apps/webpeer/package.json b/src/apps/webpeer/package.json index 35544428..7474c1cd 100644 --- a/src/apps/webpeer/package.json +++ b/src/apps/webpeer/package.json @@ -1,7 +1,7 @@ { "name": "webpeer", "private": true, - "version": "0.25.83-webpeer", + "version": "1.0.0-webpeer", "type": "module", "scripts": { "dev": "vite", @@ -22,9 +22,5 @@ "svelte-check": "^4.6.0", "typescript": "5.9.3", "vite": "^8.0.16" - }, - "imports": { - "../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts", - "@lib/worker/bgWorker.ts": "@lib/worker/bgWorker.mock.ts" } } diff --git a/src/apps/webpeer/src/CommandsShim.ts b/src/apps/webpeer/src/CommandsShim.ts index 0f53cd1c..31a9a352 100644 --- a/src/apps/webpeer/src/CommandsShim.ts +++ b/src/apps/webpeer/src/CommandsShim.ts @@ -1,6 +1,6 @@ -import { LOG_LEVEL_VERBOSE } from "@lib/common/types"; +import { LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { defaultLoggerEnv, setGlobalLogFunction } from "@lib/common/logger"; +import { defaultLoggerEnv, setGlobalLogFunction } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { writable } from "svelte/store"; export const logs = writable([] as string[]); @@ -9,7 +9,6 @@ let _logs = [] as string[]; const maxLines = 10000; setGlobalLogFunction((msg, level) => { - console.log(msg); const msgstr = typeof msg === "string" ? msg : JSON.stringify(msg); const strLog = `${new Date().toISOString()}\u2001${msgstr}`; _logs.push(strLog); diff --git a/src/apps/webpeer/src/P2PReplicatorShim.ts b/src/apps/webpeer/src/P2PReplicatorShim.ts index a4d8d625..9fbfae5e 100644 --- a/src/apps/webpeer/src/P2PReplicatorShim.ts +++ b/src/apps/webpeer/src/P2PReplicatorShim.ts @@ -1,4 +1,4 @@ -import { PouchDB } from "@lib/pouchdb/pouchdb-browser"; +import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser"; import { type EntryDoc, type ObsidianLiveSyncSettings, @@ -6,33 +6,32 @@ import { LOG_LEVEL_VERBOSE, P2P_DEFAULT_SETTINGS, REMOTE_P2P, -} from "@lib/common/types"; -import { eventHub } from "@lib/hub/hub"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@lib/common/logger"; +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; +import { LOG_LEVEL_NOTICE, Logger, type LOG_LEVEL } from "@vrtmrz/livesync-commonlib/compat/common/logger"; import { EVENT_P2P_PEER_SHOW_EXTRA_MENU, type PeerStatus, type PluginShim, -} from "@lib/replication/trystero/P2PReplicatorPaneCommon"; -import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector"; -import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase.ts"; +} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorPaneCommon"; +import { useP2PReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorCore"; +import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector"; +import type { P2PReplicatorBase } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PReplicatorBase"; import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase"; import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents"; +import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import { unique } from "octagonal-wheels/collection"; -import { BrowserServiceHub } from "@lib/services/BrowserServices"; -import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types"; -import { ServiceContext } from "@lib/services/base/ServiceBase"; -import type { InjectableServiceHub } from "@lib/services/InjectableServices"; -import { Menu } from "@lib/services/implements/browser/Menu"; +import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub"; +import { Menu } from "@/apps/browser/BrowserMenu"; import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2"; -import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService"; -import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService"; -import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; -import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; +import type { BrowserAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/BrowserAPIService"; +import type { InjectableSettingService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableSettingService"; +import { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; +import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; +import { createLiveSyncBrowserServiceHub } from "@/apps/browser/createLiveSyncBrowserServiceHub"; function addToList(item: string, list: string) { return unique( @@ -91,7 +90,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase { } constructor() { - const browserServiceHub = new BrowserServiceHub(); + const browserServiceHub = createLiveSyncBrowserServiceHub(); this.services = browserServiceHub; (this.services.API as BrowserAPIService).getSystemVaultName.setHandler( @@ -103,7 +102,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase { this.services.setting.settings = _settings; (this.services.setting as InjectableSettingService).saveData.setHandler(async (data) => { await repStore.set("settings", data); - eventHub.emitEvent(EVENT_SETTING_SAVED, data); + this.services.context.events.emitEvent(EVENT_SETTING_SAVED, data); }); (this.services.setting as InjectableSettingService).loadData.setHandler(async () => { const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) }; @@ -180,7 +179,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase { m?: Menu; afterConstructor(): void { - eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => { + this.services.context.events.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => { if (this.m) { this.m.hide(); } @@ -231,6 +230,34 @@ export class P2PReplicatorShim implements P2PReplicatorBase { return this._liveSyncReplicator?.disableBroadcastChanges(); } + enableBroadcastChanges() { + return this._liveSyncReplicator?.enableBroadcastChanges(); + } + + disableBroadcastChanges() { + return this._liveSyncReplicator?.disableBroadcastChanges(); + } + + async makeDecision(decision: Parameters[0]): Promise { + await this._liveSyncReplicator?.makeDecision(decision); + } + + async revokeDecision(decision: Parameters[0]): Promise { + await this._liveSyncReplicator?.revokeDecision(decision); + } + + watchPeer(peerId: string): void { + this._liveSyncReplicator?.watchPeer(peerId); + } + + unwatchPeer(peerId: string): void { + this._liveSyncReplicator?.unwatchPeer(peerId); + } + + async sync(peerId: string, showNotice?: boolean): Promise { + return await this._liveSyncReplicator?.sync(peerId, showNotice); + } + get replicator() { return this._liveSyncReplicator; } diff --git a/src/apps/webpeer/src/SyncMain.svelte b/src/apps/webpeer/src/SyncMain.svelte index 538fff34..1cb9209d 100644 --- a/src/apps/webpeer/src/SyncMain.svelte +++ b/src/apps/webpeer/src/SyncMain.svelte @@ -3,13 +3,12 @@ import P2PReplicatorPane from "@/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte"; import { onMount, tick } from "svelte"; import { cmdSyncShim } from "./P2PReplicatorShim"; - import { eventHub } from "@lib/hub/hub"; - import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents"; + import { EVENT_LAYOUT_READY } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; let synchronised = $state(cmdSyncShim.init()); onMount(() => { - eventHub.emitEvent(EVENT_LAYOUT_READY); + void synchronised.then((shim) => shim.services.context.events.emitEvent(EVENT_LAYOUT_READY)); return () => { synchronised.then((e) => e.close()); }; @@ -27,7 +26,7 @@
{#await synchronised then cmdSync} - + {:catch error}

{error.message}

{/await} diff --git a/src/apps/webpeer/src/UITest.svelte b/src/apps/webpeer/src/UITest.svelte index 0e6af79b..459006cc 100644 --- a/src/apps/webpeer/src/UITest.svelte +++ b/src/apps/webpeer/src/UITest.svelte @@ -1,6 +1,6 @@
- +

Available Peers

diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts index 9ac21507..39cf4a62 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.ts @@ -1,7 +1,7 @@ import type { App } from "@/deps.ts"; -import { Logger } from "@lib/common/logger"; -import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@lib/common/types"; -import type { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator"; +import { Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; +import { LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { LiveSyncTrysteroReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/LiveSyncTrysteroReplicator"; import { P2POpenReplicationModal } from "./P2POpenReplicationModal"; /** @@ -111,7 +111,7 @@ export function createOpenRebuildUI( try { replicator.setOnSetup(); Logger(`Rebuilding from peer ${peerId}`, logLevel); - const result = await replicator.replicateFrom(peerId, showResult); + const result = await replicator.replicateFrom(peerId, showResult, true); sessionResult = result?.ok ?? false; } catch (e) { Logger( diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts index e94d9ab3..86ad37e0 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts +++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts @@ -160,6 +160,19 @@ describe("createOpenRebuildUI", () => { await rebuild; await expect(session).resolves.toBe(true); expect(replicator.setOnSetup).toHaveBeenCalledOnce(); + expect(replicator.replicateFrom).toHaveBeenCalledWith("peer-a", true, true); expect(replicator.clearOnSetup).toHaveBeenCalledOnce(); }); + + it("does not complete Fetch when the rebuild dialogue closes without selecting a peer", async () => { + const replicator = createReplicator(); + const session = createOpenRebuildUI({} as any)(replicator)(true); + const modal = modalState.instances[0]; + + modal.onClosed?.(); + + await expect(session).resolves.toBe(false); + expect(replicator.replicateFrom).not.toHaveBeenCalled(); + expect(replicator.setOnSetup).not.toHaveBeenCalled(); + }); }); diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte b/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte index 5987746d..a89eded8 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte @@ -1,12 +1,12 @@ @@ -435,7 +453,7 @@

Please select an active P2P remote configuration to change P2P sync targets.

{/if} - +
@@ -481,35 +499,34 @@ > Revoke +
- WATCH + {translateMessage("Follow changes")}
-
- SYNC - -
{:else}
@@ -800,7 +817,7 @@ } .accepted-row { - grid-template-columns: 1fr auto auto; + grid-template-columns: 1fr auto auto auto; } .decision-label { diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts index e51cd7df..e10cfad9 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPaneView.ts @@ -2,7 +2,7 @@ import { WorkspaceLeaf } from "@/deps.ts"; import { mount } from "svelte"; import { SvelteItemView } from "@/common/SvelteItemView.ts"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts"; -import type { P2PPaneParams } from "@lib/replication/trystero/UseP2PReplicatorResult"; +import type { P2PPaneParams } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; import P2PServerStatusPane from "./P2PServerStatusPane.svelte"; export const VIEW_TYPE_P2P_SERVER_STATUS = "p2p-server-status"; @@ -35,7 +35,7 @@ export class P2PServerStatusPaneView extends SvelteItemView { return mount(P2PServerStatusPane, { target, props: { - liveSyncReplicator: this._p2pResult.replicator, + getLiveSyncReplicator: () => this._p2pResult.replicator, core: this.core, }, }); diff --git a/src/features/P2PSync/P2PReplicator/PeerStatusRow.svelte b/src/features/P2PSync/P2PReplicator/PeerStatusRow.svelte index 476fa270..b9b92a99 100644 --- a/src/features/P2PSync/P2PReplicator/PeerStatusRow.svelte +++ b/src/features/P2PSync/P2PReplicator/PeerStatusRow.svelte @@ -1,9 +1,9 @@ - - -

- The connection to the server has been configured successfully. As the next step, the latest synchronisation data will be downloaded from the server to this device. -

-

- PLEASE NOTE -
- After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised - files in this vault, conflicts may occur with the server data. -

-
- - Please select the button below to restart and proceed to the data fetching confirmation. - - - setResult(TYPE_APPLY)} /> - setResult(TYPE_CANCELLED)} /> - +{#if isP2P} + + +

+ {translateMessage( + "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device." + )} +

+

+ PLEASE NOTE +
+ {translateMessage( + "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data." + )} +

+
+ + + {translateMessage("Restart this device, then choose the source device when P2P Rebuild opens.")} + + + + setResult(TYPE_APPLY)} + /> + setResult(TYPE_CANCELLED)} /> + +{:else} + + +

+ The connection to the server has been configured successfully. As the next step, the latest synchronisation data will be downloaded from the server to this device. +

+

+ PLEASE NOTE +
+ After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised + files in this vault, conflicts may occur with the server data. +

+
+ + Please select the button below to restart and proceed to the data fetching confirmation. + + + setResult(TYPE_APPLY)} /> + setResult(TYPE_CANCELLED)} /> + +{/if} diff --git a/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte b/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte index 031ae858..6df6f571 100644 --- a/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte +++ b/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte @@ -1,37 +1,63 @@ - - -

- The connection to the server has been configured successfully. As the next step, the synchronisation data on the server will be built based on the current data on this device. -

-

- IMPORTANT -
- After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that - any unintended data currently on the server will be completely overwritten. -

-
- - Please select the button below to restart and proceed to the final confirmation. - - - setResult(TYPE_APPLY)} /> - setResult(TYPE_CANCELLED)} /> - +{#if isP2P} + + +

{msg("Ui.SetupWizard.OutroNewP2PUser.GuidancePrimary")}

+

+ {msg("Ui.SetupWizard.OutroNewP2PUser.Important")} +
+ {msg("Ui.SetupWizard.OutroNewP2PUser.GuidanceNotice")} +

+
+ + {msg("Ui.SetupWizard.OutroNewP2PUser.Question")} + + + setResult(TYPE_APPLY)} + /> + setResult(TYPE_CANCELLED)} /> + +{:else} + + +

+ The connection to the server has been configured successfully. As the next step, the synchronisation data on the server will be built based on the current data on this device. +

+

+ IMPORTANT +
+ After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware + that any unintended data currently on the server will be completely overwritten. +

+
+ + Please select the button below to restart and proceed to the final confirmation. + + + setResult(TYPE_APPLY)} /> + setResult(TYPE_CANCELLED)} /> + +{/if} diff --git a/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte b/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte index 0206e9c0..39d825f6 100644 --- a/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte +++ b/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte @@ -2,23 +2,27 @@ /** * Panel to check and fix CouchDB configuration issues */ - import type { ObsidianLiveSyncSettings } from "@lib/common/types"; - import Decision from "@lib/UI/components/Decision.svelte"; - import UserDecisions from "@lib/UI/components/UserDecisions.svelte"; + import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; + import Decision from "@/modules/services/LiveSyncUI/components/Decision.svelte"; + import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte"; import { checkConfig, type ConfigCheckResult, type ResultError, type ResultErrorMessage } from "./utilCheckCouchDB"; + import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger"; + import { $msg as translateMessage } from "@/common/translation"; + import { getDialogContext } from "@/modules/services/LiveSyncUI/svelteDialog"; + import { getCouchDBServerFixConfirmation } from "./couchDBServerFixConfirmation"; type Props = { trialRemoteSetting: ObsidianLiveSyncSettings; }; const { trialRemoteSetting }: Props = $props(); + const context = getDialogContext(); let detectedIssues = $state([]); async function testAndFixSettings() { detectedIssues = []; try { const fixResults = await checkConfig(trialRemoteSetting); - console.dir(fixResults); detectedIssues = fixResults; } catch (e) { - console.error("Error during testAndFixSettings:", e); + Logger(e, LOG_LEVEL_VERBOSE, "setup-couchdb-check"); detectedIssues.push({ message: `Error during testAndFixSettings: ${e}`, result: "error", classes: [] }); } } @@ -33,14 +37,23 @@ } let processing = $state(false); async function fixIssue(issue: ResultError) { + const confirmation = getCouchDBServerFixConfirmation(issue.settingKey, issue.expectedValue); + const confirmed = await context.services.confirm.askYesNoDialog(confirmation.message, { + title: confirmation.title, + defaultOption: "No", + }); + if (confirmed !== "yes") { + return; + } try { processing = true; await issue.fix(); } catch (e) { - console.error("Error during fixIssue:", e); + Logger(e, LOG_LEVEL_VERBOSE, "setup-couchdb-fix"); + } finally { + await testAndFixSettings(); + processing = false; } - await testAndFixSettings(); - processing = false; } const errorIssueCount = $derived.by(() => { return detectedIssues.filter((issue) => isErrorResult(issue)).length; @@ -64,7 +77,7 @@
{/snippet} - +
diff --git a/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte b/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte index 8db36e47..be4c3e78 100644 --- a/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte +++ b/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte @@ -1,15 +1,16 @@ - -This procedure will first delete all existing synchronisation data from the server. Following this, the server data - will be completely rebuilt, using the current state of your Vault on this device (including its local database) as - the single, authoritative master copy. - - You should perform this operation only in exceptional circumstances, such as when the server data is completely - corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually - large in comparison to the Vault size. - - - + {msg("Ui.SetupWizard.RebuildEverythingP2P.Guidance")} + {msg("Ui.SetupWizard.RebuildEverythingP2P.Note")} + + + {msg("Ui.SetupWizard.RebuildEverythingP2P.ConfirmLocalResetNote")} + + +{:else} + + This procedure will first delete all existing synchronisation data from the server. Following this, the server + data will be completely rebuilt, using the current state of your Vault on this device (including its local + database) as the single, authoritative master copy. - There is a way to resolve this on other devices. - Of course, we can back up the data before proceeding. - - - by resetting the remote, you will be informed on other devices. - - - + + You should perform this operation only in exceptional circumstances, such as when the server data is completely + corrupted, when changes on all other devices are no longer needed, or when the database size has become unusually + large in comparison to the Vault size. + + + + There is a way to resolve this on other devices. + Of course, we can back up the data before proceeding. + + + by resetting the remote, you will be informed on other devices. + + + +{/if}
Have you created a backup before proceeding? @@ -103,12 +114,19 @@ - - - - - +{#if !isP2P} + + + + + +{/if} - commit()} /> + commit()} + /> setResult(TYPE_CANCEL)} /> diff --git a/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte b/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte index fcef3134..5c582c8b 100644 --- a/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte +++ b/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte @@ -1,9 +1,9 @@ -We will now proceed with the server configuration. +{translateMessage("Ui.SetupWizard.SelectNew.Guidance")} - How would you like to configure the connection to your server? + {translateMessage("Ui.SetupWizard.SelectNew.Question")} diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte index 01a299e6..e019bb79 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte @@ -1,11 +1,12 @@ - + - Please select the type of server to which you are connecting. + {translateMessage("Ui.SetupWizard.SetupRemote.Guidance")} - - diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte index 2ee81af1..26b7ee5e 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte @@ -1,23 +1,23 @@ @@ -150,6 +175,7 @@ /> We can use only Secure (HTTPS) connections on Obsidian Mobile. +{translateMessage("Enter a complete HTTP or HTTPS URL.")} - You cannot use capital letters, spaces, or special characters in the database name. And not allowed to start with an - underscore (_). + {translateMessage("CouchDB validates the database name when you connect. The name must not be empty.")} @@ -270,6 +294,11 @@ + + {translateMessage( + "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required." + )} +
@@ -281,8 +310,19 @@ Checking connection... Please wait. {:else} - checkAndCommit()} /> - commit()} /> + checkAndCommit()} /> + {#if setupMode === "settings"} + + {translateMessage( + "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected." + )} + + commit()} + /> + {/if} cancel()} /> {/if} diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte index 09a61766..4c6eeff2 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte @@ -1,21 +1,21 @@ + +
+ +
+ + diff --git a/src/modules/services/LiveSyncUI/components/Check.svelte b/src/modules/services/LiveSyncUI/components/Check.svelte new file mode 100644 index 00000000..5b33a9f1 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Check.svelte @@ -0,0 +1,53 @@ + + + +
+ + {#if value && noteOnSelected} + {@render noteOnSelected()} + {:else if !value && noteOnUnselected} + {@render noteOnUnselected()} + {/if} + {@render children?.()} +
+ + diff --git a/src/modules/services/LiveSyncUI/components/Decision.svelte b/src/modules/services/LiveSyncUI/components/Decision.svelte new file mode 100644 index 00000000..3a90725c --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Decision.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/modules/services/LiveSyncUI/components/DialogHeader.svelte b/src/modules/services/LiveSyncUI/components/DialogHeader.svelte new file mode 100644 index 00000000..5fc71d93 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/DialogHeader.svelte @@ -0,0 +1,41 @@ + + +
+

{translatedTitle}

+ {#if translatedSubtitle} +

{translatedSubtitle}

+ {/if} +
+ + diff --git a/src/modules/services/LiveSyncUI/components/ExtraItems.svelte b/src/modules/services/LiveSyncUI/components/ExtraItems.svelte new file mode 100644 index 00000000..71227fea --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/ExtraItems.svelte @@ -0,0 +1,17 @@ + + +
+ {translatedTitle} +
+ {@render children?.()} +
+
diff --git a/src/modules/services/LiveSyncUI/components/Guidance.svelte b/src/modules/services/LiveSyncUI/components/Guidance.svelte new file mode 100644 index 00000000..cb6af576 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Guidance.svelte @@ -0,0 +1,21 @@ + + +
+ {#if translatedTitle} +

{translatedTitle}

+ {/if} + {@render children?.()} +
diff --git a/src/modules/services/LiveSyncUI/components/InfoNote.svelte b/src/modules/services/LiveSyncUI/components/InfoNote.svelte new file mode 100644 index 00000000..117e1760 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/InfoNote.svelte @@ -0,0 +1,87 @@ + + +{#if visible === undefined || visible === true} +
+ {#if signalWordText} +
{signalWordText}
+ {/if} + {#if translatedTitle}

{translatedTitle}

{/if} + {#if translatedMessage}

{translatedMessage}

{/if} + {@render children?.()} +
+{/if} diff --git a/src/modules/services/LiveSyncUI/components/InfoTable.svelte b/src/modules/services/LiveSyncUI/components/InfoTable.svelte new file mode 100644 index 00000000..e779e624 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/InfoTable.svelte @@ -0,0 +1,74 @@ + + +
+
+ {#each infoEntries as [key, value]} +
+
{key}
+
+
+
{value}
+
+ {/each} +
+
+ + diff --git a/src/modules/services/LiveSyncUI/components/InputRow.svelte b/src/modules/services/LiveSyncUI/components/InputRow.svelte new file mode 100644 index 00000000..75eed247 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/InputRow.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/modules/services/LiveSyncUI/components/Instruction.svelte b/src/modules/services/LiveSyncUI/components/Instruction.svelte new file mode 100644 index 00000000..fde612f5 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Instruction.svelte @@ -0,0 +1,10 @@ + + +
+ {@render children?.()} +
diff --git a/src/modules/services/LiveSyncUI/components/Option.svelte b/src/modules/services/LiveSyncUI/components/Option.svelte new file mode 100644 index 00000000..50248699 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Option.svelte @@ -0,0 +1,81 @@ + + +
+ +
+ + diff --git a/src/modules/services/LiveSyncUI/components/Options.svelte b/src/modules/services/LiveSyncUI/components/Options.svelte new file mode 100644 index 00000000..678a37fd --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Options.svelte @@ -0,0 +1,14 @@ + + +
+ {@render children?.()} +
diff --git a/src/modules/services/LiveSyncUI/components/Password.svelte b/src/modules/services/LiveSyncUI/components/Password.svelte new file mode 100644 index 00000000..5ebf3884 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Password.svelte @@ -0,0 +1,34 @@ + + + + diff --git a/src/modules/services/LiveSyncUI/components/Question.svelte b/src/modules/services/LiveSyncUI/components/Question.svelte new file mode 100644 index 00000000..3956bf11 --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/Question.svelte @@ -0,0 +1,26 @@ + + +
+ {#if question}

{@render question?.()}

{/if} +
+ {@render children?.()} +
+
+ + diff --git a/src/modules/services/LiveSyncUI/components/UserDecisions.svelte b/src/modules/services/LiveSyncUI/components/UserDecisions.svelte new file mode 100644 index 00000000..7aadf81c --- /dev/null +++ b/src/modules/services/LiveSyncUI/components/UserDecisions.svelte @@ -0,0 +1,12 @@ + + +
+ {#if children} + {@render children()} + {/if} +
diff --git a/src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte b/src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte new file mode 100644 index 00000000..cdd112cb --- /dev/null +++ b/src/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte @@ -0,0 +1,59 @@ + + + + + + + + + + + Your {title || "data"} has been copied to the clipboard. + + + + + + diff --git a/src/modules/services/LiveSyncUI/svelteDialog.ts b/src/modules/services/LiveSyncUI/svelteDialog.ts new file mode 100644 index 00000000..648e4057 --- /dev/null +++ b/src/modules/services/LiveSyncUI/svelteDialog.ts @@ -0,0 +1,14 @@ +export type { + HasSetResult, + HasGetInitialData, + ComponentHasResult, + GuestDialogProps, + DialogSvelteComponentBaseProps, + DialogControlBase, +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; +export { + CONTEXT_DIALOG_CONTROLS, + setupDialogContext, + getDialogContext, + SvelteDialogManagerBase, +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; diff --git a/src/modules/services/ObsidianAPIService.ts b/src/modules/services/ObsidianAPIService.ts index daa0a895..c455d172 100644 --- a/src/modules/services/ObsidianAPIService.ts +++ b/src/modules/services/ObsidianAPIService.ts @@ -1,11 +1,11 @@ -import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { InjectableAPIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAPIService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { Platform, type Command, type ViewCreator } from "@/deps.ts"; import { ObsHttpHandler } from "@/modules/essentialObsidian/APILib/ObsHttpHandler"; import { ObsidianConfirm } from "./ObsidianConfirm"; -import type { Confirm } from "@lib/interfaces/Confirm"; +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; import { requestUrl, type RequestUrlParam } from "@/deps"; -import { compatGlobal } from "@lib/common/coreEnvFunctions"; +import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; // All Services will be migrated to be based on Plain Services, not Injectable Services. // This is a migration step. diff --git a/src/modules/services/ObsidianAPIService.unit.spec.ts b/src/modules/services/ObsidianAPIService.unit.spec.ts new file mode 100644 index 00000000..6523bfaf --- /dev/null +++ b/src/modules/services/ObsidianAPIService.unit.spec.ts @@ -0,0 +1,67 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + platform: { + isMobile: false, + }, +})); + +vi.mock("@/deps.ts", () => ({ + Platform: mocks.platform, + requestUrl: vi.fn(), +})); + +vi.mock("@/deps", () => ({ + Platform: mocks.platform, + requestUrl: vi.fn(), +})); + +vi.mock("@/modules/essentialObsidian/APILib/ObsHttpHandler", () => ({ + ObsHttpHandler: class {}, +})); + +vi.mock("./ObsidianConfirm", () => ({ + ObsidianConfirm: class {}, +})); + +import { ObsidianAPIService } from "./ObsidianAPIService"; +import type { ObsidianServiceContext } from "./ObsidianServiceContext"; + +function createService(workspace: Record, isMobile = false): ObsidianAPIService { + return new ObsidianAPIService({ + app: { workspace, isMobile }, + } as unknown as ObsidianServiceContext); +} + +beforeEach(() => { + mocks.platform.isMobile = false; + vi.clearAllMocks(); +}); + +describe("ObsidianAPIService.showWindowOnRight", () => { + it("keeps the status view in the right leaf on mobile", async () => { + mocks.platform.isMobile = true; + const rightLeaf = { + setViewState: vi.fn().mockResolvedValue(undefined), + }; + const workspace = { + getLeavesOfType: vi.fn(() => []), + getLeaf: vi.fn(), + getRightLeaf: vi.fn(() => rightLeaf), + revealLeaf: vi.fn().mockResolvedValue(undefined), + }; + const service = createService(workspace, true); + + expect(service.isMobile()).toBe(true); + await service.showWindowOnRight("p2p-status"); + + expect(workspace.getLeavesOfType).toHaveBeenCalledWith("p2p-status"); + expect(workspace.getRightLeaf).toHaveBeenCalledWith(false); + expect(workspace.getLeaf).not.toHaveBeenCalled(); + expect(rightLeaf.setViewState).toHaveBeenCalledWith({ + type: "p2p-status", + active: false, + }); + expect(workspace.revealLeaf).toHaveBeenCalledWith(rightLeaf); + }); +}); diff --git a/src/modules/services/ObsidianAppLifecycleService.ts b/src/modules/services/ObsidianAppLifecycleService.ts index 6c730e62..e2888a7a 100644 --- a/src/modules/services/ObsidianAppLifecycleService.ts +++ b/src/modules/services/ObsidianAppLifecycleService.ts @@ -1,5 +1,5 @@ -import { AppLifecycleServiceBase } from "@lib/services/implements/injectable/InjectableAppLifecycleService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { AppLifecycleServiceBase } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableAppLifecycleService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; declare module "obsidian" { interface App { commands: { diff --git a/src/modules/services/ObsidianConfirm.ts b/src/modules/services/ObsidianConfirm.ts index ca4f9c1a..f9197e02 100644 --- a/src/modules/services/ObsidianConfirm.ts +++ b/src/modules/services/ObsidianConfirm.ts @@ -1,18 +1,16 @@ import { type App, type Plugin, Notice } from "@/deps"; import { scheduleTask, memoIfNotExist, memoObject, retrieveMemoObject, disposeMemoObject } from "@/common/utils"; -import { $msg } from "@lib/common/i18n"; -import type { Confirm } from "@lib/interfaces/Confirm"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { - askYesNo, - askString, - confirmWithMessageWithWideButton, - askSelectString, - confirmWithMessage, -} from "@/modules/coreObsidian/UILib/dialogs"; +import { EVENT_PLUGIN_UNLOADED } from "@/common/events"; +import { $msg } from "@/common/translation"; +import type { Confirm, ConfirmActionLayout } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; +import { confirmAction, pickOne, promptPassword, promptText } from "@vrtmrz/obsidian-plugin-kit"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import { confirmWithMessageWithWideButton } from "@/modules/coreObsidian/UILib/dialogs"; export class ObsidianConfirm implements Confirm { private _context: T; + private readonly dialogueController = new AbortController(); + private readonly popupKeys = new Set(); get _app(): App { return this._context.app; } @@ -21,12 +19,59 @@ export class ObsidianConfirm { + this.dialogueController.abort(); + for (const popupKey of this.popupKeys) { + this.closePopup(popupKey); + } + }); } - askYesNo(message: string): Promise<"yes" | "no"> { - return askYesNo(this._app, message); + + private get dialogueLifecycle() { + return { signal: this.dialogueController.signal }; } - askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise { - return askString(this._app, title, key, placeholder, isPassword); + + private hasCountdown(timeout: number | undefined): timeout is number { + return timeout !== undefined && timeout > 0; + } + + async askYesNo(message: string): Promise<"yes" | "no"> { + const result = await confirmAction( + this._app, + { + title: $msg("moduleInputUIObsidian.defaultTitleConfirmation"), + message, + actions: ["yes", "no"] as const, + labels: { + yes: $msg("moduleInputUIObsidian.optionYes"), + no: $msg("moduleInputUIObsidian.optionNo"), + }, + actionLayout: "vertical", + defaultAction: "no", + sourcePath: "/", + }, + this.dialogueLifecycle + ); + return result === "yes" ? "yes" : "no"; + } + + async askString( + title: string, + key: string, + placeholder: string, + isPassword: boolean = false + ): Promise { + const prompt = isPassword ? promptPassword : promptText; + const result = await prompt( + this._app, + { + title, + label: key, + placeholder, + }, + this.dialogueLifecycle + ); + return result ?? false; } async askYesNoDialog( @@ -37,6 +82,22 @@ export class ObsidianConfirm { - return askSelectString(this._app, message, items); + async askSelectString(message: string, items: string[]): Promise { + const result = await pickOne( + this._app, + { + items, + getText: (item) => item, + placeholder: message, + }, + this.dialogueLifecycle + ); + return result ?? ""; } - askSelectStringDialogue( + async askSelectStringDialogue( message: string, buttons: T, opt: { title?: string; defaultAction: T[number]; timeout?: number } ): Promise { const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect"); + // Commonlib owns the transport decision, while LiveSync owns the + // concrete Obsidian view which lets users revise that decision. + const presentedMessage = + opt.title === "P2P Connection Request" + ? message.replace("Peer-to-Peer Replicator Pane", $msg("P2P Status pane")) + : message; + if (!this.hasCountdown(opt.timeout)) { + const result = await confirmAction( + this._app, + { + title: opt.title || defaultTitle, + message: presentedMessage, + actions: buttons, + actionLayout: "vertical", + defaultAction: opt.defaultAction, + sourcePath: "/", + }, + this.dialogueLifecycle + ); + return result ?? false; + } return confirmWithMessageWithWideButton( this._plugin, opt.title || defaultTitle, - message, + presentedMessage, buttons, opt.defaultAction, opt.timeout ); } - askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void) { + askInPopup( + key: string, + dialogText: string, + anchorCallback: (anchor: HTMLAnchorElement) => void, + durationMs: number = 20000 + ) { + const popupKey = "popup-" + key; + this.popupKeys.add(popupKey); const fragment = createFragment((doc) => { const [beforeText, afterText] = dialogText.split("{HERE}", 2); - doc.createEl("span", undefined, (a) => { + doc.createSpan(undefined, (a) => { a.appendText(beforeText); a.appendChild( a.createEl("a", undefined, (anchor) => { anchorCallback(anchor); + anchor.addEventListener("click", () => this.closePopup(popupKey)); }) ); a.appendText(afterText); }); }); - const popupKey = "popup-" + key; scheduleTask(popupKey, 1000, async () => { + if (this.dialogueController.signal.aborted) { + this.popupKeys.delete(popupKey); + return; + } const popup = await memoIfNotExist(popupKey, () => new Notice(fragment, 0)); const isShown = popup?.noticeEl?.isShown(); if (!isShown) { memoObject(popupKey, new Notice(fragment, 0)); } - scheduleTask(popupKey + "-close", 20000, () => { - const popup = retrieveMemoObject(popupKey); - if (!popup) return; - if (popup?.noticeEl?.isShown()) { - popup.hide(); - } - disposeMemoObject(popupKey); - }); + scheduleTask(popupKey + "-close", durationMs, () => this.closePopup(popupKey)); }); } - confirmWithMessage( + private closePopup(popupKey: string) { + const popup = retrieveMemoObject(popupKey); + if (!popup) { + this.popupKeys.delete(popupKey); + return; + } + if (popup.noticeEl?.isShown()) { + popup.hide(); + } + disposeMemoObject(popupKey); + this.popupKeys.delete(popupKey); + } + + async confirmWithMessage( title: string, contentMd: string, buttons: string[], defaultAction: (typeof buttons)[number], - timeout?: number + timeout?: number, + actionLayout?: ConfirmActionLayout ): Promise<(typeof buttons)[number] | false> { - return confirmWithMessage(this._plugin, title, contentMd, buttons, defaultAction, timeout); + if (this.hasCountdown(timeout)) { + return confirmWithMessageWithWideButton(this._plugin, title, contentMd, buttons, defaultAction, timeout); + } + const result = await confirmAction( + this._app, + { + title, + message: contentMd, + actions: buttons, + defaultAction, + sourcePath: "/", + actionLayout: actionLayout ?? "vertical", + }, + this.dialogueLifecycle + ); + return result ?? false; } } diff --git a/src/modules/services/ObsidianConfirm.unit.spec.ts b/src/modules/services/ObsidianConfirm.unit.spec.ts new file mode 100644 index 00000000..afb7e3a0 --- /dev/null +++ b/src/modules/services/ObsidianConfirm.unit.spec.ts @@ -0,0 +1,277 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + confirmAction: vi.fn(), + pickOne: vi.fn(), + promptPassword: vi.fn(), + promptText: vi.fn(), + legacyAskSelectString: vi.fn(), + legacyAskString: vi.fn(), + legacyAskYesNo: vi.fn(), + legacyConfirm: vi.fn(), + legacyWideConfirm: vi.fn(), +})); + +vi.mock("@vrtmrz/obsidian-plugin-kit", () => ({ + confirmAction: mocks.confirmAction, + pickOne: mocks.pickOne, + promptPassword: mocks.promptPassword, + promptText: mocks.promptText, +})); + +vi.mock("@/modules/coreObsidian/UILib/dialogs", () => ({ + askSelectString: mocks.legacyAskSelectString, + askString: mocks.legacyAskString, + askYesNo: mocks.legacyAskYesNo, + confirmWithMessage: mocks.legacyConfirm, + confirmWithMessageWithWideButton: mocks.legacyWideConfirm, +})); + +vi.mock("@/deps", () => ({ + Notice: class {}, +})); + +import { EVENT_PLUGIN_UNLOADED } from "@/common/events"; +import { memoObject, retrieveMemoObject } from "@/common/utils"; +import { createLiveSyncEventHub } from "@vrtmrz/livesync-commonlib/context"; +import { ObsidianConfirm } from "./ObsidianConfirm"; +import type { ObsidianServiceContext } from "./ObsidianServiceContext"; + +function createConfirm() { + const app = { id: "app" }; + const plugin = { app }; + const events = createLiveSyncEventHub(); + const context = { app, plugin, events } as unknown as ObsidianServiceContext; + return { confirm: new ObsidianConfirm(context), events, app, plugin }; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("ObsidianConfirm Fancy Kit adapter", () => { + it("uses owner-bound Kit prompts and preserves cancellation and empty input", async () => { + const { confirm, app } = createConfirm(); + mocks.promptText.mockResolvedValueOnce(null); + mocks.promptPassword.mockResolvedValueOnce(""); + + await expect(confirm.askString("Name", "Device name", "New Remote")).resolves.toBe(false); + await expect(confirm.askString("Secret", "Passphrase", "Enter it", true)).resolves.toBe(""); + + expect(mocks.promptText).toHaveBeenCalledWith( + app, + { + title: "Name", + label: "Device name", + placeholder: "New Remote", + }, + { signal: expect.any(AbortSignal) } + ); + expect(mocks.promptPassword).toHaveBeenCalledWith( + app, + { + title: "Secret", + label: "Passphrase", + placeholder: "Enter it", + }, + { signal: expect.any(AbortSignal) } + ); + expect(mocks.legacyAskString).not.toHaveBeenCalled(); + }); + + it("uses Kit for untimed yes/no and typed selection while preserving dismissed results", async () => { + const { confirm, app } = createConfirm(); + mocks.confirmAction.mockResolvedValueOnce("yes"); + mocks.pickOne.mockResolvedValueOnce("Beta").mockResolvedValueOnce(null); + + await expect(confirm.askYesNo("Continue?")).resolves.toBe("yes"); + await expect(confirm.askSelectString("Target", ["Alpha", "Beta"])).resolves.toBe("Beta"); + await expect(confirm.askSelectString("Target", ["Alpha"])).resolves.toBe(""); + + expect(mocks.confirmAction).toHaveBeenCalledWith( + app, + expect.objectContaining({ + message: "Continue?", + actions: ["yes", "no"], + actionLayout: "vertical", + defaultAction: "no", + }), + { signal: expect.any(AbortSignal) } + ); + expect(mocks.pickOne).toHaveBeenCalledWith( + app, + expect.objectContaining({ + items: ["Alpha", "Beta"], + getText: expect.any(Function), + }), + { signal: expect.any(AbortSignal) } + ); + expect(mocks.legacyAskYesNo).not.toHaveBeenCalled(); + expect(mocks.legacyAskSelectString).not.toHaveBeenCalled(); + }); + + it("keeps untimed and countdown action dialogues vertically stacked", async () => { + const { confirm, app, plugin } = createConfirm(); + mocks.confirmAction.mockResolvedValueOnce("Apply").mockResolvedValueOnce("Yes"); + mocks.legacyWideConfirm.mockResolvedValueOnce("Cancel").mockResolvedValueOnce("No"); + + await expect(confirm.confirmWithMessage("Review", "**Apply?**", ["Apply", "Cancel"], "Cancel")).resolves.toBe( + "Apply" + ); + await expect(confirm.askYesNoDialog("Continue?", { title: "Question", defaultOption: "Yes" })).resolves.toBe( + "yes" + ); + await expect(confirm.confirmWithMessage("Timed", "Wait", ["Apply", "Cancel"], "Cancel", 30)).resolves.toBe( + "Cancel" + ); + await expect(confirm.askYesNoDialog("Timed?", { defaultOption: "No", timeout: 10 })).resolves.toBe("no"); + + expect(mocks.confirmAction).toHaveBeenNthCalledWith( + 1, + app, + { + title: "Review", + message: "**Apply?**", + actions: ["Apply", "Cancel"], + actionLayout: "vertical", + defaultAction: "Cancel", + sourcePath: "/", + }, + { signal: expect.any(AbortSignal) } + ); + expect(mocks.confirmAction).toHaveBeenNthCalledWith( + 2, + app, + expect.objectContaining({ + title: "Question", + message: "Continue?", + actions: ["Yes", "No"], + actionLayout: "vertical", + defaultAction: "Yes", + }), + { signal: expect.any(AbortSignal) } + ); + expect(mocks.legacyWideConfirm).toHaveBeenNthCalledWith( + 1, + plugin, + "Timed", + "Wait", + ["Apply", "Cancel"], + "Cancel", + 30 + ); + expect(mocks.legacyWideConfirm).toHaveBeenNthCalledWith( + 2, + plugin, + expect.any(String), + "Timed?", + expect.any(Array), + expect.any(String), + 10 + ); + expect(mocks.legacyConfirm).not.toHaveBeenCalled(); + }); + + it("uses Kit for untimed multi-action selection and keeps timed wide actions on the countdown dialogue", async () => { + const { confirm, app, plugin } = createConfirm(); + const actions = ["Apply now", "Review later"] as const; + mocks.confirmAction.mockResolvedValueOnce("Review later"); + mocks.legacyWideConfirm.mockResolvedValueOnce("Apply now"); + + await expect( + confirm.askSelectStringDialogue("Choose the next step", actions, { + title: "Next step", + defaultAction: "Review later", + }) + ).resolves.toBe("Review later"); + await expect( + confirm.askSelectStringDialogue("Choose before the timer expires", actions, { + title: "Timed step", + defaultAction: "Apply now", + timeout: 15, + }) + ).resolves.toBe("Apply now"); + + expect(mocks.confirmAction).toHaveBeenCalledWith( + app, + { + title: "Next step", + message: "Choose the next step", + actions, + actionLayout: "vertical", + defaultAction: "Review later", + sourcePath: "/", + }, + { signal: expect.any(AbortSignal) } + ); + expect(mocks.legacyWideConfirm).toHaveBeenCalledWith( + plugin, + "Timed step", + "Choose before the timer expires", + actions, + "Apply now", + 15 + ); + }); + + it("refers P2P connection approvals to the current P2P Status pane", async () => { + const { confirm, plugin } = createConfirm(); + const actions = ["Accept", "Ignore"] as const; + mocks.legacyWideConfirm.mockResolvedValueOnce("Ignore"); + + await confirm.askSelectStringDialogue( + "You can revoke your decision from the Peer-to-Peer Replicator Pane.", + actions, + { + title: "P2P Connection Request", + defaultAction: "Ignore", + timeout: 30, + } + ); + + expect(mocks.legacyWideConfirm).toHaveBeenCalledWith( + plugin, + "P2P Connection Request", + "You can revoke your decision from the P2P Status pane.", + actions, + "Ignore", + 30 + ); + }); + + it("dismisses an open Kit dialogue when the plug-in unload event is emitted", async () => { + const { confirm, events } = createConfirm(); + let observedSignal: AbortSignal | undefined; + mocks.confirmAction.mockImplementation( + (_app, _options, lifecycle: { signal: AbortSignal }) => + new Promise((resolve) => { + observedSignal = lifecycle.signal; + lifecycle.signal.addEventListener("abort", () => resolve(null), { once: true }); + }) + ); + + const result = confirm.confirmWithMessage("Review", "Message", ["OK"], "OK"); + expect(observedSignal?.aborted).toBe(false); + + events.emitEvent(EVENT_PLUGIN_UNLOADED); + + await expect(result).resolves.toBe(false); + expect(observedSignal?.aborted).toBe(true); + }); + + it("closes an active Notice when the plug-in unload event is emitted", () => { + const { confirm, events } = createConfirm(); + const popupKey = "popup-remote-size-exceeded"; + const popup = { + hide: vi.fn(), + noticeEl: { isShown: vi.fn(() => true) }, + }; + memoObject(popupKey, popup); + (confirm as unknown as { popupKeys: Set }).popupKeys.add(popupKey); + + events.emitEvent(EVENT_PLUGIN_UNLOADED); + + expect(popup.hide).toHaveBeenCalledOnce(); + expect(retrieveMemoObject(popupKey)).toBe(false); + }); +}); diff --git a/src/modules/services/ObsidianDatabaseService.ts b/src/modules/services/ObsidianDatabaseService.ts index 39759f9d..2768a601 100644 --- a/src/modules/services/ObsidianDatabaseService.ts +++ b/src/modules/services/ObsidianDatabaseService.ts @@ -1,8 +1,8 @@ import { initializeStores } from "@/common/stores"; // import { InjectableDatabaseService } from "@/lib/src/services/implements/injectable/InjectableDatabaseService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import { DatabaseService, type DatabaseServiceDependencies } from "@lib/services/base/DatabaseService.ts"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import { DatabaseService, type DatabaseServiceDependencies } from "@vrtmrz/livesync-commonlib/compat/services/base/DatabaseService"; export class ObsidianDatabaseService extends DatabaseService { private __onOpenDatabase(vaultName: string) { diff --git a/src/modules/services/ObsidianNoticeGroups.ts b/src/modules/services/ObsidianNoticeGroups.ts new file mode 100644 index 00000000..a59105ec --- /dev/null +++ b/src/modules/services/ObsidianNoticeGroups.ts @@ -0,0 +1,51 @@ +import { KeyedNoticeGroupManager } from "@vrtmrz/obsidian-plugin-kit/notice"; + +export interface ObsidianNoticeGroupItem { + message: string; + action?: { + label: string; + onSelect: () => void; + }; +} + +interface KeyedNoticeGroupDriver { + setItem(groupKey: string, itemKey: string, item: ObsidianNoticeGroupItem): unknown; + finish(groupKey: string, options?: { durationMs?: number | false }): boolean; + removeItem(groupKey: string, itemKey: string): boolean; + hide(groupKey: string): boolean; + dispose(): void; +} + +/** Obsidian-owned interactive Notice capability exposed through the application Context. */ +export interface ObsidianNoticeGroups { + setItem(groupKey: string, itemKey: string, item: ObsidianNoticeGroupItem): void; + finish(groupKey: string, options?: { durationMs?: number | false }): boolean; + removeItem(groupKey: string, itemKey: string): boolean; + hide(groupKey: string): boolean; + dispose(): void; +} + +/** Adapts Fancy Kit grouped Notices without exposing Obsidian Notice instances to features. */ +export class ObsidianNoticeGroupManager implements ObsidianNoticeGroups { + constructor(private readonly manager: KeyedNoticeGroupDriver = new KeyedNoticeGroupManager()) {} + + setItem(groupKey: string, itemKey: string, item: ObsidianNoticeGroupItem): void { + this.manager.setItem(groupKey, itemKey, item); + } + + finish(groupKey: string, options?: { durationMs?: number | false }): boolean { + return this.manager.finish(groupKey, options); + } + + removeItem(groupKey: string, itemKey: string): boolean { + return this.manager.removeItem(groupKey, itemKey); + } + + hide(groupKey: string): boolean { + return this.manager.hide(groupKey); + } + + dispose(): void { + this.manager.dispose(); + } +} diff --git a/src/modules/services/ObsidianNoticeGroups.unit.spec.ts b/src/modules/services/ObsidianNoticeGroups.unit.spec.ts new file mode 100644 index 00000000..3af2893d --- /dev/null +++ b/src/modules/services/ObsidianNoticeGroups.unit.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@vrtmrz/obsidian-plugin-kit/notice", () => ({ + KeyedNoticeGroupManager: class KeyedNoticeGroupManager {}, +})); + +import { ObsidianNoticeGroupManager } from "./ObsidianNoticeGroups"; + +describe("ObsidianNoticeGroupManager", () => { + it("keeps Fancy Kit rendering behind the Context-owned capability", () => { + const driver = { + setItem: vi.fn(), + finish: vi.fn(() => true), + removeItem: vi.fn(() => true), + hide: vi.fn(() => true), + dispose: vi.fn(), + }; + const groups = new ObsidianNoticeGroupManager(driver); + const item = { + message: "Complete", + action: { label: "Review", onSelect: vi.fn() }, + }; + + groups.setItem("integrity", "result", item); + expect(driver.setItem).toHaveBeenCalledWith("integrity", "result", item); + expect(groups.finish("integrity", { durationMs: 1_000 })).toBe(true); + expect(groups.removeItem("integrity", "result")).toBe(true); + expect(groups.hide("integrity")).toBe(true); + groups.dispose(); + expect(driver.dispose).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/modules/services/ObsidianPathService.ts b/src/modules/services/ObsidianPathService.ts index ed479e03..12dee2b0 100644 --- a/src/modules/services/ObsidianPathService.ts +++ b/src/modules/services/ObsidianPathService.ts @@ -1,6 +1,6 @@ -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { normalizePath } from "@/deps"; -import { PathService } from "@lib/services/base/PathService"; +import { PathService } from "@vrtmrz/livesync-commonlib/compat/services/base/PathService"; import { type BASE_IS_NEW, @@ -11,7 +11,7 @@ import { compareFileFreshness, isMarkedAsSameChanges, } from "@/common/utils"; -import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@lib/common/types"; +import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types"; export class ObsidianPathService extends PathService { override markChangesAreSame( old: UXFileInfo | AnyEntry | FilePathWithPrefix, diff --git a/src/modules/services/ObsidianServiceContext.ts b/src/modules/services/ObsidianServiceContext.ts new file mode 100644 index 00000000..694b336e --- /dev/null +++ b/src/modules/services/ObsidianServiceContext.ts @@ -0,0 +1,22 @@ +import type ObsidianLiveSyncPlugin from "@/main"; +import type { App, Plugin } from "@/deps"; +import { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { eventHub } from "@/common/events"; +import { translateLiveSyncMessage } from "@/common/translation"; +import type { ObsidianNoticeGroups } from "./ObsidianNoticeGroups"; + +/** Host capabilities owned by one Self-hosted LiveSync plug-in instance. */ +export class ObsidianServiceContext extends ServiceContext { + app: App; + plugin: Plugin; + liveSyncPlugin: ObsidianLiveSyncPlugin; + readonly noticeGroups: ObsidianNoticeGroups; + + constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin, noticeGroups: ObsidianNoticeGroups) { + super({ events: eventHub, translate: translateLiveSyncMessage }); + this.app = app; + this.plugin = plugin; + this.liveSyncPlugin = liveSyncPlugin; + this.noticeGroups = noticeGroups; + } +} diff --git a/src/modules/services/ObsidianServiceContext.unit.spec.ts b/src/modules/services/ObsidianServiceContext.unit.spec.ts new file mode 100644 index 00000000..d95f9306 --- /dev/null +++ b/src/modules/services/ObsidianServiceContext.unit.spec.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +import { eventHub } from "@/common/events"; +import { translateLiveSyncMessage } from "@/common/translation"; +import { observeServiceContext } from "../../../test/contracts/serviceContext"; +import { ObsidianServiceContext } from "./ObsidianServiceContext"; + +const TRANSLATION_KEY = "Replicator.Message.InitialiseFatalError"; + +describe("ObsidianServiceContext contract", () => { + it("preserves the plug-in capabilities and host-neutral API results", () => { + type Parameters = ConstructorParameters; + const app = {} as Parameters[0]; + const plugin = {} as Parameters[1]; + const liveSyncPlugin = {} as Parameters[2]; + const noticeGroups = {} as Parameters[3]; + const context = new ObsidianServiceContext(app, plugin, liveSyncPlugin, noticeGroups); + + expect(observeServiceContext(context, TRANSLATION_KEY)).toEqual({ + translation: translateLiveSyncMessage(TRANSLATION_KEY), + receivedEvents: ["context-contract-event"], + }); + expect(context.events).toBe(eventHub); + expect(context.app).toBe(app); + expect(context.plugin).toBe(plugin); + expect(context.liveSyncPlugin).toBe(liveSyncPlugin); + expect(context.noticeGroups).toBe(noticeGroups); + }); +}); diff --git a/src/modules/services/ObsidianServiceHub.ts b/src/modules/services/ObsidianServiceHub.ts index 917637a2..5cfd7fde 100644 --- a/src/modules/services/ObsidianServiceHub.ts +++ b/src/modules/services/ObsidianServiceHub.ts @@ -1,6 +1,6 @@ -import { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { ServiceInstances } from "@lib/services/ServiceHub"; +import { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub"; +import { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import type { ServiceInstances } from "@vrtmrz/livesync-commonlib/compat/services/ServiceHub"; import type ObsidianLiveSyncPlugin from "@/main"; import { ObsidianConflictService, @@ -23,12 +23,17 @@ import { ObsidianPathService } from "./ObsidianPathService"; import { ObsidianVaultService } from "./ObsidianVaultService"; import { ObsidianUIService } from "./ObsidianUIService"; import { createScreenWakeLockManager } from "octagonal-wheels/browser/wakeLock"; +import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser"; +import { OpenKeyValueDatabase } from "@/common/KeyValueDB"; +import { ObsidianNoticeGroupManager } from "./ObsidianNoticeGroups"; +import { setLang } from "@/common/translation"; // InjectableServiceHub export class ObsidianServiceHub extends InjectableServiceHub { constructor(plugin: ObsidianLiveSyncPlugin) { - const context = new ObsidianServiceContext(plugin.app, plugin, plugin); + const noticeGroups = new ObsidianNoticeGroupManager(); + const context = new ObsidianServiceContext(plugin.app, plugin, plugin, noticeGroups); const API = new ObsidianAPIService(context); const conflict = new ObsidianConflictService(context); @@ -38,11 +43,13 @@ export class ObsidianServiceHub extends InjectableServiceHub { await screenWakeLock.dispose(); + noticeGroups.dispose(); return true; }); const database = new ObsidianDatabaseService(context, { + pouchDB: PouchDB, path: path, vault: vault, setting: setting, API: API, }); const keyValueDB = new ObsidianKeyValueDBService(context, { + openKeyValueDatabase: OpenKeyValueDatabase, appLifecycle: appLifecycle, databaseEvents: databaseEvents, vault: vault, diff --git a/src/modules/services/ObsidianServices.ts b/src/modules/services/ObsidianServices.ts index c6812b9e..2873e014 100644 --- a/src/modules/services/ObsidianServices.ts +++ b/src/modules/services/ObsidianServices.ts @@ -1,15 +1,15 @@ -import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService"; -import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService"; -import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService"; -import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService"; -import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService"; -import { InjectableReplicatorService } from "@lib/services/implements/injectable/InjectableReplicatorService"; -import { InjectableTestService } from "@lib/services/implements/injectable/InjectableTestService"; -import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService"; -import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts"; -import { KeyValueDBService } from "@lib/services/base/KeyValueDBService"; -import { ControlService } from "@lib/services/base/ControlService"; +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 { 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 { ConfigServiceBrowserCompat } from "@vrtmrz/livesync-commonlib/compat/services/implements/browser/ConfigServiceBrowserCompat"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import { KeyValueDBService } from "@vrtmrz/livesync-commonlib/compat/services/base/KeyValueDBService"; +import { ControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/ControlService"; export class ObsidianDatabaseEventService extends InjectableDatabaseEventService {} diff --git a/src/modules/services/ObsidianSettingService.ts b/src/modules/services/ObsidianSettingService.ts index f3a32eef..a934ebd1 100644 --- a/src/modules/services/ObsidianSettingService.ts +++ b/src/modules/services/ObsidianSettingService.ts @@ -1,19 +1,29 @@ -import { compatGlobal } from "@lib/common/coreEnvFunctions"; -import { type ObsidianLiveSyncSettings } from "@lib/common/types"; -import { EVENT_REQUEST_RELOAD_SETTING_TAB, EVENT_SETTING_SAVED } from "@lib/events/coreEvents"; -import { eventHub } from "@lib/hub/hub"; -import { SettingService, type SettingServiceDependencies } from "@lib/services/base/SettingService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; +import { type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { + EVENT_REQUEST_RELOAD_SETTING_TAB, + EVENT_SETTING_SAVED, +} from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { + SettingService, + type SettingServiceDependencies, +} from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; + +export function normaliseObsidianSettingsData(data: unknown): ObsidianLiveSyncSettings | undefined { + if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined; + return data as ObsidianLiveSyncSettings; +} export class ObsidianSettingService extends SettingService { constructor(context: T, dependencies: SettingServiceDependencies) { super(context, dependencies); this.onSettingSaved.addHandler((settings) => { - eventHub.emitEvent(EVENT_SETTING_SAVED, settings); + this.context.events.emitEvent(EVENT_SETTING_SAVED, settings); return Promise.resolve(true); }); this.onSettingLoaded.addHandler((settings) => { - eventHub.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB); + this.context.events.emitEvent(EVENT_REQUEST_RELOAD_SETTING_TAB); return Promise.resolve(true); }); } @@ -34,6 +44,6 @@ export class ObsidianSettingService extends Se return await this.context.liveSyncPlugin.saveData(data); } protected override async loadData(): Promise { - return await this.context.liveSyncPlugin.loadData(); + return normaliseObsidianSettingsData(await this.context.liveSyncPlugin.loadData()); } } diff --git a/src/modules/services/ObsidianSettingService.unit.spec.ts b/src/modules/services/ObsidianSettingService.unit.spec.ts new file mode 100644 index 00000000..9175c931 --- /dev/null +++ b/src/modules/services/ObsidianSettingService.unit.spec.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { normaliseObsidianSettingsData } from "./ObsidianSettingService.ts"; + +describe("normaliseObsidianSettingsData", () => { + it("maps Obsidian's missing data value to Commonlib's new-Vault input", () => { + expect(normaliseObsidianSettingsData(null)).toBeUndefined(); + }); + + it("preserves stored settings", () => { + const settings = { isConfigured: false } as ObsidianLiveSyncSettings; + + expect(normaliseObsidianSettingsData(settings)).toBe(settings); + }); +}); diff --git a/src/modules/services/ObsidianUIService.ts b/src/modules/services/ObsidianUIService.ts index 45975af2..c5abf23f 100644 --- a/src/modules/services/ObsidianUIService.ts +++ b/src/modules/services/ObsidianUIService.ts @@ -1,11 +1,11 @@ -import type { ConfigService } from "@lib/services/base/ConfigService"; -import type { AppLifecycleService } from "@lib/services/base/AppLifecycleService"; -import type { ReplicatorService } from "@lib/services/base/ReplicatorService"; -import { UIService } from "@lib/services/implements/base/UIService"; -import { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; +import type { ConfigService } from "@vrtmrz/livesync-commonlib/compat/services/base/ConfigService"; +import type { AppLifecycleService } from "@vrtmrz/livesync-commonlib/compat/services/base/AppLifecycleService"; +import type { ReplicatorService } from "@vrtmrz/livesync-commonlib/compat/services/base/ReplicatorService"; +import { UIService } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/UIService"; +import { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; import { ObsidianSvelteDialogManager } from "./SvelteDialogObsidian"; -import DialogToCopy from "@lib/UI/dialogues/DialogueToCopy.svelte"; -import type { IAPIService, IControlService } from "@lib/services/base/IService"; +import DialogToCopy from "@/modules/services/LiveSyncUI/dialogues/DialogueToCopy.svelte"; +import type { IAPIService, IControlService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService"; export type ObsidianUIServiceDependencies = { appLifecycle: AppLifecycleService; config: ConfigService; @@ -28,7 +28,6 @@ export class ObsidianUIService extends UIService { control: dependents.control, }); super(context, { - appLifecycle: dependents.appLifecycle, dialogManager: obsidianSvelteDialogManager, APIService: dependents.APIService, }); diff --git a/src/modules/services/ObsidianVaultService.ts b/src/modules/services/ObsidianVaultService.ts index d86c7b19..e3bd3a91 100644 --- a/src/modules/services/ObsidianVaultService.ts +++ b/src/modules/services/ObsidianVaultService.ts @@ -1,7 +1,7 @@ import { getPathFromTFile, isValidPath } from "@/common/utils"; -import { InjectableVaultService } from "@lib/services/implements/injectable/InjectableVaultService"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import type { FilePath } from "@lib/common/types"; +import { InjectableVaultService } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableVaultService"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types"; declare module "obsidian" { interface DataAdapter { diff --git a/src/modules/services/SvelteDialogObsidian.ts b/src/modules/services/SvelteDialogObsidian.ts index 95c9ba23..63e353a1 100644 --- a/src/modules/services/SvelteDialogObsidian.ts +++ b/src/modules/services/SvelteDialogObsidian.ts @@ -5,9 +5,9 @@ import { SvelteDialogMixIn, type ComponentHasResult, type SvelteDialogManagerDependencies, -} from "@lib/services/implements/base/SvelteDialog"; -import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; -import DialogHost from "@lib/UI/DialogHost.svelte"; +} from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; +import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; +import DialogHost from "@/modules/services/LiveSyncUI/DialogHost.svelte"; export const SvelteDialogBase = SvelteDialogMixIn(Modal, DialogHost); export class SvelteDialogObsidian< T, @@ -23,6 +23,11 @@ export class SvelteDialogObsidian< super(context.app); this.initDialog(context, dependents, component, initialData); } + + override onOpen(): void { + super.onOpen(); + this.contentEl.closest(".modal-container")?.classList.add("livesync-svelte-dialog-container"); + } } export class ObsidianSvelteDialogManager extends SvelteDialogManagerBase { diff --git a/src/rabinKarpBom.unit.spec.ts b/src/rabinKarpBom.unit.spec.ts index 56596aa1..67e4781f 100644 --- a/src/rabinKarpBom.unit.spec.ts +++ b/src/rabinKarpBom.unit.spec.ts @@ -1,4 +1,4 @@ -import { splitPiecesRabinKarp } from "@lib/string_and_binary/chunks.ts"; +import { splitPiecesRabinKarp } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/chunks"; import { describe, expect, it } from "vitest"; describe("Rabin-Karp text splitting", () => { diff --git a/src/serviceFeatures/compatibilityReview.ts b/src/serviceFeatures/compatibilityReview.ts new file mode 100644 index 00000000..8fc85f3e --- /dev/null +++ b/src/serviceFeatures/compatibilityReview.ts @@ -0,0 +1,186 @@ +import { fireAndForget } from "octagonal-wheels/promises"; +import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { LiveSyncCore } from "@/main.ts"; +import { + COMPATIBILITY_PAUSE_SETTING_MESSAGE, + DATABASE_COMPATIBILITY_VERSION_KEY, + evaluateCompatibilityPause, + legacyDatabaseCompatibilityVersionKey, + type CompatibilityPause, +} from "@/common/databaseCompatibility.ts"; + +export type CompatibilityReviewSummaryAction = "details" | "resume" | "keep-paused" | false; +export type CompatibilityReviewDetailsAction = "back" | false; + +// Explicit flag-file recovery runs at priorities 5, 10, and 20. Present the +// compatibility review only after those operations have completed; a recovery +// handler which stops start-up also prevents this dialogue from competing with it. +export const COMPATIBILITY_REVIEW_LAYOUT_PRIORITY = 30; + +export interface CompatibilityReviewUi { + showSummary(pause: CompatibilityPause): Promise; + showDetails(pause: CompatibilityPause): Promise; + showReminder(openReview: () => void): void; + clearReminder(): void; +} + +export class CompatibilityReviewController { + private pause: CompatibilityPause | undefined; + private activeReview: Promise | undefined; + private _initialised = false; + private disposed = false; + + constructor( + private readonly core: LiveSyncCore, + private readonly ui: CompatibilityReviewUi, + private readonly currentVersion: number = VER + ) {} + + get pendingPause(): CompatibilityPause | undefined { + return this.pause; + } + + get initialised(): boolean { + return this._initialised; + } + + private readAcknowledgedVersion(): string | null { + const setting = this.core.services.setting; + const currentMarker = setting.getSmallConfig(DATABASE_COMPATIBILITY_VERSION_KEY); + if (currentMarker) return currentMarker; + + const legacyKey = legacyDatabaseCompatibilityVersionKey(this.core.services.vault.getVaultName()); + const legacyMarker = setting.getDeviceLocalConfig(legacyKey); + if (!legacyMarker) return null; + + setting.setSmallConfig(DATABASE_COMPATIBILITY_VERSION_KEY, legacyMarker); + setting.deleteDeviceLocalConfig(legacyKey); + return legacyMarker; + } + + async initialise(): Promise { + if (this.disposed) return true; + const setting = this.core.services.setting; + const settings = setting.currentSettings(); + const migrationState = setting.getSettingsMigrationState(); + + // An existing unconfigured Vault cannot replicate, so a database + // compatibility pause would only compete with onboarding and persist + // a misleading sync warning. Do not acknowledge the missing marker: + // activation on a later start must evaluate the same state again. + // Genuinely new Vaults still initialise their marker below. + if (settings.isConfigured !== true && migrationState?.isNewVault !== true) { + this.pause = undefined; + this.ui.clearReminder(); + this._initialised = true; + return true; + } + + const acknowledgedVersion = this.readAcknowledgedVersion(); + const evaluation = evaluateCompatibilityPause({ + acknowledgedVersion, + currentVersion: this.currentVersion, + migrationState, + legacyReviewMessage: settings.versionUpFlash, + }); + + this.pause = evaluation.pause; + if (evaluation.initialiseAcknowledgedVersion) { + setting.setSmallConfig(DATABASE_COMPATIBILITY_VERSION_KEY, `${this.currentVersion}`); + this._initialised = true; + return true; + } + if (!this.pause) { + this._initialised = true; + return true; + } + + if (settings.versionUpFlash === "") { + settings.versionUpFlash = COMPATIBILITY_PAUSE_SETTING_MESSAGE; + await setting.saveSettingData(); + } + this._initialised = true; + return true; + } + + private async acknowledge(): Promise { + if (!this.pause?.resumable) return; + const setting = this.core.services.setting; + const settings = setting.currentSettings(); + const previousMessage = settings.versionUpFlash; + settings.versionUpFlash = ""; + try { + await setting.saveSettingData(); + } catch (error) { + settings.versionUpFlash = previousMessage || COMPATIBILITY_PAUSE_SETTING_MESSAGE; + throw error; + } + + setting.setSmallConfig(DATABASE_COMPATIBILITY_VERSION_KEY, `${this.currentVersion}`); + const legacyKey = legacyDatabaseCompatibilityVersionKey(this.core.services.vault.getVaultName()); + setting.deleteDeviceLocalConfig(legacyKey); + this.pause = undefined; + this.ui.clearReminder(); + await this.core.services.control.applySettings(); + } + + private async runReview(): Promise { + while (this.pause) { + const action = await this.ui.showSummary(this.pause); + if (action === "details") { + const detailsAction = await this.ui.showDetails(this.pause); + if (detailsAction === "back") continue; + break; + } + if (action === "resume" && this.pause.resumable) { + await this.acknowledge(); + return; + } + break; + } + if (this.pause && !this.disposed) { + this.ui.showReminder(() => { + fireAndForget(() => this.openReview()); + }); + } + } + + openReview(): Promise { + if (this.disposed || !this.pause) return Promise.resolve(); + if (this.activeReview) return this.activeReview; + this.ui.clearReminder(); + this.activeReview = this.runReview().finally(() => { + this.activeReview = undefined; + }); + return this.activeReview; + } + + dispose(): void { + this.disposed = true; + this.pause = undefined; + this.ui.clearReminder(); + } +} + +export function useCompatibilityReview(core: LiveSyncCore, ui: CompatibilityReviewUi): CompatibilityReviewController { + const controller = new CompatibilityReviewController(core, ui); + core.services.appLifecycle.onSettingLoaded.addHandler(() => controller.initialise()); + core.services.appLifecycle.onLayoutReady.addHandler(() => { + fireAndForget(() => controller.openReview()); + return Promise.resolve(true); + }, COMPATIBILITY_REVIEW_LAYOUT_PRIORITY); + core.services.appLifecycle.onUnload.addHandler(() => { + controller.dispose(); + return Promise.resolve(true); + }); + core.services.API.addCommand({ + id: "livesync-review-compatibility-pause", + name: "Review why synchronisation is paused", + checkCallback: (checking) => { + if (!controller.pendingPause) return false; + if (!checking) fireAndForget(() => controller.openReview()); + return true; + }, + }); + return controller; +} diff --git a/src/serviceFeatures/compatibilityReview.unit.spec.ts b/src/serviceFeatures/compatibilityReview.unit.spec.ts new file mode 100644 index 00000000..3155477f --- /dev/null +++ b/src/serviceFeatures/compatibilityReview.unit.spec.ts @@ -0,0 +1,221 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + COMPATIBILITY_PAUSE_SETTING_MESSAGE, + DATABASE_COMPATIBILITY_VERSION_KEY, + legacyDatabaseCompatibilityVersionKey, +} from "@/common/databaseCompatibility.ts"; +import { + CompatibilityReviewController, + type CompatibilityReviewUi, + useCompatibilityReview, +} from "./compatibilityReview.ts"; + +function migrationState(overrides: Record = {}) { + return { + sourceVersion: 2, + targetVersion: 2, + isNewVault: false, + isFromFutureSchema: false, + changed: false, + requiresSyncReview: false, + reviewReasons: [], + ...overrides, + }; +} + +function createFixture( + options: { + marker?: string | null; + legacyMarker?: string | null; + versionUpFlash?: string; + isConfigured?: boolean; + migration?: Record; + } = {} +) { + const local = new Map(); + if (options.marker !== undefined && options.marker !== null) { + local.set(DATABASE_COMPATIBILITY_VERSION_KEY, options.marker); + } + const legacyKey = legacyDatabaseCompatibilityVersionKey("Test Vault"); + if (options.legacyMarker !== undefined && options.legacyMarker !== null) { + local.set(legacyKey, options.legacyMarker); + } + const settings = { + versionUpFlash: options.versionUpFlash ?? "", + isConfigured: options.isConfigured ?? true, + }; + const saveSettingData = vi.fn().mockResolvedValue(undefined); + const applySettings = vi.fn().mockResolvedValue(true); + const setting = { + currentSettings: () => settings, + getSettingsMigrationState: () => migrationState(options.migration), + getSmallConfig: (key: string) => local.get(key) ?? "", + setSmallConfig: (key: string, value: string) => local.set(key, value), + getDeviceLocalConfig: (key: string) => local.get(key) ?? null, + deleteDeviceLocalConfig: (key: string) => local.delete(key), + saveSettingData, + }; + const core = { + services: { + setting, + vault: { getVaultName: () => "Test Vault" }, + control: { applySettings }, + }, + } as never; + const ui: CompatibilityReviewUi = { + showSummary: vi.fn().mockResolvedValue("keep-paused"), + showDetails: vi.fn().mockResolvedValue(false), + showReminder: vi.fn(), + clearReminder: vi.fn(), + }; + const controller = new CompatibilityReviewController(core, ui, 12); + return { controller, ui, local, legacyKey, settings, saveSettingData, applySettings }; +} + +describe("compatibility review controller", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("initialises the acknowledged version for a new Vault without showing a pause", async () => { + const fixture = createFixture({ marker: null, isConfigured: false, migration: { isNewVault: true } }); + + expect(fixture.controller.initialised).toBe(false); + + await expect(fixture.controller.initialise()).resolves.toBe(true); + + expect(fixture.controller.initialised).toBe(true); + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("12"); + expect(fixture.controller.pendingPause).toBeUndefined(); + expect(fixture.saveSettingData).not.toHaveBeenCalled(); + }); + + it("defers a missing database marker while the Vault remains unconfigured", async () => { + const fixture = createFixture({ marker: null, isConfigured: false }); + + await expect(fixture.controller.initialise()).resolves.toBe(true); + + expect(fixture.controller.pendingPause).toBeUndefined(); + expect(fixture.settings.versionUpFlash).toBe(""); + expect(fixture.local.has(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe(false); + expect(fixture.saveSettingData).not.toHaveBeenCalled(); + + fixture.settings.isConfigured = true; + await expect(fixture.controller.initialise()).resolves.toBe(true); + + expect(fixture.controller.pendingPause?.reasons).toContainEqual({ + source: "database-version", + state: "missing", + currentVersion: 12, + resumable: true, + }); + expect(fixture.settings.versionUpFlash).toBe(COMPATIBILITY_PAUSE_SETTING_MESSAGE); + expect(fixture.local.has(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe(false); + expect(fixture.saveSettingData).toHaveBeenCalledOnce(); + }); + + it("preserves preferences and advances the marker only after an upgrade review is resumed", async () => { + const fixture = createFixture({ marker: "11" }); + vi.mocked(fixture.ui.showSummary).mockResolvedValue("resume"); + + await fixture.controller.initialise(); + + expect(fixture.settings.versionUpFlash).toBe(COMPATIBILITY_PAUSE_SETTING_MESSAGE); + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("11"); + expect(fixture.saveSettingData).toHaveBeenCalledTimes(1); + + await fixture.controller.openReview(); + + expect(fixture.settings.versionUpFlash).toBe(""); + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("12"); + expect(fixture.saveSettingData).toHaveBeenCalledTimes(2); + expect(fixture.applySettings).toHaveBeenCalledOnce(); + expect(fixture.controller.pendingPause).toBeUndefined(); + expect(fixture.ui.clearReminder).toHaveBeenCalled(); + }); + + it("does not allow a downgrade pause to be resumed", async () => { + const fixture = createFixture({ marker: "13" }); + vi.mocked(fixture.ui.showSummary).mockResolvedValue("resume"); + + await fixture.controller.initialise(); + await fixture.controller.openReview(); + + expect(fixture.controller.pendingPause?.resumable).toBe(false); + expect(fixture.settings.versionUpFlash).toBe(COMPATIBILITY_PAUSE_SETTING_MESSAGE); + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("13"); + expect(fixture.applySettings).not.toHaveBeenCalled(); + expect(fixture.ui.showReminder).toHaveBeenCalledOnce(); + }); + + it("returns from details to the reason dialogue and leaves a persistent reminder", async () => { + const fixture = createFixture({ marker: "11" }); + vi.mocked(fixture.ui.showSummary).mockResolvedValueOnce("details").mockResolvedValueOnce("keep-paused"); + vi.mocked(fixture.ui.showDetails).mockResolvedValue("back"); + + await fixture.controller.initialise(); + await fixture.controller.openReview(); + + expect(fixture.ui.showSummary).toHaveBeenCalledTimes(2); + expect(fixture.ui.showDetails).toHaveBeenCalledOnce(); + expect(fixture.ui.showReminder).toHaveBeenCalledOnce(); + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("11"); + }); + + it("migrates the old Vault-scoped marker to Commonlib device-local storage", async () => { + const fixture = createFixture({ legacyMarker: "11" }); + + await fixture.controller.initialise(); + + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("11"); + expect(fixture.local.has(fixture.legacyKey)).toBe(false); + expect(fixture.controller.pendingPause).toBeDefined(); + }); + + it("restores the runtime gate if persisting an acknowledgement fails", async () => { + const fixture = createFixture({ marker: "11" }); + vi.mocked(fixture.ui.showSummary).mockResolvedValue("resume"); + await fixture.controller.initialise(); + fixture.saveSettingData.mockRejectedValueOnce(new Error("save failed")); + + await expect(fixture.controller.openReview()).rejects.toThrow("save failed"); + + expect(fixture.settings.versionUpFlash).toBe(COMPATIBILITY_PAUSE_SETTING_MESSAGE); + expect(fixture.local.get(DATABASE_COMPATIBILITY_VERSION_KEY)).toBe("11"); + expect(fixture.applySettings).not.toHaveBeenCalled(); + }); + + it("does not open a delayed review after the controller has been disposed", async () => { + const fixture = createFixture({ marker: "11" }); + await fixture.controller.initialise(); + + fixture.controller.dispose(); + await fixture.controller.openReview(); + + expect(fixture.controller.pendingPause).toBeUndefined(); + expect(fixture.ui.showSummary).not.toHaveBeenCalled(); + expect(fixture.ui.clearReminder).toHaveBeenCalledOnce(); + }); + + it("runs the review after the ordered red flag recovery handlers", () => { + const onSettingLoaded = { addHandler: vi.fn() }; + const onLayoutReady = { addHandler: vi.fn() }; + const onUnload = { addHandler: vi.fn() }; + const core = { + services: { + appLifecycle: { onSettingLoaded, onLayoutReady, onUnload }, + API: { addCommand: vi.fn() }, + }, + } as never; + const ui: CompatibilityReviewUi = { + showSummary: vi.fn(), + showDetails: vi.fn(), + showReminder: vi.fn(), + clearReminder: vi.fn(), + }; + + useCompatibilityReview(core, ui); + + expect(onLayoutReady.addHandler).toHaveBeenCalledWith(expect.any(Function), 30); + }); +}); diff --git a/src/serviceFeatures/compatibilityReviewMarkdown.ts b/src/serviceFeatures/compatibilityReviewMarkdown.ts new file mode 100644 index 00000000..c4730b3b --- /dev/null +++ b/src/serviceFeatures/compatibilityReviewMarkdown.ts @@ -0,0 +1,54 @@ +import type { CompatibilityPause, CompatibilityPauseReason } from "@/common/databaseCompatibility.ts"; + +export function compatibilityReviewSummaryMarkdown(pause: CompatibilityPause): string { + const action = !pause.resumable + ? "This installation cannot safely acknowledge the detected state. Update Self-hosted LiveSync before attempting to synchronise again." + : "Before resuming, review the compatibility details and update Self-hosted LiveSync on every device which uses this remote database."; + return `Remote synchronisation is paused on this device because its compatibility state requires attention. + +${action} + +Your automatic synchronisation preferences have not been changed. Closing this dialogue keeps synchronisation paused.`; +} + +function reasonMarkdown(reason: CompatibilityPauseReason): string { + if (reason.source === "database-version") { + if (reason.state === "upgrade") { + return `- The last acknowledged internal database version was **${reason.acknowledgedVersion}** and this installation uses **${reason.currentVersion}**.`; + } + if (reason.state === "downgrade") { + return `- This installation uses internal database version **${reason.currentVersion}**, but this device previously acknowledged newer version **${reason.acknowledgedVersion}**. An older installation must not resume synchronisation.`; + } + if (reason.state === "missing") { + return `- No previously acknowledged internal database version was found for this existing Vault. This can happen when a Vault is copied or restored, or when it is opened with a new Obsidian profile. This installation uses version **${reason.currentVersion}**. An empty local database does not mean that it is safe to resume automatically.`; + } + return `- The saved internal database version marker is invalid. This installation uses version **${reason.currentVersion}**.`; + } + if (reason.source === "settings-schema") { + if (reason.isFromFutureSchema) { + return `- The saved settings use schema **${reason.sourceVersion}**, which is newer than schema **${reason.currentVersion}** supported by this installation.`; + } + return `- The settings were migrated from schema **${reason.sourceVersion}** to **${reason.currentVersion}** and require review before synchronisation resumes.`; + } + const escapedMessage = reason.message.replace(/[\\`*_{}[\]()<>#+.!|-]/gu, "\\$&"); + return `- An earlier compatibility review remains pending: ${escapedMessage}`; +} + +export function compatibilityReviewDetailsMarkdown(pause: CompatibilityPause): string { + const resolution = !pause.resumable + ? "Install a compatible current version of Self-hosted LiveSync. This pause cannot be dismissed by the current installation." + : "After all devices have been updated, return to the compatibility review summary and explicitly resume synchronisation. The current internal version will only then be recorded as acknowledged."; + return `## Why synchronisation is paused + +${pause.reasons.map(reasonMarkdown).join("\n")} + +## What the pause changes + +- Remote replication is blocked before work begins. +- Your saved automatic synchronisation preferences remain unchanged. +- Closing either dialogue leaves the safety gate active. + +## What to do next + +${resolution}`; +} diff --git a/src/serviceFeatures/compatibilityReviewObsidian.ts b/src/serviceFeatures/compatibilityReviewObsidian.ts new file mode 100644 index 00000000..623b111d --- /dev/null +++ b/src/serviceFeatures/compatibilityReviewObsidian.ts @@ -0,0 +1,82 @@ +import { Notice } from "@/deps.ts"; +import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm"; +import type { CompatibilityPause } from "@/common/databaseCompatibility.ts"; +import type { + CompatibilityReviewDetailsAction, + CompatibilityReviewSummaryAction, + CompatibilityReviewUi, +} from "./compatibilityReview.ts"; +import { + compatibilityReviewDetailsMarkdown, + compatibilityReviewSummaryMarkdown, +} from "./compatibilityReviewMarkdown.ts"; + +const REVIEW_DETAILS = "Review compatibility details"; +const KEEP_PAUSED = "Keep synchronisation paused"; +const RESUME = "Resume synchronisation"; +const BACK = "Back to compatibility review"; + +export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi { + private reminder: Notice | undefined; + + constructor(private readonly confirm: Confirm) {} + + async showSummary(pause: CompatibilityPause): Promise { + const buttons = !pause.resumable + ? ([REVIEW_DETAILS, KEEP_PAUSED] as const) + : ([REVIEW_DETAILS, RESUME, KEEP_PAUSED] as const); + const result = await this.confirm.confirmWithMessage( + "Synchronisation paused for compatibility review", + compatibilityReviewSummaryMarkdown(pause), + [...buttons], + KEEP_PAUSED, + undefined, + "vertical" + ); + if (result === REVIEW_DETAILS) return "details"; + if (result === RESUME) return "resume"; + if (result === KEEP_PAUSED) return "keep-paused"; + return false; + } + + async showDetails(pause: CompatibilityPause): Promise { + const result = await this.confirm.confirmWithMessage( + "Compatibility review details", + compatibilityReviewDetailsMarkdown(pause), + [BACK], + BACK, + undefined, + "vertical" + ); + if (result === BACK) return "back"; + return false; + } + + showReminder(openReview: () => void): void { + this.clearReminder(); + let reminderAnchor: HTMLAnchorElement | undefined; + const fragment = createFragment((documentFragment) => { + documentFragment.createSpan({ + text: "Self-hosted LiveSync has paused remote synchronisation for compatibility review. ", + }); + documentFragment.createEl("a", { text: "Review why" }, (anchor) => { + reminderAnchor = anchor; + anchor.addEventListener("click", (event) => { + event.preventDefault(); + openReview(); + }); + }); + }); + this.reminder = new Notice(fragment, 0); + reminderAnchor?.closest(".notice")?.classList.add("livesync-compatibility-review-notice"); + } + + clearReminder(): void { + this.reminder?.hide(); + this.reminder = undefined; + } +} + +export function createObsidianCompatibilityReviewUi(confirm: Confirm): CompatibilityReviewUi { + return new ObsidianCompatibilityReviewUi(confirm); +} diff --git a/src/serviceFeatures/compatibilityReviewObsidian.unit.spec.ts b/src/serviceFeatures/compatibilityReviewObsidian.unit.spec.ts new file mode 100644 index 00000000..17a961a0 --- /dev/null +++ b/src/serviceFeatures/compatibilityReviewObsidian.unit.spec.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; +import type { CompatibilityPause } from "@/common/databaseCompatibility.ts"; +import { compatibilityReviewDetailsMarkdown } from "./compatibilityReviewMarkdown.ts"; +import { ObsidianCompatibilityReviewUi } from "./compatibilityReviewObsidian.ts"; + +vi.mock("@/deps.ts", () => ({ + Notice: class { + hide() {} + }, +})); + +const resumablePause: CompatibilityPause = { + resumable: true, + reasons: [ + { + source: "database-version", + state: "upgrade", + acknowledgedVersion: 11, + currentVersion: 12, + resumable: true, + }, + ], +}; + +describe("Obsidian compatibility review", () => { + it("explains why a configured Vault can be missing its device-local acknowledgement", async () => { + const pause: CompatibilityPause = { + resumable: true, + reasons: [ + { + source: "database-version", + state: "missing", + currentVersion: 12, + resumable: true, + }, + ], + }; + + const details = compatibilityReviewDetailsMarkdown(pause); + expect(details).toContain("copied or restored"); + expect(details).toContain("new Obsidian profile"); + expect(details).toContain("does not mean that it is safe to resume automatically"); + }); + + it("offers the generic resume action in a vertical action dialogue", async () => { + const confirmWithMessage = vi.fn().mockResolvedValue("Resume synchronisation"); + const ui = new ObsidianCompatibilityReviewUi({ confirmWithMessage } as never); + + await expect(ui.showSummary(resumablePause)).resolves.toBe("resume"); + expect(confirmWithMessage).toHaveBeenCalledWith( + "Synchronisation paused for compatibility review", + expect.any(String), + ["Review compatibility details", "Resume synchronisation", "Keep synchronisation paused"], + "Keep synchronisation paused", + undefined, + "vertical" + ); + }); +}); diff --git a/src/serviceFeatures/configuredStartupLifecycle.ts b/src/serviceFeatures/configuredStartupLifecycle.ts new file mode 100644 index 00000000..fa179e69 --- /dev/null +++ b/src/serviceFeatures/configuredStartupLifecycle.ts @@ -0,0 +1,41 @@ +export interface ConfiguredStartupLifecycleRuntime { + databaseReady: boolean; + reportDatabaseNotReady(): void; + hasCompromisedChunks(): Promise; + hasIncompleteDocuments(): Promise; + waitForCompatibilityReview(): Promise; + runDoctor(): Promise; + migrateBulkSend(): Promise; +} + +export interface StartupEntryLifecycleRuntime { + configured: boolean; + inviteToOnboarding(): void; +} + +/** + * Keeps an unconfigured Vault outside database initialisation and all + * configured-only start-up work while offering an explicit setup action. + */ +export function runStartupEntryLifecycle(runtime: StartupEntryLifecycleRuntime): boolean { + if (runtime.configured) return true; + runtime.inviteToOnboarding(); + return false; +} + +/** + * Separates the inert, unconfigured startup path from checks which must run + * before an already configured device is allowed to synchronise. + */ +export async function runConfiguredStartupLifecycle(runtime: ConfiguredStartupLifecycleRuntime): Promise { + if (!runtime.databaseReady) { + runtime.reportDatabaseNotReady(); + return false; + } + if (!(await runtime.hasCompromisedChunks())) return false; + if (!(await runtime.hasIncompleteDocuments())) return false; + await runtime.waitForCompatibilityReview(); + if (!(await runtime.runDoctor())) return false; + await runtime.migrateBulkSend(); + return true; +} diff --git a/src/serviceFeatures/configuredStartupLifecycle.unit.spec.ts b/src/serviceFeatures/configuredStartupLifecycle.unit.spec.ts new file mode 100644 index 00000000..9bfaff6e --- /dev/null +++ b/src/serviceFeatures/configuredStartupLifecycle.unit.spec.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from "vitest"; +import { + runConfiguredStartupLifecycle, + runStartupEntryLifecycle, + type ConfiguredStartupLifecycleRuntime, +} from "./configuredStartupLifecycle"; + +function createRuntime(): ConfiguredStartupLifecycleRuntime & { events: string[] } { + const events: string[] = []; + return { + events, + databaseReady: true, + reportDatabaseNotReady: vi.fn(() => events.push("database-not-ready")), + hasCompromisedChunks: vi.fn(async () => { + events.push("compromised-chunks"); + return true; + }), + hasIncompleteDocuments: vi.fn(async () => { + events.push("incomplete-documents"); + return true; + }), + waitForCompatibilityReview: vi.fn(async () => {}), + runDoctor: vi.fn(async () => { + events.push("doctor"); + return true; + }), + migrateBulkSend: vi.fn(async () => { + events.push("bulk-send"); + }), + }; +} + +describe("runConfiguredStartupLifecycle", () => { + it("runs configured checks in order before allowing initialisation", async () => { + const runtime = createRuntime(); + + await expect(runConfiguredStartupLifecycle(runtime)).resolves.toBe(true); + + expect(runtime.events).toEqual(["compromised-chunks", "incomplete-documents", "doctor", "bulk-send"]); + }); + + it("keeps Config Doctor behind the initial compatibility review", async () => { + const runtime = createRuntime(); + Object.assign(runtime, { + waitForCompatibilityReview: vi.fn(async () => { + runtime.events.push("compatibility-review"); + }), + }); + + await expect(runConfiguredStartupLifecycle(runtime)).resolves.toBe(true); + + expect(runtime.events).toEqual([ + "compromised-chunks", + "incomplete-documents", + "compatibility-review", + "doctor", + "bulk-send", + ]); + }); + + it("stops before onboarding or checks when the database is unavailable", async () => { + const runtime = createRuntime(); + runtime.databaseReady = false; + + await expect(runConfiguredStartupLifecycle(runtime)).resolves.toBe(false); + + expect(runtime.events).toEqual(["database-not-ready"]); + }); + + it("stops the configured sequence at the first failed check", async () => { + const runtime = createRuntime(); + vi.mocked(runtime.hasIncompleteDocuments).mockImplementation(async () => { + runtime.events.push("incomplete-documents"); + return false; + }); + + await expect(runConfiguredStartupLifecycle(runtime)).resolves.toBe(false); + + expect(runtime.events).toEqual(["compromised-chunks", "incomplete-documents"]); + }); +}); + +describe("runStartupEntryLifecycle", () => { + it("offers onboarding and stops before database initialisation on an unconfigured Vault", () => { + const inviteToOnboarding = vi.fn(); + + expect( + runStartupEntryLifecycle({ + configured: false, + inviteToOnboarding, + }) + ).toBe(false); + + expect(inviteToOnboarding).toHaveBeenCalledOnce(); + }); + + it("allows a configured Vault to continue to database initialisation", () => { + const inviteToOnboarding = vi.fn(); + + expect( + runStartupEntryLifecycle({ + configured: true, + inviteToOnboarding, + }) + ).toBe(true); + + expect(inviteToOnboarding).not.toHaveBeenCalled(); + }); +}); diff --git a/src/serviceFeatures/fileDatabaseInfo.ts b/src/serviceFeatures/fileDatabaseInfo.ts new file mode 100644 index 00000000..40fc01e3 --- /dev/null +++ b/src/serviceFeatures/fileDatabaseInfo.ts @@ -0,0 +1,454 @@ +import { $msg } from "@/common/translation"; +import type { + FilePath, + FilePathWithPrefix, + LoadedEntry, + ObsidianLiveSyncSettings, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { getFileRegExp } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; +import { ICHeader, ICXHeader, PSCHeader } from "@vrtmrz/livesync-commonlib/compat/common/models/fileaccess.const"; +import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; +import type { LiveSyncLocalDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/LiveSyncLocalDB"; +import type { IPathService, IUIService } from "@vrtmrz/livesync-commonlib/compat/services/base/IService"; +import { addPrefix, stripAllPrefixes } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; + +type DatabaseMeta = LoadedEntry & { + _rawStorageType: string | null; + _legacyBodyPresent: boolean; + _revs_info?: Array<{ + rev: string; + status: string; + }>; +}; + +export type FileDatabaseInfoCore = { + localDatabase: Pick< + LiveSyncLocalDB, + "allDocsRaw" | "findAllDocs" | "getDBEntryFromMeta" | "getDBEntry" | "localDatabase" + >; + services: { + path: Pick; + UI: IUIService; + }; + settings: ObsidianLiveSyncSettings; + storageAccess: Pick< + StorageAccess, + "getFileNames" | "getFilesIncludeHidden" | "isExistsIncludeHidden" | "statHidden" + >; +}; + +export type RevisionDatabaseInfo = { + documentId: string; + revision: string | null; + current: boolean; + deleted: boolean; + storageType: string; + storageLayout: "chunked" | "legacy-inline"; + ctime: number; + mtime: number; + recordedSize: number; + revisionHistory: Array<{ + revision: string; + status: string; + }>; + chunkReferences: number; + uniqueChunkReferences: number; + embeddedChunkReferences: number; + locallyStoredChunkReferences: number; + contentAvailableLocally: boolean; + chunks: Array<{ + id: string; + referenceCount: number; + embedded: boolean; + storedInLocalDatabase: boolean; + localDatabaseState: "available" | "deleted" | "missing"; + localDatabaseRevision: string | null; + }>; +}; + +export type FileDatabaseMergeBaseInfo = { + winnerRevision: string; + conflictRevision: string; + revision: string | null; + metadataAvailableLocally: boolean; + contentAvailableLocally: boolean; + missingChunkIds: string[]; + unavailableSharedRevisions: string[]; +}; + +export type FileDatabaseInfo = { + path: string; + databasePath: FilePathWithPrefix | FilePath; + storage: { + exists: boolean; + ctime?: number; + mtime?: number; + size?: number; + }; + database: { + source: "local database on this device"; + remoteQueried: false; + exists: boolean; + currentRevision: string | null; + conflictCount: number; + conflictRevisions: string[]; + unavailableConflictRevisions: string[]; + revisions: RevisionDatabaseInfo[]; + mergeBases: FileDatabaseMergeBaseInfo[]; + }; +}; + +const REPORT_WARNING = + "All revisions and chunk availability below are a snapshot of this device's local database; the remote is not queried. Review the Vault-relative path, document identifier, content-derived chunk identifiers, and metadata before sharing this report. File contents are omitted."; + +function toDatabasePath(path: string): FilePathWithPrefix | FilePath { + if (path.startsWith(".")) { + return addPrefix(path as FilePath, ICHeader); + } + return path as FilePath; +} + +type RawDatabaseDocument = { + _id: string; + _rev?: string; + _conflicts?: string[]; + _deleted?: boolean; + _revs_info?: Array<{ + rev: string; + status: string; + }>; + children?: string[]; + ctime?: number; + deleted?: boolean; + data?: string | string[]; + eden?: Record; + mtime?: number; + size?: number; + type?: string; +}; + +async function getLocalDatabaseMeta( + core: FileDatabaseInfoCore, + path: FilePathWithPrefix | FilePath, + options: PouchDB.Core.GetOptions +): Promise { + const documentId = await core.services.path.path2id(path); + let raw: RawDatabaseDocument; + try { + raw = await core.localDatabase.localDatabase.get(documentId, options); + } catch (error) { + if (isNotFoundError(error)) { + return false; + } + throw error; + } + + if (raw.type === "leaf") { + return false; + } + if (raw.type && raw.type !== "notes" && raw.type !== "newnote" && raw.type !== "plain") { + return false; + } + + const rawStorageType = raw.type ?? null; + const legacy = rawStorageType === null || rawStorageType === "notes"; + const type = legacy ? "notes" : rawStorageType; + const legacyBodyPresent = + legacy && (typeof raw.data === "string" || (Array.isArray(raw.data) && raw.data.every((item) => typeof item === "string"))); + return { + _id: raw._id, + _rev: raw._rev, + _conflicts: raw._conflicts, + _revs_info: raw._revs_info, + path, + data: legacyBodyPresent ? raw.data : "", + ctime: raw.ctime ?? 0, + mtime: raw.mtime ?? 0, + size: raw.size ?? 0, + children: type === "newnote" || type === "plain" ? (raw.children ?? []) : [], + datatype: type === "newnote" ? "newnote" : "plain", + deleted: raw.deleted ?? raw._deleted, + type, + eden: raw.eden ?? {}, + _rawStorageType: rawStorageType, + _legacyBodyPresent: legacyBodyPresent, + } as DatabaseMeta; +} + +async function collectRevisionDatabaseInfo( + core: FileDatabaseInfoCore, + meta: DatabaseMeta, + current: boolean +): Promise { + const legacy = meta._rawStorageType === null || meta._rawStorageType === "notes"; + const children = legacy ? [] : "children" in meta ? meta.children : []; + const uniqueChildren = [...new Set(children)]; + const referenceCounts = new Map(); + for (const child of children) { + referenceCounts.set(child, (referenceCounts.get(child) ?? 0) + 1); + } + const embeddedChildren = new Set( + Object.keys("eden" in meta && meta.eden ? meta.eden : {}).filter((id) => uniqueChildren.includes(id)) + ); + const localRows = + uniqueChildren.length === 0 + ? [] + : ( + await core.localDatabase.allDocsRaw({ + keys: uniqueChildren, + include_docs: false, + }) + ).rows; + const localChunkStates = new Map( + localRows + .filter((row) => "value" in row) + .map( + (row) => + [ + row.key, + { + state: row.value.deleted ? ("deleted" as const) : ("available" as const), + revision: row.value.rev, + }, + ] as const + ) + ); + + return { + documentId: meta._id, + revision: meta._rev ?? null, + current, + deleted: Boolean(meta.deleted ?? meta._deleted), + storageType: meta._rawStorageType ?? "absent", + storageLayout: legacy ? "legacy-inline" : "chunked", + ctime: meta.ctime, + mtime: meta.mtime, + recordedSize: meta.size, + revisionHistory: (meta._revs_info ?? []).map(({ rev, status }) => ({ + revision: rev, + status, + })), + chunkReferences: children.length, + uniqueChunkReferences: uniqueChildren.length, + embeddedChunkReferences: children.filter((id) => embeddedChildren.has(id)).length, + locallyStoredChunkReferences: children.filter((id) => localChunkStates.get(id)?.state === "available").length, + contentAvailableLocally: legacy + ? meta._legacyBodyPresent + : uniqueChildren.every( + (id) => embeddedChildren.has(id) || localChunkStates.get(id)?.state === "available" + ), + chunks: uniqueChildren.map((id) => { + const localState = localChunkStates.get(id); + return { + id, + referenceCount: referenceCounts.get(id) ?? 0, + embedded: embeddedChildren.has(id), + storedInLocalDatabase: localState?.state === "available", + localDatabaseState: localState?.state ?? "missing", + localDatabaseRevision: localState?.revision ?? null, + }; + }), + }; +} + +function revisionHistory(meta: DatabaseMeta): Array<{ revision: string; status: string }> { + const history = (meta._revs_info ?? []).map(({ rev, status }) => ({ + revision: rev, + status, + })); + if (meta._rev && !history.some(({ revision }) => revision === meta._rev)) { + history.unshift({ + revision: meta._rev, + status: "available", + }); + } + return history; +} + +function missingChunkIds(info: RevisionDatabaseInfo): string[] { + return info.chunks + .filter(({ embedded, localDatabaseState }) => !embedded && localDatabaseState !== "available") + .map(({ id }) => id); +} + +export async function inspectFileDatabaseInfo(core: FileDatabaseInfoCore, path: string): Promise { + const storageExists = await core.storageAccess.isExistsIncludeHidden(path); + const storageStat = storageExists ? await core.storageAccess.statHidden(path) : null; + const databasePath = toDatabasePath(path); + const currentMeta = await getLocalDatabaseMeta(core, databasePath, { + conflicts: true, + revs: true, + revs_info: true, + }); + + const revisions: RevisionDatabaseInfo[] = []; + const conflictRevisions = currentMeta === false ? [] : (currentMeta._conflicts ?? []); + const unavailableConflictRevisions: string[] = []; + const mergeBases: FileDatabaseMergeBaseInfo[] = []; + const metadataByRevision = new Map(); + if (currentMeta !== false && currentMeta._rev) { + metadataByRevision.set(currentMeta._rev, currentMeta); + } + const getRevisionMeta = async (revision: string): Promise => { + const cached = metadataByRevision.get(revision); + if (cached !== undefined) { + return cached; + } + const meta = await getLocalDatabaseMeta(core, databasePath, { + rev: revision, + revs: true, + revs_info: true, + }); + metadataByRevision.set(revision, meta); + return meta; + }; + + if (currentMeta) { + revisions.push(await collectRevisionDatabaseInfo(core, currentMeta, true)); + for (const revision of conflictRevisions) { + const conflictMeta = await getRevisionMeta(revision); + if (conflictMeta) { + revisions.push(await collectRevisionDatabaseInfo(core, conflictMeta, false)); + const winnerHistory = revisionHistory(currentMeta); + const conflictHistory = revisionHistory(conflictMeta); + const conflictHistoryByRevision = new Map( + conflictHistory.map(({ revision: historyRevision, status }) => [historyRevision, status]) + ); + const sharedHistory = winnerHistory.filter(({ revision: historyRevision }) => + conflictHistoryByRevision.has(historyRevision) + ); + const sharedRevision = sharedHistory[0]?.revision ?? null; + const unavailableSharedRevisions = sharedHistory + .filter( + ({ revision: historyRevision, status }) => + status !== "available" || + conflictHistoryByRevision.get(historyRevision) !== "available" + ) + .map(({ revision: historyRevision }) => historyRevision); + const sharedMeta = sharedRevision ? await getRevisionMeta(sharedRevision) : false; + const sharedInfo = sharedMeta + ? await collectRevisionDatabaseInfo(core, sharedMeta, false) + : undefined; + mergeBases.push({ + winnerRevision: currentMeta._rev ?? "", + conflictRevision: revision, + revision: sharedRevision, + metadataAvailableLocally: Boolean(sharedMeta), + contentAvailableLocally: sharedInfo?.contentAvailableLocally ?? false, + missingChunkIds: sharedInfo ? missingChunkIds(sharedInfo) : [], + unavailableSharedRevisions, + }); + } else { + unavailableConflictRevisions.push(revision); + } + } + } + + const report: FileDatabaseInfo = { + path, + databasePath, + storage: storageStat + ? { + exists: true, + ctime: storageStat.ctime, + mtime: storageStat.mtime, + size: storageStat.size, + } + : { + exists: false, + }, + database: { + source: "local database on this device", + remoteQueried: false, + exists: currentMeta !== false, + currentRevision: currentMeta ? (currentMeta._rev ?? null) : null, + conflictCount: conflictRevisions.length, + conflictRevisions, + unavailableConflictRevisions, + revisions, + mergeBases, + }, + }; + + return report; +} + +export async function readFileDatabaseRevisionLocally( + core: FileDatabaseInfoCore, + path: string, + revision: string +): Promise { + const databasePath = toDatabasePath(path); + const meta = await getLocalDatabaseMeta(core, databasePath, { + rev: revision, + revs: true, + revs_info: true, + }); + if (!meta) { + return false; + } + const info = await collectRevisionDatabaseInfo(core, meta, false); + if (info.deleted || !info.contentAvailableLocally) { + return false; + } + return await core.localDatabase.getDBEntryFromMeta(meta, false, false); +} + +export async function retryReadFileDatabaseRevision( + core: FileDatabaseInfoCore, + path: string, + revision: string +): Promise { + return await core.localDatabase.getDBEntry(toDatabasePath(path), { rev: revision }, false, true, true); +} + +export async function buildFileDatabaseInfoReport(core: FileDatabaseInfoCore, path: string): Promise { + const report = await inspectFileDatabaseInfo(core, path); + return `${$msg(REPORT_WARNING)} + +\`\`\`json +${JSON.stringify(report, null, 2)} +\`\`\``; +} + +export async function copyFileDatabaseInfo(core: FileDatabaseInfoCore, path: string): Promise { + const report = await buildFileDatabaseInfoReport(core, path); + return await core.services.UI.promptCopyToClipboard( + $msg("Database information for ${FILE}", { FILE: path }), + report + ); +} + +export async function collectFileDatabaseInfoPaths(core: FileDatabaseInfoCore): Promise { + const ignorePatterns = getFileRegExp(core.settings, "syncInternalFilesIgnorePatterns"); + const targetPatterns = getFileRegExp(core.settings, "syncInternalFilesTargetPatterns"); + const storagePaths = core.settings.syncInternalFiles + ? await core.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns) + : await core.storageAccess.getFileNames(); + const databasePaths: string[] = []; + + for await (const entry of core.localDatabase.findAllDocs()) { + const prefixedPath = entry.path; + if (prefixedPath.startsWith(ICXHeader) || prefixedPath.startsWith(PSCHeader)) { + continue; + } + if (!core.settings.syncInternalFiles && prefixedPath.startsWith(ICHeader)) { + continue; + } + databasePaths.push(stripAllPrefixes(prefixedPath)); + } + + return [...new Set([...storagePaths, ...databasePaths])].sort((left, right) => + left < right ? -1 : left > right ? 1 : 0 + ); +} + +export async function chooseAndCopyFileDatabaseInfo(core: FileDatabaseInfoCore): Promise { + const paths = await collectFileDatabaseInfoPaths(core); + const selected = await core.services.UI.confirm.askSelectString($msg("Choose a file to inspect"), paths); + if (!selected) { + return false; + } + return await copyFileDatabaseInfo(core, selected); +} diff --git a/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts b/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts new file mode 100644 index 00000000..1d1b6207 --- /dev/null +++ b/src/serviceFeatures/fileDatabaseInfo.unit.spec.ts @@ -0,0 +1,413 @@ +import { describe, expect, it, vi } from "vitest"; +import { + buildFileDatabaseInfoReport, + chooseAndCopyFileDatabaseInfo, + collectFileDatabaseInfoPaths, + inspectFileDatabaseInfo, + readFileDatabaseRevisionLocally, + retryReadFileDatabaseRevision, +} from "./fileDatabaseInfo"; + +async function* documents(paths: string[]) { + for (const path of paths) { + yield { + _id: `f:${path}`, + path, + }; + } +} + +function createCore() { + const current = { + _id: "f:note", + _rev: "3-current", + _conflicts: ["2-conflict"], + _revs_info: [ + { rev: "3-current", status: "available" }, + { rev: "2-parent", status: "missing" }, + ], + path: "note.md", + ctime: 100, + mtime: 300, + size: 42, + type: "plain", + datatype: "plain", + data: "secret current body", + children: ["h:private-current", "h:private-current", "h:private-embedded", "h:private-deleted"], + eden: { + "h:private-embedded": { + data: "secret embedded body", + epoch: 1, + }, + }, + }; + const conflict = { + ...current, + _rev: "2-conflict", + _conflicts: undefined, + _revs_info: [{ rev: "2-conflict", status: "available" }], + mtime: 200, + data: "secret conflict body", + children: ["h:private-missing"], + eden: {}, + }; + const promptCopyToClipboard = vi.fn(async (_title: string, _value: string) => true); + const askSelectString = vi.fn(async () => "db-only.md"); + const core = { + settings: { + syncInternalFiles: false, + syncInternalFilesIgnorePatterns: "", + syncInternalFilesTargetPatterns: "", + }, + storageAccess: { + isExistsIncludeHidden: vi.fn(async () => true), + statHidden: vi.fn(async () => ({ + ctime: 90, + mtime: 310, + size: 45, + type: "file", + })), + getFileNames: vi.fn(async () => ["z.md", "a.md"]), + getFilesIncludeHidden: vi.fn(async () => [".obsidian/app.json", "a.md"]), + }, + localDatabase: { + getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({ + ...meta, + data: ["loaded body"], + })), + getDBEntry: vi.fn(async () => current), + localDatabase: { + get: vi.fn(async (_id: string, options?: { rev?: string }) => + options?.rev === "2-conflict" ? conflict : current + ), + }, + allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({ + rows: [ + ...(keys.includes("h:private-current") + ? [ + { + id: "h:private-current", + key: "h:private-current", + value: { rev: "1-chunk" }, + }, + ] + : []), + ...(keys.includes("h:private-deleted") + ? [ + { + id: "h:private-deleted", + key: "h:private-deleted", + value: { rev: "4-deleted-chunk", deleted: true }, + }, + ] + : []), + ], + })), + findAllDocs: vi.fn(() => documents(["db-only.md", "i:.obsidian/app.json", "ix:ignore", "ps:setting"])), + }, + services: { + path: { + path2id: vi.fn(async () => "f:note"), + }, + UI: { + promptCopyToClipboard, + confirm: { + askSelectString, + }, + }, + }, + }; + return { + askSelectString, + conflict, + core, + current, + promptCopyToClipboard, + }; +} + +describe("file database information", () => { + it("reports document and chunk revisions without exposing file contents", async () => { + const { core } = createCore(); + + const report = await buildFileDatabaseInfoReport(core as never, "note.md"); + + expect(report).toContain('"path": "note.md"'); + expect(report).toContain('"documentId": "f:note"'); + expect(report).toContain('"revision": "3-current"'); + expect(report).toContain('"revision": "2-conflict"'); + expect(report).toContain('"storageType": "plain"'); + expect(report).toContain('"storageLayout": "chunked"'); + expect(report).toContain('"contentAvailableLocally": false'); + expect(report).toContain('"id": "h:private-current"'); + expect(report).toContain('"localDatabaseRevision": "1-chunk"'); + expect(report).toContain('"referenceCount": 2'); + expect(report).toContain('"id": "h:private-embedded"'); + expect(report).toContain('"embedded": true'); + expect(report).toContain('"id": "h:private-deleted"'); + expect(report).toContain('"localDatabaseState": "deleted"'); + expect(report).toContain('"localDatabaseRevision": "4-deleted-chunk"'); + expect(report).toContain('"id": "h:private-missing"'); + expect(report).toContain('"localDatabaseState": "missing"'); + expect(report).toContain('"localDatabaseRevision": null'); + expect(report).not.toContain("secret current body"); + expect(report).not.toContain("secret conflict body"); + expect(report).not.toContain("secret embedded body"); + }); + + it.each([ + { + name: "notes", + document: { + type: "notes", + data: "secret legacy body", + }, + storageType: "notes", + }, + { + name: "an absent type", + document: { + type: undefined, + data: ["secret", " legacy body"], + }, + storageType: "absent", + }, + ])("reports $name as legacy inline storage without exposing its body", async ({ document, storageType }) => { + const { core, current } = createCore(); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + ...document, + _conflicts: [], + children: ["h:must-not-be-treated-as-a-chunk"], + } as never); + + const info = await inspectFileDatabaseInfo(core as never, "note.md"); + const report = await buildFileDatabaseInfoReport(core as never, "note.md"); + + expect(info.database.revisions).toEqual([ + expect.objectContaining({ + storageType, + storageLayout: "legacy-inline", + chunkReferences: 0, + contentAvailableLocally: true, + }), + ]); + expect(report).not.toContain("secret legacy body"); + expect(report).not.toContain("h:must-not-be-treated-as-a-chunk"); + }); + + it("reports the exact shared ancestor and its missing chunks for each conflict", async () => { + const { conflict, core, current } = createCore(); + const parent = { + ...current, + _rev: "2-parent", + _conflicts: undefined, + _revs_info: [ + { rev: "2-parent", status: "available" }, + { rev: "1-root", status: "missing" }, + ], + children: ["h:missing-parent"], + eden: {}, + }; + core.localDatabase.localDatabase.get.mockImplementation(async (_id: string, options?: { rev?: string }) => { + if (options?.rev === "2-conflict") { + return { + ...conflict, + _revs_info: [ + { rev: "2-conflict", status: "available" }, + { rev: "2-parent", status: "available" }, + { rev: "1-root", status: "missing" }, + ], + }; + } + if (options?.rev === "2-parent") { + return parent; + } + return { + ...current, + _revs_info: [ + { rev: "3-current", status: "available" }, + { rev: "2-parent", status: "available" }, + { rev: "1-root", status: "missing" }, + ], + }; + }); + + const info = await inspectFileDatabaseInfo(core as never, "note.md"); + + expect(info.database.mergeBases).toEqual([ + { + winnerRevision: "3-current", + conflictRevision: "2-conflict", + revision: "2-parent", + metadataAvailableLocally: true, + contentAvailableLocally: false, + missingChunkIds: ["h:missing-parent"], + unavailableSharedRevisions: ["1-root"], + }, + ]); + }); + + it("does not decode a revision whose chunks are not all available locally", async () => { + const { core } = createCore(); + + await expect(readFileDatabaseRevisionLocally(core as never, "note.md", "3-current")).resolves.toBe(false); + + expect(core.localDatabase.getDBEntryFromMeta).not.toHaveBeenCalled(); + }); + + it("decodes an exact revision after confirming that every chunk is available locally", async () => { + const { core, current } = createCore(); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + children: ["h:available"], + eden: {}, + } as never); + core.localDatabase.allDocsRaw.mockResolvedValue({ + rows: [ + { + id: "h:available", + key: "h:available", + value: { rev: "1-available" }, + }, + ], + }); + + await expect(readFileDatabaseRevisionLocally(core as never, "note.md", "3-current")).resolves.toEqual( + expect.objectContaining({ + data: ["loaded body"], + }) + ); + + expect(core.localDatabase.getDBEntryFromMeta).toHaveBeenCalledWith( + expect.objectContaining({ + _rev: "3-current", + }), + false, + false + ); + }); + + it("retries an exact revision through the configured chunk retrieval path", async () => { + const { core } = createCore(); + + await retryReadFileDatabaseRevision(core as never, "note.md", "2-conflict"); + + expect(core.localDatabase.getDBEntry).toHaveBeenCalledWith( + "note.md", + { rev: "2-conflict" }, + false, + true, + true + ); + }); + + it("reports the exact revision as locally available after retry recovers its missing chunk", async () => { + const { conflict, core } = createCore(); + let recovered = false; + core.localDatabase.getDBEntry.mockImplementation(async () => { + recovered = true; + return conflict as never; + }); + core.localDatabase.allDocsRaw.mockImplementation(async ({ keys }: { keys: string[] }) => ({ + rows: + recovered && keys.includes("h:private-missing") + ? [ + { + id: "h:private-missing", + key: "h:private-missing", + value: { rev: "1-recovered" }, + }, + ] + : [], + })); + + await expect( + retryReadFileDatabaseRevision(core as never, "note.md", "2-conflict") + ).resolves.not.toBe(false); + const information = await inspectFileDatabaseInfo(core as never, "note.md"); + + expect( + information.database.revisions.find(({ revision }) => revision === "2-conflict") + ).toEqual( + expect.objectContaining({ + contentAvailableLocally: true, + chunks: [ + expect.objectContaining({ + id: "h:private-missing", + localDatabaseState: "available", + localDatabaseRevision: "1-recovered", + }), + ], + }) + ); + }); + + it("keeps the exact revision identifiers when conflict metadata is unavailable", async () => { + const { conflict, core, current } = createCore(); + core.localDatabase.localDatabase.get.mockImplementation(async (_id: string, options?: { rev?: string }) => { + if (options?.rev === "2-unavailable") { + throw Object.assign(new Error("missing"), { status: 404 }); + } + if (options?.rev === "2-conflict") { + return conflict; + } + return { ...current, _conflicts: ["2-conflict", "2-unavailable"] }; + }); + + const report = await buildFileDatabaseInfoReport(core as never, "note.md"); + + expect(report).toContain('"conflictRevisions"'); + expect(report).toContain('"2-conflict"'); + expect(report).toContain('"2-unavailable"'); + expect(report).toContain('"unavailableConflictRevisions"'); + }); + + it("reads an existing local document even when current synchronisation filters exclude its path", async () => { + const { core, current } = createCore(); + core.services.path.path2id.mockResolvedValue("f:ignored"); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + _id: "f:ignored", + _rev: "5-ignored", + _conflicts: [], + _revs_info: [], + path: "ignored.md", + ctime: 10, + mtime: 20, + size: 30, + children: [], + }); + + const report = await buildFileDatabaseInfoReport(core as never, "ignored.md"); + + expect(report).toContain('"exists": true'); + expect(report).toContain('"documentId": "f:ignored"'); + expect(report).toContain('"revision": "5-ignored"'); + }); + + it("offers the union of storage and database paths and excludes inactive internal namespaces", async () => { + const { core } = createCore(); + + await expect(collectFileDatabaseInfoPaths(core as never)).resolves.toEqual(["a.md", "db-only.md", "z.md"]); + + core.settings.syncInternalFiles = true; + await expect(collectFileDatabaseInfoPaths(core as never)).resolves.toEqual([ + ".obsidian/app.json", + "a.md", + "db-only.md", + ]); + }); + + it("copies the selected file report through the existing copy dialogue", async () => { + const { askSelectString, core, promptCopyToClipboard } = createCore(); + + await expect(chooseAndCopyFileDatabaseInfo(core as never)).resolves.toBe(true); + + expect(askSelectString).toHaveBeenCalledWith("Choose a file to inspect", ["a.md", "db-only.md", "z.md"]); + expect(promptCopyToClipboard).toHaveBeenCalledWith( + "Database information for db-only.md", + expect.stringContaining('"path": "db-only.md"') + ); + }); +}); diff --git a/src/serviceFeatures/fileRepair.ts b/src/serviceFeatures/fileRepair.ts new file mode 100644 index 00000000..64172843 --- /dev/null +++ b/src/serviceFeatures/fileRepair.ts @@ -0,0 +1,132 @@ +import type { LoadedEntry } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { createBlob, isDocContentSame, readAsBlob } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { StorageAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/StorageAccess"; +import type { IFileHandler } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileHandler"; +import { + inspectFileDatabaseInfo, + readFileDatabaseRevisionLocally, + type FileDatabaseInfo, + type FileDatabaseInfoCore, + type RevisionDatabaseInfo, +} from "./fileDatabaseInfo"; + +export type FileRepairCore = FileDatabaseInfoCore & { + fileHandler: Pick; + storageAccess: FileDatabaseInfoCore["storageAccess"] & Pick; +}; + +export type FileRepairRevision = { + role: "winner" | "conflict"; + metadata: RevisionDatabaseInfo; + contentReadable: boolean; + contentMatchesStorage: boolean | null; + loadedEntry: LoadedEntry | false; +}; + +export type FileRepairInspection = { + information: FileDatabaseInfo; + revisions: FileRepairRevision[]; + requiresAttention: boolean; +}; + +export type DiscardUnreadableRevisionResult = + | "discarded" + | "failed" + | "no-longer-live" + | "revision-is-readable"; + +export type DiscardLiveBranchResult = "discarded" | "failed" | "no-longer-live" | "only-live-revision"; + +export async function inspectFileRepair(core: FileRepairCore, path: string): Promise { + const information = await inspectFileDatabaseInfo(core, path); + const storageContent = information.storage.exists + ? createBlob(await core.storageAccess.readHiddenFileBinary(path)) + : undefined; + const revisions: FileRepairRevision[] = []; + + for (const metadata of information.database.revisions) { + const loadedEntry = + metadata.deleted || !metadata.contentAvailableLocally + ? false + : await readFileDatabaseRevisionLocally(core, path, metadata.revision ?? ""); + const contentReadable = metadata.deleted || loadedEntry !== false; + const contentMatchesStorage = + storageContent && loadedEntry !== false + ? await isDocContentSame(storageContent, readAsBlob(loadedEntry)) + : null; + revisions.push({ + role: metadata.current ? "winner" : "conflict", + metadata, + contentReadable, + contentMatchesStorage, + loadedEntry, + }); + } + + const winner = revisions.find(({ role }) => role === "winner"); + const winnerRepresentsStoredFile = winner !== undefined && !winner.metadata.deleted; + const databaseAndStorageDiffer = + information.storage.exists !== winnerRepresentsStoredFile || + (information.storage.exists && + winnerRepresentsStoredFile && + winner.contentMatchesStorage === false); + const unreadableLiveRevision = + information.database.unavailableConflictRevisions.length > 0 || + revisions.some(({ contentReadable }) => !contentReadable); + const requiresAttention = + databaseAndStorageDiffer || + information.database.conflictCount > 0 || + unreadableLiveRevision || + (information.database.exists && winner === undefined); + + return { + information, + revisions, + requiresAttention, + }; +} + +export async function discardUnreadableLiveRevision( + core: FileRepairCore, + path: string, + revision: string +): Promise { + const latest = await inspectFileDatabaseInfo(core, path); + const liveRevisions = [ + latest.database.currentRevision, + ...latest.database.conflictRevisions, + ].filter((candidate): candidate is string => candidate !== null); + if (!liveRevisions.includes(revision)) { + return "no-longer-live"; + } + + const metadata = latest.database.revisions.find((candidate) => candidate.revision === revision); + const metadataUnavailable = latest.database.unavailableConflictRevisions.includes(revision); + if (!metadataUnavailable && (metadata?.deleted || metadata?.contentAvailableLocally)) { + return "revision-is-readable"; + } + + const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); + return deleted ? "discarded" : "failed"; +} + +export async function discardLiveBranch( + core: FileRepairCore, + path: string, + revision: string +): Promise { + const latest = await inspectFileDatabaseInfo(core, path); + const liveRevisions = [ + latest.database.currentRevision, + ...latest.database.conflictRevisions, + ].filter((candidate): candidate is string => candidate !== null); + if (!liveRevisions.includes(revision)) { + return "no-longer-live"; + } + if (liveRevisions.length < 2) { + return "only-live-revision"; + } + + const deleted = await core.fileHandler.deleteRevisionFromDB(latest.databasePath, revision); + return deleted ? "discarded" : "failed"; +} diff --git a/src/serviceFeatures/fileRepair.unit.spec.ts b/src/serviceFeatures/fileRepair.unit.spec.ts new file mode 100644 index 00000000..af12a34b --- /dev/null +++ b/src/serviceFeatures/fileRepair.unit.spec.ts @@ -0,0 +1,228 @@ +import { describe, expect, it, vi } from "vitest"; +import { + discardLiveBranch, + discardUnreadableLiveRevision, + inspectFileRepair, +} from "./fileRepair"; + +function createCore() { + const current = { + _id: "f:note", + _rev: "3-current", + _conflicts: ["2-conflict"], + _revs_info: [{ rev: "3-current", status: "available" }], + path: "note.md", + ctime: 1, + mtime: 3, + size: 7, + type: "plain", + children: ["h:current"], + deleted: false, + eden: {}, + }; + const conflict = { + ...current, + _rev: "2-conflict", + _conflicts: undefined, + _revs_info: [{ rev: "2-conflict", status: "available" }], + mtime: 2, + children: ["h:missing-conflict"], + }; + const deleteRevisionFromDB = vi.fn(async () => true); + const core = { + settings: { + syncInternalFiles: false, + syncInternalFilesIgnorePatterns: "", + syncInternalFilesTargetPatterns: "", + }, + storageAccess: { + isExistsIncludeHidden: vi.fn(async () => true), + statHidden: vi.fn(async () => ({ + ctime: 1, + mtime: 3, + size: 7, + type: "file", + })), + readHiddenFileBinary: vi.fn(async () => new TextEncoder().encode("current").buffer), + getFileNames: vi.fn(async () => ["note.md"]), + getFilesIncludeHidden: vi.fn(async () => ["note.md"]), + }, + localDatabase: { + localDatabase: { + get: vi.fn(async (_id: string, options?: { rev?: string }) => + options?.rev === "2-conflict" ? conflict : current + ), + }, + allDocsRaw: vi.fn(async ({ keys }: { keys: string[] }) => ({ + rows: keys.includes("h:current") + ? [ + { + id: "h:current", + key: "h:current", + value: { rev: "1-current" }, + }, + ] + : [], + })), + getDBEntryFromMeta: vi.fn(async (meta: typeof current) => ({ + ...meta, + data: [meta._rev === "3-current" ? "current" : "conflict"], + })), + getDBEntry: vi.fn(async () => false), + findAllDocs: vi.fn(async function* () { + yield current; + }), + }, + fileHandler: { + deleteRevisionFromDB, + }, + services: { + path: { + path2id: vi.fn(async () => "f:note"), + }, + UI: { + confirm: {}, + }, + }, + }; + return { + conflict, + core, + current, + deleteRevisionFromDB, + }; +} + +describe("file repair inspection", () => { + it("shows the winner and every conflict revision independently", async () => { + const { core } = createCore(); + + const inspection = await inspectFileRepair(core as never, "note.md"); + + expect(inspection.revisions).toEqual([ + expect.objectContaining({ + role: "winner", + contentReadable: true, + contentMatchesStorage: true, + metadata: expect.objectContaining({ + revision: "3-current", + }), + }), + expect.objectContaining({ + role: "conflict", + contentReadable: false, + contentMatchesStorage: null, + metadata: expect.objectContaining({ + revision: "2-conflict", + }), + }), + ]); + expect(inspection.requiresAttention).toBe(true); + }); + + it("omits a logical deletion which already matches an absent Vault file", async () => { + const { core, current } = createCore(); + current.deleted = true; + current._conflicts = []; + current.children = []; + core.storageAccess.isExistsIncludeHidden.mockResolvedValue(false); + core.storageAccess.statHidden.mockResolvedValue(null as never); + + const inspection = await inspectFileRepair(core as never, "note.md"); + + expect(inspection.revisions).toEqual([ + expect.objectContaining({ + role: "winner", + contentReadable: true, + metadata: expect.objectContaining({ + deleted: true, + revision: "3-current", + }), + }), + ]); + expect(inspection.requiresAttention).toBe(false); + }); + + it("rechecks liveness and readability before discarding an exact revision", async () => { + const { core, deleteRevisionFromDB } = createCore(); + + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "2-conflict") + ).resolves.toBe("discarded"); + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "3-current") + ).resolves.toBe("revision-is-readable"); + + expect(deleteRevisionFromDB).toHaveBeenCalledOnce(); + expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "2-conflict"); + }); + + it("allows an exact unreadable generation-one winner to be discarded explicitly", async () => { + const { core, current, deleteRevisionFromDB } = createCore(); + current._rev = "1-root"; + current._conflicts = []; + current.children = ["h:missing-root"]; + core.localDatabase.allDocsRaw.mockResolvedValue({ rows: [] }); + + const inspection = await inspectFileRepair(core as never, "note.md"); + + expect(inspection.revisions).toEqual([ + expect.objectContaining({ + role: "winner", + contentReadable: false, + metadata: expect.objectContaining({ + revision: "1-root", + }), + }), + ]); + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "1-root") + ).resolves.toBe("discarded"); + expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "1-root"); + }); + + it("refuses to discard a revision which stopped being a live leaf", async () => { + const { core, current, deleteRevisionFromDB } = createCore(); + core.localDatabase.localDatabase.get.mockResolvedValue({ + ...current, + _conflicts: [], + }); + + await expect( + discardUnreadableLiveRevision(core as never, "note.md", "2-conflict") + ).resolves.toBe("no-longer-live"); + + expect(deleteRevisionFromDB).not.toHaveBeenCalled(); + }); + + it("discards an exact readable winner while another live branch remains", async () => { + const { core, deleteRevisionFromDB } = createCore(); + + await expect( + discardLiveBranch(core as never, "note.md", "3-current") + ).resolves.toBe("discarded"); + + expect(deleteRevisionFromDB).toHaveBeenCalledWith("note.md", "3-current"); + }); + + it("refuses to discard the only live branch", async () => { + const { core, current, deleteRevisionFromDB } = createCore(); + current._conflicts = []; + + await expect( + discardLiveBranch(core as never, "note.md", "3-current") + ).resolves.toBe("only-live-revision"); + + expect(deleteRevisionFromDB).not.toHaveBeenCalled(); + }); + + it("refuses to discard a branch which is no longer live", async () => { + const { core, deleteRevisionFromDB } = createCore(); + + await expect( + discardLiveBranch(core as never, "note.md", "1-stale") + ).resolves.toBe("no-longer-live"); + + expect(deleteRevisionFromDB).not.toHaveBeenCalled(); + }); +}); diff --git a/src/serviceFeatures/fileRepairPresentation.ts b/src/serviceFeatures/fileRepairPresentation.ts new file mode 100644 index 00000000..4906f4cb --- /dev/null +++ b/src/serviceFeatures/fileRepairPresentation.ts @@ -0,0 +1,144 @@ +import { + BASE_IS_NEW, + EVEN, + TARGET_IS_NEW, +} from "@vrtmrz/livesync-commonlib/compat/common/models/shared.const.symbols"; +import { + compareMTime, + readAsBlob, +} from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import { isPlainText } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; +import type { + FileRepairInspection, + FileRepairRevision, +} from "./fileRepair"; + +export type FileRepairRevisionActions = { + compareWithVault: boolean; + applyRevisionToVault: boolean; + markAsVaultRevision: boolean; + storeVaultOnBranch: boolean; + applyLogicalDeletionToVault: boolean; + retryRevision: boolean; + discardBranch: boolean; + discardRevision: boolean; +}; + +export type FileRepairTimestampRelation = + | "vault-newer" + | "database-newer" + | "same-window" + | "unavailable"; + +export type FileRepairRevisionComparison = { + recordedSize: number; + decodedSize: number | null; + recordedToDecodedSizeDifference: number | null; + vaultSize: number | null; + databaseToVaultSizeDifference: number | null; + databaseMtime: number; + vaultMtime: number | null; + timestampDifferenceMs: number | null; + timestampRelation: FileRepairTimestampRelation; +}; + +export function getFileRepairRevisionActions( + inspection: FileRepairInspection, + revision: FileRepairRevision +): FileRepairRevisionActions { + const storageExists = inspection.information.storage.exists; + const hasRevision = revision.metadata.revision !== null; + const readableFileRevision = + !revision.metadata.deleted && + revision.contentReadable && + revision.loadedEntry !== false; + const matchesVault = storageExists && revision.contentMatchesStorage === true; + const hasConflictBranches = inspection.information.database.conflictCount > 0; + + return { + compareWithVault: + readableFileRevision && + storageExists && + revision.contentMatchesStorage === false && + isPlainText(inspection.information.path), + applyRevisionToVault: + hasRevision && + readableFileRevision && + (!storageExists || revision.contentMatchesStorage !== true), + markAsVaultRevision: + hasRevision && + readableFileRevision && + matchesVault, + storeVaultOnBranch: + hasRevision && + storageExists && + revision.contentMatchesStorage !== true, + applyLogicalDeletionToVault: + hasRevision && + revision.metadata.deleted && + storageExists, + retryRevision: + hasRevision && + !revision.metadata.deleted && + !revision.contentReadable, + discardBranch: hasRevision && hasConflictBranches, + discardRevision: + hasRevision && + !hasConflictBranches && + !revision.metadata.deleted && + !revision.contentReadable, + }; +} + +export function getFileRepairRevisionComparison( + inspection: FileRepairInspection, + revision: FileRepairRevision +): FileRepairRevisionComparison { + const decodedSize = + revision.loadedEntry === false + ? null + : readAsBlob(revision.loadedEntry).size; + const vaultSize = + inspection.information.storage.exists + ? (inspection.information.storage.size ?? null) + : null; + const databaseMtime = revision.metadata.mtime; + const vaultMtime = + inspection.information.storage.exists + ? (inspection.information.storage.mtime ?? null) + : null; + const timestampDifferenceMs = + databaseMtime > 0 && vaultMtime !== null && vaultMtime > 0 + ? vaultMtime - databaseMtime + : null; + let timestampRelation: FileRepairTimestampRelation = "unavailable"; + if (timestampDifferenceMs !== null) { + const comparison = compareMTime(vaultMtime!, databaseMtime); + timestampRelation = + comparison === EVEN + ? "same-window" + : comparison === BASE_IS_NEW + ? "vault-newer" + : comparison === TARGET_IS_NEW + ? "database-newer" + : "unavailable"; + } + + return { + recordedSize: revision.metadata.recordedSize, + decodedSize, + recordedToDecodedSizeDifference: + decodedSize === null + ? null + : decodedSize - revision.metadata.recordedSize, + vaultSize, + databaseToVaultSizeDifference: + decodedSize === null || vaultSize === null + ? null + : vaultSize - decodedSize, + databaseMtime, + vaultMtime, + timestampDifferenceMs, + timestampRelation, + }; +} diff --git a/src/serviceFeatures/fileRepairPresentation.unit.spec.ts b/src/serviceFeatures/fileRepairPresentation.unit.spec.ts new file mode 100644 index 00000000..1f5f968c --- /dev/null +++ b/src/serviceFeatures/fileRepairPresentation.unit.spec.ts @@ -0,0 +1,230 @@ +import { describe, expect, it } from "vitest"; +import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { FileRepairInspection, FileRepairRevision } from "./fileRepair"; +import { + getFileRepairRevisionActions, + getFileRepairRevisionComparison, +} from "./fileRepairPresentation"; + +function createInspection( + revision: Partial = {}, + storage: { exists: boolean; size?: number; mtime?: number } = { + exists: true, + size: 12, + mtime: 5_500, + } +): { inspection: FileRepairInspection; revision: FileRepairRevision } { + const completeRevision = { + role: "conflict", + metadata: { + documentId: "f:note", + revision: "2-conflict", + current: false, + deleted: false, + storageType: "plain", + storageLayout: "chunked", + ctime: 1, + mtime: 2_000, + recordedSize: 9, + revisionHistory: [], + chunkReferences: 0, + uniqueChunkReferences: 0, + embeddedChunkReferences: 0, + locallyStoredChunkReferences: 0, + contentAvailableLocally: true, + chunks: [], + }, + contentReadable: true, + contentMatchesStorage: false, + loadedEntry: { + _id: "f:note", + _rev: "2-conflict", + path: "note.md", + ctime: 1, + mtime: 2_000, + size: 9, + type: "plain", + datatype: "plain", + children: [], + eden: {}, + data: "content", + }, + ...revision, + } as FileRepairRevision; + const inspection = { + information: { + path: "note.md", + databasePath: "note.md" as FilePathWithPrefix, + storage, + database: { + source: "local database on this device", + remoteQueried: false, + exists: true, + currentRevision: "3-winner", + conflictCount: 1, + conflictRevisions: ["2-conflict"], + unavailableConflictRevisions: [], + revisions: [], + mergeBases: [], + }, + }, + revisions: [completeRevision], + requiresAttention: true, + } satisfies FileRepairInspection; + return { inspection, revision: completeRevision }; +} + +describe("file repair presentation", () => { + it("offers both reconciliation directions for a readable differing revision", () => { + const { inspection, revision } = createInspection(); + + expect(getFileRepairRevisionActions(inspection, revision)).toEqual({ + compareWithVault: true, + applyRevisionToVault: true, + markAsVaultRevision: false, + storeVaultOnBranch: true, + applyLogicalDeletionToVault: false, + retryRevision: false, + discardRevision: false, + discardBranch: true, + }); + }); + + it("marks an exact matching revision without creating another child", () => { + const { inspection, revision } = createInspection({ + contentMatchesStorage: true, + }); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + compareWithVault: false, + applyRevisionToVault: false, + markAsVaultRevision: true, + storeVaultOnBranch: false, + discardBranch: true, + }); + }); + + it("does not offer a text comparison for a binary file", () => { + const { inspection, revision } = createInspection(); + inspection.information.path = "image.png"; + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + compareWithVault: false, + applyRevisionToVault: true, + storeVaultOnBranch: true, + }); + }); + + it("offers explicit deletion or branch extension for a logical deletion", () => { + const { inspection, revision } = createInspection({ + metadata: { + ...createInspection().revision.metadata, + deleted: true, + }, + contentReadable: true, + contentMatchesStorage: null, + loadedEntry: false, + }); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + applyRevisionToVault: false, + storeVaultOnBranch: true, + applyLogicalDeletionToVault: true, + retryRevision: false, + discardRevision: false, + discardBranch: true, + }); + }); + + it("offers retry, discard, and branch extension for an unreadable live revision", () => { + const { inspection, revision } = createInspection({ + contentReadable: false, + contentMatchesStorage: null, + loadedEntry: false, + }); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + compareWithVault: false, + applyRevisionToVault: false, + markAsVaultRevision: false, + storeVaultOnBranch: true, + retryRevision: true, + discardRevision: false, + discardBranch: true, + }); + }); + + it("keeps the existing unreadable-leaf escape hatch when there is no conflict branch", () => { + const { inspection, revision } = createInspection({ + role: "winner", + contentReadable: false, + contentMatchesStorage: null, + loadedEntry: false, + }); + inspection.information.database.conflictCount = 0; + inspection.information.database.conflictRevisions = []; + inspection.information.database.currentRevision = revision.metadata.revision; + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + discardRevision: true, + discardBranch: false, + }); + }); + + it("does not offer a storage action for a matching absent logical deletion", () => { + const { inspection, revision } = createInspection( + { + metadata: { + ...createInspection().revision.metadata, + deleted: true, + }, + contentReadable: true, + contentMatchesStorage: null, + loadedEntry: false, + }, + { exists: false } + ); + + expect(getFileRepairRevisionActions(inspection, revision)).toMatchObject({ + applyLogicalDeletionToVault: false, + storeVaultOnBranch: false, + }); + }); + + it("reports recorded, decoded, Vault-size, and timestamp differences", () => { + const { inspection, revision } = createInspection(); + + expect(getFileRepairRevisionComparison(inspection, revision)).toEqual({ + recordedSize: 9, + decodedSize: 7, + recordedToDecodedSizeDifference: -2, + vaultSize: 12, + databaseToVaultSizeDifference: 5, + databaseMtime: 2_000, + vaultMtime: 5_500, + timestampDifferenceMs: 3_500, + timestampRelation: "vault-newer", + }); + }); + + it("uses the same two-second timestamp comparison window as synchronisation", () => { + const { inspection, revision } = createInspection( + { + metadata: { + ...createInspection().revision.metadata, + mtime: 3_001, + }, + }, + { + exists: true, + size: 12, + mtime: 3_999, + } + ); + + expect(getFileRepairRevisionComparison(inspection, revision)).toMatchObject({ + timestampDifferenceMs: 998, + timestampRelation: "same-window", + }); + }); +}); diff --git a/src/serviceFeatures/onLayoutReady/enablei18n.ts b/src/serviceFeatures/onLayoutReady/enablei18n.ts index fe8e98a8..5a83eafb 100644 --- a/src/serviceFeatures/onLayoutReady/enablei18n.ts +++ b/src/serviceFeatures/onLayoutReady/enablei18n.ts @@ -1,26 +1,66 @@ -import { getLanguage } from "@/deps"; -import { createServiceFeature } from "@lib/interfaces/ServiceModule"; -import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta"; -import { $msg, __onMissingTranslation, setLang } from "@lib/common/i18n"; +import { getLanguage, Notice, requireApiVersion } from "@/deps"; +import { createServiceFeature } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@/common/rosetta"; +import { $msg, __onMissingTranslation, setLang } from "@/common/translation"; +import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -function tryGetLanguage() { - try { - // Note: 1.8.7+ is required. but it is 18, Feb., 2025. we want to fallback on earlier versions, so we catch the error here. - // eslint-disable-next-line obsidianmd/no-unsupported-api - return getLanguage(); - } catch (e) { - console.error("Failed to get Obsidian language, defaulting to 'def'", e); - return "en"; +function tryGetLanguage(onError: (error: unknown) => void) { + if (requireApiVersion("1.8.7")) { + try { + return getLanguage(); + } catch (e) { + onError(e); + } + } + return "en"; +} + +class ObsidianLanguageAppliedNotice { + private reminder: Notice | undefined; + + show(openDetails: () => void): void { + this.clear(); + let reminderAnchor: HTMLAnchorElement | undefined; + const appliedMessage = + $msg("dialog.yourLanguageAvailable") + .split(/\r?\n\s*\r?\n/u, 1)[0] + ?.trim() ?? $msg("Display Language"); + const fragment = createFragment((documentFragment) => { + documentFragment.createSpan({ + text: `${appliedMessage} `, + }); + documentFragment.createEl("a", { text: $msg("Open the dialog") }, (anchor) => { + reminderAnchor = anchor; + anchor.addEventListener("click", (event) => { + event.preventDefault(); + this.clear(); + openDetails(); + }); + }); + }); + this.reminder = new Notice(fragment, 0); + reminderAnchor?.closest(".notice")?.classList.add("livesync-language-applied-notice"); + } + + clear(): void { + this.reminder?.hide(); + this.reminder = undefined; } } -export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => { +export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API, appLifecycle } }) => { // Clear missing translation handler to avoid unnecessary warnings. __onMissingTranslation(() => {}); let isChanged = false; const settings = setting.currentSettings(); if (settings.displayLanguage == "") { - const obsidianLanguage = tryGetLanguage(); + const obsidianLanguage = tryGetLanguage((error) => { + API.addLog( + `Failed to get Obsidian language; defaulting to 'en': ${String(error)}`, + LOG_LEVEL_VERBOSE, + "i18n-language" + ); + }); if ( SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported obsidianLanguage != settings.displayLanguage // Check if the language is different from the current setting @@ -29,26 +69,48 @@ export const enableI18nFeature = createServiceFeature(async ({ services: { setti // settings.displayLanguage = obsidianLanguage as I18N_LANGS; await setting.applyPartial({ displayLanguage: obsidianLanguage as I18N_LANGS }); isChanged = true; - setLang(settings.displayLanguage); + setLang(obsidianLanguage as I18N_LANGS); } else if (settings.displayLanguage == "") { // settings.displayLanguage = "def"; await setting.applyPartial({ displayLanguage: "def" }); - setLang(settings.displayLanguage); + setLang("def"); await setting.saveSettingData(); } } if (isChanged) { - const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault"); - if ( - (await API.confirm.askSelectStringDialogue($msg(`dialog.yourLanguageAvailable`), ["OK", revert], { - defaultAction: "OK", - title: $msg(`dialog.yourLanguageAvailable.Title`), - })) == revert - ) { - await setting.applyPartial({ displayLanguage: "def" }); - setLang(settings.displayLanguage); - } await setting.saveSettingData(); + const reminder = new ObsidianLanguageAppliedNotice(); + appLifecycle.onUnload.addHandler(() => { + reminder.clear(); + return Promise.resolve(true); + }); + reminder.show(() => { + void (async () => { + try { + const revert = $msg("dialog.yourLanguageAvailable.btnRevertToDefault"); + if ( + (await API.confirm.askSelectStringDialogue( + $msg(`dialog.yourLanguageAvailable`), + ["OK", revert], + { + defaultAction: "OK", + title: $msg("Display Language"), + } + )) == revert + ) { + await setting.applyPartial({ displayLanguage: "def" }); + setLang("def"); + await setting.saveSettingData(); + } + } catch (error) { + API.addLog( + `Failed to open translation details: ${String(error)}`, + LOG_LEVEL_VERBOSE, + "i18n-language" + ); + } + })(); + }); } return true; }); diff --git a/src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts b/src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts new file mode 100644 index 00000000..8f88eb62 --- /dev/null +++ b/src/serviceFeatures/onLayoutReady/enablei18n.unit.spec.ts @@ -0,0 +1,110 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const noticeState = vi.hoisted(() => ({ + instances: [] as Array<{ hide: ReturnType; duration: number }>, + spanTexts: [] as string[], +})); + +vi.mock("@/deps", () => ({ + getLanguage: () => "ja", + requireApiVersion: () => true, + Notice: class { + hide = vi.fn(); + + constructor(_fragment: unknown, duration: number) { + noticeState.instances.push({ hide: this.hide, duration }); + } + }, +})); + +vi.mock("@/common/translation", () => ({ + $msg: (key: string) => + ({ + "dialog.yourLanguageAvailable": "Translation has been applied.\n\nMore details.", + "dialog.yourLanguageAvailable.btnRevertToDefault": "Keep Default", + "dialog.yourLanguageAvailable.Title": "Translation is available!", + "Display Language": "Display language", + "Open the dialog": "Open the dialogue", + })[key] ?? key, + __onMissingTranslation: vi.fn(), + setLang: vi.fn(), +})); + +import { enableI18nFeature } from "./enablei18n.ts"; + +describe("automatic display language", () => { + let clickDetails: ((event: { preventDefault(): void }) => void) | undefined; + + beforeEach(() => { + noticeState.instances.length = 0; + noticeState.spanTexts.length = 0; + clickDetails = undefined; + vi.stubGlobal("createFragment", (build: (fragment: unknown) => void) => { + const anchor = { + addEventListener: (_event: string, listener: (event: { preventDefault(): void }) => void) => { + clickDetails = listener; + }, + closest: () => ({ classList: { add: vi.fn() } }), + }; + const fragment = { + createSpan: ({ text }: { text: string }) => noticeState.spanTexts.push(text), + createEl: (_tag: string, _options: unknown, configure: (element: typeof anchor) => void) => { + configure(anchor); + return anchor; + }, + }; + build(fragment); + return fragment; + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("lets start-up continue and opens translation details only from a persistent Notice", async () => { + const settings = { displayLanguage: "" }; + const applyPartial = vi.fn(async (partial: Partial) => Object.assign(settings, partial)); + const saveSettingData = vi.fn().mockResolvedValue(undefined); + const askSelectStringDialogue = vi.fn().mockResolvedValue("Keep Default"); + const unloadHandlers: Array<() => Promise> = []; + const host = { + services: { + setting: { + currentSettings: () => settings, + applyPartial, + saveSettingData, + }, + API: { + addLog: vi.fn(), + confirm: { askSelectStringDialogue }, + }, + appLifecycle: { + onUnload: { + addHandler: (handler: () => Promise) => unloadHandlers.push(handler), + }, + }, + }, + }; + + await expect(enableI18nFeature(host as never)).resolves.toBe(true); + + expect(settings.displayLanguage).toBe("ja"); + expect(saveSettingData).toHaveBeenCalledOnce(); + expect(askSelectStringDialogue).not.toHaveBeenCalled(); + expect(noticeState.instances).toHaveLength(1); + expect(noticeState.instances[0]?.duration).toBe(0); + expect(noticeState.spanTexts).toEqual(["Translation has been applied. "]); + expect(clickDetails).toBeTypeOf("function"); + + clickDetails?.({ preventDefault: vi.fn() }); + await vi.waitFor(() => expect(askSelectStringDialogue).toHaveBeenCalledOnce()); + expect(askSelectStringDialogue.mock.calls[0]?.[2]).toMatchObject({ title: "Display language" }); + await vi.waitFor(() => expect(settings.displayLanguage).toBe("def")); + expect(saveSettingData).toHaveBeenCalledTimes(2); + + await expect(unloadHandlers[0]?.()).resolves.toBe(true); + expect(noticeState.instances[0]?.hide).toHaveBeenCalled(); + }); +}); diff --git a/src/serviceFeatures/redFlag.simpleFetch.ts b/src/serviceFeatures/redFlag.simpleFetch.ts index 981556d6..214cad2e 100644 --- a/src/serviceFeatures/redFlag.simpleFetch.ts +++ b/src/serviceFeatures/redFlag.simpleFetch.ts @@ -1,7 +1,7 @@ import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type LogFunction } from "@lib/services/lib/logUtils"; -import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { type LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { UnresolvedErrorManager } from "@vrtmrz/livesync-commonlib/compat/services/base/UnresolvedErrorManager"; import { ExtraOnLocal, ExtraOnRemote, @@ -9,12 +9,12 @@ import { normaliseFullScanOptions, synchroniseAllFilesBetweenDBandStorage, type FullScanOptions, -} from "@lib/serviceFeatures/offlineScanner"; +} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner"; import { adjustSettingToRemoteIfNeeded, processVaultInitialisation } from "./redFlag"; export const SIMPLE_FETCH_STAGE1_REMOTE_WINS = "Overwrite all with remote files"; export const SIMPLE_FETCH_STAGE1_NEWER_WINS = "Compare time and take newer"; -export const SIMPLE_FETCH_STAGE1_LEGACY = "Use the detailed flow"; +export const SIMPLE_FETCH_STAGE1_DETAILED = "Use the detailed flow"; export const SIMPLE_FETCH_STAGE1_CANCEL = "Cancel"; export const SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE = "Keep local files even if not on remote"; @@ -27,8 +27,8 @@ export const STAGE2_ABORT = "Cancel all and reboot"; const SIMPLE_FETCH_MODE_KEY = "simple-fetch-mode"; function buildSimpleFetchResult(stage1: string, stage2?: string) { - if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { - return { mode: "legacy", options: {} }; + if (stage1 === SIMPLE_FETCH_STAGE1_DETAILED) { + return { mode: "detailed", options: {} }; } if (stage1 === SIMPLE_FETCH_STAGE1_REMOTE_WINS && stage2) { if (![SIMPLE_FETCH_STAGE2_REMOTE_DELETE_ALL, SIMPLE_FETCH_STAGE2_REMOTE_DELETE_NONE].includes(stage2)) { @@ -93,14 +93,14 @@ export async function askSimpleFetchMode( const msg = `We are about to retrieve the remote data. -Firstly, how shall we handle the data retrieved from this remote server? +Firstly, how shall we handle the data retrieved from this remote source? - **${SIMPLE_FETCH_STAGE1_NEWER_WINS}**: Compares the modified time of files and takes the newer one. If you have been using Self-hosted LiveSync and have made changes on multiple devices, this option may be suitable for you as it tries to merge changes based on modified time. - **${SIMPLE_FETCH_STAGE1_REMOTE_WINS}**: Remote data is the source of truth. If you are new to using Self-hosted LiveSync. This option may be easiest to understand and get started with. It will overwrite all your local files with the remote data, so please make sure you have a backup if there is any important data in your vault. -- **${SIMPLE_FETCH_STAGE1_LEGACY}**: Opens the detailed setup wizard. +- **${SIMPLE_FETCH_STAGE1_DETAILED}**: Opens the detailed setup wizard. If you want to have more control over the synchronisation process, or want to review the changes before applying, you can choose this option to use the detailed flow. `; const stage1 = await host.services.UI.confirm.confirmWithMessage( @@ -109,7 +109,7 @@ Firstly, how shall we handle the data retrieved from this remote server? [ SIMPLE_FETCH_STAGE1_NEWER_WINS, SIMPLE_FETCH_STAGE1_REMOTE_WINS, - SIMPLE_FETCH_STAGE1_LEGACY, + SIMPLE_FETCH_STAGE1_DETAILED, SIMPLE_FETCH_STAGE1_CANCEL, ], SIMPLE_FETCH_STAGE1_NEWER_WINS, @@ -118,7 +118,7 @@ Firstly, how shall we handle the data retrieved from this remote server? if (!stage1 || stage1 === SIMPLE_FETCH_STAGE1_CANCEL) return "cancelled"; - if (stage1 === SIMPLE_FETCH_STAGE1_LEGACY) { + if (stage1 === SIMPLE_FETCH_STAGE1_DETAILED) { return buildSimpleFetchResult(stage1)!; } @@ -204,8 +204,8 @@ export async function askAndPerformFastSetupOnScheduledFetchAll( host.services.appLifecycle.performRestart(); return false; } - if (result.mode === "legacy") { - return undefined; // Let the legacy flow handle it. + if (result.mode === "detailed") { + return undefined; // Let the detailed setup flow handle it. } return await processVaultInitialisation(host, log, async () => { @@ -215,7 +215,7 @@ export async function askAndPerformFastSetupOnScheduledFetchAll( await host.serviceModules.rebuilder.$fetchLocalDBFast(false); // 2. Call the extended synchroniseAllFilesBetweenDBandStorage to reflect changes in storage - const errorManager = new UnresolvedErrorManager(host.services.appLifecycle); + const errorManager = new UnresolvedErrorManager(host.services.appLifecycle, host.services.context.events); const syncResult = await synchroniseAllFilesBetweenDBandStorage( host, log, diff --git a/src/serviceFeatures/redFlag.ts b/src/serviceFeatures/redFlag.ts index 4fe9d8d8..67399057 100644 --- a/src/serviceFeatures/redFlag.ts +++ b/src/serviceFeatures/redFlag.ts @@ -1,20 +1,24 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { createInstanceLogFunction, type LogFunction } from "@lib/services/lib/logUtils"; -import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { createInstanceLogFunction, type LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { + FlagFilesHumanReadable, + FlagFilesOriginal, +} from "@vrtmrz/livesync-commonlib/compat/common/models/redflag.const"; import FetchEverything from "@/modules/features/SetupWizard/dialogs/FetchEverything.svelte"; import RebuildEverything from "@/modules/features/SetupWizard/dialogs/RebuildEverything.svelte"; import { extractObject } from "octagonal-wheels/object"; -import { REMOTE_MINIO, REMOTE_P2P } from "@lib/common/models/setting.const"; -import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type"; -import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition"; +import { REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; +import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/settings"; +import { TweakValuesShouldMatchedTemplate } from "@vrtmrz/livesync-commonlib/compat/common/models/tweak.definition"; import type { FetchEverythingResult, RebuildEverythingResult, } from "@/modules/features/SetupWizard/dialogs/setupDialogTypes"; import { askAndPerformFastSetupOnScheduledFetchAll } from "./redFlag.simpleFetch"; -import { ConnectionStringParser } from "@lib/common/ConnectionString"; -import { activateRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig"; +import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; +import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations"; +import { isP2PMainRemote } from "@/common/remoteConfiguration"; /** * Flag file handler interface, similar to target filter pattern. @@ -382,8 +386,11 @@ export function createRebuildFlagHandler( // Handle the rebuild everything scheduled operation const onScheduled = async () => { - const method = - await host.services.UI.dialogManager.openWithExplicitCancel(RebuildEverything); + const settings = host.services.setting.currentSettings(); + const method = await host.services.UI.dialogManager.openWithExplicitCancel< + RebuildEverythingResult, + { isP2P: boolean } + >(RebuildEverything, { isP2P: isP2PMainRemote(settings) }); if (method === "cancelled") { log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE); await cleanupFlag(); @@ -391,7 +398,6 @@ export function createRebuildFlagHandler( return false; } const { extra } = method; - const settings = host.services.setting.currentSettings(); await adjustSettingToRemoteIfNeeded(host, log, extra, settings); return await processVaultInitialisation(host, log, async () => { await host.serviceModules.rebuilder.$rebuildEverything(); diff --git a/src/serviceFeatures/redFlag.unit.spec.ts b/src/serviceFeatures/redFlag.unit.spec.ts index f94103e0..de8d998c 100644 --- a/src/serviceFeatures/redFlag.unit.spec.ts +++ b/src/serviceFeatures/redFlag.unit.spec.ts @@ -1,7 +1,11 @@ import { describe, it, expect, vi } from "vitest"; -import type { LogFunction } from "@lib/services/lib/logUtils"; -import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const"; -import { REMOTE_MINIO } from "@lib/common/models/setting.const"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { + FlagFilesHumanReadable, + FlagFilesOriginal, +} from "@vrtmrz/livesync-commonlib/compat/common/models/redflag.const"; +import { REMOTE_MINIO, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; import { createFetchAllFlagHandler, createRebuildFlagHandler, @@ -18,14 +22,14 @@ import { TweakValuesRecommendedTemplate, TweakValuesShouldMatchedTemplate, TweakValuesTemplate, -} from "@lib/common/types"; +} from "@vrtmrz/livesync-commonlib/compat/common/types"; import { ExtraOnLocal, FullScanModes, synchroniseAllFilesBetweenDBandStorage, -} from "@lib/serviceFeatures/offlineScanner"; +} from "@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner"; import { - SIMPLE_FETCH_STAGE1_LEGACY, + SIMPLE_FETCH_STAGE1_DETAILED, SIMPLE_FETCH_STAGE1_NEWER_WINS, SIMPLE_FETCH_STAGE1_REMOTE_WINS, SIMPLE_FETCH_STAGE2_NEWER_CLEANUP, @@ -36,9 +40,9 @@ import { askAndPerformFastSetupOnScheduledFetchAll, askSimpleFetchMode, } from "./redFlag.simpleFetch"; -import { activateRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig"; +import { activateRemoteConfiguration } from "@vrtmrz/livesync-commonlib/remote-configurations"; //Mock synchroniseAllFilesBetweenDBandStorage -vi.mock("@/lib/src/serviceFeatures/offlineScanner", async (importOriginal) => { +vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/offlineScanner", async (importOriginal) => { const originalModule = (await importOriginal()) as any; return { ...originalModule, @@ -46,7 +50,7 @@ vi.mock("@/lib/src/serviceFeatures/offlineScanner", async (importOriginal) => { }; }); -vi.mock("@lib/serviceFeatures/remoteConfig", () => { +vi.mock("@vrtmrz/livesync-commonlib/compat/serviceFeatures/remoteConfig", () => { return { activateRemoteConfiguration: vi.fn((settings: any, configurationId: string) => { if (!settings?.remoteConfigurations?.[configurationId]) return false; @@ -159,6 +163,7 @@ const createHostMock = () => { return { services: { + context: createServiceContext(), setting: settingMock, appLifecycle: appLifecycleMock, UI: uiMock, @@ -464,16 +469,19 @@ describe("Red Flag Feature", () => { expect(result).toBe(true); expect(host.mocks.rebuilder.$fetchLocalDBFast).toHaveBeenCalled(); expect(synchroniseAllFilesBetweenDBandStorage).toHaveBeenCalled(); + const firstPrompt = host.mocks.ui.confirm.confirmWithMessage.mock.calls[0]?.[1]; + expect(firstPrompt).toContain("data retrieved from this remote source"); + expect(firstPrompt).not.toContain("remote server"); // We can't easily check performFullScan call here because it's imported, // but we can verify rebuilder was called. }); - it("should restore legacy fetch flow when requested", async () => { + it("opens the detailed Fetch flow when requested", async () => { const host = createHostMock(); const log = createLoggerMock(); host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({ vault: "identical", backup: "backup_skipped", @@ -657,11 +665,11 @@ describe("Red Flag Feature", () => { await expect(askSimpleFetchMode(host as any)).resolves.toBe("cancelled"); }); - it("should return legacy mode when selected", async () => { + it("selects the detailed Fetch flow", async () => { const host = createHostMock(); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); - await expect(askSimpleFetchMode(host as any)).resolves.toEqual({ mode: "legacy", options: {} }); + await expect(askSimpleFetchMode(host as any)).resolves.toEqual({ mode: "detailed", options: {} }); }); it("should return remote-only with keep-local option", async () => { @@ -810,12 +818,12 @@ describe("Red Flag Feature", () => { expect(host.mocks.appLifecycle.performRestart).toHaveBeenCalled(); }); - it("should return undefined when legacy mode is selected", async () => { + it("leaves the detailed Fetch flow to its existing handler", async () => { const host = createHostMock(); const log = createLoggerMock(); const cleanupFlag = vi.fn().mockResolvedValue(undefined); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); const result = await askAndPerformFastSetupOnScheduledFetchAll(host as any, log, cleanupFlag); @@ -866,6 +874,20 @@ describe("Red Flag Feature", () => { }); describe("Rebuild All Flag Handler", () => { + it("identifies P2P when opening the scheduled rebuild confirmation", async () => { + const host = createHostMock(); + const log = createLoggerMock(); + host.mocks.setting.settings.remoteType = REMOTE_P2P; + host.mocks.storageAccess.files.add(FlagFilesOriginal.REBUILD_ALL); + host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled"); + + await createRebuildFlagHandler(host as any, log).handle(); + + expect(host.mocks.ui.dialogManager.openWithExplicitCancel).toHaveBeenCalledWith(expect.anything(), { + isP2P: true, + }); + }); + it("should detect rebuild all flag using original filename", async () => { const host = createHostMock(); const log = createLoggerMock(); @@ -1455,7 +1477,7 @@ describe("Red Flag Feature", () => { host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL); host.mocks.tweakValue.fetchRemotePreferred.mockResolvedValueOnce({}); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled"); const handler = createFetchAllFlagHandler(host as any, log); @@ -1537,7 +1559,7 @@ describe("Red Flag Feature", () => { } as any); host.mocks.storageAccess.files.add(FlagFilesOriginal.FETCH_ALL); - host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_LEGACY); + host.mocks.ui.confirm.confirmWithMessage.mockResolvedValueOnce(SIMPLE_FETCH_STAGE1_DETAILED); host.mocks.ui.dialogManager.openWithExplicitCancel.mockResolvedValueOnce({ vault: "identical", extra: {} }); host.mocks.rebuilder.$fetchLocal.mockResolvedValueOnce(); const handler = createFetchAllFlagHandler(host as any, log); diff --git a/src/serviceFeatures/setupObsidian/qrCode.ts b/src/serviceFeatures/setupObsidian/qrCode.ts new file mode 100644 index 00000000..844719c2 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/qrCode.ts @@ -0,0 +1,79 @@ +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { + encodeQR, + encodeSettingsToQRCodeData, + OutputFormat, +} from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; +import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { SetupFeatureHost } from "./types"; + +export async function encodeSetupSettingsAsQR(host: SetupFeatureHost) { + const settingString = encodeSettingsToQRCodeData(host.services.setting.currentSettings()); + const result = encodeQR(settingString, OutputFormat.SVG); + if (result === "") { + return ""; + } + + if (typeof result === "string") { + const msg = host.services.context.translate("Setup.QRCode", { qr_image: result }); + await host.services.UI.confirm.confirmWithMessage("Settings QR Code", msg, ["OK"], "OK"); + return result; + } else { + // Multi-page QR code + let currentIndex = 0; + while (currentIndex < result.total) { + const msg = `The setting is too large for a single QR code. +We are using the aggregator to combine multiple QR codes. +Your settings will not be sent to any server; they will be processed only on your device. +Please scan this QR code with your mobile's camera, and open the page in your browser. +After all parts are collected, the page will navigate you back to Obsidian with the aggregated settings. + +Progress: ${currentIndex + 1} / ${result.total} +${result.parts[currentIndex]}`; + + const buttons = []; + if (currentIndex > 0) buttons.push("Back"); + if (currentIndex < result.total - 1) { + buttons.push("Next"); + buttons.push("Cancel"); + } else { + buttons.push("Done"); + } + + const choice = await host.services.UI.confirm.confirmWithMessage( + "Settings QR Code (Aggregated)", + msg, + buttons, + buttons[buttons.indexOf("Next") !== -1 ? buttons.indexOf("Next") : buttons.indexOf("Done")] + ); + + if (choice === "Next") { + currentIndex++; + } else if (choice === "Back") { + currentIndex--; + } else { + break; + } + } + return result.parts[0]; // Return the first one for compatibility + } +} + +export function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>) { + host.services.appLifecycle.onLoaded.addHandler(() => { + host.services.API.addCommand({ + id: "livesync-setting-qr", + name: "Show settings as a QR code", + checkCallback: (checking) => { + if (!host.services.setting.currentSettings().isConfigured) return false; + if (!checking) fireAndForget(encodeSetupSettingsAsQR(host)); + return true; + }, + }); + host.services.context.events.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () => + fireAndForget(() => encodeSetupSettingsAsQR(host)) + ); + return Promise.resolve(true); + }); +} diff --git a/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts b/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts new file mode 100644 index 00000000..61244f7c --- /dev/null +++ b/src/serviceFeatures/setupObsidian/qrCode.unit.spec.ts @@ -0,0 +1,157 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { EVENT_REQUEST_SHOW_SETUP_QR } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { encodeSetupSettingsAsQR, useSetupQRCodeFeature } from "./qrCode"; +import { encodeQR, encodeSettingsToQRCodeData } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; + +vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => { + return { + encodeQR: vi.fn(), + encodeSettingsToQRCodeData: vi.fn(), + OutputFormat: { + SVG: "svg", + }, + }; +}); + +describe("setupObsidian/qrCode", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it("encodeSetupSettingsAsQR should return empty string when QR generation fails", async () => { + const confirmWithMessage = vi.fn(); + const host = { + services: { + context: createServiceContext(), + setting: { + currentSettings: vi.fn(() => ({ any: "settings" })), + }, + UI: { + confirm: { + confirmWithMessage, + }, + }, + }, + } as any; + + vi.mocked(encodeSettingsToQRCodeData).mockReturnValue("encoded-settings"); + vi.mocked(encodeQR).mockReturnValue(""); + + const result = await encodeSetupSettingsAsQR(host); + + expect(result).toBe(""); + expect(confirmWithMessage).not.toHaveBeenCalled(); + }); + + it("encodeSetupSettingsAsQR should show confirm dialog when QR is generated", async () => { + const confirmWithMessage = vi.fn(() => true); + const translate = vi.fn(() => "qr-message"); + const host = { + services: { + context: createServiceContext({ translate }), + setting: { + currentSettings: vi.fn(() => ({ any: "settings" })), + }, + UI: { + confirm: { + confirmWithMessage, + }, + }, + }, + } as any; + + vi.mocked(encodeSettingsToQRCodeData).mockReturnValue("encoded-settings"); + vi.mocked(encodeQR).mockReturnValue(""); + + const result = await encodeSetupSettingsAsQR(host); + + expect(result).toBe(""); + expect(translate).toHaveBeenCalledWith("Setup.QRCode", { qr_image: "" }); + expect(confirmWithMessage).toHaveBeenCalledWith("Settings QR Code", "qr-message", ["OK"], "OK"); + }); + + it("useSetupQRCodeFeature should register onLoaded handler that wires command and event", async () => { + const addHandler = vi.fn(); + const addCommand = vi.fn(); + const context = createServiceContext(); + const onEventSpy = vi.spyOn(context.events, "onEvent"); + + const host = { + services: { + context, + API: { + addCommand, + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => ({ any: "settings" })), + }, + UI: { + confirm: { + confirmWithMessage: vi.fn(), + }, + }, + }, + } as any; + + useSetupQRCodeFeature(host); + expect(addHandler).toHaveBeenCalledTimes(1); + + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + expect(addCommand).toHaveBeenCalledWith( + expect.objectContaining({ + id: "livesync-setting-qr", + name: "Show settings as a QR code", + }) + ); + expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_SHOW_SETUP_QR, expect.any(Function)); + }); + + it("keeps the QR command out of the palette until setup is complete", async () => { + const addHandler = vi.fn(); + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings = { isConfigured: false }; + const host = { + services: { + context: createServiceContext(), + API: { + addCommand: vi.fn((command) => commands.push(command)), + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => settings), + }, + UI: { + confirm: { + confirmWithMessage: vi.fn(), + }, + }, + }, + } as any; + + useSetupQRCodeFeature(host); + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + const command = commands.find((candidate) => candidate.id === "livesync-setting-qr")!; + expect(command.checkCallback?.(true)).toBe(false); + + settings.isConfigured = true; + expect(command.checkCallback?.(true)).toBe(true); + }); +}); diff --git a/src/serviceFeatures/setupObsidian/settingsReset.ts b/src/serviceFeatures/setupObsidian/settingsReset.ts new file mode 100644 index 00000000..10862195 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/settingsReset.ts @@ -0,0 +1,10 @@ +import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings"; + +export function createEditingSettingsAfterFullReset(editingSettings: T): T { + return { ...editingSettings, ...createNewVaultSettings(), isConfigured: false }; +} + +export function createCoreSettingsAfterFullReset(): ObsidianLiveSyncSettings { + return { ...createNewVaultSettings(), isConfigured: false }; +} diff --git a/src/serviceFeatures/setupObsidian/settingsReset.unit.spec.ts b/src/serviceFeatures/setupObsidian/settingsReset.unit.spec.ts new file mode 100644 index 00000000..4d0a5cf6 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/settingsReset.unit.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings"; +import { createCoreSettingsAfterFullReset, createEditingSettingsAfterFullReset } from "./settingsReset.ts"; + +describe("full settings reset", () => { + it("resets the persisted settings to the recommended new-Vault values", () => { + const settings = createCoreSettingsAfterFullReset(); + expect(settings).toEqual({ + ...createNewVaultSettings(), + isConfigured: false, + }); + }); + + it("preserves settings-dialog fields while applying the new-Vault values", () => { + const editing = { + ...DEFAULT_SETTINGS, + configPassphrase: "dialog-only", + } as ObsidianLiveSyncSettings & { configPassphrase: string }; + + expect(createEditingSettingsAfterFullReset(editing)).toEqual({ + ...editing, + ...createNewVaultSettings(), + isConfigured: false, + }); + }); +}); diff --git a/src/serviceFeatures/setupObsidian/setupActivationLifecycle.ts b/src/serviceFeatures/setupObsidian/setupActivationLifecycle.ts new file mode 100644 index 00000000..3b4f2a25 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/setupActivationLifecycle.ts @@ -0,0 +1,35 @@ +export type SetupInitialisationMode = "fetch" | "rebuild"; + +export interface SetupInitialisationScheduler { + scheduleFetch(prepareBeforeRestart?: () => Promise): Promise; + scheduleRebuild(prepareBeforeRestart?: () => Promise): Promise; +} + +/** + * Reserves the next-start initialisation operation before enabling settings. + * The scheduler owns suspension, rollback, and restart ordering. + */ +export function applySettingsWithScheduledInitialisation( + scheduler: SetupInitialisationScheduler, + mode: SetupInitialisationMode, + applySettings: () => Promise +): Promise { + return mode === "fetch" ? scheduler.scheduleFetch(applySettings) : scheduler.scheduleRebuild(applySettings); +} + +/** + * Uses Fetch only for the transition from an unconfigured device to an + * explicitly configured existing device. Ordinary edits apply immediately. + */ +export async function applySettingsAndFetchOnActivation( + scheduler: SetupInitialisationScheduler, + wasConfigured: boolean | undefined, + willBeConfigured: boolean | undefined, + applySettings: () => Promise +): Promise { + if (!wasConfigured && willBeConfigured) { + return await applySettingsWithScheduledInitialisation(scheduler, "fetch", applySettings); + } + await applySettings(); + return true; +} diff --git a/src/serviceFeatures/setupObsidian/setupActivationLifecycle.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupActivationLifecycle.unit.spec.ts new file mode 100644 index 00000000..89b1bc74 --- /dev/null +++ b/src/serviceFeatures/setupObsidian/setupActivationLifecycle.unit.spec.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from "vitest"; +import { + applySettingsAndFetchOnActivation, + applySettingsWithScheduledInitialisation, + type SetupInitialisationScheduler, +} from "./setupActivationLifecycle"; + +function createScheduler() { + const events: string[] = []; + const scheduler: SetupInitialisationScheduler = { + scheduleFetch: vi.fn(async (prepare) => { + events.push("fetch-reserved"); + await prepare?.(); + return true; + }), + scheduleRebuild: vi.fn(async (prepare) => { + events.push("rebuild-reserved"); + await prepare?.(); + return true; + }), + }; + const applySettings = vi.fn(async () => { + events.push("settings-applied"); + }); + return { applySettings, events, scheduler }; +} + +describe("setup activation lifecycle", () => { + it.each([ + ["fetch", "fetch-reserved"], + ["rebuild", "rebuild-reserved"], + ] as const)("reserves %s before applying settings", async (mode, reservedEvent) => { + const { applySettings, events, scheduler } = createScheduler(); + + await expect(applySettingsWithScheduledInitialisation(scheduler, mode, applySettings)).resolves.toBe(true); + + expect(events).toEqual([reservedEvent, "settings-applied"]); + }); + + it("reserves Fetch when existing settings activate an unconfigured device", async () => { + const { applySettings, events, scheduler } = createScheduler(); + + await expect(applySettingsAndFetchOnActivation(scheduler, false, true, applySettings)).resolves.toBe(true); + + expect(events).toEqual(["fetch-reserved", "settings-applied"]); + }); + + it("applies an ordinary configured-device edit without scheduling initialisation", async () => { + const { applySettings, events, scheduler } = createScheduler(); + + await expect(applySettingsAndFetchOnActivation(scheduler, true, true, applySettings)).resolves.toBe(true); + + expect(events).toEqual(["settings-applied"]); + expect(scheduler.scheduleFetch).not.toHaveBeenCalled(); + expect(scheduler.scheduleRebuild).not.toHaveBeenCalled(); + }); + + it("does not apply settings when the scheduler cannot reserve its flag", async () => { + const { applySettings, scheduler } = createScheduler(); + vi.mocked(scheduler.scheduleFetch).mockResolvedValueOnce(false); + + await expect(applySettingsWithScheduledInitialisation(scheduler, "fetch", applySettings)).resolves.toBe(false); + + expect(applySettings).not.toHaveBeenCalled(); + }); +}); diff --git a/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts b/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts index 4eb9e425..d745a563 100644 --- a/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts +++ b/src/serviceFeatures/setupObsidian/setupManagerHandlers.ts @@ -1,9 +1,38 @@ import { type SetupManager, UserMode } from "@/modules/features/SetupManager"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; -import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents"; -import { eventHub } from "@lib/hub/hub"; -import { fireAndForget } from "@lib/common/utils"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; +import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types"; +import { + EVENT_REQUEST_OPEN_P2P_SETTINGS, + EVENT_REQUEST_OPEN_SETUP_URI, +} from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { $msg } from "@/common/translation"; + +const ONBOARDING_NOTICE_DURATION_MS = 60_000; + +export async function openOnboarding(setupManager: SetupManager) { + return await setupManager.startOnBoarding(); +} + +export function showOnboardingInvitation(host: NecessaryServices<"UI", never>, setupManager: SetupManager): void { + const message = `${$msg("Welcome to Self-hosted LiveSync")} ${$msg( + "We will now guide you through a few questions to simplify the synchronisation setup." + )} {HERE}`; + host.services.UI.confirm.askInPopup( + "initial-onboarding", + message, + (anchor) => { + anchor.href = "#"; + anchor.classList.add("sls-onboarding-invitation-action"); + anchor.textContent = $msg("Ui.SetupWizard.Invitation.Start"); + anchor.addEventListener("click", (event) => { + event.preventDefault(); + fireAndForget(() => openOnboarding(setupManager)); + }); + }, + ONBOARDING_NOTICE_DURATION_MS + ); +} export async function openSetupURI(setupManager: SetupManager) { await setupManager.onUseSetupURI(UserMode.Unknown); @@ -24,8 +53,10 @@ export function useSetupManagerHandlersFeature( callback: () => fireAndForget(openSetupURI(setupManager)), }); - eventHub.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => fireAndForget(() => openSetupURI(setupManager))); - eventHub.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () => + host.services.context.events.onEvent(EVENT_REQUEST_OPEN_SETUP_URI, () => + fireAndForget(() => openSetupURI(setupManager)) + ); + host.services.context.events.onEvent(EVENT_REQUEST_OPEN_P2P_SETTINGS, () => fireAndForget(() => openP2PSettings(host, setupManager)) ); diff --git a/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts index 067d860c..bdae06ca 100644 --- a/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts +++ b/src/serviceFeatures/setupObsidian/setupManagerHandlers.unit.spec.ts @@ -1,7 +1,15 @@ import { describe, expect, it, vi, afterEach } from "vitest"; -import { eventHub } from "@lib/hub/hub"; -import { EVENT_REQUEST_OPEN_P2P_SETTINGS, EVENT_REQUEST_OPEN_SETUP_URI } from "@lib/events/coreEvents"; -import { openP2PSettings, openSetupURI, useSetupManagerHandlersFeature } from "./setupManagerHandlers"; +import { + EVENT_REQUEST_OPEN_P2P_SETTINGS, + EVENT_REQUEST_OPEN_SETUP_URI, +} from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { + openOnboarding, + openP2PSettings, + openSetupURI, + showOnboardingInvitation, + useSetupManagerHandlersFeature, +} from "./setupManagerHandlers"; vi.mock("@/modules/features/SetupManager", () => { return { @@ -44,16 +52,79 @@ describe("setupObsidian/setupManagerHandlers", () => { expect(setupManager.onP2PManualSetup).toHaveBeenCalledWith("unknown", settings, false); }); - it("useSetupManagerHandlersFeature should register onLoaded handler that wires command and events", async () => { + it("openOnboarding should delegate to SetupManager.startOnBoarding", async () => { + const setupManager = { + startOnBoarding: vi.fn(async () => await Promise.resolve(false)), + } as any; + + await openOnboarding(setupManager); + + expect(setupManager.startOnBoarding).toHaveBeenCalledOnce(); + }); + + it("showOnboardingInvitation should wait for its fixed action link before opening onboarding", async () => { + let configureAnchor: ((anchor: HTMLAnchorElement) => void) | undefined; + const askInPopup = vi.fn( + (_key: string, _text: string, callback: (anchor: HTMLAnchorElement) => void, _durationMs?: number) => { + configureAnchor = callback; + } + ); + const host = { + services: { + UI: { confirm: { askInPopup } }, + }, + } as any; + const setupManager = { + startOnBoarding: vi.fn(async () => await Promise.resolve(false)), + } as any; + + showOnboardingInvitation(host, setupManager); + + expect(setupManager.startOnBoarding).not.toHaveBeenCalled(); + expect(askInPopup).toHaveBeenCalledWith( + "initial-onboarding", + expect.stringContaining("{HERE}"), + expect.any(Function), + 60_000 + ); + + let click: ((event: { preventDefault(): void }) => void) | undefined; + const addClass = vi.fn(); + const anchor = { + href: "", + textContent: "", + classList: { add: addClass }, + addEventListener: vi.fn((_name: string, listener: typeof click) => { + click = listener; + }), + } as unknown as HTMLAnchorElement; + configureAnchor!(anchor); + + expect(anchor.href).toBe("#"); + expect(anchor.textContent).toBe("Start setup"); + expect(addClass).toHaveBeenCalledWith("sls-onboarding-invitation-action"); + const preventDefault = vi.fn(); + click!({ preventDefault }); + await vi.waitFor(() => expect(setupManager.startOnBoarding).toHaveBeenCalledOnce()); + expect(preventDefault).toHaveBeenCalledOnce(); + }); + + it("keeps onboarding out of the command palette while wiring the setup URI command and events", async () => { const addHandler = vi.fn(); const addCommand = vi.fn(); - const onEventSpy = vi.spyOn(eventHub, "onEvent"); + const events = { onEvent: vi.fn() }; const host = { services: { + context: { events }, API: { addCommand, }, + UI: { + confirm: { + askInPopup: vi.fn(), + }, + }, appLifecycle: { onLoaded: { addHandler, @@ -65,6 +136,7 @@ describe("setupObsidian/setupManagerHandlers", () => { }, } as any; const setupManager = { + startOnBoarding: vi.fn(async () => await Promise.resolve(false)), onUseSetupURI: vi.fn(async () => await Promise.resolve(true)), onP2PManualSetup: vi.fn(async () => await Promise.resolve(true)), } as any; @@ -75,13 +147,18 @@ describe("setupObsidian/setupManagerHandlers", () => { const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; await loadedHandler(); + expect(addCommand).not.toHaveBeenCalledWith( + expect.objectContaining({ + id: "livesync-open-onboarding", + }) + ); expect(addCommand).toHaveBeenCalledWith( expect.objectContaining({ id: "livesync-opensetupuri", name: "Use the copied setup URI (Formerly Open setup URI)", }) ); - expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function)); - expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function)); + expect(events.onEvent).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_SETUP_URI, expect.any(Function)); + expect(events.onEvent).toHaveBeenCalledWith(EVENT_REQUEST_OPEN_P2P_SETTINGS, expect.any(Function)); }); }); diff --git a/src/serviceFeatures/setupObsidian/setupProtocol.ts b/src/serviceFeatures/setupObsidian/setupProtocol.ts index 5310fbf8..3c3566ff 100644 --- a/src/serviceFeatures/setupObsidian/setupProtocol.ts +++ b/src/serviceFeatures/setupObsidian/setupProtocol.ts @@ -1,9 +1,9 @@ -import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@lib/common/types"; -import type { LogFunction } from "@lib/services/lib/logUtils"; -import { createInstanceLogFunction } from "@lib/services/lib/logUtils"; -import type { SetupFeatureHost } from "@lib/serviceFeatures/setupObsidian/types"; +import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import type { SetupFeatureHost } from "@/serviceFeatures/setupObsidian/types"; import { configURIBase } from "@/common/types"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; import { type SetupManager, UserMode } from "@/modules/features/SetupManager"; async function handleSetupProtocol(setupManager: SetupManager, conf: Record) { diff --git a/src/serviceFeatures/setupObsidian/setupUri.ts b/src/serviceFeatures/setupObsidian/setupUri.ts new file mode 100644 index 00000000..a4fe4aec --- /dev/null +++ b/src/serviceFeatures/setupObsidian/setupUri.ts @@ -0,0 +1,87 @@ +import { LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { LogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { createInstanceLogFunction } from "@vrtmrz/livesync-commonlib/compat/services/lib/logUtils"; +import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; +import { EVENT_REQUEST_COPY_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import type { SetupFeatureHost } from "./types"; + +export async function askEncryptingPassphrase(host: SetupFeatureHost): Promise { + return await host.services.UI.confirm.askString( + "Encrypt your settings", + "The passphrase to encrypt the setup URI", + "", + true + ); +} + +export async function copySetupURI(host: SetupFeatureHost, log: LogFunction, stripExtra = true) { + const encryptingPassphrase = await askEncryptingPassphrase(host); + if (encryptingPassphrase === false) return; + const encryptedURI = await encodeSettingsToSetupURI( + host.services.setting.currentSettings(), + encryptingPassphrase, + [...((stripExtra ? ["pluginSyncExtendedSetting"] : []) as (keyof ObsidianLiveSyncSettings)[])], + true + ); + if (await host.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) { + log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); + } +} + +export async function copySetupURIFull(host: SetupFeatureHost, log: LogFunction) { + const encryptingPassphrase = await askEncryptingPassphrase(host); + if (encryptingPassphrase === false) return; + const encryptedURI = await encodeSettingsToSetupURI( + host.services.setting.currentSettings(), + encryptingPassphrase, + [], + false + ); + if (await host.services.UI.promptCopyToClipboard("Setup URI", encryptedURI)) { + log("Setup URI copied to clipboard", LOG_LEVEL_NOTICE); + } +} + +export function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setting" | "appLifecycle", never>) { + const log = createInstanceLogFunction("SF:SetupURI", host.services.API); + host.services.appLifecycle.onLoaded.addHandler(() => { + host.services.API.addCommand({ + id: "livesync-copysetupuri", + name: "Copy settings as a new setup URI", + checkCallback: (checking) => { + if (!host.services.setting.currentSettings().isConfigured) return false; + if (!checking) fireAndForget(copySetupURI(host, log)); + return true; + }, + }); + + host.services.API.addCommand({ + id: "livesync-copysetupuri-short", + name: "Copy settings as a new setup URI (With customization sync)", + checkCallback: (checking) => { + const settings = host.services.setting.currentSettings(); + if (!settings.isConfigured || !settings.usePluginSync) return false; + if (!checking) fireAndForget(copySetupURI(host, log, false)); + return true; + }, + }); + + host.services.API.addCommand({ + id: "livesync-copysetupurifull", + name: "Copy settings as a new setup URI (Full)", + checkCallback: (checking) => { + const settings = host.services.setting.currentSettings(); + if (!settings.isConfigured || !settings.useAdvancedMode) return false; + if (!checking) fireAndForget(copySetupURIFull(host, log)); + return true; + }, + }); + + host.services.context.events.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () => + fireAndForget(() => copySetupURI(host, log)) + ); + return Promise.resolve(true); + }); +} diff --git a/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts b/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts new file mode 100644 index 00000000..27ec04ef --- /dev/null +++ b/src/serviceFeatures/setupObsidian/setupUri.unit.spec.ts @@ -0,0 +1,212 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { EVENT_REQUEST_COPY_SETUP_URI } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { askEncryptingPassphrase, copySetupURI, copySetupURIFull, useSetupURIFeature } from "./setupUri"; +import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting"; + +vi.mock("@vrtmrz/livesync-commonlib/compat/API/processSetting", () => { + return { + encodeSettingsToSetupURI: vi.fn(), + }; +}); + +describe("setupObsidian/setupUri", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it("askEncryptingPassphrase should delegate to confirm.askString", async () => { + const askString = vi.fn(() => "secret"); + const host = { + services: { + UI: { + confirm: { + askString, + }, + }, + }, + } as any; + + const result = await askEncryptingPassphrase(host); + expect(result).toBe("secret"); + expect(askString).toHaveBeenCalled(); + }); + + it("copySetupURI should return early when user cancels passphrase", async () => { + const promptCopyToClipboard = vi.fn(); + const host = { + services: { + setting: { + currentSettings: vi.fn(() => ({ foo: "bar" })), + }, + UI: { + confirm: { + askString: vi.fn(() => false), + }, + promptCopyToClipboard, + }, + }, + } as any; + const log = vi.fn(); + + await copySetupURI(host, log); + + expect(encodeSettingsToSetupURI).not.toHaveBeenCalled(); + expect(promptCopyToClipboard).not.toHaveBeenCalled(); + expect(log).not.toHaveBeenCalled(); + }); + + it("copySetupURI should encode with short mode by default", async () => { + const promptCopyToClipboard = vi.fn(() => true); + const currentSettings = { pluginSyncExtendedSetting: true, x: 1 }; + const host = { + services: { + setting: { + currentSettings: vi.fn(() => currentSettings), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass"), + }, + promptCopyToClipboard, + }, + }, + } as any; + const log = vi.fn(); + vi.mocked(encodeSettingsToSetupURI).mockResolvedValue("uri://value" as any); + + await copySetupURI(host, log); + + expect(encodeSettingsToSetupURI).toHaveBeenCalledWith( + currentSettings, + "pass", + ["pluginSyncExtendedSetting"], + true + ); + expect(promptCopyToClipboard).toHaveBeenCalledWith("Setup URI", "uri://value"); + expect(log).toHaveBeenCalled(); + }); + + it("copySetupURIFull should encode with full mode", async () => { + const promptCopyToClipboard = vi.fn(() => true); + const currentSettings = { pluginSyncExtendedSetting: true, x: 1 }; + const host = { + services: { + setting: { + currentSettings: vi.fn(() => currentSettings), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass-full"), + }, + promptCopyToClipboard, + }, + }, + } as any; + const log = vi.fn(); + vi.mocked(encodeSettingsToSetupURI).mockResolvedValue("uri://full" as any); + + await copySetupURIFull(host, log); + + expect(encodeSettingsToSetupURI).toHaveBeenCalledWith(currentSettings, "pass-full", [], false); + expect(promptCopyToClipboard).toHaveBeenCalledWith("Setup URI", "uri://full"); + expect(log).toHaveBeenCalled(); + }); + + it("useSetupURIFeature should register onLoaded handler that wires commands and event", async () => { + const addHandler = vi.fn(); + const addCommand = vi.fn(); + const context = createServiceContext(); + const onEventSpy = vi.spyOn(context.events, "onEvent"); + + const host = { + services: { + context, + API: { + addCommand, + addLog: vi.fn(), + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => ({ x: 1 })), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass"), + }, + promptCopyToClipboard: vi.fn(() => true), + }, + }, + } as any; + + useSetupURIFeature(host); + expect(addHandler).toHaveBeenCalledTimes(1); + + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + expect(addCommand).toHaveBeenCalledTimes(3); + expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupuri" })); + expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupuri-short" })); + expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupurifull" })); + expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_COPY_SETUP_URI, expect.any(Function)); + }); + + it("shows Setup URI variants only when their configuration level is relevant", async () => { + const addHandler = vi.fn(); + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + const settings = { + isConfigured: false, + usePluginSync: false, + useAdvancedMode: false, + }; + const host = { + services: { + context: createServiceContext(), + API: { + addCommand: vi.fn((command) => commands.push(command)), + addLog: vi.fn(), + }, + appLifecycle: { + onLoaded: { + addHandler, + }, + }, + setting: { + currentSettings: vi.fn(() => settings), + }, + UI: { + confirm: { + askString: vi.fn(() => "pass"), + }, + promptCopyToClipboard: vi.fn(() => true), + }, + }, + } as any; + + useSetupURIFeature(host); + const loadedHandler = addHandler.mock.calls[0][0] as () => Promise; + await loadedHandler(); + + const command = (id: string) => commands.find((candidate) => candidate.id === id)!; + expect(command("livesync-copysetupuri").checkCallback?.(true)).toBe(false); + + settings.isConfigured = true; + expect(command("livesync-copysetupuri").checkCallback?.(true)).toBe(true); + expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(false); + expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(false); + + settings.usePluginSync = true; + settings.useAdvancedMode = true; + expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(true); + expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(true); + }); +}); diff --git a/src/serviceFeatures/setupObsidian/types.ts b/src/serviceFeatures/setupObsidian/types.ts new file mode 100644 index 00000000..0e15898e --- /dev/null +++ b/src/serviceFeatures/setupObsidian/types.ts @@ -0,0 +1,3 @@ +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; + +export type SetupFeatureHost = NecessaryServices<"API" | "UI" | "setting", never>; diff --git a/src/serviceFeatures/useP2PReplicatorUI.ts b/src/serviceFeatures/useP2PReplicatorUI.ts index 8dc5a86e..a30f6182 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.ts @@ -1,24 +1,48 @@ import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events"; import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; -import type { NecessaryServices } from "@lib/interfaces/ServiceModule"; -import { type UseP2PReplicatorResult } from "@lib/replication/trystero/UseP2PReplicatorResult"; -import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector"; -import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView"; +import type { NecessaryServices } from "@vrtmrz/livesync-commonlib/compat/interfaces/ServiceModule"; +import { type UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; +import { P2PLogCollector } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/P2PLogCollector"; import { P2PServerStatusPaneView, VIEW_TYPE_P2P_SERVER_STATUS, } from "@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView"; import type { LiveSyncCore } from "@/main"; import type { WorkspaceLeaf } from "@/deps"; -import { REMOTE_P2P } from "@lib/common/models/setting.const"; +import { REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; +import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type"; +import { ConnectionStringParser } from "@vrtmrz/livesync-commonlib/compat/common/ConnectionString"; + +export const LEGACY_VIEW_TYPE_P2P = "p2p-replicator"; + +class LegacyP2PStatusPaneView extends P2PServerStatusPaneView { + override getViewType() { + return LEGACY_VIEW_TYPE_P2P; + } +} + +export function hasP2PConfiguration(settings: Partial): boolean { + if ( + settings.remoteType === REMOTE_P2P || + settings.P2P_Enabled === true || + (settings.P2P_roomID ?? "").trim() !== "" || + (settings.P2P_passphrase ?? "").trim() !== "" + ) { + return true; + } + return Object.values(settings.remoteConfigurations ?? {}).some((configuration) => { + try { + return ConnectionStringParser.parse(configuration.uri).type === "p2p"; + } catch { + return false; + } + }); +} /** - * ServiceFeature: P2P Replicator lifecycle management. - * Binds a LiveSyncTrysteroReplicator to the host's lifecycle events, - * following the same middleware style as useOfflineScanner. - * - * @param viewTypeAndFactory Optional [viewType, factory] pair for registering the P2P pane view. - * When provided, also registers commands and ribbon icon via services.API. + * Obsidian-specific P2P views, commands, status collection, and ribbon wiring. + * Replicator ownership and lifecycle remain in Commonlib's + * `useP2PReplicatorFeature`; this feature only consumes its current result. */ export function useP2PReplicatorUI( @@ -43,40 +67,40 @@ export function useP2PReplicatorUI( showWindow: (type: string) => Promise; showWindowOnRight?: (type: string) => Promise; registerWindow: (type: string, factory: (leaf: WorkspaceLeaf) => unknown) => void; - addCommand: (command: { id: string; name: string; callback: () => void }) => unknown; + addCommand: (command: { + id: string; + name: string; + callback?: () => void; + checkCallback?: (checking: boolean) => boolean | void; + }) => unknown; addRibbonIcon: ( icon: string, title: string, callback: () => void - ) => { addClass?: (name: string) => unknown } | undefined; - getPlatform: () => string; + ) => { addClass?: (name: string) => unknown; remove?: () => void } | undefined; }; // const env: LiveSyncTrysteroReplicatorEnv = { services: host.services as any }; const getReplicator = () => replicator.replicator; - const p2pLogCollector = new P2PLogCollector(); + const p2pLogCollector = new P2PLogCollector(host.services.context.events); const storeP2PStatusLine = reactiveSource(""); p2pLogCollector.p2pReplicationLine.onChanged((line) => { storeP2PStatusLine.value = line.value; }); + const p2pParams = { + get replicator() { + return getReplicator(); + }, + p2pLogCollector, + storeP2PStatusLine, + }; - // Register view, commands and ribbon if a view factory is provided - const viewType = VIEW_TYPE_P2P; - const factory = (leaf: WorkspaceLeaf) => { - return new P2PReplicatorPaneView(leaf, core, { - replicator: getReplicator(), - p2pLogCollector, - storeP2PStatusLine, - }); - }; const statusFactory = (leaf: WorkspaceLeaf) => { - return new P2PServerStatusPaneView(leaf, core, { - replicator: getReplicator(), - p2pLogCollector, - storeP2PStatusLine, - }); + return new P2PServerStatusPaneView(leaf, core, p2pParams); + }; + const legacyStatusFactory = (leaf: WorkspaceLeaf) => { + return new LegacyP2PStatusPaneView(leaf, core, p2pParams); }; - const openPane = () => api.showWindow(viewType); const openStatusPane = () => { if (api.showWindowOnRight) { return api.showWindowOnRight(VIEW_TYPE_P2P_SERVER_STATUS); @@ -92,27 +116,47 @@ export function useP2PReplicatorUI( { label: "replication" } ); }; - api.registerWindow(viewType, factory); + // Keep the retired view type registered only long enough to restore an + // existing workspace leaf with the current status UI. Layout-ready + // migration below rewrites it to the current type without opening a leaf. + api.registerWindow(LEGACY_VIEW_TYPE_P2P, legacyStatusFactory); api.registerWindow(VIEW_TYPE_P2P_SERVER_STATUS, statusFactory); + let ribbonElement: { addClass?: (name: string) => unknown; remove?: () => void } | undefined; + const updateRibbon = (settings: Partial) => { + if (hasP2PConfiguration(settings)) { + if (ribbonElement) return; + ribbonElement = api.addRibbonIcon("waypoints", "P2P Status", () => { + void openStatusPane(); + }); + ribbonElement?.addClass?.("livesync-ribbon-p2p-server-status"); + return; + } + ribbonElement?.remove?.(); + ribbonElement = undefined; + }; + + // Settings are loaded after onInitialise. Reading them from the earlier + // phase aborts the plug-in lifecycle before the local database can open. + host.services.appLifecycle.onSettingLoaded.addHandler(() => { + updateRibbon(host.services.setting.currentSettings()); + return Promise.resolve(true); + }); + host.services.appLifecycle.onInitialise.addHandler(() => { eventHub.onEvent(EVENT_REQUEST_OPEN_P2P, () => { - void openPane(); - }); - - api.addCommand({ - id: "open-p2p-replicator", - name: "P2P Sync : Open P2P Replicator (Old UI)", - callback: () => { - void openPane(); - }, + void openStatusPane(); }); api.addCommand({ id: "open-p2p-server-status", name: "P2P Sync : Open P2P Status", - callback: () => { - void openStatusPane(); + checkCallback: (checking) => { + if (!hasP2PConfiguration(host.services.setting.currentSettings())) return false; + if (!checking) { + void openStatusPane(); + } + return true; }, }); host.services.API.addCommand({ @@ -120,11 +164,15 @@ export function useP2PReplicatorUI( name: "Replicate P2P to default peer", checkCallback: (isChecking: boolean) => { const settings = host.services.setting.currentSettings(); - if (isChecking) { - if (settings.remoteType == REMOTE_P2P) return false; - return replicator.replicator?.server?.isServing ?? false; + const isAvailable = + hasP2PConfiguration(settings) && + settings.remoteType !== REMOTE_P2P && + (replicator.replicator?.server?.isServing ?? false); + if (!isAvailable) return false; + if (!isChecking) { + runOpenReplication(); } - runOpenReplication(); + return true; }, }); host.services.API.addCommand({ @@ -132,11 +180,15 @@ export function useP2PReplicatorUI( name: "Replicate now by P2P", checkCallback: (isChecking: boolean) => { const settings = host.services.setting.currentSettings(); - if (isChecking) { - if (settings.remoteType == REMOTE_P2P) return false; - return replicator.replicator?.server?.isServing ?? false; + const isAvailable = + hasP2PConfiguration(settings) && + settings.remoteType !== REMOTE_P2P && + (replicator.replicator?.server?.isServing ?? false); + if (!isAvailable) return false; + if (!isChecking) { + runOpenReplication(); } - runOpenReplication(); + return true; }, }); @@ -144,34 +196,48 @@ export function useP2PReplicatorUI( id: "p2p-sync-targets", name: "P2P: Sync with targets", checkCallback: (isChecking: boolean) => { - if (isChecking) { - return replicator.replicator?.server?.isServing ?? false; + const isAvailable = + hasP2PConfiguration(host.services.setting.currentSettings()) && + (replicator.replicator?.server?.isServing ?? false); + if (!isAvailable) return false; + if (!isChecking) { + void replicator.replicator?.replicateFromCommand(true); } - void replicator.replicator?.replicateFromCommand(true); + return true; }, }); - // api.addRibbonIcon("waypoints", "P2P Replicator", () => { - // void openPane(); - // })?.addClass?.("livesync-ribbon-replicate-p2p"); - - api.addRibbonIcon("waypoints", "P2P Status", () => { - void openStatusPane(); - })?.addClass?.("livesync-ribbon-p2p-server-status"); - - return Promise.resolve(true); - }); - - host.services.appLifecycle.onLayoutReady.addHandler(() => { - if (api.getPlatform() !== "obsidian") { + host.services.setting.onSettingSaved?.addHandler((settings) => { + updateRibbon(settings); return Promise.resolve(true); - } - if (api.showWindowOnRight) { - void api.showWindowOnRight(VIEW_TYPE_P2P_SERVER_STATUS); - } else { - void api.showWindow(VIEW_TYPE_P2P_SERVER_STATUS); - } + }); + return Promise.resolve(true); }); - return { replicator: getReplicator(), p2pLogCollector, storeP2PStatusLine }; + + host.services.appLifecycle.onLayoutReady.addHandler(async () => { + const workspace = ( + host.services.context as { + app?: { + workspace?: { + getLeavesOfType(type: string): WorkspaceLeaf[]; + }; + }; + } + ).app?.workspace; + if (!workspace) { + return true; + } + const legacyLeaves = workspace.getLeavesOfType(LEGACY_VIEW_TYPE_P2P); + await Promise.all( + legacyLeaves.map((leaf) => + leaf.setViewState({ + type: VIEW_TYPE_P2P_SERVER_STATUS, + active: false, + }) + ) + ); + return true; + }); + return p2pParams; } diff --git a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts index 5495d4d3..b37e3944 100644 --- a/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts +++ b/src/serviceFeatures/useP2PReplicatorUI.unit.spec.ts @@ -1,17 +1,66 @@ import { describe, expect, it, vi } from "vitest"; +import { createServiceContext } from "@vrtmrz/livesync-commonlib/context"; +import { eventHub, EVENT_REQUEST_OPEN_P2P } from "@/common/events"; -vi.mock("@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView", () => ({ - P2PReplicatorPaneView: class {}, - VIEW_TYPE_P2P: "p2p", -})); vi.mock("@/features/P2PSync/P2PReplicator/P2PServerStatusPaneView", () => ({ - P2PServerStatusPaneView: class {}, + P2PServerStatusPaneView: class { + getViewType() { + return "p2p-status"; + } + }, VIEW_TYPE_P2P_SERVER_STATUS: "p2p-status", })); import { useP2PReplicatorUI } from "./useP2PReplicatorUI"; describe("useP2PReplicatorUI commands", () => { + it("waits for settings to load before deciding whether to show the P2P ribbon", async () => { + let initialise: (() => Promise) | undefined; + let settingLoaded: (() => Promise) | undefined; + let settings: Record | undefined; + const currentSettings = vi.fn(() => settings); + const host = { + services: { + context: createServiceContext(), + API: { + showWindow: vi.fn(async () => undefined), + registerWindow: vi.fn(), + addCommand: vi.fn(), + addRibbonIcon: vi.fn(), + }, + appLifecycle: { + onInitialise: { + addHandler: vi.fn((handler) => { + initialise = handler; + }), + }, + onSettingLoaded: { + addHandler: vi.fn((handler) => { + settingLoaded = handler; + }), + }, + onLayoutReady: { addHandler: vi.fn() }, + }, + setting: { + currentSettings, + onSettingSaved: { addHandler: vi.fn() }, + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + + useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + + await expect(initialise?.()).resolves.toBe(true); + expect(currentSettings).not.toHaveBeenCalled(); + settings = { + remoteType: "COUCHDB", + remoteConfigurations: {}, + }; + await expect(settingLoaded?.()).resolves.toBe(true); + expect(currentSettings).toHaveBeenCalledOnce(); + }); + it("exposes a direct modal P2P replication command as finite replication activity", async () => { const commands: Array<{ id: string; checkCallback?: (isChecking: boolean) => unknown }> = []; let initialise: (() => Promise) | undefined; @@ -19,6 +68,7 @@ describe("useP2PReplicatorUI commands", () => { const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task()); const host = { services: { + context: createServiceContext(), API: { showWindow: vi.fn(async () => undefined), registerWindow: vi.fn(), @@ -32,9 +82,15 @@ describe("useP2PReplicatorUI commands", () => { initialise = handler; }), }, + onSettingLoaded: { addHandler: vi.fn() }, onLayoutReady: { addHandler: vi.fn() }, }, - setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) }, + setting: { + currentSettings: vi.fn(() => ({ + remoteType: "COUCHDB", + P2P_Enabled: true, + })), + }, replicator: { runFiniteReplicationActivity }, }, } as any; @@ -55,4 +111,343 @@ describe("useP2PReplicatorUI commands", () => { label: "replication", }); }); + + it("keeps the current replicator in the pane parameters after replacement", () => { + const first = { id: "first" }; + const second = { id: "second" }; + let current = first; + const p2p = { + get replicator() { + return current; + }, + } as any; + const host = { + services: { + context: createServiceContext(), + API: { + showWindow: vi.fn(async () => undefined), + registerWindow: vi.fn(), + addCommand: vi.fn(), + addRibbonIcon: vi.fn(), + getPlatform: vi.fn(() => "obsidian"), + }, + appLifecycle: { + onInitialise: { addHandler: vi.fn() }, + onSettingLoaded: { addHandler: vi.fn() }, + onLayoutReady: { addHandler: vi.fn() }, + }, + setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + + const paneParams = useP2PReplicatorUI(host, {} as any, p2p); + current = second; + + expect(paneParams.replicator).toBe(second); + }); + + it("retains only the current P2P status command and routes existing open requests to it", async () => { + const commands: Array<{ + id: string; + callback?: () => void; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + let initialise: (() => Promise) | undefined; + const showWindow = vi.fn(async () => undefined); + const showWindowOnRight = vi.fn(async () => undefined); + const host = { + services: { + context: createServiceContext(), + API: { + showWindow, + showWindowOnRight, + registerWindow: vi.fn(), + addCommand: vi.fn((command) => commands.push(command)), + addRibbonIcon: vi.fn(), + getPlatform: vi.fn(() => "desktop"), + }, + appLifecycle: { + onInitialise: { + addHandler: vi.fn((handler) => { + initialise = handler; + }), + }, + onSettingLoaded: { addHandler: vi.fn() }, + onLayoutReady: { addHandler: vi.fn() }, + }, + setting: { + currentSettings: vi.fn(() => ({ + remoteType: "COUCHDB", + remoteConfigurations: {}, + })), + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + const p2p = { replicator: undefined } as any; + + useP2PReplicatorUI(host, {} as any, p2p); + await initialise?.(); + + expect(commands.map((command) => command.id)).not.toContain("open-p2p-replicator"); + expect(commands.map((command) => command.id)).toContain("open-p2p-server-status"); + expect(commands.find((command) => command.id === "open-p2p-server-status")?.checkCallback?.(true)).toBe(false); + + eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P); + await vi.waitFor(() => expect(showWindowOnRight).toHaveBeenCalledWith("p2p-status")); + expect(showWindow).not.toHaveBeenCalledWith("p2p"); + }); + + it("shows P2P commands only when a P2P configuration exists and their runtime prerequisites are met", async () => { + const commands: Array<{ + id: string; + checkCallback?: (checking: boolean) => boolean | void; + }> = []; + let initialise: (() => Promise) | undefined; + let settings: Record = { + remoteType: "COUCHDB", + remoteConfigurations: {}, + }; + const host = { + services: { + context: createServiceContext(), + API: { + showWindow: vi.fn(async () => undefined), + showWindowOnRight: vi.fn(async () => undefined), + registerWindow: vi.fn(), + addCommand: vi.fn((command) => commands.push(command)), + addRibbonIcon: vi.fn(), + }, + appLifecycle: { + onInitialise: { + addHandler: vi.fn((handler) => { + initialise = handler; + }), + }, + onSettingLoaded: { addHandler: vi.fn() }, + onLayoutReady: { addHandler: vi.fn() }, + }, + setting: { + currentSettings: vi.fn(() => settings), + onSettingSaved: { addHandler: vi.fn() }, + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + const p2p = { + replicator: { + server: { isServing: true }, + openReplication: vi.fn(), + replicateFromCommand: vi.fn(), + }, + } as any; + + useP2PReplicatorUI(host, {} as any, p2p); + await initialise?.(); + + for (const commandId of [ + "open-p2p-server-status", + "replicate-now-by-p2p-default-peer", + "replicate-now-by-p2p", + "p2p-sync-targets", + ]) { + expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(false); + } + + settings = { + ...settings, + remoteConfigurations: { + peer: { + id: "peer", + name: "Peer", + uri: "sls+p2p://room?passphrase=secret", + isEncrypted: false, + }, + }, + }; + for (const commandId of [ + "open-p2p-server-status", + "replicate-now-by-p2p-default-peer", + "replicate-now-by-p2p", + "p2p-sync-targets", + ]) { + expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(true); + } + }); + + it("does not open the P2P status pane automatically when the workspace becomes ready", async () => { + let layoutReady: (() => Promise) | undefined; + const showWindow = vi.fn(async () => undefined); + const showWindowOnRight = vi.fn(async () => undefined); + const host = { + services: { + context: createServiceContext(), + API: { + showWindow, + showWindowOnRight, + registerWindow: vi.fn(), + addCommand: vi.fn(), + addRibbonIcon: vi.fn(), + getPlatform: vi.fn(() => "obsidian"), + }, + appLifecycle: { + onInitialise: { addHandler: vi.fn() }, + onSettingLoaded: { addHandler: vi.fn() }, + onLayoutReady: { + addHandler: vi.fn((handler) => { + layoutReady = handler; + }), + }, + }, + setting: { + currentSettings: vi.fn(() => ({ + remoteType: "COUCHDB", + remoteConfigurations: {}, + })), + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + + useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + await layoutReady?.(); + + expect(showWindow).not.toHaveBeenCalled(); + expect(showWindowOnRight).not.toHaveBeenCalled(); + }); + + it("shows the ribbon only whilst a P2P configuration exists", async () => { + let initialise: (() => Promise) | undefined; + let settingLoaded: (() => Promise) | undefined; + let onSettingSaved: ((settings: unknown) => Promise) | undefined; + let currentSettings: any = { + remoteType: "COUCHDB", + remoteConfigurations: {}, + P2P_Enabled: false, + P2P_roomID: "", + P2P_passphrase: "", + }; + const ribbon = { addClass: vi.fn(), remove: vi.fn() }; + const addRibbonIcon = vi.fn(() => ribbon); + const host = { + services: { + context: createServiceContext(), + API: { + showWindow: vi.fn(async () => undefined), + showWindowOnRight: vi.fn(async () => undefined), + registerWindow: vi.fn(), + addCommand: vi.fn(), + addRibbonIcon, + getPlatform: vi.fn(() => "desktop"), + }, + appLifecycle: { + onInitialise: { + addHandler: vi.fn((handler) => { + initialise = handler; + }), + }, + onSettingLoaded: { + addHandler: vi.fn((handler) => { + settingLoaded = handler; + }), + }, + onLayoutReady: { addHandler: vi.fn() }, + }, + setting: { + currentSettings: vi.fn(() => currentSettings), + onSettingSaved: { + addHandler: vi.fn((handler) => { + onSettingSaved = handler; + }), + }, + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + + useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + await initialise?.(); + await settingLoaded?.(); + expect(addRibbonIcon).not.toHaveBeenCalled(); + + currentSettings = { + ...currentSettings, + remoteConfigurations: { + peer: { + id: "peer", + name: "Peer", + uri: "sls+p2p://room?passphrase=secret", + isEncrypted: false, + }, + }, + }; + await onSettingSaved?.(currentSettings); + expect(addRibbonIcon).toHaveBeenCalledOnce(); + + await onSettingSaved?.(currentSettings); + expect(addRibbonIcon).toHaveBeenCalledOnce(); + + currentSettings = { + ...currentSettings, + remoteConfigurations: {}, + }; + await onSettingSaved?.(currentSettings); + expect(ribbon.remove).toHaveBeenCalledOnce(); + }); + + it("compatibility: migrates a restored P2P leaf to the current status view without opening another leaf", async () => { + let layoutReady: (() => Promise) | undefined; + const legacyLeaf = { + setViewState: vi.fn(async () => undefined), + }; + const workspace = { + getLeavesOfType: vi.fn((type: string) => (type === "p2p-replicator" ? [legacyLeaf] : [])), + }; + const context = createServiceContext() as ReturnType & { + app: { workspace: typeof workspace }; + }; + context.app = { workspace }; + const showWindow = vi.fn(async () => undefined); + const showWindowOnRight = vi.fn(async () => undefined); + const host = { + services: { + context, + API: { + showWindow, + showWindowOnRight, + registerWindow: vi.fn(), + addCommand: vi.fn(), + addRibbonIcon: vi.fn(), + getPlatform: vi.fn(() => "desktop"), + }, + appLifecycle: { + onInitialise: { addHandler: vi.fn() }, + onSettingLoaded: { addHandler: vi.fn() }, + onLayoutReady: { + addHandler: vi.fn((handler) => { + layoutReady = handler; + }), + }, + }, + setting: { + currentSettings: vi.fn(() => ({ + remoteType: "COUCHDB", + remoteConfigurations: {}, + })), + }, + replicator: { runFiniteReplicationActivity: vi.fn() }, + }, + } as any; + + useP2PReplicatorUI(host, {} as any, { replicator: undefined } as any); + await layoutReady?.(); + + expect(legacyLeaf.setViewState).toHaveBeenCalledWith({ + type: "p2p-status", + active: false, + }); + expect(showWindow).not.toHaveBeenCalled(); + expect(showWindowOnRight).not.toHaveBeenCalled(); + }); }); diff --git a/src/serviceFeatures/useReviewHarness.ts b/src/serviceFeatures/useReviewHarness.ts new file mode 100644 index 00000000..04d42755 --- /dev/null +++ b/src/serviceFeatures/useReviewHarness.ts @@ -0,0 +1,125 @@ +import { NEW_VAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/settings"; +import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; +import type { UseP2PReplicatorResult } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/UseP2PReplicatorResult"; +import type ObsidianLiveSyncPlugin from "@/main"; +import type { LiveSyncCore } from "@/main"; +import type { WorkspaceLeaf } from "@/deps"; +import { + ReviewHarnessController, + REVIEW_HARNESS_STATE_KEY, + type ReviewHarnessRuntime, +} from "@/features/ReviewHarness/reviewHarnessController"; +import { ReviewHarnessView, VIEW_TYPE_REVIEW_HARNESS } from "@/features/ReviewHarness/ReviewHarnessView"; +import type { ReviewHarnessScenarioResult } from "@/features/ReviewHarness/reviewHarnessContract"; +import { + REVIEW_HARNESS_FIXTURE_ROOT, + runReviewHarnessVaultRoundTrip, +} from "@/features/ReviewHarness/reviewHarnessVaultFixture"; +import type { CompatibilityReviewController } from "./compatibilityReview"; + +async function runVaultRoundTrip(plugin: ObsidianLiveSyncPlugin): Promise { + const vault = plugin.app.vault; + return runReviewHarnessVaultRoundTrip({ + confirmFixtureAccess: async () => + (await plugin.core.services.UI.confirm.askYesNoDialog( + "This scenario creates, reads, modifies, renames, and removes one owned fixture tree. Use a dedicated test Vault. Continue?", + { + title: "Review Harness: Vault fixture access", + defaultOption: "No", + } + )) === "yes", + fixtureRootExists: () => vault.getAbstractFileByPath(REVIEW_HARNESS_FIXTURE_ROOT) !== null, + createFixtureRoot: async () => { + await vault.createFolder(REVIEW_HARNESS_FIXTURE_ROOT); + }, + createFile: (path, content) => vault.create(path, content), + readFile: (file) => vault.read(file), + modifyFile: (file, content) => vault.modify(file, content), + renameFile: (file, path) => vault.rename(file, path), + filePath: (file) => file.path, + removeFixtureRoot: async () => { + const fixtureRoot = vault.getAbstractFileByPath(REVIEW_HARNESS_FIXTURE_ROOT); + if (fixtureRoot) await plugin.app.fileManager.trashFile(fixtureRoot); + }, + }); +} + +export function useReviewHarness( + core: LiveSyncCore, + plugin: ObsidianLiveSyncPlugin, + p2p: UseP2PReplicatorResult, + compatibilityReview: CompatibilityReviewController +): ReviewHarnessController { + const services = core.services; + const runtime: ReviewHarnessRuntime = { + now: () => new Date(), + getSettings: () => services.setting.currentSettings(), + getNewVaultSettings: () => NEW_VAULT_SETTINGS, + getSettingsMigrationState: () => services.setting.getSettingsMigrationState(), + isCompatibilityReviewInitialised: () => compatibilityReview.initialised, + getCompatibilityPause: () => compatibilityReview.pendingPause, + openCompatibilityReview: () => compatibilityReview.openReview(), + getP2PComposition: () => ({ + first: p2p.replicator, + second: p2p.replicator, + expectedServices: services, + }), + runVaultRoundTrip: () => runVaultRoundTrip(plugin), + readContinuation: () => services.setting.getSmallConfig(REVIEW_HARNESS_STATE_KEY), + writeContinuation: (value) => services.setting.setSmallConfig(REVIEW_HARNESS_STATE_KEY, value), + deleteContinuation: () => services.setting.deleteSmallConfig(REVIEW_HARNESS_STATE_KEY), + restart: () => services.appLifecycle.performRestart(), + reportError: (error) => services.API.addLog(error, LOG_LEVEL_NOTICE), + copyText: async (value) => { + if (!activeWindow.navigator.clipboard) throw new Error("Clipboard access is unavailable on this device."); + await activeWindow.navigator.clipboard.writeText(value); + }, + getEnvironment: () => ({ + pluginVersion: services.API.getPluginVersion(), + obsidianVersion: services.API.getAppVersion(), + platform: services.API.getPlatform(), + userAgent: activeWindow.navigator.userAgent || "unavailable", + viewport: + typeof activeWindow.innerWidth === "number" && typeof activeWindow.innerHeight === "number" + ? `${activeWindow.innerWidth}x${activeWindow.innerHeight}` + : "unavailable", + }), + }; + const controller = new ReviewHarnessController(runtime); + let continuationConsumed = false; + let registered = false; + let openAfterLayout = false; + + services.appLifecycle.onSettingLoaded.addHandler(() => { + if (!continuationConsumed) { + continuationConsumed = true; + controller.consumeContinuation(); + } + const snapshot = controller.snapshot(); + openAfterLayout = snapshot.resumedRequestId !== null || snapshot.continuationError !== null; + if (!services.setting.currentSettings().enableDebugTools || registered) return Promise.resolve(true); + + registered = true; + services.API.registerWindow( + VIEW_TYPE_REVIEW_HARNESS, + (leaf: WorkspaceLeaf) => new ReviewHarnessView(leaf, controller) + ); + services.API.addCommand({ + id: "open-review-harness", + name: "Open review harness", + callback: () => { + void services.API.showWindow(VIEW_TYPE_REVIEW_HARNESS); + }, + }); + return Promise.resolve(true); + }); + + services.appLifecycle.onLayoutReady.addHandler(() => { + if (openAfterLayout && services.setting.currentSettings().enableDebugTools) { + void services.API.showWindow(VIEW_TYPE_REVIEW_HARNESS); + } + return Promise.resolve(true); + }); + + return controller; +} diff --git a/src/serviceFeatures/useReviewHarness.unit.spec.ts b/src/serviceFeatures/useReviewHarness.unit.spec.ts new file mode 100644 index 00000000..1b3f48e4 --- /dev/null +++ b/src/serviceFeatures/useReviewHarness.unit.spec.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, vi } from "vitest"; +import type { CompatibilityPause } from "@/common/databaseCompatibility.ts"; +import { REVIEW_HARNESS_STATE_KEY } from "@/features/ReviewHarness/reviewHarnessController.ts"; +import { useReviewHarness } from "./useReviewHarness.ts"; + +const VIEW_TYPE_REVIEW_HARNESS = "self-hosted-livesync-review-harness"; + +vi.mock("@/features/ReviewHarness/ReviewHarnessView", () => ({ + VIEW_TYPE_REVIEW_HARNESS: "self-hosted-livesync-review-harness", + ReviewHarnessView: class {}, +})); + +function compatibilityPause(): CompatibilityPause { + return { + resumable: true, + reasons: [ + { + source: "settings-schema", + sourceVersion: 9, + currentVersion: 10, + isFromFutureSchema: false, + resumable: true, + reviewReasons: [], + }, + ], + }; +} + +function createFixture(options: { enableDebugTools?: boolean; continuation?: string } = {}) { + const settingLoadedHandlers: Array<() => Promise> = []; + const layoutReadyHandlers: Array<() => Promise> = []; + const local = new Map(); + if (options.continuation) local.set(REVIEW_HARNESS_STATE_KEY, options.continuation); + const settings = { + enableDebugTools: options.enableDebugTools ?? true, + liveSync: false, + syncOnSave: false, + syncOnEditorSave: true, + syncOnStart: false, + syncOnFileOpen: true, + syncAfterMerge: false, + periodicReplication: true, + }; + const services = {} as Record; + const replicator = { env: { services } }; + const api = { + registerWindow: vi.fn(), + addCommand: vi.fn(), + showWindow: vi.fn().mockResolvedValue(undefined), + addLog: vi.fn(), + getPluginVersion: () => "1.0.0-rc.0", + getAppVersion: () => "1.12.7", + getPlatform: () => "desktop", + }; + Object.assign(services, { + setting: { + currentSettings: () => settings, + getSettingsMigrationState: () => ({ + sourceVersion: 9, + targetVersion: 10, + isNewVault: false, + isFromFutureSchema: false, + changed: true, + requiresSyncReview: true, + reviewReasons: [], + }), + getSmallConfig: (key: string) => local.get(key) ?? "", + setSmallConfig: (key: string, value: string) => local.set(key, value), + deleteSmallConfig: (key: string) => local.delete(key), + }, + appLifecycle: { + onSettingLoaded: { + addHandler: vi.fn((handler: () => Promise) => settingLoadedHandlers.push(handler)), + }, + onLayoutReady: { + addHandler: vi.fn((handler: () => Promise) => layoutReadyHandlers.push(handler)), + }, + performRestart: vi.fn(), + }, + API: api, + UI: { + confirm: { askYesNoDialog: vi.fn().mockResolvedValue("no") }, + }, + }); + let pause: CompatibilityPause | undefined = compatibilityPause(); + const compatibilityReview = { + initialised: true, + get pendingPause() { + return pause; + }, + openReview: vi.fn(async () => { + pause = undefined; + }), + }; + const core = { services }; + const plugin = { + core, + app: { + vault: {}, + fileManager: {}, + }, + }; + + const controller = useReviewHarness(core as never, plugin as never, { replicator } as never, compatibilityReview as never); + return { + controller, + api, + settings, + local, + settingLoadedHandlers, + layoutReadyHandlers, + compatibilityReview, + }; +} + +describe("Review Harness composition", () => { + it("does not register the command or view when developer debug tools are disabled", async () => { + const fixture = createFixture({ enableDebugTools: false }); + + await fixture.settingLoadedHandlers[0](); + + expect(fixture.api.registerWindow).not.toHaveBeenCalled(); + expect(fixture.api.addCommand).not.toHaveBeenCalled(); + }); + + it("removes a one-shot continuation before reopening the Harness after layout", async () => { + const requestedAt = "2026-07-18T11:59:00.000Z"; + const continuation = JSON.stringify({ + formatVersion: 1, + requestId: `compatibility-review-${requestedAt}`, + scenarioId: "compatibility-review", + stage: "awaiting-restart", + requestedAt, + }); + const fixture = createFixture({ continuation }); + + await fixture.settingLoadedHandlers[0](); + + expect(fixture.local.has(REVIEW_HARNESS_STATE_KEY)).toBe(false); + expect(fixture.controller.snapshot().resumedRequestId).toBe(`compatibility-review-${requestedAt}`); + expect(fixture.api.registerWindow).toHaveBeenCalledWith(VIEW_TYPE_REVIEW_HARNESS, expect.any(Function)); + + await fixture.layoutReadyHandlers[0](); + + expect(fixture.api.showWindow).toHaveBeenCalledWith(VIEW_TYPE_REVIEW_HARNESS); + }); + + it("uses the actual compatibility controller as the guided review boundary", async () => { + const fixture = createFixture(); + + await fixture.controller.runScenario("compatibility-review"); + expect(fixture.controller.snapshot().results["compatibility-review"].status).toBe("waiting-for-user"); + + await fixture.controller.openCompatibilityReview(); + + expect(fixture.compatibilityReview.openReview).toHaveBeenCalledOnce(); + expect(fixture.controller.snapshot().results["compatibility-review"].status).toBe("passed"); + }); +}); diff --git a/src/serviceModules/DatabaseFileAccess.ts b/src/serviceModules/DatabaseFileAccess.ts index 645888ef..93bdb0a3 100644 --- a/src/serviceModules/DatabaseFileAccess.ts +++ b/src/serviceModules/DatabaseFileAccess.ts @@ -1,8 +1,8 @@ -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; -import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase"; +import type { DatabaseFileAccess } from "@vrtmrz/livesync-commonlib/compat/interfaces/DatabaseFileAccess"; +import { ServiceDatabaseFileAccessBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceDatabaseFileAccessBase"; // markChangesAreSame uses persistent data implicitly, we should refactor it too. // For now, to make the refactoring done once, we just use them directly. -// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB). +// Hence it remains in the plug-in rather than Commonlib. (markChangesAreSame is using indexedDB). // Refactored, now migrating... export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {} diff --git a/src/serviceModules/FileAccessObsidian.ts b/src/serviceModules/FileAccessObsidian.ts index 5b318cc8..2d5cacb0 100644 --- a/src/serviceModules/FileAccessObsidian.ts +++ b/src/serviceModules/FileAccessObsidian.ts @@ -1,5 +1,5 @@ import { type App } from "@/deps"; -import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts"; +import { FileAccessBase, type FileAccessBaseDependencies } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase"; import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; /** diff --git a/src/serviceModules/FileHandler.ts b/src/serviceModules/FileHandler.ts index c47cc337..46febb4a 100644 --- a/src/serviceModules/FileHandler.ts +++ b/src/serviceModules/FileHandler.ts @@ -1,7 +1,7 @@ -import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase"; +import { ServiceFileHandlerBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileHandlerBase"; // markChangesAreSame uses persistent data implicitly, we should refactor it too. // also, compareFileFreshness depends on marked changes, so we should refactor it as well. For now, to make the refactoring done once, we just use them directly. -// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB). +// Hence it remains in the plug-in rather than Commonlib. (markChangesAreSame is using indexedDB). // Refactored: markChangesAreSame, unmarkChanges, compareFileFreshness, isMarkedAsSameChanges are now moved to PathService export class ServiceFileHandler extends ServiceFileHandlerBase {} diff --git a/src/serviceModules/FileReflectionProvenance.ts b/src/serviceModules/FileReflectionProvenance.ts new file mode 100644 index 00000000..f07ef4b6 --- /dev/null +++ b/src/serviceModules/FileReflectionProvenance.ts @@ -0,0 +1,27 @@ +import { + StoredFileReflectionProvenance, + type FileReflectionProvenanceRecord, +} from "@vrtmrz/livesync-commonlib/compat/interfaces/FileReflectionProvenance"; +import type { SimpleStore } from "@vrtmrz/livesync-commonlib/compat/common/utils"; + +export const FILE_REFLECTION_PROVENANCE_STORE = "file-reflection-provenance-v1"; + +export type FileReflectionProvenanceStoreFactory = { + openSimpleStore(kind: string): SimpleStore; +}; + +/** + * Create the device-local record which links a Vault file to the exact + * database revision most recently reflected in that Vault. + * + * This runs during service composition, before KeyValueDB is opened. The + * returned namespaced handle is inert until its first operation; normal hosts + * complete the sequential onSettingLoaded lifecycle before Vault scanning, + * watching, or replication can invoke it. Operations are never held waiting for + * readiness; they fail on a lifecycle violation and may fail during reset. + */ +export function createFileReflectionProvenance(keyValueDB: FileReflectionProvenanceStoreFactory) { + return new StoredFileReflectionProvenance( + keyValueDB.openSimpleStore(FILE_REFLECTION_PROVENANCE_STORE) + ); +} diff --git a/src/serviceModules/FileReflectionProvenance.unit.spec.ts b/src/serviceModules/FileReflectionProvenance.unit.spec.ts new file mode 100644 index 00000000..6eadf3dc --- /dev/null +++ b/src/serviceModules/FileReflectionProvenance.unit.spec.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from "vitest"; +import type { SimpleStore } from "@vrtmrz/livesync-commonlib/compat/common/utils"; +import type { FileReflectionProvenanceRecord } from "@vrtmrz/livesync-commonlib/compat/interfaces/FileReflectionProvenance"; +import type { FilePathWithPrefix } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { + createFileReflectionProvenance, + FILE_REFLECTION_PROVENANCE_STORE, +} from "./FileReflectionProvenance"; + +describe("createFileReflectionProvenance", () => { + it("uses one reset-scoped host store for exact reflected revisions", async () => { + const values = new Map(); + const store = { + get: vi.fn(async (key: string) => values.get(key)), + set: vi.fn(async (key: string, value: FileReflectionProvenanceRecord) => { + values.set(key, value); + }), + delete: vi.fn(async (key: string) => { + values.delete(key); + }), + keys: vi.fn(async () => [...values.keys()]), + db: undefined, + } as unknown as SimpleStore; + const openSimpleStore = vi.fn().mockReturnValue(store); + const path = "note.md" as FilePathWithPrefix; + + const provenance = createFileReflectionProvenance({ openSimpleStore }); + expect(openSimpleStore).toHaveBeenCalledWith(FILE_REFLECTION_PROVENANCE_STORE); + await provenance.set(path, { revision: "3-displayed", observedStorageMtime: 123.456 }); + + expect(openSimpleStore).toHaveBeenCalledTimes(1); + await expect(provenance.get(path)).resolves.toEqual({ + revision: "3-displayed", + observedStorageMtime: 123.456, + }); + }); +}); diff --git a/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts index 02957595..179d4d9d 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts @@ -1,5 +1,5 @@ -import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types"; -import type { IConversionAdapter } from "@lib/serviceModules/adapters"; +import type { UXFileInfoStub, UXFolderInfo } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IConversionAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian"; import type { TFile, TFolder } from "obsidian"; diff --git a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts index 58f1f694..534d1063 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts @@ -1,4 +1,4 @@ -import type { FilePath, UXStat } from "@lib/common/types"; +import type { FilePath, UXStat } from "@vrtmrz/livesync-commonlib/compat/common/types"; import type { IFileSystemAdapter, IPathAdapter, @@ -6,7 +6,7 @@ import type { IConversionAdapter, IStorageAdapter, IVaultAdapter, -} from "@lib/serviceModules/adapters"; +} from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian"; import { ObsidianConversionAdapter } from "./ObsidianConversionAdapter"; import { ObsidianPathAdapter } from "./ObsidianPathAdapter"; diff --git a/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts index a21ced14..6557c7b0 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts @@ -1,6 +1,6 @@ import { type TAbstractFile, normalizePath } from "@/deps"; -import type { FilePath } from "@lib/common/types"; -import type { IPathAdapter } from "@lib/serviceModules/adapters"; +import type { FilePath } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IPathAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; /** * Path adapter implementation for Obsidian diff --git a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts index a9133018..2cec3048 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts @@ -1,6 +1,6 @@ -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IStorageAdapter } from "@lib/serviceModules/adapters"; -import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase"; +import type { UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; +import { toArrayBuffer } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase"; import type { Stat, App } from "obsidian"; /** diff --git a/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts index 74e05bd5..656fabc2 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts @@ -1,4 +1,4 @@ -import type { ITypeGuardAdapter } from "@lib/serviceModules/adapters"; +import type { ITypeGuardAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; import { TFile, TFolder } from "obsidian"; /** diff --git a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts index 81d51e04..fb5fff32 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts @@ -1,6 +1,6 @@ -import type { UXDataWriteOptions } from "@lib/common/types"; -import type { IVaultAdapter } from "@lib/serviceModules/adapters"; -import { toArrayBuffer } from "@lib/serviceModules/FileAccessBase"; +import type { UXDataWriteOptions } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { IVaultAdapter } from "@vrtmrz/livesync-commonlib/compat/serviceModules/adapters"; +import { toArrayBuffer } from "@vrtmrz/livesync-commonlib/compat/serviceModules/FileAccessBase"; import type { TFile, App, TFolder } from "obsidian"; /** @@ -41,22 +41,12 @@ export class ObsidianVaultAdapter implements IVaultAdapter { return await this.app.vault.rename(file, newPath); } - async delete(file: TFile | TFolder, force = false): Promise { - if ("trashFile" in this.app.fileManager) { - // eslint-disable-next-line obsidianmd/no-unsupported-api - return await this.app.fileManager.trashFile(file); - } - // eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support - return await this.app.vault.delete(file, force); + async delete(file: TFile | TFolder): Promise { + return await this.app.fileManager.trashFile(file); } - async trash(file: TFile | TFolder, force = false): Promise { - if ("trashFile" in this.app.fileManager) { - // eslint-disable-next-line obsidianmd/no-unsupported-api - return await this.app.fileManager.trashFile(file); - } - // eslint-disable-next-line obsidianmd/prefer-file-manager-trash-file -- Fallback for older versions of Obsidian without trashFile support - return await this.app.vault.trash(file, force); + async trash(file: TFile | TFolder): Promise { + return await this.app.fileManager.trashFile(file); } trigger(name: string, ...data: unknown[]): void { diff --git a/src/serviceModules/ServiceFileAccessImpl.ts b/src/serviceModules/ServiceFileAccessImpl.ts index 204855ef..c2b40187 100644 --- a/src/serviceModules/ServiceFileAccessImpl.ts +++ b/src/serviceModules/ServiceFileAccessImpl.ts @@ -1,4 +1,4 @@ -import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase"; +import { ServiceFileAccessBase } from "@vrtmrz/livesync-commonlib/compat/serviceModules/ServiceFileAccessBase"; import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; // For now, this is just a re-export of ServiceFileAccess with the Obsidian-specific adapter type. diff --git a/src/types.ts b/src/types.ts index b4c40bfd..bb3bb5c5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ -import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess"; -import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder"; -import type { IFileHandler } from "@lib/interfaces/FileHandler"; -import type { StorageAccess } from "@lib/interfaces/StorageAccess"; -import type { IServiceHub } from "@lib/services/base/IService"; +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 { IServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/base/IService"; export interface ServiceModules { storageAccess: StorageAccess; diff --git a/styles.css b/styles.css index e478615e..7815af18 100644 --- a/styles.css +++ b/styles.css @@ -12,13 +12,26 @@ background-color: var(--text-muted); } +.vpk-action-dialog__message { + user-select: text; + -webkit-user-select: text; +} + .conflict-dev-name { display: inline-block; min-width: 5em; } +.conflict-action-container { + display: flex; + flex-direction: column; + gap: var(--size-4-2); + margin-top: var(--size-4-2); +} + .conflict-action-button { - margin-right: 4px; + width: 100%; + margin: 0; } .op-scrollable { @@ -138,8 +151,8 @@ div.sls-setting-menu-btn { /* width: 100%; */ } -.sls-setting-tab:hover~div.sls-setting-menu-btn, -.sls-setting-label.selected .sls-setting-tab:checked~div.sls-setting-menu-btn { +.sls-setting-tab:hover ~ div.sls-setting-menu-btn, +.sls-setting-label.selected .sls-setting-tab:checked ~ div.sls-setting-menu-btn { background-color: var(--interactive-accent); color: var(--text-on-accent); } @@ -174,9 +187,16 @@ body { /* padding: 2px; */ margin: 1px; border-radius: 4px; - background-image: linear-gradient(-45deg, - var(--sls-col-warn-stripe1) 25%, var(--sls-col-warn-stripe2) 25%, var(--sls-col-warn-stripe2) 50%, - var(--sls-col-warn-stripe1) 50%, var(--sls-col-warn-stripe1) 75%, var(--sls-col-warn-stripe2) 75%, var(--sls-col-warn-stripe2)); + background-image: linear-gradient( + -45deg, + var(--sls-col-warn-stripe1) 25%, + var(--sls-col-warn-stripe2) 25%, + var(--sls-col-warn-stripe2) 50%, + var(--sls-col-warn-stripe1) 50%, + var(--sls-col-warn-stripe1) 75%, + var(--sls-col-warn-stripe2) 75%, + var(--sls-col-warn-stripe2) + ); background-size: 30px 30px; display: flex; flex-direction: row; @@ -196,7 +216,6 @@ body { 100% { background-position: 30px 0; } - } .sls-setting-menu-buttons label { @@ -301,33 +320,99 @@ body { background-color: rgba(var(--background-modifier-error-rgb), 0.3); } -.sls-setting-disabled input[type=text], -.sls-setting-disabled input[type=number], -.sls-setting-disabled input[type=password] { +.sls-setting-disabled input[type="text"], +.sls-setting-disabled input[type="number"], +.sls-setting-disabled input[type="password"] { filter: brightness(80%); color: var(--text-muted); - } .sls-setting-hidden { display: none; } - - .sls-setting-obsolete { /* background-image: linear-gradient(-45deg, var(--sls-col-warn-stripe1) 25%, var(--sls-col-warn-stripe2) 25%, var(--sls-col-warn-stripe2) 50%, var(--sls-col-warn-stripe1) 50%, var(--sls-col-warn-stripe1) 75%, var(--sls-col-warn-stripe2) 75%, var(--sls-col-warn-stripe2)); */ - background-image: linear-gradient(-45deg, - transparent 25%, rgba(var(--background-secondary), 0.1) 25%, rgba(var(--background-secondary), 0.1) 50%, transparent 50%, transparent 75%, rgba(var(--background-secondary), 0.1) 75%, rgba(var(--background-secondary), 0.1)); + background-image: linear-gradient( + -45deg, + transparent 25%, + rgba(var(--background-secondary), 0.1) 25%, + rgba(var(--background-secondary), 0.1) 50%, + transparent 50%, + transparent 75%, + rgba(var(--background-secondary), 0.1) 75%, + rgba(var(--background-secondary), 0.1) + ); background-size: 60px 60px; } -.password-input>.setting-item-control>input { +.password-input > .setting-item-control > input { -webkit-text-security: disc; } +.sls-onboarding-invitation-action { + display: inline-flex; + min-width: 44px; + min-height: 44px; + align-items: center; + justify-content: center; +} + +body:not(.is-mobile):has(.sls-setting) .notice:has(.sls-onboarding-invitation-action) { + margin-right: 96px; +} + +.sls-review-harness { + box-sizing: border-box; + max-width: 100%; + overflow-x: clip; + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} + +.sls-review-harness .setting-item, +.sls-review-harness .setting-item-control { + flex-wrap: wrap; + max-width: 100%; +} + +.sls-review-harness .setting-item-info { + min-width: min(16rem, 100%); +} + +.sls-review-harness .setting-item-control { + gap: 0.5rem; +} + +.sls-review-harness button { + min-height: 44px; + max-width: 100%; + white-space: normal; +} + +.sls-review-harness__result, +.sls-review-harness__warning, +.sls-review-harness__privacy, +.sls-review-harness__error, +.sls-review-harness__resumed { + max-width: 100%; + overflow-wrap: anywhere; +} + +.sls-review-harness__result { + margin: 0 0 1rem; +} + +.sls-review-harness__warning, +.sls-review-harness__error { + color: var(--text-error); +} + +.sls-review-harness__resumed { + color: var(--text-success); +} + span.ls-mark-cr::after { user-select: none; content: "↲"; @@ -378,7 +463,6 @@ span.ls-mark-cr::after { } } - .livesync-status { user-select: none; pointer-events: none; @@ -401,13 +485,13 @@ span.ls-mark-cr::after { font-size: 80%; } -div.workspace-leaf-content[data-type=bases] .livesync-status { +div.workspace-leaf-content[data-type="bases"] .livesync-status { top: calc(var(--bases-header-height) + var(--header-height)); padding: 5px; padding-right: 18px; } -.is-mobile div.workspace-leaf-content[data-type=bases] .livesync-status { +.is-mobile div.workspace-leaf-content[data-type="bases"] .livesync-status { top: calc(var(--bases-header-height) + var(--view-header-height)); padding: 6px; padding-right: 18px; @@ -422,7 +506,6 @@ div.workspace-leaf-content[data-type=bases] .livesync-status { .livesync-status .livesync-status-loghistory { text-align: left; opacity: 0.4; - } .livesync-status div.livesync-status-messagearea:empty { @@ -440,7 +523,6 @@ div.workspace-leaf-content[data-type=bases] .livesync-status { margin-left: auto; } - .menu-setting-poweruser-disabled .sls-setting-poweruser { display: none; } @@ -459,7 +541,7 @@ div.workspace-leaf-content[data-type=bases] .livesync-status { top: 2.5em; background-color: var(--background-secondary-alt); border-radius: 10px; - padding: 0.5em 1.0em; + padding: 0.5em 1em; } .active-pane .sls-setting-panel-title { @@ -476,6 +558,34 @@ div.workspace-leaf-content[data-type=bases] .livesync-status { font-size: 0.8em; } +body.is-mobile .livesync-message-box-container { + box-sizing: border-box; + padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px)); + padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px)); + padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)); + padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px)); +} + +body.is-mobile .livesync-message-box-container .modal { + max-height: 100%; +} + +body.is-mobile .livesync-message-box-container button { + height: auto; + min-height: var(--input-height); + white-space: normal; +} + +body.is-mobile .livesync-compatibility-review-notice { + box-sizing: border-box; + margin-right: calc(64px + var(--safe-area-inset-right, env(safe-area-inset-right, 0px))); + min-width: 0; + width: calc( + 100vw - 88px - var(--safe-area-inset-left, env(safe-area-inset-left, 0px)) - + var(--safe-area-inset-right, env(safe-area-inset-right, 0px)) + ); +} + .sls-qr { display: flex; justify-content: center; @@ -488,7 +598,98 @@ div.workspace-leaf-content[data-type=bases] .livesync-status { overflow-x: auto; white-space: pre-wrap; word-break: break-all; +} +.sls-repair-results { + display: grid; + gap: var(--size-4-3); +} + +.sls-repair-result { + padding: var(--size-4-3); + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + background: var(--background-secondary); +} + +.sls-repair-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--size-4-2); + min-width: 0; +} + +.sls-repair-header > :first-child { + flex: 1 1 auto; + min-width: 0; +} + +.sls-repair-header h6 { + margin: 0; + overflow-wrap: anywhere; +} + +.sls-repair-status { + display: flex; + flex-wrap: wrap; + gap: var(--size-4-2); + margin-top: var(--size-4-1); + font-size: var(--font-ui-smaller); +} + +.sls-repair-status-ok { + color: var(--text-success); +} + +.sls-repair-status-warning { + color: var(--text-warning); +} + +.sls-repair-metric { + margin-top: var(--size-4-1); + font-size: var(--font-ui-smaller); + line-height: var(--line-height-tight); + overflow-wrap: anywhere; +} + +.sls-repair-action-menu { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: var(--clickable-icon-size); + width: var(--clickable-icon-size); + height: var(--clickable-icon-size); + padding: 0; +} + +.sls-repair-action-menu .svg-icon { + width: 18px; + height: 18px; +} + +.sls-repair-revision { + margin-top: var(--size-4-2); + padding: var(--size-4-2); + border-left: 3px solid var(--background-modifier-border); + background: var(--background-primary-alt); +} + +.sls-repair-revision-title { + font-weight: var(--font-semibold); + overflow-wrap: anywhere; +} + +.sls-repair-revision code { + display: block; + margin-top: var(--size-4-1); + overflow-wrap: anywhere; + white-space: normal; +} + +.sls-repair-ancestor-warning { + margin-top: var(--size-4-2); + color: var(--text-warning); } /* Diff navigation */ @@ -570,4 +771,3 @@ div.workspace-leaf-content[data-type=bases] .livesync-status { outline-offset: 1px; border-radius: 2px; } - diff --git a/test/bench-network/Dockerfile.runner b/test/bench-network/Dockerfile.runner index 04d387c3..c04677bb 100644 --- a/test/bench-network/Dockerfile.runner +++ b/test/bench-network/Dockerfile.runner @@ -3,7 +3,7 @@ FROM node:24-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl unzip python3 make g++ iproute2 \ + && apt-get install -y --no-install-recommends ca-certificates curl unzip python3 make g++ iproute2 libjpeg-turbo-progs time \ && rm -rf /var/lib/apt/lists/* ENV DENO_INSTALL=/usr/local @@ -18,7 +18,7 @@ COPY src/apps/webpeer/package.json ./src/apps/webpeer/package.json RUN npm ci COPY . . -RUN npm run build -w self-hosted-livesync-cli +RUN LIVESYNC_CLI_TEST_SUPPORT=1 npm run build -w self-hosted-livesync-cli WORKDIR /workspace/src/apps/cli/testdeno @@ -28,7 +28,10 @@ RUN deno cache --lock=deno.lock \ bench-p2p-split-node.ts \ bench-p2p.ts \ bench-couchdb.ts \ - test-p2p-sync.ts + bench-compression.ts \ + test-p2p-sync.ts \ + test-p2p-replicator-replacement.ts \ + test-p2p-relay-disconnect.ts COPY test/bench-network/run-bench.sh /usr/local/bin/run-livesync-bench COPY src/apps/cli/testdeno/run-cli-e2e.sh /usr/local/bin/run-livesync-cli-e2e diff --git a/test/bench-network/README.md b/test/bench-network/README.md index 330b48a1..537f9b18 100644 --- a/test/bench-network/README.md +++ b/test/bench-network/README.md @@ -127,6 +127,49 @@ This sweep is useful for finding where the remote CouchDB path falls behind the local direct P2P path in the current HTTP-proxy latency model. It should not be presented as a full smartphone/VPN model. +## Data Compression benchmark + +The [Data Compression specification](../../docs/specs_data_compression.md) records the current storage contract, 1.0 decision, measured result, and follow-up optimisation candidates. This section is the benchmark runbook. + +The compression benchmark compares the exact CLI and Commonlib CouchDB path in +four configurations: E2EE off or on, each with Data Compression off or on. It +uses the normal CLI `mirror` and `sync` commands, so the recorded CouchDB +documents have passed through the current Rabin–Karp chunk splitter, optional +fflate compression, and E2EE V2 rather than a synthetic document transform. + +```bash +BENCH_COMMAND=compression \ +BENCH_COMPRESSION_REPEAT_COUNT=3 \ +BENCH_COUCHDB_RTT_MS=1 \ +docker compose -f test/bench-network/compose.yml run --build --rm bench-runner +``` + +The fixture contains current repository Markdown, PNG, JSON, and TypeScript +files; two deterministic JPEGs generated with `cjpeg`; a gzip-compressed +Markdown file; and deterministic high-entropy binary data. Every run verifies +all files after the second client has synchronised them. The JSON result under +`test/bench-network/bench-results/` records: + +- source bytes and stored chunk bytes by file kind; +- raw CouchDB external, active, and file sizes; +- request and response body bytes observed by the local HTTP proxy, including + the combined initial sync and full materialisation download; +- upload and download wall time, user and system CPU time, and maximum resident + memory for each CLI process; and +- percentage changes caused by enabling compression with E2EE both off and on. + +Use at least three repeats when making a default-setting decision. A `1 ms` +requested RTT keeps the local run focused on transform and storage costs. Run a +separate representative RTT when evaluating whether reduced request bodies +outweigh compression CPU on the intended network. HTTP byte counters cover +decoded bodies and exclude headers, while process timings include CLI start-up. +Full materialisation starts one CLI process per file and can repeat lazy chunk +fetches, so treat that phase as a CLI workflow measurement rather than a raw +download lower bound. Stored chunk size and upload request size are the more +direct transform comparisons. +The generated JPEGs are deterministic image-like fixtures, not a photographic +corpus, so broader media conclusions require a separately reviewed corpus. + ## Network emulation smoke The optional `netem` profile checks whether a Linux runner can apply traffic diff --git a/test/bench-network/compose.yml b/test/bench-network/compose.yml index c7d53d36..2c2b2407 100644 --- a/test/bench-network/compose.yml +++ b/test/bench-network/compose.yml @@ -75,6 +75,8 @@ services: BENCH_CASES: ${BENCH_CASES:-couchdb-baseline,p2p-direct-local} BENCH_REPEAT_COUNT: ${BENCH_REPEAT_COUNT:-1} BENCH_CASES_ROOT: /workspace/src/apps/cli/testdeno/bench-results + BENCH_COMPRESSION_RESULT_ROOT: /workspace/src/apps/cli/testdeno/bench-results + BENCH_COMPRESSION_REPEAT_COUNT: ${BENCH_COMPRESSION_REPEAT_COUNT:-1} BENCH_SWEEP_ROOT: /workspace/src/apps/cli/testdeno/bench-results BENCH_SWEEP_RTT_MS: ${BENCH_SWEEP_RTT_MS:-20,50,100,150,300} BENCH_SWEEP_INCLUDE_P2P: ${BENCH_SWEEP_INCLUDE_P2P:-true} @@ -97,7 +99,10 @@ services: BENCH_PEERS_TIMEOUT: ${BENCH_PEERS_TIMEOUT:-60} LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: ${LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS:-60000} BENCH_LIVESYNC_TEST_TEE: ${BENCH_LIVESYNC_TEST_TEE:-0} - CLI_E2E_TASK: ${CLI_E2E_TASK:-test:p2p-sync} + LIVESYNC_TEST_TEE: ${BENCH_LIVESYNC_TEST_TEE:-0} + LIVESYNC_CLI_DEBUG: ${LIVESYNC_CLI_DEBUG:-0} + LIVESYNC_CLI_VERBOSE: ${LIVESYNC_CLI_VERBOSE:-0} + CLI_E2E_TASK: ${CLI_E2E_TASK:-test:p2p:ci} RELAY: ${RELAY:-ws://nostr-relay:7777/} PEERS_TIMEOUT: ${PEERS_TIMEOUT:-20} SYNC_TIMEOUT: ${SYNC_TIMEOUT:-60} diff --git a/test/bench-network/run-bench.sh b/test/bench-network/run-bench.sh index 32272054..5cb158f0 100644 --- a/test/bench-network/run-bench.sh +++ b/test/bench-network/run-bench.sh @@ -8,12 +8,15 @@ case "${BENCH_COMMAND:-cases}" in latency-sweep) exec deno task bench:latency-sweep ;; + compression) + exec deno task bench:compression + ;; p2p-split-node) exec deno task bench:p2p-split-node ;; *) echo "Unknown BENCH_COMMAND: ${BENCH_COMMAND}" >&2 - echo "Expected one of: cases, latency-sweep, p2p-split-node" >&2 + echo "Expected one of: cases, latency-sweep, compression, p2p-split-node" >&2 exit 2 ;; esac diff --git a/test/contracts/serviceContext.ts b/test/contracts/serviceContext.ts new file mode 100644 index 00000000..ddfb6ce4 --- /dev/null +++ b/test/contracts/serviceContext.ts @@ -0,0 +1,78 @@ +import type { CommonlibMessageKey, ServiceContextContract } from "@vrtmrz/livesync-commonlib/context"; +import type { ServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/ServiceHub"; + +export const SERVICE_CONTEXT_MEMBERS = [ + "API", + "path", + "database", + "databaseEvents", + "replicator", + "fileProcessing", + "replication", + "remote", + "conflict", + "appLifecycle", + "setting", + "tweakValue", + "vault", + "test", + "UI", + "config", + "keyValueDB", + "control", +] as const satisfies readonly Exclude[]; + +export type ServiceContextMember = (typeof SERVICE_CONTEXT_MEMBERS)[number]; +type MissingServiceContextMember = Exclude, ServiceContextMember>; +const serviceContextMembersAreExhaustive: [MissingServiceContextMember] extends [never] ? true : never = true; +void serviceContextMembersAreExhaustive; + +export type ServiceContextResult = { + translation: string; + receivedEvents: string[]; +}; + +export type ServiceCompositionResult = { + hubUsesExpectedContext: boolean; + servicesUsingExpectedContext: Record; +}; + +/** + * Observe the host-neutral results promised by ServiceContextContract. + * + * The caller chooses the translation key because translated text is + * host-configured. Event delivery itself is shared behaviour. + */ +export function observeServiceContext( + context: ServiceContextContract, + translationKey: CommonlibMessageKey +): ServiceContextResult { + const receivedEvents: string[] = []; + const unsubscribe = context.events.onEvent("hello", (value) => receivedEvents.push(value)); + try { + context.events.emitEvent("hello", "context-contract-event"); + } finally { + unsubscribe(); + } + return { + translation: context.translate(translationKey), + receivedEvents, + }; +} + +/** + * Inspect whether a Service Hub and all public services preserve one exact + * context object instead of silently constructing or substituting another. + */ +export function observeServiceComposition( + hub: { readonly context: ServiceContextContract }, + expectedContext: ServiceContextContract +): ServiceCompositionResult { + const members = hub as unknown as Record; + return { + hubUsesExpectedContext: hub.context === expectedContext, + servicesUsingExpectedContext: Object.fromEntries( + SERVICE_CONTEXT_MEMBERS.map((member) => [member, members[member].context === expectedContext]) + ) as Record, + }; +} diff --git a/test/e2e-obsidian/README.md b/test/e2e-obsidian/README.md index 56008f2f..e8a749b8 100644 --- a/test/e2e-obsidian/README.md +++ b/test/e2e-obsidian/README.md @@ -1,20 +1,22 @@ # Real Obsidian E2E Runner -This directory contains the experimental real Obsidian end-to-end runner. +This directory contains the maintained real Obsidian end-to-end runner. The generic application discovery, isolated-vault, plug-in installation, process lifecycle, CLI, CDP, and readiness implementation comes from `@vrtmrz/obsidian-test-session`. The small modules under `runner/` preserve LiveSync's existing imports and supply its plug-in ID and artefact location. LiveSync-specific fixtures, services, settings, workflows, and assertions remain in this repository. -The current smoke runner verifies only the launch path: +The current smoke runner verifies the launch path and the loaded plug-in's Service Context composition: 1. create a temporary vault, -2. install the built Self-hosted LiveSync plug-in artifacts, +2. install the built Self-hosted LiveSync plug-in artefacts, 3. launch real Obsidian, 4. open the temporary vault through `obsidian-cli`, -5. enable Obsidian community plug-ins for the temporary app profile, -6. reload Self-hosted LiveSync through `obsidian-cli`, -7. verify through `obsidian-cli eval` that the plug-in is loaded, -8. optionally drive a real vault or CouchDB workflow through Obsidian's own API, -9. terminate Obsidian and remove the temporary vault. +5. prepare the isolated Vault trust state and handle any Obsidian trust prompt, +6. preserve natural plug-in loading, or complete requested pre-load work before loading the plug-in once in controlled start-up, +7. verify through the active renderer that the plug-in is loaded, +8. observe event and translation results from the actual `ObsidianServiceContext`, +9. verify that the Service Hub and every exposed service retain that exact Context, +10. optionally drive a real vault or CouchDB workflow through Obsidian's own API, and +11. terminate Obsidian and remove the temporary vault. The runner does not require Self-hosted LiveSync to expose an E2E-only bridge. Readiness is checked from outside the plug-in through Obsidian's own CLI. @@ -22,11 +24,35 @@ Obsidian 1.12 stores the global community plug-in switch outside `.obsidian/comm Future workflows should use `startObsidianLiveSyncSession()` from `runner/session.ts` rather than repeating the launch and plug-in readiness sequence. Add generic Obsidian bootstrap improvements to Fancy Kit; keep LiveSync behaviour and scenario helpers here. +When a LiveSync-owned scenario must establish application state before the plug-in's first load, pass an instance-scoped `lifecycle.beforePluginStart` callback through that wrapper. For example, the P2P pane scenario calls `setObsidianMobileTestModeBeforePluginStart()` there so LiveSync observes the mobile application state while registering its command and view. Mobile emulation reopens Obsidian's workspace layout; this helper waits for both the `is-mobile` body state and `workspace.layoutReady` before controlled loading continues. The shared package owns the controlled start-up order and guarantees that the plug-in loads once; the LiveSync scenario owns the resulting command, workspace placement, and visible UI assertions. Changing the state only after loading the plug-in is not evidence of its mobile start-up behaviour. + Each test vault uses an isolated Obsidian profile. The runner creates temporary directories for `HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, and Electron `--user-data-dir`, writes the vault registry into those directories, pre-seeds the temporary Chromium local storage so community plug-ins are trusted for that generated vault ID, and passes the same environment to `obsidian-cli`. This is intended to keep real Obsidian E2E runs separate from a developer's daily Obsidian profile and vault registry. +On macOS, `@vrtmrz/obsidian-test-session` keeps the generated Vault and profile below `/tmp` so Obsidian's Unix-domain CLI socket remains below the platform path limit. It also gives only the isolated Obsidian process Chromium's mock-keychain flag, preventing the empty test HOME from opening a blocking login-keychain dialogue. LiveSync's deterministic fixture selects the built-in default language so a host-language translation prompt cannot pause plug-in readiness. The case-only rename check enumerates the parent directory and compares exact spellings because an old-path lookup still resolves the renamed file on the default case-insensitive macOS filesystem. + +Multi-session workflows must keep each started Obsidian session tracked until its stop operation completes. If a scenario throws, teardown stops every active session before disposing its temporary Vault and profile, so a failed CLI or synchronisation operation cannot leave Obsidian using directories which have already been removed. + +## Observing and diagnosing a scenario + +Use externally visible behaviour as the pass condition: Vault files, remote-service state, revision data, or visible Obsidian UI. A log line can explain a failure, but should not replace an assertion about the resulting behaviour. + +The maintained runner provides several complementary observation paths: + +- `evalObsidianJson()` and `obsidian-cli eval` can read a small, explicitly selected piece of LiveSync or Obsidian state. +- `withObsidianPage()` can inspect the active renderer, invoke a registered command, or interact with visible UI through CDP. `captureObsidianPage()`, `captureObsidianDialogue()`, and `captureObsidianElement()` retain screenshots; the capture helpers also write a full-page `.failure.png` before rethrowing a UI assertion failure. +- `session.app.output()` returns the standard output and standard error captured from the isolated Obsidian process. This is especially useful when the renderer or CLI becomes unreachable. +- **Show log** (`obsidian-livesync:view-log`) exposes the recent LiveSync log, while **Copy full report to clipboard** (`obsidian-livesync:dump-debug-info`) opens the generated diagnostic report. `dialog-mounts.ts` verifies both surfaces, and focused scenarios may inspect the log pane and `appLifecycle.getUnresolvedMessages()` for a bounded set of expected errors. +- Renderer `console` messages and uncaught page errors are not retained automatically. A focused investigation can attach `page.on("console", ...)` and `page.on("pageerror", ...)` while it owns a `withObsidianPage()` callback. That observer ends when the callback closes its CDP connection, so use it around the action under investigation rather than treating it as a session-wide audit trail. + +If a scenario times out or appears to do nothing, capture the visible page before teardown, then record a bounded state snapshot and the relevant tail of the LiveSync log, unresolved messages, and process output. If an unexplained Notice appears, retain a screenshot while it is still visible before opening or dismissing it, then use the log or full report to identify its source. A Notice alone is not enough evidence for its cause. + +Set `showVerboseLog: true` only in isolated plug-in data when a focused investigation needs it. Keep captured output short and redact it before retaining or sharing it: logs and reports can contain Vault paths, document names, endpoints, credentials, Setup URIs, passphrases, or Security Seed material. Do not collect verbose logs from an ordinary user Vault. + +Collect evidence before cleanup, and keep process, Vault, profile, and remote-fixture cleanup in `finally`. After `app.emulateMobile(true)`, use the active CDP renderer for fixture operations because Obsidian may remove desktop-only CLI commands. Visually inspect screenshots before copying selected images into user documentation; a passing locator assertion does not establish that a dialogue is readable or unobstructed. + ## Local Setup -Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location. +Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location. Set `OBSIDIAN_CLI` as well when its companion executable is outside the built-in discovery paths. For an AppImage on Linux without FUSE, use the helper script: @@ -44,34 +70,66 @@ These tests are intended for local verification, not the default CI gate. Reuse ## Commands +After changing plug-in source, use the focused wrapper rather than invoking a scenario directly. It always rebuilds `main.js` before launching real Obsidian, and it builds the local CLI too when the CLI-to-Obsidian scenario needs it: + ```bash +npm run test:e2e:obsidian:focused -- settings-ui +npm run test:e2e:obsidian:focused -- two-vault-sync +npm run test:e2e:obsidian:focused -- security-seed-reconnect +``` + +The wrapper accepts only maintained real-Obsidian scenario names; run it with `--help` for the current list. It deliberately does not manage CouchDB, Object Storage, or the P2P signalling relay. Start the required fixture first, or use the complete service-managed suite. + +The principal entry points are: + +```bash +npm run test:contract:contexts +npm run test:contract:context:webapp +npm run test:contract:context:cli +npm run test:contract:context:obsidian +npm run test:e2e:obsidian:runner npm run test:e2e:obsidian:install-appimage npm run test:e2e:obsidian:discover npm run test:e2e:obsidian:cli-help -- vaults verbose -npm run test:e2e:obsidian:smoke -npm run test:e2e:obsidian:vault-reflection -npm run test:e2e:obsidian:couchdb-upload -npm run test:e2e:obsidian:cli-to-obsidian-sync -npm run test:e2e:obsidian:minio-upload -npm run test:e2e:obsidian:startup-scan -npm run test:e2e:obsidian:two-vault-sync -npm run test:e2e:obsidian:hidden-file-snippet-sync -npm run test:e2e:obsidian:customisation-sync -npm run test:e2e:obsidian:setting-markdown-export +npm run test:e2e:obsidian:upgrade-from-stable -- --transport all npm run test:e2e:obsidian:local-suite npm run test:e2e:obsidian:local-suite:services ``` -`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, vault reflection, CouchDB upload, CLI-to-Obsidian synchronisation, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB and MinIO fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. +The underlying `test:e2e:obsidian:` scripts remain available for an immediate rerun against an already built, unchanged bundle. They do not build `main.js`; do not use them as the first verification after a source change. The complete local suite performs its own build. -`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, configures Self-hosted LiveSync through `obsidian-cli eval`, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents. +`test:contract:contexts` runs the directly observable host contract against the Obsidian, CLI, and Webapp compositions. It verifies event and translation results, host-specific capabilities, and that the CLI and Webapp Service Hubs pass one exact Context to all exposed services. `test:contract:context:webapp` runs only the Webapp part. + +`test:contract:context:cli` builds the Node CLI and runs its existing Deno setup, put, read, list, information, remove, conflict-resolution, and revision workflow. `test:contract:context:obsidian` builds the plug-in and runs the real-Obsidian smoke test, including the Context inspection. These runtime scripts are local validation entry points and are not added to the default CI gate by this change. + +`test:e2e:obsidian:onboarding-invitation` starts an unconfigured temporary Vault with no plug-in data and verifies that startup selects Commonlib's new-Vault recommendations, offers the setup wizard without opening it, and does not scan Vault files automatically. It checks the invitation action and introduction in mobile test mode, then reopens the wizard from **Self-hosted LiveSync settings** → **Setup** on the desktop. This scenario owns the unconfigured-startup boundary only; configured compatibility review remains covered by `settings-ui`, and the setup workflows remain covered by their dedicated scenarios. + +`test:e2e:obsidian:dialog-mounts` starts a temporary real Obsidian session and exercises remote selection and CouchDB settings through `SetupManager`, plus Setup URI entry through the registered command. It verifies the compatibility pause and remote-size review, the distinction between a central data-storage server and P2P signalling, the explicit tested and untested CouchDB save actions, the internal-API warning, the Setup URI controls, automatic adjustment when differences are limited to compatible chunk settings, and both manual configuration-mismatch routes. The same session opens the live log and generated full report, reaches the `Hatch` recovery controls, writes and removes its own persistent log, and runs the missing-chunk recreation and file-verification actions against the empty disposable Vault. It captures representative desktop and mobile dialogues, checks the mobile layout and vertically stacked actions, closes each route through its normal controls, and verifies that each mounted operation settles without an error. It does not apply a remote configuration, contact a remote service, or claim to repair a deliberately damaged database. + +`test:e2e:obsidian:settings-ui` starts with a pending compatibility review and verifies the dedicated pause summary, its detailed explanation, and the explicit resume action in a temporary real Obsidian session. It captures the desktop summary and the iPhone-sized summary and detail dialogues; the mobile checks cover viewport containment, horizontal overflow, safe-area containment, and the close control's touch target. It confirms that the acknowledged internal version advances only after the review is accepted, and checks that the Change Log contains no acknowledgement control. It then selects the Synchronisation Settings pane and verifies that the deletion panel still exposes the effective 'Keep empty folder' setting without presenting the legacy `trashInsteadDelete` control, whose value no longer changes Obsidian deletion behaviour. + +The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixel viewport, and explicit iPhone-style safe-area insets of 47 pixels at the top and 34 pixels at the bottom. The public `@vrtmrz/obsidian-test-session` layout assertions require each modal to remain within the viewport and safe area without horizontal overflow. They also require the Obsidian Close control to remain within the safe area and provide at least a 44 by 44 CSS-pixel touch target. The runner clicks that control to verify actionability, then completes the explicit cancellation path. These simulated checks cover deterministic layout and interaction boundaries; they do not claim to reproduce a native operating-system overlay. + +`test:e2e:obsidian:review-harness` exercises only the boundaries owned by the opt-in maintainer Harness. It retains a real compatibility pause, uses the fixed Harness restart action to persist a device-local continuation and reload Obsidian, and requires the Harness to delete that state before reopening. It also runs the bounded local observations, confirms the dedicated Vault fixture root is removed, captures the copied privacy-bounded Markdown report, and checks the Harness layout and touch targets in mobile test mode. Compatibility explanation and persistence details remain owned by `settings-ui`, real P2P transfer remains owned by the dedicated P2P suites, and general Vault reflection remains owned by `vault-reflection`; the Harness test does not duplicate those workflows. + +`test:e2e:obsidian:p2p-pane` starts one configured CouchDB-only session with no P2P profile and separate configured P2P sessions for desktop and mobile. It proves that the command remains registered while the retired command, automatic pane, and ribbon entry without a P2P configuration are absent. For the configured P2P profiles, it verifies that the desktop ribbon is available, the current status command reaches the pane without it opening at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. The mobile session uses a fresh Vault, profile, and Obsidian process, enters `app.emulateMobile(true)` through `lifecycle.beforePluginStart`, and requires the P2P view to belong to the right drawer rather than inheriting desktop workspace state. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions. + +`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, revision repair, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run. + +`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, starts from configured plug-in data without the device-local compatibility marker, and verifies the copied-or-restored Vault explanation in the actual compatibility dialogue. It captures the summary and details, resumes explicitly, confirms that the marker was recorded, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents. The same workflow checks the two remote-activity status boundaries. It first holds a real CouchDB request at the selected fetch implementation and confirms that `🌐N` is visible while `📲` is absent. It then holds the real one-shot replication immediately before its replicator call, confirms that `📲` is visible while no physical request is active, releases it, and requires the finite and bounded activity counts to return to zero, the request and response counts to balance, and both indicators to disappear. Finally, it creates a remote-only chunk, holds the real on-demand fetch immediately before its remote call, makes the same logical active and idle assertions, and verifies that the fetched chunk is written into the local database. These gates make the active states deterministic without replacing the remote request or operation. -If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. +`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible onboarding path for the first device when no Setup URI is available. It enters end-to-end encryption and CouchDB details, runs the read-only `Check server requirements` step, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After Rebuild completes on the first device, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked. + +If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option, and the Security Seed reconnect workflow captures each significant application state. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory. + +The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a decision for the first device. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode. `test:e2e:obsidian:cli-to-obsidian-sync` is the cross-runtime compatibility check for the official LiveSync CLI and the real Obsidian plug-in. Build the plug-in first, and build the local CLI too when no external CLI command is selected. The script uses E2EE, Path Obfuscation, and the current preferred chunk settings to create and synchronise a note through the CLI, starts real Obsidian with an isolated Vault and profile, synchronises the same CouchDB database, and verifies that the plug-in materialises identical note content. This covers the boundary that CLI-only and plug-in-only round trips do not exercise. +The isolated Obsidian session starts with its CouchDB settings and device-local compatibility acknowledgement already in place. This keeps the scenario focused on cross-runtime data compatibility; unconfigured start-up and visible CouchDB onboarding are covered by their dedicated workflows. + By default, the compatibility check runs `node src/apps/cli/dist/index.cjs`. Set `LIVESYNC_CLI_COMMAND` to test another CLI build or distribution. The value may be a quoted command line or a JSON array of executable and prefix arguments; the scenario arguments are appended without going through a shell. For example, to test an executable on `PATH`: @@ -87,23 +145,58 @@ LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) -- npm run test:e2e:obsidian:cli-to-obsidian-sync ``` -`test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service, configures Self-hosted LiveSync for Object Storage through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix. Adapter tests separately observe an in-progress SDK command, while this real-runtime workflow verifies the resulting request counters advance and rebalance. +`test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service and starts with isolated Object Storage settings and the device-local compatibility acknowledgement already in place, keeping the scenario focused on upload rather than unconfigured start-up or setup. It confirms those settings through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix. Adapter tests separately observe an in-progress SDK command, while this real-runtime workflow verifies the resulting request counters advance and rebalance. -`test:e2e:obsidian:startup-scan` configures a temporary CouchDB database, stops Obsidian, writes a note directly into the vault, restarts Obsidian, and verifies from CouchDB that the boot-time scan picked up the offline file. +`test:e2e:obsidian:object-storage-setup-uri-workflow` uses the public Commonlib-backed tool to generate the initial Setup URI for a unique MinIO prefix, completes visible initialisation on the first device, and then asks that working real Obsidian device to create a new Setup URI through the registered command. A second real Obsidian device imports only the device-generated URI. The workflow verifies A-to-B and B-to-A notes, captures the documented onboarding choices, and removes the Object Storage prefix only after both sessions have stopped. -`test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, ordinary rename, a case-only file name change within the same directory, deletion, per-device target filters where one vault ignores a note that the other vault synchronises, and a separate encrypted round-trip with Path Obfuscation enabled. Directory case changes deliberately remain outside this scenario because they require directory-aware rename handling. The optional Markdown conflict automatic merge check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`, but it is not part of the default local suite. +`test:e2e:obsidian:p2p-setup-uri-workflow` runs two concurrent isolated real Obsidian sessions against the local Compose Nostr relay fixture. The first device imports a generated initial Setup URI and completes its signalling test with zero peers, creates a Setup URI for the second device through the registered command, and remains online while the second device imports it. The second device must select the expected online source before Fetch can rebuild its local database. The workflow accepts each connection request visibly on the receiving device, verifies the initial A-to-B fetch, checks that the menu for the three persistent per-peer actions remains within the viewport, reconnects both P2P sessions in join order, and verifies the B-to-A return journey. Every started session remains tracked until teardown completes. -`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. +`test:e2e:obsidian:startup-scan` starts from a CouchDB fixture using current settings with its device-local compatibility marker already acknowledged, stops Obsidian, writes a note directly into the Vault, restarts the same isolated Vault and profile without rewriting its plug-in data, and verifies from CouchDB that the start-up scan picked up the offline file. Onboarding remains covered by `onboarding-invitation`; this scenario owns the ordinary configured restart and start-up scan. + +`test:e2e:obsidian:setup-uri-workflow` runs the repository's public Commonlib-backed CouchDB provisioning and Setup URI tools against the local CouchDB fixture. It configures a new, empty Vault in the first real Obsidian session through the visible onboarding wizard and uses Rebuild. After that device is working, it generates a new Setup URI through the registered command; the second real Obsidian Vault uses that URI for Fetch instead of reusing the initial Setup URI produced by the provisioning tool. The workflow verifies ordinary notes from the first device to the second and back again, independently enables Hidden File Sync on each device, and verifies a snippet. The retained Setup URI screenshots show only encrypted URIs and visually masked Setup URI passphrases; plaintext credentials are not captured. Files prefixed with `guide-` capture the relevant dialogue, settings panel, or workspace leaf without transient Notices. Public documentation copies selected images only after visual inspection; the E2E run does not overwrite repository documentation assets. + +`test:e2e:obsidian:two-vault-sync` runs a two-vault note synchronisation workflow. It verifies note creation, update, ordinary rename, a case-only file name change within the same directory, deletion, and a separate encrypted round-trip with Path Obfuscation enabled. Its target-filter scenario confirms that one Vault receives and checkpoints a remote document without reflecting it, restarts with the same profile and filter, and then reflects the stored document after the filter is broadened through the settings service. Directory case changes deliberately remain outside this scenario because they require directory-aware rename handling. The optional Markdown conflict check can be enabled with `E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT=true`. It creates divergent revisions in two separate Vaults, performs a conservative merge on one Vault, edits that result again, and requires the other Vault to replace its known deleted losing revision without recreating the conflict. The separate `E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS=true` check keeps four conflicts active while one Vault edits, deletes, performs a case-only rename, and performs a cross-path rename. It asserts that each operation extends the revision displayed on that device, replicates the exact resulting revision tree, and preserves the other live branch. During focused development, `E2E_OBSIDIAN_ONLY_CONFLICT_OPERATIONS=true` runs that self-contained scope without the ordinary, target-filter, or encrypted scenarios. Both conflict checks remain outside the default local suite. + +`test:e2e:obsidian:security-seed-reconnect` is a focused CouchDB release-acceptance workflow. Device A first recognises an initial remote Security Seed, stops automatic replication while remaining open, and creates an unsent note. The runner replaces only the Security Seed in the managed remote synchronisation-parameter fixture. Device A must retain its deliberately stale cached value until the next one-shot synchronisation, refresh it before sending, and upload an HKDF-encrypted payload which uses the replacement value. A fresh device B must decrypt that note and send an encrypted note back; the original device A then receives the return journey with its Vault and isolated profile preserved. Desktop Obsidian may enforce a single application instance, so the two device sessions run sequentially after the same-process stale-cache assertion has completed. + +The workflow creates a random dedicated database, records only SHA-256 Seed fingerprints, and never writes a Seed, passphrase, or CouchDB credentials to its result. It also requires the remote Seed and all other synchronisation parameters to remain unchanged after the replacement revision, rejects HKDF and Seed errors from either session, writes `security-seed-reconnect-result.json`, and verifies that every Obsidian process, temporary Vault, isolated profile, and database has been removed. The result file and stage screenshots are retained in `E2E_OBSIDIAN_DIAGNOSTICS_DIR`; the screenshots show ordinary Vault content, not settings or secrets. The strict cleanup workflow rejects `E2E_OBSIDIAN_KEEP_VAULT` and `E2E_OBSIDIAN_KEEP_COUCHDB`. + +This proves in real Obsidian the plug-in behaviour shared by supported platforms, including the encrypted bidirectional round-trip and protection against a stale client restoring the old remote Seed. It does not verify iPadOS-specific background or reconnect lifecycle behaviour, and it does not count as Android device evidence. The workflow remains outside `test:e2e:obsidian:local-suite` because it is a focused release-acceptance check. + +`test:e2e:obsidian:conflict-dialog-policy` creates three real local revision leaves without a remote service and opens the pairwise merge dialogue in Obsidian. It verifies the three-version count, requires the four decision buttons to be stacked vertically, concatenates the displayed pair as a child of the displayed winner, confirms that the untouched leaf remains as one conflict, postpones that remaining pair, restarts the same isolated Vault and profile, and confirms that only the two live versions are reconstructed. It also verifies that an ordinary repeated conflict check does not reopen a postponed dialogue, that **Resolve if conflicted.** explicitly reopens it, and that the active editor retains the appropriate unresolved-conflict warning. The scenario then invokes the same Commonlib consumer boundary used for an incoming replicated document and checks that a postponed warning disappears, an open stale dialogue closes, and the conflict-processing queue completes even when the dialogue closes immediately. This isolates the Obsidian UI contract from transport and second-device setup. The fixture owns one temporary Vault and profile, and the session runner stops Obsidian before removing them. + +`test:e2e:obsidian:revision-repair` creates an ordinary healthy logical deletion and two conflicting live revisions in a temporary real Obsidian Vault, then removes a chunk used only by the non-winning revision. It proves that automatic conflict checking does not discard the unreadable branch, and that a healthy logical deletion with no Vault file is neither reported nor retained as Vault provenance. **Inspect conflicts and file/database differences** must show the winner and conflict separately, identify the exact unreadable revision and missing chunk, show the compact `Δsize` and `Δtime` diagnostics, and expose a wrench menu with the appropriate actions for each branch. The scenario opens the existing comparison dialogue in read-only mode, applies the readable winner to the Vault, shows the compact matching-winner and remaining-conflict status, records the exact winner as Vault provenance without creating a child, and confirms that retrying the unreadable branch leaves the revision tree unchanged. It then verifies both the cancellation path and the explicit confirmation path for discarding only that selected live branch, requires the winner and its Vault provenance to remain unchanged, and captures the repair card, a 360-pixel-wide reflow check, the matching-winner status, both revision menus, and the read-only comparison. The narrow capture checks responsive layout, not a mobile operating-system lifecycle. The scenario uses no remote service; a retry is therefore expected to remain unreadable unless the chunk is already available locally. + +`test:e2e:obsidian:hidden-file-snippet-sync` runs a two-vault hidden file round-trip. It verifies creation and deletion of a real `.obsidian/snippets/*.css` file, automatic JSON conflict merging for a hidden file with the merged result propagated by a second synchronisation, manual JSON Resolve dialogue application through Obsidian's UI, and per-device target patterns where one vault ignores a hidden file that the other vault synchronises. Initial enablement must open one user-visible progress Notice before the enabled setting is saved, then retain that Notice while its nested rebuild and scan phases continue in the ordinary log. The configured fixture starts with a current CouchDB remote profile, so migration from legacy remote settings remains the responsibility of the upgrade scenarios and cannot add unrelated Notices to this check. It also covers [issue #555](https://github.com/vrtmrz/obsidian-livesync/issues/555) by requiring several plug-in and settings changes to share one separate action Notice whose controls remain usable in mobile layouts; a manually dismissed group must not repeat its acknowledged rows when a later change arrives. `test:e2e:obsidian:customisation-sync` runs a two-vault Customisation Sync workflow. It scans a real snippet CSS file, config JSON file, and sample plug-in fixture into per-file Customisation Sync data, synchronises the entries through CouchDB, applies them on the second vault, verifies the resulting `.obsidian` files, propagates a snippet update, and verifies deletion of the source-vault snippet sync data without confusing it with the target vault's own applied copy. `test:e2e:obsidian:setting-markdown-export` enables setting Markdown export, waits for the generated Markdown file in the vault, and verifies that credentials are omitted when `writeCredentialsForSettingSync=false`. +`test:e2e:obsidian:upgrade-from-stable` is the release-acceptance upgrade workflow. It installs the exact published 0.25.83 artefacts into an isolated Vault, verifies their pinned SHA-256 values, and then replaces only the plug-in artefacts with the current target while retaining the same Vault and isolated Obsidian profile. The first run downloads the old release into the ignored `_testdata/releases` cache; every later run verifies the cached bytes before use. + +The workflow first exercises a non-empty legacy settings document which has no `isConfigured` or file-name case value. It verifies that 0.25.83 treats a default-equivalent document as unconfigured. That release can persist the inferred boolean during a later, unrelated settings-save event, so the runner accepts either an absent value or the inferred `false` on disk, then restores the same minimal pre-flag document deliberately before installing 1.0. The target independently proves its direct migration: the Vault remains unconfigured instead of receiving new-Vault recommendations, case-insensitive handling becomes explicit, no compatibility pause or acknowledgement marker is created while onboarding remains pending, and a second 1.0 start is idempotent. The absent marker is deliberately deferred rather than accepted; a later configured start must evaluate it. This fixture rewrite is limited to the missing-flag boundary; the configured transport upgrades use only state created and saved by 0.25.83 itself. + +For CouchDB and Object Storage, the workflow then configures 0.25.83 from its own defaults, saves the selected remote, and restarts that release with the same profile before creating history. This both verifies that the old settings persist and lets the old release initialise its replicator from the same saved state as an ordinary existing Vault. The runner waits for that release's asynchronously initialised persistent node identity, creates, edits, renames, and deletes notes, and synchronises each transition before installing the target. Every launch of the upgraded device uses the same isolated Obsidian profile. The session layer closes the renderer before its process-tree fallback, so Chromium persists the legacy compatibility marker naturally; the target must read and migrate that actual profile state to its current namespaced key. The final target restart likewise consumes the marker persisted by the preceding target session. The runner does not reconstruct that device's Vault data, plug-in settings, local database files, device-local state, or remote state. Before the target performs any synchronisation, it must retain the same Vault profile, local database, node identity, remote profile, local checkpoint, and remote milestone. The local node-info document is the identity source of truth; a transient replicator field is used only to confirm that the old asynchronous initialisation has completed. Its first synchronisation must be a no-op: CouchDB document revisions and `update_seq` must remain unchanged, while Object Storage must neither upload nor download journal bodies. The upgraded device then sends a new delta. A separate fresh 1.0 verifier starts from an explicit fixture containing settings and compatibility state for the current version, receives the complete surviving history, and returns another delta; it is not part of the migration assertion for legacy remote settings. The upgraded Vault receives that return journey and retains it across restart. + +Before creating stable-release history, the runner waits until the remote Security Seed can be read and only then marks the remote as resolved. Completion of the old release's remote-creation method alone does not prove that this asynchronous fixture boundary is ready. + +Run the focused wrapper after source changes so that the target plug-in is rebuilt first: + +```bash +npm run test:e2e:obsidian:focused -- upgrade-from-stable --transport all --manage-services +``` + +Use `--transport couchdb` or `--transport object-storage` for a focused rerun. `--manage-services` starts and stops the required local fixture or fixtures; add `--keep-services` only when they should remain available for inspection. Set `E2E_LIVESYNC_TARGET_ARTIFACT_ROOT` to validate another already-built target directory, or `E2E_LIVESYNC_SOURCE_ARTIFACT_ROOT` to use an explicit cache directory whose files still match the pinned release hashes. + +This workflow is deliberately excluded from `local-suite`. It downloads a published historical artefact, reuses one profile across multiple application versions, and is an expensive release-acceptance gate rather than a routine current-version scenario. P2P is also excluded because cross-version P2P interoperability is a separate physical validation boundary. + Start the local fixtures first when they are not already running: ```bash npm run test:docker-couchdb:start npm run test:docker-s3:start +npm run test:docker-p2p:start npm run test:e2e:obsidian:local-suite ``` @@ -116,6 +209,7 @@ npm run test:e2e:obsidian:local-suite:services Useful environment variables: - `OBSIDIAN_BINARY`: explicit Obsidian executable path. +- `OBSIDIAN_CLI`: explicit companion `obsidian-cli` executable path. - `E2E_OBSIDIAN_VERSION`: Obsidian AppImage version for `test:e2e:obsidian:install-appimage`; default is `1.12.7`. - `E2E_OBSIDIAN_APPIMAGE_ARCH`: AppImage architecture override, such as `arm64` or `x86_64`. - `E2E_OBSIDIAN_APPIMAGE_URL`: explicit AppImage URL override. @@ -123,17 +217,30 @@ Useful environment variables: - `E2E_OBSIDIAN_FORCE_DOWNLOAD=true`: re-download the AppImage even when it exists. - `E2E_OBSIDIAN_SKIP_EXTRACT=true`: download the AppImage without extracting it. - `E2E_OBSIDIAN_SMOKE_TIMEOUT_MS`: smoke timeout in milliseconds. +- `E2E_OBSIDIAN_DIALOG_TIMEOUT_MS`: timeout for a representative Svelte dialogue to mount, expose its principal controls, and close; default is 10 seconds. +- `E2E_OBSIDIAN_REVISION_REPAIR_TIMEOUT_MS`: timeout for each visible revision-repair control and result; default is 15 seconds. +- `E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS`: timeout for the settings pane and its deletion controls to become visible; default is 10 seconds. +- `E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS`: timeout for Review Harness view and action boundaries; default is 15 seconds. +- `E2E_OBSIDIAN_P2P_PANE_TIMEOUT_MS`: timeout for the P2P status pane and its principal connection control; default is 10 seconds. +- `E2E_OBSIDIAN_P2P_WORKFLOW_TIMEOUT_MS`: timeout for each visible P2P Setup URI, peer-discovery, approval, and replication control; default is 60 seconds. +- `E2E_P2P_RELAY_URL`: signalling relay used by the real-Obsidian P2P workflow; default is the local relay at `ws://127.0.0.1:4010/`. +- `E2E_P2P_RELAY_PORT`: host port for the local P2P relay fixture; default is `4010`. +- `E2E_OBSIDIAN_SECONDARY_REMOTE_DEBUGGING_PORT`: CDP port for the second concurrent real Obsidian session; default is one greater than the primary port. - `E2E_OBSIDIAN_READY_TIMEOUT_MS`: plug-in readiness timeout in milliseconds. - `E2E_OBSIDIAN_CLI_READY_TIMEOUT_MS`: timeout for waiting until the vault-side Obsidian CLI exposes the plug-in catalogue. - `E2E_OBSIDIAN_CLI_TIMEOUT_MS`: timeout for each `obsidian-cli` invocation. - `E2E_LIVESYNC_CLI_TIMEOUT_MS`: timeout for each official LiveSync CLI invocation in the CLI-to-Obsidian compatibility check; default is 60 seconds. - `LIVESYNC_CLI_COMMAND`: optional LiveSync CLI executable and prefix arguments used by the CLI-to-Obsidian compatibility check. The default is the locally built CLI. +- `E2E_LIVESYNC_SOURCE_ARTIFACT_ROOT`: optional cache directory containing the exact pinned 0.25.83 plug-in artefacts. Cached files are always checksum-verified. +- `E2E_LIVESYNC_TARGET_ARTIFACT_ROOT`: directory containing the built 1.0 target `main.js`, `manifest.json`, and `styles.css`; default is the repository root. +- `E2E_OBSIDIAN_ARTIFACT_ROOT`: directory containing the plug-in artefact installed by a direct scenario invocation; default is the repository root. +- `E2E_OBSIDIAN_ARTIFACT_REVISION`: exact source commit recorded by the Security Seed reconnect result when `E2E_OBSIDIAN_ARTIFACT_ROOT` is a downloaded artefact rather than a Git worktree. - `E2E_OBSIDIAN_FILE_TIMEOUT_MS`: timeout for waiting until a note created through Obsidian's vault API is reflected to disk. - `E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS`: timeout for waiting until Self-hosted LiveSync reports that its core lifecycle and local database are ready. - `E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS`: timeout for waiting until a file appears in Self-hosted LiveSync's local database. - `E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS`: timeout for waiting until CouchDB contains uploaded E2E documents. - `E2E_OBSIDIAN_REMOTE_ACTIVITY_TIMEOUT_MS`: timeout for an observed remote activity to enter or leave its status boundary; default is 30 seconds. -- `E2E_OBSIDIAN_DIAGNOSTICS_DIR`: directory for screenshots and status snapshots captured after a remote-activity failure; default is `/tmp/obsidian-livesync-e2e`. +- `E2E_OBSIDIAN_DIAGNOSTICS_DIR`: directory for screenshots and status snapshots, including the Security Seed reconnect stages; default is `/tmp/obsidian-livesync-e2e`. - `E2E_OBSIDIAN_OBJECT_STORAGE_TIMEOUT_MS`: timeout for waiting until Object Storage contains uploaded E2E objects. - `E2E_OBSIDIAN_KEEP_COUCHDB=true`: keep the temporary CouchDB database for inspection. - `E2E_OBSIDIAN_KEEP_OBJECT_STORAGE=true`: keep the temporary Object Storage prefix for inspection. diff --git a/test/e2e-obsidian/runner/couchdb.ts b/test/e2e-obsidian/runner/couchdb.ts index 0f7c1550..1ed61f88 100644 --- a/test/e2e-obsidian/runner/couchdb.ts +++ b/test/e2e-obsidian/runner/couchdb.ts @@ -26,6 +26,28 @@ export type CouchDbAllDocsResponse = { }>; }; +export type CouchDbLocalDocsResponse = { + rows: Array<{ + id: string; + key: string; + value: { rev: string }; + doc?: CouchDbDocument; + }>; +}; + +export type CouchDbDatabaseInfo = { + db_name: string; + doc_count: number; + doc_del_count: number; + update_seq: number | string; +}; + +export type CouchDbPutResponse = { + ok: boolean; + id: string; + rev: string; +}; + function parseEnvFile(content: string): Record { const entries = content .split(/\r?\n/u) @@ -63,6 +85,14 @@ function databaseUrl(config: Pick, dbName: string, suffix return `${config.uri.replace(/\/+$/u, "")}/${encodeURIComponent(dbName)}${suffix}`; } +function documentSuffix(documentId: string): string { + const localPrefix = "_local/"; + if (documentId.startsWith(localPrefix)) { + return `/_local/${encodeURIComponent(documentId.slice(localPrefix.length))}`; + } + return `/${encodeURIComponent(documentId)}`; +} + async function couchDbRequest( config: Pick, path: string, @@ -130,8 +160,8 @@ export async function putCouchDbDocument( config: CouchDbConfig, dbName: string, document: CouchDbDocument -): Promise { - const response = await fetch(databaseUrl(config, dbName, `/${encodeURIComponent(document._id)}`), { +): Promise { + const response = await fetch(databaseUrl(config, dbName, documentSuffix(document._id)), { method: "PUT", headers: { authorization: authHeader(config), @@ -144,6 +174,23 @@ export async function putCouchDbDocument( `Failed to write CouchDB document ${document._id}. HTTP ${response.status}: ${await response.text()}` ); } + return (await response.json()) as CouchDbPutResponse; +} + +export async function fetchCouchDbDocument( + config: CouchDbConfig, + dbName: string, + documentId: string +): Promise { + const response = await fetch(databaseUrl(config, dbName, documentSuffix(documentId)), { + headers: { authorization: authHeader(config) }, + }); + if (!response.ok) { + throw new Error( + `Failed to read CouchDB document ${documentId}. HTTP ${response.status}: ${await response.text()}` + ); + } + return (await response.json()) as CouchDbDocument; } export async function deleteCouchDbDatabase(config: CouchDbConfig, dbName: string): Promise { @@ -158,6 +205,19 @@ export async function deleteCouchDbDatabase(config: CouchDbConfig, dbName: strin } } +export async function couchDbDatabaseExists(config: CouchDbConfig, dbName: string): Promise { + const response = await fetch(databaseUrl(config, dbName), { + headers: { authorization: authHeader(config) }, + }); + if (response.status === 404) { + return false; + } + if (!response.ok) { + throw new Error(`Failed to inspect CouchDB ${dbName}. HTTP ${response.status}: ${await response.text()}`); + } + return true; +} + export async function fetchAllCouchDbDocs(config: CouchDbConfig, dbName: string): Promise { const response = await fetch(databaseUrl(config, dbName, "/_all_docs?include_docs=true"), { headers: { authorization: authHeader(config) }, @@ -170,6 +230,28 @@ export async function fetchAllCouchDbDocs(config: CouchDbConfig, dbName: string) return (await response.json()) as CouchDbAllDocsResponse; } +export async function fetchCouchDbLocalDocs(config: CouchDbConfig, dbName: string): Promise { + const response = await fetch(databaseUrl(config, dbName, "/_local_docs?include_docs=true"), { + headers: { authorization: authHeader(config) }, + }); + if (!response.ok) { + throw new Error( + `Failed to read CouchDB local documents from ${dbName}. HTTP ${response.status}: ${await response.text()}` + ); + } + return (await response.json()) as CouchDbLocalDocsResponse; +} + +export async function fetchCouchDbDatabaseInfo(config: CouchDbConfig, dbName: string): Promise { + const response = await fetch(databaseUrl(config, dbName), { + headers: { authorization: authHeader(config) }, + }); + if (!response.ok) { + throw new Error(`Failed to inspect CouchDB ${dbName}. HTTP ${response.status}: ${await response.text()}`); + } + return (await response.json()) as CouchDbDatabaseInfo; +} + export async function waitForCouchDbDocs( config: CouchDbConfig, dbName: string, diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts new file mode 100644 index 00000000..85f64421 --- /dev/null +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.test.ts @@ -0,0 +1,95 @@ +import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { describe, expect, it, vi } from "vitest"; + +const { evalObsidianJson } = vi.hoisted(() => ({ + evalObsidianJson: vi.fn(), +})); + +vi.mock("./cli.ts", () => ({ evalObsidianJson })); + +import { + assertE2eCompatibilityMarker, + createE2eCouchDbPluginData, + prepareRemote, + waitForLiveSyncCoreReady, + type CompatibilityMarkerState, +} from "./liveSyncWorkflow.ts"; + +describe("compatibility marker persistence", () => { + it("waits for an accepted review to reach device-local storage", async () => { + const pending: CompatibilityMarkerState = { + vaultName: "fixture", + additionalSuffix: "-", + expectedStorageKey: "fixture--database-compatibility-version", + rawStorageValue: null, + serviceValue: "", + versionUpFlash: "", + }; + const persisted: CompatibilityMarkerState = { + ...pending, + rawStorageValue: `${VER}`, + serviceValue: `${VER}`, + }; + evalObsidianJson.mockResolvedValueOnce(pending).mockResolvedValueOnce(persisted); + + await expect( + assertE2eCompatibilityMarker("obsidian-cli", {}, { timeoutMs: 100, intervalMs: 0 }) + ).resolves.toEqual(persisted); + expect(evalObsidianJson).toHaveBeenCalledTimes(2); + }); +}); + +describe("configured CouchDB fixture", () => { + it("uses a current remote profile for ordinary configured fixtures", () => { + const pluginData = createE2eCouchDbPluginData({ + uri: "https://couch.example", + username: "alice", + password: "secret", + dbName: "notes", + }); + const remoteConfigurations = pluginData.remoteConfigurations as + | Record + | undefined; + + expect(remoteConfigurations).toBeDefined(); + expect(Object.keys(remoteConfigurations ?? {})).toHaveLength(1); + expect(pluginData.activeConfigurationId).toBe(Object.keys(remoteConfigurations ?? {})[0]); + }); +}); + +describe("Real Obsidian core readiness", () => { + it("retries while the plug-in core is temporarily unavailable during reload", async () => { + evalObsidianJson.mockReset(); + evalObsidianJson + .mockRejectedValueOnce(new Error("Cannot read properties of undefined (reading 'core')")) + .mockResolvedValueOnce({ + databaseReady: true, + appReady: true, + configured: true, + remoteType: "", + settingVersion: 10, + suspended: false, + }); + + await expect(waitForLiveSyncCoreReady("obsidian-cli", {}, 1000)).resolves.toMatchObject({ + databaseReady: true, + appReady: true, + }); + expect(evalObsidianJson).toHaveBeenCalledTimes(2); + }); +}); + +describe("remote fixture preparation", () => { + it("waits for the remote Security Seed after resolving a new remote", async () => { + evalObsidianJson.mockReset(); + evalObsidianJson.mockResolvedValueOnce({ status: "resolved", securitySeedReady: true }); + + await prepareRemote("obsidian-cli", {}); + + const evaluatedCode = String(evalObsidianJson.mock.calls[0]?.[1] ?? ""); + expect(evaluatedCode.indexOf("markRemoteResolved")).toBeLessThan( + evaluatedCode.indexOf("ensurePBKDF2Salt") + ); + expect(evaluatedCode).toContain("Timed out preparing the remote Security Seed"); + }); +}); diff --git a/test/e2e-obsidian/runner/liveSyncWorkflow.ts b/test/e2e-obsidian/runner/liveSyncWorkflow.ts index ef842c30..896fb058 100644 --- a/test/e2e-obsidian/runner/liveSyncWorkflow.ts +++ b/test/e2e-obsidian/runner/liveSyncWorkflow.ts @@ -1,6 +1,12 @@ import { evalObsidianJson } from "./cli.ts"; +import { SERVICE_CONTEXT_MEMBERS } from "../../contracts/serviceContext.ts"; +import { DATABASE_COMPATIBILITY_VERSION_KEY } from "../../../src/common/databaseCompatibility.ts"; +import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; +import { type ObsidianLiveSyncSettings, VER } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { upsertRemoteConfigurationInPlace } from "@vrtmrz/livesync-commonlib/remote-configurations"; import type { CouchDbConfig } from "./couchdb.ts"; import type { ObjectStorageConfig } from "./objectStorage.ts"; +import { captureObsidianDialogue, withObsidianPage } from "./ui.ts"; export type ConfiguredSettings = { isConfigured: boolean; @@ -18,6 +24,48 @@ export type ConfiguredSettings = { export type CoreReadiness = { databaseReady: boolean; appReady: boolean; + configured?: boolean; + remoteType?: string; + settingVersion?: number; + suspended?: boolean; +}; + +export type ReplicationAttempt = CoreReadiness & { + succeeded: boolean; + isOnline: boolean; + activeReplicator: string; + versionUpFlash: string; + unresolvedMessages: unknown[]; +}; + +export type CompatibilityMarkerState = { + vaultName: string; + additionalSuffix: string; + expectedStorageKey: string; + rawStorageValue: string | null; + serviceValue: string; + versionUpFlash: string; +}; + +export type CompatibilityMarkerWaitOptions = { + timeoutMs?: number; + intervalMs?: number; +}; + +export type ResumeCompatibilityReviewOptions = { + verifyMissingDeviceMarkerExplanation?: boolean; + screenshotPrefix?: string; +}; + +export type ObsidianServiceContextContractResult = { + contextType: string; + eventResult: string[]; + translationResult: string; + hubUsesContext: boolean; + serviceContextMismatches: string[]; + appCapabilityMatches: boolean; + pluginCapabilityMatches: boolean; + liveSyncPluginCapabilityMatches: boolean; }; export type LocalDatabaseEntry = { @@ -28,28 +76,182 @@ export type LocalDatabaseEntry = { children: string[]; }; -function e2ePreferredSettingsSource(): string[] { - return [ - "liveSync:false,", - "syncOnStart:false,", - "syncOnSave:false,", - "usePluginSync:false,", - "usePluginSyncV2:true,", - "useEden:false,", - "customChunkSize:60,", - "sendChunksBulk:false,", - "sendChunksBulkMaxSize:1,", - "chunkSplitterVersion:'v3-rabin-karp',", - "readChunksOnline:true,", - "disableCheckingConfigMismatch:false,", - "enableCompression:false,", - "hashAlg:'xxhash64',", - "handleFilenameCaseSensitive:false,", - "doNotUseFixedRevisionForChunks:true,", - "E2EEAlgorithm:'v2',", - "doctorProcessedVersion:'0.25.27',", - "isConfigured:true,", - ]; +const E2E_PREFERRED_SETTINGS = { + displayLanguage: "def", + liveSync: false, + syncOnStart: false, + syncOnSave: false, + usePluginSync: false, + usePluginSyncV2: true, + useEden: false, + customChunkSize: 60, + sendChunksBulk: false, + sendChunksBulkMaxSize: 1, + chunkSplitterVersion: "v3-rabin-karp", + readChunksOnline: true, + disableCheckingConfigMismatch: false, + enableCompression: false, + hashAlg: "xxhash64", + handleFilenameCaseSensitive: false, + doNotUseFixedRevisionForChunks: true, + E2EEAlgorithm: "v2", + doctorProcessedVersion: "0.25.27", + settingVersion: CURRENT_SETTING_VERSION, + isConfigured: true, +} as const; + +export function createE2eObsidianDeviceLocalState( + vaultName: string, + additionalSuffixOfDatabaseName = "" +): Readonly> { + return { + [`${vaultName}-${additionalSuffixOfDatabaseName}-${DATABASE_COMPATIBILITY_VERSION_KEY}`]: `${VER}`, + }; +} + +export async function readE2eCompatibilityMarker( + cliBinary: string, + env: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const setting=core.services.setting;", + "const settings=setting.currentSettings();", + "const vaultName=core.services.API.getSystemVaultName();", + `const markerKey=${JSON.stringify(DATABASE_COMPATIBILITY_VERSION_KEY)};`, + "const additionalSuffix=`-${settings.additionalSuffixOfDatabaseName??''}`;", + "const expectedStorageKey=`${vaultName}${additionalSuffix}-${markerKey}`;", + "return JSON.stringify({", + "vaultName,additionalSuffix,expectedStorageKey,", + "rawStorageValue:localStorage.getItem(expectedStorageKey),", + "serviceValue:setting.getSmallConfig(markerKey),", + "versionUpFlash:settings.versionUpFlash,", + "});", + "})()", + ].join(""), + env + ); +} + +export async function assertE2eCompatibilityMarker( + cliBinary: string, + env: NodeJS.ProcessEnv, + options: CompatibilityMarkerWaitOptions = {} +): Promise { + const timeoutMs = options.timeoutMs ?? Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000); + const intervalMs = options.intervalMs ?? 100; + const deadline = Date.now() + timeoutMs; + let state = await readE2eCompatibilityMarker(cliBinary, env); + while (state.serviceValue !== `${VER}` && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + state = await readE2eCompatibilityMarker(cliBinary, env); + } + if (state.serviceValue !== `${VER}`) + throw new Error(`The E2E compatibility marker was not persisted before timeout: ${JSON.stringify(state)}`); + return state; +} + +export async function assertE2eCompatibilityReviewPending( + cliBinary: string, + env: NodeJS.ProcessEnv +): Promise { + const state = await readE2eCompatibilityMarker(cliBinary, env); + if (state.serviceValue !== "" || state.rawStorageValue !== null || state.versionUpFlash === "") { + throw new Error(`The copied-Vault compatibility review was not pending: ${JSON.stringify(state)}`); + } + return state; +} + +export async function resumeCompatibilityReview( + port: number, + options: ResumeCompatibilityReviewOptions = {} +): Promise { + const timeoutMs = Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000); + const title = "Synchronisation paused for compatibility review"; + const summaryLocator = (page: Parameters[1]>[0]) => + page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: title }), + }); + + if (options.screenshotPrefix) { + const summaryScreenshot = await captureObsidianDialogue( + port, + `${options.screenshotPrefix}-summary.png`, + async (page) => { + await summaryLocator(page).waitFor({ state: "visible", timeout: timeoutMs }); + } + ); + console.log(`Compatibility review summary screenshot: ${summaryScreenshot}`); + } + + if (options.verifyMissingDeviceMarkerExplanation === true) { + await withObsidianPage(port, async (page) => { + const summary = summaryLocator(page); + await summary.waitFor({ state: "visible", timeout: timeoutMs }); + await summary.getByRole("button", { name: "Review compatibility details" }).click(); + }); + const detailsScreenshot = options.screenshotPrefix + ? await captureObsidianDialogue(port, `${options.screenshotPrefix}-details.png`, async (page) => { + const details = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }), + }); + await details.waitFor({ state: "visible", timeout: timeoutMs }); + await details.getByText("copied or restored", { exact: false }).waitFor({ + state: "visible", + timeout: timeoutMs, + }); + await details.getByText("new Obsidian profile", { exact: false }).waitFor({ + state: "visible", + timeout: timeoutMs, + }); + await details + .getByText("does not mean that it is safe to resume automatically", { exact: false }) + .waitFor({ + state: "visible", + timeout: timeoutMs, + }); + }) + : undefined; + if (detailsScreenshot) console.log(`Compatibility review details screenshot: ${detailsScreenshot}`); + await withObsidianPage(port, async (page) => { + const details = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }), + }); + await details.getByRole("button", { name: "Back to compatibility review" }).click(); + await summaryLocator(page).waitFor({ state: "visible", timeout: timeoutMs }); + }); + } + + await withObsidianPage(port, async (page) => { + const summary = summaryLocator(page); + await summary.waitFor({ state: "visible", timeout: timeoutMs }); + await summary.getByRole("button", { name: "Resume synchronisation" }).click(); + await summary.waitFor({ state: "hidden", timeout: timeoutMs }); + }); +} + +export function createE2eCouchDbPluginData( + settings: Pick & { dbName: string }, + overrides: Record = {} +): Record { + const pluginData = { + couchDB_URI: settings.uri, + couchDB_USER: settings.username, + couchDB_PASSWORD: settings.password, + couchDB_DBNAME: settings.dbName, + remoteType: "", + ...E2E_PREFERRED_SETTINGS, + ...overrides, + }; + upsertRemoteConfigurationInPlace(pluginData as ObsidianLiveSyncSettings, "couchdb", { + id: "e2e-couchdb", + name: "E2E CouchDB", + activate: true, + }); + return pluginData; } export function assertEqual(actual: unknown, expected: unknown, message: string): void { @@ -64,21 +266,14 @@ export async function configureCouchDb( settings: Pick & { dbName: string }, overrides: Record = {} ): Promise { + const nextSettings = createE2eCouchDbPluginData(settings, overrides); return await evalObsidianJson( cliBinary, [ "(async()=>{", "const plugin=app.plugins.plugins['obsidian-livesync'];", "const core=plugin.core;", - "const nextSettings={", - `couchDB_URI:${JSON.stringify(settings.uri)},`, - `couchDB_USER:${JSON.stringify(settings.username)},`, - `couchDB_PASSWORD:${JSON.stringify(settings.password)},`, - `couchDB_DBNAME:${JSON.stringify(settings.dbName)},`, - "remoteType:'',", - ...e2ePreferredSettingsSource(), - ...Object.entries(overrides).map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)},`), - "};", + `const nextSettings=${JSON.stringify(nextSettings)};`, "await core.services.setting.applyExternalSettings(nextSettings,true);", "await core.services.control.applySettings();", "const current=core.services.setting.currentSettings();", @@ -103,25 +298,14 @@ export async function configureObjectStorage( settings: ObjectStorageConfig & { bucketPrefix: string }, overrides: Record = {} ): Promise { + const nextSettings = createE2eObjectStoragePluginData(settings, overrides); return await evalObsidianJson( cliBinary, [ "(async()=>{", "const plugin=app.plugins.plugins['obsidian-livesync'];", "const core=plugin.core;", - "const nextSettings={", - "remoteType:'MINIO',", - `endpoint:${JSON.stringify(settings.endpoint)},`, - `accessKey:${JSON.stringify(settings.accessKey)},`, - `secretKey:${JSON.stringify(settings.secretKey)},`, - `bucket:${JSON.stringify(settings.bucket)},`, - `region:${JSON.stringify(settings.region)},`, - `forcePathStyle:${JSON.stringify(settings.forcePathStyle)},`, - `bucketPrefix:${JSON.stringify(settings.bucketPrefix)},`, - "bucketCustomHeaders:'',", - ...e2ePreferredSettingsSource(), - ...Object.entries(overrides).map(([key, value]) => `${JSON.stringify(key)}:${JSON.stringify(value)},`), - "};", + `const nextSettings=${JSON.stringify(nextSettings)};`, "await core.services.setting.applyExternalSettings(nextSettings,true);", "await core.services.control.applySettings();", "const current=core.services.setting.currentSettings();", @@ -143,6 +327,25 @@ export async function configureObjectStorage( ); } +export function createE2eObjectStoragePluginData( + settings: ObjectStorageConfig & { bucketPrefix: string }, + overrides: Record = {} +): Record { + return { + remoteType: "MINIO", + endpoint: settings.endpoint, + accessKey: settings.accessKey, + secretKey: settings.secretKey, + bucket: settings.bucket, + region: settings.region, + forcePathStyle: settings.forcePathStyle, + bucketPrefix: settings.bucketPrefix, + bucketCustomHeaders: "", + ...E2E_PREFERRED_SETTINGS, + ...overrides, + }; +} + export async function waitForLiveSyncCoreReady( cliBinary: string, env: NodeJS.ProcessEnv, @@ -150,29 +353,116 @@ export async function waitForLiveSyncCoreReady( ): Promise { const deadline = Date.now() + timeoutMs; let lastReadiness: CoreReadiness | undefined; + let lastError: unknown; while (Date.now() < deadline) { - lastReadiness = await evalObsidianJson( - cliBinary, - [ - "(async()=>{", - "const core=app.plugins.plugins['obsidian-livesync'].core;", - "return JSON.stringify({", - "databaseReady:core.services.database.isDatabaseReady(),", - "appReady:core.services.appLifecycle.isReady(),", - "});", - "})()", - ].join(""), - env - ); + try { + lastReadiness = await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync']?.core;", + "if(!core) return JSON.stringify({databaseReady:false,appReady:false});", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "configured:settings?.isConfigured===true,", + "remoteType:settings?.remoteType??'',", + "settingVersion:settings?.settingVersion,", + "suspended:core.services.appLifecycle.isSuspended(),", + "});", + "})()", + ].join(""), + env + ); + lastError = undefined; + } catch (error) { + // Obsidian reloads the renderer while enabling the plug-in. During + // that short window the CLI can reach the Vault before the plug-in + // catalogue has exposed its core. This is a readiness state, not a + // failed scenario, so retain the error for the eventual timeout. + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 500)); + continue; + } if (lastReadiness.databaseReady && lastReadiness.appReady) { return lastReadiness; } await new Promise((resolve) => setTimeout(resolve, 500)); } - throw new Error(`Timed out waiting for Self-hosted LiveSync core readiness: ${JSON.stringify(lastReadiness)}`); + const errorSuffix = + lastError === undefined + ? "" + : ` Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`; + throw new Error( + `Timed out waiting for Self-hosted LiveSync core readiness: ${JSON.stringify(lastReadiness)}${errorSuffix}` + ); +} + +/** + * Inspect the actual Obsidian composition through Obsidian's CLI. + * + * This observes public Context results and verifies that the Hub and every + * exposed service retain the exact Context created by the plug-in host. + */ +export async function inspectObsidianServiceContextContract( + cliBinary: string, + env: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const plugin=app.plugins.plugins['obsidian-livesync'];", + "const services=plugin.core.services;", + "const context=services.context;", + `const serviceNames=${JSON.stringify(SERVICE_CONTEXT_MEMBERS)};`, + "const eventResult=[];", + "const unsubscribe=context.events.onEvent('hello',(value)=>eventResult.push(value));", + "try{context.events.emitEvent('hello','context-contract-event');}finally{unsubscribe();}", + "return JSON.stringify({", + "contextType:context.constructor.name,", + "eventResult,", + "translationResult:context.translate('Replicator.Message.InitialiseFatalError'),", + "hubUsesContext:services.context===context,", + "serviceContextMismatches:serviceNames.filter((name)=>services[name].context!==context),", + "appCapabilityMatches:context.app===app,", + "pluginCapabilityMatches:context.plugin===plugin,", + "liveSyncPluginCapabilityMatches:context.liveSyncPlugin===plugin,", + "});", + "})()", + ].join(""), + env + ); +} + +export function assertObsidianServiceContextContract(result: ObsidianServiceContextContractResult): void { + assertEqual(result.contextType, "ObsidianServiceContext", "Unexpected Obsidian service Context type."); + assertEqual(result.hubUsesContext, true, "The Obsidian Service Hub substituted its host Context."); + assertEqual( + result.serviceContextMismatches.length, + 0, + `Services used a different Context: ${result.serviceContextMismatches.join(", ")}` + ); + assertEqual( + JSON.stringify(result.eventResult), + JSON.stringify(["context-contract-event"]), + "The Obsidian Context event API returned an unexpected result." + ); + if (result.translationResult.length === 0) { + throw new Error("The Obsidian Context translator returned an empty result."); + } + assertEqual(result.appCapabilityMatches, true, "The Obsidian Context lost its App capability."); + assertEqual(result.pluginCapabilityMatches, true, "The Obsidian Context lost its Plugin capability."); + assertEqual( + result.liveSyncPluginCapabilityMatches, + true, + "The Obsidian Context lost its Self-hosted LiveSync plug-in capability." + ); } export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + const timeoutMs = Number(process.env.E2E_OBSIDIAN_REMOTE_PREPARE_TIMEOUT_MS ?? 20000); await evalObsidianJson( cliBinary, [ @@ -182,8 +472,16 @@ export async function prepareRemote(cliBinary: string, env: NodeJS.ProcessEnv): "const replicator=core.services.replicator.getActiveReplicator();", "await replicator.tryCreateRemoteDatabase(settings);", "await replicator.markRemoteResolved(settings);", + `const deadline=Date.now()+${JSON.stringify(timeoutMs)};`, + "let securitySeedReady=false;", + "do{", + "securitySeedReady=await replicator.ensurePBKDF2Salt(settings,false,false);", + "if(securitySeedReady) break;", + "await new Promise((resolve)=>setTimeout(resolve,250));", + "}while(Date.now() { - await evalObsidianJson( + const attempt = await evalObsidianJson( cliBinary, [ "(async()=>{", "const core=app.plugins.plugins['obsidian-livesync'].core;", "await core.services.fileProcessing.commitPendingFileEvents();", "const result=await core.services.replication.replicate(true);", - "return JSON.stringify({result:!!result});", + "const settings=core.services.setting.currentSettings();", + "const activeReplicator=core.services.replicator.getActiveReplicator();", + "return JSON.stringify({", + "succeeded:!!result,", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "isOnline:core.services.API.isOnline,", + "activeReplicator:activeReplicator?.constructor?.name??'(none)',", + "versionUpFlash:settings.versionUpFlash,", + "unresolvedMessages:(await core.services.appLifecycle.getUnresolvedMessages()).flat(),", + "});", "})()", ].join(""), env ); + if (!attempt.succeeded) { + throw new Error(`Finite replication did not start or complete: ${JSON.stringify(attempt)}`); + } } export async function waitForLocalDatabaseEntry( diff --git a/test/e2e-obsidian/runner/mobileUi.ts b/test/e2e-obsidian/runner/mobileUi.ts new file mode 100644 index 00000000..d492092d --- /dev/null +++ b/test/e2e-obsidian/runner/mobileUi.ts @@ -0,0 +1,163 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { + assertLocatorHasMinimumTouchTarget, + assertLocatorWithinSafeArea, + assertLocatorWithinViewport, + assertNoHorizontalOverflow, +} from "@vrtmrz/obsidian-test-session"; +import type { Locator, Page } from "playwright"; +import { withObsidianPage } from "./ui.ts"; + +export const mobileViewport = { width: 390, height: 844 } as const; +export const desktopViewport = { width: 1024, height: 768 } as const; +export const iPhoneSafeArea = { top: 47, right: 0, bottom: 34, left: 0 } as const; + +type ObsidianTestApp = { + isMobile?: boolean; + emulateMobile?: (mobile: boolean) => void; + plugins?: { plugins: Record }; + workspace?: { layoutReady?: boolean }; +}; + +type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; + +async function applyObsidianMobileTestMode( + port: number, + enabled: boolean, + timeoutMs: number, + waitForLiveSync: boolean +): Promise { + await withObsidianPage(port, async (page) => { + await page.setViewportSize(enabled ? mobileViewport : desktopViewport); + await page.evaluate((nextEnabled) => { + const obsidianApp = (globalThis as ObsidianTestGlobal).app; + if (typeof obsidianApp?.emulateMobile !== "function") { + throw new Error("app.emulateMobile is unavailable"); + } + obsidianApp.emulateMobile(nextEnabled); + }, enabled); + // Obsidian reopens its workspace layout when platform emulation + // changes. Loading a controlled plug-in before that transition has + // completed can leave the plug-in enabled but absent from the active + // renderer. + try { + await page.waitForFunction( + ({ nextEnabled, waitForLiveSync }) => { + const obsidianApp = (globalThis as ObsidianTestGlobal).app; + return ( + document.body.classList.contains("is-mobile") === nextEnabled && + obsidianApp?.workspace?.layoutReady === true && + (!waitForLiveSync || obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined) + ); + }, + { nextEnabled: enabled, waitForLiveSync }, + { timeout: timeoutMs } + ); + } catch (error) { + const state = await page.evaluate(() => { + const obsidianApp = (globalThis as ObsidianTestGlobal).app; + return { + appIsMobile: obsidianApp?.isMobile ?? null, + bodyClasses: document.body.className, + documentReadyState: document.readyState, + liveSyncLoaded: obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined, + viewport: { width: window.innerWidth, height: window.innerHeight }, + workspaceLayoutReady: obsidianApp?.workspace?.layoutReady ?? null, + }; + }); + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + await mkdir(outputDirectory, { recursive: true }); + const screenshotPath = join( + outputDirectory, + waitForLiveSync + ? "mobile-mode-transition.failure.png" + : "mobile-mode-before-plugin-start.failure.png" + ); + await page.screenshot({ path: screenshotPath, fullPage: true }); + const detail = error instanceof Error ? error.message : String(error); + throw new Error( + `Obsidian mobile-mode transition did not settle: ${JSON.stringify(state)}; screenshot=${screenshotPath}; cause=${detail}` + ); + } + await page.evaluate( + (safeArea) => { + for (const edge of ["top", "right", "bottom", "left"] as const) { + const property = `--safe-area-inset-${edge}`; + if (safeArea === null) document.body.style.removeProperty(property); + else document.body.style.setProperty(property, `${safeArea[edge]}px`); + } + }, + enabled ? iPhoneSafeArea : null + ); + }); +} + +/** Enters mobile emulation before LiveSync's first load in a controlled session. */ +export async function setObsidianMobileTestModeBeforePluginStart( + port: number, + enabled: boolean, + timeoutMs: number +): Promise { + await applyObsidianMobileTestMode(port, enabled, timeoutMs, false); +} + +export async function setObsidianMobileTestMode(port: number, enabled: boolean, timeoutMs: number): Promise { + await applyObsidianMobileTestMode(port, enabled, timeoutMs, true); +} + +export async function assertMobileDialogueLayout(page: Page, container: Locator, label: string): Promise { + const dialogue = container.locator(".modal").last(); + const closeButton = dialogue.locator(".modal-close-button"); + await assertLocatorWithinViewport(page, dialogue, { label }); + await assertNoHorizontalOverflow(page, dialogue, { label }); + await assertLocatorWithinSafeArea(page, dialogue, { + label, + safeAreaInsets: iPhoneSafeArea, + }); + await assertLocatorWithinSafeArea(page, closeButton, { + label: `${label} close button`, + safeAreaInsets: iPhoneSafeArea, + }); + await assertLocatorHasMinimumTouchTarget(page, closeButton, { + label: `${label} close button`, + }); + + const visibleButtons = dialogue.locator("button:visible"); + for (let index = 0; index < (await visibleButtons.count()); index++) { + const button = visibleButtons.nth(index); + const buttonLabel = (await button.innerText()).trim() || `button ${index + 1}`; + await assertLocatorWithinViewport(page, button, { label: `${label}: ${buttonLabel}` }); + await assertLocatorWithinSafeArea(page, button, { + label: `${label}: ${buttonLabel}`, + safeAreaInsets: iPhoneSafeArea, + }); + await assertNoHorizontalOverflow(page, button, { label: `${label}: ${buttonLabel}` }); + await assertLocatorHasMinimumTouchTarget(page, button, { label: `${label}: ${buttonLabel}` }); + } +} + +export async function assertMobileNoticeLayout( + page: Page, + notice: Locator, + label: string, + reservedRightPx = 56 +): Promise { + await assertLocatorWithinViewport(page, notice, { label }); + await assertNoHorizontalOverflow(page, notice, { label }); + await assertLocatorWithinSafeArea(page, notice, { + label, + safeAreaInsets: iPhoneSafeArea, + }); + const box = await notice.boundingBox(); + if (box === null) { + throw new Error(`${label} did not expose a measurable viewport rectangle.`); + } + const viewportWidth = await page.evaluate(() => window.innerWidth); + const rightEdge = box.x + box.width; + if (rightEdge > viewportWidth - reservedRightPx) { + throw new Error( + `${label} overlaps the reserved close-control column: right edge ${rightEdge}, limit ${viewportWidth - reservedRightPx}.` + ); + } +} diff --git a/test/e2e-obsidian/runner/objectStorage.ts b/test/e2e-obsidian/runner/objectStorage.ts index f0ea8455..01fee5cf 100644 --- a/test/e2e-obsidian/runner/objectStorage.ts +++ b/test/e2e-obsidian/runner/objectStorage.ts @@ -1,6 +1,7 @@ import { CreateBucketCommand, DeleteObjectsCommand, + GetObjectCommand, ListObjectsV2Command, S3Client, type _Object, @@ -120,6 +121,22 @@ export async function listObjectStorageObjects(config: ObjectStorageConfig, pref } } +export async function readObjectStorageObject(config: ObjectStorageConfig, key: string): Promise { + const client = createObjectStorageClient(config); + try { + const response = await client.send(new GetObjectCommand({ Bucket: config.bucket, Key: key })); + if (!response.Body) throw new Error(`Object Storage returned an empty body for ${key}.`); + return await response.Body.transformToByteArray(); + } finally { + client.destroy(); + } +} + +export async function readObjectStorageJson(config: ObjectStorageConfig, key: string): Promise { + const bytes = await readObjectStorageObject(config, key); + return JSON.parse(new TextDecoder().decode(bytes)) as T; +} + export async function deleteObjectStoragePrefix(config: ObjectStorageConfig, prefix: string): Promise { const client = createObjectStorageClient(config); try { diff --git a/test/e2e-obsidian/runner/pathAssertions.test.ts b/test/e2e-obsidian/runner/pathAssertions.test.ts new file mode 100644 index 00000000..c7306737 --- /dev/null +++ b/test/e2e-obsidian/runner/pathAssertions.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { hasExactCaseOnlyRename } from "./pathEntries.ts"; + +describe("case-only rename assertions", () => { + it("accepts only the exact new spelling", () => { + expect(hasExactCaseOnlyRename(["case-rename.md"], "Case-Rename.md", "case-rename.md")).toBe(true); + }); + + it("rejects the old spelling even when a case-insensitive lookup would resolve it", () => { + expect(hasExactCaseOnlyRename(["Case-Rename.md"], "Case-Rename.md", "case-rename.md")).toBe(false); + }); + + it("rejects an ambiguous directory containing both spellings", () => { + expect(hasExactCaseOnlyRename(["Case-Rename.md", "case-rename.md"], "Case-Rename.md", "case-rename.md")).toBe( + false + ); + }); +}); diff --git a/test/e2e-obsidian/runner/pathAssertions.ts b/test/e2e-obsidian/runner/pathAssertions.ts new file mode 100644 index 00000000..4d9730d9 --- /dev/null +++ b/test/e2e-obsidian/runner/pathAssertions.ts @@ -0,0 +1,30 @@ +import { readdir } from "node:fs/promises"; +import { basename, dirname, join } from "node:path"; +import { hasExactCaseOnlyRename } from "./pathEntries.ts"; + +export async function waitForExactCaseOnlyRename( + vaultPath: string, + oldPath: string, + newPath: string, + timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 10000) +): Promise { + const oldDirectory = dirname(oldPath); + const newDirectory = dirname(newPath); + if (oldDirectory !== newDirectory) { + throw new Error(`Case-only rename paths must share one parent directory: ${oldPath} -> ${newPath}`); + } + + const oldName = basename(oldPath); + const newName = basename(newPath); + const directoryPath = join(vaultPath, newDirectory); + const deadline = Date.now() + timeoutMs; + let lastEntries: string[] = []; + while (Date.now() < deadline) { + lastEntries = await readdir(directoryPath); + if (hasExactCaseOnlyRename(lastEntries, oldName, newName)) return; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error( + `Timed out waiting for exact case-only rename: ${oldPath} -> ${newPath}. Directory entries: ${JSON.stringify(lastEntries)}` + ); +} diff --git a/test/e2e-obsidian/runner/pathEntries.ts b/test/e2e-obsidian/runner/pathEntries.ts new file mode 100644 index 00000000..241061c7 --- /dev/null +++ b/test/e2e-obsidian/runner/pathEntries.ts @@ -0,0 +1,3 @@ +export function hasExactCaseOnlyRename(entries: readonly string[], oldName: string, newName: string): boolean { + return entries.includes(newName) && !entries.includes(oldName); +} diff --git a/test/e2e-obsidian/runner/releaseArtifact.test.ts b/test/e2e-obsidian/runner/releaseArtifact.test.ts new file mode 100644 index 00000000..ef76b7ce --- /dev/null +++ b/test/e2e-obsidian/runner/releaseArtifact.test.ts @@ -0,0 +1,76 @@ +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + ensurePinnedReleaseArtifact, + type PinnedPluginRelease, +} from "./releaseArtifact.ts"; + +const temporaryDirectories: string[] = []; + +function sha256(content: string): string { + return createHash("sha256").update(content).digest("hex"); +} + +function fixtureRelease(contents: Record<"main.js" | "manifest.json" | "styles.css", string>): PinnedPluginRelease { + return { + pluginId: "fixture-plugin", + version: "1.2.3", + files: (Object.keys(contents) as Array).map((name) => ({ + name, + url: `https://example.invalid/${name}`, + sha256: sha256(contents[name]), + })), + }; +} + +afterEach(async () => { + for (const path of temporaryDirectories.splice(0)) { + await rm(path, { recursive: true, force: true }); + } +}); + +describe("pinned plug-in release artefacts", () => { + it("downloads, verifies, and reuses an immutable release cache", async () => { + const root = await mkdtemp(join(tmpdir(), "livesync-release-artifact-")); + temporaryDirectories.push(root); + const contents = { + "main.js": "console.log('fixture');\n", + "manifest.json": '{"id":"fixture-plugin","version":"1.2.3"}\n', + "styles.css": ".fixture {}\n", + }; + const release = fixtureRelease(contents); + const fetchImplementation = vi.fn(async (input: string | URL | Request) => { + const name = new URL(String(input)).pathname.split("/").pop() as keyof typeof contents; + return new Response(contents[name], { status: 200 }); + }) as unknown as typeof fetch; + + await expect( + ensurePinnedReleaseArtifact(release, { artifactRoot: root, fetchImplementation }) + ).resolves.toBe(root); + await expect(readFile(join(root, "main.js"), "utf8")).resolves.toBe(contents["main.js"]); + expect(fetchImplementation).toHaveBeenCalledTimes(3); + + await ensurePinnedReleaseArtifact(release, { artifactRoot: root, fetchImplementation }); + expect(fetchImplementation).toHaveBeenCalledTimes(3); + }); + + it("rejects a downloaded file before it enters the release cache when its checksum differs", async () => { + const root = await mkdtemp(join(tmpdir(), "livesync-release-artifact-")); + temporaryDirectories.push(root); + const contents = { + "main.js": "expected\n", + "manifest.json": '{"id":"fixture-plugin","version":"1.2.3"}\n', + "styles.css": ".fixture {}\n", + }; + const release = fixtureRelease(contents); + const fetchImplementation = vi.fn(async () => new Response("tampered\n", { status: 200 })) as unknown as typeof fetch; + + await expect( + ensurePinnedReleaseArtifact(release, { artifactRoot: root, fetchImplementation }) + ).rejects.toThrow("checksum mismatch"); + await expect(readFile(join(root, "main.js"))).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); diff --git a/test/e2e-obsidian/runner/releaseArtifact.ts b/test/e2e-obsidian/runner/releaseArtifact.ts new file mode 100644 index 00000000..2c0e62e1 --- /dev/null +++ b/test/e2e-obsidian/runner/releaseArtifact.ts @@ -0,0 +1,128 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; + +export type PinnedReleaseArtifactFile = { + name: "main.js" | "manifest.json" | "styles.css"; + url: string; + sha256: string; +}; + +export type PinnedPluginRelease = { + pluginId: string; + version: string; + files: readonly PinnedReleaseArtifactFile[]; +}; + +export type EnsurePinnedReleaseArtifactOptions = { + artifactRoot?: string; + fetchImplementation?: typeof fetch; +}; + +export const UPGRADE_SOURCE_RELEASE: PinnedPluginRelease = { + pluginId: "obsidian-livesync", + version: "0.25.83", + files: [ + { + name: "main.js", + url: "https://github.com/vrtmrz/obsidian-livesync/releases/download/0.25.83/main.js", + sha256: "5e57f990635ab0cf2ff3879f3c6cb91ddfdbc146958d33d1e5d21f1869dff6a4", + }, + { + name: "manifest.json", + url: "https://github.com/vrtmrz/obsidian-livesync/releases/download/0.25.83/manifest.json", + sha256: "4944f5665c94bcbb58db0e3708ec2bd8ee36118791271c01d085668876dc8ba6", + }, + { + name: "styles.css", + url: "https://github.com/vrtmrz/obsidian-livesync/releases/download/0.25.83/styles.css", + sha256: "37d31798186d7e97ea979e6d2aae8021ea1ac1df2c3b9d2b03dce269959c27f3", + }, + ], +}; + +function digest(content: Uint8Array): string { + return createHash("sha256").update(content).digest("hex"); +} + +function assertDigest(file: PinnedReleaseArtifactFile, content: Uint8Array): void { + const actual = digest(content); + if (actual !== file.sha256) { + throw new Error( + `Release artefact checksum mismatch for ${file.name}. Expected ${file.sha256}, received ${actual}.` + ); + } +} + +async function readCachedFile( + path: string, + file: PinnedReleaseArtifactFile +): Promise | undefined> { + try { + const content = new Uint8Array(await readFile(path)); + assertDigest(file, content); + return content; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + throw error; + } +} + +async function downloadVerifiedFile( + root: string, + file: PinnedReleaseArtifactFile, + fetchImplementation: typeof fetch +): Promise> { + const path = join(root, file.name); + const cached = await readCachedFile(path, file); + if (cached) return cached; + + const response = await fetchImplementation(file.url, { redirect: "follow" }); + if (!response.ok) { + throw new Error(`Could not download ${file.url}. HTTP ${response.status}: ${await response.text()}`); + } + const content = new Uint8Array(await response.arrayBuffer()); + assertDigest(file, content); + + const temporaryPath = `${path}.download-${process.pid}-${Date.now()}`; + try { + await writeFile(temporaryPath, content, { flag: "wx" }); + await rename(temporaryPath, path); + } finally { + await rm(temporaryPath, { force: true }); + } + return content; +} + +/** + * Materialise one immutable published plug-in release in the ignored E2E cache. + * + * Existing files are always verified before use. A mismatched cache is left in + * place for inspection and must be removed explicitly by the operator. + */ +export async function ensurePinnedReleaseArtifact( + release: PinnedPluginRelease = UPGRADE_SOURCE_RELEASE, + options: EnsurePinnedReleaseArtifactOptions = {} +): Promise { + const root = resolve( + options.artifactRoot ?? + process.env.E2E_LIVESYNC_SOURCE_ARTIFACT_ROOT?.trim() ?? + join("_testdata", "releases", release.pluginId, release.version) + ); + await mkdir(root, { recursive: true }); + + const fetched = new Map>(); + for (const file of release.files) { + fetched.set(file.name, await downloadVerifiedFile(root, file, options.fetchImplementation ?? fetch)); + } + + const manifestBytes = fetched.get("manifest.json"); + if (!manifestBytes) throw new Error("The pinned release does not define manifest.json."); + const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as { id?: unknown; version?: unknown }; + if (manifest.id !== release.pluginId || manifest.version !== release.version) { + throw new Error( + `Release manifest identity mismatch. Expected ${release.pluginId}@${release.version}, received ${String(manifest.id)}@${String(manifest.version)}.` + ); + } + return root; +} diff --git a/test/e2e-obsidian/runner/securitySeed.test.ts b/test/e2e-obsidian/runner/securitySeed.test.ts new file mode 100644 index 00000000..02966683 --- /dev/null +++ b/test/e2e-obsidian/runner/securitySeed.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { + SECURITY_SEED_DOCUMENT_ID, + changedSynchronisationParameterFields, + fingerprintSecuritySeed, + replaceSecuritySeed, + requireSecuritySeedDocument, + snapshotSecuritySeedDocument, +} from "./securitySeed.ts"; + +const seedA = Buffer.alloc(32, 1).toString("base64"); +const seedB = Buffer.alloc(32, 2).toString("base64"); + +describe("Security Seed E2E evidence", () => { + it("reports stable, non-secret fingerprints", () => { + expect(fingerprintSecuritySeed(seedA)).toMatch(/^sha256:[0-9a-f]{16}$/u); + expect(fingerprintSecuritySeed(seedA)).toBe(fingerprintSecuritySeed(seedA)); + expect(fingerprintSecuritySeed(seedA)).not.toBe(fingerprintSecuritySeed(seedB)); + }); + + it("redacts the Seed from the machine-readable document snapshot", () => { + const document = requireSecuritySeedDocument({ + _id: SECURITY_SEED_DOCUMENT_ID, + _rev: "0-1", + type: "syncinfo", + protocolVersion: 2, + pbkdf2salt: seedA, + }); + + const snapshot = snapshotSecuritySeedDocument(document); + + expect(snapshot).toEqual({ + id: SECURITY_SEED_DOCUMENT_ID, + revision: "0-1", + fingerprint: fingerprintSecuritySeed(seedA), + fields: { + type: "syncinfo", + protocolVersion: 2, + }, + }); + expect(JSON.stringify(snapshot)).not.toContain(seedA); + }); + + it("replaces only the Seed and identifies later synchronisation-parameter changes", () => { + const before = requireSecuritySeedDocument({ + _id: SECURITY_SEED_DOCUMENT_ID, + _rev: "0-1", + type: "syncinfo", + protocolVersion: 2, + pbkdf2salt: seedA, + }); + const replaced = replaceSecuritySeed(before, seedB); + const laterRevision = { + ...replaced, + _rev: "0-3", + }; + + expect(before.pbkdf2salt).toBe(seedA); + expect(replaced.pbkdf2salt).toBe(seedB); + expect(changedSynchronisationParameterFields(before, replaced)).toEqual(["pbkdf2salt"]); + expect(changedSynchronisationParameterFields(replaced, laterRevision)).toEqual([]); + }); +}); diff --git a/test/e2e-obsidian/runner/securitySeed.ts b/test/e2e-obsidian/runner/securitySeed.ts new file mode 100644 index 00000000..6892f424 --- /dev/null +++ b/test/e2e-obsidian/runner/securitySeed.ts @@ -0,0 +1,88 @@ +/** + * Supplies the runner-owned CouchDB fixture operations for the Security Seed + * reconnect scenario. Production code remains responsible for fetching, + * caching, and applying the Seed; this helper only validates the managed + * synchronisation-parameter document, replaces the one intended field, and + * compares document snapshots. + * + * Callers expose only short SHA-256 fingerprints. The original and replacement + * Seed values must stay inside the isolated test process and must not be + * written to diagnostics, screenshots, or machine-readable results. + */ +import { createHash, randomBytes } from "node:crypto"; +import type { CouchDbDocument } from "./couchdb.ts"; + +export const SECURITY_SEED_DOCUMENT_ID = "_local/obsidian_livesync_sync_parameters"; + +export type SecuritySeedDocument = CouchDbDocument & { + _id: typeof SECURITY_SEED_DOCUMENT_ID; + _rev: string; + pbkdf2salt: string; +}; + +export type SecuritySeedDocumentSnapshot = { + id: string; + revision: string; + fingerprint: string; + fields: Record; +}; + +function decodeSecuritySeed(seed: string): Buffer { + const bytes = Buffer.from(seed, "base64"); + if (seed.length === 0 || bytes.length === 0) { + throw new Error("The Security Seed is empty or is not valid base64."); + } + return bytes; +} + +export function createSecuritySeed(): string { + return randomBytes(32).toString("base64"); +} + +export function fingerprintSecuritySeed(seed: string): string { + const bytes = Uint8Array.from(decodeSecuritySeed(seed)); + return `sha256:${createHash("sha256").update(bytes).digest("hex").slice(0, 16)}`; +} + +export function requireSecuritySeedDocument(document: CouchDbDocument): SecuritySeedDocument { + if (document._id !== SECURITY_SEED_DOCUMENT_ID) { + throw new Error(`Unexpected synchronisation-parameter document: ${document._id}`); + } + if (typeof document._rev !== "string" || document._rev.length === 0) { + throw new Error("The synchronisation-parameter document does not have a revision."); + } + if (typeof document.pbkdf2salt !== "string") { + throw new Error("The synchronisation-parameter document does not have a Security Seed."); + } + decodeSecuritySeed(document.pbkdf2salt); + return document as SecuritySeedDocument; +} + +export function replaceSecuritySeed(document: SecuritySeedDocument, replacementSeed: string): SecuritySeedDocument { + decodeSecuritySeed(replacementSeed); + return { + ...document, + pbkdf2salt: replacementSeed, + }; +} + +export function snapshotSecuritySeedDocument(document: SecuritySeedDocument): SecuritySeedDocumentSnapshot { + const { _id, _rev, pbkdf2salt, ...fields } = document; + return { + id: _id, + revision: _rev, + fingerprint: fingerprintSecuritySeed(pbkdf2salt), + fields, + }; +} + +export function changedSynchronisationParameterFields( + before: SecuritySeedDocument, + after: SecuritySeedDocument +): string[] { + const ignoredFields = new Set(["_rev"]); + return [...new Set([...Object.keys(before), ...Object.keys(after)])] + .filter((key) => !ignoredFields.has(key)) + .filter((key) => JSON.stringify(before[key]) !== JSON.stringify(after[key])) + .sort(); +} diff --git a/test/e2e-obsidian/runner/session.test.ts b/test/e2e-obsidian/runner/session.test.ts new file mode 100644 index 00000000..8977cb96 --- /dev/null +++ b/test/e2e-obsidian/runner/session.test.ts @@ -0,0 +1,87 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { startObsidianPluginSession } from "@vrtmrz/obsidian-test-session"; +import { + startObsidianLiveSyncSession, + type StartObsidianLiveSyncSessionOptions, +} from "./session.ts"; + +vi.mock("@vrtmrz/obsidian-test-session", () => ({ + startObsidianPluginSession: vi.fn(async () => ({ + app: {}, + cliEnv: {}, + install: {}, + readiness: {}, + pluginId: "obsidian-livesync", + remoteDebuggingPort: 28052, + })), +})); + +describe("LiveSync real-Obsidian session", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("installs an explicitly selected plug-in artefact while retaining the supplied Vault and profile", async () => { + const vault = { + path: "/tmp/upgrade-vault", + statePath: "/tmp/upgrade-state", + name: "upgrade-vault", + id: "upgrade-vault-id", + homePath: "/tmp/upgrade-state/home", + xdgConfigPath: "/tmp/upgrade-state/xdg-config", + xdgCachePath: "/tmp/upgrade-state/xdg-cache", + xdgDataPath: "/tmp/upgrade-state/xdg-data", + userDataPath: "/tmp/upgrade-state/user-data", + processMarker: "/tmp/upgrade-state", + dispose: vi.fn(async () => undefined), + }; + const options: StartObsidianLiveSyncSessionOptions & { artifactRoot: string } = { + binary: "/Applications/Obsidian", + cliBinary: "obsidian-cli", + vault, + artifactRoot: "/tmp/obsidian-livesync-0.25.83", + }; + + await startObsidianLiveSyncSession(options); + + expect(startObsidianPluginSession).toHaveBeenCalledWith( + expect.objectContaining({ + artifactRoot: options.artifactRoot, + pluginId: "obsidian-livesync", + vault, + }) + ); + }); + + it("forwards instance-scoped lifecycle hooks and the selected plug-in start mode", async () => { + const beforePluginStart = vi.fn(async () => undefined); + const vault = { + path: "/tmp/mobile-vault", + statePath: "/tmp/mobile-state", + name: "mobile-vault", + id: "mobile-vault-id", + homePath: "/tmp/mobile-state/home", + xdgConfigPath: "/tmp/mobile-state/xdg-config", + xdgCachePath: "/tmp/mobile-state/xdg-cache", + xdgDataPath: "/tmp/mobile-state/xdg-data", + userDataPath: "/tmp/mobile-state/user-data", + processMarker: "/tmp/mobile-state", + dispose: vi.fn(async () => undefined), + }; + + await startObsidianLiveSyncSession({ + binary: "/Applications/Obsidian", + cliBinary: "obsidian-cli", + vault, + pluginStartup: "controlled", + lifecycle: { beforePluginStart }, + }); + + expect(startObsidianPluginSession).toHaveBeenCalledWith( + expect.objectContaining({ + lifecycle: { beforePluginStart }, + pluginStartup: "controlled", + }) + ); + }); +}); diff --git a/test/e2e-obsidian/runner/session.ts b/test/e2e-obsidian/runner/session.ts index ede769da..8347f5e3 100644 --- a/test/e2e-obsidian/runner/session.ts +++ b/test/e2e-obsidian/runner/session.ts @@ -1,4 +1,9 @@ -import { startObsidianPluginSession, type ObsidianPluginSession } from "@vrtmrz/obsidian-test-session"; +import { + startObsidianPluginSession, + type ObsidianPluginSession, + type ObsidianPluginSessionLifecycle, + type ObsidianPluginStartupMode, +} from "@vrtmrz/obsidian-test-session"; import type { TemporaryVault } from "./vault.ts"; export type ObsidianLiveSyncSession = ObsidianPluginSession; @@ -7,7 +12,13 @@ export type StartObsidianLiveSyncSessionOptions = { binary: string; cliBinary: string; vault: TemporaryVault; + artifactRoot?: string; startupGraceMs?: number; + pluginData?: Record; + localStorageEntries?: Readonly>; + pluginStartup?: ObsidianPluginStartupMode; + lifecycle?: ObsidianPluginSessionLifecycle; + env?: NodeJS.ProcessEnv; }; export async function startObsidianLiveSyncSession( @@ -18,7 +29,12 @@ export async function startObsidianLiveSyncSession( cliBinary: options.cliBinary, vault: options.vault, pluginId: "obsidian-livesync", - artifactRoot: process.cwd(), + artifactRoot: options.artifactRoot ?? process.cwd(), startupGraceMs: options.startupGraceMs, + pluginData: options.pluginData, + localStorageEntries: options.localStorageEntries, + pluginStartup: options.pluginStartup, + lifecycle: options.lifecycle, + env: options.env, }); } diff --git a/test/e2e-obsidian/runner/setupUri.ts b/test/e2e-obsidian/runner/setupUri.ts new file mode 100644 index 00000000..0c1697be --- /dev/null +++ b/test/e2e-obsidian/runner/setupUri.ts @@ -0,0 +1,439 @@ +import type { Locator, Page } from "playwright"; +import { evalObsidianJson } from "./cli.ts"; +import { captureObsidianDialogue, captureObsidianElement, withObsidianPage } from "./ui.ts"; + +export type SetupArtifact = { + setupURI: string; + setupPassphrase: string; +}; + +export type SetupState = { + configured: boolean; + databaseReady: boolean; + appReady: boolean; + suspended: boolean; + remoteType: string; + activeConfigurationId: string; + remoteConfigurationCount: number; + endpoint: string; + bucket: string; + bucketPrefix: string; + p2pEnabled: boolean; + p2pRelays: string; + p2pRoomId: string; +}; + +export type SetupCaptureNames = { + scenario: string; + guide: string; +}; + +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000); +const initialisationTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_INITIALISATION_TIMEOUT_MS ?? 120000); + +export function modalByTitle(page: Page, title: string): Locator { + return page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: title }), + }); +} + +export async function captureGuideDialogue(port: number, filename: string, title: string): Promise { + return await captureObsidianElement(port, filename, (page) => modalByTitle(page, title).locator(".modal").first()); +} + +export async function assertVerticalActionLayout(port: number, title: string): Promise { + await withObsidianPage(port, async (page) => { + const actions = modalByTitle(page, title).locator(".vpk-action-dialog__actions").first(); + await actions.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection); + if (flexDirection !== "column") { + throw new Error(`Expected vertically stacked actions in '${title}', received '${flexDirection}'.`); + } + }); +} + +export async function selectRadioOption(modal: Locator, title: string): Promise { + const radio = modal.locator("label").filter({ hasText: title }).locator('input[type="radio"]').first(); + await radio.check({ timeout: uiTimeoutMs }); +} + +export async function selectCheckbox(modal: Locator, title: string): Promise { + const checkbox = modal.locator("label").filter({ hasText: title }).locator('input[type="checkbox"]').first(); + await checkbox.check({ timeout: uiTimeoutMs }); +} + +export async function enterSetupURI( + port: number, + mode: "new" | "existing", + artifact: SetupArtifact, + captures: SetupCaptureNames +): Promise { + await withObsidianPage(port, async (page) => { + const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" }); + await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs }); + + const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync"); + await intro.waitFor({ state: "visible", timeout: uiTimeoutMs }); + if (mode === "new") { + await selectRadioOption(intro, "I am setting this up for the first time"); + await intro + .getByRole("button", { name: "Yes, I want to set up a new synchronisation" }) + .click({ timeout: uiTimeoutMs }); + } else { + await selectRadioOption(intro, "I am adding a device to an existing synchronisation setup"); + await intro + .getByRole("button", { name: "Yes, I want to add this device to my existing synchronisation" }) + .click({ timeout: uiTimeoutMs }); + } + + const method = modalByTitle(page, mode === "new" ? "Connection Method" : "Device Setup Method"); + await method.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await selectRadioOption(method, "Use a Setup URI (Recommended)"); + await method.getByRole("button", { name: "Proceed with Setup URI" }).click({ timeout: uiTimeoutMs }); + + const setup = modalByTitle(page, "Enter Setup URI"); + await setup.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await setup.locator('input[placeholder^="obsidian://setuplivesync"]').fill(artifact.setupURI); + await setup.locator('input[name="password"]').fill(artifact.setupPassphrase); + }); + const screenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-${mode === "new" ? "first" : "second"}-setup-uri.png`, + "Enter Setup URI" + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, "Enter Setup URI") + .getByRole("button", { name: "Test Settings and Continue" }) + .click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +export async function generateSetupURIFromDevice( + port: number, + setupPassphrase: string, + captures: SetupCaptureNames +): Promise<{ artifact: SetupArtifact; screenshots: string[] }> { + const opened = await withObsidianPage(port, async (page) => { + return await page.evaluate( + (commandId) => + ( + globalThis as typeof globalThis & { + app?: { commands?: { executeCommandById(id: string): boolean } }; + } + ).app?.commands?.executeCommandById(commandId) === true, + "obsidian-livesync:livesync-copysetupuri" + ); + }); + if (!opened) throw new Error("The command for generating a Setup URI was not registered."); + + const promptTitle = "Encrypt your settings"; + await withObsidianPage(port, async (page) => { + const prompt = modalByTitle(page, promptTitle); + await prompt.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await prompt.locator('input[type="password"]').fill(setupPassphrase); + }); + const promptScreenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-copy-setup-uri-passphrase.png`, + promptTitle + ); + await withObsidianPage(port, async (page) => { + const prompt = modalByTitle(page, promptTitle); + await prompt.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs }); + await prompt.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const resultTitle = "Your Setup URI is ready to be copied"; + const setupURI = await withObsidianPage(port, async (page) => { + const result = modalByTitle(page, resultTitle); + await result.waitFor({ state: "visible", timeout: uiTimeoutMs }); + return await result.locator("textarea[readonly]").inputValue(); + }); + if (!setupURI.startsWith("obsidian://setuplivesync?settings=")) { + throw new Error("The first device did not generate a valid Setup URI."); + } + const resultScreenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-copy-setup-uri-result.png`, + resultTitle + ); + await withObsidianPage(port, async (page) => { + const result = modalByTitle(page, resultTitle); + await result.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs }); + await result.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + return { + artifact: { setupURI, setupPassphrase }, + screenshots: [promptScreenshot, resultScreenshot], + }; +} + +export async function captureAndStartInitialisation( + port: number, + mode: "new" | "existing", + captures: SetupCaptureNames +): Promise { + const p2pFirstDevice = mode === "new" && captures.guide === "p2p-setup"; + const p2pAdditionalDevice = mode === "existing" && captures.guide === "p2p-setup"; + const title = p2pFirstDevice + ? "Setup Complete: Preparing This P2P Device" + : p2pAdditionalDevice + ? "Setup Complete: Preparing to Fetch from Another Device" + : mode === "new" + ? "Setup Complete: Preparing to Initialise Server" + : "Setup Complete: Preparing to Fetch Synchronisation Data"; + const button = p2pFirstDevice + ? "Restart and Prepare This Device" + : p2pAdditionalDevice + ? "Restart and Select Source Device" + : mode === "new" + ? "Restart and Initialise Server" + : "Restart and Fetch Data"; + if (p2pAdditionalDevice) { + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, title); + await modal + .getByText("After restarting, select an online source device for the initial Fetch.", { + exact: false, + }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + if ((await modal.getByText("downloaded from the server", { exact: false }).count()) !== 0) { + throw new Error("P2P additional-device setup still describes the initial Fetch as a server download."); + } + }); + } + const screenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-${mode === "new" ? "first-initialise" : "second-fetch"}.png`, + title + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, title).getByRole("button", { name: button }).click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +export async function confirmRebuild(port: number, captures: SetupCaptureNames): Promise { + const isP2P = captures.guide === "p2p-setup"; + const title = isP2P + ? "Final Confirmation: Prepare This Device for P2P" + : "Final Confirmation: Overwrite Server Data with This Device's Files"; + const screenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-first-rebuild-confirmation.png`, + title + ); + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, title); + if (isP2P) { + await selectCheckbox( + modal, + "I understand that this resets only this device's local synchronisation database." + ); + await selectRadioOption(modal, "I understand the risks and will proceed without a backup."); + await modal + .getByRole("button", { name: "I Understand, Prepare This Device" }) + .click({ timeout: uiTimeoutMs }); + return; + } + await selectCheckbox( + modal, + "I understand that all changes made on other smartphones or computers possibly could be lost." + ); + await selectCheckbox( + modal, + "I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information." + ); + await selectCheckbox(modal, "I understand that this action is irreversible once performed."); + await selectRadioOption(modal, "I understand the risks and will proceed without a backup."); + await modal.getByRole("button", { name: "I Understand, Overwrite Server" }).click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +export async function skipMissingRemoteConfiguration(port: number, captures: SetupCaptureNames): Promise { + const title = "Fetch Remote Configuration Failed"; + const screenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-missing-remote-configuration.png`, + title + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, title) + .getByRole("button", { name: "Skip and proceed" }) + .click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +export async function acknowledgeDisabledOptionalFeatures(port: number, captures: SetupCaptureNames): Promise { + const title = "All optional features are disabled"; + const screenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-optional-features-disabled.png`, + title + ); + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, title); + await modal.getByRole("button", { name: "OK" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return screenshot; +} + +export async function confirmFastFetch(port: number, captures: SetupCaptureNames): Promise { + const firstTitle = "Data retrieval scheduled"; + await assertVerticalActionLayout(port, firstTitle); + const firstScreenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-retrieval-method.png`, + firstTitle + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, firstTitle) + .getByRole("button", { name: "Overwrite all with remote files" }) + .click({ timeout: uiTimeoutMs }); + }); + + const secondTitle = "How to handle extra existing local files?"; + await assertVerticalActionLayout(port, secondTitle); + const secondScreenshot = await captureGuideDialogue( + port, + `guide-${captures.guide}-local-file-policy.png`, + secondTitle + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, secondTitle) + .getByRole("button", { name: "Keep local files even if not on remote" }) + .click({ timeout: uiTimeoutMs }); + }); + return [firstScreenshot, secondScreenshot]; +} + +function isConfiguredSetupReady(state: SetupState): boolean { + return ( + state.configured && + state.databaseReady && + state.appReady && + !state.suspended && + state.activeConfigurationId !== "" && + state.remoteConfigurationCount === 1 + ); +} + +export async function readSetupState(cliBinary: string, environment: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "configured:settings.isConfigured===true,", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "suspended:core.services.appLifecycle.isSuspended(),", + "remoteType:settings.remoteType||'',", + "activeConfigurationId:settings.activeConfigurationId||'',", + "remoteConfigurationCount:Object.keys(settings.remoteConfigurations||{}).length,", + "endpoint:settings.endpoint||'',", + "bucket:settings.bucket||'',", + "bucketPrefix:settings.bucketPrefix||'',", + "p2pEnabled:settings.P2P_Enabled===true,", + "p2pRelays:settings.P2P_relays||'',", + "p2pRoomId:settings.P2P_roomID||'',", + "});", + "})()", + ].join(""), + environment + ); +} + +export async function waitForConfiguredSetup( + cliBinary: string, + environment: NodeJS.ProcessEnv, + timeoutMs = initialisationTimeoutMs +): Promise { + const deadline = Date.now() + timeoutMs; + let lastState: SetupState | undefined; + let lastError: unknown; + while (Date.now() < deadline) { + try { + lastState = await readSetupState(cliBinary, environment); + if (isConfiguredSetupReady(lastState)) return lastState; + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error( + `Timed out waiting for configured Setup URI state: ${JSON.stringify(lastState)}${ + lastError instanceof Error ? `; last error: ${lastError.message}` : "" + }` + ); +} + +export async function finishInitialisation( + port: number, + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + const message = "Do you want to resume file and database processing, and restart obsidian now?"; + const deadline = Date.now() + initialisationTimeoutMs; + let readySince: number | undefined; + while (Date.now() < deadline) { + const resumeVisible = await withObsidianPage(port, async (page) => { + return await modalByTitle(page, "Confirmation").filter({ hasText: message }).isVisible(); + }).catch(() => false); + if (resumeVisible) { + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, "Confirmation").filter({ hasText: message }); + await modal.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return await waitForConfiguredSetup(cliBinary, environment); + } + try { + const state = await readSetupState(cliBinary, environment); + if (isConfiguredSetupReady(state)) { + readySince ??= Date.now(); + if (Date.now() - readySince >= 1000) return state; + } else { + readySince = undefined; + } + } catch { + // Obsidian may be reloading while the scheduled operation runs. + readySince = undefined; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error("Timed out waiting for Setup URI initialisation to finish."); +} + +export async function resumeCompatibilityReviewIfShown(port: number): Promise { + const title = "Synchronisation paused for compatibility review"; + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000); + let available = false; + while (Date.now() < deadline && !available) { + available = await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, title); + if (await modal.isVisible()) return true; + const reminder = page.locator(".notice.livesync-compatibility-review-notice"); + if (!(await reminder.isVisible())) return false; + await reminder.getByRole("link", { name: "Review why" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + return true; + }).catch(() => false); + if (!available) await new Promise((resolve) => setTimeout(resolve, 200)); + } + if (!available) return false; + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, title); + await modal.getByRole("button", { name: "Resume synchronisation" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return true; +} diff --git a/test/e2e-obsidian/runner/twoVaultSyncLifecycle.test.ts b/test/e2e-obsidian/runner/twoVaultSyncLifecycle.test.ts new file mode 100644 index 00000000..7ee47ffd --- /dev/null +++ b/test/e2e-obsidian/runner/twoVaultSyncLifecycle.test.ts @@ -0,0 +1,111 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const state = vi.hoisted(() => ({ + events: [] as string[], + sessions: [] as Array<{ app: { stop: ReturnType } }>, + vaultCount: 0, +})); + +vi.mock("./cli.ts", () => ({ + evalObsidianJson: vi.fn(async () => ({ ok: true })), +})); + +vi.mock("./couchdb.ts", () => ({ + assertCouchDbReachable: vi.fn(async () => undefined), + createCouchDbDatabase: vi.fn(async () => undefined), + deleteCouchDbDatabase: vi.fn(async () => undefined), + loadCouchDbConfig: vi.fn(async () => ({ + uri: "http://localhost:5984", + username: "admin", + password: "password", + dbPrefix: "e2e", + })), + makeUniqueDatabaseName: vi.fn((_prefix: string, suffix: string) => suffix), + waitForCouchDbDocs: vi.fn(async () => undefined), +})); + +vi.mock("./environment.ts", () => ({ + discoverObsidianCli: vi.fn(() => ({ binary: "obsidian-cli", checked: [] })), + requireObsidianBinary: vi.fn(() => "Obsidian"), +})); + +vi.mock("./pathAssertions.ts", () => ({ + waitForExactCaseOnlyRename: vi.fn(async () => undefined), +})); + +vi.mock("./liveSyncWorkflow.ts", () => ({ + assertEqual: vi.fn(), + assertE2eCompatibilityMarker: vi.fn(async () => undefined), + assertE2eCompatibilityReviewPending: vi.fn(async () => undefined), + configureCouchDb: vi.fn(async () => undefined), + createE2eCouchDbPluginData: vi.fn(() => ({})), + prepareRemote: vi.fn(async () => undefined), + pushLocalChanges: vi.fn(async () => { + throw new Error("simulated Obsidian CLI timeout"); + }), + resumeCompatibilityReview: vi.fn(async () => undefined), + waitForLiveSyncCoreReady: vi.fn(async () => undefined), + waitForLocalDatabaseEntry: vi.fn(async () => ({ id: "note-id", children: [] })), +})); + +vi.mock("./session.ts", () => ({ + startObsidianLiveSyncSession: vi.fn(async () => { + const session = { + app: { + stop: vi.fn(async () => { + state.events.push("session:stop"); + }), + }, + cliEnv: {}, + remoteDebuggingPort: 28052, + }; + state.sessions.push(session); + return session; + }), +})); + +vi.mock("./vault.ts", () => ({ + createTemporaryVault: vi.fn(async () => { + state.vaultCount += 1; + const name = `vault-${state.vaultCount}`; + return { + name, + path: `/tmp/${name}`, + dispose: vi.fn(async () => { + state.events.push(`${name}:dispose`); + }), + }; + }), +})); + +describe("two-vault runner lifecycle", () => { + beforeEach(() => { + vi.resetModules(); + state.events.length = 0; + state.sessions.length = 0; + state.vaultCount = 0; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("stops the active Obsidian session before disposing temporary Vaults when synchronisation fails", async () => { + let resolveExit!: (code: number) => void; + const exitCode = new Promise((resolve) => { + resolveExit = resolve; + }); + vi.spyOn(console, "error").mockImplementation(() => undefined); + vi.spyOn(process, "exit").mockImplementation((code) => { + resolveExit(Number(code)); + return undefined as never; + }); + + await import("../scripts/two-vault-sync.ts"); + + expect(await exitCode).toBe(1); + expect(state.sessions).toHaveLength(1); + expect(state.sessions[0].app.stop).toHaveBeenCalledOnce(); + expect(state.events.indexOf("session:stop")).toBeLessThan(state.events.indexOf("vault-1:dispose")); + }); +}); diff --git a/test/e2e-obsidian/runner/ui.ts b/test/e2e-obsidian/runner/ui.ts index 8c4f36d5..27efa226 100644 --- a/test/e2e-obsidian/runner/ui.ts +++ b/test/e2e-obsidian/runner/ui.ts @@ -1,4 +1,7 @@ +import { mkdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; import { withObsidianPage } from "@vrtmrz/obsidian-test-session"; +import type { Locator, Page } from "playwright"; export { obsidianRemoteDebuggingPort, @@ -7,6 +10,78 @@ export { withObsidianPage, } from "@vrtmrz/obsidian-test-session"; +export async function captureObsidianPage( + port: number, + filename: string, + assertReady: (page: Page) => Promise +): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + const screenshotPath = join(outputDirectory, filename); + await mkdir(dirname(screenshotPath), { recursive: true }); + + await withObsidianPage(port, async (page) => { + try { + await assertReady(page); + } catch (error) { + const failurePath = screenshotPath.replace(/\.png$/u, ".failure.png"); + await page.screenshot({ path: failurePath, fullPage: true }); + console.error(`UI failure screenshot: ${failurePath}`); + throw error; + } + await page.screenshot({ path: screenshotPath, fullPage: true }); + }); + + return screenshotPath; +} + +export async function captureObsidianDialogue( + port: number, + filename: string, + assertReady: (page: Page) => Promise +): Promise { + return await captureObsidianPage(port, filename, assertReady); +} + +export async function captureObsidianElement( + port: number, + filename: string, + resolveElement: (page: Page) => Locator | Promise +): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + const screenshotPath = join(outputDirectory, filename); + await mkdir(dirname(screenshotPath), { recursive: true }); + + await withObsidianPage(port, async (page) => { + try { + const element = await resolveElement(page); + await element.waitFor({ state: "visible", timeout: 10000 }); + await element.screenshot({ + path: screenshotPath, + animations: "disabled", + style: ".notice-container { visibility: hidden !important; }", + }); + } catch (error) { + const failurePath = screenshotPath.replace(/\.png$/u, ".failure.png"); + await page.screenshot({ path: failurePath, fullPage: true }); + console.error(`UI element failure screenshot: ${failurePath}`); + throw error; + } + }); + + return screenshotPath; +} + +export async function captureJsonResolveDialogue(port: number): Promise { + return await captureObsidianDialogue(port, "hidden-file-json-resolve-dialogue.png", async (page) => { + const optionAB = page.locator('label:has(input[name="disp"][value="AB"])'); + const optionBA = page.locator('label:has(input[name="disp"][value="BA"])'); + const applyButton = page.getByRole("button", { name: "Apply" }); + await optionAB.waitFor({ state: "visible", timeout: 10000 }); + await optionBA.waitFor({ state: "visible", timeout: 10000 }); + await applyButton.waitFor({ state: "visible", timeout: 10000 }); + }); +} + export async function clickJsonResolveOption(port: number, mode: "AB" | "BA"): Promise { await withObsidianPage(port, async (page) => { const option = page.locator(`label:has(input[name="disp"][value="${mode}"])`); diff --git a/test/e2e-obsidian/runner/upgradeContinuity.test.ts b/test/e2e-obsidian/runner/upgradeContinuity.test.ts new file mode 100644 index 00000000..505cecc8 --- /dev/null +++ b/test/e2e-obsidian/runner/upgradeContinuity.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { + assertCouchDbCheckpointContinuity, + assertJournalCheckpointLoaded, + assertNoJournalReplay, + type JournalCheckpointSnapshot, +} from "./upgradeContinuity.ts"; + +const journalCheckpoint: JournalCheckpointSnapshot = { + remoteKey: "remote-a", + lastLocalSeq: 42, + journalEpoch: "2:salt", + knownIDs: ["known-a"], + sentIDs: ["sent-a"], + receivedFiles: ["100-docs.jsonl.gz"], + sentFiles: ["101-docs.jsonl.gz"], +}; + +describe("upgrade synchronisation continuity assertions", () => { + it("rejects a fresh CouchDB checkpoint lineage even when final documents could still converge", () => { + expect(() => + assertCouchDbCheckpointContinuity( + [{ id: "_local/original", lastSequence: 42 }], + [{ id: "_local/replacement", lastSequence: 42 }] + ) + ).toThrow("checkpoint identity changed"); + }); + + it("rejects an Object Storage checkpoint which was reset to its initial state", () => { + expect(() => + assertJournalCheckpointLoaded(journalCheckpoint, { + remoteKey: journalCheckpoint.remoteKey, + lastLocalSeq: 0, + journalEpoch: "", + knownIDs: [], + sentIDs: [], + receivedFiles: [], + sentFiles: [], + }) + ).toThrow(/lastLocalSeq regressed|history was lost/u); + }); + + it("rejects hidden Object Storage replay during an otherwise unchanged sync", () => { + expect(() => + assertNoJournalReplay( + journalCheckpoint, + journalCheckpoint, + [{ key: "101-docs.jsonl.gz", size: 10, etag: "etag" }], + [{ key: "101-docs.jsonl.gz", size: 10, etag: "etag" }], + { downloadedJournalKeys: ["101-docs.jsonl.gz"], uploadedJournalKeys: [] } + ) + ).toThrow("downloaded previously processed journals"); + }); +}); diff --git a/test/e2e-obsidian/runner/upgradeContinuity.ts b/test/e2e-obsidian/runner/upgradeContinuity.ts new file mode 100644 index 00000000..b5426265 --- /dev/null +++ b/test/e2e-obsidian/runner/upgradeContinuity.ts @@ -0,0 +1,199 @@ +export type CouchDbCheckpointSnapshot = { + id: string; + lastSequence: unknown; +}; + +export type CouchDbDocumentRevision = { + id: string; + revision: string; + deleted: boolean; +}; + +export type JournalCheckpointSnapshot = { + remoteKey: string; + lastLocalSeq: number | string; + journalEpoch: string; + knownIDs: readonly string[]; + sentIDs: readonly string[]; + receivedFiles: readonly string[]; + sentFiles: readonly string[]; +}; + +export type JournalIoObservation = { + downloadedJournalKeys: readonly string[]; + uploadedJournalKeys: readonly string[]; +}; + +export type RemoteObjectSnapshot = { + key: string; + size: number; + etag: string; +}; + +export type MilestoneIdentity = { + created: unknown; + locked: boolean; + acceptedNodes: readonly string[]; +}; + +function sorted(values: readonly string[]): string[] { + return [...values].sort((left, right) => left.localeCompare(right)); +} + +function assertEqualStrings(actual: readonly string[], expected: readonly string[], message: string): void { + const actualSorted = sorted(actual); + const expectedSorted = sorted(expected); + if (JSON.stringify(actualSorted) !== JSON.stringify(expectedSorted)) { + throw new Error(`${message}\nExpected: ${JSON.stringify(expectedSorted)}\nActual: ${JSON.stringify(actualSorted)}`); + } +} + +function assertSubset(previous: readonly string[], current: readonly string[], message: string): void { + const currentSet = new Set(current); + const missing = previous.filter((value) => !currentSet.has(value)); + if (missing.length > 0) throw new Error(`${message}: ${missing.join(", ")}`); +} + +function sequenceNumber(sequence: unknown): number | undefined { + if (typeof sequence === "number" && Number.isFinite(sequence)) return sequence; + if (typeof sequence !== "string") return undefined; + const match = /^(\d+)/u.exec(sequence); + return match ? Number(match[1]) : undefined; +} + +function assertSequenceDidNotRegress(before: unknown, after: unknown, label: string): void { + const beforeNumber = sequenceNumber(before); + const afterNumber = sequenceNumber(after); + if (beforeNumber !== undefined && afterNumber !== undefined) { + if (afterNumber < beforeNumber) { + throw new Error(`${label} regressed from ${String(before)} to ${String(after)}.`); + } + return; + } + if (before !== after) { + throw new Error(`${label} changed from an opaque sequence ${String(before)} to ${String(after)}.`); + } +} + +export function assertCouchDbCheckpointContinuity( + before: readonly CouchDbCheckpointSnapshot[], + after: readonly CouchDbCheckpointSnapshot[] +): void { + if (before.length === 0) throw new Error("The stable release did not create a CouchDB replication checkpoint."); + assertEqualStrings( + after.map(({ id }) => id), + before.map(({ id }) => id), + "The CouchDB replication checkpoint identity changed during the upgrade." + ); + const afterById = new Map(after.map((checkpoint) => [checkpoint.id, checkpoint])); + for (const checkpoint of before) { + assertSequenceDidNotRegress( + checkpoint.lastSequence, + afterById.get(checkpoint.id)?.lastSequence, + `CouchDB checkpoint ${checkpoint.id}` + ); + } +} + +export function assertSomeCouchDbCheckpointAdvanced( + before: readonly CouchDbCheckpointSnapshot[], + after: readonly CouchDbCheckpointSnapshot[] +): void { + assertCouchDbCheckpointContinuity(before, after); + const afterById = new Map(after.map((checkpoint) => [checkpoint.id, checkpoint])); + const advanced = before.some((checkpoint) => { + const previous = sequenceNumber(checkpoint.lastSequence); + const current = sequenceNumber(afterById.get(checkpoint.id)?.lastSequence); + return previous !== undefined && current !== undefined && current > previous; + }); + if (!advanced) throw new Error("No CouchDB replication checkpoint advanced after the post-upgrade change."); +} + +export function assertCouchDbDocumentsUnchanged( + before: readonly CouchDbDocumentRevision[], + after: readonly CouchDbDocumentRevision[] +): void { + const serialise = (documents: readonly CouchDbDocumentRevision[]) => + [...documents].sort((left, right) => left.id.localeCompare(right.id)); + if (JSON.stringify(serialise(before)) !== JSON.stringify(serialise(after))) { + throw new Error("A no-op post-upgrade CouchDB synchronisation changed ordinary remote documents."); + } +} + +export function assertJournalCheckpointLoaded( + before: JournalCheckpointSnapshot, + after: JournalCheckpointSnapshot +): void { + if (sequenceNumber(before.lastLocalSeq) === 0) { + throw new Error("The stable release did not advance the Object Storage local checkpoint."); + } + if (after.remoteKey !== before.remoteKey) { + throw new Error(`The Object Storage checkpoint key changed from ${before.remoteKey} to ${after.remoteKey}.`); + } + assertSequenceDidNotRegress(before.lastLocalSeq, after.lastLocalSeq, "Object Storage lastLocalSeq"); + assertSubset(before.knownIDs, after.knownIDs, "Object Storage known revision history was lost"); + assertSubset(before.sentIDs, after.sentIDs, "Object Storage sent revision history was lost"); + assertSubset(before.receivedFiles, after.receivedFiles, "Object Storage received journal history was lost"); + assertSubset(before.sentFiles, after.sentFiles, "Object Storage sent journal history was lost"); + if (before.journalEpoch && after.journalEpoch !== before.journalEpoch) { + throw new Error( + `The Object Storage journal epoch changed from ${before.journalEpoch} to ${after.journalEpoch}.` + ); + } +} + +export function assertNoJournalReplay( + beforeCheckpoint: JournalCheckpointSnapshot, + afterCheckpoint: JournalCheckpointSnapshot, + beforeObjects: readonly RemoteObjectSnapshot[], + afterObjects: readonly RemoteObjectSnapshot[], + observation: JournalIoObservation +): void { + assertJournalCheckpointLoaded(beforeCheckpoint, afterCheckpoint); + assertEqualStrings( + afterObjects.map(({ key }) => key), + beforeObjects.map(({ key }) => key), + "A no-op post-upgrade Object Storage synchronisation changed the journal object set." + ); + if (observation.downloadedJournalKeys.length > 0) { + throw new Error( + `The no-op synchronisation downloaded previously processed journals: ${observation.downloadedJournalKeys.join(", ")}` + ); + } + if (observation.uploadedJournalKeys.length > 0) { + throw new Error( + `The no-op synchronisation uploaded replay journals: ${observation.uploadedJournalKeys.join(", ")}` + ); + } +} + +export function assertJournalCheckpointAdvanced( + before: JournalCheckpointSnapshot, + after: JournalCheckpointSnapshot, + observation: JournalIoObservation +): void { + assertJournalCheckpointLoaded(before, after); + const beforeSequence = sequenceNumber(before.lastLocalSeq); + const afterSequence = sequenceNumber(after.lastLocalSeq); + if (beforeSequence === undefined || afterSequence === undefined || afterSequence <= beforeSequence) { + throw new Error( + `The Object Storage checkpoint did not advance after the post-upgrade change (${String(before.lastLocalSeq)} -> ${String(after.lastLocalSeq)}).` + ); + } + if (observation.uploadedJournalKeys.length === 0) { + throw new Error("The post-upgrade Object Storage change did not create a new journal."); + } +} + +export function assertMilestoneContinuity(before: MilestoneIdentity, after: MilestoneIdentity): void { + if (before.created === undefined || before.created === null) { + throw new Error("The stable release milestone does not expose a remote generation identity."); + } + if (after.created !== before.created) { + throw new Error(`The remote milestone generation changed from ${String(before.created)} to ${String(after.created)}.`); + } + if (after.locked !== before.locked) { + throw new Error(`The remote milestone lock changed from ${String(before.locked)} to ${String(after.locked)}.`); + } + assertSubset(before.acceptedNodes, after.acceptedNodes, "The remote milestone lost an accepted device"); +} diff --git a/test/e2e-obsidian/runner/upgradeWorkflow.test.ts b/test/e2e-obsidian/runner/upgradeWorkflow.test.ts new file mode 100644 index 00000000..efe4d3da --- /dev/null +++ b/test/e2e-obsidian/runner/upgradeWorkflow.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { evalObsidianJson } = vi.hoisted(() => ({ + evalObsidianJson: vi.fn(), +})); + +vi.mock("./cli.ts", () => ({ evalObsidianJson })); + +import { prepareStableRemote } from "./upgradeWorkflow.ts"; + +describe("stable remote preparation", () => { + afterEach(() => { + vi.unstubAllEnvs(); + vi.clearAllMocks(); + }); + + it("waits for a readable Security Seed before marking the remote as resolved", async () => { + vi.stubEnv("E2E_OBSIDIAN_REMOTE_READY_INTERVAL_MS", "0"); + vi.stubEnv("E2E_OBSIDIAN_REMOTE_READY_TIMEOUT_MS", "100"); + evalObsidianJson + .mockResolvedValueOnce({ ok: true }) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true) + .mockResolvedValueOnce({ ok: true }); + + await prepareStableRemote("obsidian-cli", {}); + + expect(evalObsidianJson).toHaveBeenCalledTimes(4); + const scripts = evalObsidianJson.mock.calls.map(([, script]) => String(script)); + expect(scripts[0]).toContain("tryCreateRemoteDatabase"); + expect(scripts[0]).not.toContain("markRemoteResolved"); + expect(scripts[1]).toContain("ensurePBKDF2Salt"); + expect(scripts[2]).toContain("ensurePBKDF2Salt"); + expect(scripts[3]).toContain("markRemoteResolved"); + }); +}); diff --git a/test/e2e-obsidian/runner/upgradeWorkflow.ts b/test/e2e-obsidian/runner/upgradeWorkflow.ts new file mode 100644 index 00000000..76b6c7aa --- /dev/null +++ b/test/e2e-obsidian/runner/upgradeWorkflow.ts @@ -0,0 +1,874 @@ +import { mkdir, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { evalObsidianJson } from "./cli.ts"; +import type { CouchDbConfig } from "./couchdb.ts"; +import type { ObjectStorageConfig } from "./objectStorage.ts"; +import { withObsidianPage } from "./ui.ts"; +import type { + CouchDbCheckpointSnapshot, + JournalCheckpointSnapshot, + JournalIoObservation, +} from "./upgradeContinuity.ts"; +import { waitForLocalDatabaseEntry } from "./liveSyncWorkflow.ts"; +import type { TemporaryVault } from "./vault.ts"; + +export const STABLE_RELEASE_VERSION = "0.25.83"; + +export type UpgradeTransportConfiguration = + | { + kind: "couchdb"; + config: CouchDbConfig; + databaseName: string; + } + | { + kind: "object-storage"; + config: ObjectStorageConfig; + bucketPrefix: string; + }; + +export type UpgradeScenarioPaths = { + original: string; + renamed: string; + deleted: string; + postUpgrade: string; + returnFromVerifier: string; +}; + +export type RuntimeUpgradeState = { + pluginVersion: string; + vaultName: string; + localDatabaseName: string; + localDatabaseUpdateSequence: number | string; + localDatabaseDocumentCount: number; + nodeId: string; + legacyCompatibilityMarker: string | null; + compatibilityMarker: string; + compatibilityStorageEntries: Record; + migrationState?: { + sourceVersion: number; + targetVersion: number; + isNewVault: boolean; + isFromFutureSchema: boolean; + changed: boolean; + requiresSyncReview: boolean; + reviewReasons: Array<{ code: string; fromVersion: number; toVersion: number }>; + }; + settings: { + isConfigured: boolean | undefined; + settingVersion: number; + versionUpFlash: string; + liveSync: boolean; + syncOnStart: boolean; + syncOnSave: boolean; + syncOnEditorSave: boolean; + syncOnFileOpen: boolean; + syncAfterMerge: boolean; + periodicReplication: boolean; + encrypt: boolean; + usePathObfuscation: boolean; + syncInternalFiles: boolean; + customChunkSize: number; + usePluginSyncV2: boolean; + enableCompression: boolean; + useEden: boolean; + filenameCaseType: string; + handleFilenameCaseSensitive?: boolean; + doNotUseFixedRevisionForChunks: boolean; + chunkSplitterVersion: string; + E2EEAlgorithm: string; + additionalSuffixOfDatabaseName: string; + remoteType: string; + couchDB_DBNAME: string; + endpoint: string; + bucket: string; + bucketPrefix: string; + activeConfigurationId: string; + remoteConfigurationIds: string[]; + doctorProcessedVersion: string; + }; +}; + +export type RuntimeSettingsUpgradeState = Pick & { + compatibilityMarker: string; +}; + +export type CouchDbReplicationObservation = { + succeeded: boolean; + sentDocuments: number; + arrivedDocuments: number; +}; + +export type JournalReplicationObservation = JournalIoObservation & { + succeeded: boolean; +}; + +const firstContent = "# Stable release history\n\nCreated before the 1.0 upgrade.\n"; +const editedContent = "# Stable release history\n\nEdited and renamed before the 1.0 upgrade.\n"; +const deletedContent = "# Deleted before upgrade\n\nThis note must not be resurrected.\n"; +const postUpgradeContent = "# Post-upgrade delta\n\nCreated by the upgraded 1.0 device.\n"; +const returnContent = "# Return journey\n\nCreated by a fresh 1.0 verifier device.\n"; + +function assertEqual(actual: unknown, expected: unknown, message: string): void { + if (actual !== expected) { + throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`); + } +} + +function assertStringArraysEqual(actual: readonly string[], expected: readonly string[], message: string): void { + const actualSorted = [...actual].sort(); + const expectedSorted = [...expected].sort(); + if (JSON.stringify(actualSorted) !== JSON.stringify(expectedSorted)) { + throw new Error( + `${message}\nExpected: ${JSON.stringify(expectedSorted)}\nActual: ${JSON.stringify(actualSorted)}` + ); + } +} + +export function createUpgradeScenarioPaths(label: string): UpgradeScenarioPaths { + const root = `E2E/upgrade-from-${STABLE_RELEASE_VERSION}/${label}`; + return { + original: `${root}/rename-source.md`, + renamed: `${root}/renamed.md`, + deleted: `${root}/deleted.md`, + postUpgrade: `${root}/post-upgrade.md`, + returnFromVerifier: `${root}/return-from-verifier.md`, + }; +} + +function remoteSettings(configuration: UpgradeTransportConfiguration): Record { + if (configuration.kind === "couchdb") { + return { + remoteType: "", + couchDB_URI: configuration.config.uri, + couchDB_USER: configuration.config.username, + couchDB_PASSWORD: configuration.config.password, + couchDB_DBNAME: configuration.databaseName, + isConfigured: true, + }; + } + return { + remoteType: "MINIO", + endpoint: configuration.config.endpoint, + accessKey: configuration.config.accessKey, + secretKey: configuration.config.secretKey, + bucket: configuration.config.bucket, + region: configuration.config.region, + forcePathStyle: configuration.config.forcePathStyle, + bucketPrefix: configuration.bucketPrefix, + bucketCustomHeaders: "", + isConfigured: true, + }; +} + +export async function configureStableRelease( + cliBinary: string, + environment: NodeJS.ProcessEnv, + configuration: UpgradeTransportConfiguration +): Promise { + const partial = remoteSettings(configuration); + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + `const partial=${JSON.stringify(partial)};`, + "await core.services.setting.applyExternalSettings(partial,true);", + "await core.services.control.applySettings();", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +export async function prepareStableRemote(cliBinary: string, environment: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "const replicator=core.services.replicator.getActiveReplicator();", + "await replicator.tryCreateRemoteDatabase(settings);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); + + const timeoutMs = Number(process.env.E2E_OBSIDIAN_REMOTE_READY_TIMEOUT_MS ?? 15000); + const intervalMs = Number(process.env.E2E_OBSIDIAN_REMOTE_READY_INTERVAL_MS ?? 250); + const deadline = Date.now() + timeoutMs; + let securitySeedReady = false; + do { + securitySeedReady = await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "const replicator=core.services.replicator.getActiveReplicator();", + "return JSON.stringify(!!(await replicator.ensurePBKDF2Salt(settings,true,false)));", + "})()", + ].join(""), + environment + ); + if (securitySeedReady) break; + if (Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } while (Date.now() < deadline); + if (!securitySeedReady) { + throw new Error(`Timed out waiting for the stable release Security Seed after ${timeoutMs}ms.`); + } + + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "const replicator=core.services.replicator.getActiveReplicator();", + "await replicator.markRemoteResolved(settings);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +export async function waitForPersistentNodeIdentity( + cliBinary: string, + environment: NodeJS.ProcessEnv, + timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000) +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const deadline=Date.now()+${JSON.stringify(timeoutMs)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const database=core.localDatabase.localDatabase;", + "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));", + "let persistent='';let active='';", + "while(Date.now()null);", + "persistent=typeof nodeInfo?.nodeid==='string'?nodeInfo.nodeid:'';", + "active=core.services.replicator.getActiveReplicator()?.nodeid??'';", + "if(persistent!==''&&active===persistent) return JSON.stringify(persistent);", + "await sleep(100);", + "}", + "throw new Error(`Timed out waiting for persistent node identity: persistent=${persistent}, active=${active}`);", + "})()", + ].join(""), + environment + ); +} + +export async function readRuntimeUpgradeState( + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const plugin=app.plugins.plugins['obsidian-livesync'];", + "const core=plugin.core;", + "const setting=core.services.setting;", + "const settings=setting.currentSettings();", + "const vaultName=core.services.vault.getVaultName();", + "const replicator=core.services.replicator.getActiveReplicator();", + "const databaseInfo=await core.localDatabase.localDatabase.info();", + "const nodeInfo=await core.localDatabase.localDatabase.get('_local/obsydian_livesync_nodeinfo').catch(()=>null);", + "const migrationState=setting.getSettingsMigrationState?.();", + "return JSON.stringify({", + "pluginVersion:app.plugins.manifests['obsidian-livesync']?.version??'unknown',", + "vaultName,", + "localDatabaseName:databaseInfo.db_name,", + "localDatabaseUpdateSequence:databaseInfo.update_seq,", + "localDatabaseDocumentCount:databaseInfo.doc_count,", + "nodeId:nodeInfo?.nodeid??replicator?.nodeid??'',", + "legacyCompatibilityMarker:localStorage.getItem(`obsidian-live-sync-ver${vaultName}`),", + "compatibilityMarker:setting.getSmallConfig('database-compatibility-version')??'',", + "compatibilityStorageEntries:Object.fromEntries(Array.from({length:localStorage.length},(_,index)=>localStorage.key(index))", + ".filter((key)=>key!==null)", + ".filter(key=>key.startsWith('obsidian-live-sync-ver')||key.endsWith('-database-compatibility-version'))", + ".map(key=>[key,localStorage.getItem(key)??''])),", + "migrationState,", + "settings:{", + "isConfigured:settings.isConfigured,settingVersion:settings.settingVersion,", + "versionUpFlash:settings.versionUpFlash,", + "liveSync:settings.liveSync,syncOnStart:settings.syncOnStart,syncOnSave:settings.syncOnSave,", + "syncOnEditorSave:settings.syncOnEditorSave,syncOnFileOpen:settings.syncOnFileOpen,", + "syncAfterMerge:settings.syncAfterMerge,periodicReplication:settings.periodicReplication,", + "encrypt:settings.encrypt,usePathObfuscation:settings.usePathObfuscation,", + "syncInternalFiles:settings.syncInternalFiles,customChunkSize:settings.customChunkSize,", + "usePluginSyncV2:settings.usePluginSyncV2,enableCompression:settings.enableCompression,", + "useEden:settings.useEden,filenameCaseType:typeof settings.handleFilenameCaseSensitive,", + "handleFilenameCaseSensitive:settings.handleFilenameCaseSensitive,", + "doNotUseFixedRevisionForChunks:settings.doNotUseFixedRevisionForChunks,", + "chunkSplitterVersion:settings.chunkSplitterVersion,E2EEAlgorithm:settings.E2EEAlgorithm,", + "additionalSuffixOfDatabaseName:settings.additionalSuffixOfDatabaseName??'',", + "remoteType:settings.remoteType,couchDB_DBNAME:settings.couchDB_DBNAME??'',", + "endpoint:settings.endpoint??'',bucket:settings.bucket??'',bucketPrefix:settings.bucketPrefix??'',", + "activeConfigurationId:settings.activeConfigurationId??'',", + "remoteConfigurationIds:Object.keys(settings.remoteConfigurations??{}),", + "doctorProcessedVersion:settings.doctorProcessedVersion??'',", + "}", + "});", + "})()", + ].join(""), + environment + ); +} + +export async function readRuntimeSettingsUpgradeState( + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const plugin=app.plugins.plugins['obsidian-livesync'];", + "const setting=plugin.core.services.setting;", + "const settings=setting.currentSettings();", + "const migrationState=setting.getSettingsMigrationState?.();", + "return JSON.stringify({", + "pluginVersion:app.plugins.manifests['obsidian-livesync']?.version??'unknown',", + "compatibilityMarker:setting.getSmallConfig?.('database-compatibility-version')??'',", + "migrationState,", + "settings:{", + "isConfigured:settings.isConfigured,settingVersion:settings.settingVersion,", + "versionUpFlash:settings.versionUpFlash,", + "liveSync:settings.liveSync,syncOnStart:settings.syncOnStart,syncOnSave:settings.syncOnSave,", + "syncOnEditorSave:settings.syncOnEditorSave,syncOnFileOpen:settings.syncOnFileOpen,", + "syncAfterMerge:settings.syncAfterMerge,periodicReplication:settings.periodicReplication,", + "encrypt:settings.encrypt,usePathObfuscation:settings.usePathObfuscation,", + "syncInternalFiles:settings.syncInternalFiles,customChunkSize:settings.customChunkSize,", + "usePluginSyncV2:settings.usePluginSyncV2,enableCompression:settings.enableCompression,", + "useEden:settings.useEden,filenameCaseType:typeof settings.handleFilenameCaseSensitive,", + "handleFilenameCaseSensitive:settings.handleFilenameCaseSensitive,", + "doNotUseFixedRevisionForChunks:settings.doNotUseFixedRevisionForChunks,", + "chunkSplitterVersion:settings.chunkSplitterVersion,E2EEAlgorithm:settings.E2EEAlgorithm,", + "additionalSuffixOfDatabaseName:settings.additionalSuffixOfDatabaseName??'',", + "remoteType:settings.remoteType,couchDB_DBNAME:settings.couchDB_DBNAME??'',", + "endpoint:settings.endpoint??'',bucket:settings.bucket??'',bucketPrefix:settings.bucketPrefix??'',", + "activeConfigurationId:settings.activeConfigurationId??'',", + "remoteConfigurationIds:Object.keys(settings.remoteConfigurations??{}),", + "doctorProcessedVersion:settings.doctorProcessedVersion??'',", + "}", + "});", + "})()", + ].join(""), + environment + ); +} + +export function assertStableReleaseDefaults(state: RuntimeSettingsUpgradeState, configured: boolean): void { + assertEqual( + state.pluginVersion, + STABLE_RELEASE_VERSION, + "The source session did not load the pinned stable release." + ); + assertEqual(state.settings.isConfigured, configured, "The stable release configuration lifecycle was unexpected."); + assertEqual(state.settings.settingVersion, 10, "The stable release settings schema was not version 10."); + assertEqual(state.settings.liveSync, false, "The stable release LiveSync default changed."); + assertEqual(state.settings.syncOnStart, false, "The stable release sync-on-start default changed."); + assertEqual(state.settings.syncOnSave, false, "The stable release sync-on-save default changed."); + assertEqual(state.settings.syncOnEditorSave, false, "The stable release editor-save default changed."); + assertEqual(state.settings.syncOnFileOpen, false, "The stable release file-open default changed."); + assertEqual(state.settings.syncAfterMerge, false, "The stable release post-merge default changed."); + assertEqual(state.settings.periodicReplication, false, "The stable release periodic default changed."); + assertEqual(state.settings.encrypt, false, "The stable release encryption default changed."); + assertEqual(state.settings.usePathObfuscation, false, "The stable release path-obfuscation default changed."); + assertEqual(state.settings.syncInternalFiles, false, "The stable release Hidden File default changed."); + assertEqual(state.settings.customChunkSize, 0, "The stable release custom chunk default changed."); + assertEqual(state.settings.usePluginSyncV2, false, "The stable release Customisation Sync V2 default changed."); + assertEqual(state.settings.enableCompression, false, "The stable release compression default changed."); + assertEqual(state.settings.useEden, false, "The stable release Eden default changed."); + assertEqual( + state.settings.filenameCaseType, + "undefined", + "The stable release filename-case decision was preselected." + ); + assertEqual( + state.settings.doNotUseFixedRevisionForChunks, + true, + "The stable release fixed-revision compatibility value changed." + ); + assertEqual(state.settings.chunkSplitterVersion, "v3-rabin-karp", "The stable release chunk splitter changed."); + assertEqual(state.settings.E2EEAlgorithm, "v2", "The stable release E2EE algorithm changed."); + assertEqual( + state.settings.remoteConfigurationIds.length, + configured ? 1 : 0, + "The stable release remote-profile count was unexpected." + ); +} + +export function assertUnconfiguredUpgradeReady( + stable: RuntimeSettingsUpgradeState, + upgraded: RuntimeSettingsUpgradeState, + targetVersion: string +): void { + assertStableReleaseDefaults(stable, false); + assertEqual(upgraded.pluginVersion, targetVersion, "The unconfigured Vault did not load the target artefact."); + assertEqual(upgraded.settings.isConfigured, false, "The upgrade changed an unconfigured Vault to configured."); + assertEqual( + upgraded.settings.usePluginSyncV2, + stable.settings.usePluginSyncV2, + "The upgrade applied a new-Vault recommendation to a non-empty legacy store." + ); + assertEqual( + upgraded.settings.handleFilenameCaseSensitive, + false, + "The unconfigured legacy Vault did not retain case-insensitive handling." + ); + assertEqual(upgraded.settings.versionUpFlash, "", "The unconfigured Vault was paused for compatibility review."); + assertEqual( + upgraded.compatibilityMarker, + "", + "The unconfigured Vault acknowledged database compatibility before activation." + ); + if (!upgraded.migrationState) throw new Error("The unconfigured settings migration state was not available."); + assertEqual(upgraded.migrationState.isNewVault, false, "The non-empty legacy store was treated as a new store."); + // The real-session helper deliberately reloads an already enabled plug-in. + // The first target load performs and persists the migration; the observed + // post-reload state can therefore report changed=false. The workflow reads + // data.json after stopping the session to prove the persisted values. + assertEqual( + upgraded.migrationState.requiresSyncReview, + false, + "The unconfigured legacy settings unexpectedly required compatibility review." + ); + assertEqual(upgraded.migrationState.reviewReasons.length, 0, "The unconfigured migration emitted a review reason."); +} + +export function assertUnconfiguredUpgradeRestarted(state: RuntimeSettingsUpgradeState, targetVersion: string): void { + assertEqual(state.pluginVersion, targetVersion, "The unconfigured restart did not load the target artefact."); + assertEqual(state.settings.isConfigured, false, "The unconfigured state was not persisted across restart."); + assertEqual(state.settings.usePluginSyncV2, false, "Restart applied a new-Vault recommendation."); + assertEqual(state.settings.handleFilenameCaseSensitive, false, "Restart lost the case-insensitive policy."); + assertEqual( + state.compatibilityMarker, + "", + "Restart acknowledged database compatibility while the Vault remained unconfigured." + ); + if (!state.migrationState) throw new Error("The restarted settings migration state was not available."); + assertEqual(state.migrationState.changed, false, "The settings migration was not idempotent after restart."); + assertEqual(state.migrationState.requiresSyncReview, false, "Restart introduced a compatibility review."); +} + +export function assertStableRemoteSelection( + state: RuntimeUpgradeState, + configuration: UpgradeTransportConfiguration +): void { + assertEqual(state.settings.isConfigured, true, "The stable release was not marked as configured."); + if (!state.settings.activeConfigurationId) throw new Error("The stable release did not select its remote profile."); + if (!state.settings.remoteConfigurationIds.includes(state.settings.activeConfigurationId)) { + throw new Error("The stable release active remote profile was not persisted."); + } + if (configuration.kind === "couchdb") { + assertEqual(state.settings.remoteType, "", "The stable release did not select CouchDB."); + assertEqual( + state.settings.couchDB_DBNAME, + configuration.databaseName, + "The stable release CouchDB database changed." + ); + } else { + assertEqual(state.settings.remoteType, "MINIO", "The stable release did not select Object Storage."); + assertEqual(state.settings.endpoint, configuration.config.endpoint, "The Object Storage endpoint changed."); + assertEqual(state.settings.bucket, configuration.config.bucket, "The Object Storage bucket changed."); + assertEqual(state.settings.bucketPrefix, configuration.bucketPrefix, "The Object Storage prefix changed."); + } +} + +export function assertUpgradeCompatibilityReady( + stable: RuntimeUpgradeState, + upgraded: RuntimeUpgradeState, + targetVersion: string, + configuration: UpgradeTransportConfiguration +): void { + assertEqual(upgraded.pluginVersion, targetVersion, "The upgraded session did not load the target artefact."); + assertEqual( + upgraded.localDatabaseName, + stable.localDatabaseName, + "The upgrade opened a different local synchronisation database." + ); + if (upgraded.localDatabaseDocumentCount < stable.localDatabaseDocumentCount) { + throw new Error("The upgrade lost local synchronisation documents before its first sync."); + } + if (stable.nodeId.length === 0) { + throw new Error("The stable release did not persist a device node identity."); + } + assertEqual(upgraded.nodeId, stable.nodeId, "The upgrade changed the persistent device node identity."); + assertEqual( + upgraded.settings.additionalSuffixOfDatabaseName, + stable.settings.additionalSuffixOfDatabaseName, + "The upgrade changed the local database suffix." + ); + assertStringArraysEqual( + upgraded.settings.remoteConfigurationIds, + stable.settings.remoteConfigurationIds, + "The upgrade changed the stored remote-profile identities." + ); + assertEqual( + upgraded.settings.activeConfigurationId, + stable.settings.activeConfigurationId, + "The upgrade changed the active remote profile." + ); + assertStableRemoteSelection(upgraded, configuration); + for (const key of [ + "liveSync", + "syncOnStart", + "syncOnSave", + "syncOnEditorSave", + "syncOnFileOpen", + "syncAfterMerge", + "periodicReplication", + "customChunkSize", + "usePluginSyncV2", + "enableCompression", + "useEden", + "doNotUseFixedRevisionForChunks", + ] as const) { + assertEqual(upgraded.settings[key], stable.settings[key], `The upgrade rewrote the stored ${key} preference.`); + } + if (!upgraded.migrationState) throw new Error("The 1.0 settings migration state was not available."); + // The session helper reloads the enabled target after its first load has + // persisted the normalised case value. Runtime settings below and the + // later restart prove that persisted result without depending on whether + // this observation came from the first or second load. + assertEqual( + upgraded.migrationState.requiresSyncReview, + false, + "The legacy case-insensitive setting unexpectedly required compatibility review." + ); + assertEqual(upgraded.migrationState.reviewReasons.length, 0, "The settings migration emitted a spurious review."); + assertEqual(stable.legacyCompatibilityMarker, "12", "The stable release did not persist its legacy marker."); + assertEqual(upgraded.legacyCompatibilityMarker, null, "The upgrade did not retire the legacy marker."); + assertEqual( + upgraded.compatibilityMarker, + "12", + [ + "The upgrade did not migrate the legacy compatibility marker.", + `Vault: ${upgraded.vaultName}`, + `Database suffix: ${upgraded.settings.additionalSuffixOfDatabaseName}`, + `Device-local entries: ${JSON.stringify(upgraded.compatibilityStorageEntries)}`, + ].join("\n") + ); + assertEqual(upgraded.settings.versionUpFlash, "", "Synchronisation was unexpectedly paused after migration."); + assertEqual( + upgraded.settings.handleFilenameCaseSensitive, + false, + "The missing legacy filename-case value did not preserve case-insensitive handling." + ); +} + +export function assertUpgradeRemainsReady(state: RuntimeUpgradeState, targetVersion: string): void { + assertEqual(state.pluginVersion, targetVersion, "The upgraded session changed target artefact."); + assertEqual(state.settings.versionUpFlash, "", "A compatibility pause reappeared."); + assertEqual(state.compatibilityMarker, "12", "The compatibility acknowledgement was not persisted."); + assertEqual( + state.settings.handleFilenameCaseSensitive, + false, + "The migrated legacy case-insensitive policy was not persisted." + ); +} + +export async function dismissConfigDoctorIfShown(port: number): Promise { + const timeoutMs = Number(process.env.E2E_OBSIDIAN_UI_TIMEOUT_MS ?? 10000); + return await withObsidianPage(port, async (page) => { + const doctor = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Self-hosted LiveSync Config Doctor" }), + }); + const visible = await doctor + .waitFor({ state: "visible", timeout: Math.min(timeoutMs, 5000) }) + .then(() => true) + .catch(() => false); + if (!visible) return false; + await doctor.getByRole("button", { name: /No, and do not ask again/u }).click(); + await doctor.waitFor({ state: "hidden", timeout: timeoutMs }); + return true; + }); +} + +async function writeNote( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content); else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +async function renameNote( + cliBinary: string, + environment: NodeJS.ProcessEnv, + fromPath: string, + toPath: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const fromPath=${JSON.stringify(fromPath)};`, + `const toPath=${JSON.stringify(toPath)};`, + "const folder=toPath.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(fromPath);", + "if(!existing) throw new Error(`Could not find note to rename: ${fromPath}`);", + "await app.vault.rename(existing,toPath);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +async function deleteNote(cliBinary: string, environment: NodeJS.ProcessEnv, path: string): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const existing=app.vault.getAbstractFileByPath(path);", + "if(!existing) throw new Error(`Could not find note to delete: ${path}`);", + "await app.vault.delete(existing);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +async function waitForChangedRevision( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string, + previousRevision: string +): Promise { + const timeoutMs = Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000); + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const previousRevision=${JSON.stringify(previousRevision)};`, + `const deadline=Date.now()+${JSON.stringify(timeoutMs)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));", + "while(Date.now()false);", + "if(entry&&entry._rev&&entry._rev!==previousRevision) return JSON.stringify({rev:entry._rev});", + "await sleep(250);", + "}", + "throw new Error(`Timed out waiting for a changed local revision: ${path}`);", + "})()", + ].join(""), + environment + ); +} + +export async function runStableFileHistory( + cliBinary: string, + environment: NodeJS.ProcessEnv, + paths: UpgradeScenarioPaths, + synchronise: () => Promise +): Promise { + await writeNote(cliBinary, environment, paths.original, firstContent); + await writeNote(cliBinary, environment, paths.deleted, deletedContent); + const originalEntry = await waitForLocalDatabaseEntry(cliBinary, environment, paths.original); + await waitForLocalDatabaseEntry(cliBinary, environment, paths.deleted); + await synchronise(); + + await writeNote(cliBinary, environment, paths.original, editedContent); + await waitForChangedRevision(cliBinary, environment, paths.original, originalEntry.rev); + await synchronise(); + + await renameNote(cliBinary, environment, paths.original, paths.renamed); + await waitForLocalDatabaseEntry(cliBinary, environment, paths.renamed); + await synchronise(); + + await deleteNote(cliBinary, environment, paths.deleted); + await synchronise(); +} + +export async function createPostUpgradeDelta( + cliBinary: string, + environment: NodeJS.ProcessEnv, + paths: UpgradeScenarioPaths +): Promise { + await writeNote(cliBinary, environment, paths.postUpgrade, postUpgradeContent); + await waitForLocalDatabaseEntry(cliBinary, environment, paths.postUpgrade); +} + +export async function createVerifierReturnDelta( + cliBinary: string, + environment: NodeJS.ProcessEnv, + paths: UpgradeScenarioPaths +): Promise { + await writeNote(cliBinary, environment, paths.returnFromVerifier, returnContent); + await waitForLocalDatabaseEntry(cliBinary, environment, paths.returnFromVerifier); +} + +async function pathExists(vault: TemporaryVault, path: string): Promise { + try { + await readFile(join(vault.path, path)); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return false; + throw error; + } +} + +async function waitForPathContent(vault: TemporaryVault, path: string, content: string): Promise { + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000); + let lastContent = ""; + while (Date.now() < deadline) { + try { + lastContent = await readFile(join(vault.path, path), "utf8"); + if (lastContent === content) return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +export async function verifyPreUpgradeHistory(vault: TemporaryVault, paths: UpgradeScenarioPaths): Promise { + await waitForPathContent(vault, paths.renamed, editedContent); + if (await pathExists(vault, paths.original)) throw new Error(`Renamed source was resurrected: ${paths.original}`); + if (await pathExists(vault, paths.deleted)) throw new Error(`Deleted note was resurrected: ${paths.deleted}`); +} + +export async function verifyPostUpgradeHistory(vault: TemporaryVault, paths: UpgradeScenarioPaths): Promise { + await verifyPreUpgradeHistory(vault, paths); + await waitForPathContent(vault, paths.postUpgrade, postUpgradeContent); +} + +export async function verifyReturnDelta(vault: TemporaryVault, paths: UpgradeScenarioPaths): Promise { + await waitForPathContent(vault, paths.returnFromVerifier, returnContent); +} + +export async function ensureScenarioDirectory(vault: TemporaryVault, paths: UpgradeScenarioPaths): Promise { + await mkdir(dirname(join(vault.path, paths.original)), { recursive: true }); +} + +export async function runCouchDbReplicationObserved( + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const replicator=core.services.replicator.getActiveReplicator();", + "await core.services.fileProcessing.commitPendingFileEvents();", + "const beforeSent=Number(replicator.docSent??0);", + "const beforeArrived=Number(replicator.docArrived??0);", + "const result=await core.services.replication.replicate(true);", + "return JSON.stringify({", + "succeeded:!!result,", + "sentDocuments:Number(replicator.docSent??0)-beforeSent,", + "arrivedDocuments:Number(replicator.docArrived??0)-beforeArrived,", + "});", + "})()", + ].join(""), + environment + ); +} + +export async function runJournalReplicationObserved( + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const replicator=core.services.replicator.getActiveReplicator();", + "const client=replicator.client;", + "const storage=client.storage;", + "const originalDownload=storage.download.bind(storage);", + "const originalUpload=storage.upload.bind(storage);", + "const downloadedJournalKeys=[];const uploadedJournalKeys=[];", + "const isJournal=(key)=>!String(key).split('/').pop().startsWith('_');", + "storage.download=async(key,...args)=>{if(isJournal(key))downloadedJournalKeys.push(String(key));return await originalDownload(key,...args);};", + "storage.upload=async(key,...args)=>{if(isJournal(key))uploadedJournalKeys.push(String(key));return await originalUpload(key,...args);};", + "let succeeded=false;", + "try{", + "await core.services.fileProcessing.commitPendingFileEvents();", + "succeeded=!!(await core.services.replication.replicate(true));", + "}finally{storage.download=originalDownload;storage.upload=originalUpload;}", + "return JSON.stringify({succeeded,downloadedJournalKeys,uploadedJournalKeys});", + "})()", + ].join(""), + environment + ); +} + +export async function readJournalCheckpoint( + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const replicator=core.services.replicator.getActiveReplicator();", + "const client=replicator.client;", + "const checkpoint=await client.getCheckpointInfo();", + "const sorted=(value)=>[...(value??[])].sort();", + "return JSON.stringify({", + "remoteKey:client.getRemoteKey(),lastLocalSeq:checkpoint.lastLocalSeq,journalEpoch:checkpoint.journalEpoch,", + "knownIDs:sorted(checkpoint.knownIDs),sentIDs:sorted(checkpoint.sentIDs),", + "receivedFiles:sorted(checkpoint.receivedFiles),sentFiles:sorted(checkpoint.sentFiles),", + "});", + "})()", + ].join(""), + environment + ); +} + +export async function readLocalCouchDbCheckpoints( + cliBinary: string, + environment: NodeJS.ProcessEnv, + checkpointIds: readonly string[] +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const ids=${JSON.stringify(checkpointIds)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const database=core.localDatabase.localDatabase;", + "const checkpoints=[];", + "for(const id of ids){", + "const doc=await database.get(id).catch(()=>false);", + "if(doc&&Object.prototype.hasOwnProperty.call(doc,'last_seq')) checkpoints.push({id,lastSequence:doc.last_seq});", + "}", + "return JSON.stringify(checkpoints);", + "})()", + ].join(""), + environment + ); +} diff --git a/test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts b/test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts index b1edadef..d0a13a12 100644 --- a/test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts +++ b/test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts @@ -13,7 +13,8 @@ import { import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, - configureCouchDb, + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, waitForLiveSyncCoreReady, @@ -311,25 +312,23 @@ async function main(): Promise { cliBinary: obsidianCli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData( + { + uri: couchDb.uri, + username: couchDb.username, + password: couchDb.password, + dbName, + }, + { + encrypt: true, + passphrase: e2eePassphrase, + usePathObfuscation: true, + E2EEAlgorithm: "v2", + } + ), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv); - await configureCouchDb( - obsidianCli.binary, - session.cliEnv, - { - uri: couchDb.uri, - username: couchDb.username, - password: couchDb.password, - dbName, - }, - { - encrypt: true, - passphrase: e2eePassphrase, - usePathObfuscation: true, - E2EEAlgorithm: "v2", - } - ); - await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv); await prepareRemote(obsidianCli.binary, session.cliEnv); await pushLocalChanges(obsidianCli.binary, session.cliEnv); diff --git a/test/e2e-obsidian/scripts/conflict-dialog-policy.ts b/test/e2e-obsidian/scripts/conflict-dialog-policy.ts new file mode 100644 index 00000000..254256bc --- /dev/null +++ b/test/e2e-obsidian/scripts/conflict-dialog-policy.ts @@ -0,0 +1,434 @@ +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + createE2eObsidianDeviceLocalState, + waitForLiveSyncCoreReady, + waitForLocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianElement, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const path = "conflict-dialog-policy.md"; +const baseContent = "Conflict dialogue policy\n\nShared base.\n"; +const leftContent = "Conflict dialogue policy\n\nChanged on the left.\n"; +const rightContent = "Conflict dialogue policy\n\nChanged on the right.\n"; +const thirdContent = "Conflict dialogue policy\n\nChanged on the third branch.\n"; +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_CONFLICT_DIALOG_TIMEOUT_MS ?? 10000); + +type ConflictFixture = { + currentRev: string; + currentParentRev?: string; + conflicts: string[]; +}; + +type ObsidianTestApp = { + commands?: { executeCommandById(commandId: string): boolean }; +}; + +type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; + +async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(baseContent)};`, + "let file=app.vault.getAbstractFileByPath(path);", + "if(!file) file=await app.vault.create(path,content);", + "await app.workspace.getLeaf(false).openFile(file);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function createManualConflict( + cliBinary: string, + env: NodeJS.ProcessEnv, + baseRev: string, + contents: readonly string[] +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const baseRev=${JSON.stringify(baseRev)};`, + `const contents=${JSON.stringify(contents)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const id=await core.services.path.path2id(path);", + "for(const [index,content] of contents.entries()){", + " const blob=new Blob([content],{type:'text/plain'});", + " const now=Date.now()+index;", + " const result=await core.localDatabase.putDBEntry({", + " _id:id,path,data:blob,ctime:now,mtime:now,", + " size:(await blob.arrayBuffer()).byteLength,children:[],", + " datatype:'plain',type:'plain',eden:{},", + " },false,baseRev);", + " if(!result?.ok) throw new Error(`Could not create conflict branch: ${path}`);", + "}", + "const meta=await core.localDatabase.getDBEntryMeta(path,{conflicts:true},true);", + "if(!meta?._rev||!meta._conflicts?.length){", + " throw new Error(`Conflict fixture did not produce multiple live leaves: ${path}`);", + "}", + "return JSON.stringify({currentRev:meta._rev,conflicts:meta._conflicts});", + "})()", + ].join(""), + env + ); +} + +async function readConflictFixture(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const meta=await core.localDatabase.getDBEntryMeta(path,{conflicts:true,revs:true},true);", + "if(!meta?._rev){", + " throw new Error(`Could not read the conflict fixture: ${path}`);", + "}", + "const revisions=meta._revisions;", + "const currentParentRev=revisions?.ids?.length>1", + " ? `${revisions.start-1}-${revisions.ids[1]}`", + " : undefined;", + "return JSON.stringify({currentRev:meta._rev,currentParentRev,conflicts:meta._conflicts??[]});", + "})()", + ].join(""), + env + ); +} + +async function waitForConflictCount( + cliBinary: string, + env: NodeJS.ProcessEnv, + expectedConflictCount: number +): Promise { + const deadline = Date.now() + uiTimeoutMs; + let fixture = await readConflictFixture(cliBinary, env); + while (fixture.conflicts.length !== expectedConflictCount && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 100)); + fixture = await readConflictFixture(cliBinary, env); + } + if (fixture.conflicts.length !== expectedConflictCount) { + throw new Error( + `Expected ${expectedConflictCount + 1} live version(s), but found ${fixture.conflicts.length + 1}: ${JSON.stringify(fixture)}` + ); + } + return fixture; +} + +async function requestConflictCheck(cliBinary: string, env: NodeJS.ProcessEnv, waitForCompletion = false) { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const waitForCompletion=${JSON.stringify(waitForCompletion)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "await core.services.conflict.queueCheckFor(path);", + "if(waitForCompletion){", + " await core.services.conflict.ensureAllProcessed();", + "}", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function waitForConflictChecks(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "await core.services.conflict.ensureAllProcessed();", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function applyReplicatedConflictResolution( + cliBinary: string, + env: NodeJS.ProcessEnv, + revisionToDelete: string, + expectedConflictCount = 0 +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const revisionToDelete=${JSON.stringify(revisionToDelete)};`, + `const expectedConflictCount=${JSON.stringify(expectedConflictCount)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "if(!(await core.fileHandler.deleteRevisionFromDB(path,revisionToDelete))){", + " throw new Error(`Could not apply the replicated conflict resolution: ${path} ${revisionToDelete}`);", + "}", + "const entry=await core.databaseFileAccess.fetchEntryMeta(path,undefined,true);", + "if(!entry){", + " throw new Error(`Could not read the surviving revision after replicated resolution: ${path}`);", + "}", + // This is the same Commonlib consumer boundary invoked after a remote + // document has already entered the local database. Calling it here + // isolates the dialogue policy from transport and second-device setup. + "await core.fileHandler._anyProcessReplicatedDoc(entry);", + "const conflicts=await core.databaseFileAccess.getConflictedRevs(path);", + "if(conflicts.length!==expectedConflictCount){", + " throw new Error(`Replicated resolution left an unexpected conflict count: ${path} ${JSON.stringify(conflicts)}`);", + "}", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +function conflictDialogue(page: Parameters[1]>[0]) { + return page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Conflicting changes" }), + }); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const cliBinary = cli.binary; + + const vault = await createTemporaryVault("obsidian-livesync-conflict-dialog-"); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: { + doctorProcessedVersion: "1.0.0", + isConfigured: true, + liveSync: false, + remoteType: "", + couchDB_URI: "http://127.0.0.1:5984", + couchDB_DBNAME: "conflict-dialog-policy", + couchDB_USER: "", + couchDB_PASSWORD: "", + notifyThresholdOfRemoteStorageSize: -1, + periodicReplication: false, + syncAfterMerge: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncOnSave: false, + syncOnStart: false, + disableMarkdownAutoMerge: true, + showMergeDialogOnlyOnActive: true, + showStatusOnEditor: true, + }, + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + }); + await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); + await createAndOpenBaseFile(cliBinary, session.cliEnv); + const base = await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path); + const fixture = await createManualConflict(cliBinary, session.cliEnv, base.rev, [ + leftContent, + rightContent, + thirdContent, + ]); + if (fixture.conflicts.length !== 2) { + throw new Error(`Expected exactly three live leaves: ${JSON.stringify(fixture)}`); + } + + await requestConflictCheck(cliBinary, session.cliEnv); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const modal = conflictDialogue(page); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await page + .locator(".livesync-status-messagearea") + .filter({ + hasText: "This file has 3 unresolved versions. They will be reviewed one pair at a time.", + }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Concat both", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + const actionButtonBounds = await modal.locator(".conflict-action-button").evaluateAll((buttons) => + buttons.map((button) => { + const bounds = button.getBoundingClientRect(); + return { top: bounds.top, bottom: bounds.bottom }; + }) + ); + if ( + actionButtonBounds.length !== 4 || + actionButtonBounds.some( + (bounds, index) => index > 0 && bounds.top < actionButtonBounds[index - 1].bottom + ) + ) { + throw new Error( + `Conflict action buttons are not stacked vertically: ${JSON.stringify(actionButtonBounds)}` + ); + } + }); + const firstDialogueScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "conflict-dialog-three-versions.png", + (page) => conflictDialogue(page).locator(".modal").first() + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const modal = conflictDialogue(page); + await modal.getByRole("button", { name: "Concat both", exact: true }).click({ timeout: uiTimeoutMs }); + }); + + const remainingAfterConcatenation = await waitForConflictCount(cliBinary, session.cliEnv, 1); + if ( + remainingAfterConcatenation.currentRev === fixture.currentRev || + remainingAfterConcatenation.currentParentRev !== fixture.currentRev + ) { + throw new Error( + `Concatenation did not extend the compared winner before retaining the remaining branch: ${JSON.stringify( + { + before: fixture, + after: remainingAfterConcatenation, + } + )}` + ); + } + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const modal = conflictDialogue(page); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const warning = page.locator(".livesync-status-messagearea").filter({ + hasText: "This file has unresolved conflicts.", + }); + await warning.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Not now", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + const warningScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "conflict-dialog-postponed-warning.png", + (page) => + page.locator(".livesync-status-messagearea").filter({ + hasText: "This file has unresolved conflicts.", + }) + ); + + await session.app.stop(); + session = undefined; + session = await startObsidianLiveSyncSession({ + binary, + cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + }); + await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); + await createAndOpenBaseFile(cliBinary, session.cliEnv); + const remainingAfterRestart = await waitForConflictCount(cliBinary, session.cliEnv, 1); + + await requestConflictCheck(cliBinary, session.cliEnv); + const restartedSession = session; + await withObsidianPage(restartedSession.remoteDebuggingPort, async (page) => { + const modal = conflictDialogue(page); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await page + .locator(".livesync-status-messagearea") + .filter({ hasText: "This file has unresolved conflicts." }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await applyReplicatedConflictResolution( + cliBinary, + restartedSession.cliEnv, + remainingAfterRestart.conflicts[0] + ); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + await page + .locator(".livesync-status-messagearea") + .filter({ hasText: "This file has unresolved conflicts." }) + .waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + // End the replicated-resolution episode before creating another + // conflict at the same path. This prevents a late cancellation event + // from the first episode from closing the later episode's dialogue. + await session.app.stop(); + session = undefined; + session = await startObsidianLiveSyncSession({ + binary, + cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + }); + await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); + await createAndOpenBaseFile(cliBinary, session.cliEnv); + + const resolved = await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path); + const laterFixture = await createManualConflict(cliBinary, session.cliEnv, resolved.rev, [ + leftContent, + rightContent, + ]); + if (laterFixture.conflicts.length !== 1) { + throw new Error(`Expected a later conflict with exactly two live leaves: ${JSON.stringify(laterFixture)}`); + } + await requestConflictCheck(cliBinary, session.cliEnv); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const modal = conflictDialogue(page); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Not now", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await waitForConflictChecks(cliBinary, session.cliEnv); + + await requestConflictCheck(cliBinary, session.cliEnv, true); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.waitForTimeout(1500); + if (await conflictDialogue(page).isVisible()) { + throw new Error("The postponed conflict dialogue reopened during an ordinary conflict check."); + } + }); + await waitForConflictCount(cliBinary, session.cliEnv, 1); + + const laterCommandExecuted = await withObsidianPage(session.remoteDebuggingPort, async (page) => { + return await page.evaluate( + (commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true, + "obsidian-livesync:livesync-checkdoc-conflicted" + ); + }); + if (!laterCommandExecuted) { + throw new Error("The explicit conflict-resolution command was not registered for the active editor."); + } + const laterActiveSession = session; + await withObsidianPage(laterActiveSession.remoteDebuggingPort, async (page) => { + const modal = conflictDialogue(page); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await applyReplicatedConflictResolution(cliBinary, laterActiveSession.cliEnv, laterFixture.conflicts[0]); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + await page + .locator(".livesync-status-messagearea") + .filter({ hasText: "This file has unresolved conflicts." }) + .waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + console.log( + "Real Obsidian reviewed three versions pairwise, retained the completed stage across restart, suppressed an ordinary repeat prompt after Not now, reopened the dialogue after the explicit command, and cleared both postponed and open-dialogue states after replicated resolutions." + ); + console.log(`Dialogue screenshot: ${firstDialogueScreenshot}`); + console.log(`Postponed warning screenshot: ${warningScreenshot}`); + } finally { + if (session) { + await session.app.stop(); + } + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts new file mode 100644 index 00000000..2a662a6f --- /dev/null +++ b/test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts @@ -0,0 +1,369 @@ +import { randomBytes } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + assertCouchDbReachable, + deleteCouchDbDatabase, + loadCouchDbConfig, + makeUniqueDatabaseName, + waitForCouchDbDocs, + type CouchDbConfig, +} from "../runner/couchdb.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { assertEqual, pushLocalChanges, waitForLocalDatabaseEntry } from "../runner/liveSyncWorkflow.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + acknowledgeDisabledOptionalFeatures, + captureAndStartInitialisation, + captureGuideDialogue, + confirmFastFetch, + confirmRebuild, + enterSetupURI, + finishInitialisation, + generateSetupURIFromDevice, + modalByTitle, + resumeCompatibilityReviewIfShown, + selectRadioOption, + skipMissingRemoteConfiguration, + type SetupArtifact, +} from "../runner/setupUri.ts"; +import { captureObsidianPage, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000"; +process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "30000"; + +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000); +const notePath = "E2E/manual-couchdb/from-first-device.md"; +const noteContent = "# Manual CouchDB setup\n\nThis note was sent by the manually configured first device.\n"; +const returnNotePath = "E2E/manual-couchdb/from-second-device.md"; +const returnNoteContent = + "# Manual CouchDB return journey\n\nThis note returned through a Setup URI generated by the first device.\n"; +const captures = { + scenario: "couchdb-manual-setup-workflow", + guide: "couchdb-manual", +} as const; + +type RunnerContext = { + binary: string; + cliBinary: string; + couchDb: CouchDbConfig; + dbName: string; + activeSessions: Set; +}; + +async function startUnconfiguredSession( + context: RunnerContext, + vault: TemporaryVault +): Promise { + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + }); + context.activeSessions.add(session); + return session; +} + +async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) return; + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function stopTrackedSessions(context: RunnerContext): Promise { + for (const session of [...context.activeSessions]) { + await stopTrackedSession(context, session); + } +} + +async function captureFailure(session: ObsidianLiveSyncSession, label: string): Promise { + const screenshot = await captureObsidianPage( + session.remoteDebuggingPort, + `couchdb-manual-${label}-failure.png`, + async () => undefined + ).catch(() => undefined); + if (screenshot) { + console.error(`Manual CouchDB failure screenshot: ${screenshot}`); + } +} + +async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, dbName: string): Promise { + const screenshots: string[] = []; + await withObsidianPage(port, async (page) => { + const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" }); + await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs }); + + const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync"); + await intro.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await selectRadioOption(intro, "I am setting this up for the first time"); + await intro + .getByRole("button", { name: "Yes, I want to set up a new synchronisation" }) + .click({ timeout: uiTimeoutMs }); + }); + + screenshots.push( + await captureGuideDialogue(port, "guide-couchdb-manual-connection-method.png", "Connection Method") + ); + await withObsidianPage(port, async (page) => { + const method = modalByTitle(page, "Connection Method"); + await selectRadioOption(method, "Configure a remote manually"); + await method + .getByRole("button", { name: "Proceed with manual configuration" }) + .click({ timeout: uiTimeoutMs }); + + const encryption = modalByTitle(page, "End-to-End Encryption"); + await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await encryption + .locator("label.row") + .filter({ hasText: "End-to-End Encryption" }) + .locator('input[type="checkbox"]') + .first() + .check({ timeout: uiTimeoutMs }); + await encryption + .locator("label.row") + .filter({ hasText: "Obfuscate Properties" }) + .locator('input[type="checkbox"]') + .first() + .check({ timeout: uiTimeoutMs }); + await encryption.locator('input[name="e2ee-passphrase"]').fill(randomBytes(24).toString("base64url")); + }); + screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption")); + await withObsidianPage(port, async (page) => { + const encryption = modalByTitle(page, "End-to-End Encryption"); + await encryption.getByRole("button", { name: "Proceed", exact: true }).click({ timeout: uiTimeoutMs }); + }); + + screenshots.push( + await captureGuideDialogue(port, "guide-couchdb-manual-remote-selection.png", "Choose a synchronisation remote") + ); + await withObsidianPage(port, async (page) => { + const remoteSelection = modalByTitle(page, "Choose a synchronisation remote"); + await selectRadioOption(remoteSelection, "CouchDB"); + await remoteSelection + .getByRole("button", { name: "Continue to CouchDB setup", exact: true }) + .click({ timeout: uiTimeoutMs }); + + const couchDB = modalByTitle(page, "CouchDB Configuration"); + await couchDB.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await couchDB.locator('input[name="couchdb-url"]').fill(couchDb.uri); + await couchDB.locator('input[name="couchdb-username"]').fill(couchDb.username); + await couchDB.locator('input[name="couchdb-password"]').fill(couchDb.password); + await couchDB.locator('input[name="couchdb-database"]').fill(dbName); + }); + screenshots.push( + await captureGuideDialogue(port, "guide-couchdb-manual-connection-details.png", "CouchDB Configuration") + ); + + await withObsidianPage(port, async (page) => { + const couchDB = modalByTitle(page, "CouchDB Configuration"); + await couchDB + .getByRole("button", { name: "Check server requirements", exact: true }) + .click({ timeout: uiTimeoutMs }); + const summary = couchDB.locator(".check-results summary"); + await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await summary + .filter({ hasText: /All checks passed successfully!|issue\(s\) detected!/u }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const errors = couchDB.locator(".check-result.error"); + if ((await errors.count()) > 0) { + const messages = await errors.locator(".message").allTextContents(); + throw new Error(`The documented CouchDB fixture failed its server requirements: ${messages.join(" | ")}`); + } + const details = couchDB.locator(".check-results details"); + if (!(await details.evaluate((element) => (element as HTMLDetailsElement).open))) { + await summary.click({ timeout: uiTimeoutMs }); + } + }); + screenshots.push( + await captureGuideDialogue(port, "guide-couchdb-manual-server-requirements.png", "CouchDB Configuration") + ); + + await withObsidianPage(port, async (page) => { + const couchDB = modalByTitle(page, "CouchDB Configuration"); + await couchDB + .getByRole("button", { name: "Create or connect to database and continue", exact: true }) + .click({ timeout: uiTimeoutMs }); + await modalByTitle(page, "Setup Complete: Preparing to Initialise Server").waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + }); + return screenshots; +} + +async function writeNoteViaObsidian( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content);", + "else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +async function waitForVaultFile( + vault: TemporaryVault, + path: string, + expected: string, + timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000) +): Promise { + const deadline = Date.now() + timeoutMs; + let lastContent = ""; + while (Date.now() < deadline) { + try { + lastContent = await readFile(join(vault.path, path), "utf8"); + if (lastContent === expected) return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +async function waitForRemoteEntry(context: RunnerContext, entry: { id: string; children: string[] }): Promise { + await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => { + const ids = new Set(docs.map((doc) => doc._id)); + return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId)); + }); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const couchDb = await loadCouchDbConfig(); + const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "manual-setup"); + const vaultA = await createTemporaryVault(); + const vaultB = await createTemporaryVault(); + const context: RunnerContext = { + binary, + cliBinary: cli.binary, + couchDb, + dbName, + activeSessions: new Set(), + }; + const screenshots: string[] = []; + let secondDeviceArtifact: SetupArtifact | undefined; + + try { + await assertCouchDbReachable(couchDb); + console.log(`Using Obsidian executable: ${binary}`); + console.log(`Temporary Vault A: ${vaultA.path}`); + console.log(`Temporary Vault B: ${vaultB.path}`); + console.log(`CouchDB database to be created by the onboarding dialogue: ${dbName}`); + + let session = await startUnconfiguredSession(context, vaultA); + try { + screenshots.push(...(await enterManualCouchDBSettings(session.remoteDebuggingPort, couchDb, dbName))); + screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "new", captures)); + screenshots.push(await confirmRebuild(session.remoteDebuggingPort, captures)); + screenshots.push(await skipMissingRemoteConfiguration(session.remoteDebuggingPort, captures)); + screenshots.push(await acknowledgeDisabledOptionalFeatures(session.remoteDebuggingPort, captures)); + const state = await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv); + await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + assertEqual(state.activeConfigurationId !== "", true, "Manual CouchDB setup did not activate a profile."); + assertEqual( + state.remoteConfigurationCount, + 1, + "Manual CouchDB setup did not persist exactly one remote profile." + ); + + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, notePath, noteContent); + const entry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, notePath); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await waitForRemoteEntry(context, entry); + + const generated = await generateSetupURIFromDevice( + session.remoteDebuggingPort, + randomBytes(24).toString("base64url"), + captures + ); + secondDeviceArtifact = generated.artifact; + screenshots.push(...generated.screenshots); + } catch (error) { + await captureFailure(session, "first-device"); + throw error; + } finally { + await stopTrackedSession(context, session); + } + + session = await startUnconfiguredSession(context, vaultB); + try { + if (!secondDeviceArtifact) { + throw new Error("The manually configured first device did not generate a Setup URI."); + } + screenshots.push( + await enterSetupURI(session.remoteDebuggingPort, "existing", secondDeviceArtifact, captures) + ); + screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "existing", captures)); + screenshots.push(...(await confirmFastFetch(session.remoteDebuggingPort, captures))); + await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv); + await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await waitForVaultFile(vaultB, notePath, noteContent); + + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, returnNotePath, returnNoteContent); + const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, returnNotePath); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await waitForRemoteEntry(context, returnEntry); + } catch (error) { + await captureFailure(session, "second-device"); + throw error; + } finally { + await stopTrackedSession(context, session); + } + + session = await startUnconfiguredSession(context, vaultA); + try { + await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await waitForVaultFile(vaultA, returnNotePath, returnNoteContent); + } catch (error) { + await captureFailure(session, "return-journey"); + throw error; + } finally { + await stopTrackedSession(context, session); + } + + console.log( + `Manual CouchDB onboarding created and tested its database, generated a second-device Setup URI, and completed a bidirectional note round-trip. Screenshots: ${screenshots.join(", ")}` + ); + } finally { + await stopTrackedSessions(context).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + await vaultA.dispose(); + await vaultB.dispose(); + if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") { + await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + } + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/couchdb-upload.ts b/test/e2e-obsidian/scripts/couchdb-upload.ts index 67da56b1..ee76f73c 100644 --- a/test/e2e-obsidian/scripts/couchdb-upload.ts +++ b/test/e2e-obsidian/scripts/couchdb-upload.ts @@ -11,8 +11,12 @@ import { import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, + assertE2eCompatibilityMarker, + assertE2eCompatibilityReviewPending, configureCouchDb, + createE2eCouchDbPluginData, prepareRemote, + resumeCompatibilityReview, waitForLiveSyncCoreReady, type LocalDatabaseEntry, } from "../runner/liveSyncWorkflow.ts"; @@ -101,8 +105,20 @@ async function main(): Promise { cliBinary: cli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData({ + uri: couchDb.uri, + username: couchDb.username, + password: couchDb.password, + dbName, + }), }); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + await assertE2eCompatibilityReviewPending(cli.binary, session.cliEnv); + await resumeCompatibilityReview(session.remoteDebuggingPort, { + verifyMissingDeviceMarkerExplanation: true, + screenshotPrefix: "compatibility-review-copied-vault", + }); + await assertE2eCompatibilityMarker(cli.binary, session.cliEnv); const configured = await configureCouchDb(cli.binary, session.cliEnv, { uri: couchDb.uri, diff --git a/test/e2e-obsidian/scripts/dialog-mounts.ts b/test/e2e-obsidian/scripts/dialog-mounts.ts new file mode 100644 index 00000000..86af117a --- /dev/null +++ b/test/e2e-obsidian/scripts/dialog-mounts.ts @@ -0,0 +1,1118 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts"; +import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + captureObsidianDialogue, + captureObsidianElement, + captureObsidianPage, + obsidianRemoteDebuggingPort, + withObsidianPage, +} from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const dialogRunStateKey = "__livesyncE2EDialogMount"; +const repairRunStateKey = "__livesyncE2ETroubleshootingRepair"; +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_DIALOG_TIMEOUT_MS ?? 10000); + +type DialogueMode = "desktop" | "mobile"; + +type DialogueRunState = { + done: boolean; + error?: string; + expected?: unknown; + kind: string; + result?: unknown; +}; + +type SetupManagerHandle = { + constructor: { name: string }; + onSelectServer?: (settings: unknown, remoteType: string) => Promise; + _askUseRemoteConfiguration?: (settings: unknown, preferred: unknown) => Promise; + _checkAndAskResolvingMismatchedTweaks?: (preferred: unknown) => Promise; + __addLog?: (message: string) => void; +}; + +type LiveSyncTestPlugin = { + core: { + fileHandler: { + createAllChunks(force: boolean): Promise; + }; + modules: SetupManagerHandle[]; + settings: Record; + }; +}; + +type ObsidianSettingsController = { + open(): void; + openTabById(tabId: string): void; +}; + +type ObsidianVaultFile = { + path: string; +}; + +type ObsidianTestApp = { + commands?: { executeCommandById(commandId: string): boolean }; + plugins?: { plugins: Record }; + setting?: ObsidianSettingsController; + vault?: { + delete(file: ObsidianVaultFile, force: boolean): Promise; + getFiles(): ObsidianVaultFile[]; + read(file: ObsidianVaultFile): Promise; + }; +}; + +type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; + +async function openRemoteSelectionDialogue(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const manager = plugin.core.modules.find((module) => module.constructor.name === "SetupManager"); + if (typeof manager?.onSelectServer !== "function") throw new Error("Could not find SetupManager"); + const state: DialogueRunState = { kind: "remote-selection", done: false }; + (globalThis as unknown as Record)[stateKey] = state; + void manager.onSelectServer(plugin.core.settings, "unknown").then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, dialogRunStateKey); + }); +} + +async function openSetupUriDialogue(): Promise { + const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + return await page.evaluate( + (commandId) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(commandId) === true, + "obsidian-livesync:livesync-opensetupuri" + ); + }); + if (!opened) { + throw new Error("The Setup URI command was not registered or could not be executed."); + } +} + +async function openConfigurationMismatchDialogue( + kind: "connected" | "connected-rebuild-recommended" | "remote-configuration" +): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate( + ({ stateKey, kind }) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const resolver = plugin.core.modules.find( + (module) => module.constructor.name === "ModuleResolvingMismatchedTweaks" + ); + if (resolver === undefined) throw new Error("Could not find ModuleResolvingMismatchedTweaks"); + if (kind === "connected-rebuild-recommended") { + plugin.core.settings.autoAcceptCompatibleTweak = false; + } + + const preferred = + kind === "connected-rebuild-recommended" + ? { + ...plugin.core.settings, + hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32", + } + : { + ...plugin.core.settings, + enableCompression: !Boolean(plugin.core.settings.enableCompression), + }; + const state: DialogueRunState = { + kind: `configuration-mismatch-${kind}`, + done: false, + expected: preferred, + }; + (globalThis as unknown as Record)[stateKey] = state; + const operation = + kind === "remote-configuration" + ? resolver._askUseRemoteConfiguration?.(plugin.core.settings, preferred) + : resolver._checkAndAskResolvingMismatchedTweaks?.(preferred); + if (operation === undefined) { + throw new Error(`The configuration mismatch resolver does not support ${kind}.`); + } + void operation.then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, + { stateKey: dialogRunStateKey, kind } + ); + }); +} + +async function assertDialogueRunCompleted(): Promise { + const state = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[stateKey]?.done === true, + dialogRunStateKey, + { timeout: uiTimeoutMs } + ); + return await page.evaluate( + (stateKey) => (globalThis as unknown as Record)[stateKey], + dialogRunStateKey + ); + }); + if (!state) { + throw new Error("The mounted dialogue did not record its completion state."); + } + if (state.error) { + throw new Error(`The mounted dialogue failed: ${state.error}`); + } + return state; +} + +async function verifyRemoteSizeNoticeAndDialogue(): Promise<{ + compatibilityReview: string; + notice: string; + dialogue: string; +}> { + const compatibilityReviewScreenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "compatibility-review-dialogue.png", + async (page) => { + const compatibilityReview = page.locator(".modal-container").filter({ + has: page + .locator(".modal-title") + .filter({ hasText: "Synchronisation paused for compatibility review" }), + }); + await compatibilityReview.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const message = compatibilityReview.locator(".vpk-action-dialog__message"); + const textSelection = await message.evaluate((element) => { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element); + selection?.removeAllRanges(); + selection?.addRange(range); + const selectedText = selection?.toString() ?? ""; + selection?.removeAllRanges(); + return { + selectedText, + userSelect: getComputedStyle(element).userSelect, + }; + }); + if ( + textSelection.userSelect !== "text" || + !textSelection.selectedText.includes("Remote synchronisation is paused on this device") + ) { + throw new Error( + `Expected the action dialogue message to be selectable, received user-select=${textSelection.userSelect} and selected text '${textSelection.selectedText}'.` + ); + } + const actions = compatibilityReview.locator(".vpk-action-dialog__actions--vertical"); + await actions.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection); + if (flexDirection !== "column") { + throw new Error(`Expected vertically stacked compatibility actions, received ${flexDirection}.`); + } + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const compatibilityReview = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review" }), + }); + await compatibilityReview + .getByRole("button", { name: "Keep synchronisation paused" }) + .click({ timeout: uiTimeoutMs }); + await compatibilityReview.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const noticeScreenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "remote-size-startup-notice.png", + async (page) => { + const notice = page.locator(".notice").filter({ + hasText: "Remote storage size notifications are not configured.", + }); + await notice.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await notice.getByRole("link", { name: "Review options" }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + ); + + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const notice = page.locator(".notice").filter({ + hasText: "Remote storage size notifications are not configured.", + }); + await notice.getByRole("link", { name: "Review options" }).click({ timeout: uiTimeoutMs }); + await notice.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const dialogueScreenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "remote-size-review-dialogue.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const action of [ + "No, never warn please", + "800MB (Cloudant, fly.io)", + "2GB (Standard)", + "Ask me later", + ]) { + await modal.getByRole("button", { name: action }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + } + ); + + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }), + }); + await modal.getByRole("button", { name: "Ask me later" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + return { + compatibilityReview: compatibilityReviewScreenshot, + notice: noticeScreenshot, + dialogue: dialogueScreenshot, + }; +} + +async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise { + await openRemoteSelectionDialogue(); + const screenshotPath = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + `setup-remote-selection-dialogue${mode === "mobile" ? "-mobile" : ""}.png`, + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }), + }); + const dialogue = modal.locator(".dialog-host"); + await modal.waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + for (const label of ["CouchDB", "S3-compatible Object Storage", "Peer-to-Peer (P2P)"]) { + await dialogue.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + const p2pDescription = dialogue.getByText( + "No central data-storage server is required, but a signalling relay is required for peer discovery.", + { exact: false } + ); + await p2pDescription.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const textSelection = await p2pDescription.evaluate((element) => { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element); + selection?.removeAllRanges(); + selection?.addRange(range); + const selectedText = selection?.toString() ?? ""; + selection?.removeAllRanges(); + return { + selectedText, + userSelect: getComputedStyle(element).userSelect, + }; + }); + if ( + textSelection.userSelect !== "text" || + !textSelection.selectedText.includes("signalling relay is required for peer discovery") + ) { + throw new Error( + `Expected Svelte dialogue prose to be selectable, received user-select=${textSelection.userSelect} and selected text '${textSelection.selectedText}'.` + ); + } + await modal + .getByRole("button", { name: "No, please take me back" }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + if (mode === "mobile") { + await assertMobileDialogueLayout(page, modal, "remote selection dialogue"); + } + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }), + }); + if (mode === "mobile") { + const previousDialogue = await modal.locator(".modal").last().elementHandle(); + if (previousDialogue === null) { + throw new Error("The remote selection dialogue did not expose a close control."); + } + await modal.locator(".modal-close-button").click({ timeout: uiTimeoutMs }); + await page.waitForFunction((element) => !element.isConnected, previousDialogue, { timeout: uiTimeoutMs }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await modal.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + return screenshotPath; +} + +async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise { + await openRemoteSelectionDialogue(); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const remoteSelection = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }), + }); + await remoteSelection + .locator("label") + .filter({ hasText: "CouchDB" }) + .locator('input[type="radio"]') + .first() + .check({ timeout: uiTimeoutMs }); + await remoteSelection + .getByRole("button", { name: "Continue to CouchDB setup", exact: true }) + .click({ timeout: uiTimeoutMs }); + }); + const screenshotPath = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + `setup-couchdb-dialogue${mode === "mobile" ? "-mobile" : ""}.png`, + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "CouchDB Configuration" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const label of [ + "Check server requirements", + "Test connection and save", + "Save without connecting", + "Cancel", + ]) { + await modal.getByRole("button", { name: label, exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + await modal + .getByText( + "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server.", + { exact: false } + ) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByText("CouchDB validates the database name when you connect.", { exact: false }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + if ((await modal.getByRole("button", { name: "Continue anyway", exact: true }).count()) !== 0) { + throw new Error("CouchDB onboarding still exposes the ambiguous Continue anyway action."); + } + const buttonGroups = modal.locator(".button-group"); + for (let index = 0; index < (await buttonGroups.count()); index++) { + const flexDirection = await buttonGroups.nth(index).evaluate((element) => { + return getComputedStyle(element).flexDirection; + }); + if (flexDirection !== "column") { + throw new Error(`Expected vertical CouchDB actions, received ${flexDirection}.`); + } + } + if (mode === "mobile") { + await assertMobileDialogueLayout(page, modal, "CouchDB settings dialogue"); + } + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "CouchDB Configuration" }), + }); + await modal.getByRole("button", { name: "Cancel", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + return screenshotPath; +} + +async function verifySetupUriDialogue(mode: DialogueMode): Promise { + await openSetupUriDialogue(); + const screenshotPath = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + `setup-uri-dialogue${mode === "mobile" ? "-mobile" : ""}.png`, + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Enter Setup URI" }), + }); + await modal.waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal + .locator('input[placeholder^="obsidian://setuplivesync"]') + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.locator('input[name="password"]').waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Test Settings and Continue" }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Cancel" }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + if (mode === "mobile") { + await assertMobileDialogueLayout(page, modal, "Setup URI dialogue"); + } + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Enter Setup URI" }), + }); + await modal.getByRole("button", { name: "Cancel" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return screenshotPath; +} + +async function verifyCompatibleMismatchAutoAdjustment(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const resolver = plugin.core.modules.find( + (module) => module.constructor.name === "ModuleResolvingMismatchedTweaks" + ); + if (typeof resolver?._checkAndAskResolvingMismatchedTweaks !== "function") { + throw new Error("Could not find the configuration mismatch resolver"); + } + plugin.core.settings.autoAcceptCompatibleTweak = undefined; + const currentModified = + typeof plugin.core.settings.tweakModified === "number" ? plugin.core.settings.tweakModified : 0; + const preferred = { + ...plugin.core.settings, + hashAlg: plugin.core.settings.hashAlg === "xxhash32" ? "xxhash64" : "xxhash32", + tweakModified: currentModified + 1, + }; + const state: DialogueRunState = { + kind: "configuration-mismatch-compatible-auto-adjustment", + done: false, + expected: preferred, + }; + (globalThis as unknown as Record)[stateKey] = state; + void resolver._checkAndAskResolvingMismatchedTweaks(preferred).then( + (result) => { + state.result = result; + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ); + }, dialogRunStateKey); + }); + + const state = await assertDialogueRunCompleted(); + if (!Array.isArray(state.result) || state.result.length !== 2) { + throw new Error("The compatible mismatch did not return its settings and rebuild decision."); + } + const [appliedSettings, shouldRebuild] = state.result; + const expectedSettings = state.expected; + if ( + typeof appliedSettings !== "object" || + appliedSettings === null || + typeof expectedSettings !== "object" || + expectedSettings === null || + !("hashAlg" in appliedSettings) || + !("hashAlg" in expectedSettings) || + appliedSettings.hashAlg !== expectedSettings.hashAlg || + shouldRebuild !== false + ) { + throw new Error("The compatible mismatch was not adjusted to the newer setting without a rebuild."); + } + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const autoAcceptEnabled = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.autoAcceptCompatibleTweak; + }); + if (autoAcceptEnabled !== true) { + throw new Error("Compatible mismatch auto-adjustment was not persisted as the default."); + } + for (const title of ["Auto-Accept Available", "Configuration Mismatch Detected"]) { + const dialogue = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: title }), + }); + if ((await dialogue.count()) !== 0) { + throw new Error(`Compatible mismatch auto-adjustment unexpectedly opened '${title}'.`); + } + } + }); +} + +async function verifyCompatibleAlignmentSettingDefault(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const persistedValue = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + return plugin.core.settings.autoAcceptCompatibleTweak; + }); + if (persistedValue !== undefined) { + throw new Error( + `The default-display fixture expected an undefined preference, received ${persistedValue}.` + ); + } + + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Advanced"]').click({ timeout: uiTimeoutMs }); + const settingItem = liveSyncSettings.locator(".setting-item").filter({ + has: page.getByText("Auto-accept compatible tweak mismatches", { exact: true }), + }); + await settingItem.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const toggle = settingItem.locator(".checkbox-container"); + if (!(await toggle.evaluate((element) => element.classList.contains("is-enabled")))) { + throw new Error("The automatic compatible-setting policy was displayed as disabled while still undefined."); + } + }); +} + +async function verifyConfigurationMismatchDialogues(): Promise<{ general: string; fetch: string }> { + await verifyCompatibleMismatchAutoAdjustment(); + await openConfigurationMismatchDialogue("remote-configuration"); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Use Remote Configuration" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Use configured settings", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Dismiss", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + + await openConfigurationMismatchDialogue("connected"); + const generalScreenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-configuration-mismatch-dialogue.png", + async (page) => { + const container = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + const modal = container.locator(".modal").last(); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const action of ["Apply settings to this device", "Update remote database settings"]) { + await modal + .getByRole("button", { name: action, exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await modal.getByRole("button", { name: /Dismiss$/u }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + for (const retiredAction of ["Use configured", "Update with mine"]) { + if ((await modal.getByRole("button", { name: retiredAction, exact: true }).count()) !== 0) { + throw new Error(`The mismatch dialogue still exposes the retired action '${retiredAction}'.`); + } + } + const actions = modal.locator(".setting-item-control").last(); + const flexDirection = await actions.evaluate((element) => getComputedStyle(element).flexDirection); + if (flexDirection !== "column") { + throw new Error(`Expected vertically stacked mismatch actions, received ${flexDirection}.`); + } + return modal; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + await modal.getByRole("button", { name: /Dismiss$/u }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + await assertDialogueRunCompleted(); + + await openConfigurationMismatchDialogue("connected-rebuild-recommended"); + const fetchScreenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-configuration-mismatch-fetch-dialogue.png", + async (page) => { + const container = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + const modal = container.locator(".modal").last(); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + return modal; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Configuration Mismatch Detected" }), + }); + await modal + .getByRole("button", { name: "Apply settings to this device, and fetch again", exact: true }) + .click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + const fetchResult = await assertDialogueRunCompleted(); + if (!Array.isArray(fetchResult.result) || fetchResult.result.length !== 2) { + throw new Error("The configuration-mismatch Fetch action did not return its settings and Fetch decision."); + } + const [appliedSettings, shouldFetch] = fetchResult.result; + const expectedSettings = fetchResult.expected; + if ( + typeof appliedSettings !== "object" || + appliedSettings === null || + typeof expectedSettings !== "object" || + expectedSettings === null || + !("hashAlg" in appliedSettings) || + !("hashAlg" in expectedSettings) || + appliedSettings.hashAlg !== expectedSettings.hashAlg || + shouldFetch !== true + ) { + throw new Error("The configuration-mismatch Fetch action did not apply the remote setting before Fetch."); + } + + return { general: generalScreenshotPath, fetch: fetchScreenshotPath }; +} + +async function executeRegisteredCommand(commandId: string): Promise { + const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + return await page.evaluate( + (id) => (globalThis as ObsidianTestGlobal).app?.commands?.executeCommandById(id) === true, + commandId + ); + }); + if (!opened) { + throw new Error(`The command was not registered or could not be executed: ${commandId}`); + } +} + +async function verifyLogAndReportSurfaces(): Promise<{ log: string; report: string }> { + await executeRegisteredCommand("obsidian-livesync:view-log"); + const logScreenshot = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-show-log.png", + async (page) => { + const logPane = page.locator(".logpane"); + await logPane.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const label of ["Wrap", "Auto scroll", "Pause"]) { + await logPane.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + await logPane.getByRole("button", { name: "Close", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + return logPane; + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const logPane = page.locator(".logpane"); + await logPane.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs }); + await logPane.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + await executeRegisteredCommand("obsidian-livesync:dump-debug-info"); + const reportScreenshot = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-full-report.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + hasText: "Your Debug info is ready to be copied", + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const report = await modal.locator("textarea").inputValue({ timeout: uiTimeoutMs }); + if (!report.includes("# ---- Debug Info Dump ----")) { + throw new Error("The full-report dialogue did not contain the generated debug report."); + } + await modal.getByRole("button", { name: "OK", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + return modal.locator(".modal").last(); + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + hasText: "Your Debug info is ready to be copied", + }); + await modal.getByRole("button", { name: "OK", exact: true }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + return { log: logScreenshot, report: reportScreenshot }; +} + +async function verifyHatchSurfacesAndSafeActions(): Promise { + const screenshotPath = await captureObsidianElement( + obsidianRemoteDebuggingPort(), + "troubleshooting-hatch.png", + async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + for (const label of [ + "Write logs into the file", + "Recreate chunks for current Vault files", + "Inspect conflicts and file/database differences", + "Resolve All conflicted files by the newer one", + ]) { + await liveSyncSettings.locator(".setting-item-name", { hasText: label }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + const settingNames = await liveSyncSettings.locator(".setting-item-name").allTextContents(); + const recreateIndex = settingNames.findIndex((name) => + name.includes("Recreate chunks for current Vault files") + ); + const inspectIndex = settingNames.findIndex((name) => + name.includes("Inspect conflicts and file/database differences") + ); + const resolveIndex = settingNames.findIndex((name) => + name.includes("Resolve All conflicted files by the newer one") + ); + if ( + recreateIndex === -1 || + inspectIndex === -1 || + resolveIndex === -1 || + !(recreateIndex < inspectIndex && inspectIndex < resolveIndex) + ) { + throw new Error( + "Recovery actions are not ordered from chunk recreation through inspection to bulk conflict resolution" + ); + } + await liveSyncSettings.getByRole("button", { name: "Recreate current chunks", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings.getByRole("button", { name: "Begin inspection", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await liveSyncSettings + .locator(".setting-item-name", { hasText: "Recreate chunks for current Vault files" }) + .scrollIntoViewIfNeeded(); + return liveSyncSettings; + } + ); + + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const liveSyncSettings = page.locator(".sls-setting"); + const logSetting = liveSyncSettings.locator(".setting-item").filter({ + has: page.getByText("Write logs into the file", { exact: true }), + }); + await logSetting.locator(".checkbox-container").click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.writeLogToTheFile === true; + }, + undefined, + { timeout: uiTimeoutMs } + ); + + const persistentLogMarker = "E2E persistent troubleshooting log"; + await page.evaluate((marker) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const module = plugin.core.modules.find((candidate) => candidate.constructor.name === "ModuleLog"); + if (typeof module?.__addLog !== "function") throw new Error("Could not find ModuleLog"); + module.__addLog(marker); + }, persistentLogMarker); + await page.waitForFunction( + async (marker) => { + const vault = (globalThis as ObsidianTestGlobal).app?.vault; + if (vault === undefined) return false; + const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_")); + if (logFile === undefined) return false; + return (await vault.read(logFile)).includes(marker); + }, + persistentLogMarker, + { timeout: uiTimeoutMs } + ); + + // Saving a toggle refreshes the settings pane. Resolve the visible control again so the + // second action does not target the detached pre-save element. + const refreshedLogToggle = page + .locator(".sls-setting:visible .setting-item:visible") + .filter({ + has: page.getByText("Write logs into the file", { exact: true }), + }) + .locator(".checkbox-container:visible") + .last(); + await refreshedLogToggle.click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + return plugin?.core.settings.writeLogToTheFile === false; + }, + undefined, + { timeout: uiTimeoutMs } + ); + await page.evaluate(async () => { + const vault = (globalThis as ObsidianTestGlobal).app?.vault; + if (vault === undefined) throw new Error("Obsidian Vault is unavailable"); + const logFile = vault.getFiles().find((file) => file.path.startsWith("livesync_log_")); + if (logFile === undefined) throw new Error("The persistent troubleshooting log was not created"); + await vault.delete(logFile, true); + }); + await page.waitForFunction( + () => + !(globalThis as ObsidianTestGlobal).app?.vault + ?.getFiles() + .some((file) => file.path.startsWith("livesync_log_")), + undefined, + { timeout: uiTimeoutMs } + ); + + await page.evaluate((stateKey) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is not loaded"); + const original = plugin.core.fileHandler.createAllChunks.bind(plugin.core.fileHandler); + const state: DialogueRunState = { kind: "recreate-missing-chunks", done: false }; + (globalThis as unknown as Record)[stateKey] = state; + plugin.core.fileHandler.createAllChunks = async (force) => { + try { + state.result = await original(force); + } catch (error) { + state.error = error instanceof Error ? error.message : String(error); + } finally { + state.done = true; + plugin.core.fileHandler.createAllChunks = original; + } + }; + }, repairRunStateKey); + await page + .locator(".sls-setting:visible") + .last() + .getByRole("button", { name: "Recreate current chunks", exact: true }) + .click({ + timeout: uiTimeoutMs, + }); + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[stateKey]?.done === true, + repairRunStateKey, + { timeout: uiTimeoutMs } + ); + const repairState = await page.evaluate( + (stateKey) => (globalThis as unknown as Record)[stateKey], + repairRunStateKey + ); + if (repairState?.error) { + throw new Error(`Recreate missing chunks failed: ${repairState.error}`); + } + + await page + .locator(".sls-setting:visible") + .last() + .getByRole("button", { name: "Begin inspection", exact: true }) + .click({ + timeout: uiTimeoutMs, + }); + await page + .locator(".notice") + .filter({ hasText: /^done$/u }) + .waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + }); + + return screenshotPath; +} + +async function verifyMobileStartupReviews(): Promise<{ compatibilityReview: string; remoteSizeReview: string }> { + const compatibilityReviewScreenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "compatibility-review-dialogue-mobile.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page + .locator(".modal-title") + .filter({ hasText: "Synchronisation paused for compatibility review" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.locator(".vpk-action-dialog__actions--vertical").waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await assertMobileDialogueLayout(page, modal, "compatibility review dialogue"); + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Synchronisation paused for compatibility review" }), + }); + await modal.getByRole("button", { name: "Keep synchronisation paused" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + + const compatibilityReminder = page.locator(".livesync-compatibility-review-notice"); + await compatibilityReminder.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertMobileNoticeLayout(page, compatibilityReminder, "compatibility review reminder"); + + const notice = page.locator(".notice").filter({ + hasText: "Remote storage size notifications are not configured.", + }); + await notice.getByRole("link", { name: "Review options" }).click({ timeout: uiTimeoutMs }); + await notice.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const remoteSizeReviewScreenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "remote-size-review-dialogue-mobile.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertMobileDialogueLayout(page, modal, "remote size review dialogue"); + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Setting up database size notification" }), + }); + await modal.getByRole("button", { name: "Ask me later" }).click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + return { + compatibilityReview: compatibilityReviewScreenshot, + remoteSizeReview: remoteSizeReviewScreenshot, + }; +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const vault = await createTemporaryVault(); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary: cli.binary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData( + { + uri: "http://127.0.0.1:5984", + username: "", + password: "", + dbName: "dialog-mounts-ui-only", + }, + { + notifyThresholdOfRemoteStorageSize: -1, + syncOnStart: false, + syncOnSave: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncAfterMerge: false, + periodicReplication: false, + useAdvancedMode: true, + } + ), + }); + try { + await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + } catch (error) { + const screenshot = await captureObsidianPage( + obsidianRemoteDebuggingPort(), + "dialog-mounts-core-not-ready.png", + async () => undefined + ); + console.error(`Core readiness diagnostic screenshot: ${screenshot}`); + const persistedSettings = JSON.parse( + await readFile(join(session.install.pluginDir, "data.json"), "utf8") + ) as Record; + console.error( + `Persisted readiness settings: ${JSON.stringify({ + isConfigured: persistedSettings.isConfigured, + remoteType: persistedSettings.remoteType, + settingVersion: persistedSettings.settingVersion, + couchDB_URI: persistedSettings.couchDB_URI, + couchDB_DBNAME: persistedSettings.couchDB_DBNAME, + remoteConfigurationCount: Object.keys( + (persistedSettings.remoteConfigurations as Record | undefined) ?? {} + ).length, + })}` + ); + throw error; + } + + const remoteSizeScreenshots = await verifyRemoteSizeNoticeAndDialogue(); + console.log( + `Compatibility review actions were stacked vertically, and the remote-size startup notice opened an untimed review dialogue successfully. Screenshots: ${remoteSizeScreenshots.compatibilityReview}, ${remoteSizeScreenshots.notice}, ${remoteSizeScreenshots.dialogue}` + ); + + const remoteScreenshot = await verifyRemoteSelectionDialogue("desktop"); + console.log(`Remote selection dialogue mounted and closed successfully. Screenshot: ${remoteScreenshot}`); + const couchDBScreenshot = await verifyCouchDBSettingsDialogue("desktop"); + console.log( + `CouchDB settings mode exposed explicit connection, unverified-save, and server-check actions. Screenshot: ${couchDBScreenshot}` + ); + const setupUriScreenshot = await verifySetupUriDialogue("desktop"); + console.log(`Setup URI dialogue mounted and closed successfully. Screenshot: ${setupUriScreenshot}`); + await verifyCompatibleAlignmentSettingDefault(); + console.log("The undefined compatible-setting preference is displayed with its effective enabled default."); + const mismatchScreenshots = await verifyConfigurationMismatchDialogues(); + console.log( + `A mismatch limited to compatible chunk settings was adjusted without a dialogue, current manual mismatch actions mounted successfully, and the Fetch action applied the remote setting before scheduling Fetch. Screenshots: ${mismatchScreenshots.general}, ${mismatchScreenshots.fetch}` + ); + const troubleshootingScreenshots = await verifyLogAndReportSurfaces(); + console.log( + `Show log and the generated full-report dialogue were reached through their registered commands. Screenshots: ${troubleshootingScreenshots.log}, ${troubleshootingScreenshots.report}` + ); + const hatchScreenshot = await verifyHatchSurfacesAndSafeActions(); + console.log( + `Hatch repair controls were reachable, safe empty-fixture runs completed, and persistent logging was enabled, verified, disabled, and removed. Screenshot: ${hatchScreenshot}` + ); + + await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs); + try { + const mobileStartupScreenshots = await verifyMobileStartupReviews(); + console.log( + `Mobile compatibility and remote-size reviews passed viewport, safe-area, touch-target, and vertical-action checks. Screenshots: ${mobileStartupScreenshots.compatibilityReview}, ${mobileStartupScreenshots.remoteSizeReview}` + ); + const mobileRemoteScreenshot = await verifyRemoteSelectionDialogue("mobile"); + console.log( + `Mobile remote selection dialogue passed viewport, safe-area, touch-target, and close-control checks. Screenshot: ${mobileRemoteScreenshot}` + ); + const mobileCouchDBScreenshot = await verifyCouchDBSettingsDialogue("mobile"); + console.log( + `Mobile CouchDB settings dialogue passed viewport, touch-target, and vertical-action checks. Screenshot: ${mobileCouchDBScreenshot}` + ); + const mobileSetupUriScreenshot = await verifySetupUriDialogue("mobile"); + console.log( + `Mobile Setup URI dialogue passed viewport, safe-area, and touch-target checks. Screenshot: ${mobileSetupUriScreenshot}` + ); + } finally { + await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs); + } + } finally { + if (session) { + await session.app.stop(); + } + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts b/test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts index ae645e1e..4af593c8 100644 --- a/test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts +++ b/test/e2e-obsidian/scripts/hidden-file-snippet-sync.ts @@ -1,5 +1,11 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; +import { + assertLocatorHasMinimumTouchTarget, + assertLocatorWithinSafeArea, + assertLocatorWithinViewport, + assertNoHorizontalOverflow, +} from "@vrtmrz/obsidian-test-session"; import { evalObsidianJson } from "../runner/cli.ts"; import { assertCouchDbReachable, @@ -13,7 +19,10 @@ import { import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, + assertE2eCompatibilityMarker, configureCouchDb, + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, waitForLiveSyncCoreReady, @@ -21,7 +30,14 @@ import { type LocalDatabaseEntry, } from "../runner/liveSyncWorkflow.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; -import { clickJsonResolveOption, obsidianRemoteDebuggingPort } from "../runner/ui.ts"; +import { + captureObsidianPage, + captureJsonResolveDialogue, + clickJsonResolveOption, + obsidianRemoteDebuggingPort, + withObsidianPage, +} from "../runner/ui.ts"; +import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000"; @@ -43,6 +59,7 @@ const mergeJsonPath = ".obsidian/livesync-e2e-merge.json"; const manualMergeJsonPath = ".obsidian/livesync-e2e-manual-merge.json"; const targetPath = ".obsidian/livesync-targeted/only-a.json"; const hiddenFileCliTimeoutMs = Number(process.env.E2E_OBSIDIAN_HIDDEN_FILE_CLI_TIMEOUT_MS ?? 90000); +const hiddenFileInitialisationStateKey = "__livesyncE2EHiddenFileInitialisation"; type RunnerContext = { binary: string; @@ -300,31 +317,33 @@ async function startConfiguredSession( vault: TemporaryVault, overrides: Record = {} ): Promise { + const couchDbSettings = { + uri: context.couchDb.uri, + username: context.couchDb.username, + password: context.couchDb.password, + dbName: context.dbName, + }; + const hiddenFileSettings = { + syncInternalFiles: true, + syncInternalFilesBeforeReplication: true, + watchInternalFileChanges: false, + syncInternalFilesTargetPatterns: "", + ...overrides, + }; const session = await startObsidianLiveSyncSession({ binary: context.binary, cliBinary: context.cliBinary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + // A fresh Vault waits for onboarding before opening its local database. + // Seed the same isolated settings used by configureCouchDb so that the + // application lifecycle can become ready without a user interaction. + pluginData: createE2eCouchDbPluginData(couchDbSettings, hiddenFileSettings), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); - await configureCouchDb( - context.cliBinary, - session.cliEnv, - { - uri: context.couchDb.uri, - username: context.couchDb.username, - password: context.couchDb.password, - dbName: context.dbName, - }, - { - syncInternalFiles: true, - syncInternalFilesBeforeReplication: true, - watchInternalFileChanges: false, - syncInternalFilesTargetPatterns: "", - ...overrides, - } - ); - await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + await assertE2eCompatibilityMarker(context.cliBinary, session.cliEnv); + await configureCouchDb(context.cliBinary, session.cliEnv, couchDbSettings, hiddenFileSettings); await prepareRemote(context.cliBinary, session.cliEnv); return session; } @@ -431,6 +450,7 @@ async function runJsonManualConflictResolution(context: RunnerContext, vault: Te const session = await startConfiguredSession(context, vault); await createHiddenJsonConflict(context, session, vault, manualMergeJsonPath, base, left, right); await openHiddenJsonResolveModal(context.cliBinary, session.cliEnv, manualMergeJsonPath); + const screenshotPath = await captureJsonResolveDialogue(obsidianRemoteDebuggingPort()); await clickJsonResolveOption(obsidianRemoteDebuggingPort(), "AB"); const merged = await waitForPathContent(vault.path, manualMergeJsonPath, (content) => @@ -442,7 +462,7 @@ async function runJsonManualConflictResolution(context: RunnerContext, vault: Te assertEqual(parsed.shared, "right", "Manual JSON conflict resolution did not apply the selected merged result."); assertEqual(parsed.fromA, true, "Manual JSON conflict resolution lost the first-side value."); assertEqual(parsed.fromB, true, "Manual JSON conflict resolution lost the second-side value."); - console.log("Hidden JSON conflict modal applied the selected merged result."); + console.log(`Hidden JSON conflict modal applied the selected merged result. Screenshot: ${screenshotPath}`); } async function runTargetMismatch( @@ -489,6 +509,342 @@ async function runTargetMismatch( console.log("Hidden target mismatch respected per-device target patterns, then applied after enabling the target."); } +async function setHiddenFileNoticeFixtures(port: number, itemIds: string[], includeRestart: boolean): Promise { + await withObsidianPage(port, async (page) => { + await page.evaluate( + ({ nextItemIds, nextIncludeRestart }) => { + const obsidianApp = (globalThis as typeof globalThis & { app: any }).app; + const plugin = obsidianApp.plugins.plugins["obsidian-livesync"]; + const core = plugin.core; + const addOn = core.getAddOn("HiddenFileSync"); + for (const id of ["alpha", "beta", "gamma"]) { + const pluginId = `livesync-e2e-${id}`; + obsidianApp.plugins.manifests[pluginId] = { + id: pluginId, + name: `E2E ${id[0]?.toUpperCase()}${id.slice(1)}`, + version: "1.0.0", + minAppVersion: "1.0.0", + description: "E2E fixture", + author: "Self-hosted LiveSync", + isDesktopOnly: false, + dir: `.obsidian/plugins/${pluginId}`, + }; + obsidianApp.plugins.enabledPlugins.add(pluginId); + } + addOn.queuedNotificationFiles.clear(); + for (const id of nextItemIds) { + addOn.queuedNotificationFiles.add(`.obsidian/plugins/livesync-e2e-${id}`); + } + if (nextIncludeRestart) { + addOn.queuedNotificationFiles.add(core.services.API.getSystemConfigDir()); + } + addOn.notifyConfigChange(); + }, + { nextItemIds: itemIds, nextIncludeRestart: includeRestart } + ); + }); +} + +async function clearHiddenFileNoticeFixtures(port: number): Promise { + await withObsidianPage(port, async (page) => { + await page.evaluate(() => { + const obsidianApp = (globalThis as typeof globalThis & { app: any }).app; + const plugin = obsidianApp.plugins.plugins["obsidian-livesync"]; + plugin.core.services.context.noticeGroups.hide("hidden-file-changes"); + for (const id of ["alpha", "beta", "gamma"]) { + const pluginId = `livesync-e2e-${id}`; + obsidianApp.plugins.enabledPlugins.delete(pluginId); + delete obsidianApp.plugins.manifests[pluginId]; + } + }); + }); +} + +async function runInitialisationNoticeGrouping(context: RunnerContext, vault: TemporaryVault): Promise { + const session = await startConfiguredSession(context, vault, { + syncInternalFiles: false, + syncInternalFilesBeforeReplication: false, + }); + const port = session.remoteDebuggingPort; + const timeoutMs = Number(process.env.E2E_OBSIDIAN_HIDDEN_FILE_NOTICE_TIMEOUT_MS ?? 10_000); + try { + await withObsidianPage(port, async (page) => { + const deadline = Date.now() + timeoutMs; + while ((await page.locator(".notice:visible").count()) > 0 && Date.now() < deadline) { + await page.locator(".notice:visible").first().click({ + force: true, + position: { x: 2, y: 2 }, + timeout: timeoutMs, + }); + } + assertEqual( + await page.locator(".notice:visible").count(), + 0, + "Transient start-up Notices remained before the Hidden File Sync initialisation check." + ); + }); + await withObsidianPage(port, async (page) => { + await page.evaluate((stateKey) => { + const obsidianApp = (globalThis as typeof globalThis & { app: any }).app; + const plugin = obsidianApp.plugins.plugins["obsidian-livesync"]; + const core = plugin.core; + const addOn = core.getAddOn("HiddenFileSync"); + const setting = core.services.setting; + const originalApplyPartial = setting.applyPartial; + const originalRebuildMerging = addOn.rebuildMerging; + const state = { + done: false, + reachedPreparation: false, + reachedInitialisation: false, + maxVisibleProgressNotices: 0, + visibleProgressNoticeTexts: [] as string[], + sawStandaloneGatheringNotice: false, + sawStandaloneRestartNotice: false, + releasePreparation: undefined as (() => void) | undefined, + releaseInitialisation: undefined as (() => void) | undefined, + error: undefined as string | undefined, + }; + (globalThis as unknown as Record)[stateKey] = state; + + const observer = new MutationObserver(() => { + const notices = Array.from(document.querySelectorAll(".notice")); + const progressNotices = notices.filter((notice) => notice.textContent?.includes("[⚙")); + state.sawStandaloneGatheringNotice ||= notices.some((notice) => + notice.textContent?.includes("Gathering files for enabling Hidden File Sync") + ); + state.sawStandaloneRestartNotice ||= notices.some((notice) => + notice.textContent?.includes("Done! Restarting the app is strongly recommended!") + ); + if (progressNotices.length > state.maxVisibleProgressNotices) { + state.maxVisibleProgressNotices = progressNotices.length; + state.visibleProgressNoticeTexts = progressNotices.map( + (notice) => notice.textContent?.trim() ?? "" + ); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true, + }); + + setting.applyPartial = async (...args: unknown[]) => { + const update = args[0] as { syncInternalFiles?: unknown } | undefined; + if (update?.syncInternalFiles === true && !state.reachedPreparation) { + state.reachedPreparation = true; + await new Promise((resolve) => { + state.releasePreparation = resolve; + }); + } + return await originalApplyPartial.apply(setting, args); + }; + + addOn.rebuildMerging = async (...args: unknown[]) => { + state.reachedInitialisation = true; + await new Promise((resolve) => { + state.releaseInitialisation = resolve; + }); + return await originalRebuildMerging.apply(addOn, args); + }; + + void core.services.setting + .enableOptionalFeature("MERGE") + .then( + () => { + state.done = true; + }, + (error: unknown) => { + state.error = error instanceof Error ? error.message : String(error); + state.done = true; + } + ) + .finally(() => { + setting.applyPartial = originalApplyPartial; + addOn.rebuildMerging = originalRebuildMerging; + const notices = Array.from(document.querySelectorAll(".notice")); + const progressNotices = notices.filter((notice) => notice.textContent?.includes("[⚙")); + state.sawStandaloneGatheringNotice ||= notices.some((notice) => + notice.textContent?.includes("Gathering files for enabling Hidden File Sync") + ); + state.sawStandaloneRestartNotice ||= notices.some((notice) => + notice.textContent?.includes("Done! Restarting the app is strongly recommended!") + ); + if (progressNotices.length > state.maxVisibleProgressNotices) { + state.maxVisibleProgressNotices = progressNotices.length; + state.visibleProgressNoticeTexts = progressNotices.map( + (notice) => notice.textContent?.trim() ?? "" + ); + } + observer.disconnect(); + }); + }, hiddenFileInitialisationStateKey); + }); + + const screenshotPath = await captureObsidianPage( + port, + "hidden-file-initial-scan-progress.png", + async (page) => { + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[ + stateKey + ]?.reachedPreparation === true, + hiddenFileInitialisationStateKey, + { timeout: timeoutMs } + ); + const progressNotices = page.locator(".notice").filter({ hasText: "[⚙" }); + await progressNotices.first().waitFor({ state: "visible", timeout: timeoutMs }); + assertEqual( + await progressNotices.count(), + 1, + "Hidden File Sync showed more than one progress Notice before saving its enabled setting." + ); + await progressNotices + .filter({ hasText: "Preparing Hidden File Sync..." }) + .waitFor({ state: "visible", timeout: timeoutMs }); + } + ); + + const result = await withObsidianPage(port, async (page) => { + await page.evaluate((stateKey) => { + const state = (globalThis as unknown as Record< + string, + { releasePreparation?: () => void } | undefined + >)[stateKey]; + state?.releasePreparation?.(); + }, hiddenFileInitialisationStateKey); + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[ + stateKey + ]?.reachedInitialisation === true, + hiddenFileInitialisationStateKey, + { timeout: timeoutMs } + ); + const progressNotices = page.locator(".notice").filter({ hasText: "[⚙" }); + assertEqual( + await progressNotices.count(), + 1, + "Hidden File Sync replaced its parent progress Notice when the first child phase started." + ); + await page.evaluate((stateKey) => { + const state = ( + globalThis as unknown as Record void } | undefined> + )[stateKey]; + state?.releaseInitialisation?.(); + }, hiddenFileInitialisationStateKey); + await page.waitForFunction( + (stateKey) => + (globalThis as unknown as Record)[stateKey]?.done === true, + hiddenFileInitialisationStateKey, + { timeout: hiddenFileCliTimeoutMs } + ); + return await page.evaluate( + (stateKey) => + ( + globalThis as unknown as Record< + string, + { + error?: string; + maxVisibleProgressNotices: number; + visibleProgressNoticeTexts: string[]; + sawStandaloneGatheringNotice: boolean; + sawStandaloneRestartNotice: boolean; + } + > + )[stateKey], + hiddenFileInitialisationStateKey + ); + }); + if (result.error) { + throw new Error(`Hidden File Sync initialisation failed: ${result.error}`); + } + assertEqual( + result.maxVisibleProgressNotices, + 1, + `Hidden File Sync split initialisation across multiple progress Notices: ${JSON.stringify( + result.visibleProgressNoticeTexts + )}` + ); + assertEqual( + result.sawStandaloneGatheringNotice, + false, + "Hidden File Sync showed the old standalone gathering Notice." + ); + assertEqual( + result.sawStandaloneRestartNotice, + false, + "Hidden File Sync showed the old standalone restart recommendation." + ); + console.log( + `Hidden File Sync showed one progress Notice before settings were saved and retained it throughout initialisation. Screenshot: ${screenshotPath}` + ); + } finally { + await session.app.stop(); + } +} + +async function runConfigurationNoticeGrouping(context: RunnerContext, vault: TemporaryVault): Promise { + const session = await startConfiguredSession(context, vault); + const port = session.remoteDebuggingPort; + const timeoutMs = Number(process.env.E2E_OBSIDIAN_HIDDEN_FILE_NOTICE_TIMEOUT_MS ?? 10_000); + try { + await setObsidianMobileTestMode(port, true, timeoutMs); + await setHiddenFileNoticeFixtures(port, ["alpha", "beta"], true); + + await withObsidianPage(port, async (page) => { + const visibleGroups = page.locator(".notice:has(.vpk-keyed-notice-group):visible"); + await visibleGroups.first().waitFor({ state: "visible", timeout: timeoutMs }); + assertEqual(await visibleGroups.count(), 1, "Hidden File Sync created more than one visible Notice group."); + + const notice = visibleGroups.first(); + const rows = notice.locator(".vpk-keyed-notice-group__item"); + assertEqual(await rows.count(), 3, "Hidden File Sync did not group every configuration-change action."); + await notice.getByText("Files in E2E Alpha were updated.", { exact: true }).waitFor(); + await notice.getByText("Files in E2E Beta were updated.", { exact: true }).waitFor(); + await notice.getByText("Other Obsidian settings files were updated.", { exact: true }).waitFor(); + + await assertLocatorWithinViewport(page, notice, { label: "Hidden File Sync notification group" }); + await assertNoHorizontalOverflow(page, notice, { label: "Hidden File Sync notification group" }); + await assertLocatorWithinSafeArea(page, notice, { + label: "Hidden File Sync notification group", + safeAreaInsets: iPhoneSafeArea, + }); + const buttons = notice.getByRole("button"); + for (let index = 0; index < (await buttons.count()); index += 1) { + await assertLocatorHasMinimumTouchTarget(page, buttons.nth(index), { + label: `Hidden File Sync notification action ${index + 1}`, + }); + } + + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + const screenshotPath = join(outputDirectory, "hidden-file-notice-group-mobile.png"); + await mkdir(dirname(screenshotPath), { recursive: true }); + await page.screenshot({ path: screenshotPath, fullPage: true, animations: "disabled" }); + + await rows.first().getByText("Files in E2E Alpha were updated.", { exact: true }).click(); + await notice.waitFor({ state: "hidden", timeout: timeoutMs }); + }); + + await setHiddenFileNoticeFixtures(port, ["gamma"], false); + await withObsidianPage(port, async (page) => { + const notice = page.locator(".notice:has(.vpk-keyed-notice-group):visible").first(); + await notice.waitFor({ state: "visible", timeout: timeoutMs }); + const rows = notice.locator(".vpk-keyed-notice-group__item"); + assertEqual(await rows.count(), 1, "A dismissed Hidden File Sync Notice repeated acknowledged rows."); + await rows.getByText("Files in E2E Gamma were updated.", { exact: true }).waitFor(); + }); + + console.log( + "Hidden File Sync grouped configuration notifications passed the mobile regression for issue #555." + ); + } finally { + await clearHiddenFileNoticeFixtures(port).catch(() => undefined); + await setObsidianMobileTestMode(port, false, timeoutMs).catch(() => undefined); + await session.app.stop(); + } +} + async function main(): Promise { const binary = requireObsidianBinary(); const cli = discoverObsidianCli(); @@ -516,6 +872,8 @@ async function main(): Promise { await runJsonConflictRoundTrip(context, vaultA, vaultB); await runJsonManualConflictResolution(context, vaultB); await runTargetMismatch(context, vaultA, vaultB); + await runInitialisationNoticeGrouping(context, vaultB); + await runConfigurationNoticeGrouping(context, vaultB); } finally { await vaultA.dispose(); await vaultB.dispose(); diff --git a/test/e2e-obsidian/scripts/local-suite.ts b/test/e2e-obsidian/scripts/local-suite.ts index c509edc2..9dfc225f 100644 --- a/test/e2e-obsidian/scripts/local-suite.ts +++ b/test/e2e-obsidian/scripts/local-suite.ts @@ -13,14 +13,30 @@ const testSteps: Step[] = [ : []), { name: "discover", args: ["run", "test:e2e:obsidian:discover"] }, { name: "smoke", args: ["run", "test:e2e:obsidian:smoke"] }, + { name: "onboarding invitation", args: ["run", "test:e2e:obsidian:onboarding-invitation"] }, + { name: "Svelte dialogue mounts", args: ["run", "test:e2e:obsidian:dialog-mounts"] }, + { name: "revision repair", args: ["run", "test:e2e:obsidian:revision-repair"] }, + { name: "settings UI", args: ["run", "test:e2e:obsidian:settings-ui"] }, + { name: "Review Harness", args: ["run", "test:e2e:obsidian:review-harness"] }, + { name: "P2P status pane", args: ["run", "test:e2e:obsidian:p2p-pane"] }, { name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] }, { name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] }, + { + name: "manual CouchDB setup workflow", + args: ["run", "test:e2e:obsidian:couchdb-manual-setup-workflow"], + }, { name: "CLI to real Obsidian synchronisation", args: ["run", "test:e2e:obsidian:cli-to-obsidian-sync"], }, { name: "Object Storage upload", args: ["run", "test:e2e:obsidian:minio-upload"] }, + { + name: "Object Storage Setup URI workflow", + args: ["run", "test:e2e:obsidian:object-storage-setup-uri-workflow"], + }, + { name: "P2P Setup URI workflow", args: ["run", "test:e2e:obsidian:p2p-setup-uri-workflow"] }, { name: "startup scan", args: ["run", "test:e2e:obsidian:startup-scan"] }, + { name: "provisioned Setup URI workflow", args: ["run", "test:e2e:obsidian:setup-uri-workflow"] }, { name: "two-vault synchronisation", args: ["run", "test:e2e:obsidian:two-vault-sync"] }, { name: "hidden file snippet synchronisation", args: ["run", "test:e2e:obsidian:hidden-file-snippet-sync"] }, { name: "Customisation Sync", args: ["run", "test:e2e:obsidian:customisation-sync"] }, @@ -29,9 +45,11 @@ const testSteps: Step[] = [ const manageCouchDb = process.argv.includes("--manage-couchdb") || process.argv.includes("--manage-services"); const manageMinio = process.argv.includes("--manage-minio") || process.argv.includes("--manage-services"); +const manageP2P = process.argv.includes("--manage-p2p") || process.argv.includes("--manage-services"); const keepServices = process.argv.includes("--keep-services"); const keepCouchDb = keepServices || process.argv.includes("--keep-couchdb"); const keepMinio = keepServices || process.argv.includes("--keep-minio"); +const keepP2P = keepServices || process.argv.includes("--keep-p2p"); function npmBinary(): string { return process.platform === "win32" ? "npm.cmd" : "npm"; @@ -78,9 +96,18 @@ async function stopManagedMinio(): Promise { }); } +async function stopManagedP2P(): Promise { + await runStep({ + name: "stop P2P relay fixture", + args: ["run", "test:docker-p2p:stop"], + optional: true, + }); +} + async function main(): Promise { let shouldStopCouchDb = false; let shouldStopMinio = false; + let shouldStopP2P = false; try { if (manageCouchDb) { await stopManagedCouchDb(); @@ -92,11 +119,19 @@ async function main(): Promise { await runStep({ name: "start MinIO fixture", args: ["run", "test:docker-s3:start"] }); shouldStopMinio = !keepMinio; } + if (manageP2P) { + await stopManagedP2P(); + await runStep({ name: "start P2P relay fixture", args: ["run", "test:docker-p2p:start"] }); + shouldStopP2P = !keepP2P; + } for (const step of testSteps) { await runStep(step); } } finally { + if (shouldStopP2P) { + await stopManagedP2P(); + } if (shouldStopMinio) { await stopManagedMinio(); } diff --git a/test/e2e-obsidian/scripts/minio-upload.ts b/test/e2e-obsidian/scripts/minio-upload.ts index 6af8b42a..b0899f12 100644 --- a/test/e2e-obsidian/scripts/minio-upload.ts +++ b/test/e2e-obsidian/scripts/minio-upload.ts @@ -1,8 +1,27 @@ +/** + * Verifies one complete Object Storage upload from a real Obsidian Vault, + * through LiveSync's local database and Journal Sync, to an S3-compatible + * service observed independently through the AWS SDK. + * + * The isolated Vault starts with Object Storage settings and the device-local + * compatibility acknowledgement already in place. Unconfigured start-up is + * intentionally inert and belongs to the onboarding scenario; compatibility + * review and visible setup have their own dedicated workflows. Supplying those + * prerequisites here keeps this scenario focused on the upload boundary. + * + * Note creation, local-database observation, one-shot synchronisation, request + * accounting, remote-object inspection, and prefix cleanup remain in one + * scenario so that a pass proves the same payload crossed every boundary. + * Separate successes would not prove that those observations belonged to the + * same upload. + */ import { evalObsidianJson } from "../runner/cli.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, configureObjectStorage, + createE2eObjectStoragePluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, waitForLiveSyncCoreReady, @@ -100,6 +119,11 @@ async function main(): Promise { cliBinary: cli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eObjectStoragePluginData({ + ...objectStorage, + bucketPrefix, + }), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); diff --git a/test/e2e-obsidian/scripts/object-storage-setup-uri-workflow.ts b/test/e2e-obsidian/scripts/object-storage-setup-uri-workflow.ts new file mode 100644 index 00000000..19276252 --- /dev/null +++ b/test/e2e-obsidian/scripts/object-storage-setup-uri-workflow.ts @@ -0,0 +1,324 @@ +import { execFile } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + assertEqual, + pushLocalChanges, + waitForLiveSyncCoreReady, + waitForLocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { + deleteObjectStoragePrefix, + ensureObjectStorageBucket, + listObjectStorageObjects, + loadObjectStorageConfig, + makeUniqueBucketPrefix, + type ObjectStorageConfig, +} from "../runner/objectStorage.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + acknowledgeDisabledOptionalFeatures, + captureAndStartInitialisation, + confirmFastFetch, + confirmRebuild, + enterSetupURI, + finishInitialisation, + generateSetupURIFromDevice, + resumeCompatibilityReviewIfShown, + skipMissingRemoteConfiguration, + type SetupArtifact, + type SetupCaptureNames, +} from "../runner/setupUri.ts"; +import { + captureObsidianElement, + captureObsidianPage, + obsidianRemoteDebuggingPort, + withObsidianPage, +} from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000"; + +const execFileAsync = promisify(execFile); +const captures: SetupCaptureNames = { scenario: "object-storage-setup-uri", guide: "object-storage-setup" }; +const noteFromFirst = "E2E/object-storage/from-first.md"; +const noteFromSecond = "E2E/object-storage/from-second.md"; +const firstContent = + "# Object Storage from the first device\n\nThis note travelled through the first device's Setup URI.\n"; +const secondContent = "# Object Storage from the second device\n\nThis note completed the return journey.\n"; + +type RunnerContext = { + binary: string; + cliBinary: string; + activeSessions: Set; +}; + +function sessionEnvironment(port: number): NodeJS.ProcessEnv { + return { ...process.env, E2E_OBSIDIAN_REMOTE_DEBUGGING_PORT: String(port) }; +} + +function sessionPorts(): readonly [number, number] { + const first = obsidianRemoteDebuggingPort(process.env); + const second = Number(process.env.E2E_OBSIDIAN_SECONDARY_REMOTE_DEBUGGING_PORT ?? first + 1); + if (!Number.isInteger(second) || second < 1 || second > 65535 || second === first) { + throw new Error(`Invalid secondary Obsidian remote debugging port: ${second}`); + } + return [first, second]; +} + +async function runDeno(script: string, environment: NodeJS.ProcessEnv): Promise { + const { stdout } = await execFileAsync( + "deno", + [ + "run", + "--minimum-dependency-age=0", + "--config=utils/flyio/deno.jsonc", + "--frozen", + "--lock=utils/flyio/deno.lock", + "--allow-env", + script, + ], + { cwd: process.cwd(), env: environment, maxBuffer: 4 * 1024 * 1024 } + ); + return stdout; +} + +async function generateBootstrapSetupURI( + objectStorage: ObjectStorageConfig, + bucketPrefix: string +): Promise { + const setupPassphrase = randomBytes(24).toString("base64url"); + const output = await runDeno("utils/setup/generate_setup_uri.ts", { + ...process.env, + remote_type: "s3", + endpoint: objectStorage.endpoint, + access_key: objectStorage.accessKey, + secret_key: objectStorage.secretKey, + bucket: objectStorage.bucket, + region: objectStorage.region, + force_path_style: String(objectStorage.forcePathStyle), + bucket_prefix: bucketPrefix, + passphrase: randomBytes(24).toString("base64url"), + uri_passphrase: setupPassphrase, + }); + const setupURI = output.split(/\r?\n/u).find((line) => line.startsWith("obsidian://setuplivesync?settings=")); + if (!setupURI) throw new Error("The public Setup URI generator did not emit an Object Storage Setup URI."); + return { setupURI, setupPassphrase }; +} + +async function startSession( + context: RunnerContext, + vault: TemporaryVault, + port: number +): Promise { + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + env: sessionEnvironment(port), + }); + context.activeSessions.add(session); + return session; +} + +async function stopSessions(context: RunnerContext): Promise { + for (const session of [...context.activeSessions]) { + await stopSession(context, session); + } +} + +async function stopSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) return; + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function writeNote( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content);", + "else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); + await waitForLocalDatabaseEntry(cliBinary, environment, path); +} + +async function waitForPathContent(vault: TemporaryVault, path: string, expected: string): Promise { + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000); + let lastContent = ""; + while (Date.now() < deadline) { + try { + lastContent = await readFile(join(vault.path, path), "utf8"); + if (lastContent === expected) return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +async function waitForObjectStorageData(config: ObjectStorageConfig, prefix: string): Promise { + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_OBJECT_STORAGE_TIMEOUT_MS ?? 30000); + while (Date.now() < deadline) { + if ((await listObjectStorageObjects(config, prefix)).length > 0) return; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`Timed out waiting for Object Storage data under ${prefix}.`); +} + +async function captureNote(port: number, path: string, text: string, filename: string): Promise { + await withObsidianPage(port, async (page) => { + await page.evaluate((notePath) => { + const obsidian = globalThis as typeof globalThis & { + app?: { + workspace?: { openLinkText(path: string, sourcePath: string, newLeaf: boolean): Promise }; + }; + }; + return obsidian.app?.workspace?.openLinkText(notePath, "", false); + }, path); + }); + await captureObsidianPage(port, `${filename}.full.png`, async (page) => { + await page.getByText(text, { exact: false }).first().waitFor({ state: "visible", timeout: 30000 }); + }); + return await captureObsidianElement(port, filename, (page) => page.locator(".workspace-leaf.mod-active").first()); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + + const objectStorage = await loadObjectStorageConfig(); + const bucketPrefix = makeUniqueBucketPrefix("setup-uri-workflow"); + const bootstrapArtifact = await generateBootstrapSetupURI(objectStorage, bucketPrefix); + const vaultA = await createTemporaryVault(); + const vaultB = await createTemporaryVault(); + const [portA, portB] = sessionPorts(); + const context: RunnerContext = { binary, cliBinary: cli.binary, activeSessions: new Set() }; + const screenshots: string[] = []; + + try { + await ensureObjectStorageBucket(objectStorage); + console.log(`Temporary Object Storage target: ${objectStorage.bucket}/${bucketPrefix}`); + + const sessionA = await startSession(context, vaultA, portA); + screenshots.push(await enterSetupURI(portA, "new", bootstrapArtifact, captures)); + screenshots.push(await captureAndStartInitialisation(portA, "new", captures)); + screenshots.push(await confirmRebuild(portA, captures)); + screenshots.push(await skipMissingRemoteConfiguration(portA, captures)); + screenshots.push(await acknowledgeDisabledOptionalFeatures(portA, captures)); + const firstState = await finishInitialisation(portA, context.cliBinary, sessionA.cliEnv); + await resumeCompatibilityReviewIfShown(portA); + assertEqual( + firstState.endpoint, + objectStorage.endpoint, + "The first device did not activate the Object Storage endpoint." + ); + assertEqual( + firstState.bucket, + objectStorage.bucket, + "The first device did not activate the Object Storage bucket." + ); + assertEqual( + firstState.bucketPrefix, + bucketPrefix, + "The first device did not activate the unique bucket prefix." + ); + + await writeNote(context.cliBinary, sessionA.cliEnv, noteFromFirst, firstContent); + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + await waitForObjectStorageData(objectStorage, bucketPrefix); + const generated = await generateSetupURIFromDevice(portA, randomBytes(24).toString("base64url"), captures); + if (generated.artifact.setupURI === bootstrapArtifact.setupURI) { + throw new Error("The first device returned the bootstrap Setup URI instead of generating a new one."); + } + screenshots.push(...generated.screenshots); + await stopSession(context, sessionA); + + const sessionB = await startSession(context, vaultB, portB); + screenshots.push(await enterSetupURI(portB, "existing", generated.artifact, captures)); + screenshots.push(await captureAndStartInitialisation(portB, "existing", captures)); + screenshots.push(...(await confirmFastFetch(portB, captures))); + const secondState = await finishInitialisation(portB, context.cliBinary, sessionB.cliEnv); + await resumeCompatibilityReviewIfShown(portB); + assertEqual( + secondState.endpoint, + objectStorage.endpoint, + "The second device did not import the Object Storage endpoint." + ); + assertEqual( + secondState.bucketPrefix, + bucketPrefix, + "The second device did not import the unique bucket prefix." + ); + await pushLocalChanges(context.cliBinary, sessionB.cliEnv); + await waitForPathContent(vaultB, noteFromFirst, firstContent); + screenshots.push( + await captureNote( + portB, + noteFromFirst, + "Object Storage from the first device", + "guide-object-storage-setup-first-to-second.png" + ) + ); + + await writeNote(context.cliBinary, sessionB.cliEnv, noteFromSecond, secondContent); + await pushLocalChanges(context.cliBinary, sessionB.cliEnv); + await stopSession(context, sessionB); + + const returningSessionA = await startSession(context, vaultA, portA); + await waitForLiveSyncCoreReady(context.cliBinary, returningSessionA.cliEnv); + await resumeCompatibilityReviewIfShown(portA); + await pushLocalChanges(context.cliBinary, returningSessionA.cliEnv); + await waitForPathContent(vaultA, noteFromSecond, secondContent); + screenshots.push( + await captureNote( + portA, + noteFromSecond, + "Object Storage from the second device", + "guide-object-storage-setup-second-to-first.png" + ) + ); + + console.log( + `Object Storage Setup URI and two-device roundtrip succeeded. Screenshots: ${screenshots.join(", ")}` + ); + } finally { + await stopSessions(context).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + await vaultA.dispose(); + await vaultB.dispose(); + if (process.env.E2E_OBSIDIAN_KEEP_OBJECT_STORAGE !== "true") { + await deleteObjectStoragePrefix(objectStorage, bucketPrefix).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + } + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/onboarding-invitation.ts b/test/e2e-obsidian/scripts/onboarding-invitation.ts new file mode 100644 index 00000000..8367fb82 --- /dev/null +++ b/test/e2e-obsidian/scripts/onboarding-invitation.ts @@ -0,0 +1,246 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { + assertLocatorHasMinimumTouchTarget, + assertLocatorWithinSafeArea, + assertNoHorizontalOverflow, +} from "@vrtmrz/obsidian-test-session"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { assertMobileDialogueLayout, iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_ONBOARDING_TIMEOUT_MS ?? 15000); +const markerPath = "E2E/unconfigured-startup-must-not-scan.md"; + +type UnconfiguredStartupEvidence = { + configured: boolean; + markerInDatabase: boolean; + offlineScanInitialised: boolean; + recommendedDefaults: { + usePluginSyncV2: boolean; + handleFilenameCaseSensitive: boolean; + }; +}; + +type ObsidianTestApp = { + setting?: { + open(): void; + openTabById(tabId: string): void; + }; +}; + +type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; + +async function writeMarker(vaultPath: string): Promise { + const fullPath = join(vaultPath, markerPath); + await mkdir(dirname(fullPath), { recursive: true }); + await writeFile(fullPath, "# This file must remain outside the database until setup completes.\n", "utf8"); +} + +async function inspectUnconfiguredStartup( + cliBinary: string, + env: NodeJS.ProcessEnv +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + `const markerPath=${JSON.stringify(markerPath)};`, + "let entry=false;", + "try{entry=await core.localDatabase.getDBEntry(markerPath,undefined,false,false);}catch{}", + "let initialised=false;", + "try{initialised=(await core.kvDB.get('initialized'))===true;}catch{}", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "configured:settings?.isConfigured===true,", + "markerInDatabase:Boolean(entry&&entry._id),", + "offlineScanInitialised:initialised,", + "recommendedDefaults:{", + "usePluginSyncV2:settings?.usePluginSyncV2,", + "handleFilenameCaseSensitive:settings?.handleFilenameCaseSensitive,", + "},", + "});", + "})()", + ].join(""), + env + ); +} + +function onboardingNotice(page: Parameters[1]>[0]) { + return page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" }); +} + +function onboardingDialogue(page: Parameters[1]>[0]) { + return page.locator(".modal-container").filter({ hasText: "Welcome to Self-hosted LiveSync" }); +} + +async function requireInvitationWithoutDialogue(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const invitation = onboardingNotice(page); + await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await invitation.locator(".sls-onboarding-invitation-action").waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + if ((await onboardingDialogue(page).count()) !== 0) { + throw new Error("The onboarding dialogue opened before the user selected the invitation."); + } + const compatibilityReview = page.locator(".modal-container").filter({ + hasText: "Synchronisation paused for compatibility review", + }); + if ((await compatibilityReview.count()) !== 0) { + throw new Error("A new unconfigured Vault was incorrectly treated as an existing compatibility state."); + } + }); +} + +async function captureDesktopInvitation(): Promise { + return await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "onboarding-invitation-desktop.png", + async (page) => { + const invitation = onboardingNotice(page); + await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertNoHorizontalOverflow(page, invitation, { label: "desktop onboarding invitation" }); + } + ); +} + +async function captureAndSelectMobileInvitation(): Promise { + const port = obsidianRemoteDebuggingPort(); + await setObsidianMobileTestMode(port, true, uiTimeoutMs); + const screenshot = await captureObsidianDialogue(port, "onboarding-invitation-mobile.png", async (page) => { + const invitation = onboardingNotice(page); + const action = invitation.locator(".sls-onboarding-invitation-action"); + await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertLocatorWithinSafeArea(page, invitation, { + label: "mobile onboarding invitation", + safeAreaInsets: iPhoneSafeArea, + }); + await assertNoHorizontalOverflow(page, invitation, { label: "mobile onboarding invitation" }); + await assertLocatorHasMinimumTouchTarget(page, action, { + label: "mobile onboarding invitation action", + }); + }); + await withObsidianPage(port, async (page) => { + await onboardingNotice(page).locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function captureAndCloseIntro(filename: string, mobile: boolean): Promise { + const port = obsidianRemoteDebuggingPort(); + const screenshot = await captureObsidianDialogue(port, filename, async (page) => { + const container = onboardingDialogue(page); + await container.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await container.getByText("I am setting this up for the first time", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await container + .getByText("I am adding a device to an existing synchronisation setup", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + if (mobile) await assertMobileDialogueLayout(page, container, "mobile onboarding introduction"); + }); + await withObsidianPage(port, async (page) => { + const container = onboardingDialogue(page); + await container.getByRole("button", { name: "No, please take me back" }).click({ timeout: uiTimeoutMs }); + await container.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function openOnboardingFromSettings(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Setup"]').click({ timeout: uiTimeoutMs }); + + const onboardingSetting = liveSyncSettings.locator(".setting-item").filter({ + has: page.locator(".setting-item-name").filter({ hasText: "Rerun Onboarding Wizard" }), + }); + await onboardingSetting.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await onboardingSetting + .getByRole("button", { name: "Rerun Wizard", exact: true }) + .click({ timeout: uiTimeoutMs }); + await onboardingDialogue(page).waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); +} + +async function closeSettings(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const settingsContainer = page.locator(".modal-container").filter({ + has: page.locator(".sls-setting"), + }); + await settingsContainer.locator(".modal-close-button").click({ timeout: uiTimeoutMs }); + await settingsContainer.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + const vault = await createTemporaryVault(); + let session: ObsidianLiveSyncSession | undefined; + try { + await writeMarker(vault.path); + session = await startObsidianLiveSyncSession({ + binary, + cliBinary: cli.binary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + }); + + await requireInvitationWithoutDialogue(); + const evidence = await inspectUnconfiguredStartup(cli.binary, session.cliEnv); + if ( + evidence.configured || + evidence.markerInDatabase || + evidence.offlineScanInitialised || + evidence.recommendedDefaults.usePluginSyncV2 !== true || + evidence.recommendedDefaults.handleFilenameCaseSensitive !== false + ) { + throw new Error(`Fresh Vault startup state did not match its contract: ${JSON.stringify(evidence)}`); + } + console.log(`Fresh Vault startup evidence: ${JSON.stringify(evidence)}`); + + const desktopInvitation = await captureDesktopInvitation(); + await openOnboardingFromSettings(); + const settingsIntro = await captureAndCloseIntro("onboarding-intro-settings-desktop.png", false); + await closeSettings(); + const mobileInvitation = await captureAndSelectMobileInvitation(); + const mobileIntro = await captureAndCloseIntro("onboarding-intro-mobile.png", true); + + console.log( + `Onboarding remained opt-in and kept unconfigured startup inert. Screenshots: ${[ + desktopInvitation, + mobileInvitation, + mobileIntro, + settingsIntro, + ].join(", ")}` + ); + } finally { + if (session) { + await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), false, uiTimeoutMs).catch(() => undefined); + await session.app.stop(); + } + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/p2p-pane.ts b/test/e2e-obsidian/scripts/p2p-pane.ts new file mode 100644 index 00000000..fc57b6c8 --- /dev/null +++ b/test/e2e-obsidian/scripts/p2p-pane.ts @@ -0,0 +1,415 @@ +/** + * Verifies the complete user-visible contract of the P2P status pane in real + * Obsidian: a configured CouchDB-only Vault with no P2P profile is not + * presented with P2P controls, while configured P2P devices can deliberately + * open the current pane in the appropriate workspace area. + * + * Desktop and mobile use separate Vaults, profiles, and Obsidian processes. + * Mobile mode is enabled before LiveSync's first load so that command and view + * registration observe the mobile application state, and no desktop workspace + * state can make a misplaced or restored pane appear correct. + * + * Command registration, automatic-opening policy, ribbon availability, + * workspace ownership, layout, and screenshots are kept in one scenario + * because together they describe one navigation path. Checking them in + * isolation could miss a pane which is registered correctly but opens in the + * wrong area, or one which is visible only because another session restored it. + */ +import { assertLocatorWithinViewport, assertNoHorizontalOverflow } from "@vrtmrz/obsidian-test-session"; +import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type"; +import { upsertRemoteConfigurationInPlace } from "@vrtmrz/livesync-commonlib/remote-configurations"; +import type { ConsoleMessage, Page } from "playwright"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, + waitForLiveSyncCoreReady, +} from "../runner/liveSyncWorkflow.ts"; +import { setObsidianMobileTestModeBeforePluginStart } from "../runner/mobileUi.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianPage, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_P2P_PANE_TIMEOUT_MS ?? 10000); + +type ObsidianTestLeaf = { + containerEl?: HTMLElement; + view?: { getViewType?: () => string }; +}; + +type ObsidianTestWorkspace = { + activeLeaf?: ObsidianTestLeaf; + getLeavesOfType?: (type: string) => ObsidianTestLeaf[]; + getRightLeaf?: (split: boolean) => ObsidianTestLeaf | null; + rightSplit?: { containerEl?: HTMLElement }; +}; + +type ObsidianTestApp = { + commands?: { + commands?: Record; + executeCommandById(commandId: string): boolean; + }; + isMobile?: boolean; + plugins?: { + plugins?: Record< + string, + { + core?: { + services?: { + API?: { + isMobile?: () => boolean; + }; + }; + }; + } + >; + }; + workspace?: ObsidianTestWorkspace; +}; + +type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; + +async function openP2PStatusPane(page: Page) { + return await page.evaluate((commandId) => { + const app = (globalThis as ObsidianTestGlobal).app; + const plugin = app?.plugins?.plugins?.["obsidian-livesync"]; + return { + opened: app?.commands?.executeCommandById(commandId) === true, + appIsMobile: app?.isMobile ?? null, + apiIsMobile: plugin?.core?.services?.API?.isMobile?.() ?? null, + bodyIsMobile: document.body.classList.contains("is-mobile"), + }; + }, "obsidian-livesync:open-p2p-server-status"); +} + +async function collectP2PWorkspaceState(page: Page) { + return await page.evaluate(() => { + const workspace = (globalThis as ObsidianTestGlobal).app?.workspace; + const activeLeaf = workspace?.activeLeaf; + const p2pLeaves = workspace?.getLeavesOfType?.("p2p-server-status") ?? []; + const rightLeaf = workspace?.getRightLeaf?.(false); + return { + bodyClasses: document.body.className, + activeLeaf: { + type: activeLeaf?.view?.getViewType?.() ?? null, + visible: activeLeaf?.containerEl?.checkVisibility?.() ?? null, + classes: activeLeaf?.containerEl?.className ?? null, + }, + p2pLeaves: p2pLeaves.map((leaf) => ({ + type: leaf.view?.getViewType?.() ?? null, + visible: leaf.containerEl?.checkVisibility?.() ?? null, + classes: leaf.containerEl?.className ?? null, + })), + rightLeaf: { + type: rightLeaf?.view?.getViewType?.() ?? null, + visible: rightLeaf?.containerEl?.checkVisibility?.() ?? null, + classes: rightLeaf?.containerEl?.className ?? null, + }, + visibleP2PContents: document.querySelectorAll( + ".workspace-leaf-content[data-type='p2p-server-status']:not(.is-hidden)" + ).length, + }; + }); +} + +async function assertMobileP2PPlacement(page: Page): Promise { + const placement = await page.evaluate(() => { + const workspace = (globalThis as ObsidianTestGlobal).app?.workspace; + const p2pLeaves = workspace?.getLeavesOfType?.("p2p-server-status") ?? []; + const rightSplit = workspace?.rightSplit?.containerEl; + const rightLeaf = workspace?.getRightLeaf?.(false); + return { + p2pLeafCount: p2pLeaves.length, + inRightSplit: p2pLeaves.some( + (leaf) => + (rightSplit?.contains(leaf.containerEl ?? null) ?? false) || + (leaf.containerEl?.closest(".mod-right-split, .workspace-drawer.mod-right") ?? null) !== null + ), + rightLeafType: rightLeaf?.view?.getViewType?.() ?? null, + p2pLeafClasses: p2pLeaves.map((leaf) => leaf.containerEl?.className ?? null), + rightSplitClasses: rightSplit?.className ?? null, + }; + }); + if (!placement.inRightSplit) { + throw new Error(`The mobile P2P status view was not opened in the right leaf: ${JSON.stringify(placement)}`); + } +} + +async function verifyP2PStatusPane(filename: string, mobile: boolean): Promise { + return await captureObsidianPage(obsidianRemoteDebuggingPort(), filename, async (page) => { + const runtimeErrors: string[] = []; + const onPageError = (error: Error) => runtimeErrors.push(`pageerror: ${error.message}`); + const onConsole = (message: ConsoleMessage) => { + if (message.type() === "error") runtimeErrors.push(`console: ${message.text()}`); + }; + page.on("pageerror", onPageError); + page.on("console", onConsole); + let dispatchState: Awaited> | undefined; + const heading = page.getByRole("heading", { name: "Signalling Status" }).last(); + try { + dispatchState = await openP2PStatusPane(page); + if (!dispatchState.opened) { + throw new Error("The P2P status command was not registered or could not be executed."); + } + if ( + mobile && + (dispatchState.appIsMobile !== true || + dispatchState.apiIsMobile !== true || + dispatchState.bodyIsMobile !== true) + ) { + throw new Error( + `The mobile P2P command did not observe a fully mobile application state: ${JSON.stringify(dispatchState)}` + ); + } + await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + } catch (error) { + const workspaceState = await collectP2PWorkspaceState(page); + console.error( + `P2P command state after failed open: ${JSON.stringify({ dispatchState, runtimeErrors })}` + ); + console.error(`P2P workspace state after failed open: ${JSON.stringify(workspaceState)}`); + throw error; + } finally { + page.off("pageerror", onPageError); + page.off("console", onConsole); + } + if (mobile) { + await assertMobileP2PPlacement(page); + } + const pane = heading.locator( + "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" + ); + await pane.getByText("Connection:", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + await pane.getByRole("button", { name: "Open connection" }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + const remoteSelector = pane.getByRole("combobox", { name: "Select active P2P remote" }); + await remoteSelector.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const remoteSelectionDeadline = Date.now() + uiTimeoutMs; + let remoteConfigurationId = ""; + while (Date.now() < remoteSelectionDeadline) { + remoteConfigurationId = (await remoteSelector.inputValue()).trim(); + if (remoteConfigurationId !== "") break; + await page.waitForTimeout(50); + } + if (remoteConfigurationId === "") { + throw new Error("The configured P2P status pane did not select an active P2P remote."); + } + if ( + (await pane.getByText("Please select an active P2P remote configuration to change P2P sync targets.").count()) !== + 0 + ) { + throw new Error("The configured P2P status pane still requested an active P2P remote."); + } + await assertNoHorizontalOverflow(page, pane, { label: "P2P status pane" }); + if (mobile) { + await assertLocatorWithinViewport(page, pane, { label: "mobile P2P status pane" }); + } + await dismissOpenNotices(page); + }); +} + +async function assertP2PUIIsOptIn(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const state = await page.evaluate(() => { + const commands = (globalThis as ObsidianTestGlobal).app?.commands?.commands ?? {}; + return { + currentCommand: commands["obsidian-livesync:open-p2p-server-status"] !== undefined, + legacyCommand: commands["obsidian-livesync:open-p2p-replicator"] !== undefined, + }; + }); + if (!state.currentCommand) { + throw new Error("The current P2P status command was not registered."); + } + if (state.legacyCommand) { + throw new Error("The retired P2P pane command is still exposed."); + } + if ((await page.locator(".workspace-leaf-content[data-type='p2p-server-status']:visible").count()) !== 0) { + throw new Error("The P2P status pane opened automatically for a CouchDB user without P2P configured."); + } + if ((await page.locator(".livesync-ribbon-p2p-server-status").count()) !== 0) { + throw new Error("The P2P ribbon icon was shown without a P2P configuration."); + } + }); +} + +async function assertConfiguredP2PUIIsAvailable(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.locator(".livesync-ribbon-p2p-server-status").waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + if ((await page.locator(".workspace-leaf-content[data-type='p2p-server-status']:visible").count()) !== 0) { + throw new Error("The configured P2P status pane opened before the user requested it."); + } + }); +} + +async function assertConfiguredP2PCommandIsAvailable(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const state = await page.evaluate(() => { + const app = (globalThis as ObsidianTestGlobal).app; + const commands = app?.commands?.commands ?? {}; + return { + commandRegistered: commands["obsidian-livesync:open-p2p-server-status"] !== undefined, + openPaneCount: app?.workspace?.getLeavesOfType?.("p2p-server-status").length ?? 0, + }; + }); + if (!state.commandRegistered) { + throw new Error("The configured P2P status command was not registered in mobile mode."); + } + if (state.openPaneCount !== 0) { + throw new Error("The configured P2P status pane opened before the mobile user requested it."); + } + }); +} + +async function dismissOpenNotices(page: Page): Promise { + const deadline = Date.now() + uiTimeoutMs; + let quietSince = Date.now(); + while (Date.now() < deadline) { + const dismissed = await page.evaluate(() => { + const notices = (Array.from(document.querySelectorAll(".notice")) as HTMLElement[]).filter( + (notice) => notice.checkVisibility?.() ?? notice.offsetParent !== null + ); + for (const notice of notices) { + const closeButton = notice.querySelector(".notice-close-button") as HTMLElement | null; + // Obsidian 1.12 does not render a separate close control for + // every Notice; clicking the Notice itself is its standard + // dismiss action. + (closeButton ?? notice).click(); + } + return notices.length; + }); + if (dismissed === 0) { + if (Date.now() - quietSince >= 500) { + return; + } + await page.waitForTimeout(100); + continue; + } + quietSince = Date.now(); + await page.waitForTimeout(50); + } + throw new Error("Transient Obsidian notices did not become quiet before the P2P status screenshot."); +} + +function createBaseP2PPluginData(): Record { + return createE2eCouchDbPluginData( + { + uri: "http://127.0.0.1:5984", + username: "", + password: "", + dbName: "p2p-pane-ui-only", + }, + { + notifyThresholdOfRemoteStorageSize: -1, + periodicReplication: false, + P2P_Enabled: false, + P2P_AutoStart: false, + syncAfterMerge: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncOnSave: false, + syncOnStart: false, + } + ); +} + +function createConfiguredP2PPluginData(): Record { + const pluginData = { + ...createBaseP2PPluginData(), + P2P_roomID: "configured-p2p-room", + P2P_passphrase: "configured-p2p-passphrase", + }; + upsertRemoteConfigurationInPlace(pluginData as ObsidianLiveSyncSettings, "p2p", { + id: "e2e-p2p", + name: "P2P Remote", + activateForP2P: true, + }); + return pluginData; +} + +async function withP2PSession( + binary: string, + cliBinary: string, + pluginData: Record, + verify: () => Promise, + options: { mobileBeforePluginStart?: boolean } = {} +): Promise { + const vault = await createTemporaryVault(); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData, + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + lifecycle: options.mobileBeforePluginStart + ? { + beforePluginStart: async ({ remoteDebuggingPort }) => { + await setObsidianMobileTestModeBeforePluginStart( + remoteDebuggingPort, + true, + uiTimeoutMs + ); + }, + } + : undefined, + }); + await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); + await verify(); + } finally { + if (session) { + await session.app.stop(); + } + await vault.dispose(); + } +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + + await withP2PSession(binary, cli.binary, createBaseP2PPluginData(), async () => { + await assertP2PUIIsOptIn(); + }); + + await withP2PSession( + binary, + cli.binary, + createConfiguredP2PPluginData(), + async () => { + await assertConfiguredP2PUIIsAvailable(); + const desktopScreenshot = await verifyP2PStatusPane("p2p-status-pane.png", false); + console.log( + `Configured P2P status UI remained opt-in and was reachable on desktop. Screenshot: ${desktopScreenshot}` + ); + } + ); + + await withP2PSession( + binary, + cli.binary, + createConfiguredP2PPluginData(), + async () => { + await assertConfiguredP2PCommandIsAvailable(); + const mobileScreenshot = await verifyP2PStatusPane("p2p-status-pane-mobile.png", true); + console.log( + `Configured P2P status UI remained opt-in and was reachable on mobile. Screenshot: ${mobileScreenshot}` + ); + }, + { mobileBeforePluginStart: true } + ); +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/p2p-setup-uri-workflow.ts b/test/e2e-obsidian/scripts/p2p-setup-uri-workflow.ts new file mode 100644 index 00000000..1a67edb6 --- /dev/null +++ b/test/e2e-obsidian/scripts/p2p-setup-uri-workflow.ts @@ -0,0 +1,589 @@ +import { execFile } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { connect } from "node:net"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { assertEqual, waitForLocalDatabaseEntry } from "../runner/liveSyncWorkflow.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + acknowledgeDisabledOptionalFeatures, + captureAndStartInitialisation, + captureGuideDialogue, + confirmFastFetch, + confirmRebuild, + enterSetupURI, + finishInitialisation, + generateSetupURIFromDevice, + modalByTitle, + resumeCompatibilityReviewIfShown, + type SetupArtifact, + type SetupCaptureNames, +} from "../runner/setupUri.ts"; +import { + captureObsidianElement, + captureObsidianPage, + obsidianRemoteDebuggingPort, + withObsidianPage, +} from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000"; + +const execFileAsync = promisify(execFile); +const captures: SetupCaptureNames = { scenario: "p2p-setup-uri", guide: "p2p-setup" }; +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_P2P_WORKFLOW_TIMEOUT_MS ?? 60000); +const noteFromFirst = "E2E/p2p/from-first.md"; +const noteFromSecond = "E2E/p2p/from-second.md"; +const firstContent = "# P2P from the first device\n\nThis note was fetched directly from the first device.\n"; +const secondContent = "# P2P from the second device\n\nThis note completed the return journey.\n"; + +type RunnerContext = { + binary: string; + cliBinary: string; + activeSessions: Set; +}; + +function sessionEnvironment(port: number): NodeJS.ProcessEnv { + return { ...process.env, E2E_OBSIDIAN_REMOTE_DEBUGGING_PORT: String(port) }; +} + +function sessionPorts(): readonly [number, number] { + const first = obsidianRemoteDebuggingPort(process.env); + const second = Number(process.env.E2E_OBSIDIAN_SECONDARY_REMOTE_DEBUGGING_PORT ?? first + 1); + if (!Number.isInteger(second) || second < 1 || second > 65535 || second === first) { + throw new Error(`Invalid secondary Obsidian remote debugging port: ${second}`); + } + return [first, second]; +} + +async function runDeno(script: string, environment: NodeJS.ProcessEnv): Promise { + const { stdout } = await execFileAsync( + "deno", + [ + "run", + "--minimum-dependency-age=0", + "--config=utils/flyio/deno.jsonc", + "--frozen", + "--lock=utils/flyio/deno.lock", + "--allow-env", + script, + ], + { cwd: process.cwd(), env: environment, maxBuffer: 4 * 1024 * 1024 } + ); + return stdout; +} + +async function generateBootstrapSetupURI(relay: string): Promise { + const setupPassphrase = randomBytes(24).toString("base64url"); + const output = await runDeno("utils/setup/generate_setup_uri.ts", { + ...process.env, + remote_type: "p2p", + p2p_relays: relay, + p2p_room_id: `real-obsidian-${randomBytes(12).toString("hex")}`, + p2p_passphrase: randomBytes(24).toString("base64url"), + p2p_app_id: "self-hosted-livesync-real-obsidian-e2e", + p2p_auto_start: "false", + p2p_auto_broadcast: "false", + passphrase: randomBytes(24).toString("base64url"), + uri_passphrase: setupPassphrase, + }); + const setupURI = output.split(/\r?\n/u).find((line) => line.startsWith("obsidian://setuplivesync?settings=")); + if (!setupURI) throw new Error("The public Setup URI generator did not emit a P2P Setup URI."); + return { setupURI, setupPassphrase }; +} + +async function waitForRelay(relay: string): Promise { + const endpoint = new URL(relay); + const port = Number(endpoint.port || (endpoint.protocol === "wss:" ? 443 : 80)); + const host = endpoint.hostname === "localhost" ? "127.0.0.1" : endpoint.hostname; + const deadline = Date.now() + Number(process.env.E2E_P2P_RELAY_READY_TIMEOUT_MS ?? 30000); + let lastError: unknown; + while (Date.now() < deadline) { + try { + await new Promise((resolve, reject) => { + const socket = connect({ host, port }); + socket.setTimeout(1000); + socket.once("connect", () => { + socket.destroy(); + resolve(); + }); + socket.once("timeout", () => { + socket.destroy(); + reject(new Error("connection timed out")); + }); + socket.once("error", reject); + }); + await new Promise((resolve) => setTimeout(resolve, 1500)); + return; + } catch (error) { + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + } + throw new Error( + `P2P relay is not ready at ${relay}: ${lastError instanceof Error ? lastError.message : lastError}` + ); +} + +async function startSession( + context: RunnerContext, + vault: TemporaryVault, + port: number +): Promise { + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + env: sessionEnvironment(port), + }); + context.activeSessions.add(session); + return session; +} + +async function stopSessions(context: RunnerContext): Promise { + for (const session of [...context.activeSessions]) { + await session.app.stop(); + context.activeSessions.delete(session); + } +} + +async function writeNote( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content);", + "else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); + await waitForLocalDatabaseEntry(cliBinary, environment, path); +} + +async function waitForPathContent(vault: TemporaryVault, path: string, expected: string): Promise { + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 60000); + let lastContent = ""; + while (Date.now() < deadline) { + try { + lastContent = await readFile(join(vault.path, path), "utf8"); + if (lastContent === expected) return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +async function readReflectionDiagnostics( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "const entry=await core.localDatabase.getDBEntry(path,undefined,false,true).catch(()=>false);", + "const chunks=entry&&Array.isArray(entry.children)?await Promise.all(entry.children.map(async(id)=>{", + "const chunk=await core.localDatabase.getDBEntry(id,undefined,false,true).catch(()=>false);", + "return {id,found:!!chunk};", + "})):[];", + "return JSON.stringify({", + "suspendFileWatching:settings.suspendFileWatching,", + "suspendParseReplicationResult:settings.suspendParseReplicationResult,", + "configured:settings.isConfigured,", + "entry:entry?{id:entry._id,path:entry.path,children:entry.children||[]}:false,", + "chunks,", + "databaseQueueCount:core.services.replication.databaseQueueCount?.value,", + "storageApplyingCount:core.services.replication.storageApplyingCount?.value,", + "replicationResultCount:core.services.replication.replicationResultCount?.value,", + "});", + "})()", + ].join(""), + environment + ); +} + +async function executeCommand(port: number, commandId: string): Promise { + const opened = await withObsidianPage(port, async (page) => { + return await page.evaluate( + (id) => + ( + globalThis as typeof globalThis & { + app?: { commands?: { executeCommandById(commandId: string): boolean } }; + } + ).app?.commands?.executeCommandById(id) === true, + commandId + ); + }); + if (!opened) throw new Error(`Obsidian command was not available: ${commandId}`); +} + +async function openP2PStatus(port: number, filename: string): Promise { + await executeCommand(port, "obsidian-livesync:open-p2p-server-status"); + await withObsidianPage(port, async (page) => { + const heading = page.getByRole("heading", { name: "Signalling Status" }).last(); + await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const pane = heading.locator( + "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" + ); + const open = pane.getByRole("button", { name: "Open connection" }); + if (await open.isVisible()) { + const blockingDialogues = await page.locator(".modal-container:visible").evaluateAll((elements) => + elements.map((element) => ({ + title: element.querySelector(".modal-title")?.textContent?.trim() ?? "", + text: element.textContent?.trim().replace(/\s+/gu, " ").slice(0, 240) ?? "", + })) + ); + if (blockingDialogues.length > 0) { + throw new Error( + `P2P connection control is blocked by a dialogue: ${JSON.stringify(blockingDialogues)}` + ); + } + await open.click({ timeout: uiTimeoutMs }); + } + await pane.locator(".status-value.connected").waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + return await captureObsidianElement(port, filename, (page) => { + const heading = page.getByRole("heading", { name: "Signalling Status" }).last(); + return heading.locator( + "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" + ); + }); +} + +async function reconnectP2PStatus(port: number): Promise { + await executeCommand(port, "obsidian-livesync:open-p2p-server-status"); + await withObsidianPage(port, async (page) => { + const heading = page.getByRole("heading", { name: "Signalling Status" }).last(); + await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const pane = heading.locator( + "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" + ); + const disconnect = pane.getByRole("button", { name: "Disconnect", exact: true }); + if (await disconnect.isVisible()) { + await disconnect.click({ timeout: uiTimeoutMs }); + } + const open = pane.getByRole("button", { name: "Open connection", exact: true }); + await open.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await open.click({ timeout: uiTimeoutMs }); + await pane.locator(".status-value.connected").waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); +} + +async function acceptConnectionRequests( + ports: readonly number[], + stop: () => boolean, + screenshots: string[] +): Promise { + const captured = new Set(); + while (!stop()) { + for (const port of ports) { + const visible = await withObsidianPage(port, async (page) => { + return await modalByTitle(page, "P2P Connection Request").isVisible(); + }).catch(() => false); + if (!visible) continue; + if (!captured.has(port)) { + const requestNumber = + screenshots.filter((filename) => filename.includes("guide-p2p-setup-connection-request-")).length + + 1; + screenshots.push( + await captureGuideDialogue( + port, + `guide-p2p-setup-connection-request-${requestNumber}.png`, + "P2P Connection Request" + ) + ); + captured.add(port); + } + await withObsidianPage(port, async (page) => { + await modalByTitle(page, "P2P Connection Request") + .getByRole("button", { name: "Accept", exact: true }) + .click({ timeout: uiTimeoutMs }); + }); + } + await new Promise((resolve) => setTimeout(resolve, 200)); + } +} + +async function fetchFromFirstPeer( + sessionA: ObsidianLiveSyncSession, + portA: number, + portB: number, + screenshots: string[] +): Promise { + try { + await withObsidianPage(portB, async (page) => { + const modal = modalByTitle(page, "P2P Rebuild"); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.locator(".peer-item").first().waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + } catch (error) { + const firstDeviceAlive = sessionA.app.process.exitCode === null && sessionA.app.process.signalCode === null; + const firstDeviceUi = await withObsidianPage(portA, async (page) => { + return await page.locator("body").innerText(); + }).catch(() => undefined); + const secondDeviceDialogue = await withObsidianPage(portB, async (page) => { + return await modalByTitle(page, "P2P Rebuild").innerText(); + }).catch(() => undefined); + throw new Error( + [ + error instanceof Error ? error.message : String(error), + `First Obsidian process alive: ${firstDeviceAlive}`, + `First Obsidian CDP reachable: ${firstDeviceUi !== undefined}`, + firstDeviceUi === undefined ? undefined : `First device UI: ${firstDeviceUi.slice(0, 1_500)}`, + secondDeviceDialogue === undefined + ? undefined + : `Second-device P2P Rebuild dialogue: ${secondDeviceDialogue.slice(0, 1_500)}`, + sessionA.app.output().stderr + ? `First Obsidian stderr: ${sessionA.app.output().stderr.slice(-2_000)}` + : undefined, + ] + .filter(Boolean) + .join("\n") + ); + } + screenshots.push(await captureGuideDialogue(portB, "guide-p2p-setup-select-first-device.png", "P2P Rebuild")); + + let finished = false; + const acceptor = acceptConnectionRequests([portA, portB], () => finished, screenshots); + try { + await withObsidianPage(portB, async (page) => { + const modal = modalByTitle(page, "P2P Rebuild"); + await modal + .locator(".peer-item") + .first() + .getByRole("button", { name: "Sync", exact: true }) + .click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + } finally { + finished = true; + await acceptor; + } + + await withObsidianPage(portB, async (page) => { + const modal = modalByTitle(page, "P2P Rebuild"); + if (await modal.isVisible()) { + await modal.getByRole("button", { name: "Skip and close" }).click({ timeout: uiTimeoutMs }); + } + }); +} + +async function replicateFromStatusPane(port: number): Promise { + await withObsidianPage(port, async (page) => { + const heading = page.getByRole("heading", { name: "Detected Peers" }).last(); + await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const pane = heading.locator( + "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" + ); + await pane.getByRole("button", { name: "Refresh", exact: true }).click({ timeout: uiTimeoutMs }); + const replicate = pane.getByRole("button", { name: "Replicate now" }).first(); + await replicate.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await replicate.click({ timeout: uiTimeoutMs }); + }); +} + +async function waitForDetectedPeer(port: number): Promise { + await withObsidianPage(port, async (page) => { + const heading = page.getByRole("heading", { name: "Detected Peers" }).last(); + await heading.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const pane = heading.locator( + "xpath=ancestor::*[contains(concat(' ', normalize-space(@class), ' '), ' workspace-leaf-content ')][1]" + ); + await pane.getByRole("button", { name: "Refresh", exact: true }).click({ timeout: uiTimeoutMs }); + await pane.getByRole("button", { name: "Replicate now" }).first().waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + }); +} + +async function capturePeerActionsMenu(port: number): Promise { + await withObsidianPage(port, async (page) => { + const moreActions = page.getByRole("button", { name: /^More actions for /u }).first(); + await moreActions.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await moreActions.click({ timeout: uiTimeoutMs }); + const menu = page.locator(".menu:visible").last(); + await menu.waitFor({ state: "visible", timeout: uiTimeoutMs }); + for (const label of [ + "Synchronise when this device connects", + "Follow whenever this device connects", + "Include in the P2P synchronisation command", + ]) { + await menu.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + const layout = await menu.evaluate((element) => { + const rect = element.getBoundingClientRect(); + return { + insideViewport: + rect.left >= 0 && + rect.top >= 0 && + rect.right <= document.documentElement.clientWidth && + rect.bottom <= document.documentElement.clientHeight, + hasHorizontalOverflow: element.scrollWidth > element.clientWidth, + }; + }); + if (!layout.insideViewport || layout.hasHorizontalOverflow) { + throw new Error(`P2P peer actions menu did not fit the viewport: ${JSON.stringify(layout)}`); + } + }); + const screenshot = await captureObsidianElement( + port, + "guide-p2p-setup-peer-actions-menu.png", + (page) => page.locator(".menu:visible").last() + ); + await withObsidianPage(port, async (page) => { + await page.keyboard.press("Escape"); + await page.locator(".menu:visible").waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function captureNote(port: number, path: string, text: string, filename: string): Promise { + await withObsidianPage(port, async (page) => { + await page.evaluate((notePath) => { + const obsidian = globalThis as typeof globalThis & { + app?: { + workspace?: { openLinkText(path: string, sourcePath: string, newLeaf: boolean): Promise }; + }; + }; + return obsidian.app?.workspace?.openLinkText(notePath, "", false); + }, path); + }); + await captureObsidianPage(port, `${filename}.full.png`, async (page) => { + await page.getByText(text, { exact: false }).first().waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + return await captureObsidianElement(port, filename, (page) => page.locator(".workspace-leaf.mod-active").first()); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + + const relay = process.env.E2E_P2P_RELAY_URL ?? `ws://127.0.0.1:${process.env.E2E_P2P_RELAY_PORT ?? "4010"}/`; + await waitForRelay(relay); + const bootstrapArtifact = await generateBootstrapSetupURI(relay); + const vaultA = await createTemporaryVault(); + const vaultB = await createTemporaryVault(); + const [portA, portB] = sessionPorts(); + const context: RunnerContext = { binary, cliBinary: cli.binary, activeSessions: new Set() }; + const screenshots: string[] = []; + + try { + console.log(`Temporary P2P relay: ${relay}`); + console.log(`Temporary P2P devices: ${vaultA.name}, ${vaultB.name}`); + + const sessionA = await startSession(context, vaultA, portA); + screenshots.push(await enterSetupURI(portA, "new", bootstrapArtifact, captures)); + screenshots.push(await captureAndStartInitialisation(portA, "new", captures)); + screenshots.push(await confirmRebuild(portA, captures)); + screenshots.push(await acknowledgeDisabledOptionalFeatures(portA, captures)); + const firstState = await finishInitialisation(portA, context.cliBinary, sessionA.cliEnv); + await resumeCompatibilityReviewIfShown(portA); + assertEqual(firstState.p2pEnabled, true, "The first device did not enable P2P."); + assertEqual(firstState.p2pRelays, relay, "The first device did not activate the P2P relay."); + await writeNote(context.cliBinary, sessionA.cliEnv, noteFromFirst, firstContent); + + const generated = await generateSetupURIFromDevice(portA, randomBytes(24).toString("base64url"), captures); + if (generated.artifact.setupURI === bootstrapArtifact.setupURI) { + throw new Error("The first device returned the bootstrap Setup URI instead of generating a new one."); + } + screenshots.push(...generated.screenshots); + screenshots.push(await openP2PStatus(portA, "guide-p2p-setup-first-device-connected.png")); + + const sessionB = await startSession(context, vaultB, portB); + screenshots.push(await enterSetupURI(portB, "existing", generated.artifact, captures)); + screenshots.push(await captureAndStartInitialisation(portB, "existing", captures)); + screenshots.push(...(await confirmFastFetch(portB, captures))); + await fetchFromFirstPeer(sessionA, portA, portB, screenshots); + await waitForLocalDatabaseEntry(context.cliBinary, sessionB.cliEnv, noteFromFirst, { + timeoutMs: uiTimeoutMs, + }); + const secondState = await finishInitialisation(portB, context.cliBinary, sessionB.cliEnv); + await resumeCompatibilityReviewIfShown(portB); + assertEqual(secondState.p2pEnabled, true, "The second device did not enable P2P."); + assertEqual(secondState.p2pRelays, relay, "The second device did not import the P2P relay."); + assertEqual(secondState.p2pRoomId, firstState.p2pRoomId, "The two devices did not join the same P2P room."); + try { + await waitForPathContent(vaultB, noteFromFirst, firstContent); + } catch (error) { + const diagnostics = await readReflectionDiagnostics(context.cliBinary, sessionB.cliEnv, noteFromFirst); + throw new Error( + `${error instanceof Error ? error.message : String(error)}\nReflection diagnostics: ${JSON.stringify(diagnostics)}` + ); + } + screenshots.push( + await captureNote(portB, noteFromFirst, "P2P from the first device", "guide-p2p-setup-first-to-second.png") + ); + console.log("P2P workflow: initial Fetch from the first device completed."); + + await writeNote(context.cliBinary, sessionB.cliEnv, noteFromSecond, secondContent); + await reconnectP2PStatus(portA); + await reconnectP2PStatus(portB); + await waitForDetectedPeer(portA); + screenshots.push(await openP2PStatus(portA, "guide-p2p-setup-devices-connected.png")); + screenshots.push(await capturePeerActionsMenu(portA)); + console.log("P2P workflow: peer actions menu verified; starting the return journey."); + let returnJourneyFinished = false; + const returnJourneyAcceptor = acceptConnectionRequests( + [portA, portB], + () => returnJourneyFinished, + screenshots + ); + try { + await replicateFromStatusPane(portA); + console.log("P2P workflow: return replication requested; waiting for the second device's note."); + await waitForPathContent(vaultA, noteFromSecond, secondContent); + console.log("P2P workflow: return note reached the first device."); + } finally { + returnJourneyFinished = true; + await returnJourneyAcceptor; + console.log("P2P workflow: return connection approval loop stopped."); + } + screenshots.push( + await captureNote( + portA, + noteFromSecond, + "P2P from the second device", + "guide-p2p-setup-second-to-first.png" + ) + ); + + console.log(`P2P Setup URI and two-device roundtrip succeeded. Screenshots: ${screenshots.join(", ")}`); + } finally { + console.log("P2P workflow: stopping tracked Obsidian sessions."); + await stopSessions(context).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + console.log("P2P workflow: disposing temporary Vaults."); + await vaultA.dispose(); + await vaultB.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/review-harness.ts b/test/e2e-obsidian/scripts/review-harness.ts new file mode 100644 index 00000000..3c7fb96f --- /dev/null +++ b/test/e2e-obsidian/scripts/review-harness.ts @@ -0,0 +1,469 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { + assertLocatorHasMinimumTouchTarget, + assertLocatorWithinSafeArea, + assertNoHorizontalOverflow, +} from "@vrtmrz/obsidian-test-session"; +import { CURRENT_SETTING_VERSION } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const"; +import { REVIEW_HARNESS_STATE_KEY } from "../../../src/features/ReviewHarness/reviewHarnessController.ts"; +import { REVIEW_HARNESS_FIXTURE_ROOT } from "../../../src/features/ReviewHarness/reviewHarnessVaultFixture.ts"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts"; +import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + captureObsidianDialogue, + captureObsidianPage, + obsidianRemoteDebuggingPort, + withObsidianPage, +} from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVIEW_HARNESS_TIMEOUT_MS ?? 15000); + +type ObsidianTestApp = { + commands?: { executeCommandById(commandId: string): boolean }; + plugins?: { plugins: Record }; + vault?: { getAbstractFileByPath(path: string): unknown | null }; +}; + +type ReviewHarnessTestGlobal = typeof globalThis & { + app?: ObsidianTestApp; + reviewHarnessCopiedReport?: string; +}; + +type ReviewHarnessReadinessSnapshot = { + coreAvailable: boolean; + databaseReady?: boolean; + appReady?: boolean; + configured?: boolean; + remoteType?: string; + settingVersion?: number; + suspended?: boolean; + unresolvedMessages: string[]; +}; + +const sensitiveDiagnosticLine = + /security seed|passphrase|password|credential|secret|access.?key|jwt.?key|authori[sz]ation|obsidian:\/\/setuplivesync|sls\+/iu; +const interruptedStartupMessages = [ + "No replicator has been activated or has not been initialised yet.", + "Self-hosted LiveSync cannot be initialised, exiting loading.", +]; + +function redactDiagnosticLine(line: string): string { + if (sensitiveDiagnosticLine.test(line)) return "[REDACTED SENSITIVE LOG LINE]"; + return line.replace(/\bhttps?:\/\/[^/\s:@]+:[^@\s/]+@/giu, "https://[REDACTED]@"); +} + +async function assertNoInterruptedStartupNotice(stage: string): Promise { + const notices = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.waitForTimeout(1500); + return await page.locator(".notice").allTextContents(); + }); + const interrupted = notices.filter((notice) => + interruptedStartupMessages.some((message) => notice.includes(message)) + ); + if (interrupted.length > 0) { + throw new Error(`LiveSync emitted an interrupted-startup Notice during ${stage}: ${interrupted.join(" | ")}`); + } + console.log(`No interrupted-startup Notice observed during ${stage}.`); +} + +async function captureReadinessFailure( + cliBinary: string, + session: ObsidianLiveSyncSession, + readinessError: unknown +): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + await mkdir(outputDirectory, { recursive: true }); + + const captureErrors: string[] = []; + let screenshotPath: string | undefined; + try { + screenshotPath = await captureObsidianPage( + obsidianRemoteDebuggingPort(), + "review-harness-core-not-ready.png", + async () => undefined + ); + } catch (error) { + captureErrors.push(`screenshot: ${error instanceof Error ? error.message : String(error)}`); + } + + let readiness: ReviewHarnessReadinessSnapshot | undefined; + try { + readiness = await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync']?.core;", + "if(!core)return JSON.stringify({coreAvailable:false,unresolvedMessages:[]});", + "const settings=core.services.setting.currentSettings();", + "let unresolvedMessages=[];", + "try{", + "unresolvedMessages=(await core.services.appLifecycle.getUnresolvedMessages()).flat()", + ".filter((message)=>message!==undefined&&message!==null)", + ".map((message)=>String(message)).slice(-50);", + "}catch(error){unresolvedMessages=[`Could not inspect unresolved messages: ${String(error)}`];}", + "return JSON.stringify({", + "coreAvailable:true,", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "configured:settings?.isConfigured===true,", + "remoteType:settings?.remoteType??'',", + "settingVersion:settings?.settingVersion,", + "suspended:core.services.appLifecycle.isSuspended(),", + "unresolvedMessages,", + "});", + "})()", + ].join(""), + session.cliEnv + ); + readiness.unresolvedMessages = readiness.unresolvedMessages.map(redactDiagnosticLine); + } catch (error) { + captureErrors.push(`readiness snapshot: ${error instanceof Error ? error.message : String(error)}`); + } + + let recentLog: string[] = []; + try { + recentLog = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const opened = await page.evaluate( + (commandId) => + (globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true, + "obsidian-livesync:view-log" + ); + if (!opened) throw new Error("The Show log command was not registered."); + const logPane = page.locator(".logpane"); + await logPane.waitFor({ state: "visible", timeout: 5000 }); + return (await logPane.locator(".log pre").allTextContents()).slice(-80).map(redactDiagnosticLine); + }); + } catch (error) { + captureErrors.push(`recent log: ${error instanceof Error ? error.message : String(error)}`); + } + + const resultPath = join(outputDirectory, "review-harness-core-not-ready.json"); + await writeFile( + resultPath, + `${JSON.stringify( + { + capturedAt: new Date().toISOString(), + failure: readinessError instanceof Error ? readinessError.message : String(readinessError), + screenshotPath, + readiness, + recentLog, + captureErrors, + }, + null, + 2 + )}\n`, + "utf8" + ); + if (screenshotPath) console.error(`Review Harness core readiness screenshot: ${screenshotPath}`); + console.error(`Review Harness core readiness diagnostics: ${resultPath}`); +} + +async function openHarness(): Promise { + const opened = await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + return await page.evaluate( + (commandId) => (globalThis as ReviewHarnessTestGlobal).app?.commands?.executeCommandById(commandId) === true, + "obsidian-livesync:open-review-harness" + ); + }); + if (!opened) throw new Error("The Review Harness command was not registered."); +} + +async function waitForHarness(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.locator('[data-testid="review-harness"]').waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); +} + +async function keepCompatibilityPaused(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const summary = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await summary.getByRole("button", { name: "Keep synchronisation paused" }).click({ timeout: uiTimeoutMs }); + await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); +} + +async function runAutomaticScenarios(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness.locator('[data-testid="review-harness-run-automatic"]').click({ timeout: uiTimeoutMs }); + for (const id of ["settings-lifecycle", "p2p-composition"]) { + await harness + .locator(`[data-testid="review-harness-result-${id}"]`) + .getByText("Passed:", { exact: false }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + }); +} + +async function runVaultFixture(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page + .locator('[data-testid="review-harness-run-vault-round-trip"]') + .click({ timeout: uiTimeoutMs }); + const confirmation = page.locator(".modal-container").filter({ + has: page.getByText("Review Harness: Vault fixture access", { exact: true }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + const screenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "review-harness-vault-confirmation.png", + async (page) => { + const confirmation = page.locator(".modal-container").filter({ + has: page.getByText("Review Harness: Vault fixture access", { exact: true }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertNoHorizontalOverflow(page, confirmation, { label: "Vault fixture confirmation" }); + } + ); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + const confirmation = page.locator(".modal-container").filter({ + has: page.getByText("Review Harness: Vault fixture access", { exact: true }), + }); + await confirmation.getByRole("button", { name: "Yes" }).click({ timeout: uiTimeoutMs }); + await harness + .locator('[data-testid="review-harness-result-vault-round-trip"]') + .getByText("Passed:", { exact: false }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const fixtureRemoved = await page.evaluate( + (root) => (globalThis as ReviewHarnessTestGlobal).app?.vault?.getAbstractFileByPath(root) === null, + REVIEW_HARNESS_FIXTURE_ROOT + ); + if (!fixtureRemoved) throw new Error("The Review Harness fixture root remained after the scenario."); + }); + return screenshot; +} + +async function restartAndResumeHarness(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness + .locator('[data-testid="review-harness-run-compatibility-review"]') + .click({ timeout: uiTimeoutMs }); + await harness + .locator('[data-testid="review-harness-result-compatibility-review"]') + .getByText("Waiting for review:", { exact: false }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await harness.locator('[data-testid="review-harness-restart"]').click({ timeout: uiTimeoutMs }); + }); + + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.waitForFunction( + () => { + const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) return false; + const core = (plugin as { core: { services: { appLifecycle: { isReady(): boolean } } } }).core; + return core.services.appLifecycle.isReady(); + }, + undefined, + { timeout: uiTimeoutMs * 2 } + ); + }); + await keepCompatibilityPaused(); + await waitForHarness(); + return await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "review-harness-resumed.png", + async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness + .locator('[data-testid="review-harness-resumed"]') + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const continuationRemoved = await page.evaluate((stateKey) => { + const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) { + throw new Error("Self-hosted LiveSync is unavailable after restart."); + } + const core = (plugin as { core: { services: { setting: { getSmallConfig(key: string): string } } } }) + .core; + return core.services.setting.getSmallConfig(stateKey) === ""; + }, REVIEW_HARNESS_STATE_KEY); + if (!continuationRemoved) throw new Error("The one-shot continuation was not removed before use."); + await assertNoHorizontalOverflow(page, harness, { label: "resumed Review Harness" }); + } + ); +} + +async function completeResumedCompatibilityStep(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness + .locator('[data-testid="review-harness-open-compatibility-review"]') + .click({ timeout: uiTimeoutMs }); + const summary = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await summary.getByRole("button", { name: "Resume synchronisation" }).click({ timeout: uiTimeoutMs }); + await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + await harness + .locator('[data-testid="review-harness-result-compatibility-review"]') + .getByText("The device-local compatibility pause was reviewed and cleared.", { exact: false }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); +} + +async function copyAndReadReport(): Promise { + return await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate(` + globalThis.reviewHarnessCopiedReport = undefined; + navigator.clipboard.writeText = function (value) { + globalThis.reviewHarnessCopiedReport = value; + return Promise.resolve(); + }; + `); + await page.locator('[data-testid="review-harness-copy-report"]').click({ timeout: uiTimeoutMs }); + await page.waitForFunction( + () => typeof (globalThis as ReviewHarnessTestGlobal).reviewHarnessCopiedReport === "string", + undefined, + { timeout: uiTimeoutMs } + ); + return await page.evaluate( + () => (globalThis as ReviewHarnessTestGlobal).reviewHarnessCopiedReport ?? "" + ); + }); +} + +async function verifyMobileHarness(): Promise { + await setObsidianMobileTestMode(obsidianRemoteDebuggingPort(), true, uiTimeoutMs); + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + if (await harness.isVisible()) return; + await page.evaluate(async (viewType) => { + const plugin = (globalThis as ReviewHarnessTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (typeof plugin !== "object" || plugin === null || !("core" in plugin)) { + throw new Error("Self-hosted LiveSync is unavailable in mobile test mode."); + } + const core = (plugin as { + core: { services: { API: { showWindow(type: string): Promise } } }; + }).core; + await core.services.API.showWindow(viewType); + }, "self-hosted-livesync-review-harness"); + }); + return await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "review-harness-mobile.png", + async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertNoHorizontalOverflow(page, harness, { label: "mobile Review Harness" }); + const heading = harness.getByRole("heading", { name: "Self-hosted LiveSync review harness" }); + await assertLocatorWithinSafeArea(page, heading, { + label: "mobile Review Harness heading", + safeAreaInsets: iPhoneSafeArea, + }); + for (const testId of [ + "review-harness-run-automatic", + "review-harness-run-full", + "review-harness-copy-report", + ]) { + await assertLocatorHasMinimumTouchTarget(page, harness.locator(`[data-testid="${testId}"]`), { + label: testId, + }); + } + } + ); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + const vault = await createTemporaryVault(); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary: cli.binary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: { + doctorProcessedVersion: "1.0.0", + settingVersion: CURRENT_SETTING_VERSION, + isConfigured: true, + additionalSuffixOfDatabaseName: "", + enableDebugTools: true, + notifyThresholdOfRemoteStorageSize: 0, + P2P_Enabled: false, + P2P_AutoStart: false, + liveSync: false, + syncOnSave: false, + syncOnEditorSave: true, + syncOnStart: false, + syncOnFileOpen: true, + syncAfterMerge: false, + periodicReplication: true, + }, + }); + await assertNoInterruptedStartupNotice("plug-in session start"); + try { + await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + } catch (error) { + await captureReadinessFailure(cli.binary, session, error).catch((diagnosticError: unknown) => { + console.error( + `Could not capture Review Harness readiness diagnostics: ${ + diagnosticError instanceof Error ? diagnosticError.message : String(diagnosticError) + }` + ); + }); + throw error; + } + await assertNoInterruptedStartupNotice("core readiness"); + await keepCompatibilityPaused(); + await openHarness(); + await waitForHarness(); + + const initialScreenshot = await captureObsidianDialogue( + obsidianRemoteDebuggingPort(), + "review-harness-initial.png", + async (page) => { + const harness = page.locator('[data-testid="review-harness"]'); + await harness.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertNoHorizontalOverflow(page, harness, { label: "Review Harness" }); + } + ); + + await runAutomaticScenarios(); + const vaultConfirmationScreenshot = await runVaultFixture(); + const resumedScreenshot = await restartAndResumeHarness(); + await completeResumedCompatibilityStep(); + const report = await copyAndReadReport(); + if (!report.includes("## Self-hosted LiveSync Review Harness report")) { + throw new Error("The copied Review Harness report was not Markdown evidence."); + } + for (const forbidden of [vault.name, REVIEW_HARNESS_FIXTURE_ROOT]) { + if (report.includes(forbidden)) throw new Error(`The Review Harness report exposed local state: ${forbidden}`); + } + + const mobileScreenshot = await verifyMobileHarness(); + console.log( + `Review Harness passed one-shot, fixture, report, and mobile checks. Screenshots: ${[ + initialScreenshot, + vaultConfirmationScreenshot, + resumedScreenshot, + mobileScreenshot, + ].join(", ")}` + ); + } finally { + if (session) await session.app.stop(); + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/revision-repair.ts b/test/e2e-obsidian/scripts/revision-repair.ts new file mode 100644 index 00000000..8dbab659 --- /dev/null +++ b/test/e2e-obsidian/scripts/revision-repair.ts @@ -0,0 +1,735 @@ +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + createE2eObsidianDeviceLocalState, + waitForLiveSyncCoreReady, + waitForLocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianElement, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; +import type { Locator, Page } from "playwright"; + +const path = "revision-repair.md"; +const healthyDeletedPath = "healthy-logical-deletion.md"; +const baseContent = "Revision repair\n\nShared base.\n"; +const branchContents = [ + `Revision repair\n\nLeft branch.\n${"L".repeat(4096)}\n`, + `Revision repair\n\nRight branch.\n${"R".repeat(4096)}\n`, +] as const; +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_REVISION_REPAIR_TIMEOUT_MS ?? 15000); + +type BrokenRevisionFixture = { + winnerRevision: string; + conflictRevision: string; + missingChunkId: string; +}; + +type RevisionTree = { + winnerRevision: string; + conflictRevisions: string[]; +}; + +type VaultWinnerState = { + matches: boolean; + winnerRevision: string; +}; + +type ObsidianSettingsController = { + open(): void; + openTabById(tabId: string): void; +}; + +type ObsidianTestGlobal = typeof globalThis & { + app?: { + setting?: ObsidianSettingsController; + }; +}; + +async function createAndOpenBaseFile(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(baseContent)};`, + "let file=app.vault.getAbstractFileByPath(path);", + "if(!file) file=await app.vault.create(path,content);", + "await app.workspace.getLeaf(false).openFile(file);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function createHealthyLogicalDeletion(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(healthyDeletedPath)};`, + `const content=${JSON.stringify(`Healthy logical deletion\n\n${"D".repeat(4096)}\n`)};`, + "let file=app.vault.getAbstractFileByPath(path);", + "if(!file) file=await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); + await waitForLocalDatabaseEntry(cliBinary, env, healthyDeletedPath); + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(healthyDeletedPath)};`, + `const timeoutMs=${JSON.stringify(uiTimeoutMs)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const file=app.vault.getAbstractFileByPath(path);", + "if(!file) throw new Error(`Logical-deletion fixture is missing from the Vault: ${path}`);", + "await app.vault.delete(file);", + "const id=await core.services.path.path2id(path);", + "const deadline=Date.now()+timeoutMs;", + "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));", + "while(Date.now()false);", + " if(!app.vault.getAbstractFileByPath(path)&&doc?.deleted&&(doc._conflicts??[]).length===0){", + " return JSON.stringify(doc._rev);", + " }", + " await sleep(250);", + "}", + "throw new Error(`Timed out waiting for a healthy logical deletion: ${path}`);", + "})()", + ].join(""), + env + ); +} + +async function createBrokenConflict( + cliBinary: string, + env: NodeJS.ProcessEnv, + baseRevision: string +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const baseRevision=${JSON.stringify(baseRevision)};`, + `const contents=${JSON.stringify(branchContents)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const id=await core.services.path.path2id(path);", + "for(const [index,content] of contents.entries()){", + " const blob=new Blob([content],{type:'text/plain'});", + " const now=Date.now()+index;", + " const result=await core.localDatabase.putDBEntry({", + " _id:id,path,data:blob,ctime:now,mtime:now,", + " size:(await blob.arrayBuffer()).byteLength,children:[],", + " datatype:'plain',type:'plain',eden:{},", + " },false,baseRevision);", + " if(!result?.ok) throw new Error(`Could not create repair conflict: ${path}`);", + "}", + "const tree=await core.localDatabase.localDatabase.get(id,{conflicts:true});", + "const conflictRevision=tree._conflicts?.[0];", + "if(!tree._rev||!conflictRevision){", + " throw new Error(`Repair fixture did not produce two live revisions: ${path}`);", + "}", + "const conflict=await core.localDatabase.localDatabase.get(id,{rev:conflictRevision});", + "const embedded=new Set(Object.keys(conflict.eden??{}));", + "const missingChunkId=(conflict.children??[]).find((child)=>!embedded.has(child));", + "if(!missingChunkId){", + " throw new Error(`Repair fixture did not create an independent chunk: ${conflictRevision}`);", + "}", + "const chunk=await core.localDatabase.localDatabase.get(missingChunkId);", + "await core.localDatabase.localDatabase.remove(chunk);", + "core.localDatabase.clearCaches();", + "const unreadable=await core.localDatabase.getDBEntry(path,{rev:conflictRevision},false,true,true);", + "if(unreadable!==false){", + " throw new Error(`The selected revision remained readable after its chunk was removed: ${conflictRevision}`);", + "}", + "return JSON.stringify({", + " winnerRevision:tree._rev,", + " conflictRevision,", + " missingChunkId,", + "});", + "})()", + ].join(""), + env + ); +} + +async function readRevisionTree(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const id=await core.services.path.path2id(path);", + "const tree=await core.localDatabase.localDatabase.get(id,{conflicts:true});", + "return JSON.stringify({", + " winnerRevision:tree._rev,", + " conflictRevisions:tree._conflicts??[],", + "});", + "})()", + ].join(""), + env + ); +} + +async function readVaultWinnerState(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const file=app.vault.getAbstractFileByPath(path);", + "if(!file) throw new Error(`Vault file is missing: ${path}`);", + "const entry=await core.localDatabase.getDBEntry(path,undefined,false,true,true);", + "if(!entry||!entry._rev) throw new Error(`Database winner is missing: ${path}`);", + "const vaultContent=await app.vault.read(file);", + "const data=Array.isArray(entry.data)?entry.data:[entry.data];", + "const databaseContent=await new Blob(data).text();", + "return JSON.stringify({", + " matches:vaultContent===databaseContent,", + " winnerRevision:entry._rev,", + "});", + "})()", + ].join(""), + env + ); +} + +async function readFileReflectionProvenance( + cliBinary: string, + env: NodeJS.ProcessEnv, + targetPath = path +): Promise<{ revision: string; observedStorageMtime?: number } | null> { + return await evalObsidianJson<{ revision: string; observedStorageMtime?: number } | null>( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(targetPath)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const store=core.services.keyValueDB.openSimpleStore('file-reflection-provenance-v1');", + "return JSON.stringify((await store.get(path))??null);", + "})()", + ].join(""), + env + ); +} + +function repairCard(settings: Locator): Locator { + return settings.locator(".sls-repair-result").filter({ hasText: path }); +} + +function revisionCard(settings: Locator, revision: string): Locator { + return repairCard(settings).locator(".sls-repair-revision").filter({ hasText: revision }); +} + +async function openRevisionActionMenu(page: Page, settings: Locator, revision: string): Promise { + const actionButton = revisionCard(settings, revision).getByRole("button", { + name: `More actions for revision ${revision}`, + exact: true, + }); + await actionButton.locator("svg.lucide-wrench").waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await actionButton.click({ timeout: uiTimeoutMs }); + const menu = page.locator(".menu:visible").last(); + await menu.waitFor({ state: "visible", timeout: uiTimeoutMs }); + const box = await menu.boundingBox(); + const viewport = await page.evaluate(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + if ( + box === null || + box.y < 0 || + box.y + box.height > viewport.height - 4 + ) { + throw new Error( + `Revision action menu is outside the viewport: ${JSON.stringify({ + box, + viewport, + })}` + ); + } + return menu; +} + +async function selectRevisionAction(page: Page, settings: Locator, revision: string, action: string): Promise { + const menu = await openRevisionActionMenu(page, settings, revision); + const item = menu.getByText(action, { exact: true }); + await item.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await item.click({ timeout: uiTimeoutMs }); +} + +async function requestConflictCheck(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "core.localDatabase.clearCaches();", + "await core.services.conflict.queueCheckFor(path);", + "await core.services.conflict.ensureAllProcessed();", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const cliBinary = cli.binary; + const vault = await createTemporaryVault("obsidian-livesync-revision-repair-"); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: { + doctorProcessedVersion: "1.0.0", + isConfigured: true, + liveSync: false, + remoteType: "", + couchDB_URI: "", + couchDB_DBNAME: "", + couchDB_USER: "", + couchDB_PASSWORD: "", + remoteConfigurations: {}, + activeConfigurationId: "", + notifyThresholdOfRemoteStorageSize: -1, + periodicReplication: false, + syncAfterMerge: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncOnSave: false, + syncOnStart: false, + disableMarkdownAutoMerge: true, + showMergeDialogOnlyOnActive: true, + useEden: false, + }, + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + }); + await waitForLiveSyncCoreReady(cliBinary, session.cliEnv); + await createAndOpenBaseFile(cliBinary, session.cliEnv); + const base = await waitForLocalDatabaseEntry(cliBinary, session.cliEnv, path); + const healthyDeletionRevision = await createHealthyLogicalDeletion(cliBinary, session.cliEnv); + const fixture = await createBrokenConflict(cliBinary, session.cliEnv, base.rev); + const healthyDeletionProvenance = await readFileReflectionProvenance( + cliBinary, + session.cliEnv, + healthyDeletedPath + ); + if (healthyDeletionProvenance !== null) { + throw new Error( + `A healthy logical deletion retained Vault provenance indefinitely: ${JSON.stringify({ + healthyDeletedPath, + healthyDeletionRevision, + healthyDeletionProvenance, + })}` + ); + } + + await requestConflictCheck(cliBinary, session.cliEnv); + const afterAutomaticCheck = await readRevisionTree(cliBinary, session.cliEnv); + if ( + afterAutomaticCheck.winnerRevision !== fixture.winnerRevision || + !afterAutomaticCheck.conflictRevisions.includes(fixture.conflictRevision) + ) { + throw new Error( + `Automatic conflict checking discarded the unreadable revision: ${JSON.stringify({ + fixture, + afterAutomaticCheck, + })}` + ); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const settings = page.locator(".sls-setting"); + await settings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await settings.locator('.sls-setting-menu-btn[title="Hatch"]').click({ timeout: uiTimeoutMs }); + const verifySetting = settings.locator(".setting-item").filter({ + has: page.getByText("Inspect conflicts and file/database differences", { + exact: true, + }), + }); + await verifySetting.getByRole("button", { name: "Begin inspection", exact: true }).click({ + timeout: uiTimeoutMs, + }); + const card = repairCard(settings); + await card.waitFor({ state: "visible", timeout: uiTimeoutMs }); + if ((await settings.locator(".sls-repair-result").filter({ hasText: healthyDeletedPath }).count()) !== 0) { + throw new Error( + `File/database inspection reported the healthy logical deletion ${healthyDeletedPath} (${healthyDeletionRevision}).` + ); + } + const winnerRevision = revisionCard(settings, fixture.winnerRevision); + const brokenRevision = revisionCard(settings, fixture.conflictRevision); + await brokenRevision + .getByText(/🧩 Missing chunks: 1/u) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await brokenRevision.getByText(fixture.missingChunkId, { exact: false }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + if ((await card.locator(".sls-repair-revision").count()) !== 2) { + throw new Error("Verify and Repair did not render the winner and conflict revision separately."); + } + for (const label of [ + /📦 DB: recorded/u, + /📁 Vault:/u, + /Δsize vs DB/u, + /🕒 DB /u, + /Δtime /u, + /⚠️ Differs from Vault/u, + ]) { + await winnerRevision.getByText(label).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + await brokenRevision.getByText(/decoded unavailable/u).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + const winnerMenu = await openRevisionActionMenu(page, settings, fixture.winnerRevision); + for (const label of [ + "Compare with Vault", + "Apply this revision to Vault", + "Store Vault file as a child of this revision", + "Discard this branch", + ]) { + await winnerMenu.getByText(label, { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + if ( + (await winnerMenu + .getByText("Mark this revision as the Vault version", { + exact: true, + }) + .count()) !== 0 + ) { + throw new Error("A differing revision incorrectly offered to record an exact Vault match."); + } + await page.keyboard.press("Escape"); + }); + + const repairCardScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-unreadable-conflict.png", + (page) => page.locator(".sls-repair-result").filter({ hasText: path }) + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const card = page.locator(".sls-repair-result").filter({ hasText: path }); + await card.evaluate((element) => { + const htmlElement = element as HTMLElement; + htmlElement.dataset.e2eOriginalStyle = htmlElement.getAttribute("style") ?? ""; + htmlElement.style.width = "360px"; + htmlElement.style.maxWidth = "100%"; + }); + const dimensions = await card.evaluate((element) => ({ + clientWidth: element.clientWidth, + scrollWidth: element.scrollWidth, + })); + if (dimensions.scrollWidth > dimensions.clientWidth + 1) { + throw new Error( + `Revision repair card overflowed at mobile width: ${JSON.stringify(dimensions)}` + ); + } + }); + const mobileWidthScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-mobile-width.png", + (page) => page.locator(".sls-repair-result").filter({ hasText: path }) + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const card = page.locator(".sls-repair-result").filter({ hasText: path }); + await card.evaluate((element) => { + const htmlElement = element as HTMLElement; + const originalStyle = htmlElement.dataset.e2eOriginalStyle ?? ""; + if (originalStyle.length > 0) { + htmlElement.setAttribute("style", originalStyle); + } else { + htmlElement.removeAttribute("style"); + } + delete htmlElement.dataset.e2eOriginalStyle; + }); + }); + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + await openRevisionActionMenu(page, settings, fixture.winnerRevision); + }); + const readableMenuScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-readable-actions.png", + (page) => page.locator(".menu:visible").last() + ); + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.keyboard.press("Escape"); + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.winnerRevision, "Compare with Vault"); + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Vault and database revision", + }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByText(path, { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal.getByText(/Vault file:/u).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal.getByText(/Database revision:/u).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + const actions = modal.locator(".conflict-action-container"); + await actions.getByRole("button", { name: "Close", exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + for (const action of ["Use Vault file", "Use Database revision", "Concat both", "Not now"]) { + if ((await actions.getByRole("button", { name: action, exact: true }).count()) !== 0) { + throw new Error(`Read-only comparison exposed the resolution action '${action}'.`); + } + } + }); + const comparisonScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-read-only-comparison.png", + (page) => + page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Vault and database revision", + }), + }) + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Vault and database revision", + }), + }); + await modal + .locator(".conflict-action-container") + .getByRole("button", { name: "Close", exact: true }) + .click({ timeout: uiTimeoutMs }); + await modal.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const beforeApply = await readRevisionTree(cliBinary, session.cliEnv); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.winnerRevision, "Apply this revision to Vault"); + const confirmation = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Apply database revision to Vault", + }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await confirmation.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); + await revisionCard(settings, fixture.winnerRevision) + .getByText("✅ Matches Vault", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + const status = repairCard(settings).locator(".sls-repair-status"); + await status + .getByText("✅ Vault matches winner", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await status + .getByText("⚠️ Conflicts: 1", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + const matchedWinnerWithConflictScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-winner-match-with-conflict.png", + (page) => page.locator(".sls-repair-result").filter({ hasText: path }) + ); + const afterApply = await readRevisionTree(cliBinary, session.cliEnv); + if (JSON.stringify(afterApply) !== JSON.stringify(beforeApply)) { + throw new Error( + `Applying a live revision to the Vault changed the revision tree: ${JSON.stringify({ + beforeApply, + afterApply, + })}` + ); + } + const vaultWinner = await readVaultWinnerState(cliBinary, session.cliEnv); + const appliedProvenance = await readFileReflectionProvenance(cliBinary, session.cliEnv); + if ( + !vaultWinner.matches || + vaultWinner.winnerRevision !== fixture.winnerRevision || + appliedProvenance?.revision !== fixture.winnerRevision + ) { + throw new Error( + `Applying the winner did not preserve exact Vault provenance: ${JSON.stringify({ + vaultWinner, + appliedProvenance, + fixture, + })}` + ); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + const menu = await openRevisionActionMenu(page, settings, fixture.winnerRevision); + await menu + .getByText("Mark this revision as the Vault version", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await menu + .getByText("Discard this branch", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await menu + .getByText("Mark this revision as the Vault version", { exact: true }) + .click({ timeout: uiTimeoutMs }); + await revisionCard(settings, fixture.winnerRevision) + .getByText("✅ Matches Vault", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + const afterExactMark = await readRevisionTree(cliBinary, session.cliEnv); + const markedProvenance = await readFileReflectionProvenance(cliBinary, session.cliEnv); + if ( + JSON.stringify(afterExactMark) !== JSON.stringify(beforeApply) || + markedProvenance?.revision !== fixture.winnerRevision + ) { + throw new Error( + `Recording an exact Vault match changed the tree or lost provenance: ${JSON.stringify({ + beforeApply, + afterExactMark, + markedProvenance, + })}` + ); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + const menu = await openRevisionActionMenu(page, settings, fixture.conflictRevision); + for (const label of [ + "Store Vault file as a child of this revision", + "Retry reading revision", + "Discard this branch", + ]) { + await menu.getByText(label, { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + } + }); + const unreadableMenuScreenshot = await captureObsidianElement( + session.remoteDebuggingPort, + "revision-repair-unreadable-actions-context.png", + (page) => page.locator("body") + ); + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + await page.keyboard.press("Escape"); + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.conflictRevision, "Retry reading revision"); + await revisionCard(settings, fixture.conflictRevision) + .getByText(/🧩 Missing chunks:/u) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + + const afterRetry = await readRevisionTree(cliBinary, session.cliEnv); + if (JSON.stringify(afterRetry) !== JSON.stringify(beforeApply)) { + throw new Error(`Retry changed the revision tree: ${JSON.stringify(afterRetry)}`); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.conflictRevision, "Discard this branch"); + const confirmation = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Discard branch" }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await confirmation.getByRole("button", { name: "No", exact: true }).click({ timeout: uiTimeoutMs }); + await confirmation.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const afterCancellation = await readRevisionTree(cliBinary, session.cliEnv); + if (JSON.stringify(afterCancellation) !== JSON.stringify(beforeApply)) { + throw new Error(`Cancelling discard changed the revision tree: ${JSON.stringify(afterCancellation)}`); + } + + await withObsidianPage(session.remoteDebuggingPort, async (page) => { + const settings = page.locator(".sls-setting"); + await selectRevisionAction(page, settings, fixture.conflictRevision, "Discard this branch"); + const confirmation = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Discard branch" }), + }); + await confirmation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await confirmation.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); + await repairCard(settings).waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); + + const afterDiscard = await readRevisionTree(cliBinary, session.cliEnv); + if (afterDiscard.winnerRevision !== fixture.winnerRevision || afterDiscard.conflictRevisions.length !== 0) { + throw new Error( + `Explicit discard did not remove only the selected unreadable revision: ${JSON.stringify({ + fixture, + afterDiscard, + })}` + ); + } + const finalVaultWinner = await readVaultWinnerState(cliBinary, session.cliEnv); + const finalProvenance = await readFileReflectionProvenance(cliBinary, session.cliEnv); + if ( + !finalVaultWinner.matches || + finalVaultWinner.winnerRevision !== fixture.winnerRevision || + finalProvenance?.revision !== fixture.winnerRevision + ) { + throw new Error( + `Discarding the unreadable branch disturbed the healthy Vault reflection: ${JSON.stringify({ + finalVaultWinner, + finalProvenance, + fixture, + })}` + ); + } + + console.log( + "Real Obsidian omitted a healthy logical deletion; rendered each live revision with compact actions and diagnostics; showed that the Vault matched the winner while one conflict remained; compared and applied an exact readable revision without changing the tree; preserved Vault provenance; kept an unreadable branch through automatic checking, retry, and cancelled discard; and discarded only the selected branch after confirmation." + ); + console.log(`Repair card screenshot: ${repairCardScreenshot}`); + console.log(`Mobile-width repair card screenshot: ${mobileWidthScreenshot}`); + console.log(`Readable revision actions screenshot: ${readableMenuScreenshot}`); + console.log(`Read-only comparison screenshot: ${comparisonScreenshot}`); + console.log(`Matching winner with conflict screenshot: ${matchedWinnerWithConflictScreenshot}`); + console.log(`Unreadable revision actions screenshot: ${unreadableMenuScreenshot}`); + } finally { + if (session) { + await session.app.stop(); + } + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/run-focused.ts b/test/e2e-obsidian/scripts/run-focused.ts new file mode 100644 index 00000000..a46bc515 --- /dev/null +++ b/test/e2e-obsidian/scripts/run-focused.ts @@ -0,0 +1,89 @@ +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +// Keep the public wrapper deliberately narrower than package.json. Discovery, +// installation, runner contracts, and the complete suite have different setup +// requirements and remain separate entry points. +const focusedScenarios = new Set([ + "smoke", + "onboarding-invitation", + "dialog-mounts", + "revision-repair", + "settings-ui", + "review-harness", + "p2p-pane", + "vault-reflection", + "couchdb-upload", + "couchdb-manual-setup-workflow", + "cli-to-obsidian-sync", + "minio-upload", + "object-storage-setup-uri-workflow", + "p2p-setup-uri-workflow", + "startup-scan", + "setup-uri-workflow", + "two-vault-sync", + "security-seed-reconnect", + "hidden-file-snippet-sync", + "customisation-sync", + "setting-markdown-export", + "upgrade-from-stable", +]); + +function usage(): string { + return `Usage: npm run test:e2e:obsidian:focused -- [scenario arguments] + +Builds the current Self-hosted LiveSync plug-in before running one maintained +real-Obsidian scenario. Supported scenarios: + +${[...focusedScenarios].map((scenario) => ` ${scenario}`).join("\n")} + +This wrapper does not start CouchDB, Object Storage, or the P2P signalling +relay. Use the documented service commands or the complete +local-suite:services wrapper when required.`; +} + +// npm receives each argument directly. In particular, environment values and +// scenario arguments never pass through a shell for re-interpretation. +function runNpm(args: string[]): void { + const npm = process.platform === "win32" ? "npm.cmd" : "npm"; + const result = spawnSync(npm, args, { + cwd: fileURLToPath(new URL("../../..", import.meta.url)), + stdio: "inherit", + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`npm ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}.`); + } +} + +function main(): void { + const [scenario, ...scenarioArguments] = process.argv.slice(2); + if (!scenario || scenario === "-h" || scenario === "--help") { + process.stdout.write(`${usage()}\n`); + return; + } + if (!focusedScenarios.has(scenario)) { + throw new Error(`Unsupported focused real-Obsidian scenario: ${scenario}\n\n${usage()}`); + } + + // Individual scenario scripts intentionally remain fast, raw entry points. + // The wrapper owns the freshness guarantee which was previously easy to + // miss after changing TypeScript source. + runNpm(["run", "build"]); + + // The compatibility scenario defaults to the repository CLI. Build it only + // when the caller has not selected an external CLI distribution. + if (scenario === "cli-to-obsidian-sync" && !process.env.LIVESYNC_CLI_COMMAND) { + runNpm(["run", "build", "--workspace", "self-hosted-livesync-cli"]); + } + + const script = `test:e2e:obsidian:${scenario}`; + runNpm(["run", script, ...(scenarioArguments.length > 0 ? ["--", ...scenarioArguments] : [])]); +} + +try { + main(); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +} diff --git a/test/e2e-obsidian/scripts/security-seed-reconnect.ts b/test/e2e-obsidian/scripts/security-seed-reconnect.ts new file mode 100644 index 00000000..d75ffc21 --- /dev/null +++ b/test/e2e-obsidian/scripts/security-seed-reconnect.ts @@ -0,0 +1,768 @@ +/** + * Provides release evidence for the Security Seed refresh behaviour shared by + * supported platforms in real Obsidian. It verifies that an already-open + * device keeps its deliberately stale cached Seed until replication, refreshes + * from the managed CouchDB fixture before encrypting, and never restores the + * old Seed to the remote synchronisation-parameter document. + * + * The scenario uses isolated Vaults, profiles, and a random database because + * settings, the local database, the renderer process, and CouchDB must all + * participate in the result. Device A is restarted with the same Vault and + * profile, while device B is fresh. The devices run sequentially after the + * same-process stale-cache assertion because desktop Obsidian may enforce a + * single application instance; running them concurrently would test launcher + * behaviour rather than LiveSync's shared plug-in implementation. + * + * Seed replacement, A-to-B decryption, B-to-A return synchronisation, final + * remote-document comparison, error-log inspection, screenshots, and strict + * teardown remain one scenario. Together they prove that the same replacement + * Seed was used across the complete encrypted round trip and was not later + * rolled back. Independent passing checks would not establish that continuity. + * The result records fingerprints only and does not claim to cover an + * iPadOS-specific background or reconnect lifecycle. + */ +import { execFileSync } from "node:child_process"; +import { createHash, randomUUID } from "node:crypto"; +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + assertCouchDbReachable, + couchDbDatabaseExists, + createCouchDbDatabase, + deleteCouchDbDatabase, + fetchAllCouchDbDocs, + fetchCouchDbDocument, + loadCouchDbConfig, + makeUniqueDatabaseName, + putCouchDbDocument, + waitForCouchDbDocs, + type CouchDbConfig, + type CouchDbDocument, +} from "../runner/couchdb.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, + prepareRemote, + pushLocalChanges, + waitForLiveSyncCoreReady, + waitForLocalDatabaseEntry, + type LocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { + SECURITY_SEED_DOCUMENT_ID, + changedSynchronisationParameterFields, + createSecuritySeed, + fingerprintSecuritySeed, + replaceSecuritySeed, + requireSecuritySeedDocument, + snapshotSecuritySeedDocument, + type SecuritySeedDocument, + type SecuritySeedDocumentSnapshot, +} from "../runner/securitySeed.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000"; +process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "20000"; +process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ??= "15000"; + +const outboundPath = "E2E/security-seed/device-a.md"; +const returnPath = "E2E/security-seed/device-b.md"; +const hkdfErrorMessages = [ + "Encryption with HKDF failed", + "Decryption with HKDF failed", + "Failed to initialise the encryption key", + "Failed to obtain PBKDF2 salt", +] as const; + +type RunnerContext = { + binary: string; + cliBinary: string; + artifactRoot: string; + couchDb: CouchDbConfig; + dbName: string; + activeSessions: Set; + allSessions: ObsidianLiveSyncSession[]; + screenshots: string[]; +}; + +type DeviceLabel = "device-a" | "device-a-return" | "device-b"; + +type SourceEvidence = { + exactCommit: string; + revisionSource: "git-worktree" | "provided-artifact"; + workingTreeClean: boolean | null; + pluginVersion: string; + pluginArtifactSha256: string; +}; + +type ReplicationSettingsState = { + liveSync: boolean; + syncOnStart: boolean; + syncOnSave: boolean; + periodicReplication: boolean; + syncOnFileOpen: boolean; + syncOnEditorSave: boolean; +}; + +type SessionHealth = { + matchingErrorMessages: string[]; +}; + +type ScenarioEvidence = { + source: SourceEvidence; + securitySeed: { + initial: SecuritySeedDocumentSnapshot; + replacement: SecuritySeedDocumentSnapshot; + final: SecuritySeedDocumentSnapshot; + cachedBeforeReplacement: string; + cachedAfterRemoteReplacement: string; + cachedAfterReplication: string; + replacementChangedFields: string[]; + finalChangedFields: string[]; + }; + synchronisation: { + deviceAToDeviceB: boolean; + deviceBToDeviceA: boolean; + deviceAEncryptedPayload: boolean; + deviceBEncryptedPayload: boolean; + }; + health: { + deviceA: SessionHealth; + deviceB: SessionHealth; + }; + screenshots: string[]; +}; + +type TeardownEvidence = { + sessionsStopped: boolean; + vaultRemoved: boolean; + profileRemoved: boolean; + databaseRemoved: boolean; + remainingTrackedSessions: number; +}; + +class MultipleErrors extends Error { + readonly errors: unknown[]; + + constructor(message: string, errors: unknown[]) { + super(message); + this.name = "MultipleErrors"; + this.errors = errors; + } +} + +function assertEqual(actual: unknown, expected: unknown, message: string): void { + if (actual !== expected) { + throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`); + } +} + +function inspectGitRevision( + artifactRoot: string +): Pick { + try { + const exactCommit = execFileSync("git", ["-C", artifactRoot, "rev-parse", "HEAD"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + const status = execFileSync("git", ["-C", artifactRoot, "status", "--porcelain"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + return { + exactCommit, + revisionSource: "git-worktree", + workingTreeClean: status.length === 0, + }; + } catch { + const exactCommit = process.env.E2E_OBSIDIAN_ARTIFACT_REVISION?.trim(); + if (!exactCommit) { + throw new Error( + "E2E_OBSIDIAN_ARTIFACT_REVISION is required when the plug-in artefact is not in a Git worktree." + ); + } + return { + exactCommit, + revisionSource: "provided-artifact", + workingTreeClean: null, + }; + } +} + +async function inspectSourceEvidence(artifactRoot: string): Promise { + const manifest = JSON.parse(await readFile(join(artifactRoot, "manifest.json"), "utf-8")) as { + version?: unknown; + }; + if (typeof manifest.version !== "string" || manifest.version.length === 0) { + throw new Error("The plug-in manifest does not have a version."); + } + const mainJs = await readFile(join(artifactRoot, "main.js")); + return { + ...inspectGitRevision(artifactRoot), + pluginVersion: manifest.version, + pluginArtifactSha256: createHash("sha256").update(Uint8Array.from(mainJs)).digest("hex"), + }; +} + +function e2eeSettings(passphrase: string): Record { + return { + encrypt: true, + passphrase, + usePathObfuscation: true, + E2EEAlgorithm: "v2", + }; +} + +async function captureStage(context: RunnerContext, session: ObsidianLiveSyncSession, filename: string): Promise { + const screenshot = await captureObsidianPage(session.remoteDebuggingPort, filename, async () => undefined); + context.screenshots.push(screenshot); + console.log(`Security Seed E2E screenshot: ${screenshot}`); +} + +async function startConfiguredSession( + context: RunnerContext, + vault: TemporaryVault, + passphrase: string, + deviceLabel: DeviceLabel +): Promise { + const couchDbSettings = { + uri: context.couchDb.uri, + username: context.couchDb.username, + password: context.couchDb.password, + dbName: context.dbName, + }; + const overrides = e2eeSettings(passphrase); + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + artifactRoot: context.artifactRoot, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData(couchDbSettings, overrides), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), + }); + context.activeSessions.add(session); + context.allSessions.push(session); + try { + await captureStage(context, session, `security-seed-${deviceLabel}-startup.png`); + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + await prepareRemote(context.cliBinary, session.cliEnv); + await captureStage(context, session, `security-seed-${deviceLabel}-configured.png`); + return session; + } catch (error) { + await captureStage(context, session, `security-seed-${deviceLabel}-setup-failure.png`).catch(() => undefined); + await stopTrackedSession(context, session); + throw error; + } +} + +async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) { + return; + } + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function stopTrackedSessions(context: RunnerContext): Promise { + const errors: unknown[] = []; + for (const session of [...context.activeSessions]) { + try { + await stopTrackedSession(context, session); + } catch (error) { + errors.push(error); + } + } + if (errors.length > 0) { + throw new MultipleErrors("Could not stop every Real Obsidian session.", errors); + } +} + +async function pauseAutomaticReplication(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + const state = await evalObsidianJson( + cliBinary, + [ + "(()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "core.services.replicator.getActiveReplicator()?.closeReplication();", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "liveSync:Boolean(settings.liveSync),", + "syncOnStart:Boolean(settings.syncOnStart),", + "syncOnSave:Boolean(settings.syncOnSave),", + "periodicReplication:Boolean(settings.periodicReplication),", + "syncOnFileOpen:Boolean(settings.syncOnFileOpen),", + "syncOnEditorSave:Boolean(settings.syncOnEditorSave),", + "});", + "})()", + ].join(""), + env + ); + for (const [name, enabled] of Object.entries(state)) { + if (enabled) { + throw new Error(`Automatic replication remained enabled through ${name}.`); + } + } + return state; +} + +async function cachedSecuritySeedFingerprint(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + const result = await evalObsidianJson<{ fingerprint: string }>( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "const replicator=core.services.replicator.getActiveReplicator();", + "const seed=await replicator.getReplicationPBKDF2Salt(settings,false);", + "const digest=await crypto.subtle.digest('SHA-256',seed);", + "const fingerprint='sha256:'+Array.from(new Uint8Array(digest))", + ".map((value)=>value.toString(16).padStart(2,'0')).join('').slice(0,16);", + "return JSON.stringify({fingerprint});", + "})()", + ].join(""), + env + ); + return result.fingerprint; +} + +async function writeNoteViaObsidian( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content);", + "else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function openNoteViaObsidian(cliBinary: string, env: NodeJS.ProcessEnv, path: string): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const file=app.vault.getAbstractFileByPath(path);", + "if(!file) throw new Error(`Could not find note to open: ${path}`);", + "await app.workspace.getLeaf(false).openFile(file);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + env + ); +} + +async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return false; + } + throw error; + } +} + +async function waitForPathContent( + vaultPath: string, + path: string, + expected: string, + timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 15000) +): Promise { + const fullPath = join(vaultPath, path); + const deadline = Date.now() + timeoutMs; + let lastContent = ""; + while (Date.now() < deadline) { + if (await pathExists(fullPath)) { + lastContent = await readFile(fullPath, "utf-8"); + if (lastContent === expected) { + return; + } + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +function remoteContainsEntry(documents: CouchDbDocument[], entry: LocalDatabaseEntry): boolean { + const ids = new Set(documents.map((document) => document._id)); + return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId)); +} + +async function assertEntryNotRemote(context: RunnerContext, entry: LocalDatabaseEntry): Promise { + const response = await fetchAllCouchDbDocs(context.couchDb, context.dbName); + const documents = response.rows.flatMap((row) => (row.doc ? [row.doc] : [])); + if (remoteContainsEntry(documents, entry)) { + throw new Error("The pending device-A document reached CouchDB before the Security Seed replacement."); + } +} + +async function waitForEncryptedRemoteEntry(context: RunnerContext, entry: LocalDatabaseEntry): Promise { + const documents = await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => + remoteContainsEntry(docs, entry) + ); + const byId = new Map(documents.map((document) => [document._id, document])); + const encrypted = entry.children.every((childId) => { + const data = byId.get(childId)?.data; + return typeof data === "string" && data.startsWith("%="); + }); + if (!encrypted) { + throw new Error("A replicated chunk did not use the expected HKDF-encrypted payload format."); + } + return true; +} + +async function inspectSessionHealth(cliBinary: string, env: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const patterns=${JSON.stringify(hkdfErrorMessages)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "await core.services.API.showWindow('log-log');", + "const sleep=(ms)=>new Promise((resolve)=>setTimeout(resolve,ms));", + "let text='';", + "for(let i=0;i<20;i++){", + "text=Array.from(document.querySelectorAll('.logpane .log pre'))", + ".map((element)=>element.textContent??'').join('\\n');", + "if(text.length>0) break;", + "await sleep(50);", + "}", + "const unresolved=JSON.stringify((await core.services.appLifecycle.getUnresolvedMessages()).flat());", + "const matchingErrorMessages=patterns.filter((pattern)=>text.includes(pattern)||unresolved.includes(pattern));", + "for(const leaf of app.workspace.getLeavesOfType('log-log')) leaf.detach();", + "return JSON.stringify({matchingErrorMessages});", + "})()", + ].join(""), + env + ); +} + +async function fetchSecuritySeedDocument(context: RunnerContext): Promise { + return requireSecuritySeedDocument( + await fetchCouchDbDocument(context.couchDb, context.dbName, SECURITY_SEED_DOCUMENT_ID) + ); +} + +async function replaceRemoteSecuritySeed( + context: RunnerContext, + before: SecuritySeedDocument, + replacementSeed: string +): Promise { + const replacement = replaceSecuritySeed(before, replacementSeed); + const putResult = await putCouchDbDocument(context.couchDb, context.dbName, replacement); + const after = await fetchSecuritySeedDocument(context); + assertEqual(after._rev, putResult.rev, "The replacement Security Seed revision was not stored."); + assertEqual( + fingerprintSecuritySeed(after.pbkdf2salt), + fingerprintSecuritySeed(replacementSeed), + "The replacement Security Seed was not stored." + ); + const changedFields = changedSynchronisationParameterFields(before, after); + assertEqual( + JSON.stringify(changedFields), + JSON.stringify(["pbkdf2salt"]), + "Replacing the remote Security Seed changed another synchronisation parameter." + ); + return after; +} + +async function runScenario( + context: RunnerContext, + vaultA: TemporaryVault, + vaultB: TemporaryVault +): Promise { + const passphrase = `security-seed-e2e-${randomUUID()}`; + const source = await inspectSourceEvidence(context.artifactRoot); + let sessionA = await startConfiguredSession(context, vaultA, passphrase, "device-a"); + + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + await captureStage(context, sessionA, "security-seed-device-a-initial-sync.png"); + const initialDocument = await fetchSecuritySeedDocument(context); + const initial = snapshotSecuritySeedDocument(initialDocument); + const cachedBeforeReplacement = await cachedSecuritySeedFingerprint(context.cliBinary, sessionA.cliEnv); + assertEqual( + cachedBeforeReplacement, + initial.fingerprint, + "Device A did not cache the initial remote Security Seed." + ); + + await pauseAutomaticReplication(context.cliBinary, sessionA.cliEnv); + const outboundContent = `Encrypted from device A: ${randomUUID()}\n`; + await writeNoteViaObsidian(context.cliBinary, sessionA.cliEnv, outboundPath, outboundContent); + const outboundEntry = await waitForLocalDatabaseEntry(context.cliBinary, sessionA.cliEnv, outboundPath); + await assertEntryNotRemote(context, outboundEntry); + + const replacementSeed = createSecuritySeed(); + const replacementDocument = await replaceRemoteSecuritySeed(context, initialDocument, replacementSeed); + const replacement = snapshotSecuritySeedDocument(replacementDocument); + await openNoteViaObsidian(context.cliBinary, sessionA.cliEnv, outboundPath); + await captureStage(context, sessionA, "security-seed-device-a-replacement-pending.png"); + const cachedAfterRemoteReplacement = await cachedSecuritySeedFingerprint(context.cliBinary, sessionA.cliEnv); + assertEqual( + cachedAfterRemoteReplacement, + initial.fingerprint, + "Device A did not retain the deliberately stale Security Seed before replication." + ); + assertEqual( + replacement.fingerprint, + fingerprintSecuritySeed(replacementSeed), + "The runner did not install the intended replacement Security Seed." + ); + + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + const cachedAfterReplication = await cachedSecuritySeedFingerprint(context.cliBinary, sessionA.cliEnv); + assertEqual( + cachedAfterReplication, + replacement.fingerprint, + "Device A did not refresh the Security Seed before replication." + ); + const deviceAEncryptedPayload = await waitForEncryptedRemoteEntry(context, outboundEntry); + await captureStage(context, sessionA, "security-seed-device-a-refreshed-sync.png"); + const deviceAHealthBeforeRestart = await inspectSessionHealth(context.cliBinary, sessionA.cliEnv); + await stopTrackedSession(context, sessionA); + + const sessionB = await startConfiguredSession(context, vaultB, passphrase, "device-b"); + await pushLocalChanges(context.cliBinary, sessionB.cliEnv); + await waitForPathContent(vaultB.path, outboundPath, outboundContent); + await openNoteViaObsidian(context.cliBinary, sessionB.cliEnv, outboundPath); + await captureStage(context, sessionB, "security-seed-device-b-received.png"); + + await pauseAutomaticReplication(context.cliBinary, sessionB.cliEnv); + const returnContent = `Encrypted from device B: ${randomUUID()}\n`; + await writeNoteViaObsidian(context.cliBinary, sessionB.cliEnv, returnPath, returnContent); + const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, sessionB.cliEnv, returnPath); + await pushLocalChanges(context.cliBinary, sessionB.cliEnv); + const deviceBEncryptedPayload = await waitForEncryptedRemoteEntry(context, returnEntry); + const deviceBHealth = await inspectSessionHealth(context.cliBinary, sessionB.cliEnv); + await stopTrackedSession(context, sessionB); + + sessionA = await startConfiguredSession(context, vaultA, passphrase, "device-a-return"); + await pushLocalChanges(context.cliBinary, sessionA.cliEnv); + await waitForPathContent(vaultA.path, returnPath, returnContent); + await openNoteViaObsidian(context.cliBinary, sessionA.cliEnv, returnPath); + await captureStage(context, sessionA, "security-seed-device-a-return-received.png"); + + const finalDocument = await fetchSecuritySeedDocument(context); + const final = snapshotSecuritySeedDocument(finalDocument); + assertEqual( + final.fingerprint, + replacement.fingerprint, + "A client rolled the remote Security Seed back after reconnecting." + ); + const finalChangedFields = changedSynchronisationParameterFields(replacementDocument, finalDocument); + if (finalChangedFields.length > 0) { + throw new Error( + `A client rewrote unexpected synchronisation-parameter fields: ${finalChangedFields.join(", ")}` + ); + } + + const deviceAHealthAfterRestart = await inspectSessionHealth(context.cliBinary, sessionA.cliEnv); + const deviceAHealth = { + matchingErrorMessages: [ + ...new Set([ + ...deviceAHealthBeforeRestart.matchingErrorMessages, + ...deviceAHealthAfterRestart.matchingErrorMessages, + ]), + ], + }; + if (deviceAHealth.matchingErrorMessages.length > 0 || deviceBHealth.matchingErrorMessages.length > 0) { + throw new Error( + `HKDF or Security Seed errors were logged: ${JSON.stringify({ + deviceA: deviceAHealth.matchingErrorMessages, + deviceB: deviceBHealth.matchingErrorMessages, + })}` + ); + } + + return { + source, + securitySeed: { + initial, + replacement, + final, + cachedBeforeReplacement, + cachedAfterRemoteReplacement, + cachedAfterReplication, + replacementChangedFields: changedSynchronisationParameterFields(initialDocument, replacementDocument), + finalChangedFields, + }, + synchronisation: { + deviceAToDeviceB: true, + deviceBToDeviceA: true, + deviceAEncryptedPayload, + deviceBEncryptedPayload, + }, + health: { + deviceA: deviceAHealth, + deviceB: deviceBHealth, + }, + screenshots: [...context.screenshots], + }; +} + +async function cleanupResources( + context: RunnerContext, + vaults: TemporaryVault[], + databaseCreated: boolean +): Promise { + const errors: unknown[] = []; + try { + await stopTrackedSessions(context); + } catch (error) { + errors.push(error); + } + for (const vault of vaults) { + try { + await vault.dispose(); + } catch (error) { + errors.push(error); + } + } + if (databaseCreated) { + try { + await deleteCouchDbDatabase(context.couchDb, context.dbName); + } catch (error) { + errors.push(error); + } + } + + const sessionsStopped = context.allSessions.every( + (session) => session.app.process.exitCode !== null || session.app.process.signalCode !== null + ); + const vaultRemoved = (await Promise.all(vaults.map(async (vault) => !(await pathExists(vault.path))))).every( + Boolean + ); + const profileRemoved = (await Promise.all(vaults.map(async (vault) => !(await pathExists(vault.statePath))))).every( + Boolean + ); + let databaseRemoved = !databaseCreated; + if (databaseCreated) { + try { + databaseRemoved = !(await couchDbDatabaseExists(context.couchDb, context.dbName)); + } catch (error) { + errors.push(error); + } + } + const evidence = { + sessionsStopped, + vaultRemoved, + profileRemoved, + databaseRemoved, + remainingTrackedSessions: context.activeSessions.size, + }; + if (!sessionsStopped || !vaultRemoved || !profileRemoved || !databaseRemoved || context.activeSessions.size > 0) { + errors.push(new Error(`Security Seed E2E teardown was incomplete: ${JSON.stringify(evidence)}`)); + } + if (errors.length > 0) { + throw Object.assign(new MultipleErrors("Security Seed E2E teardown failed.", errors), { + evidence, + }); + } + return evidence; +} + +async function writeResult(result: unknown): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + const resultPath = join(outputDirectory, "security-seed-reconnect-result.json"); + await mkdir(outputDirectory, { recursive: true }); + await writeFile(resultPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8"); + return resultPath; +} + +async function main(): Promise { + if (process.env.E2E_OBSIDIAN_KEEP_VAULT === "true" || process.env.E2E_OBSIDIAN_KEEP_COUCHDB === "true") { + throw new Error("The Security Seed reconnect scenario requires strict Vault, profile, and database cleanup."); + } + + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const artifactRoot = resolve(process.env.E2E_OBSIDIAN_ARTIFACT_ROOT ?? process.cwd()); + const couchDb = await loadCouchDbConfig(); + const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "security-seed-reconnect"); + const context: RunnerContext = { + binary, + cliBinary: cli.binary, + artifactRoot, + couchDb, + dbName, + activeSessions: new Set(), + allSessions: [], + screenshots: [], + }; + const vaults: TemporaryVault[] = []; + let databaseCreated = false; + let evidence: ScenarioEvidence | undefined; + let scenarioError: unknown; + let teardown: TeardownEvidence | undefined; + let teardownError: unknown; + + try { + await assertCouchDbReachable(couchDb); + await createCouchDbDatabase(couchDb, dbName); + databaseCreated = true; + vaults.push(await createTemporaryVault("obsidian-livesync-security-seed-a-")); + vaults.push(await createTemporaryVault("obsidian-livesync-security-seed-b-")); + evidence = await runScenario(context, vaults[0], vaults[1]); + } catch (error) { + scenarioError = error; + } finally { + try { + teardown = await cleanupResources(context, vaults, databaseCreated); + } catch (error) { + teardownError = error; + } + } + + if (scenarioError !== undefined || teardownError !== undefined) { + const errors = [scenarioError, teardownError].filter((error) => error !== undefined); + if (errors.length === 1) { + throw errors[0]; + } + throw new MultipleErrors("Security Seed reconnect scenario and teardown both failed.", errors); + } + if (!evidence || !teardown) { + throw new Error("Security Seed reconnect evidence was not produced."); + } + + const result = { + scenario: "security-seed-reconnect", + ...evidence, + teardown, + limitations: { + platformCommonRealObsidian: true, + iPadOsBackgroundReconnect: false, + androidDeviceLifecycle: false, + }, + }; + const resultPath = await writeResult(result); + console.log(`Security Seed E2E result: ${resultPath}`); + console.log(JSON.stringify(result, null, 2)); +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exitCode = 1; +}); diff --git a/test/e2e-obsidian/scripts/settings-ui.ts b/test/e2e-obsidian/scripts/settings-ui.ts new file mode 100644 index 00000000..93b85786 --- /dev/null +++ b/test/e2e-obsidian/scripts/settings-ui.ts @@ -0,0 +1,294 @@ +import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts"; +import { assertMobileDialogueLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { captureObsidianDialogue, obsidianRemoteDebuggingPort, withObsidianPage } from "../runner/ui.ts"; +import { createTemporaryVault } from "../runner/vault.ts"; + +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS ?? 10000); +const compatibilityReviewMessage = "Review the internal database compatibility change before synchronisation resumes."; + +type ObsidianSettingsController = { + open(): void; + openTabById(tabId: string): void; +}; + +type LiveSyncTestPlugin = { + core: { + services: { + setting: { + currentSettings(): { versionUpFlash: string }; + getSmallConfig(key: string): string | null; + }; + }; + }; +}; + +type ObsidianTestApp = { + setting?: ObsidianSettingsController; + plugins?: { plugins: Record }; +}; + +type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp }; + +async function verifyCompatibilityReview(): Promise { + const port = obsidianRemoteDebuggingPort(); + const summaryScreenshot = await captureObsidianDialogue(port, "compatibility-review-summary.png", async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByText("Your automatic synchronisation preferences have not been changed.", { exact: false }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Review compatibility details" }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { + name: "Resume synchronisation", + }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByRole("button", { name: "Keep synchronisation paused" }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + + await withObsidianPage(port, async (page) => { + const markerBeforeAcknowledgement = await page.evaluate(() => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable"); + return plugin.core.services.setting.getSmallConfig("database-compatibility-version"); + }); + if (markerBeforeAcknowledgement !== null && markerBeforeAcknowledgement !== "") { + throw new Error( + `The database version was marked as acknowledged before review: ${markerBeforeAcknowledgement}` + ); + } + }); + + await setObsidianMobileTestMode(port, true, uiTimeoutMs); + const mobileSummaryScreenshot = await captureObsidianDialogue( + port, + "compatibility-review-summary-mobile.png", + async (page) => { + const summary = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await assertMobileDialogueLayout(page, summary, "compatibility review summary"); + const doctor = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Self-hosted LiveSync Config Doctor" }), + }); + if (await doctor.isVisible()) { + throw new Error("Config Doctor must wait until the initial compatibility review has closed."); + } + } + ); + + await withObsidianPage(port, async (page) => { + const summary = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await summary.getByRole("button", { name: "Review compatibility details" }).click(); + }); + + const detailsScreenshot = await captureObsidianDialogue( + port, + "compatibility-review-details-mobile.png", + async (page) => { + const modal = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }), + }); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal.getByText("Why synchronisation is paused", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal.getByText("Remote replication is blocked before work begins.", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await modal + .getByRole("button", { name: "Back to compatibility review" }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + if ((await modal.getByRole("button", { name: "Keep synchronisation paused" }).count()) !== 0) { + throw new Error("The explanatory details dialogue must not make the pause decision."); + } + await assertMobileDialogueLayout(page, modal, "compatibility review details"); + } + ); + + await withObsidianPage(port, async (page) => { + const details = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Compatibility review details" }), + }); + await details.getByRole("button", { name: "Back to compatibility review" }).click(); + const summary = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await summary.waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + + await setObsidianMobileTestMode(port, false, uiTimeoutMs); + await withObsidianPage(port, async (page) => { + const summary = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ + hasText: "Synchronisation paused for compatibility review", + }), + }); + await summary + .getByRole("button", { + name: "Resume synchronisation", + }) + .click(); + await summary.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + await page.waitForFunction( + (expectedVersion) => { + const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"]; + if (plugin === undefined) return false; + const setting = plugin.core.services.setting; + return ( + setting.getSmallConfig("database-compatibility-version") === expectedVersion && + setting.currentSettings().versionUpFlash === "" + ); + }, + `${VER}`, + { timeout: uiTimeoutMs } + ); + }); + + console.log( + `Compatibility review screenshots: ${summaryScreenshot}, ${mobileSummaryScreenshot}, ${detailsScreenshot}` + ); +} + +async function verifyConfigDoctorFollowsCompatibilityReview(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + const doctor = page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: "Self-hosted LiveSync Config Doctor" }), + }); + await doctor.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await doctor.getByText("Per-file-saved customization sync", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + await doctor.getByText("Enhance chunk size", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + if ((await doctor.getByText("Data Compression", { exact: true }).count()) !== 0) { + throw new Error("Config Doctor still treats supported Data Compression as a problem."); + } + await doctor.getByRole("button", { name: /No, and do not ask again/u }).click(); + await doctor.waitFor({ state: "hidden", timeout: uiTimeoutMs }); + }); +} + +async function verifyEffectiveSettings(): Promise { + await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => { + await page.evaluate(() => { + const setting = (globalThis as ObsidianTestGlobal).app?.setting; + if (setting === undefined) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + + const liveSyncSettings = page.locator(".sls-setting"); + await liveSyncSettings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Change Log"]').click(); + const removedAcknowledgements = liveSyncSettings.getByRole("button", { + name: /I got it and updated|OK, I have read everything/u, + }); + if ((await removedAcknowledgements.count()) !== 0) { + throw new Error("The Change Log still contains a compatibility or release-note acknowledgement control."); + } + + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Remote Configuration"]').click(); + const connectionPanel = liveSyncSettings + .locator("h4.sls-setting-panel-title") + .filter({ hasText: "Connection settings" }) + .locator(".."); + await connectionPanel.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await connectionPanel.getByText("Saved connections", { exact: true }).waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + + await liveSyncSettings.locator('.sls-setting-menu-btn[title="Sync Settings"]').click(); + const deletionPanel = liveSyncSettings + .locator("h4.sls-setting-panel-title") + .filter({ hasText: "Deletion Propagation" }) + .locator(".."); + await deletionPanel + .getByText("Keep empty folder", { exact: true }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + + // Retirement guard: the removed toggle must not reappear in the current settings pane. + const obsoleteToggleCount = await deletionPanel.getByText("Use the trash bin", { exact: true }).count(); + if (obsoleteToggleCount !== 0) { + throw new Error( + `The obsolete LiveSync trash toggle is still present in the settings UI (${obsoleteToggleCount} found).` + ); + } + }); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) { + throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + } + const vault = await createTemporaryVault(); + let session: ObsidianLiveSyncSession | undefined; + try { + session = await startObsidianLiveSyncSession({ + binary, + cliBinary: cli.binary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: { + doctorProcessedVersion: "0.25.27", + isConfigured: true, + liveSync: false, + versionUpFlash: compatibilityReviewMessage, + notifyThresholdOfRemoteStorageSize: 0, + syncOnStart: false, + syncOnSave: false, + syncOnEditorSave: false, + syncOnFileOpen: false, + syncAfterMerge: false, + periodicReplication: false, + handleFilenameCaseSensitive: false, + useAdvancedMode: true, + useEdgeCaseMode: true, + }, + }); + await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + await verifyCompatibilityReview(); + await verifyConfigDoctorFollowsCompatibilityReview(); + await verifyEffectiveSettings(); + console.log("Compatibility review and settings expose only effective user controls."); + } finally { + if (session) { + await session.app.stop(); + } + await vault.dispose(); + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/setup-uri-workflow.ts b/test/e2e-obsidian/scripts/setup-uri-workflow.ts new file mode 100644 index 00000000..a53ca5ac --- /dev/null +++ b/test/e2e-obsidian/scripts/setup-uri-workflow.ts @@ -0,0 +1,859 @@ +import { execFile } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types"; +import type { Locator, Page } from "playwright"; +import { evalObsidianJson } from "../runner/cli.ts"; +import { + assertCouchDbReachable, + deleteCouchDbDatabase, + loadCouchDbConfig, + makeUniqueDatabaseName, + waitForCouchDbDocs, + type CouchDbConfig, +} from "../runner/couchdb.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + assertEqual, + pushLocalChanges, + waitForLocalDatabaseEntry, + type LocalDatabaseEntry, +} from "../runner/liveSyncWorkflow.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + assertVerticalActionLayout, + generateSetupURIFromDevice, + resumeCompatibilityReviewIfShown, +} from "../runner/setupUri.ts"; +import { + captureObsidianDialogue, + captureObsidianElement, + captureObsidianPage, + withObsidianPage, +} from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000"; +process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "30000"; + +const execFileAsync = promisify(execFile); +const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000); +const initialisationTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_INITIALISATION_TIMEOUT_MS ?? 120000); +const hiddenFileCliTimeoutMs = Number(process.env.E2E_OBSIDIAN_HIDDEN_FILE_CLI_TIMEOUT_MS ?? 90000); +const notePath = "E2E/setup-uri/provisioned-workflow.md"; +const noteContent = "# Provisioned Setup URI\n\nThis note travelled through the generated CouchDB Setup URI.\n"; +const returnNotePath = "E2E/setup-uri/from-second-device.md"; +const returnNoteContent = + "# CouchDB from the second device\n\nThis note completed the return journey through CouchDB.\n"; +const snippetPath = ".obsidian/snippets/setup-uri-workflow.css"; +const snippetContent = [ + "body {", + " --setup-uri-workflow-colour: #245a70;", + "}", + "", + ".setup-uri-workflow {", + " color: var(--setup-uri-workflow-colour);", + "}", + "", +].join("\n"); + +type SetupArtifact = { + setupURI: string; + setupPassphrase: string; +}; + +type SetupState = { + configured: boolean; + databaseReady: boolean; + appReady: boolean; + suspended: boolean; + remoteType: string; + activeConfigurationId: string; + remoteConfigurationCount: number; + syncInternalFiles: boolean; + syncInternalFilesBeforeReplication: boolean; +}; + +type RunnerContext = { + binary: string; + cliBinary: string; + couchDb: CouchDbConfig; + dbName: string; + activeSessions: Set; +}; + +function modalByTitle(page: Page, title: string): Locator { + return page.locator(".modal-container").filter({ + has: page.locator(".modal-title").filter({ hasText: title }), + }); +} + +function settingPanelByTitle(page: Page, title: string): Locator { + return page + .locator(".sls-setting") + .locator("h4.sls-setting-panel-title:visible") + .filter({ hasText: title }) + .locator(".."); +} + +async function captureGuideDialogue(port: number, filename: string, title: string): Promise { + return await captureObsidianElement(port, filename, (page) => modalByTitle(page, title).locator(".modal").first()); +} + +async function selectRadioOption(modal: Locator, title: string): Promise { + const radio = modal.locator("label").filter({ hasText: title }).locator('input[type="radio"]').first(); + await radio.check({ timeout: uiTimeoutMs }); +} + +async function selectCheckbox(modal: Locator, title: string): Promise { + const checkbox = modal.locator("label").filter({ hasText: title }).locator('input[type="checkbox"]').first(); + await checkbox.check({ timeout: uiTimeoutMs }); +} + +async function writeVaultFile(vaultPath: string, path: string, content: string): Promise { + const fullPath = join(vaultPath, path); + await mkdir(dirname(fullPath), { recursive: true }); + await writeFile(fullPath, content, "utf8"); +} + +async function readVaultFile(vaultPath: string, path: string): Promise { + return await readFile(join(vaultPath, path), "utf8"); +} + +async function waitForPathContent( + vaultPath: string, + path: string, + expected: string, + timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000) +): Promise { + const deadline = Date.now() + timeoutMs; + let lastContent = ""; + while (Date.now() < deadline) { + try { + lastContent = await readVaultFile(vaultPath, path); + if (lastContent === expected) return lastContent; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`); +} + +async function runDeno(script: string, permissions: string[], environment: NodeJS.ProcessEnv): Promise { + const { stdout } = await execFileAsync( + "deno", + [ + "run", + "--minimum-dependency-age=0", + "--config=utils/flyio/deno.jsonc", + "--frozen", + "--lock=utils/flyio/deno.lock", + ...permissions, + script, + ], + { + cwd: process.cwd(), + env: environment, + maxBuffer: 4 * 1024 * 1024, + } + ); + return stdout; +} + +async function provisionAndGenerateSetupURI(couchDb: CouchDbConfig, dbName: string): Promise { + const setupPassphrase = randomBytes(24).toString("base64url"); + const environment = { + ...process.env, + hostname: couchDb.uri, + username: couchDb.username, + password: couchDb.password, + database: dbName, + passphrase: randomBytes(24).toString("base64url"), + uri_passphrase: setupPassphrase, + remote_type: "couchdb", + retry_count: "3", + retry_delay_ms: "250", + }; + + await runDeno("utils/couchdb/provision.ts", ["--allow-env", "--allow-net"], environment); + const output = await runDeno("utils/setup/generate_setup_uri.ts", ["--allow-env"], environment); + const setupURI = output.split(/\r?\n/u).find((line) => line.startsWith("obsidian://setuplivesync?settings=")); + if (!setupURI) throw new Error("The public Setup URI generator did not emit a Setup URI."); + return { setupURI, setupPassphrase }; +} + +async function startUnconfiguredSession( + context: RunnerContext, + vault: TemporaryVault +): Promise { + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + vault, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + }); + context.activeSessions.add(session); + return session; +} + +async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) return; + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function stopTrackedSessions(context: RunnerContext): Promise { + for (const session of [...context.activeSessions]) { + await stopTrackedSession(context, session); + } +} + +async function enterSetupURI(port: number, mode: "new" | "existing", artifact: SetupArtifact): Promise { + await withObsidianPage(port, async (page) => { + const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" }); + await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs }); + + const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync"); + await intro.waitFor({ state: "visible", timeout: uiTimeoutMs }); + if (mode === "new") { + await selectRadioOption(intro, "I am setting this up for the first time"); + await intro + .getByRole("button", { name: "Yes, I want to set up a new synchronisation" }) + .click({ timeout: uiTimeoutMs }); + } else { + await selectRadioOption(intro, "I am adding a device to an existing synchronisation setup"); + await intro + .getByRole("button", { name: "Yes, I want to add this device to my existing synchronisation" }) + .click({ timeout: uiTimeoutMs }); + } + + const method = modalByTitle(page, mode === "new" ? "Connection Method" : "Device Setup Method"); + await method.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await selectRadioOption(method, "Use a Setup URI (Recommended)"); + await method.getByRole("button", { name: "Proceed with Setup URI" }).click({ timeout: uiTimeoutMs }); + + const setup = modalByTitle(page, "Enter Setup URI"); + await setup.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await setup.locator('input[placeholder^="obsidian://setuplivesync"]').fill(artifact.setupURI); + await setup.locator('input[name="password"]').fill(artifact.setupPassphrase); + }); + await captureGuideDialogue( + port, + `guide-quick-setup-${mode === "new" ? "first" : "second"}-setup-uri.png`, + "Enter Setup URI" + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, "Enter Setup URI") + .getByRole("button", { name: "Test Settings and Continue" }) + .click({ timeout: uiTimeoutMs }); + }); +} + +async function captureAndStartInitialisation(port: number, mode: "new" | "existing"): Promise { + const title = + mode === "new" + ? "Setup Complete: Preparing to Initialise Server" + : "Setup Complete: Preparing to Fetch Synchronisation Data"; + const button = mode === "new" ? "Restart and Initialise Server" : "Restart and Fetch Data"; + const screenshot = await captureObsidianDialogue( + port, + `setup-uri-${mode === "new" ? "first-initialise" : "second-fetch"}.png`, + async (page) => { + await modalByTitle(page, title).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + ); + await captureGuideDialogue( + port, + `guide-quick-setup-${mode === "new" ? "first-initialise" : "second-fetch"}.png`, + title + ); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, title).getByRole("button", { name: button }).click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function confirmRebuild(port: number): Promise { + const title = "Final Confirmation: Overwrite Server Data with This Device's Files"; + const screenshot = await captureObsidianDialogue(port, "setup-uri-first-rebuild-confirmation.png", async (page) => { + await modalByTitle(page, title).waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + await captureGuideDialogue(port, "guide-quick-setup-first-rebuild-confirmation.png", title); + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, title); + await selectCheckbox( + modal, + "I understand that all changes made on other smartphones or computers possibly could be lost." + ); + await selectCheckbox( + modal, + "I understand that other devices will no longer be able to synchronise, and will need to be reset the synchronisation information." + ); + await selectCheckbox(modal, "I understand that this action is irreversible once performed."); + await selectRadioOption(modal, "I understand the risks and will proceed without a backup."); + await modal.getByRole("button", { name: "I Understand, Overwrite Server" }).click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function skipMissingRemoteConfiguration(port: number): Promise { + const title = "Fetch Remote Configuration Failed"; + const screenshot = await captureObsidianDialogue( + port, + "setup-uri-first-missing-remote-configuration.png", + async (page) => { + const modal = modalByTitle(page, title); + await modal.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await modal + .getByText("If you are new to the Self-hosted LiveSync, this might be expected.", { + exact: false, + }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + ); + await captureGuideDialogue(port, "guide-quick-setup-missing-remote-configuration.png", title); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, title) + .getByRole("button", { name: "Skip and proceed" }) + .click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function acknowledgeDisabledOptionalFeatures(port: number): Promise { + const title = "All optional features are disabled"; + const screenshot = await captureObsidianDialogue( + port, + "setup-uri-first-optional-features-disabled.png", + async (page) => { + const modal = modalByTitle(page, title); + await modal.waitFor({ state: "visible", timeout: initialisationTimeoutMs }); + await modal + .getByText("Please enable them from the settings screen after setup is complete.", { + exact: false, + }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + ); + await captureGuideDialogue(port, "guide-quick-setup-optional-features-disabled.png", title); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, title).getByRole("button", { name: "OK" }).click({ timeout: uiTimeoutMs }); + }); + return screenshot; +} + +async function confirmFastFetch(port: number): Promise { + const firstTitle = "Data retrieval scheduled"; + await assertVerticalActionLayout(port, firstTitle); + const firstScreenshot = await captureObsidianDialogue( + port, + "setup-uri-second-retrieval-method.png", + async (page) => { + await modalByTitle(page, firstTitle).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + ); + await captureGuideDialogue(port, "guide-quick-setup-retrieval-method.png", firstTitle); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, firstTitle) + .getByRole("button", { name: "Overwrite all with remote files" }) + .click({ timeout: uiTimeoutMs }); + }); + + const secondTitle = "How to handle extra existing local files?"; + await assertVerticalActionLayout(port, secondTitle); + const secondScreenshot = await captureObsidianDialogue( + port, + "setup-uri-second-local-file-policy.png", + async (page) => { + await modalByTitle(page, secondTitle).waitFor({ state: "visible", timeout: uiTimeoutMs }); + } + ); + await captureGuideDialogue(port, "guide-quick-setup-local-file-policy.png", secondTitle); + await withObsidianPage(port, async (page) => { + await modalByTitle(page, secondTitle) + .getByRole("button", { name: "Keep local files even if not on remote" }) + .click({ timeout: uiTimeoutMs }); + }); + return [firstScreenshot, secondScreenshot]; +} + +function isConfiguredSetupReady(state: SetupState): boolean { + return ( + state.configured && + state.databaseReady && + state.appReady && + !state.suspended && + state.activeConfigurationId !== "" && + state.remoteConfigurationCount === 1 + ); +} + +async function finishInitialisation( + port: number, + filename: string, + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise<{ state: SetupState; screenshot?: string }> { + const message = "Do you want to resume file and database processing, and restart obsidian now?"; + const deadline = Date.now() + initialisationTimeoutMs; + let lastState: SetupState | undefined; + let lastError: unknown; + while (Date.now() < deadline) { + const resumeVisible = await withObsidianPage(port, async (page) => { + return await modalByTitle(page, "Confirmation").filter({ hasText: message }).isVisible(); + }).catch(() => false); + if (resumeVisible) { + const screenshot = await captureObsidianDialogue(port, filename, async (page) => { + await modalByTitle(page, "Confirmation") + .filter({ hasText: message }) + .waitFor({ state: "visible", timeout: uiTimeoutMs }); + }); + await withObsidianPage(port, async (page) => { + const modal = modalByTitle(page, "Confirmation").filter({ hasText: message }); + await modal.getByText(message, { exact: true }).click({ timeout: uiTimeoutMs }); + await modal.getByRole("button", { name: "Yes", exact: true }).click({ timeout: uiTimeoutMs }); + }); + return { + state: await waitForConfiguredSetup(cliBinary, environment, initialisationTimeoutMs), + screenshot, + }; + } + try { + lastState = await readSetupState(cliBinary, environment); + if (isConfiguredSetupReady(lastState)) return { state: lastState }; + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error( + `Timed out waiting for Setup URI initialisation to finish: ${JSON.stringify(lastState)}${ + lastError instanceof Error ? `; last error: ${lastError.message}` : "" + }` + ); +} + +async function readSetupState(cliBinary: string, environment: NodeJS.ProcessEnv): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const settings=core.services.setting.currentSettings();", + "return JSON.stringify({", + "configured:settings.isConfigured===true,", + "databaseReady:core.services.database.isDatabaseReady(),", + "appReady:core.services.appLifecycle.isReady(),", + "suspended:core.services.appLifecycle.isSuspended(),", + "remoteType:settings.remoteType,", + "activeConfigurationId:settings.activeConfigurationId||'',", + "remoteConfigurationCount:Object.keys(settings.remoteConfigurations||{}).length,", + "syncInternalFiles:settings.syncInternalFiles===true,", + "syncInternalFilesBeforeReplication:settings.syncInternalFilesBeforeReplication===true,", + "});", + "})()", + ].join(""), + environment + ); +} + +async function waitForConfiguredSetup( + cliBinary: string, + environment: NodeJS.ProcessEnv, + timeoutMs = initialisationTimeoutMs +): Promise { + const deadline = Date.now() + timeoutMs; + let lastState: SetupState | undefined; + let lastError: unknown; + while (Date.now() < deadline) { + try { + lastState = await readSetupState(cliBinary, environment); + if (isConfiguredSetupReady(lastState)) { + return lastState; + } + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error( + `Timed out waiting for configured Setup URI state: ${JSON.stringify(lastState)}${ + lastError instanceof Error ? `; last error: ${lastError.message}` : "" + }` + ); +} + +async function enableHiddenFileSync(cliBinary: string, environment: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "await core.services.setting.applyPartial({", + "syncInternalFiles:true,", + "syncInternalFilesBeforeReplication:true,", + "},true);", + "await core.services.control.applySettings();", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); + const state = await waitForConfiguredSetup(cliBinary, environment); + if (!state.syncInternalFiles || !state.syncInternalFilesBeforeReplication) { + throw new Error(`Hidden File Sync was not enabled after setup: ${JSON.stringify(state)}`); + } + return state; +} + +async function captureHiddenFileGuideSettings( + port: number, + cliBinary: string, + environment: NodeJS.ProcessEnv +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "await core.services.setting.applyPartial({", + "useAdvancedMode:true,", + "syncInternalFilesTargetPatterns:'^\\\\.obsidian(?:$|/snippets(?:/|$))',", + "},true);", + "await core.services.control.applySettings();", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); + + await withObsidianPage(port, async (page) => { + await page.evaluate(() => { + const obsidian = globalThis as typeof globalThis & { + app?: { + setting?: { + open(): void; + openTabById(tabId: string): void; + }; + }; + }; + const setting = obsidian.app?.setting; + if (!setting) throw new Error("Obsidian settings are unavailable"); + setting.open(); + setting.openTabById("obsidian-livesync"); + }); + const settings = page.locator(".sls-setting"); + await settings.waitFor({ state: "visible", timeout: uiTimeoutMs }); + await settings.locator('.sls-setting-menu-btn[title="Setup"]').click({ timeout: uiTimeoutMs }); + }); + + const screenshots = [ + await captureObsidianElement(port, "guide-hidden-file-advanced-features.png", (page) => + settingPanelByTitle(page, "Enable extra and advanced features") + ), + ]; + + await withObsidianPage(port, async (page) => { + await page + .locator(".sls-setting") + .locator('.sls-setting-menu-btn[title="Selector"]') + .click({ timeout: uiTimeoutMs }); + }); + screenshots.push( + await captureObsidianElement(port, "guide-hidden-file-selector.png", (page) => + settingPanelByTitle(page, "Hidden Files") + ) + ); + + await withObsidianPage(port, async (page) => { + await page + .locator(".sls-setting") + .locator('.sls-setting-menu-btn[title="Sync Settings"]') + .click({ timeout: uiTimeoutMs }); + }); + screenshots.push( + await captureObsidianElement(port, "guide-hidden-file-enable.png", (page) => + settingPanelByTitle(page, "Hidden Files") + ) + ); + + await withObsidianPage(port, async (page) => { + await page.keyboard.press("Escape"); + }); + return screenshots; +} + +async function writeNoteViaObsidian( + cliBinary: string, + environment: NodeJS.ProcessEnv, + path: string, + content: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const content=${JSON.stringify(content)};`, + "const folder=path.split('/').slice(0,-1).join('/');", + "if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);", + "const existing=app.vault.getAbstractFileByPath(path);", + "if(existing) await app.vault.modify(existing,content);", + "else await app.vault.create(path,content);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment + ); +} + +async function scanHiddenStorage(cliBinary: string, environment: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const addOn=core.getAddOn('HiddenFileSync');", + "await addOn.scanAllStorageChanges(true);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment, + hiddenFileCliTimeoutMs + ); +} + +async function scanHiddenDatabase(cliBinary: string, environment: NodeJS.ProcessEnv): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const addOn=core.getAddOn('HiddenFileSync');", + "await addOn.scanAllDatabaseChanges(true);", + "return JSON.stringify({ok:true});", + "})()", + ].join(""), + environment, + hiddenFileCliTimeoutMs + ); +} + +async function waitForRemoteEntry(context: RunnerContext, entry: LocalDatabaseEntry): Promise { + await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => { + const ids = new Set(docs.map((doc) => doc._id)); + return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId)); + }); +} + +async function uploadWorkflowFiles( + context: RunnerContext, + session: ObsidianLiveSyncSession, + vault: TemporaryVault +): Promise { + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, notePath, noteContent); + await writeVaultFile(vault.path, snippetPath, snippetContent); + await scanHiddenStorage(context.cliBinary, session.cliEnv); + const noteEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, notePath); + const snippetEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, snippetPath, { + hidden: true, + }); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await waitForRemoteEntry(context, noteEntry); + await waitForRemoteEntry(context, snippetEntry); +} + +async function captureSynchronisedNote(port: number): Promise { + await withObsidianPage(port, async (page) => { + await page.evaluate((path) => { + const obsidian = globalThis as typeof globalThis & { + app?: { + workspace?: { openLinkText(path: string, sourcePath: string, newLeaf: boolean): Promise }; + }; + }; + return obsidian.app?.workspace?.openLinkText(path, "", false); + }, notePath); + }); + await captureObsidianPage(port, "setup-uri-synchronised-note.png", async (page) => { + await page.getByText("Provisioned Setup URI", { exact: false }).first().waitFor({ + state: "visible", + timeout: uiTimeoutMs, + }); + }); + return await captureObsidianElement(port, "guide-quick-setup-synchronised-note.png", (page) => + page.locator(".workspace-leaf.mod-active").first() + ); +} + +async function captureFailure(session: ObsidianLiveSyncSession): Promise { + await captureObsidianPage( + session.remoteDebuggingPort, + "setup-uri-workflow.failure.png", + async () => undefined + ).catch(() => undefined); +} + +async function main(): Promise { + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + + const couchDb = await loadCouchDbConfig(); + const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "setup-uri-workflow"); + const vaultA = await createTemporaryVault(); + const vaultB = await createTemporaryVault(); + const context: RunnerContext = { + binary, + cliBinary: cli.binary, + couchDb, + dbName, + activeSessions: new Set(), + }; + const screenshots: string[] = []; + let secondDeviceArtifact: SetupArtifact | undefined; + + try { + await assertCouchDbReachable(couchDb); + const artifact = await provisionAndGenerateSetupURI(couchDb, dbName); + const provisionedDocs = await waitForCouchDbDocs(couchDb, dbName, (docs) => + docs.some((doc) => doc._id === "obsydian_livesync_version" && doc.version === VER) + ); + if (!provisionedDocs.some((doc) => doc._id === "obsydian_livesync_version" && doc.version === VER)) { + throw new Error("The public provisioning tool did not initialise the Commonlib database version."); + } + + console.log(`Using Obsidian executable: ${binary}`); + console.log(`Temporary vault A: ${vaultA.path}`); + console.log(`Temporary vault B: ${vaultB.path}`); + console.log(`Temporary provisioned CouchDB database: ${dbName}`); + + let session = await startUnconfiguredSession(context, vaultA); + try { + await enterSetupURI(session.remoteDebuggingPort, "new", artifact); + screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "new")); + screenshots.push(await confirmRebuild(session.remoteDebuggingPort)); + screenshots.push(await skipMissingRemoteConfiguration(session.remoteDebuggingPort)); + screenshots.push(await acknowledgeDisabledOptionalFeatures(session.remoteDebuggingPort)); + const firstCompletion = await finishInitialisation( + session.remoteDebuggingPort, + "setup-uri-first-initialisation-complete.png", + context.cliBinary, + session.cliEnv + ); + if (firstCompletion.screenshot) screenshots.push(firstCompletion.screenshot); + const firstState = firstCompletion.state; + await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + assertEqual(firstState.remoteType, "", "The first device did not activate the CouchDB remote profile."); + assertEqual( + firstState.syncInternalFiles, + false, + "Rebuild did not retain the documented optional-feature safety boundary." + ); + screenshots.push( + ...(await captureHiddenFileGuideSettings( + session.remoteDebuggingPort, + context.cliBinary, + session.cliEnv + )) + ); + await enableHiddenFileSync(context.cliBinary, session.cliEnv); + await uploadWorkflowFiles(context, session, vaultA); + const generated = await generateSetupURIFromDevice( + session.remoteDebuggingPort, + randomBytes(24).toString("base64url"), + { scenario: "setup-uri-workflow", guide: "quick-setup" } + ); + if (generated.artifact.setupURI === artifact.setupURI) { + throw new Error("The first device returned the bootstrap Setup URI instead of generating a new one."); + } + secondDeviceArtifact = generated.artifact; + screenshots.push(...generated.screenshots); + } catch (error) { + await captureFailure(session); + throw error; + } finally { + await stopTrackedSession(context, session); + } + + session = await startUnconfiguredSession(context, vaultB); + try { + if (!secondDeviceArtifact) + throw new Error("The first device did not generate the second-device Setup URI."); + await enterSetupURI(session.remoteDebuggingPort, "existing", secondDeviceArtifact); + screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "existing")); + screenshots.push(...(await confirmFastFetch(session.remoteDebuggingPort))); + const secondCompletion = await finishInitialisation( + session.remoteDebuggingPort, + "setup-uri-second-initialisation-complete.png", + context.cliBinary, + session.cliEnv + ); + if (secondCompletion.screenshot) screenshots.push(secondCompletion.screenshot); + const secondState = secondCompletion.state; + await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + assertEqual(secondState.remoteType, "", "The second device did not activate the CouchDB remote profile."); + await enableHiddenFileSync(context.cliBinary, session.cliEnv); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await scanHiddenDatabase(context.cliBinary, session.cliEnv); + const receivedNote = await waitForPathContent(vaultB.path, notePath, noteContent); + const receivedSnippet = await waitForPathContent(vaultB.path, snippetPath, snippetContent); + assertEqual(receivedNote, noteContent, "The ordinary note did not reach the second Setup URI device."); + assertEqual( + receivedSnippet, + snippetContent, + "The hidden snippet did not reach the second Setup URI device." + ); + screenshots.push(await captureSynchronisedNote(session.remoteDebuggingPort)); + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, returnNotePath, returnNoteContent); + const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, returnNotePath); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await waitForRemoteEntry(context, returnEntry); + } catch (error) { + await captureFailure(session); + throw error; + } finally { + await stopTrackedSession(context, session); + } + + session = await startUnconfiguredSession(context, vaultA); + try { + await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort); + await pushLocalChanges(context.cliBinary, session.cliEnv); + const receivedReturnNote = await waitForPathContent(vaultA.path, returnNotePath, returnNoteContent); + assertEqual( + receivedReturnNote, + returnNoteContent, + "The second device's ordinary note did not return to the first Setup URI device." + ); + } catch (error) { + await captureFailure(session); + throw error; + } finally { + await stopTrackedSession(context, session); + } + + console.log( + `The public provisioning and first-device-generated Setup URI workflow configured two fresh devices, completed an ordinary-note round-trip, and synchronised a hidden snippet. Screenshots: ${screenshots.join(", ")}` + ); + } finally { + await stopTrackedSessions(context).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + await vaultA.dispose(); + await vaultB.dispose(); + if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") { + await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + } + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/e2e-obsidian/scripts/smoke.ts b/test/e2e-obsidian/scripts/smoke.ts index c00644f3..8dee624a 100644 --- a/test/e2e-obsidian/scripts/smoke.ts +++ b/test/e2e-obsidian/scripts/smoke.ts @@ -1,4 +1,8 @@ import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + assertObsidianServiceContextContract, + inspectObsidianServiceContextContract, +} from "../runner/liveSyncWorkflow.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; import { createTemporaryVault } from "../runner/vault.ts"; @@ -25,6 +29,11 @@ async function main(): Promise { console.log( `Obsidian plug-in ready: ${readiness.pluginId}@${readiness.pluginVersion} in ${readiness.vaultName}` ); + const contextContract = await inspectObsidianServiceContextContract(cli.binary, session.cliEnv); + assertObsidianServiceContextContract(contextContract); + console.log( + `Obsidian service Context contract passed: ${contextContract.contextType}, ${contextContract.serviceContextMismatches.length} mismatches.` + ); await new Promise((resolve) => setTimeout(resolve, Number(process.env.E2E_OBSIDIAN_SMOKE_TIMEOUT_MS ?? 1000))); console.log("Obsidian stayed alive after the plug-in readiness check."); } finally { diff --git a/test/e2e-obsidian/scripts/startup-scan.ts b/test/e2e-obsidian/scripts/startup-scan.ts index 9aaa4de0..432145a3 100644 --- a/test/e2e-obsidian/scripts/startup-scan.ts +++ b/test/e2e-obsidian/scripts/startup-scan.ts @@ -1,3 +1,13 @@ +/** + * Proves that a configured LiveSync Vault scans files created while Obsidian + * was stopped. The first launch receives a CouchDB profile using current + * settings and its acknowledged device-local compatibility marker before the + * plug-in loads. + * + * The second launch reuses the same Vault, profile, local database, and + * settings without rewriting plug-in data, so the assertion covers an + * ordinary configured restart rather than the separate onboarding flow. + */ import { mkdir, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { @@ -11,7 +21,8 @@ import { import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; import { assertEqual, - configureCouchDb, + createE2eCouchDbPluginData, + createE2eObsidianDeviceLocalState, prepareRemote, pushLocalChanges, waitForLiveSyncCoreReady, @@ -47,6 +58,12 @@ async function main(): Promise { const couchDb = await loadCouchDbConfig(); const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "startup-scan"); + const couchDbSettings = { + uri: couchDb.uri, + username: couchDb.username, + password: couchDb.password, + dbName, + }; const vault = await createTemporaryVault(); let session: ObsidianLiveSyncSession | undefined; @@ -63,15 +80,11 @@ async function main(): Promise { cliBinary: cli.binary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData(couchDbSettings), + localStorageEntries: createE2eObsidianDeviceLocalState(vault.name), }); - await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); - const configured = await configureCouchDb(cli.binary, session.cliEnv, { - uri: couchDb.uri, - username: couchDb.username, - password: couchDb.password, - dbName, - }); - assertEqual(configured.isConfigured, true, "Self-hosted LiveSync was not configured."); + const initialReadiness = await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + assertEqual(initialReadiness.configured, true, "Self-hosted LiveSync did not start configured."); await prepareRemote(cli.binary, session.cliEnv); await session.app.stop(); session = undefined; @@ -84,7 +97,8 @@ async function main(): Promise { vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), }); - await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + const restartedReadiness = await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); + assertEqual(restartedReadiness.configured, true, "Self-hosted LiveSync lost its configuration on restart."); const localEntry = await waitForLocalDatabaseEntry(cli.binary, session.cliEnv, notePath); await pushLocalChanges(cli.binary, session.cliEnv); diff --git a/test/e2e-obsidian/scripts/two-vault-sync.ts b/test/e2e-obsidian/scripts/two-vault-sync.ts index 52203afa..ca69c038 100644 --- a/test/e2e-obsidian/scripts/two-vault-sync.ts +++ b/test/e2e-obsidian/scripts/two-vault-sync.ts @@ -11,11 +11,16 @@ import { type CouchDbConfig, } from "../runner/couchdb.ts"; import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { waitForExactCaseOnlyRename } from "../runner/pathAssertions.ts"; import { assertEqual, + assertE2eCompatibilityMarker, + assertE2eCompatibilityReviewPending, configureCouchDb, + createE2eCouchDbPluginData, prepareRemote, pushLocalChanges, + resumeCompatibilityReview, waitForLiveSyncCoreReady, waitForLocalDatabaseEntry, type LocalDatabaseEntry, @@ -34,6 +39,12 @@ const renameToPath = "E2E/two-vault/renamed/rename-target.md"; const caseRenameFromPath = "E2E/two-vault/Case-Rename.md"; const caseRenameToPath = "E2E/two-vault/case-rename.md"; const conflictPath = "E2E/two-vault/conflict.md"; +const conflictEditPath = "E2E/two-vault/conflict-operations/edit.md"; +const conflictDeletePath = "E2E/two-vault/conflict-operations/delete.md"; +const conflictCaseFromPath = "E2E/two-vault/conflict-operations/Case-Rename.md"; +const conflictCaseToPath = "E2E/two-vault/conflict-operations/case-rename.md"; +const conflictRenameFromPath = "E2E/two-vault/conflict-operations/rename-source.md"; +const conflictRenameToPath = "E2E/two-vault/conflict-operations/renamed/rename-target.md"; const targetMismatchPath = "E2E/two-vault/target-mismatch.md"; const encryptedPath = "E2E/two-vault/encrypted.md"; @@ -42,6 +53,19 @@ type RunnerContext = { cliBinary: string; couchDb: CouchDbConfig; dbName: string; + reviewedVaults: Set; + activeSessions: Set; +}; + +type FileConflictState = { + currentRev: string; + branches: { + rev: string; + parentRev?: string; + content: string; + deleted: boolean; + path: string; + }[]; }; async function writeVaultFile(vaultPath: string, path: string, content: string): Promise { @@ -70,6 +94,18 @@ async function pathExists(vaultPath: string, path: string): Promise { } } +async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) return; + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function stopTrackedSessions(context: RunnerContext): Promise { + for (const session of [...context.activeSessions]) { + await stopTrackedSession(context, session); + } +} + async function waitForPathContent( vaultPath: string, path: string, @@ -163,27 +199,44 @@ async function startConfiguredSession( vault: TemporaryVault, overrides: Record = {} ): Promise { + const couchDbSettings = { + uri: context.couchDb.uri, + username: context.couchDb.username, + password: context.couchDb.password, + dbName: context.dbName, + }; + const reviewAlreadyCompleted = context.reviewedVaults.has(vault.path); const session = await startObsidianLiveSyncSession({ binary: context.binary, cliBinary: context.cliBinary, vault, startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + pluginData: createE2eCouchDbPluginData(couchDbSettings, overrides), }); - await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); - await configureCouchDb( - context.cliBinary, - session.cliEnv, - { - uri: context.couchDb.uri, - username: context.couchDb.username, - password: context.couchDb.password, - dbName: context.dbName, - }, - overrides - ); - await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); - await prepareRemote(context.cliBinary, session.cliEnv); - return session; + context.activeSessions.add(session); + try { + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + if (!reviewAlreadyCompleted) { + await assertE2eCompatibilityReviewPending(context.cliBinary, session.cliEnv); + await resumeCompatibilityReview(session.remoteDebuggingPort); + } + await assertE2eCompatibilityMarker(context.cliBinary, session.cliEnv); + if (!reviewAlreadyCompleted) context.reviewedVaults.add(vault.path); + await configureCouchDb(context.cliBinary, session.cliEnv, couchDbSettings, overrides); + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + await prepareRemote(context.cliBinary, session.cliEnv); + return session; + } catch (error) { + try { + await stopTrackedSession(context, session); + } catch (stopError) { + throw Object.assign(new Error("Could not stop Obsidian after session setup failed."), { + cause: error, + stopError, + }); + } + throw error; + } } async function uploadNote( @@ -243,25 +296,116 @@ async function storeFileRevision( return result.rev; } -async function createMarkdownConflict( - context: RunnerContext, - session: ObsidianLiveSyncSession, - vault: TemporaryVault, - path: string, - base: string, - left: string, - right: string -): Promise { - const baseRev = await storeFileRevision(context.cliBinary, session.cliEnv, path, base); - await pushLocalChanges(context.cliBinary, session.cliEnv); - await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, path); - await storeFileRevision(context.cliBinary, session.cliEnv, path, left, baseRev); - await storeFileRevision(context.cliBinary, session.cliEnv, path, right, baseRev); - await writeVaultFile(vault.path, path, right); +async function readFileConflictState( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string +): Promise { + return await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const meta=await core.localDatabase.getDBEntryMeta(path,{conflicts:true},true);", + "if(!meta) throw new Error(`Could not find conflict metadata: ${path}`);", + "const revisions=[meta._rev,...(meta._conflicts??[])];", + "const branches=[];", + "for(const rev of revisions){", + " const branchMeta=await core.localDatabase.getDBEntryMeta(path,{rev,revs:true},true);", + " const entry=await core.localDatabase.getDBEntry(path,{rev},false,true,true);", + " if(!branchMeta||!entry) throw new Error(`Could not read conflict revision: ${path} ${rev}`);", + " const content=Array.isArray(entry.data)?entry.data.join(''):entry.data;", + " if(typeof content!=='string') throw new Error(`Conflict revision was not text: ${path} ${rev}`);", + " const ids=branchMeta._revisions?.ids??[];", + " const parentRev=ids[1]?`${branchMeta._revisions.start-1}-${ids[1]}`:undefined;", + " branches.push({rev,parentRev,content,deleted:Boolean(branchMeta.deleted||branchMeta._deleted),path:branchMeta.path});", + "}", + "return JSON.stringify({currentRev:meta._rev,branches});", + "})()", + ].join(""), + env + ); } -async function autoMergeMarkdownConflict(cliBinary: string, env: NodeJS.ProcessEnv, path: string): Promise { - await evalObsidianJson( +async function waitForFileConflict( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string +): Promise { + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000); + let state = await readFileConflictState(cliBinary, env, path); + while (state.branches.length < 2 && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 250)); + state = await readFileConflictState(cliBinary, env, path); + } + if (state.branches.length < 2) { + throw new Error(`Timed out waiting for a file conflict: ${path}`); + } + return state; +} + +async function waitForConflictBranch( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string, + predicate: (branch: FileConflictState["branches"][number]) => boolean +): Promise { + const deadline = Date.now() + Number(process.env.E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS ?? 15000); + let state = await readFileConflictState(cliBinary, env, path); + while (Date.now() < deadline) { + const branch = state.branches.find(predicate); + if (branch) return branch; + await new Promise((resolve) => setTimeout(resolve, 250)); + state = await readFileConflictState(cliBinary, env, path); + } + throw new Error(`Timed out waiting for the expected conflict branch: ${path}; ${JSON.stringify(state)}`); +} + +async function readFileReflectionProvenance( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string +): Promise<{ revision: string; observedStorageMtime?: number } | null> { + return await evalObsidianJson<{ revision: string; observedStorageMtime?: number } | null>( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const store=core.services.keyValueDB.openSimpleStore('file-reflection-provenance-v1');", + "return JSON.stringify((await store.get(path))??null);", + "})()", + ].join(""), + env + ); +} + +async function readPathIdentity( + cliBinary: string, + env: NodeJS.ProcessEnv, + paths: readonly string[] +): Promise<{ caseSensitive: boolean; ids: Record }> { + return await evalObsidianJson<{ caseSensitive: boolean; ids: Record }>( + cliBinary, + [ + "(async()=>{", + `const paths=${JSON.stringify(paths)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "const ids={};", + "for(const path of paths) ids[path]=await core.services.path.path2id(path);", + "return JSON.stringify({", + " caseSensitive:Boolean(core.services.setting.currentSettings().handleFilenameCaseSensitive),", + " ids,", + "});", + "})()", + ].join(""), + env + ); +} + +async function calculateMarkdownAutoMerge(cliBinary: string, env: NodeJS.ProcessEnv, path: string): Promise { + const result = await evalObsidianJson<{ content: string }>( cliBinary, [ "(async()=>{", @@ -271,11 +415,29 @@ async function autoMergeMarkdownConflict(cliBinary: string, env: NodeJS.ProcessE "if(!('result' in result)){", " throw new Error(`Markdown conflict was not auto-mergeable: ${path}; ${JSON.stringify(result)}`);", "}", - "if(!(await core.databaseFileAccess.storeContent(path,result.result))){", - " throw new Error(`Could not store merged Markdown content: ${path}`);", - "}", - "if(!(await core.fileHandler.deleteRevisionFromDB(path,result.conflictedRev))){", - " throw new Error(`Could not delete conflicted revision: ${path}`);", + "return JSON.stringify({content:result.result});", + "})()", + ].join(""), + env + ); + return result.content; +} + +async function deleteRevisionAndReflect( + cliBinary: string, + env: NodeJS.ProcessEnv, + path: string, + revision: string +): Promise { + await evalObsidianJson( + cliBinary, + [ + "(async()=>{", + `const path=${JSON.stringify(path)};`, + `const revision=${JSON.stringify(revision)};`, + "const core=app.plugins.plugins['obsidian-livesync'].core;", + "if(!(await core.fileHandler.deleteRevisionFromDB(path,revision))){", + " throw new Error(`Could not delete conflicted revision: ${path} ${revision}`);", "}", "if(!(await core.fileHandler.dbToStorage(path,path,true))){", " throw new Error(`Could not reflect merged Markdown content: ${path}`);", @@ -296,12 +458,12 @@ async function runCreateUpdateDelete( let session = await startConfiguredSession(context, vaultA); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, createPath, createdContent); await uploadNote(context, session, createPath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); const createdOnB = await waitForPathContent(vaultB.path, createPath, (content) => content === createdContent); - await session.app.stop(); + await stopTrackedSession(context, session); assertEqual(createdOnB, createdContent, "Created note did not round-trip to the second vault."); const initialUpdateContent = "# Update target\n\nInitial content.\n"; @@ -311,34 +473,34 @@ async function runCreateUpdateDelete( await uploadNote(context, session, updatePath); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, updatePath, updatedContent); await uploadNote(context, session, updatePath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); const updatedOnB = await waitForPathContent(vaultB.path, updatePath, (content) => content === updatedContent); - await session.app.stop(); + await stopTrackedSession(context, session); assertEqual(updatedOnB, updatedContent, "Updated note content did not round-trip to the second vault."); const deleteContent = "# Delete target\n\nThis note should be removed from B.\n"; session = await startConfiguredSession(context, vaultA); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, deletePath, deleteContent); await uploadNote(context, session, deletePath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); await waitForPathContent(vaultB.path, deletePath, (content) => content === deleteContent); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultA); await deleteNoteViaObsidian(context.cliBinary, session.cliEnv, deletePath); await pushLocalChanges(context.cliBinary, session.cliEnv); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); await waitForPathDeleted(vaultB.path, deletePath); - await session.app.stop(); + await stopTrackedSession(context, session); console.log("Two-vault note creation, update, and deletion round-tripped."); } @@ -352,13 +514,13 @@ async function runRename(context: RunnerContext, vaultA: TemporaryVault, vaultB: await renameNoteViaObsidian(context.cliBinary, session.cliEnv, renameFromPath, renameToPath); await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, renameToPath); await pushLocalChanges(context.cliBinary, session.cliEnv); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); const renamedOnB = await waitForPathContent(vaultB.path, renameToPath, (content) => content === renamedContent); await waitForPathDeleted(vaultB.path, renameFromPath); - await session.app.stop(); + await stopTrackedSession(context, session); assertEqual(renamedOnB, renamedContent, "Renamed note content did not round-trip to the second vault."); console.log("Two-vault note rename round-tripped."); @@ -374,24 +536,24 @@ async function runCaseOnlyRename( let session = await startConfiguredSession(context, vaultA); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, caseRenameFromPath, fileContent); await uploadNote(context, session, caseRenameFromPath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); await waitForPathContent(vaultB.path, caseRenameFromPath, (content) => content === fileContent); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultA); await renameNoteViaObsidian(context.cliBinary, session.cliEnv, caseRenameFromPath, caseRenameToPath); await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, caseRenameToPath); await pushLocalChanges(context.cliBinary, session.cliEnv); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB); await syncAndApply(context, session); const renamedOnB = await waitForPathContent(vaultB.path, caseRenameToPath, (content) => content === fileContent); - await waitForPathDeleted(vaultB.path, caseRenameFromPath); - await session.app.stop(); + await waitForExactCaseOnlyRename(vaultB.path, caseRenameFromPath, caseRenameToPath); + await stopTrackedSession(context, session); assertEqual(renamedOnB, fileContent, "Case-only note rename did not round-trip to the second vault."); console.log("Two-vault case-only note rename round-tripped without a tombstone."); @@ -413,12 +575,12 @@ async function runEncryptedRoundTrip( let session = await startConfiguredSession(context, vaultA, encryptedOverrides); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, encryptedPath, encryptedContent); await uploadNote(context, session, encryptedPath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB, encryptedOverrides); await syncAndApply(context, session); const received = await waitForPathContent(vaultB.path, encryptedPath, (content) => content === encryptedContent); - await session.app.stop(); + await stopTrackedSession(context, session); assertEqual(received, encryptedContent, "Encrypted note did not round-trip to the second vault."); console.log("Two-vault encrypted note synchronisation round-tripped."); @@ -432,31 +594,290 @@ async function runMarkdownAutoMerge( const base = "# Conflict\n\nTop anchor\n\nMiddle anchor\n\nBottom anchor\n"; const left = "# Conflict\n\nTop anchor\n\nLeft line\n\nMiddle anchor\n\nBottom anchor\n"; const right = "# Conflict\n\nTop anchor\n\nMiddle anchor\n\nRight tail\n\nBottom anchor\n"; + const conflictOverrides = { + disableMarkdownAutoMerge: true, + checkConflictOnlyOnOpen: true, + showMergeDialogOnlyOnActive: true, + }; - let session = await startConfiguredSession(context, vaultB); - await createMarkdownConflict(context, session, vaultB, conflictPath, base, left, right); - await autoMergeMarkdownConflict(context.cliBinary, session.cliEnv, conflictPath); + let session = await startConfiguredSession(context, vaultA, conflictOverrides); + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, conflictPath, base); + await uploadNote(context, session, conflictPath); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, conflictOverrides); + await syncAndApply(context, session); + const baseOnB = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, conflictPath); + await waitForPathContent(vaultB.path, conflictPath, (content) => content === base); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultA, conflictOverrides); + const baseOnA = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, conflictPath); + await storeFileRevision(context.cliBinary, session.cliEnv, conflictPath, left, baseOnA.rev); + await writeVaultFile(vaultA.path, conflictPath, left); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, conflictOverrides); + await storeFileRevision(context.cliBinary, session.cliEnv, conflictPath, right, baseOnB.rev); + await writeVaultFile(vaultB.path, conflictPath, right); + await pushLocalChanges(context.cliBinary, session.cliEnv); + const conflict = await waitForFileConflict(context.cliBinary, session.cliEnv, conflictPath); + const leftBranch = conflict.branches.find((branch) => branch.content === left); + const rightBranch = conflict.branches.find((branch) => branch.content === right); + if (!leftBranch || !rightBranch) { + throw new Error(`The two Vault edits did not form the expected conflict: ${JSON.stringify(conflict)}`); + } + + const merged = await calculateMarkdownAutoMerge(context.cliBinary, session.cliEnv, conflictPath); + if (!merged.includes("Left line") || !merged.includes("Right tail")) { + throw new Error(`Markdown auto-merge discarded a non-overlapping edit: ${JSON.stringify({ merged })}`); + } + const mergedRev = await storeFileRevision(context.cliBinary, session.cliEnv, conflictPath, merged, rightBranch.rev); + await deleteRevisionAndReflect(context.cliBinary, session.cliEnv, conflictPath, leftBranch.rev); await pushLocalChanges(context.cliBinary, session.cliEnv); const mergedOnB = await waitForPathContent( vaultB.path, conflictPath, - (content) => content.includes("Left line") && content.includes("Right tail"), + (content) => content === merged, Number(process.env.E2E_OBSIDIAN_MERGE_FILE_TIMEOUT_MS ?? 30000) ); - await session.app.stop(); - session = await startConfiguredSession(context, vaultA); + const afterResolution = `${merged.trimEnd()}\n\nPost-resolution edit on B.\n`; + await storeFileRevision(context.cliBinary, session.cliEnv, conflictPath, afterResolution, mergedRev); + await writeVaultFile(vaultB.path, conflictPath, afterResolution); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultA, conflictOverrides); await syncAndApply(context, session); - const mergedOnA = await waitForPathContent( + const resolvedOnA = await waitForPathContent( vaultA.path, conflictPath, - (content) => content.includes("Left line") && content.includes("Right tail"), + (content) => content === afterResolution, Number(process.env.E2E_OBSIDIAN_MERGE_FILE_TIMEOUT_MS ?? 30000) ); - await session.app.stop(); + const resolvedState = await readFileConflictState(context.cliBinary, session.cliEnv, conflictPath); + await stopTrackedSession(context, session); - assertEqual(mergedOnA, mergedOnB, "Merged Markdown content was not consistent across both vaults."); - console.log("Markdown conflict was automatically merged and propagated by the next synchronisation."); + assertEqual(mergedOnB, merged, "The resolving Vault did not reflect the merged Markdown content."); + assertEqual( + resolvedOnA, + afterResolution, + "The resolved Markdown content did not replace the known losing revision." + ); + assertEqual( + resolvedState.branches.length, + 1, + "The receiving Vault recreated a conflict from the known losing revision." + ); + console.log( + "A two-Vault Markdown conflict was merged, edited again, and propagated to the Vault holding the resolved losing revision." + ); +} + +async function runConflictTimeStorageOperations( + context: RunnerContext, + vaultA: TemporaryVault, + vaultB: TemporaryVault +): Promise { + const paths = [conflictEditPath, conflictDeletePath, conflictCaseFromPath, conflictRenameFromPath] as const; + const conflictOverrides = { + disableMarkdownAutoMerge: true, + checkConflictOnlyOnOpen: true, + showMergeDialogOnlyOnActive: true, + handleFilenameCaseSensitive: false, + }; + const baseContent = Object.fromEntries(paths.map((path) => [path, `# Conflict operation\n\nBase for ${path}.\n`])) as Record< + (typeof paths)[number], + string + >; + const leftContent = Object.fromEntries( + paths.map((path) => [path, `${baseContent[path]}\nEdit made on Vault A.\n`]) + ) as Record<(typeof paths)[number], string>; + const rightContent = Object.fromEntries( + paths.map((path) => [path, `${baseContent[path]}\nDisplayed edit made on Vault B.\n`]) + ) as Record<(typeof paths)[number], string>; + + let session = await startConfiguredSession(context, vaultA, conflictOverrides); + for (const path of paths) { + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, path, baseContent[path]); + await uploadNote(context, session, path); + } + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, conflictOverrides); + await syncAndApply(context, session); + for (const path of paths) { + await waitForPathContent(vaultB.path, path, (content) => content === baseContent[path]); + } + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultA, conflictOverrides); + for (const path of paths) { + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, path, leftContent[path]); + await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, path); + } + await pushLocalChanges(context.cliBinary, session.cliEnv); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, conflictOverrides); + for (const path of paths) { + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, path, rightContent[path]); + await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, path); + } + await pushLocalChanges(context.cliBinary, session.cliEnv); + + const displayedRevisions = new Map(); + const initialBranchRevisions = new Map>(); + for (const path of paths) { + const state = await waitForFileConflict(context.cliBinary, session.cliEnv, path); + const displayedBranch = state.branches.find((branch) => branch.content === rightContent[path] && !branch.deleted); + if (!displayedBranch) { + throw new Error(`Could not identify the branch displayed by Vault B: ${path}; ${JSON.stringify(state)}`); + } + const provenance = await readFileReflectionProvenance(context.cliBinary, session.cliEnv, path); + assertEqual( + provenance?.revision, + displayedBranch.rev, + `Vault B did not retain the exact displayed revision for ${path}.` + ); + displayedRevisions.set(path, displayedBranch.rev); + initialBranchRevisions.set(path, new Set(state.branches.map((branch) => branch.rev))); + } + + const editedAgain = `${rightContent[conflictEditPath]}\nSecond edit while the conflict is active.\n`; + await writeNoteViaObsidian(context.cliBinary, session.cliEnv, conflictEditPath, editedAgain); + const editedBranch = await waitForConflictBranch( + context.cliBinary, + session.cliEnv, + conflictEditPath, + (branch) => branch.content === editedAgain + ); + assertEqual( + editedBranch.parentRev, + displayedRevisions.get(conflictEditPath), + "A conflict-time edit did not extend the displayed revision." + ); + + await deleteNoteViaObsidian(context.cliBinary, session.cliEnv, conflictDeletePath); + const deletedBranch = await waitForConflictBranch( + context.cliBinary, + session.cliEnv, + conflictDeletePath, + (branch) => branch.deleted + ); + assertEqual( + deletedBranch.parentRev, + displayedRevisions.get(conflictDeletePath), + "A conflict-time deletion did not extend the displayed revision." + ); + + await renameNoteViaObsidian( + context.cliBinary, + session.cliEnv, + conflictCaseFromPath, + conflictCaseToPath + ); + const caseRenamedBranch = await waitForConflictBranch( + context.cliBinary, + session.cliEnv, + conflictCaseToPath, + (branch) => + !initialBranchRevisions.get(conflictCaseFromPath)?.has(branch.rev) && + branch.path === conflictCaseToPath && + branch.content === rightContent[conflictCaseFromPath] && + !branch.deleted + ); + const expectedCaseParent = displayedRevisions.get(conflictCaseFromPath); + if (caseRenamedBranch.parentRev !== expectedCaseParent) { + const [state, oldProvenance, newProvenance, identity] = await Promise.all([ + readFileConflictState(context.cliBinary, session.cliEnv, conflictCaseToPath), + readFileReflectionProvenance(context.cliBinary, session.cliEnv, conflictCaseFromPath), + readFileReflectionProvenance(context.cliBinary, session.cliEnv, conflictCaseToPath), + readPathIdentity(context.cliBinary, session.cliEnv, [conflictCaseFromPath, conflictCaseToPath]), + ]); + throw new Error( + `A conflict-time case-only rename did not extend the displayed revision: ${JSON.stringify({ + expectedCaseParent, + caseRenamedBranch, + state, + oldProvenance, + newProvenance, + identity, + })}` + ); + } + const [oldCaseProvenance, newCaseProvenance] = await Promise.all([ + readFileReflectionProvenance(context.cliBinary, session.cliEnv, conflictCaseFromPath), + readFileReflectionProvenance(context.cliBinary, session.cliEnv, conflictCaseToPath), + ]); + assertEqual(oldCaseProvenance, null, "A conflict-time case-only rename retained the old provenance path."); + assertEqual( + newCaseProvenance?.revision, + caseRenamedBranch.rev, + "A conflict-time case-only rename did not record the new displayed revision." + ); + + await renameNoteViaObsidian( + context.cliBinary, + session.cliEnv, + conflictRenameFromPath, + conflictRenameToPath + ); + const renamedTarget = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, conflictRenameToPath); + const renamedSourceDeletion = await waitForConflictBranch( + context.cliBinary, + session.cliEnv, + conflictRenameFromPath, + (branch) => branch.deleted + ); + assertEqual( + renamedSourceDeletion.parentRev, + displayedRevisions.get(conflictRenameFromPath), + "A conflict-time cross-path rename did not soft-delete the displayed source revision." + ); + await pushLocalChanges(context.cliBinary, session.cliEnv); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultA, conflictOverrides); + await syncAndApply(context, session); + const replicatedBranches = [ + [conflictEditPath, editedBranch], + [conflictDeletePath, deletedBranch], + [conflictCaseToPath, caseRenamedBranch], + [conflictRenameFromPath, renamedSourceDeletion], + ] as const; + for (const [path, expectedBranch] of replicatedBranches) { + const replicated = await waitForConflictBranch( + context.cliBinary, + session.cliEnv, + path, + (branch) => branch.rev === expectedBranch.rev + ); + assertEqual( + replicated.parentRev, + expectedBranch.parentRev, + `The exact conflict-operation revision tree did not replicate for ${path}.` + ); + } + await waitForPathContent( + vaultA.path, + conflictRenameToPath, + (content) => content === rightContent[conflictRenameFromPath] + ); + const targetOnA = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, conflictRenameToPath); + assertEqual(targetOnA.id, renamedTarget.id, "The cross-path rename target did not replicate as the same document."); + assertEqual( + await readVaultFile(vaultA.path, conflictDeletePath), + leftContent[conflictDeletePath], + "A logical deletion from one conflict branch removed the other Vault's live branch." + ); + await stopTrackedSession(context, session); + + console.log( + "Conflict-time edit, logical deletion, case-only rename, and cross-path rename extended the displayed branches and replicated their revision trees." + ); } async function runTargetMismatch( @@ -470,23 +891,57 @@ async function runTargetMismatch( let session = await startConfiguredSession(context, vaultA); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, targetMismatchPath, ignoredContent); await uploadNote(context, session, targetMismatchPath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB, { syncOnlyRegEx: "^E2E/two-vault/allowed/.*", }); await syncAndApply(context, session); + await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, targetMismatchPath); assertEqual( await pathExists(vaultB.path, targetMismatchPath), false, "A note was reflected on a device where it was not a target file." ); - await session.app.stop(); + await stopTrackedSession(context, session); + + session = await startConfiguredSession(context, vaultB, { + syncOnlyRegEx: "^E2E/two-vault/allowed/.*", + }); + assertEqual( + await pathExists(vaultB.path, targetMismatchPath), + false, + "A checkpointed non-target note was reflected before its target filter changed." + ); + await configureCouchDb( + context.cliBinary, + session.cliEnv, + { + uri: context.couchDb.uri, + username: context.couchDb.username, + password: context.couchDb.password, + dbName: context.dbName, + }, + { syncOnlyRegEx: "" } + ); + await syncAndApply(context, session); + const reflectedAfterEnabling = await waitForPathContent( + vaultB.path, + targetMismatchPath, + (content) => content === ignoredContent + ); + await stopTrackedSession(context, session); + + assertEqual( + reflectedAfterEnabling, + ignoredContent, + "Target file was not reflected after the device accepted the path." + ); session = await startConfiguredSession(context, vaultA); await writeNoteViaObsidian(context.cliBinary, session.cliEnv, targetMismatchPath, acceptedContent); await uploadNote(context, session, targetMismatchPath); - await session.app.stop(); + await stopTrackedSession(context, session); session = await startConfiguredSession(context, vaultB, { syncOnlyRegEx: "", @@ -497,10 +952,12 @@ async function runTargetMismatch( targetMismatchPath, (content) => content === acceptedContent ); - await session.app.stop(); + await stopTrackedSession(context, session); - assertEqual(received, acceptedContent, "Target file was not reflected after the device accepted the path."); - console.log("Two-vault target mismatch skipped a non-target note, then reflected it after enabling the target."); + assertEqual(received, acceptedContent, "Target file update was not reflected after the device accepted the path."); + console.log( + "Two-vault target mismatch skipped a non-target note, reflected it after enabling the target, and accepted a later update." + ); } async function main(): Promise { @@ -517,8 +974,22 @@ async function main(): Promise { const vaultB = await createTemporaryVault(); const encryptedVaultA = await createTemporaryVault(); const encryptedVaultB = await createTemporaryVault(); - const context: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName }; - const encryptedContext: RunnerContext = { binary, cliBinary: cli.binary, couchDb, dbName: encryptedDbName }; + const context: RunnerContext = { + binary, + cliBinary: cli.binary, + couchDb, + dbName, + reviewedVaults: new Set(), + activeSessions: new Set(), + }; + const encryptedContext: RunnerContext = { + binary, + cliBinary: cli.binary, + couchDb, + dbName: encryptedDbName, + reviewedVaults: new Set(), + activeSessions: new Set(), + }; try { await assertCouchDbReachable(couchDb); @@ -531,15 +1002,25 @@ async function main(): Promise { console.log(`Temporary CouchDB database: ${dbName}`); console.log(`Temporary encrypted CouchDB database: ${encryptedDbName}`); - await runCreateUpdateDelete(context, vaultA, vaultB); - await runRename(context, vaultA, vaultB); - await runCaseOnlyRename(context, vaultA, vaultB); - if (process.env.E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT === "true") { - await runMarkdownAutoMerge(context, vaultA, vaultB); + const onlyConflictOperations = process.env.E2E_OBSIDIAN_ONLY_CONFLICT_OPERATIONS === "true"; + if (!onlyConflictOperations) { + await runCreateUpdateDelete(context, vaultA, vaultB); + await runRename(context, vaultA, vaultB); + await runCaseOnlyRename(context, vaultA, vaultB); + if (process.env.E2E_OBSIDIAN_INCLUDE_MARKDOWN_CONFLICT === "true") { + await runMarkdownAutoMerge(context, vaultA, vaultB); + } + } + if (onlyConflictOperations || process.env.E2E_OBSIDIAN_INCLUDE_CONFLICT_OPERATIONS === "true") { + await runConflictTimeStorageOperations(context, vaultA, vaultB); + } + if (!onlyConflictOperations) { + await runTargetMismatch(context, vaultA, vaultB); + await runEncryptedRoundTrip(encryptedContext, encryptedVaultA, encryptedVaultB); } - await runTargetMismatch(context, vaultA, vaultB); - await runEncryptedRoundTrip(encryptedContext, encryptedVaultA, encryptedVaultB); } finally { + await stopTrackedSessions(context); + await stopTrackedSessions(encryptedContext); await vaultA.dispose(); await vaultB.dispose(); await encryptedVaultA.dispose(); diff --git a/test/e2e-obsidian/scripts/upgrade-from-stable.ts b/test/e2e-obsidian/scripts/upgrade-from-stable.ts new file mode 100644 index 00000000..fb7114e5 --- /dev/null +++ b/test/e2e-obsidian/scripts/upgrade-from-stable.ts @@ -0,0 +1,756 @@ +import { spawn } from "node:child_process"; +import { access, readFile, writeFile } from "node:fs/promises"; +import { basename, resolve } from "node:path"; +import { + assertCouchDbReachable, + createCouchDbDatabase, + deleteCouchDbDatabase, + fetchAllCouchDbDocs, + fetchCouchDbDatabaseInfo, + fetchCouchDbLocalDocs, + loadCouchDbConfig, + makeUniqueDatabaseName, + type CouchDbConfig, + type CouchDbDatabaseInfo, + type CouchDbDocument, +} from "../runner/couchdb.ts"; +import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts"; +import { + configureCouchDb, + configureObjectStorage, + createE2eCouchDbPluginData, + createE2eObjectStoragePluginData, + createE2eObsidianDeviceLocalState, + prepareRemote, + pushLocalChanges, + waitForLiveSyncCoreReady, +} from "../runner/liveSyncWorkflow.ts"; +import { + deleteObjectStoragePrefix, + ensureObjectStorageBucket, + listObjectStorageObjects, + loadObjectStorageConfig, + makeUniqueBucketPrefix, + readObjectStorageJson, + type ObjectStorageConfig, +} from "../runner/objectStorage.ts"; +import { ensurePinnedReleaseArtifact, UPGRADE_SOURCE_RELEASE } from "../runner/releaseArtifact.ts"; +import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; +import { + assertCouchDbCheckpointContinuity, + assertCouchDbDocumentsUnchanged, + assertJournalCheckpointAdvanced, + assertJournalCheckpointLoaded, + assertMilestoneContinuity, + assertNoJournalReplay, + assertSomeCouchDbCheckpointAdvanced, + type CouchDbCheckpointSnapshot, + type CouchDbDocumentRevision, + type MilestoneIdentity, + type RemoteObjectSnapshot, +} from "../runner/upgradeContinuity.ts"; +import { + assertStableReleaseDefaults, + assertStableRemoteSelection, + assertUnconfiguredUpgradeReady, + assertUnconfiguredUpgradeRestarted, + assertUpgradeCompatibilityReady, + assertUpgradeRemainsReady, + configureStableRelease, + createPostUpgradeDelta, + createUpgradeScenarioPaths, + createVerifierReturnDelta, + dismissConfigDoctorIfShown, + prepareStableRemote, + readJournalCheckpoint, + readLocalCouchDbCheckpoints, + readRuntimeUpgradeState, + readRuntimeSettingsUpgradeState, + runCouchDbReplicationObserved, + runJournalReplicationObserved, + runStableFileHistory, + STABLE_RELEASE_VERSION, + verifyPostUpgradeHistory, + verifyPreUpgradeHistory, + verifyReturnDelta, + waitForPersistentNodeIdentity, + type CouchDbReplicationObservation, + type RuntimeUpgradeState, + type UpgradeTransportConfiguration, +} from "../runner/upgradeWorkflow.ts"; +import { obsidianRemoteDebuggingPort } from "../runner/ui.ts"; +import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts"; + +process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000"; + +type Transport = "couchdb" | "object-storage"; + +type RemoteMilestone = CouchDbDocument & { + created?: unknown; + locked?: unknown; + accepted_nodes?: unknown; + tweak_values?: unknown; +}; + +type CouchDbRemoteSnapshot = { + checkpoints: CouchDbCheckpointSnapshot[]; + documents: CouchDbDocumentRevision[]; + info: CouchDbDatabaseInfo; + milestone: MilestoneIdentity; + preferredTweaks: Record; +}; + +type ObjectStorageRemoteSnapshot = { + journalObjects: RemoteObjectSnapshot[]; + milestone: MilestoneIdentity; + preferredTweaks: Record; +}; + +type RunnerContext = { + binary: string; + cliBinary: string; + sourceArtifactRoot: string; + targetArtifactRoot: string; + targetVersion: string; + activeSessions: Set; +}; + +type ParsedArguments = { + transports: Transport[]; + manageServices: boolean; + keepServices: boolean; +}; + +type StartSessionOptions = { + pluginData?: Record; + localStorageEntries?: Readonly>; + waitForCoreReady?: boolean; +}; + +const MILESTONE_ID = "_local/obsydian_livesync_milestone"; +const JOURNAL_MILESTONE_NAME = "_00000000-milestone.json"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +function assertEqual(actual: unknown, expected: unknown, message: string): void { + if (actual !== expected) { + throw new Error(`${message}\nExpected: ${String(expected)}\nActual: ${String(actual)}`); + } +} + +function parseArguments(argv: readonly string[]): ParsedArguments { + let transportValue = "all"; + for (let index = 0; index < argv.length; index++) { + const argument = argv[index]; + if (argument === "--transport") { + transportValue = argv[index + 1] ?? ""; + index++; + } else if (argument.startsWith("--transport=")) { + transportValue = argument.slice("--transport=".length); + } + } + const transports: Transport[] = + transportValue === "all" + ? ["couchdb", "object-storage"] + : transportValue === "couchdb" || transportValue === "object-storage" + ? [transportValue] + : (() => { + throw new Error(`Unsupported transport '${transportValue}'. Use couchdb, object-storage, or all.`); + })(); + return { + transports, + manageServices: argv.includes("--manage-services"), + keepServices: argv.includes("--keep-services"), + }; +} + +function sessionEnvironment(port: number): NodeJS.ProcessEnv { + return { ...process.env, E2E_OBSIDIAN_REMOTE_DEBUGGING_PORT: String(port) }; +} + +function sessionPorts(): readonly [number, number] { + const first = obsidianRemoteDebuggingPort(process.env); + const second = Number(process.env.E2E_OBSIDIAN_SECONDARY_REMOTE_DEBUGGING_PORT ?? first + 1); + if (!Number.isInteger(second) || second < 1 || second > 65535 || second === first) { + throw new Error(`Invalid secondary Obsidian remote debugging port: ${second}`); + } + return [first, second]; +} + +function npmBinary(): string { + return process.platform === "win32" ? "npm.cmd" : "npm"; +} + +function runNpmScript(name: string, optional = false): Promise { + return new Promise((resolvePromise, reject) => { + console.log(`\n# ${name}`); + const child = spawn(npmBinary(), ["run", name], { + cwd: process.cwd(), + env: process.env, + stdio: "inherit", + }); + child.on("error", reject); + child.on("exit", (code, signal) => { + if (code === 0 || optional) { + if (code !== 0) { + console.warn(`${name} did not complete cleanly (${signal ? `signal ${signal}` : `exit ${code}`}).`); + } + resolvePromise(); + return; + } + reject(new Error(`${name} failed (${signal ? `signal ${signal}` : `exit ${code}`}).`)); + }); + }); +} + +async function validateTargetArtifact(root: string): Promise { + await Promise.all( + ["main.js", "manifest.json", "styles.css"].map(async (name) => await access(resolve(root, name))) + ); + const manifest = JSON.parse(await readFile(resolve(root, "manifest.json"), "utf8")) as { + id?: unknown; + version?: unknown; + }; + assertEqual(manifest.id, UPGRADE_SOURCE_RELEASE.pluginId, "The target artefact has an unexpected plug-in id."); + assert(typeof manifest.version === "string" && manifest.version.length > 0, "The target manifest has no version."); + assert( + manifest.version !== STABLE_RELEASE_VERSION, + `The target artefact is still the source release ${STABLE_RELEASE_VERSION}.` + ); + return manifest.version; +} + +async function startSession( + context: RunnerContext, + vault: TemporaryVault, + port: number, + artifactRoot: string, + options: StartSessionOptions = {} +): Promise { + const session = await startObsidianLiveSyncSession({ + binary: context.binary, + cliBinary: context.cliBinary, + vault, + artifactRoot, + pluginData: options.pluginData, + localStorageEntries: options.localStorageEntries, + startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000), + env: sessionEnvironment(port), + }); + context.activeSessions.add(session); + try { + if (options.waitForCoreReady !== false) { + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + } + return session; + } catch (error) { + await stopSession(context, session).catch(() => undefined); + throw error; + } +} + +async function readStoredPluginData(vault: TemporaryVault): Promise> { + const path = resolve(vault.path, ".obsidian", "plugins", "obsidian-livesync", "data.json"); + return JSON.parse(await readFile(path, "utf8")) as Record; +} + +async function writeStoredPluginData(vault: TemporaryVault, data: Record): Promise { + const path = resolve(vault.path, ".obsidian", "plugins", "obsidian-livesync", "data.json"); + await writeFile(path, `${JSON.stringify(data, null, 2)}\n`); +} + +async function runUnconfiguredSettingsUpgrade(context: RunnerContext, port: number): Promise { + console.log(`\n# Upgrade from ${STABLE_RELEASE_VERSION}: unconfigured legacy settings`); + const vault = await createTemporaryVault("obsidian-livesync-upgrade-unconfigured-"); + + try { + let session = await startSession(context, vault, port, context.sourceArtifactRoot, { + pluginData: { liveSync: false }, + waitForCoreReady: false, + }); + const stableState = await readRuntimeSettingsUpgradeState(context.cliBinary, session.cliEnv); + assertStableReleaseDefaults(stableState, false); + await stopSession(context, session); + + const stableData = await readStoredPluginData(vault); + if (stableData.isConfigured !== undefined) { + assertEqual( + stableData.isConfigured, + false, + "The stable release persisted a configured state for its default-equivalent settings." + ); + } + + // 0.25.83 infers the runtime boolean, but persistence depends on an + // unrelated settings-save event. Restore the pre-flag document + // explicitly so the target proves the direct legacy migration in + // either case rather than depending on that timing. + await writeStoredPluginData(vault, { liveSync: false }); + + session = await startSession(context, vault, port, context.targetArtifactRoot, { + waitForCoreReady: false, + }); + const upgradedState = await readRuntimeSettingsUpgradeState(context.cliBinary, session.cliEnv); + assertUnconfiguredUpgradeReady(stableState, upgradedState, context.targetVersion); + await stopSession(context, session); + + const migratedData = await readStoredPluginData(vault); + assertEqual(migratedData.isConfigured, false, "The inferred unconfigured state was not saved."); + assertEqual( + migratedData.handleFilenameCaseSensitive, + false, + "The inferred case-insensitive setting was not saved." + ); + + session = await startSession(context, vault, port, context.targetArtifactRoot, { + waitForCoreReady: false, + }); + const restartedState = await readRuntimeSettingsUpgradeState(context.cliBinary, session.cliEnv); + assertUnconfiguredUpgradeRestarted(restartedState, context.targetVersion); + await stopSession(context, session); + + console.log( + `PASS unconfigured settings: ${STABLE_RELEASE_VERSION} -> ${context.targetVersion}; legacy inference, persistence, and restart idempotence verified.` + ); + } finally { + await stopSessions(context); + await vault.dispose(); + } +} + +async function stopSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise { + if (!context.activeSessions.has(session)) return; + await session.app.stop(); + context.activeSessions.delete(session); +} + +async function stopSessions(context: RunnerContext): Promise { + for (const session of [...context.activeSessions]) await stopSession(context, session); +} + +function milestoneIdentity(document: RemoteMilestone): MilestoneIdentity { + assert(document.created !== undefined && document.created !== null, "The remote milestone has no generation."); + assert(typeof document.locked === "boolean", "The remote milestone has no lock state."); + assert(Array.isArray(document.accepted_nodes), "The remote milestone has no accepted-device list."); + assert( + document.accepted_nodes.every((value) => typeof value === "string"), + "The remote milestone accepted-device list is malformed." + ); + return { + created: document.created, + locked: document.locked, + acceptedNodes: document.accepted_nodes, + }; +} + +function preferredTweaks(document: RemoteMilestone): Record { + const values = document.tweak_values; + assert(values !== null && typeof values === "object" && !Array.isArray(values), "The remote has no tweak map."); + const preferred = (values as Record).PREFERRED; + assert( + preferred !== null && typeof preferred === "object" && !Array.isArray(preferred), + "The remote has no preferred tweak settings." + ); + return { ...(preferred as Record) }; +} + +async function readCouchDbRemoteSnapshot(config: CouchDbConfig, databaseName: string): Promise { + const [allDocs, localDocs, info] = await Promise.all([ + fetchAllCouchDbDocs(config, databaseName), + fetchCouchDbLocalDocs(config, databaseName), + fetchCouchDbDatabaseInfo(config, databaseName), + ]); + const milestone = localDocs.rows.find(({ id }) => id === MILESTONE_ID)?.doc as RemoteMilestone | undefined; + assert(milestone, "The CouchDB remote milestone is missing."); + const checkpoints = localDocs.rows.flatMap(({ id, doc }) => + doc && Object.prototype.hasOwnProperty.call(doc, "last_seq") ? [{ id, lastSequence: doc.last_seq }] : [] + ); + const documents = allDocs.rows.map(({ id, value }) => ({ + id, + revision: value.rev, + deleted: value.deleted === true, + })); + return { + checkpoints, + documents, + info, + milestone: milestoneIdentity(milestone), + preferredTweaks: preferredTweaks(milestone), + }; +} + +async function readObjectStorageRemoteSnapshot( + config: ObjectStorageConfig, + prefix: string +): Promise { + const [objects, milestone] = await Promise.all([ + listObjectStorageObjects(config, prefix), + readObjectStorageJson(config, `${prefix}${JOURNAL_MILESTONE_NAME}`), + ]); + const journalObjects = objects.flatMap((object) => { + if (!object.Key || basename(object.Key).startsWith("_")) return []; + return [ + { + key: object.Key, + size: object.Size ?? 0, + etag: object.ETag ?? "", + }, + ]; + }); + return { + journalObjects, + milestone: milestoneIdentity(milestone), + preferredTweaks: preferredTweaks(milestone), + }; +} + +function assertNoOpCouchDbObservation(observation: CouchDbReplicationObservation): void { + assert(observation.succeeded, "The first post-upgrade CouchDB synchronisation failed."); + assertEqual(observation.sentDocuments, 0, "The no-op CouchDB synchronisation resent documents."); + assertEqual(observation.arrivedDocuments, 0, "The no-op CouchDB synchronisation refetched documents."); +} + +function assertNoOpCouchDbDatabase(before: CouchDbRemoteSnapshot, after: CouchDbRemoteSnapshot): void { + assertCouchDbCheckpointContinuity(before.checkpoints, after.checkpoints); + assertCouchDbDocumentsUnchanged(before.documents, after.documents); + assertEqual( + after.info.update_seq, + before.info.update_seq, + "The no-op CouchDB synchronisation advanced update_seq." + ); + assertEqual(after.info.doc_count, before.info.doc_count, "The no-op CouchDB synchronisation changed doc_count."); + assertMilestoneContinuity(before.milestone, after.milestone); +} + +function assertRestartContinuity(before: RuntimeUpgradeState, after: RuntimeUpgradeState): void { + assertEqual(after.localDatabaseName, before.localDatabaseName, "Restart opened a different local database."); + assertEqual(after.nodeId, before.nodeId, "Restart changed the device node identity."); + assertEqual( + after.settings.activeConfigurationId, + before.settings.activeConfigurationId, + "Restart changed the active remote profile." + ); +} + +async function configureFreshCouchDbVerifier( + context: RunnerContext, + session: ObsidianLiveSyncSession, + config: CouchDbConfig, + databaseName: string, + tweaks: Record +): Promise { + await configureCouchDb( + context.cliBinary, + session.cliEnv, + { uri: config.uri, username: config.username, password: config.password, dbName: databaseName }, + tweaks + ); + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + await prepareRemote(context.cliBinary, session.cliEnv); +} + +async function configureFreshObjectStorageVerifier( + context: RunnerContext, + session: ObsidianLiveSyncSession, + config: ObjectStorageConfig, + prefix: string, + tweaks: Record +): Promise { + await configureObjectStorage(context.cliBinary, session.cliEnv, { ...config, bucketPrefix: prefix }, tweaks); + await waitForLiveSyncCoreReady(context.cliBinary, session.cliEnv); + await prepareRemote(context.cliBinary, session.cliEnv); +} + +async function runCouchDbUpgrade(context: RunnerContext, ports: readonly [number, number]): Promise { + console.log(`\n# Upgrade from ${STABLE_RELEASE_VERSION}: CouchDB`); + const config = await loadCouchDbConfig(); + const databaseName = makeUniqueDatabaseName(config.dbPrefix, "upgrade-from-stable"); + const remote: UpgradeTransportConfiguration = { kind: "couchdb", config, databaseName }; + const paths = createUpgradeScenarioPaths("couchdb"); + const upgradeVault = await createTemporaryVault("obsidian-livesync-upgrade-couchdb-"); + const verifierVault = await createTemporaryVault("obsidian-livesync-upgrade-couchdb-verifier-"); + let upgradedSession: ObsidianLiveSyncSession | undefined; + + try { + await assertCouchDbReachable(config); + await createCouchDbDatabase(config, databaseName); + + let session = await startSession(context, upgradeVault, ports[0], context.sourceArtifactRoot); + assertStableReleaseDefaults(await readRuntimeUpgradeState(context.cliBinary, session.cliEnv), false); + await configureStableRelease(context.cliBinary, session.cliEnv, remote); + const configuredStable = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + assertStableReleaseDefaults(configuredStable, true); + assertStableRemoteSelection(configuredStable, remote); + await stopSession(context, session); + + session = await startSession(context, upgradeVault, ports[0], context.sourceArtifactRoot); + const restartedStable = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + assertStableReleaseDefaults(restartedStable, true); + assertStableRemoteSelection(restartedStable, remote); + await waitForPersistentNodeIdentity(context.cliBinary, session.cliEnv); + await prepareStableRemote(context.cliBinary, session.cliEnv); + await runStableFileHistory(context.cliBinary, session.cliEnv, paths, async () => { + const result = await runCouchDbReplicationObserved(context.cliBinary, session.cliEnv); + assert(result.succeeded, "The stable CouchDB synchronisation failed."); + }); + await verifyPreUpgradeHistory(upgradeVault, paths); + + const stableState = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + const stableRemote = await readCouchDbRemoteSnapshot(config, databaseName); + const stableLocalCheckpoints = await readLocalCouchDbCheckpoints( + context.cliBinary, + session.cliEnv, + stableRemote.checkpoints.map(({ id }) => id) + ); + assertCouchDbCheckpointContinuity(stableRemote.checkpoints, stableLocalCheckpoints); + await stopSession(context, session); + + session = await startSession(context, upgradeVault, ports[0], context.targetArtifactRoot); + upgradedSession = session; + await dismissConfigDoctorIfShown(session.remoteDebuggingPort); + const upgradedState = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + assertUpgradeCompatibilityReady(stableState, upgradedState, context.targetVersion, remote); + await verifyPreUpgradeHistory(upgradeVault, paths); + + const loadedLocalCheckpoints = await readLocalCouchDbCheckpoints( + context.cliBinary, + session.cliEnv, + stableRemote.checkpoints.map(({ id }) => id) + ); + assertCouchDbCheckpointContinuity(stableLocalCheckpoints, loadedLocalCheckpoints); + const noOpObservation = await runCouchDbReplicationObserved(context.cliBinary, session.cliEnv); + assertNoOpCouchDbObservation(noOpObservation); + const noOpRemote = await readCouchDbRemoteSnapshot(config, databaseName); + assertNoOpCouchDbDatabase(stableRemote, noOpRemote); + + await createPostUpgradeDelta(context.cliBinary, session.cliEnv, paths); + const deltaObservation = await runCouchDbReplicationObserved(context.cliBinary, session.cliEnv); + assert(deltaObservation.succeeded, "The post-upgrade CouchDB delta failed."); + assert(deltaObservation.sentDocuments > 0, "The post-upgrade CouchDB delta sent no documents."); + const deltaRemote = await readCouchDbRemoteSnapshot(config, databaseName); + assertSomeCouchDbCheckpointAdvanced(noOpRemote.checkpoints, deltaRemote.checkpoints); + assertMilestoneContinuity(noOpRemote.milestone, deltaRemote.milestone); + + const verifierSettings = { + uri: config.uri, + username: config.username, + password: config.password, + dbName: databaseName, + }; + const verifier = await startSession(context, verifierVault, ports[1], context.targetArtifactRoot, { + pluginData: createE2eCouchDbPluginData(verifierSettings, deltaRemote.preferredTweaks), + localStorageEntries: createE2eObsidianDeviceLocalState(verifierVault.name), + }); + await configureFreshCouchDbVerifier(context, verifier, config, databaseName, deltaRemote.preferredTweaks); + await pushLocalChanges(context.cliBinary, verifier.cliEnv); + await verifyPostUpgradeHistory(verifierVault, paths); + await createVerifierReturnDelta(context.cliBinary, verifier.cliEnv, paths); + await pushLocalChanges(context.cliBinary, verifier.cliEnv); + + const returnObservation = await runCouchDbReplicationObserved(context.cliBinary, session.cliEnv); + assert(returnObservation.succeeded, "The upgraded CouchDB device could not receive the verifier delta."); + assert(returnObservation.arrivedDocuments > 0, "The verifier CouchDB delta did not arrive."); + await verifyReturnDelta(upgradeVault, paths); + await stopSession(context, verifier); + await stopSession(context, session); + upgradedSession = undefined; + + const restarted = await startSession(context, upgradeVault, ports[0], context.targetArtifactRoot); + const restartedState = await readRuntimeUpgradeState(context.cliBinary, restarted.cliEnv); + assertUpgradeRemainsReady(restartedState, context.targetVersion); + assertRestartContinuity(upgradedState, restartedState); + await verifyReturnDelta(upgradeVault, paths); + await stopSession(context, restarted); + + console.log( + `PASS CouchDB: ${STABLE_RELEASE_VERSION} -> ${context.targetVersion}; checkpoint lineage, no-op sync, delta sync, fresh-device round-trip, and restart continuity verified.` + ); + } finally { + if (upgradedSession) await stopSession(context, upgradedSession).catch(() => undefined); + await stopSessions(context); + await Promise.all([upgradeVault.dispose(), verifierVault.dispose()]); + if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") { + await deleteCouchDbDatabase(config, databaseName).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + } + } +} + +async function runObjectStorageUpgrade(context: RunnerContext, ports: readonly [number, number]): Promise { + console.log(`\n# Upgrade from ${STABLE_RELEASE_VERSION}: Object Storage`); + const config = await loadObjectStorageConfig(); + const prefix = makeUniqueBucketPrefix("upgrade-from-stable"); + const remote: UpgradeTransportConfiguration = { kind: "object-storage", config, bucketPrefix: prefix }; + const paths = createUpgradeScenarioPaths("object-storage"); + const upgradeVault = await createTemporaryVault("obsidian-livesync-upgrade-object-storage-"); + const verifierVault = await createTemporaryVault("obsidian-livesync-upgrade-object-storage-verifier-"); + let upgradedSession: ObsidianLiveSyncSession | undefined; + + try { + await ensureObjectStorageBucket(config); + + let session = await startSession(context, upgradeVault, ports[0], context.sourceArtifactRoot); + assertStableReleaseDefaults(await readRuntimeUpgradeState(context.cliBinary, session.cliEnv), false); + await configureStableRelease(context.cliBinary, session.cliEnv, remote); + const configuredStable = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + assertStableReleaseDefaults(configuredStable, true); + assertStableRemoteSelection(configuredStable, remote); + await stopSession(context, session); + + session = await startSession(context, upgradeVault, ports[0], context.sourceArtifactRoot); + const restartedStable = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + assertStableReleaseDefaults(restartedStable, true); + assertStableRemoteSelection(restartedStable, remote); + await waitForPersistentNodeIdentity(context.cliBinary, session.cliEnv); + await prepareStableRemote(context.cliBinary, session.cliEnv); + await runStableFileHistory(context.cliBinary, session.cliEnv, paths, async () => { + const result = await runJournalReplicationObserved(context.cliBinary, session.cliEnv); + assert( + result.succeeded, + `The stable Object Storage synchronisation failed.\nObservation: ${JSON.stringify(result)}` + ); + }); + await verifyPreUpgradeHistory(upgradeVault, paths); + + const stableState = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + const stableCheckpoint = await readJournalCheckpoint(context.cliBinary, session.cliEnv); + const stableRemote = await readObjectStorageRemoteSnapshot(config, prefix); + await stopSession(context, session); + + session = await startSession(context, upgradeVault, ports[0], context.targetArtifactRoot); + upgradedSession = session; + await dismissConfigDoctorIfShown(session.remoteDebuggingPort); + const upgradedState = await readRuntimeUpgradeState(context.cliBinary, session.cliEnv); + assertUpgradeCompatibilityReady(stableState, upgradedState, context.targetVersion, remote); + await verifyPreUpgradeHistory(upgradeVault, paths); + + const loadedCheckpoint = await readJournalCheckpoint(context.cliBinary, session.cliEnv); + assertJournalCheckpointLoaded(stableCheckpoint, loadedCheckpoint); + const noOpObservation = await runJournalReplicationObserved(context.cliBinary, session.cliEnv); + assert(noOpObservation.succeeded, "The first post-upgrade Object Storage synchronisation failed."); + const noOpCheckpoint = await readJournalCheckpoint(context.cliBinary, session.cliEnv); + const noOpRemote = await readObjectStorageRemoteSnapshot(config, prefix); + assertNoJournalReplay( + stableCheckpoint, + noOpCheckpoint, + stableRemote.journalObjects, + noOpRemote.journalObjects, + noOpObservation + ); + assertMilestoneContinuity(stableRemote.milestone, noOpRemote.milestone); + + await createPostUpgradeDelta(context.cliBinary, session.cliEnv, paths); + const deltaObservation = await runJournalReplicationObserved(context.cliBinary, session.cliEnv); + assert(deltaObservation.succeeded, "The post-upgrade Object Storage delta failed."); + const deltaCheckpoint = await readJournalCheckpoint(context.cliBinary, session.cliEnv); + assertJournalCheckpointAdvanced(noOpCheckpoint, deltaCheckpoint, deltaObservation); + const deltaRemote = await readObjectStorageRemoteSnapshot(config, prefix); + assertMilestoneContinuity(noOpRemote.milestone, deltaRemote.milestone); + + const verifierSettings = { ...config, bucketPrefix: prefix }; + const verifier = await startSession(context, verifierVault, ports[1], context.targetArtifactRoot, { + pluginData: createE2eObjectStoragePluginData(verifierSettings, deltaRemote.preferredTweaks), + localStorageEntries: createE2eObsidianDeviceLocalState(verifierVault.name), + }); + await configureFreshObjectStorageVerifier(context, verifier, config, prefix, deltaRemote.preferredTweaks); + await pushLocalChanges(context.cliBinary, verifier.cliEnv); + await verifyPostUpgradeHistory(verifierVault, paths); + await createVerifierReturnDelta(context.cliBinary, verifier.cliEnv, paths); + await pushLocalChanges(context.cliBinary, verifier.cliEnv); + + const returnObservation = await runJournalReplicationObserved(context.cliBinary, session.cliEnv); + assert(returnObservation.succeeded, "The upgraded Object Storage device could not receive the verifier delta."); + assert(returnObservation.downloadedJournalKeys.length > 0, "The verifier Object Storage delta did not arrive."); + await verifyReturnDelta(upgradeVault, paths); + await stopSession(context, verifier); + await stopSession(context, session); + upgradedSession = undefined; + + const restarted = await startSession(context, upgradeVault, ports[0], context.targetArtifactRoot); + const restartedState = await readRuntimeUpgradeState(context.cliBinary, restarted.cliEnv); + assertUpgradeRemainsReady(restartedState, context.targetVersion); + assertRestartContinuity(upgradedState, restartedState); + await verifyReturnDelta(upgradeVault, paths); + await stopSession(context, restarted); + + console.log( + `PASS Object Storage: ${STABLE_RELEASE_VERSION} -> ${context.targetVersion}; checkpoint lineage, no replay, delta sync, fresh-device round-trip, and restart continuity verified.` + ); + } finally { + if (upgradedSession) await stopSession(context, upgradedSession).catch(() => undefined); + await stopSessions(context); + await Promise.all([upgradeVault.dispose(), verifierVault.dispose()]); + if (process.env.E2E_OBSIDIAN_KEEP_OBJECT_STORAGE !== "true") { + await deleteObjectStoragePrefix(config, prefix).catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : error); + }); + } + } +} + +async function startManagedServices(transports: readonly Transport[]): Promise { + if (transports.includes("couchdb")) { + await runNpmScript("test:docker-couchdb:stop", true); + await runNpmScript("test:docker-couchdb:start"); + } + if (transports.includes("object-storage")) { + await runNpmScript("test:docker-s3:stop", true); + await runNpmScript("test:docker-s3:start"); + } +} + +async function stopManagedServices(transports: readonly Transport[]): Promise { + if (transports.includes("object-storage")) await runNpmScript("test:docker-s3:stop", true); + if (transports.includes("couchdb")) await runNpmScript("test:docker-couchdb:stop", true); +} + +async function main(): Promise { + const arguments_ = parseArguments(process.argv.slice(2)); + const binary = requireObsidianBinary(); + const cli = discoverObsidianCli(); + if (!cli.binary) throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`); + + const targetArtifactRoot = resolve(process.env.E2E_LIVESYNC_TARGET_ARTIFACT_ROOT?.trim() || process.cwd()); + const targetVersion = await validateTargetArtifact(targetArtifactRoot); + const sourceArtifactRoot = await ensurePinnedReleaseArtifact(); + const context: RunnerContext = { + binary, + cliBinary: cli.binary, + sourceArtifactRoot, + targetArtifactRoot, + targetVersion, + activeSessions: new Set(), + }; + const ports = sessionPorts(); + let managedServicesStarted = false; + + console.log(`Using exact source release: ${STABLE_RELEASE_VERSION}`); + console.log(`Using target release candidate: ${targetVersion}`); + console.log(`Source artefact cache: ${sourceArtifactRoot}`); + console.log(`Target artefact root: ${targetArtifactRoot}`); + + try { + await runUnconfiguredSettingsUpgrade(context, ports[0]); + if (arguments_.manageServices) { + await startManagedServices(arguments_.transports); + managedServicesStarted = true; + } + for (const transport of arguments_.transports) { + if (transport === "couchdb") await runCouchDbUpgrade(context, ports); + else await runObjectStorageUpgrade(context, ports); + } + } finally { + await stopSessions(context); + if (managedServicesStarted && !arguments_.keepServices) { + await stopManagedServices(arguments_.transports); + } + } +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.stack : error); + process.exit(1); +}); diff --git a/test/fixtures/p2p-relay/compose.yml b/test/fixtures/p2p-relay/compose.yml new file mode 100644 index 00000000..44ab17ce --- /dev/null +++ b/test/fixtures/p2p-relay/compose.yml @@ -0,0 +1,12 @@ +services: + p2p-relay: + image: ghcr.io/hoytech/strfry:latest + container_name: livesync-e2e-p2p-relay + entrypoint: ["/app/strfry"] + command: ["--config", "/etc/strfry/strfry.conf", "relay"] + ports: + - "${E2E_P2P_RELAY_PORT:-4010}:7777" + volumes: + - ./strfry.conf:/etc/strfry/strfry.conf:ro + tmpfs: + - /app/strfry-db:rw,size=256m,mode=1777 diff --git a/test/fixtures/p2p-relay/strfry.conf b/test/fixtures/p2p-relay/strfry.conf new file mode 100644 index 00000000..5e91d3e7 --- /dev/null +++ b/test/fixtures/p2p-relay/strfry.conf @@ -0,0 +1,19 @@ +db = "./strfry-db/" + +relay { + bind = "0.0.0.0" + port = 7777 + nofiles = 100000 + + info { + name = "Self-hosted LiveSync E2E relay" + description = "Local Nostr signalling fixture for real-Obsidian P2P tests" + } + + maxWebsocketPayloadSize = 131072 + autoPingSeconds = 55 + + writePolicy { + plugin = "" + } +} diff --git a/test/harness/harness.ts b/test/harness/harness.ts deleted file mode 100644 index ef2d20eb..00000000 --- a/test/harness/harness.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { App } from "@/deps.ts"; -import ObsidianLiveSyncPlugin from "@/main"; -import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; -import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@lib/common/logger"; -import { SettingCache } from "./obsidian-mock"; -import { delay, fireAndForget, promiseWithResolvers } from "octagonal-wheels/promises"; -import { EVENT_PLATFORM_UNLOADED } from "@lib/events/coreEvents"; -import { EVENT_LAYOUT_READY, eventHub } from "@/common/events"; - -import { env } from "../suite/variables"; - -export type LiveSyncHarness = { - app: App; - plugin: ObsidianLiveSyncPlugin; - dispose: () => Promise; - disposalPromise: Promise; - isDisposed: () => boolean; -}; -const isLiveSyncLogEnabled = env?.PRINT_LIVESYNC_LOGS === "true"; -function overrideLogFunction(vaultName: string) { - setGlobalLogFunction((msg, level, key) => { - if (!isLiveSyncLogEnabled) { - return; - } - if (level && level < LOG_LEVEL_VERBOSE) { - return; - } - if (msg instanceof Error) { - console.error(msg.stack); - } else { - console.log( - `[${vaultName}] :: [${key ?? "Global"}][${level ?? 1}]: ${msg instanceof Error ? msg.stack : msg}` - ); - } - }); -} - -export async function generateHarness( - paramVaultName?: string, - settings?: Partial -): Promise { - // return await serialized("harness-generation-lock", async () => { - // Dispose previous harness to avoid multiple harness running at the same time - // if (previousHarness && !previousHarness.isDisposed()) { - // console.log(`Previous harness detected, waiting for disposal...`); - // await previousHarness.disposalPromise; - // previousHarness = null; - // await delay(100); - // } - const vaultName = paramVaultName ?? "TestVault" + Date.now(); - const setting = { - ...DEFAULT_SETTINGS, - ...settings, - }; - overrideLogFunction(vaultName); - //@ts-ignore Mocked in harness - const app = new App(vaultName); - // setting and vault name - SettingCache.set(app, setting); - SettingCache.set(app.vault, vaultName); - - //@ts-ignore - const manifest_version = `${MANIFEST_VERSION || "0.0.0-harness"}`; - overrideLogFunction(vaultName); - const manifest = { - id: "obsidian-livesync", - name: "Self-hosted LiveSync (Harnessed)", - version: manifest_version, - minAppVersion: "0.15.0", - description: "Testing", - author: "vrtmrz", - authorUrl: "", - isDesktopOnly: false, - }; - - const plugin = new ObsidianLiveSyncPlugin(app, manifest); - overrideLogFunction(vaultName); - // Initial load - await delay(100); - await plugin.onload(); - let isDisposed = false; - const waitPromise = promiseWithResolvers(); - eventHub.once(EVENT_PLATFORM_UNLOADED, () => { - fireAndForget(async () => { - console.log(`Harness for vault '${vaultName}' disposed.`); - await delay(100); - eventHub.offAll(); - isDisposed = true; - waitPromise.resolve(); - }); - }); - eventHub.once(EVENT_LAYOUT_READY, () => { - plugin.app.vault.trigger("layout-ready"); - }); - const harness: LiveSyncHarness = { - app, - plugin, - dispose: async () => { - await plugin.onunload(); - return waitPromise.promise; - }, - disposalPromise: waitPromise.promise, - isDisposed: () => isDisposed, - }; - await delay(100); - console.log(`Harness for vault '${vaultName}' is ready.`); - // previousHarness = harness; - return harness; -} -export async function waitForReady(harness: LiveSyncHarness): Promise { - for (let i = 0; i < 10; i++) { - if (harness.plugin.core.services.appLifecycle.isReady()) { - console.log("App Lifecycle is ready"); - return; - } - await delay(100); - } - throw new Error(`Initialisation Timed out!`); -} - -export async function waitForIdle(harness: LiveSyncHarness): Promise { - for (let i = 0; i < 20; i++) { - await delay(25); - const processing = - harness.plugin.core.services.replication.databaseQueueCount.value + - harness.plugin.core.services.fileProcessing.totalQueued.value + - harness.plugin.core.services.fileProcessing.batched.value + - harness.plugin.core.services.fileProcessing.processing.value + - harness.plugin.core.services.replication.storageApplyingCount.value; - - if (processing === 0) { - if (i > 0) { - console.log(`Idle after ${i} loops`); - } - return; - } - } -} -export async function waitForClosed(harness: LiveSyncHarness): Promise { - await delay(100); - for (let i = 0; i < 10; i++) { - if (harness.plugin.core.services.control.hasUnloaded()) { - console.log("App has unloaded"); - return; - } - await delay(100); - } -} diff --git a/test/harness/utils/intercept.ts b/test/harness/utils/intercept.ts deleted file mode 100644 index 098a924e..00000000 --- a/test/harness/utils/intercept.ts +++ /dev/null @@ -1,51 +0,0 @@ -export function interceptFetchForLogging() { - const originalFetch = globalThis.fetch; - globalThis.fetch = async (...params: any[]) => { - const paramObj = params[0]; - const initObj = params[1]; - const url = typeof paramObj === "string" ? paramObj : paramObj.url; - const method = initObj?.method || "GET"; - const headers = initObj?.headers || {}; - const body = initObj?.body || null; - const headersObj: Record = {}; - if (headers instanceof Headers) { - headers.forEach((value, key) => { - headersObj[key] = value; - }); - } - console.dir({ - mockedFetch: { - url, - method, - headers: headersObj, - }, - }); - try { - const res = await originalFetch.apply(globalThis, params as any); - console.log(`[Obsidian Mock] Fetch response: ${res.status} ${res.statusText} for ${method} ${url}`); - const resClone = res.clone(); - const contentType = resClone.headers.get("content-type") || ""; - const isJson = contentType.includes("application/json"); - if (isJson) { - const data = await resClone.json(); - console.dir({ mockedFetchResponseJson: data }); - } else { - const ab = await resClone.arrayBuffer(); - const text = new TextDecoder().decode(ab); - const isText = /^text\//.test(contentType); - if (isText) { - console.dir({ - mockedFetchResponseText: ab.byteLength < 1000 ? text : text.slice(0, 1000) + "...(truncated)", - }); - } else { - console.log(`[Obsidian Mock] Fetch response is of content-type ${contentType}, not logging body.`); - } - } - return res; - } catch (e) { - // console.error("[Obsidian Mock] Fetch error:", e); - console.error(`[Obsidian Mock] Fetch failed for ${method} ${url}, error:`, e); - throw e; - } - }; -} diff --git a/test/lib/commands.ts b/test/lib/commands.ts deleted file mode 100644 index 762b5c0c..00000000 --- a/test/lib/commands.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { P2PSyncSetting } from "@/lib/src/common/types"; -import { delay } from "octagonal-wheels/promises"; -import type { BrowserContext, Page } from "playwright"; -import type { Plugin } from "vitest/config"; -import type { BrowserCommand } from "vitest/node"; -import { serialized } from "octagonal-wheels/concurrency/lock"; -export const grantClipboardPermissions: BrowserCommand = async (ctx) => { - if (ctx.provider.name === "playwright") { - await ctx.context.grantPermissions(["clipboard-read", "clipboard-write"]); - console.log("Granted clipboard permissions"); - return; - } -}; -let peerPage: Page | undefined; -let peerPageContext: BrowserContext | undefined; -let previousName = ""; -async function setValue(page: Page, selector: string, value: string) { - const e = await page.waitForSelector(selector); - await e.fill(value); -} -async function closePeerContexts() { - const peerPageLocal = peerPage; - const peerPageContextLocal = peerPageContext; - if (peerPageLocal) { - await peerPageLocal.close(); - } - if (peerPageContextLocal) { - await peerPageContextLocal.close(); - } -} -export const openWebPeer: BrowserCommand<[P2PSyncSetting, serverPeerName: string]> = async ( - ctx, - setting: P2PSyncSetting, - serverPeerName: string = "p2p-livesync-web-peer" -) => { - if (ctx.provider.name === "playwright") { - const previousPage = ctx.page; - if (peerPage !== undefined) { - if (previousName === serverPeerName) { - console.log(`WebPeer for ${serverPeerName} already opened`); - return; - } - console.log(`Closing previous WebPeer for ${previousName}`); - await closePeerContexts(); - } - console.log(`Opening webPeer`); - return serialized("webpeer", async () => { - const browser = ctx.context.browser()!; - const context = await browser.newContext(); - peerPageContext = context; - peerPage = await context.newPage(); - previousName = serverPeerName; - console.log(`Navigating...`); - await peerPage.goto("http://localhost:8081"); - await peerPage.waitForLoadState(); - console.log(`Navigated!`); - await setValue(peerPage, "#app > main [placeholder*=wss]", setting.P2P_relays); - await setValue(peerPage, "#app > main [placeholder*=anything]", setting.P2P_roomID); - await setValue(peerPage, "#app > main [placeholder*=password]", setting.P2P_passphrase); - await setValue(peerPage, "#app > main [placeholder*=iphone]", serverPeerName); - // await peerPage.getByTitle("Enable P2P Replicator").setChecked(true); - await peerPage.getByRole("checkbox").first().setChecked(true); - // (await peerPage.waitForSelector("Save and Apply")).click(); - await peerPage.getByText("Save and Apply").click(); - await delay(100); - await peerPage.reload(); - await delay(500); - for (let i = 0; i < 10; i++) { - await delay(100); - const btn = peerPage.getByRole("button").filter({ hasText: /^connect/i }); - if ((await peerPage.getByText(/disconnect/i).count()) > 0) { - break; - } - await btn.click(); - } - await previousPage.bringToFront(); - ctx.context.on("close", async () => { - console.log("Browser context is closing, closing peer page if exists"); - await closePeerContexts(); - }); - console.log("Web peer page opened"); - }); - } -}; - -export const closeWebPeer: BrowserCommand = async (ctx) => { - if (ctx.provider.name === "playwright") { - return serialized("webpeer", async () => { - await closePeerContexts(); - peerPage = undefined; - peerPageContext = undefined; - previousName = ""; - console.log("Web peer page closed"); - }); - } -}; -export const acceptWebPeer: BrowserCommand = async (ctx) => { - if (peerPage) { - // Detect dialogue - const buttonsOnDialogs = await peerPage.$$("popup .buttons button"); - for (const b of buttonsOnDialogs) { - const text = (await b.innerText()).toLowerCase(); - // console.log(`Dialog button found: ${text}`); - if (text === "accept") { - console.log("Accepting dialog"); - await b.click({ timeout: 300 }); - await delay(500); - } - } - const buttons = peerPage.getByRole("button").filter({ hasText: /^accept$/i }); - const a = await buttons.all(); - for (const b of a) { - await b.click({ timeout: 300 }); - } - } - return false; -}; - -/** Write arbitrary text to a file on the Node.js host (used for phase handoff). */ -export const writeHandoffFile: BrowserCommand<[filePath: string, content: string]> = async ( - _ctx, - filePath: string, - content: string -) => { - const fs = await import("node:fs/promises"); - await fs.writeFile(filePath, content, "utf-8"); -}; - -/** Read a file from the Node.js host (used for phase handoff). */ -export const readHandoffFile: BrowserCommand<[filePath: string]> = async (_ctx, filePath: string): Promise => { - const fs = await import("node:fs/promises"); - return fs.readFile(filePath, "utf-8"); -}; - -export default function BrowserCommands(): Plugin { - return { - name: "vitest:custom-commands", - config() { - return { - test: { - browser: { - commands: { - grantClipboardPermissions, - openWebPeer, - closeWebPeer, - acceptWebPeer, - writeHandoffFile, - readHandoffFile, - }, - }, - }, - }; - }, - }; -} -declare module "vitest/browser" { - interface BrowserCommands { - grantClipboardPermissions: () => Promise; - openWebPeer: (setting: P2PSyncSetting, serverPeerName: string) => Promise; - closeWebPeer: () => Promise; - acceptWebPeer: () => Promise; - writeHandoffFile: (filePath: string, content: string) => Promise; - readHandoffFile: (filePath: string) => Promise; - } -} diff --git a/test/lib/ui.ts b/test/lib/ui.ts deleted file mode 100644 index 3d2381a6..00000000 --- a/test/lib/ui.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { page } from "vitest/browser"; -import { delay } from "@/lib/src/common/utils"; - -export async function waitForDialogShown(dialogText: string, timeout = 500) { - const ttl = Date.now() + timeout; - while (Date.now() < ttl) { - try { - await delay(50); - const dialog = page - .getByText(dialogText) - .elements() - .filter((e) => e.classList.contains("modal-title")) - .filter((e) => e.checkVisibility()); - if (dialog.length === 0) { - continue; - } - return true; - } catch (e) { - // Ignore - } - } - return false; -} -export async function waitForDialogHidden(dialogText: string | RegExp, timeout = 500) { - const ttl = Date.now() + timeout; - while (Date.now() < ttl) { - try { - await delay(50); - const dialog = page - .getByText(dialogText) - .elements() - .filter((e) => e.classList.contains("modal-title")) - .filter((e) => e.checkVisibility()); - if (dialog.length > 0) { - // console.log(`Still exist ${dialogText.toString()}`); - continue; - } - return true; - } catch (e) { - // Ignore - } - } - return false; -} - -export async function waitForButtonClick(buttonText: string | RegExp, timeout = 500) { - const ttl = Date.now() + timeout; - while (Date.now() < ttl) { - try { - await delay(100); - const buttons = page - .getByText(buttonText) - .elements() - .filter((e) => e.checkVisibility() && e.tagName.toLowerCase() == "button"); - if (buttons.length == 0) { - // console.log(`Could not found ${buttonText.toString()}`); - continue; - } - console.log(`Button detected: ${buttonText.toString()}`); - // console.dir(buttons[0]) - await page.elementLocator(buttons[0]).click(); - await delay(100); - return true; - } catch (e) { - console.error(e); - // Ignore - } - } - return false; -} diff --git a/test/lib/util.ts b/test/lib/util.ts deleted file mode 100644 index 502d0d2c..00000000 --- a/test/lib/util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { delay } from "@/lib/src/common/utils"; - -export async function waitTaskWithFollowups( - task: Promise, - followup: () => Promise, - timeout: number = 10000, - interval: number = 1000 -): Promise { - const symbolNotCompleted = Symbol("notCompleted"); - const isCompleted = () => Promise.race([task, Promise.resolve(symbolNotCompleted)]); - const ttl = Date.now() + timeout; - do { - const state = await isCompleted(); - if (state !== symbolNotCompleted) { - return state; - } - await followup(); - await delay(interval); - } while (Date.now() < ttl); - throw new Error("Task did not complete in time"); -} diff --git a/test/shell/p2p-init.sh b/test/shell/p2p-init.sh deleted file mode 100755 index dd865c9c..00000000 --- a/test/shell/p2p-init.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -echo "P2P Init - No additional initialization required." \ No newline at end of file diff --git a/test/shell/p2p-start.sh b/test/shell/p2p-start.sh deleted file mode 100755 index 8c86a45c..00000000 --- a/test/shell/p2p-start.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -e -script_dir=$(dirname "$0") -webpeer_dir=$script_dir/../../src/apps/webpeer - -docker run -d --name relay-test -p 4000:7777 \ - --tmpfs /app/strfry-db:rw,size=256m \ - --entrypoint sh \ - ghcr.io/hoytech/strfry:latest \ - -lc 'cat > /tmp/strfry.conf <<"EOF" -db = "./strfry-db/" - -relay { - bind = "0.0.0.0" - port = 7777 - nofiles = 100000 - - info { - name = "livesync test relay" - description = "local relay for livesync p2p tests" - } - - maxWebsocketPayloadSize = 131072 - autoPingSeconds = 55 - - writePolicy { - plugin = "" - } -} -EOF -exec /app/strfry --config /tmp/strfry.conf relay' -npm run --prefix $webpeer_dir build -docker run -d --name webpeer-test -p 8081:8043 -v $webpeer_dir/dist:/srv/http pierrezemb/gostatic \ No newline at end of file diff --git a/test/shell/p2p-stop.sh b/test/shell/p2p-stop.sh deleted file mode 100755 index 22925ad4..00000000 --- a/test/shell/p2p-stop.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -docker stop relay-test -docker rm relay-test -docker stop webpeer-test -docker rm webpeer-test \ No newline at end of file diff --git a/test/suite/db_common.ts b/test/suite/db_common.ts deleted file mode 100644 index f81f084f..00000000 --- a/test/suite/db_common.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { compareMTime, EVEN } from "@/common/utils"; -import { TFile, type DataWriteOptions } from "@/deps"; -import type { FilePath } from "@/lib/src/common/types"; -import { isDocContentSame, readContent } from "@/lib/src/common/utils"; -import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { expect } from "vitest"; - -export const defaultFileOption = { - mtime: new Date(2026, 0, 1, 0, 1, 2, 3).getTime(), -} as const satisfies DataWriteOptions; -export async function storeFile( - harness: LiveSyncHarness, - path: string, - content: string | Blob, - deleteBeforeSend = false, - fileOptions = defaultFileOption -) { - if (deleteBeforeSend && harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - if (content instanceof Blob) { - console.log(`Creating binary file ${path}`); - await harness.app.vault.createBinary(path, await content.arrayBuffer(), fileOptions); - } else { - await harness.app.vault.create(path, content, fileOptions); - } - - // Ensure file is created - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - expect(compareMTime(file.stat.mtime, fileOptions?.mtime ?? defaultFileOption.mtime)).toBe(EVEN); - if (content instanceof Blob) { - const readContent = await harness.app.vault.readBinary(file); - expect(await isDocContentSame(readContent, content)).toBe(true); - } else { - const readContent = await harness.app.vault.read(file); - expect(readContent).toBe(content); - } - } - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - return file; -} -export async function readFromLocalDB(harness: LiveSyncHarness, path: string) { - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - return entry; -} -export async function readFromVault( - harness: LiveSyncHarness, - path: string, - isBinary: boolean = false, - fileOptions = defaultFileOption -): Promise { - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - // console.log(`MTime: ${file.stat.mtime}, Expected: ${fileOptions.mtime}`); - if (fileOptions.mtime !== undefined) { - expect(compareMTime(file.stat.mtime, fileOptions.mtime)).toBe(EVEN); - } - const content = isBinary ? await harness.app.vault.readBinary(file) : await harness.app.vault.read(file); - return content; - } - - throw new Error("File not found in vault"); -} -export async function checkStoredFileInDB( - harness: LiveSyncHarness, - path: string, - content: string | Blob, - fileOptions = defaultFileOption -) { - const entry = await readFromLocalDB(harness, path); - if (entry === false) { - throw new Error("DB Content not found"); - } - const contentToCheck = content instanceof Blob ? await content.arrayBuffer() : content; - const isDocSame = await isDocContentSame(readContent(entry), contentToCheck); - if (fileOptions.mtime !== undefined) { - expect(compareMTime(entry.mtime, fileOptions.mtime)).toBe(EVEN); - } - expect(isDocSame).toBe(true); - return Promise.resolve(); -} -export async function testFileWrite( - harness: LiveSyncHarness, - path: string, - content: string | Blob, - skipCheckToBeWritten = false, - fileOptions = defaultFileOption -) { - const file = await storeFile(harness, path, content, false, fileOptions); - expect(file).toBeInstanceOf(TFile); - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - const vaultFile = await readFromVault(harness, path, content instanceof Blob, fileOptions); - expect(await isDocContentSame(vaultFile, content)).toBe(true); - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - if (skipCheckToBeWritten) { - return Promise.resolve(); - } - await checkStoredFileInDB(harness, path, content); - return Promise.resolve(); -} -export async function testFileRead( - harness: LiveSyncHarness, - path: string, - expectedContent: string | Blob, - fileOptions = defaultFileOption -) { - await waitForIdle(harness); - const file = await readFromVault(harness, path, expectedContent instanceof Blob, fileOptions); - const isDocSame = await isDocContentSame(file, expectedContent); - expect(isDocSame).toBe(true); - // Check local database entry - const entry = await readFromLocalDB(harness, path); - expect(entry).not.toBe(false); - if (entry === false) { - throw new Error("DB Content not found"); - } - const isDBDocSame = await isDocContentSame(readContent(entry), expectedContent); - expect(isDBDocSame).toBe(true); - return await Promise.resolve(); -} diff --git a/test/suite/onlylocaldb.test.ts b/test/suite/onlylocaldb.test.ts deleted file mode 100644 index acfbb65b..00000000 --- a/test/suite/onlylocaldb.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { beforeAll, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { TFile } from "@/deps.ts"; -import { DEFAULT_SETTINGS, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; -import { isDocContentSame, readContent } from "@/lib/src/common/utils"; -import { DummyFileSourceInisialised, generateBinaryFile, generateFile, init } from "../utils/dummyfile"; - -const localdb_test_setting = { - ...DEFAULT_SETTINGS, - isConfigured: true, - handleFilenameCaseSensitive: false, -} as ObsidianLiveSyncSettings; - -describe.skip("Plugin Integration Test (Local Database)", async () => { - let harness: LiveSyncHarness; - const vaultName = "TestVault" + Date.now(); - - beforeAll(async () => { - await DummyFileSourceInisialised; - harness = await generateHarness(vaultName, localdb_test_setting); - await waitForReady(harness); - }); - - it("should be instantiated and defined", async () => { - expect(harness.plugin).toBeDefined(); - expect(harness.plugin.app).toBe(harness.app); - return await Promise.resolve(); - }); - - it("should have services initialized", async () => { - expect(harness.plugin.core.services).toBeDefined(); - return await Promise.resolve(); - }); - it("should have local database initialized", async () => { - expect(harness.plugin.core.localDatabase).toBeDefined(); - expect(harness.plugin.core.localDatabase.isReady).toBe(true); - return await Promise.resolve(); - }); - - it("should store the changes into the local database", async () => { - const path = "test-store6.md"; - const content = "Hello, World!"; - if (harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - await harness.app.vault.create(path, content); - - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - - if (file instanceof TFile) { - const readContent = await harness.app.vault.read(file); - expect(readContent).toBe(content); - } - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - // await delay(100); // Wait a bit for the local database to process - - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - if (entry) { - expect(readContent(entry)).toBe(content); - } - return await Promise.resolve(); - }); - test.each([10, 100, 1000, 10000, 50000, 100000])("should handle large file of size %i bytes", async (size) => { - const path = `test-large-file-${size}.md`; - const content = Array.from(generateFile(size)).join(""); - if (harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - await harness.app.vault.create(path, content); - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - const readContent = await harness.app.vault.read(file); - expect(readContent).toBe(content); - } - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - if (entry) { - expect(readContent(entry)).toBe(content); - } - return await Promise.resolve(); - }); - - const binaryMap = Array.from({ length: 7 }, (_, i) => Math.pow(2, i * 4)); - test.each(binaryMap)("should handle binary file of size %i bytes", async (size) => { - const path = `test-binary-file-${size}.bin`; - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - if (harness.app.vault.getAbstractFileByPath(path)) { - console.log(`Deleting existing file ${path}`); - await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile); - } - // Create file via vault - await harness.app.vault.createBinary(path, await content.arrayBuffer()); - const file = harness.app.vault.getAbstractFileByPath(path); - expect(file).toBeInstanceOf(TFile); - if (file instanceof TFile) { - const readContent = await harness.app.vault.readBinary(file); - expect(await isDocContentSame(readContent, content)).toBe(true); - } - - await harness.plugin.core.services.fileProcessing.commitPendingFileEvents(); - await waitForIdle(harness); - const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).not.toBe(false); - if (entry) { - const entryContent = await readContent(entry); - if (!(entryContent instanceof ArrayBuffer)) { - throw new Error("Entry content is not an ArrayBuffer"); - } - // const expectedContent = await content.arrayBuffer(); - expect(await isDocContentSame(entryContent, content)).toBe(true); - } - return await Promise.resolve(); - }); -}); diff --git a/test/suite/sync.senario.basic.ts b/test/suite/sync.senario.basic.ts deleted file mode 100644 index 3eafe3cb..00000000 --- a/test/suite/sync.senario.basic.ts +++ /dev/null @@ -1,275 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { afterAll, beforeAll, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; - -import { - DummyFileSourceInisialised, - FILE_SIZE_BINS, - FILE_SIZE_MD, - generateBinaryFile, - generateFile, -} from "../utils/dummyfile"; -import { checkStoredFileInDB, testFileRead, testFileWrite } from "./db_common"; -import { delay } from "@/lib/src/common/utils"; -import { commands } from "vitest/browser"; -import { closeReplication, performReplication, prepareRemote } from "./sync_common"; -import type { DataWriteOptions } from "@/deps.ts"; - -type MTimedDataWriteOptions = DataWriteOptions & { mtime: number }; -export type TestOptions = { - setting: ObsidianLiveSyncSettings; - fileOptions: MTimedDataWriteOptions; -}; -function generateName(prefix: string, type: string, ext: string, size: number) { - return `${prefix}-${type}-file-${size}.${ext}`; -} -export function syncBasicCase(label: string, { setting, fileOptions }: TestOptions) { - describe("Replication Suite Tests - " + label, () => { - const nameFile = (type: string, ext: string, size: number) => generateName("sync-test", type, ext, size); - let serverPeerName = ""; - // TODO: Harness disposal may broke the event loop of P2P replication - // so we keep the harnesses alive until all tests are done. - // It may trystero's somethong, or not. - let harnessUpload: LiveSyncHarness; - let harnessDownload: LiveSyncHarness; - beforeAll(async () => { - await DummyFileSourceInisialised; - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - // await commands.closeWebPeer(); - serverPeerName = "t-" + Date.now(); - setting.P2P_AutoAcceptingPeers = serverPeerName; - setting.P2P_AutoSyncPeers = serverPeerName; - setting.P2P_DevicePeerName = "client-" + Date.now(); - await commands.openWebPeer(setting, serverPeerName); - } - }); - afterAll(async () => { - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - await commands.closeWebPeer(); - // await closeP2PReplicatorConnections(harnessUpload); - } - }); - - describe("Remote Database Initialization", () => { - let harnessInit: LiveSyncHarness; - const sync_test_setting_init = { - ...setting, - } as ObsidianLiveSyncSettings; - beforeAll(async () => { - const vaultName = "TestVault" + Date.now(); - console.log(`BeforeAll - Remote Database Initialization - Vault: ${vaultName}`); - harnessInit = await generateHarness(vaultName, sync_test_setting_init); - await waitForReady(harnessInit); - expect(harnessInit.plugin).toBeDefined(); - expect(harnessInit.plugin.app).toBe(harnessInit.app); - await waitForIdle(harnessInit); - }); - afterAll(async () => { - await harnessInit.plugin.core.services.replicator.getActiveReplicator()?.closeReplication(); - await harnessInit.dispose(); - await delay(1000); - }); - - it("should reset remote database", async () => { - // harnessInit = await generateHarness(vaultName, sync_test_setting_init); - await waitForReady(harnessInit); - await prepareRemote(harnessInit, sync_test_setting_init, true); - }); - it("should be prepared for replication", async () => { - await waitForReady(harnessInit); - if (setting.remoteType !== RemoteTypes.REMOTE_P2P) { - const status = await harnessInit.plugin.core.services.replicator - .getActiveReplicator() - ?.getRemoteStatus(sync_test_setting_init); - console.log("Connected devices after reset:", status); - expect(status).not.toBeFalsy(); - } - }); - }); - - describe("Replication - Upload", () => { - const sync_test_setting_upload = { - ...setting, - } as ObsidianLiveSyncSettings; - - beforeAll(async () => { - const vaultName = "TestVault" + Date.now(); - console.log(`BeforeAll - Replication Upload - Vault: ${vaultName}`); - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - sync_test_setting_upload.P2P_AutoAcceptingPeers = serverPeerName; - sync_test_setting_upload.P2P_AutoSyncPeers = serverPeerName; - sync_test_setting_upload.P2P_DevicePeerName = "up-" + Date.now(); - } - harnessUpload = await generateHarness(vaultName, sync_test_setting_upload); - await waitForReady(harnessUpload); - expect(harnessUpload.plugin).toBeDefined(); - expect(harnessUpload.plugin.app).toBe(harnessUpload.app); - await waitForIdle(harnessUpload); - }); - - afterAll(async () => { - await closeReplication(harnessUpload); - }); - - it("should be instantiated and defined", () => { - expect(harnessUpload.plugin).toBeDefined(); - expect(harnessUpload.plugin.app).toBe(harnessUpload.app); - }); - - it("should have services initialized", () => { - expect(harnessUpload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessUpload.plugin.core.localDatabase).toBeDefined(); - expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should prepare remote database", async () => { - await prepareRemote(harnessUpload, sync_test_setting_upload, false); - }); - - // describe("File Creation", async () => { - it("should a file has been created", async () => { - const content = "Hello, World!"; - const path = nameFile("store", "md", 0); - await testFileWrite(harnessUpload, path, content, false, fileOptions); - // Perform replication - // await harness.plugin.core.services.replication.replicate(true); - }); - it("should different content of several files have been created correctly", async () => { - await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions); - }); - - test.each(FILE_SIZE_MD)("should large file of size %i bytes has been created", async (size) => { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(true).toBe(true); - } else { - await testFileWrite(harnessUpload, path, content, false, fileOptions); - } - }); - - test.each(FILE_SIZE_BINS)("should binary file of size %i bytes has been created", async (size) => { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - const path = nameFile("binary", "bin", size); - await testFileWrite(harnessUpload, path, content, true, fileOptions); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(true).toBe(true); - } else { - await checkStoredFileInDB(harnessUpload, path, content, fileOptions); - } - }); - - it("Replication after uploads", async () => { - await performReplication(harnessUpload); - await performReplication(harnessUpload); - }); - }); - - describe("Replication - Download", () => { - // Download into a new vault - const sync_test_setting_download = { - ...setting, - } as ObsidianLiveSyncSettings; - beforeAll(async () => { - const vaultName = "TestVault" + Date.now(); - console.log(`BeforeAll - Replication Download - Vault: ${vaultName}`); - if (setting.remoteType === RemoteTypes.REMOTE_P2P) { - sync_test_setting_download.P2P_AutoAcceptingPeers = serverPeerName; - sync_test_setting_download.P2P_AutoSyncPeers = serverPeerName; - sync_test_setting_download.P2P_DevicePeerName = "down-" + Date.now(); - } - harnessDownload = await generateHarness(vaultName, sync_test_setting_download); - await waitForReady(harnessDownload); - await prepareRemote(harnessDownload, sync_test_setting_download, false); - - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - await delay(1000); - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - }); - afterAll(async () => { - await closeReplication(harnessDownload); - }); - - it("should be instantiated and defined", () => { - expect(harnessDownload.plugin).toBeDefined(); - expect(harnessDownload.plugin.app).toBe(harnessDownload.app); - }); - - it("should have services initialized", () => { - expect(harnessDownload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessDownload.plugin.core.localDatabase).toBeDefined(); - expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should a file has been synchronised", async () => { - const expectedContent = "Hello, World!"; - const path = nameFile("store", "md", 0); - await testFileRead(harnessDownload, path, expectedContent, fileOptions); - }); - it("should different content of several files have been synchronised", async () => { - await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions); - }); - - test.each(FILE_SIZE_MD)("should the file %i bytes had been synchronised", async (size) => { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(entry).toBe(false); - } else { - await testFileRead(harnessDownload, path, content, fileOptions); - } - }); - - test.each(FILE_SIZE_BINS)("should binary file of size %i bytes had been synchronised", async (size) => { - const path = nameFile("binary", "bin", size); - - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - console.log(`Skipping file of size ${size} bytes as it is too large to sync.`); - expect(entry).toBe(false); - } else { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - await testFileRead(harnessDownload, path, content, fileOptions); - } - }); - }); - afterAll(async () => { - if (harnessDownload) { - await closeReplication(harnessDownload); - await harnessDownload.dispose(); - await delay(1000); - } - if (harnessUpload) { - await closeReplication(harnessUpload); - await harnessUpload.dispose(); - await delay(1000); - } - }); - it("Wait for idle state", async () => { - await delay(100); - }); - }); -} diff --git a/test/suite/sync.single.test.ts b/test/suite/sync.single.test.ts deleted file mode 100644 index 9be98b44..00000000 --- a/test/suite/sync.single.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { describe } from "vitest"; -import { - PREFERRED_JOURNAL_SYNC, - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; - -import { defaultFileOption } from "./db_common"; -import { syncBasicCase } from "./sync.senario.basic.ts"; -import { settingBase } from "./variables.ts"; -const sync_test_setting_base = settingBase; -export const env = (import.meta as any).env; -function* generateCase() { - const passpharse = "thetest-Passphrase3+9-for-e2ee!"; - const REMOTE_RECOMMENDED = { - [RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED, - [RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC, - [RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED, - }; - const remoteTypes = [RemoteTypes.REMOTE_COUCHDB]; - // const remoteTypes = [RemoteTypes.REMOTE_P2P]; - const e2eeOptions = [false]; - // const e2eeOptions = [true]; - for (const remoteType of remoteTypes) { - for (const useE2EE of e2eeOptions) { - yield { - setting: { - ...sync_test_setting_base, - ...REMOTE_RECOMMENDED[remoteType], - remoteType, - encrypt: useE2EE, - passphrase: useE2EE ? passpharse : "", - usePathObfuscation: useE2EE, - } as ObsidianLiveSyncSettings, - }; - } - } -} - -describe.skip("Replication Suite Tests (Single)", async () => { - const cases = Array.from(generateCase()); - const fileOptions = defaultFileOption; - describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions }); - }); -}); diff --git a/test/suite/sync.test.ts b/test/suite/sync.test.ts deleted file mode 100644 index aa284c17..00000000 --- a/test/suite/sync.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { describe } from "vitest"; -import { - PREFERRED_JOURNAL_SYNC, - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; - -import { defaultFileOption } from "./db_common"; -import { syncBasicCase } from "./sync.senario.basic.ts"; -import { settingBase } from "./variables.ts"; -const sync_test_setting_base = settingBase; -export const env = (import.meta as any).env; -function* generateCase() { - const passpharse = "thetest-Passphrase3+9-for-e2ee!"; - const REMOTE_RECOMMENDED = { - [RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED, - [RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC, - [RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED, - }; - const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO]; - // const remoteTypes = [RemoteTypes.REMOTE_P2P]; - const e2eeOptions = [false, true]; - // const e2eeOptions = [true]; - for (const remoteType of remoteTypes) { - for (const useE2EE of e2eeOptions) { - yield { - setting: { - ...sync_test_setting_base, - ...REMOTE_RECOMMENDED[remoteType], - remoteType, - encrypt: useE2EE, - passphrase: useE2EE ? passpharse : "", - usePathObfuscation: useE2EE, - } as ObsidianLiveSyncSettings, - }; - } - } -} - -describe("Replication Suite Tests (Normal)", async () => { - const cases = Array.from(generateCase()); - const fileOptions = defaultFileOption; - describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions }); - }); -}); diff --git a/test/suite/sync_common.ts b/test/suite/sync_common.ts deleted file mode 100644 index 74da8664..00000000 --- a/test/suite/sync_common.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { expect } from "vitest"; -import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; - -import { delay, fireAndForget } from "@/lib/src/common/utils"; -import { commands } from "vitest/browser"; -import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; -import { waitTaskWithFollowups } from "../lib/util"; -async function waitForP2PPeers(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - // Wait for peers to connect - const maxRetries = 20; - let retries = maxRetries; - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - while (retries-- > 0) { - fireAndForget(() => commands.acceptWebPeer()); - await delay(1000); - const peers = replicator.knownAdvertisements; - - if (peers && peers.length > 0) { - console.log("P2P peers connected:", peers); - return; - } - fireAndForget(() => commands.acceptWebPeer()); - console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`); - console.dir(peers); - await delay(1000); - } - console.log("Failed to connect P2P peers after retries"); - throw new Error("P2P peers did not connect in time."); - } -} -export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - replicator.closeReplication(); - await delay(30); - replicator.closeReplication(); - await delay(1000); - console.log("P2P replicator connections closed"); - // if (replicator instanceof LiveSyncTrysteroReplicator) { - // replicator.closeReplication(); - // await delay(1000); - // } - } -} - -export async function performReplication(harness: LiveSyncHarness) { - await waitForP2PPeers(harness); - await delay(500); - const p = harness.plugin.core.services.replication.replicate(true); - const task = - harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P - ? waitTaskWithFollowups( - p, - () => { - // Accept any peer dialogs during replication (fire and forget) - fireAndForget(() => commands.acceptWebPeer()); - return Promise.resolve(); - }, - 30000, - 500 - ) - : p; - const result = await task; - // await waitForIdle(harness); - // if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - // await closeP2PReplicatorConnections(harness); - // } - return result; -} - -export async function closeReplication(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - return await closeP2PReplicatorConnections(harness); - } - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!replicator) { - console.log("No active replicator to close"); - return; - } - await replicator.closeReplication(); - await waitForIdle(harness); - console.log("Replication closed"); -} - -export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) { - if (setting.remoteType !== RemoteTypes.REMOTE_P2P) { - if (shouldReset) { - await delay(1000); - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryResetRemoteDatabase(harness.plugin.core.settings); - } else { - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryCreateRemoteDatabase(harness.plugin.core.settings); - } - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.markRemoteResolved(harness.plugin.core.settings); - // No exceptions should be thrown - const status = await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.getRemoteStatus(harness.plugin.core.settings); - console.log("Remote status:", status); - expect(status).not.toBeFalsy(); - } -} diff --git a/test/suite/variables.ts b/test/suite/variables.ts deleted file mode 100644 index f55cce26..00000000 --- a/test/suite/variables.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DoctorRegulation } from "@/lib/src/common/configForDoc"; -import { - DEFAULT_SETTINGS, - ChunkAlgorithms, - AutoAccepting, - type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; -export const env = (import.meta as any).env; -export const settingBase = { - ...DEFAULT_SETTINGS, - isConfigured: true, - handleFilenameCaseSensitive: false, - couchDB_URI: `${env.hostname}`, - couchDB_DBNAME: `${env.dbname}`, - couchDB_USER: `${env.username}`, - couchDB_PASSWORD: `${env.password}`, - bucket: `${env.bucketName}`, - region: "us-east-1", - endpoint: `${env.minioEndpoint}`, - accessKey: `${env.accessKey}`, - secretKey: `${env.secretKey}`, - useCustomRequestHandler: true, - forcePathStyle: true, - bucketPrefix: "", - usePluginSyncV2: true, - chunkSplitterVersion: ChunkAlgorithms.RabinKarp, - doctorProcessedVersion: DoctorRegulation.version, - notifyThresholdOfRemoteStorageSize: 800, - P2P_AutoAccepting: AutoAccepting.ALL, - P2P_AutoBroadcast: true, - P2P_AutoStart: true, - P2P_Enabled: true, - P2P_passphrase: "p2psync-test", - P2P_roomID: "p2psync-test", - P2P_DevicePeerName: "p2psync-test", - P2P_relays: "ws://localhost:4000/", - P2P_AutoAcceptingPeers: "p2p-livesync-web-peer", - P2P_SyncOnReplication: "p2p-livesync-web-peer", -} as ObsidianLiveSyncSettings; diff --git a/test/suitep2p/run-p2p-tests.sh b/test/suitep2p/run-p2p-tests.sh deleted file mode 100755 index 4d8a50c0..00000000 --- a/test/suitep2p/run-p2p-tests.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd)" -CLI_DIR="$REPO_ROOT/src/apps/cli" -CLI_TEST_HELPERS="$CLI_DIR/test/test-helpers.sh" - -source "$CLI_TEST_HELPERS" - -RUN_BUILD="${RUN_BUILD:-1}" -KEEP_TEST_DATA="${KEEP_TEST_DATA:-1}" -VERBOSE_TEST_LOGGING="${VERBOSE_TEST_LOGGING:-1}" - -RELAY="${RELAY:-ws://localhost:4000/}" -USE_INTERNAL_RELAY="${USE_INTERNAL_RELAY:-1}" -APP_ID="${APP_ID:-self-hosted-livesync-vitest-p2p}" -HOST_PEER_NAME="${HOST_PEER_NAME:-p2p-cli-host}" - -ROOM_ID="p2p-room-$(date +%s)-$RANDOM-$RANDOM" -PASSPHRASE="p2p-pass-$(date +%s)-$RANDOM-$RANDOM" -UPLOAD_PEER_NAME="p2p-upload-$(date +%s)-$RANDOM" -DOWNLOAD_PEER_NAME="p2p-download-$(date +%s)-$RANDOM" -UPLOAD_VAULT_NAME="TestVaultUpload-$(date +%s)-$RANDOM" -DOWNLOAD_VAULT_NAME="TestVaultDownload-$(date +%s)-$RANDOM" - -# ---- Build CLI ---- -if [[ "$RUN_BUILD" == "1" ]]; then - echo "[INFO] building CLI" - (cd "$CLI_DIR" && npm run build) -fi - -# ---- Temp directory ---- -WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/livesync-vitest-p2p.XXXXXX")" -VAULT_HOST="$WORK_DIR/vault-host" -SETTINGS_HOST="$WORK_DIR/settings-host.json" -HOST_LOG="$WORK_DIR/p2p-host.log" -# Handoff file: upload phase writes this; download phase reads it. -HANDOFF_FILE="$WORK_DIR/p2p-test-handoff.json" -mkdir -p "$VAULT_HOST" - -# ---- Setup CLI command (uses npm run cli from CLI_DIR) ---- -# Override run_cli to invoke the built binary directly from CLI_DIR -run_cli() { - (cd "$CLI_DIR" && node dist/index.cjs "$@") -} - -# ---- Create host settings ---- -echo "[INFO] relay=$RELAY room=$ROOM_ID app=$APP_ID host=$HOST_PEER_NAME" -cli_test_init_settings_file "$SETTINGS_HOST" -cli_test_apply_p2p_settings "$SETTINGS_HOST" "$ROOM_ID" "$PASSPHRASE" "$APP_ID" "$RELAY" "~.*" - -# Set host peer name -SETTINGS_HOST_FILE="$SETTINGS_HOST" HOST_PEER_NAME_VAL="$HOST_PEER_NAME" HOST_PASSPHRASE_VAL="$PASSPHRASE" node <<'NODE' -const fs = require("node:fs"); -const data = JSON.parse(fs.readFileSync(process.env.SETTINGS_HOST_FILE, "utf-8")); - -// Keep tweak values aligned with browser-side P2P test settings. -data.remoteType = "ONLY_P2P"; -data.encrypt = true; -data.passphrase = process.env.HOST_PASSPHRASE_VAL; -data.usePathObfuscation = true; -data.handleFilenameCaseSensitive = false; -data.customChunkSize = 50; -data.usePluginSyncV2 = true; -data.doNotUseFixedRevisionForChunks = false; - -data.P2P_DevicePeerName = process.env.HOST_PEER_NAME_VAL; -fs.writeFileSync(process.env.SETTINGS_HOST_FILE, JSON.stringify(data, null, 2), "utf-8"); -NODE - -# ---- Cleanup trap ---- -cleanup() { - local exit_code=$? - if [[ -n "${HOST_PID:-}" ]] && kill -0 "$HOST_PID" >/dev/null 2>&1; then - echo "[INFO] stopping CLI host (PID=$HOST_PID)" - kill -TERM "$HOST_PID" >/dev/null 2>&1 || true - wait "$HOST_PID" >/dev/null 2>&1 || true - fi - - if [[ "${P2P_RELAY_STARTED:-0}" == "1" ]]; then - cli_test_stop_p2p_relay - fi - - if [[ "$KEEP_TEST_DATA" != "1" ]]; then - rm -rf "$WORK_DIR" - else - echo "[INFO] KEEP_TEST_DATA=1, preserving artefacts at $WORK_DIR" - fi - - exit "$exit_code" -} -trap cleanup EXIT - -start_host() { - local attempt=0 - while [[ "$attempt" -lt 5 ]]; do - attempt=$((attempt + 1)) - echo "[INFO] starting CLI p2p-host (attempt $attempt/5)" - : >"$HOST_LOG" - (cd "$CLI_DIR" && node dist/index.cjs "$VAULT_HOST" --settings "$SETTINGS_HOST" -d p2p-host) >"$HOST_LOG" 2>&1 & - HOST_PID=$! - - local host_ready=0 - local exited_early=0 - for i in $(seq 1 30); do - if grep -qF "P2P host is running" "$HOST_LOG" 2>/dev/null; then - host_ready=1 - break - fi - if ! kill -0 "$HOST_PID" >/dev/null 2>&1; then - exited_early=1 - break - fi - echo "[INFO] waiting for p2p-host to be ready... ($i/30)" - sleep 1 - done - - if [[ "$host_ready" == "1" ]]; then - echo "[INFO] p2p-host is ready (PID=$HOST_PID)" - return 0 - fi - - wait "$HOST_PID" >/dev/null 2>&1 || true - HOST_PID= - - if grep -qF "Resource temporarily unavailable" "$HOST_LOG" 2>/dev/null; then - echo "[INFO] p2p-host database lock is still being released, retrying..." - sleep 2 - continue - fi - - if [[ "$exited_early" == "1" ]]; then - echo "[FAIL] CLI host process exited unexpectedly" >&2 - else - echo "[FAIL] p2p-host did not become ready within 30 seconds" >&2 - fi - cat "$HOST_LOG" >&2 - exit 1 - done - - echo "[FAIL] p2p-host could not be restarted after multiple attempts" >&2 - cat "$HOST_LOG" >&2 - exit 1 -} - -# ---- Start local relay if needed ---- -if [[ "$USE_INTERNAL_RELAY" == "1" ]]; then - if cli_test_is_local_p2p_relay "$RELAY"; then - cli_test_start_p2p_relay - P2P_RELAY_STARTED=1 - else - echo "[INFO] USE_INTERNAL_RELAY=1 but RELAY is not local ($RELAY), skipping" - fi -fi - -start_host - -# Common env vars passed to both vitest runs -P2P_ENV=( - P2P_TEST_ROOM_ID="$ROOM_ID" - P2P_TEST_PASSPHRASE="$PASSPHRASE" - P2P_TEST_HOST_PEER_NAME="$HOST_PEER_NAME" - P2P_TEST_RELAY="$RELAY" - P2P_TEST_APP_ID="$APP_ID" - P2P_TEST_HANDOFF_FILE="$HANDOFF_FILE" - P2P_TEST_UPLOAD_PEER_NAME="$UPLOAD_PEER_NAME" - P2P_TEST_DOWNLOAD_PEER_NAME="$DOWNLOAD_PEER_NAME" - P2P_TEST_UPLOAD_VAULT_NAME="$UPLOAD_VAULT_NAME" - P2P_TEST_DOWNLOAD_VAULT_NAME="$DOWNLOAD_VAULT_NAME" -) - -cd "$REPO_ROOT" - -# ---- Phase 1: Upload ---- -# Each vitest run gets a fresh browser process, so Trystero's module-level -# global state (occupiedRooms, didInit, etc.) is clean for every phase. -echo "[INFO] running P2P vitest — upload phase" -env "${P2P_ENV[@]}" \ - npx dotenv-cli -e .env -e .test.env -- \ - vitest run --config vitest.config.p2p.ts test/suitep2p/syncp2p.p2p-up.test.ts -echo "[INFO] upload phase completed" - -# ---- Phase 2: Download ---- -# Keep the same host process alive so its database handle and relay presence stay stable. -echo "[INFO] waiting 5s before download phase..." -sleep 5 -echo "[INFO] running P2P vitest — download phase" -env "${P2P_ENV[@]}" \ - npx dotenv-cli -e .env -e .test.env -- \ - vitest run --config vitest.config.p2p.ts test/suitep2p/syncp2p.p2p-down.test.ts -echo "[INFO] download phase completed" - -echo "[INFO] P2P vitest suite completed" diff --git a/test/suitep2p/sync_common_p2p.ts b/test/suitep2p/sync_common_p2p.ts deleted file mode 100644 index 53009894..00000000 --- a/test/suitep2p/sync_common_p2p.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * P2P-specific sync helpers. - * - * Derived from test/suite/sync_common.ts but with all acceptWebPeer() calls - * removed. When using a CLI p2p-host with P2P_AutoAcceptingPeers="~.*", peer - * acceptance is automatic and no Playwright dialog interaction is needed. - */ -import { expect } from "vitest"; -import { waitForIdle, type LiveSyncHarness } from "../harness/harness"; -import { RemoteTypes, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; -import { delay } from "@/lib/src/common/utils"; -import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator"; -import { waitTaskWithFollowups } from "../lib/util"; - -const P2P_REPLICATION_TIMEOUT_MS = 180000; - -async function testWebSocketConnection(relayUrl: string): Promise { - return new Promise((resolve, reject) => { - console.log(`[P2P Debug] Testing WebSocket connection to ${relayUrl}`); - try { - const ws = new WebSocket(relayUrl); - const timer = setTimeout(() => { - ws.close(); - reject(new Error(`WebSocket connection to ${relayUrl} timed out`)); - }, 5000); - ws.onopen = () => { - clearTimeout(timer); - console.log(`[P2P Debug] WebSocket connected to ${relayUrl} successfully`); - ws.close(); - resolve(); - }; - ws.onerror = (e) => { - clearTimeout(timer); - console.error(`[P2P Debug] WebSocket error connecting to ${relayUrl}:`, e); - reject(new Error(`WebSocket connection to ${relayUrl} failed`)); - }; - } catch (e) { - console.error(`[P2P Debug] WebSocket constructor threw:`, e); - reject(e); - } - }); -} - -async function waitForP2PPeers(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const maxRetries = 20; - let retries = maxRetries; - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - console.log("[P2P Debug] replicator type:", replicator?.constructor?.name); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - - // Ensure P2P is open (getActiveReplicator returns a fresh instance that may not be open yet) - if (!replicator.server?.isServing) { - console.log("[P2P Debug] P2P not yet serving, calling open()"); - // Test WebSocket connectivity first - const relay = harness.plugin.core.settings.P2P_relays?.split(",")[0]?.trim(); - if (relay) { - try { - await testWebSocketConnection(relay); - } catch (e) { - console.error("[P2P Debug] WebSocket connectivity test failed:", e); - } - } - try { - await replicator.open(); - console.log("[P2P Debug] open() completed, isServing:", replicator.server?.isServing); - } catch (e) { - console.error("[P2P Debug] open() threw:", e); - } - } - - // Wait for P2P server to actually start (room joined) - for (let i = 0; i < 30; i++) { - const serving = replicator.server?.isServing; - console.log(`[P2P Debug] isServing: ${serving} (${i}/30)`); - if (serving) break; - await delay(500); - if (i === 29) throw new Error("P2P server did not start in time."); - } - - while (retries-- > 0) { - await delay(1000); - const peers = replicator.knownAdvertisements; - if (peers && peers.length > 0) { - console.log("P2P peers connected:", peers); - return; - } - console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`); - console.dir(peers); - await delay(1000); - } - console.log("Failed to connect P2P peers after retries"); - throw new Error("P2P peers did not connect in time."); - } -} - -export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - replicator.closeReplication(); - await delay(30); - replicator.closeReplication(); - await delay(1000); - console.log("P2P replicator connections closed"); - } -} - -export async function performReplication(harness: LiveSyncHarness) { - await waitForP2PPeers(harness); - await delay(500); - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!(replicator instanceof LiveSyncTrysteroReplicator)) { - throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator"); - } - const knownPeers = replicator.knownAdvertisements; - - const targetPeer = knownPeers.find((peer) => peer.name.startsWith("vault-host")) ?? knownPeers[0] ?? undefined; - if (!targetPeer) { - throw new Error("No connected P2P peer to synchronise with"); - } - - const p = replicator.sync(targetPeer.peerId, true); - const result = await waitTaskWithFollowups(p, () => Promise.resolve(), P2P_REPLICATION_TIMEOUT_MS, 500); - if (result && typeof result === "object" && "error" in result && result.error) { - throw result.error; - } - return result; - } - - return await harness.plugin.core.services.replication.replicate(true); -} - -export async function closeReplication(harness: LiveSyncHarness) { - if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) { - return await closeP2PReplicatorConnections(harness); - } - const replicator = await harness.plugin.core.services.replicator.getActiveReplicator(); - if (!replicator) { - console.log("No active replicator to close"); - return; - } - await replicator.closeReplication(); - await waitForIdle(harness); - console.log("Replication closed"); -} - -export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) { - // P2P has no remote database to initialise — skip - if (setting.remoteType === RemoteTypes.REMOTE_P2P) return; - - if (shouldReset) { - await delay(1000); - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryResetRemoteDatabase(harness.plugin.core.settings); - } else { - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.tryCreateRemoteDatabase(harness.plugin.core.settings); - } - await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.markRemoteResolved(harness.plugin.core.settings); - const status = await harness.plugin.core.services.replicator - .getActiveReplicator() - ?.getRemoteStatus(harness.plugin.core.settings); - console.log("Remote status:", status); - expect(status).not.toBeFalsy(); -} diff --git a/test/suitep2p/syncp2p.p2p-down.test.ts b/test/suitep2p/syncp2p.p2p-down.test.ts deleted file mode 100644 index 7f3b77f7..00000000 --- a/test/suitep2p/syncp2p.p2p-down.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * P2P Replication Tests — Download phase (process 2 of 2) - * - * Executed by run-p2p-tests.sh as the second vitest process, after the - * upload phase has completed and the CLI host holds all the data. - * - * Reads the handoff JSON written by the upload phase to know which files - * to verify, then replicates from the CLI host and checks every file. - */ -import { afterAll, beforeAll, beforeEach, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type FilePath, - type ObsidianLiveSyncSettings, - AutoAccepting, -} from "@/lib/src/common/types"; -import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile"; -import { defaultFileOption, testFileRead } from "../suite/db_common"; -import { delay } from "@/lib/src/common/utils"; -import { closeReplication, performReplication } from "./sync_common_p2p"; -import { settingBase } from "../suite/variables"; - -const env = (import.meta as any).env; - -const ROOM_ID: string = env.P2P_TEST_ROOM_ID ?? "p2p-test-room"; -const PASSPHRASE: string = env.P2P_TEST_PASSPHRASE ?? "p2p-test-pass"; -const HOST_PEER_NAME: string = env.P2P_TEST_HOST_PEER_NAME ?? "p2p-cli-host"; -const RELAY: string = env.P2P_TEST_RELAY ?? "ws://localhost:4000/"; -const APP_ID: string = env.P2P_TEST_APP_ID ?? "self-hosted-livesync-vitest-p2p"; -const DOWNLOAD_PEER_NAME: string = env.P2P_TEST_DOWNLOAD_PEER_NAME ?? `p2p-download-${Date.now()}`; -const DOWNLOAD_VAULT_NAME: string = env.P2P_TEST_DOWNLOAD_VAULT_NAME ?? `TestVaultDownload-${Date.now()}`; -const HANDOFF_FILE: string = env.P2P_TEST_HANDOFF_FILE ?? "/tmp/p2p-test-handoff.json"; - -console.log("[P2P Down] ROOM_ID:", ROOM_ID, "HOST:", HOST_PEER_NAME, "RELAY:", RELAY, "APP_ID:", APP_ID); -console.log("[P2P Down] HANDOFF_FILE:", HANDOFF_FILE); - -const p2pSetting: ObsidianLiveSyncSettings = { - ...settingBase, - ...PREFERRED_SETTING_SELF_HOSTED, - showVerboseLog: true, - remoteType: RemoteTypes.REMOTE_P2P, - encrypt: true, - passphrase: PASSPHRASE, - usePathObfuscation: true, - P2P_Enabled: true, - P2P_AppID: APP_ID, - handleFilenameCaseSensitive: false, - P2P_AutoAccepting: AutoAccepting.ALL, - P2P_AutoBroadcast: true, - P2P_AutoStart: true, - P2P_passphrase: PASSPHRASE, - P2P_roomID: ROOM_ID, - P2P_relays: RELAY, - P2P_AutoAcceptingPeers: "~.*", - P2P_SyncOnReplication: HOST_PEER_NAME, -}; - -const fileOptions = defaultFileOption; -const nameFile = (type: string, ext: string, size: number) => `p2p-cli-test-${type}-file-${size}.${ext}`; - -/** Read the handoff JSON produced by the upload phase. */ -async function readHandoff(): Promise<{ fileSizeMd: number[]; fileSizeBins: number[] }> { - const { commands } = await import("@vitest/browser/context"); - const raw = await commands.readHandoffFile(HANDOFF_FILE); - return JSON.parse(raw); -} - -describe("P2P Replication — Download", () => { - let harnessDownload: LiveSyncHarness; - let fileSizeMd: number[] = []; - let fileSizeBins: number[] = []; - - const downloadSetting: ObsidianLiveSyncSettings = { - ...p2pSetting, - P2P_DevicePeerName: DOWNLOAD_PEER_NAME, - }; - - beforeAll(async () => { - await DummyFileSourceInisialised; - - const handoff = await readHandoff(); - fileSizeMd = handoff.fileSizeMd; - fileSizeBins = handoff.fileSizeBins; - console.log("[P2P Down] handoff loaded — md sizes:", fileSizeMd, "bin sizes:", fileSizeBins); - - const vaultName = DOWNLOAD_VAULT_NAME; - console.log(`[P2P Down] BeforeAll - Vault: ${vaultName}`); - console.log(`[P2P Down] Peer name: ${DOWNLOAD_PEER_NAME}`); - harnessDownload = await generateHarness(vaultName, downloadSetting); - await waitForReady(harnessDownload); - - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - await delay(1000); - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - await delay(3000); - }); - beforeEach(async () => { - await performReplication(harnessDownload); - await waitForIdle(harnessDownload); - }); - - afterAll(async () => { - await closeReplication(harnessDownload); - await harnessDownload.dispose(); - await delay(1000); - }); - - it("should be instantiated and defined", () => { - expect(harnessDownload.plugin).toBeDefined(); - expect(harnessDownload.plugin.app).toBe(harnessDownload.app); - }); - - it("should have services initialized", () => { - expect(harnessDownload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessDownload.plugin.core.localDatabase).toBeDefined(); - expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should have synchronised the stored file", async () => { - await testFileRead(harnessDownload, nameFile("store", "md", 0), "Hello, World!", fileOptions); - }); - - it("should have synchronised files with different content", async () => { - await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions); - await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions); - }); - - // NOTE: test.each cannot use variables populated in beforeAll, so we use - // a single it() that iterates over the sizes loaded from the handoff file. - it("should have synchronised all large md files", async () => { - for (const size of fileSizeMd) { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).toBe(false); - } else { - await testFileRead(harnessDownload, path, content, fileOptions); - } - } - }); - - it("should have synchronised all binary files", async () => { - for (const size of fileSizeBins) { - const path = nameFile("binary", "bin", size); - const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath); - expect(entry).toBe(false); - } else { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - await testFileRead(harnessDownload, path, content, fileOptions); - } - } - }); -}); diff --git a/test/suitep2p/syncp2p.p2p-up.test.ts b/test/suitep2p/syncp2p.p2p-up.test.ts deleted file mode 100644 index 7c463eb3..00000000 --- a/test/suitep2p/syncp2p.p2p-up.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * P2P Replication Tests — Upload phase (process 1 of 2) - * - * Executed by run-p2p-tests.sh as the first vitest process. - * Writes files into the local DB, replicates them to the CLI host, - * then writes a handoff JSON so the download process knows what to verify. - * - * Trystero has module-level global state (occupiedRooms, didInit, etc.) - * that cannot be safely reused across upload→download within the same - * browser process. Running upload and download as separate vitest - * invocations gives each phase a fresh browser context. - */ -import { afterAll, beforeAll, describe, expect, it, test } from "vitest"; -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, - AutoAccepting, -} from "@/lib/src/common/types"; -import { - DummyFileSourceInisialised, - FILE_SIZE_BINS, - FILE_SIZE_MD, - generateBinaryFile, - generateFile, -} from "../utils/dummyfile"; -import { checkStoredFileInDB, defaultFileOption, testFileWrite } from "../suite/db_common"; -import { delay } from "@/lib/src/common/utils"; -import { closeReplication, performReplication } from "./sync_common_p2p"; -import { settingBase } from "../suite/variables"; - -const env = (import.meta as any).env; - -const ROOM_ID: string = env.P2P_TEST_ROOM_ID ?? "p2p-test-room"; -const PASSPHRASE: string = env.P2P_TEST_PASSPHRASE ?? "p2p-test-pass"; -const HOST_PEER_NAME: string = env.P2P_TEST_HOST_PEER_NAME ?? "p2p-cli-host"; -const RELAY: string = env.P2P_TEST_RELAY ?? "ws://localhost:4000/"; -const APP_ID: string = env.P2P_TEST_APP_ID ?? "self-hosted-livesync-vitest-p2p"; -const UPLOAD_PEER_NAME: string = env.P2P_TEST_UPLOAD_PEER_NAME ?? `p2p-upload-${Date.now()}`; -const UPLOAD_VAULT_NAME: string = env.P2P_TEST_UPLOAD_VAULT_NAME ?? `TestVaultUpload-${Date.now()}`; -// Path written by run-p2p-tests.sh; the download phase reads it back. -const HANDOFF_FILE: string = env.P2P_TEST_HANDOFF_FILE ?? "/tmp/p2p-test-handoff.json"; - -console.log("[P2P Up] ROOM_ID:", ROOM_ID, "HOST:", HOST_PEER_NAME, "RELAY:", RELAY, "APP_ID:", APP_ID); -console.log("[P2P Up] HANDOFF_FILE:", HANDOFF_FILE); - -const p2pSetting: ObsidianLiveSyncSettings = { - ...settingBase, - ...PREFERRED_SETTING_SELF_HOSTED, - showVerboseLog: true, - remoteType: RemoteTypes.REMOTE_P2P, - encrypt: true, - passphrase: PASSPHRASE, - usePathObfuscation: true, - P2P_Enabled: true, - P2P_AppID: APP_ID, - handleFilenameCaseSensitive: false, - P2P_AutoAccepting: AutoAccepting.ALL, - P2P_AutoBroadcast: true, - P2P_AutoStart: true, - P2P_passphrase: PASSPHRASE, - P2P_roomID: ROOM_ID, - P2P_relays: RELAY, - P2P_AutoAcceptingPeers: "~.*", - P2P_SyncOnReplication: HOST_PEER_NAME, -}; - -const fileOptions = defaultFileOption; -const nameFile = (type: string, ext: string, size: number) => `p2p-cli-test-${type}-file-${size}.${ext}`; - -/** Write the handoff JSON so the download phase knows which files to verify. */ -async function writeHandoff() { - const handoff = { - fileSizeMd: FILE_SIZE_MD, - fileSizeBins: FILE_SIZE_BINS, - }; - const { commands } = await import("@vitest/browser/context"); - await commands.writeHandoffFile(HANDOFF_FILE, JSON.stringify(handoff)); - console.log("[P2P Up] handoff written to", HANDOFF_FILE); -} - -describe("P2P Replication — Upload", () => { - let harnessUpload: LiveSyncHarness; - - const uploadSetting: ObsidianLiveSyncSettings = { - ...p2pSetting, - P2P_DevicePeerName: UPLOAD_PEER_NAME, - }; - - beforeAll(async () => { - await DummyFileSourceInisialised; - const vaultName = UPLOAD_VAULT_NAME; - console.log(`[P2P Up] BeforeAll - Vault: ${vaultName}`); - console.log(`[P2P Up] Peer name: ${UPLOAD_PEER_NAME}`); - harnessUpload = await generateHarness(vaultName, uploadSetting); - await waitForReady(harnessUpload); - expect(harnessUpload.plugin).toBeDefined(); - await waitForIdle(harnessUpload); - }); - - afterAll(async () => { - await closeReplication(harnessUpload); - await harnessUpload.dispose(); - await delay(1000); - }); - - it("should be instantiated and defined", () => { - expect(harnessUpload.plugin).toBeDefined(); - expect(harnessUpload.plugin.app).toBe(harnessUpload.app); - }); - - it("should have services initialized", () => { - expect(harnessUpload.plugin.core.services).toBeDefined(); - }); - - it("should have local database initialized", () => { - expect(harnessUpload.plugin.core.localDatabase).toBeDefined(); - expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true); - }); - - it("should create a file", async () => { - await testFileWrite(harnessUpload, nameFile("store", "md", 0), "Hello, World!", false, fileOptions); - }); - - it("should create several files with different content", async () => { - await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions); - await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions); - }); - - test.each(FILE_SIZE_MD)("should create large md file of size %i bytes", async (size) => { - const content = Array.from(generateFile(size)).join(""); - const path = nameFile("large", "md", size); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (isTooLarge) { - expect(true).toBe(true); - } else { - await testFileWrite(harnessUpload, path, content, false, fileOptions); - } - }); - - test.each(FILE_SIZE_BINS)("should create binary file of size %i bytes", async (size) => { - const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" }); - const path = nameFile("binary", "bin", size); - await testFileWrite(harnessUpload, path, content, true, fileOptions); - const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size); - if (!isTooLarge) { - await checkStoredFileInDB(harnessUpload, path, content, fileOptions); - } - }); - - it("should replicate uploads to CLI host", async () => { - await performReplication(harnessUpload); - await performReplication(harnessUpload); - }); - - it("should write handoff file for download phase", async () => { - await writeHandoff(); - }); -}); diff --git a/test/suitep2p/syncp2p.test.ts b/test/suitep2p/syncp2p.test.ts deleted file mode 100644 index 08c2c101..00000000 --- a/test/suitep2p/syncp2p.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Functional Test on Main Cases -// This test suite only covers main functional cases of synchronisation. Event handling, error cases, -// and edge, resolving conflicts, etc. will be covered in separate test suites. -import { describe } from "vitest"; -import { - PREFERRED_JOURNAL_SYNC, - PREFERRED_SETTING_SELF_HOSTED, - RemoteTypes, - type ObsidianLiveSyncSettings, -} from "@/lib/src/common/types"; - -import { settingBase } from "../suite/variables.ts"; -import { defaultFileOption } from "../suite/db_common"; -import { syncBasicCase } from "../suite/sync.senario.basic.ts"; - -export const env = (import.meta as any).env; -function* generateCase() { - const sync_test_setting_base = settingBase; - const passpharse = "thetest-Passphrase3+9-for-e2ee!"; - const REMOTE_RECOMMENDED = { - [RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED, - [RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC, - [RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED, - }; - // const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO, RemoteTypes.REMOTE_P2P]; - const remoteTypes = [RemoteTypes.REMOTE_P2P]; - // const e2eeOptions = [false, true]; - const e2eeOptions = [true]; - for (const remoteType of remoteTypes) { - for (const useE2EE of e2eeOptions) { - yield { - setting: { - ...sync_test_setting_base, - ...REMOTE_RECOMMENDED[remoteType], - remoteType, - encrypt: useE2EE, - passphrase: useE2EE ? passpharse : "", - usePathObfuscation: useE2EE, - } as ObsidianLiveSyncSettings, - }; - } - } -} - -describe("Replication Suite Tests (P2P)", async () => { - const cases = Array.from(generateCase()); - const fileOptions = defaultFileOption; - describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions }); - }); -}); diff --git a/test/testtest/dummyfile.test.ts b/test/testtest/dummyfile.test.ts deleted file mode 100644 index 32349963..00000000 --- a/test/testtest/dummyfile.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { writeFile } from "../utils/fileapi.vite"; -import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile"; -import { describe, expect, it } from "vitest"; - -describe("Test File Teet", async () => { - await DummyFileSourceInisialised; - - it("should generate binary file correctly", async () => { - const size = 5000; - let generatedSize = 0; - const chunks: Uint8Array[] = []; - const generator = generateBinaryFile(size); - const blob = new Blob([...generator], { type: "application/octet-stream" }); - const buf = await blob.arrayBuffer(); - const hexDump = new Uint8Array(buf) - //@ts-ignore - .toHex() - .match(/.{1,32}/g) - ?.join("\n"); - const secondDummy = generateBinaryFile(size); - const secondBlob = new Blob([...secondDummy], { type: "application/octet-stream" }); - const secondBuf = await secondBlob.arrayBuffer(); - const secondHexDump = new Uint8Array(secondBuf) - //@ts-ignore - .toHex() - .match(/.{1,32}/g) - ?.join("\n"); - if (hexDump !== secondHexDump) { - throw new Error("Generated binary files do not match"); - } - expect(hexDump).toBe(secondHexDump); - // await writeFile("test/testtest/dummyfile.test.bin", buf); - // await writeFile("test/testtest/dummyfile.test.bin.hexdump.txt", hexDump || ""); - }); - it("should generate text file correctly", async () => { - const size = 25000; - let generatedSize = 0; - let content = ""; - const generator = generateFile(size); - const out = [...generator]; - // const blob = new Blob(out, { type: "text/plain" }); - content = out.join(""); - - const secondDummy = generateFile(size); - const secondOut = [...secondDummy]; - const secondContent = secondOut.join(""); - if (content !== secondContent) { - throw new Error("Generated text files do not match"); - } - expect(content).toBe(secondContent); - // await writeFile("test/testtest/dummyfile.test.txt", await blob.text()); - }); -}); diff --git a/test/unit/dialog.test.ts b/test/unit/dialog.test.ts deleted file mode 100644 index 86c424cc..00000000 --- a/test/unit/dialog.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Dialog Unit Tests -import { beforeAll, describe, expect, it } from "vitest"; -import { commands } from "vitest/browser"; - -import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness"; -import { ChunkAlgorithms, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types"; - -import { DummyFileSourceInisialised } from "../utils/dummyfile"; - -import { page } from "vitest/browser"; -import { DoctorRegulation } from "@/lib/src/common/configForDoc"; -import { waitForDialogHidden, waitForDialogShown } from "../lib/ui"; -const env = (import.meta as any).env; -const dialog_setting_base = { - ...DEFAULT_SETTINGS, - isConfigured: true, - handleFilenameCaseSensitive: false, - couchDB_URI: `${env.hostname}`, - couchDB_DBNAME: `${env.dbname}`, - couchDB_USER: `${env.username}`, - couchDB_PASSWORD: `${env.password}`, - bucket: `${env.bucketName}`, - region: "us-east-1", - endpoint: `${env.minioEndpoint}`, - accessKey: `${env.accessKey}`, - secretKey: `${env.secretKey}`, - useCustomRequestHandler: true, - forcePathStyle: true, - bucketPrefix: "", - usePluginSyncV2: true, - chunkSplitterVersion: ChunkAlgorithms.RabinKarp, - doctorProcessedVersion: DoctorRegulation.version, - notifyThresholdOfRemoteStorageSize: 800, -} as ObsidianLiveSyncSettings; - -function checkDialogVisibility(dialogText: string, shouldBeVisible: boolean): void { - const dialog = page.getByText(dialogText); - expect(dialog).toHaveClass(/modal-title/); - if (!shouldBeVisible) { - expect(dialog).not.toBeVisible(); - } else { - expect(dialog).toBeVisible(); - } - return; -} -function checkDialogShown(dialogText: string) { - checkDialogVisibility(dialogText, true); -} -function checkDialogHidden(dialogText: string) { - checkDialogVisibility(dialogText, false); -} - -describe("Dialog Tests", async () => { - // describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => { - const setting = dialog_setting_base; - beforeAll(async () => { - await DummyFileSourceInisialised; - await commands.grantClipboardPermissions(); - }); - let harness: LiveSyncHarness; - const vaultName = "TestVault" + Date.now(); - beforeAll(async () => { - harness = await generateHarness(vaultName, setting); - await waitForReady(harness); - expect(harness.plugin).toBeDefined(); - expect(harness.plugin.app).toBe(harness.app); - await waitForIdle(harness); - }); - it("should show copy to clipboard dialog and confirm", async () => { - const testString = "This is a test string to copy to clipboard."; - const title = "Copy Test"; - const result = harness.plugin.core.services.UI.promptCopyToClipboard(title, testString); - const isDialogShown = await waitForDialogShown(title, 500); - expect(isDialogShown).toBe(true); - const copyButton = page.getByText("📋"); - expect(copyButton).toBeDefined(); - expect(copyButton).toBeVisible(); - await copyButton.click(); - const copyResultButton = page.getByText("✔️"); - expect(copyResultButton).toBeDefined(); - expect(copyResultButton).toBeVisible(); - const clipboardText = await navigator.clipboard.readText(); - expect(clipboardText).toBe(testString); - const okButton = page.getByText("OK"); - expect(okButton).toBeDefined(); - expect(okButton).toBeVisible(); - await okButton.click(); - const resultValue = await result; - expect(resultValue).toBe(true); - // Check that the dialog is closed - const isDialogHidden = await waitForDialogHidden(title, 500); - expect(isDialogHidden).toBe(true); - }); -}); diff --git a/test/utils/dummyfile.ts b/test/utils/dummyfile.ts deleted file mode 100644 index ab4b8b3f..00000000 --- a/test/utils/dummyfile.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { DEFAULT_SETTINGS } from "@/lib/src/common/types.ts"; -import { readFile } from "../utils/fileapi.vite.ts"; -let charset = ""; -export async function init() { - console.log("Initializing dummyfile utils..."); - - charset = (await readFile("test/utils/testcharvariants.txt")).toString(); - console.log(`Loaded charset of length ${charset.length}`); - console.log(charset); -} -export const DummyFileSourceInisialised = init(); -function* indexer(range: number = 1000, seed: number = 0): Generator { - let t = seed | 0; - while (true) { - t = (t + 0x6d2b79f5) | 0; - let z = t; - z = Math.imul(z ^ (z >>> 15), z | 1); - z ^= z + Math.imul(z ^ (z >>> 7), z | 61); - const float = ((z ^ (z >>> 14)) >>> 0) / 4294967296; - yield Math.floor(float * range); - } -} - -export function* generateFile(size: number): Generator { - const chunkSourceStr = charset; - const chunkStore = [...chunkSourceStr]; // To support indexing avoiding multi-byte issues - const bufSize = 1024; - let buf = ""; - let generated = 0; - const indexGen = indexer(chunkStore.length); - while (generated < size) { - const f = indexGen.next().value; - buf += chunkStore[f]; - generated += 1; - if (buf.length >= bufSize) { - yield buf; - buf = ""; - } - } - if (buf.length > 0) { - yield buf; - } -} -export function* generateBinaryFile(size: number): Generator> { - let generated = 0; - const pattern = Array.from({ length: 256 }, (_, i) => i); - const indexGen = indexer(pattern.length); - const bufSize = 1024; - const buf = new Uint8Array(bufSize); - let bufIdx = 0; - while (generated < size) { - const f = indexGen.next().value; - buf[bufIdx] = pattern[f]; - bufIdx += 1; - generated += 1; - if (bufIdx >= bufSize) { - yield buf; - bufIdx = 0; - } - } - if (bufIdx > 0) { - yield buf.subarray(0, bufIdx); - } -} - -// File size for markdown test files (10B to 1MB, roughly logarithmic scale) -export const FILE_SIZE_MD = [10, 100, 1000, 10000, 100000, 1000000]; -// File size for test files (10B to 40MB, roughly logarithmic scale) -export const FILE_SIZE_BINS = [ - 10, - 100, - 1000, - 50000, - 100000, - 5000000, - DEFAULT_SETTINGS.syncMaxSizeInMB * 1024 * 1024 + 1, -]; diff --git a/test/utils/fileapi.vite.ts b/test/utils/fileapi.vite.ts deleted file mode 100644 index e84f5bde..00000000 --- a/test/utils/fileapi.vite.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { server } from "vitest/browser"; -const { readFile, writeFile } = server.commands; -export { readFile, writeFile }; diff --git a/test/utils/testcharvariants.txt b/test/utils/testcharvariants.txt deleted file mode 100644 index b517a531..00000000 --- a/test/utils/testcharvariants.txt +++ /dev/null @@ -1,17 +0,0 @@ -國破山河在,城春草木深。 -感時花濺淚,恨別鳥驚心。 -烽火連三月,家書抵萬金。 -白頭搔更短,渾欲不勝簪。 -«Nel mezzo del cammin di nostra vita -mi ritrovai per una selva oscura, -ché la diritta via era smarrita.» -Духовной жаждою томим, -В пустыне мрачной я влачился, — -И шестикрылый серафим -На перепутье мне явился. -Shall I compare thee to a summer’s day? -Thou art more lovely and more temperate: -Rough winds do shake the darling buds of May, -And summer’s lease hath all too short a date: - -📜🖋️ 🏺 🏛️ 春望𠮷‌ché🇷🇺Аa‮RTLO🏳️‍🌈👨‍👩‍👧‍👦lʼanatraアイウエオ \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 80944670..e30457f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,7 @@ "strictBindCallApply": true, "strictFunctionTypes": true, "paths": { - "@/*": ["./src/*"], - "@lib/*": ["./src/lib/src/*", "./_types/src/lib/src/*"] + "@/*": ["./src/*"] } }, "include": ["**/*.ts", "test/**/*.test.ts", "**/*.unit.spec.ts", "**/*.svelte"], diff --git a/tsconfig.types.json b/tsconfig.types.json deleted file mode 100644 index 089dbfa5..00000000 --- a/tsconfig.types.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": false, - "declaration": true, - "emitDeclarationOnly": true, - "outDir": "./_types", - "rootDir": "." - }, - "include": ["src/lib/**/*.ts"], - "exclude": [ - "_types", - "pouchdb-browser-webpack", - "utils", - "src/apps", - "src/**/*.test.ts", - "src/lib/_tools", - "src/lib/apps", - "src/lib/src/cli", - "**/_test/**", - "utilsdeno", - "node_modules", - "test/**/*.test.ts", - "**/*.unit.spec.ts" - ] -} diff --git a/updates.md b/updates.md index 36b28f43..078e03fc 100644 --- a/updates.md +++ b/updates.md @@ -1,187 +1,110 @@ -# 0.25 -Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) +# 1.0 -The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +Well then, everyone: it has been roughly a year since I declared the 0.25 beta. During that time, we have concentrated mainly on fixing defects and completing the features that the project needed. + +Version 1.0 has been in mind for some time. We have now brought together the work intended to make it possible: stronger CI, more detailed tests, an E2E runner suited to synchronisation, and testing tools for physical devices. These now form a coherent Kit rather than a collection of isolated pieces. With those foundations in place, it seems that the time has finally come to reshape the structure of this repository. + +None of this would have been possible without your issue reports, pull requests, sponsorship, and the support provided through OpenAI's Codex for Open Source. I would like to express my gratitude once again. As with every pull request contributed to the project, code produced with Codex and similar tools is reviewed and audited by me, vrtmrz. Anyone interested in how I manage that process can refer to my dotfiles. + +This will call for your help once again. I would be very grateful for your co-operation as we build a sounder foundation for the project and its future development. + +Earlier releases remain available in the 0.25 release history and the legacy release history. ## Unreleased -## 0.25.83 +## 1.0.0 -16th July, 2026 +27th July, 2026 -Our plug-in continues to improve every day thanks to all of your contributions. We have finally resolved an issue first reported in 2023. Thank you for everything you contribute. +The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here). -### Fixed +### Setup and compatibility -- Fixed the 📲 remote-activity indicator remaining visible after CouchDB requests had completed. -- Fixed missing chunks being reported unavailable while an in-progress on-demand fetch or finite replication could still deliver them. Reads now follow the actual delivery lifecycle and recheck the local database when it finishes. -- Fixed an issue where changing only the letter case of a file name within the same directory could delete it on other devices when 'Handle files as Case-Sensitive' was disabled. Directory case changes remain unsupported (#198, PR #1014; [commonlib PR #68](https://github.com/vrtmrz/livesync-commonlib/pull/68)). Thank you to @metrovoc for the fix! -- Fixed **Resolve All conflicted files by the newer one** displaying a separate success notice for every resolved file and updating its progress notice for nine out of every ten checked files. Successful per-file results are now logged, progress updates every ten files, and errors and non-bulk success notices remain visible (#1016, PR #1017). Thank you to @apple-ouyang for the fix! +#### Improved -### Improved +- An unconfigured Vault now waits for the user to start setup. Onboarding is offered through a persistent Notice and remains available from **Self-hosted LiveSync settings** → **Setup**. +- Setup now creates named CouchDB, Object Storage, and P2P connections. Setup URIs preserve their connection names and selections, and reserve Fetch or Rebuild before the ordinary start-up scan begins. +- Manual CouchDB setup distinguishes creating the first database from connecting another device. Onboarding requires a successful connection, while Settings can explicitly save an unverified connection and offers each server-setting correction separately. +- Compatible differences limited to the chunk hash algorithm, chunk size, or splitter version are aligned automatically by default. Existing chunks remain readable, an explicit opt-out remains available, and differences involving incompatible settings still require review. -- Split the status-bar remote activity display into `📲` for a finite remote operation and `🌐N` for the approximate number of tracked CouchDB or Object Storage requests currently in progress. +#### Fixed -## 0.25.82 +- Existing Vaults retain their effective legacy settings, including the case-insensitive file-name fallback used when an older release had no explicit case setting. -15th July, 2026 +#### Security -Recently, I created a repository called Fancy Kit and have been trying to build some proper infrastructure around it. Does any of this look like cumbersome bureaucracy? No need to worry: you can carry on as usual. Codex is simply tidying up my usual rambling prose, terrible pull requests, and disjointed remarks. +- Fly.io setup generates CouchDB and Vault encryption secrets with cryptographically secure randomness. +- Dependency updates address excessive CPU use from crafted path patterns and `mailto:` links. -### Fixed +### Conflict handling and recovery -- Refreshed the remote Security Seed before each replication, preventing a client that remained open during a remote database rebuild from uploading data encrypted with the previous seed (#1018, PR #1019). Thank you to @apple-ouyang for the fix! -- The P2P **Start Sync & Close** action now waits for synchronisation to settle before closing the dialogue, avoiding premature release of screen-awake protection while work remains in flight. +#### Improved -### Improved +- **Not now** postpones repeated automatic merge dialogues while retaining the unresolved-conflict warning. Three or more live revisions are reviewed one reproducible pair at a time, completed pairs remain resolved across restart, and explicit commands can reopen a postponed conflict. +- **Inspect conflicts and file/database differences** compares the Vault with the database winner and every live conflict revision. Compact indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflicts remain. +- Each reported file and live revision has a compact wrench menu for comparison, applying an exact readable revision, recording an exact byte match, storing the Vault content as a child of a selected branch, retrying missing chunks without changing the tree, or explicitly discarding one selected live branch. -- On supported mobile and desktop devices, one-shot replication, P2P peer discovery and selection, database rebuild and fetch operations, and remote chunk fetching now keep the screen awake for the duration of these finite remote operations. LiveSync also postpones its normal visibility suspension until the operation finishes, although platform restrictions may still pause or terminate background work. -- The 📲 status-bar indicator now covers finite remote work as well as HTTP requests. It reports broad remote activity, such as P2P peer selection and remote chunk fetching, rather than an exact physical connection count. +#### Fixed -### Improved (CLI) +- Automatic text and structured-data merge now uses the nearest revision actually shared by both branches. A resolution received from another device no longer recreates the same conflict merely because the Vault still contains the exact content of the removed branch. +- Edits, logical deletions, and renames made while a file remains conflicted extend the revision displayed on that device. When the relationship cannot be proved, LiveSync preserves the branches for review. +- Unreadable live revisions are preserved during automatic handling. An absent Vault file and a winning logical deletion are treated as agreement unless another live branch still requires attention. +- Garbage Collection V3 is limited to CouchDB and now protects every live conflict branch, required shared ancestry, and shared chunks. It stops when device progress cannot be verified and reports compaction failure without a contradictory success message. -- CLI container images are now published for both AMD64 and ARM64 platforms. +### P2P and optional synchronisation features -### Testing +#### Improved -- Added a local real-Obsidian compatibility test that writes an encrypted, path-obfuscated note through the LiveSync CLI and verifies that the plug-in materialises the same content. +- P2P and Hidden File Sync remain supported opt-in features. Customisation Sync remains a supported Advanced workflow, while Data Compression remains available but disabled by default. +- P2P controls remain outside the ordinary CouchDB experience until P2P is configured. The current status pane distinguishes announcing changes, following a peer, and persistent per-device actions. +- P2P setup and guidance now distinguish the required signalling relay from optional TURN and describe the replaceable public relay's privacy and availability limits. +- Enabling Hidden File Sync opens one progress Notice before saving the setting and reuses it until the initial scan has finished instead of stacking phase, reload, and restart messages. -## 0.25.81 +#### Fixed -14th July, 2026 +- First-device P2P setup can complete its signalling test without another peer online. Fetch on an additional device still requires an available source peer and a completed P2P Rebuild. +- P2P relay connections now close and are recreated reliably after settings changes and database resets. -I am releasing this update to make the chunk-boundary fix available before the next broader release. Thank you to everyone who helped reproduce, trace, and verify the issue. +### Interface, translation, and operations -### Fixed +#### Improved -- Fixed an issue where a U+FEFF character at a Rabin–Karp chunk boundary could be lost, changing the reconstructed file and causing repeated size-mismatch conflicts (#1000). +- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands retain their identifiers so that existing hotkeys continue to work. +- Setup and review dialogue text can be selected for copying or translation. +- Remote-size warnings use persistent clickable Notices. Initial uploads and Rebuild no longer ask to send every chunk in advance; ordinary replication completes the transfer. +- Obsolete controls for the plug-in trash setting and fixed chunk revisions were removed. The Change Log remains available but no longer opens automatically or tracks an unread count. +- Self-hosted LiveSync now owns its translation catalogue. Commonlib supplies canonical English to other consumers, while translation contributions can be made in the main Self-hosted LiveSync repository. -### Improved +#### Fixed -- Improved vault scanning and CLI file filtering by reusing compiled ignore patterns, reducing processing overhead for vaults with many files or ignore rules (#1006, #1007, and #1008). +- Applying an available interface translation no longer holds start-up behind an unsolicited dialogue; a persistent Notice opens the existing details on demand. +- Action buttons are arranged for narrow mobile screens, long dialogues keep their controls reachable, and persistent Notices no longer cover close controls. -### Improved (CLI and Webapp) +### Storage and file selection -- Rooted storage adapters now reject absolute, drive-qualified, backslash-separated, and traversal paths. They also prevent file writes, appends, and removal from targeting the configured root itself. -- File System Access API storage can now create files below previously missing parent directories, matching the existing Node behaviour. +#### Fixed -### Testing +- The optional Custom HTTP Handler used by Object Storage sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request. +- Broadening selectors, ignore rules, size or modification-time limits, or file-name case handling now rechecks previously received files without requiring another remote update. +- Start-up and full-inspection scans omit built-in legacy LiveSync log files and recovery flag files before comparing Vault and local-database state. Existing ignored database records remain untouched, and user-configured ignore behaviour is unchanged. -- Added shared Node and File System Access API storage contract coverage for metadata, text and binary operations, append, listing, removal, path containment, and empty-root handling. -- Added a Compose-based P2P end-to-end smoke test and repeatable network benchmark cases for local performance investigations. +### Command-line tool -### Miscellaneous +#### Fixed -- Split the internal storage adapter contract into focused capability views without changing existing runtime behaviour. +- CLI Setup URI validation now uses the supported Commonlib ESM package interface. +- The non-root Docker image no longer depends on permissions inherited from the source checkout. -## 0.25.80 +#### Security -7th July, 2026 +- The CLI rejects detected path traversal and symbolic-link components before Vault operations. -### Fixed +### Validation -- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993). - - Behaviour change: - - When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result. - - When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side. -- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994). - - Behaviour change: - - Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text. -- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992). - - Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled. +#### Testing -## 0.25.79 - -29th June, 2026 - -### Fixed - -- Fast Fetch now retries transient stream interruptions and resumes from the latest persisted checkpoint, instead of starting over after ordinary network or platform interruptions (#977, PR #978; commonlib PR #59). Thank you so much for @apple-ouyang for the fix! -- Simple Fetch now remembers the selected setup choices while an interrupted Fetch All operation is still pending, so users are not asked the same questions again on retry (#977, PR #978). Thank you so much for @apple-ouyang for the fix! -- No longer hidden storage events, such as `.git` paths, reach the normal target-file filter when internal file synchronisation is disabled. This avoids noisy non-target logs before those files are skipped (commonlib PR #60). Thank you so much for @apple-ouyang for the fix! -- Fixed an issue where a file deleted from storage could be resurrected by the offline scanner because the database tombstone was not written when the storage file was already gone (commonlib PR #56). Thank you so much for @cosmic-fire-eng for the fix! - -### Improved - -- Local database maintenance commands now ask before applying the required chunk settings, and can apply those prerequisites before continuing (#980, PR #981). Thank you so much for @apple-ouyang for the improvement! -- Improved CouchDB replication event handling by using the new `StreamInbox` helper from `octagonal-wheels` (commonlib PR #62). - -### Documentation - -- Added `nginx` to the setup documentation table of contents (PR #976). Thank you so much for @kiraventom for the improvement! - -### Miscellaneous - -- Updated `octagonal-wheels` to `0.1.47` across the plug-in and workspace packages to use the newly published helper modules. - -## 0.25.78 - -23rd June, 2026 - -### Fixed -- No longer fast synchronisation (a.k.a. Fast Fetch) causes a rewind and re-fetch of the entire database when some errors occur during the process (#972, PR #973). Thank you so much for @apple-ouyang for the fix! - -### Improved - -- Overhauled the Object Storage (e.g., MinIO and S3) replication engine ('Journal Replicator 2nd Edition'). - - It now leverages the standard Web Streams API for a resilient, backpressure-aware architecture, reducing memory footprints/temporary storage usage on large vaults. - - Decoupled the physical storage logic to make it easier to add new storage backends in the future. - - Stricter compliance with CouchDB's replication protocol (proper `_revisions` transfers with `new_edits: false`) when using Object Storage. - -### Testing - - Added comprehensive unit tests for the new `JournalSyncCore` engine, covering streams, backpressure, and `new_edits: false` validation. - - Improved integration test workflows in the CI pipeline to run MinIO tests automatically using standard environment variables. - -## 0.25.77 - -19th June, 2026 - -This update is mostly meaningless for users. But for maintainers, not, I hope. I wonder if I were done well in the start, there would be no hassles. It really was a great opportunity. - -Also, this update is a very large one, even if we had a lot of time, and we had CI tests, and mostly only fixing the types. Please let me know if you find any issues! - -### Improved - -- File deletion now respects the user's deletion preferences (by utilising the `FileManager.trashFile` API) on Obsidian v1.7.2 or newer, regardless of the plug-in's internal trashbin setting. - -### Miscellaneous -- Typings of the library are now included -- Many typing errors have been improved. -- Import paths have been normalised to be relative to the root and to the `lib/src` directory, to avoid breaking the boundary between the library and the plug-in. -- Subprojects, such as the CLI and the webapp, are now in the workspace. - -## 0.25.76 - -15th June, 2026 - -### Fixed - -- Now the S3 connection with custom headers works properly (#875). - - Previously, custom headers injected for proxy authentication were incorrectly included in the AWS Signature v4 calculation. This led to a '400 Bad Request' error (such as 'signed header is not present') on strict S3 backends (for example, Garage), or when reverse proxies modified, renamed, or stripped these headers before they reached the storage service. -- No longer connection information of the P2P synchronisation is broken on the specific platform (#956). - -## 0.25.75 - -13th June, 2026 - -### Fixed - -- Fixed an issue where using fast synchronisation caused a TypeError in some environments (#953). - -### New features -- Now we can configure to keep replication active in the background on desktop platforms (#939, PR #949). Thank you so much for @migsferro! - -### Fixed (CLI, automated) - -- Fixed an issue where the mirror command could fail to apply updates when conflict preservation checks prevented overwriting unsynchronised local changes, even when the `force` parameter or `writeDocumentsIfConflicted` setting was enabled. - -### Improved - -- (CLI) Ported the remaining bash regression tests (`test-daemon-linux.sh`, `test-decoupled-vault-linux.sh`, and `test-remote-commands-linux.sh`) to Deno for cross-platform validation. - -### Miscellaneous -- Some dependencies have been updated. -- Now we check the compatibility with iOS 15 in the CI tests to ensure the plugin continues to work on older iOS versions even after we upgrade some dependencies. - -Full notes are in -[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). +- Expanded automated Real Obsidian coverage for upgrades, two-device synchronisation, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, conflict and revision recovery, failure diagnostics, and strict clean-up. +- Real CouchDB integration coverage verifies logical deletion, shared and conflict chunk retention, compaction, downstream replication, and recreation of content-addressed chunks. +- An encrypted Real Obsidian reconnect scenario replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation adopts the replacement without restoring the old value, and proves a bidirectional encrypted round-trip. +- The plug-in code in this release was installed through BRAT and validated on macOS, iOS, and Android, including upgrade from 0.25.83, bidirectional synchronisation, P2P setup, conflict handling, recovery controls, mobile layouts, and start-up with existing configurations. +- Native and non-root Docker CLI scenarios cover setup, write, read, list, information, deletion, conflict resolution, and revision retrieval with the packaged Commonlib dependency. diff --git a/updates_old.md b/updates_old.md index ad706bf8..6d6e4ee6 100644 --- a/updates_old.md +++ b/updates_old.md @@ -1,3553 +1,10 @@ -# 0.25 -Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) +# Release history has moved -The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +The release history is now kept as one chronological sequence across smaller files: +- [Current 1.x releases](updates.md) +- [1.0 beta and release-candidate history](docs/releases/1.0-previews.md) +- [0.25 releases](docs/releases/0.25.md) +- [Releases before 0.25](docs/releases/legacy.md) -## 0.25.80 - -7th July, 2026 - -### Fixed - -- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993). - - Behaviour change: - - When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result. - - When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side. -- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994). - - Behaviour change: - - Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text. -- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992). - - Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled. - -## 0.25.79 - -29th June, 2026 - -### Fixed - -- Fast Fetch now retries transient stream interruptions and resumes from the latest persisted checkpoint, instead of starting over after ordinary network or platform interruptions (#977, PR #978; commonlib PR #59). Thank you so much for @apple-ouyang for the fix! -- Simple Fetch now remembers the selected setup choices while an interrupted Fetch All operation is still pending, so users are not asked the same questions again on retry (#977, PR #978). Thank you so much for @apple-ouyang for the fix! -- No longer hidden storage events, such as `.git` paths, reach the normal target-file filter when internal file synchronisation is disabled. This avoids noisy non-target logs before those files are skipped (commonlib PR #60). Thank you so much for @apple-ouyang for the fix! -- Fixed an issue where a file deleted from storage could be resurrected by the offline scanner because the database tombstone was not written when the storage file was already gone (commonlib PR #56). Thank you so much for @cosmic-fire-eng for the fix! - -### Improved - -- Local database maintenance commands now ask before applying the required chunk settings, and can apply those prerequisites before continuing (#980, PR #981). Thank you so much for @apple-ouyang for the improvement! -- Improved CouchDB replication event handling by using the new `StreamInbox` helper from `octagonal-wheels` (commonlib PR #62). - -### Documentation - -- Added `nginx` to the setup documentation table of contents (PR #976). Thank you so much for @kiraventom for the improvement! - -### Miscellaneous - -- Updated `octagonal-wheels` to `0.1.47` across the plug-in and workspace packages to use the newly published helper modules. - -## 0.25.78 - -23rd June, 2026 - -### Fixed -- No longer fast synchronisation (a.k.a. Fast Fetch) causes a rewind and re-fetch of the entire database when some errors occur during the process (#972, PR #973). Thank you so much for @apple-ouyang for the fix! - -### Improved - -- Overhauled the Object Storage (e.g., MinIO and S3) replication engine ('Journal Replicator 2nd Edition'). - - It now leverages the standard Web Streams API for a resilient, backpressure-aware architecture, reducing memory footprints/temporary storage usage on large vaults. - - Decoupled the physical storage logic to make it easier to add new storage backends in the future. - - Stricter compliance with CouchDB's replication protocol (proper `_revisions` transfers with `new_edits: false`) when using Object Storage. - -### Testing - - Added comprehensive unit tests for the new `JournalSyncCore` engine, covering streams, backpressure, and `new_edits: false` validation. - - Improved integration test workflows in the CI pipeline to run MinIO tests automatically using standard environment variables. - -## 0.25.77 - -19th June, 2026 - -This update is mostly meaningless for users. But for maintainers, not, I hope. I wonder if I were done well in the start, there would be no hassles. It really was a great opportunity. - -Also, this update is a very large one, even if we had a lot of time, and we had CI tests, and mostly only fixing the types. Please let me know if you find any issues! - -### Improved - -- File deletion now respects the user's deletion preferences (by utilising the `FileManager.trashFile` API) on Obsidian v1.7.2 or newer, regardless of the plug-in's internal trashbin setting. - -### Miscellaneous -- Typings of the library are now included -- Many typing errors have been improved. -- Import paths have been normalised to be relative to the root and to the `lib/src` directory, to avoid breaking the boundary between the library and the plug-in. -- Subprojects, such as the CLI and the webapp, are now in the workspace. - -## 0.25.76 - -15th June, 2026 - -### Fixed - -- Now the S3 connection with custom headers works properly (#875). - - Previously, custom headers injected for proxy authentication were incorrectly included in the AWS Signature v4 calculation. This led to a '400 Bad Request' error (such as 'signed header is not present') on strict S3 backends (for example, Garage), or when reverse proxies modified, renamed, or stripped these headers before they reached the storage service. -- No longer connection information of the P2P synchronisation is broken on the specific platform (#956). - -## 0.25.75 - -13th June, 2026 - -### Fixed - -- Fixed an issue where using fast synchronisation caused a TypeError in some environments (#953). - -### New features -- Now we can configure to keep replication active in the background on desktop platforms (#939, PR #949). Thank you so much for @migsferro! - -### Fixed (CLI, automated) - -- Fixed an issue where the mirror command could fail to apply updates when conflict preservation checks prevented overwriting unsynchronised local changes, even when the `force` parameter or `writeDocumentsIfConflicted` setting was enabled. - -### Improved - -- (CLI) Ported the remaining bash regression tests (`test-daemon-linux.sh`, `test-decoupled-vault-linux.sh`, and `test-remote-commands-linux.sh`) to Deno for cross-platform validation. - -### Miscellaneous -- Some dependencies have been updated. -- Now we check the compatibility with iOS 15 in the CI tests to ensure the plugin continues to work on older iOS versions even after we upgrade some dependencies. - -## 0.25.74 - -8th June, 2026 - -### Fixed - -- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan (#941). -- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911). -- The startup sequence now saves the state more effectively (Thank you so much for @bmcyver)! - -## Only CLI - -8th June, 2026 - -I should also consider the version numbering for the CLI... - -### Improved - -- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`. -- --vault option is now available for daemon and mirror commands! (Thank you so much for @starskyzheng)! -- Decoupled the database directory path from the actual vault directory path using the `--vault` (or `-V`) option. - -### Fixed (preventive) - -- Validated that the specified vault path exists and is indeed a directory before starting the CLI. -- Integrated path resolution and validations for one-off commands (such as `'push'`, `'pull'`, `'cat'`, `'rm'`, `'info'`, and `'resolve'`) against the decoupled vault path instead of the database path. - -## 0.25.73 - -4th June, 2026 - -### Fixed - -- Adjust CouchDB's database name checking to its specification (#926). -- `Reset Syncronisation on This Device` for minio and P2P is now working properly. - -## ~~0.25.71~~ 0.25.72 - -0.25.71 was cancelled due to the fixes needed (Object Storage related) - -3rd June, 2026 - -### Improved - -- Database fetching (a.k.a. Reset Synchronisation on This Device) on the initialisation now supports streaming and is faster (CouchDB only) -- The database fetching process has been streamlined, and database operations are now suspended until it has been completed -- The initial synchronisation process has been simplified, making it easier to synchronise files with the remote server -- We can select the remote database to fetch from during the initialisation, when there are multiple remote databases configured (e.g. multiple CouchDBs or S3 remotes) -- Hebrew (he) Translation has been added (Thank you so much, @MusiCode1)! -- Translation loading time has been reduced (Thank you so much, @bmcyver)! - -### Fixed - -- No longer does the status element break other plugins' interaction (#930). -- No longer does file events occured during initial database fetching using Object Storage. - -### Refactored - -To support the new Community automated tests, we fixed numerous lint warnings. This may have also resolved potential issues. - -## 0.25.70 - -25th May, 2026 - -### New features -- Diff dialogue now has great tools to navigate and understand the differences, including: - - A checkbox to toggle the visibility of collapsed identical sections, making it easier to focus on the actual differences (PR #889). - - A search feature to find specific text in past revisions, and navigate revisions with search results highlighted in the dialogue (PR #890). - -- Conflict resolution dialogue now has a navigation feature to jump between conflicts (PR #891). - -Thank you so much to @SeleiXi for implementing these features! - -### Improved - -- More diagnostic information for P2P connections is now shown, including why a connection failure occurred and the current connection status. - -## 0.25.69 - -22nd May, 2026 - -### Fixed -- No longer does the P2P passphrase mismatch cause a server shutdown. -- Settings related to P2P synchronisation are now correctly applied on start-up and no longer reverted. - -### New features -- Diagnostic P2P connection stats are now available. - - These stats indicate the number of connection trials, successes, and failures. - -## 0.25.68 - -22nd May, 2026 - -### Improved - -- P2P connections have improved slightly - - Upgrade to `trystero` v0.24.0, and fixes event handler assignment. This should fix some edge cases where P2P connections fail to establish or messages are not properly handled. - - Weaken terser options to avoid potential issues with minification that could cause runtime errors in some environments. - -## ~~0.25.66~~ 0.25.67 - -20th May, 2026 - -0.25.66 had a bug that the auto-accept logic for compatible but lossy mismatches was not working as intended. - -### New features -- Implement an auto-accept compatible tweak setting and enhance the mismatch resolution logic. - -### Improved -- Many messages related to tweak mismatch resolution have been updated for clarity. - -## 0.25.65 - -19th May, 2026 - -### Fixed -- Fix an issue about resuming from background on iOS (#888). -- Now Chunk Splitter: `V3: Fine Deduplication` is working fine again (#866). - - It has some drawbacks, such as fewer chunks are generated. However, it makes less transfer and storage when the files are modified but not completely changed. -- Unsynchronised local changes (which means changes that have not been sent) are now correctly preserved as a conflict (Thank you so much for @SeleiXi!). -- Avoid creating a new revision when the current and conflicted revisions have identical content (Thank you so much for @daichi-629). - -### Improved -- Improved the error verbosity on concurrent processing during the start-up process. -- Now the `report` includes recent logs (of verbosity `verbose` even settings is not set to `verbose`). -- Updating logs is now debounced to avoid excessive updates during rapid log generation. -- Added a `Generate full report for opening the issue with debug info` command to the command palette, which generates a report without opening the settings dialogue. - -## 0.25.64 - -17th May, 2026 - -### P2P Status Pane - -- Added active P2P remote selector (combo box) and `+` action to create/select a P2P remote from the P2P setup dialogue. -- Added per-peer immediate replication action on accepted peers. -- Updated status control icons for clarity: - - Replicate now: `🔄` (`⏳` while running) - - Watch: `🔔` / `🔕` - - Sync target: `🔗` / `⛓️‍💥` -- Added warning state when no active P2P remote is selected. - -### P2P Status Card - -- Added stable Room ID suffix display and placed it above Peer ID for better identification. - -### Non behavioural internal changes - -#### P2P - -- Added `P2P_ActiveRemoteConfigurationId` as a dedicated active remote selection for P2P features, separate from the normal active remote. -- Added activation logic for P2P dedicated remote configuration that reflects P2P settings while keeping `remoteType` unchanged. -- Added migration support to carry over P2P active remote selection when appropriate. -- Added shared Room ID utility functions and applied them across P2P setup and P2P panes. - -#### Tests - -- Added/updated unit test coverage around settings load behaviour for P2P active remote application. - -## 0.25.63 - -17th May, 2026 - -### Fixed -- The issue which cannot synchronise in Only-P2P mode has been fixed. -- Fixed an issue where "Failed to connect to the remote server" was shown during the redFlag rebuild flow when P2P was the primary remote type. Remote configuration fetch is now skipped for P2P. - -### P2P Replication UI Improvements -- Brand-new P2P Server Status pane has been added to provide real-time visibility into your connection status and peer network. - - For detailed instructions on using the new P2P features, please refer to the updated [User Guide: Peer-to-Peer Synchronisation (2026 Edition)](./docs/p2p_sync_updates_2026.md). -- Now `Replicate` button or ribbon icon opens a redesigned interactive replication dialogue that performs smart bidirectional sync with a single click. -- The vault rebuild flow (`replicateAllFromServer`) now opens the redesigned P2P Replication modal instead of a plain text selection dialogue, providing a consistent UI experience. - -## 0.25.62 - -14th May, 2026 - -### Fixed - -- Fixed an issue where a connection could not be established when attempting to connect to a brand-new remote database without going through the set-up wizard or configuration checking (#660). - -## 0.25.61 - -13th May, 2026 - -Reviews have started on the Obsidian Community, haven't they? It was quite a struggle, what with having to fix the outdated ESLint. -I am a bit nervous, but it is far better than just plodding along aimlessly, so let us get on with it. If you spot any issues, please let me know straight away. - -From now on, I am avoiding committing directly to the main branch. This is because you lots have all been sending so much PRs. I wanted to keep things harmonious. -That said, I am still not used to rebasing, so there are some parts where the commit history is a right mess. I will work on improving that. - -### Improved - -- P2P synchronisation has been made more robust - Now the foundation for P2P synchronisation has been rewritten, and the unit tests have been added. The foundation has been separated into the transport layer, signalling-and-connection layer, and, an RPC layers. And each layer has been unit-tested. As the result, the P2P synchronisation now uses the robust shim that uses RPC-ed PouchDB synchronisation in contrast to previous implementation. -This P2P synchronisation is not compatible with previous versions in terms of connectivity. All devices must be updated. - -### Fixed - -- No longer baffling errors occur when setting-update is triggered during the early stage of initialisation. -- Network error notice pop-ups are now suppressed when 'NetworkWarningStyle' is set to 'Hidden'. (Thank you so much @SeleiXi!) - -### New features - -- Diff navigation buttons have been added to the diff view, making it easier to move between differences. (Thank you so much @SeleiXi! #871) - -### Translations - -- Chinese (Simplified) translations for settings and the Setup Wizard have been added. (Thank you so much @zombiek731!) -- Common UI controls and signal words are now localised into Chinese (Simplified). (Thank you so much @zombiek731!) -- i18n runtime behaviour and locale coverage have been improved. (Thank you so much @52sanmao!) - -### CLI - -#### New features - -- Daemon synchronisation is now supported. (Thank you so much @andrewleech! #843) -- `HeadlessConfirm` has been implemented with sensible defaults, enabling unattended operation in headless environments. (Thank you so much @andrewleech!) -- The CLI onboarding experience has been improved. (Thank you so much @OriBoharon! #872) - -#### Fixed - -- Sub-millisecond CLI mtimes are now truncated to prevent mobile crash. (Thank you so much @brian-spackman! #893) - -## 0.25.60 - -29th April, 2026 - -### Fixed - -- Now larger settings can be exported and imported via QR code without issues. (#595) - - When the settings data exceeds the QR code capacity, it is now split into multiple QR codes. - - These QR codes are reassembled by the aggregator page, which collects the split data and reconstructs the original settings. - - Aggregator page is available at `https://vrtmrz.github.io/obsidian-livesync/aggregator.html`, and this file is also included in the repository. - - We will not send the settings data to any server. The QR code data is generated and processed entirely on the client side, ensuring that your settings remain private and secure. HOWEVER, please be careful your network environment. -- Fixed some errors during serialisation and deserialisation of the settings, which caused issues in some cases when importing/exporting settings via QR code. - -### Fixed (CLI) - -- `ls` and `mirror` commands now provide informative feedback when no documents are found or filters skip all files, resolving the issue where they would exit silently (#860). - - Improved the clarity of CLI command logs by including the total count of processed items. -- The command-line argument `vault` has been renamed to a more appropriate name, `databaseDir`. -- The `mirror` command now accepts a `vault` directory, which specifies the location where the actual files are stored. For compatibility reasons, the previous behaviour is still supported. - -## 0.25.59 - -### Fixed - -- No longer Setup-wizard drops username and password silently. (#865) - - Thank you so much for @koteitan ! -- Setup URI is now correctly imported (#859). - - Also thank you so much for @koteitan ! - -### Improved - -- now French translation is added by @foXaCe ! Thank you so much! - -## 0.25.58 - -### Fixed - -- No longer credentials are broken during object storage configuration (related: #852). -- Fixed a worker-side recursion issue that could raise `Maximum call stack size exceeded` during chunk splitting (related: #855). -- Improved background worker crash cleanup so pending split/encryption tasks are released cleanly instead of being left in a waiting state (related: #855). -- On start-up, the selected remote configuration is now applied to runtime connection fields as well, reducing intermittent authentication failures caused by stale runtime settings (related: #855). -- Issue report generation now redacts `remoteConfigurations` connection strings and keeps only the scheme (e.g. `sls+https://`), so credentials are not exposed in reports. -- Hidden file JSON conflicts no longer keep re-opening and dismissing the merge dialogue before we can act, which fixes persistent unresolvable `data.json` conflicts in plug-in settings sync (related: #850). - -## 0.25.57 - -9th April, 2026 - -- Packing a batch during the journal sync now continues even if the batch contains no items to upload. -- No unexpected error (about a replicator) during the early stage of initialisation. -- Now error messages are kept hidden if the show status inside the editor is disabled (related: #829). -- Fixed an issue where devices could no longer upload after another device performed 'Fresh Start Wipe' and 'Overwrite remote' in Object Storage mode (#848). - - Each device's local deduplication caches (`knownIDs`, `sentIDs`, `receivedFiles`, `sentFiles`) now track the remote journal epoch (derived from the encryption parameters stored on the remote). - - When the epoch changes, the plugin verifies whether the device's last uploaded file still exists on the remote. If the file is gone, it confirms a remote wipe and automatically clears the stale caches. If the file is still present (e.g. a protocol upgrade without a wipe), the caches are preserved, and only the epoch is updated. This means normal upgrades never cause unnecessary re-processing. - -### Translations - -- Russian translation has been added! Thank you so much for the contribution, @vipka1n! (#845) - -### New features - -- Now we can configure multiple Remote Databases of the same type, e.g, multiple CouchDBs or S3 remotes. - - A user interface for managing multiple remote databases has been added to the settings dialogue. I think no explanation is needed for the UI, but please let me know if you have any questions. -- We can switch between multiple Remote Databases in the settings dialogue. - -### CLI - -#### Fixed - -- Replication progress is now correctly saved and restored in the CLI (related: #846). - -## ~~0.25.55~~ 0.25.56 - -30th March, 2026 - -### Fixed - -- No longer `Peer-to-Peer Sync is not enabled. We cannot open a new connection.` error occurs when we have not enabled P2P sync and are not expected to use it (#830). - -### CLI - -- Fixed incomplete localStorage support in the CLI (#831). Thank you so much @rewse ! -- Fixed the issue where the CLI could not be connected to the remote which had been locked once (#833), also thanks to @rewse ! - -## 0.25.54 - -18th March, 2026 - -### Fixed - -- Remote storage size check now works correctly again (#818). -- Some buttons on the settings dialogue now respond correctly again (#827). - -### Refactored - -- P2P replicator has been refactored to be a little more robust and easier to understand. -- Delete items which are no longer used that might cause potential problems - -### CLI - -- Fixed the corrupted display of the help message. -- Remove some unnecessary code. - -### WebApp - -- Fixed the issue where the detail level was not being applied in the log pane. -- Pop-ups are now shown. -- Add coverage for the test. -- Pop-ups are now shown in the web app as well. - -## 0.25.53 - -17th March, 2026 - -I did wonder whether I should have released a minor version update, but when I actually tested it, compatibility seemed to be intact, so I didn’t. Hmm. - -### Fixed - -#### P2P Synchronisation - -- Fixed flaky timing issues in P2P synchronisation. -- No longer unexpected `Unhandled Rejections` during P2P operations (waiting for acceptance). - -#### Journal Sync - -- Fixed an issue where some conflicts cannot be resolved in Journal Sync. -- Many minor fixes have been made for better stability and reliability. - -### Tests - -- Rewrite P2P end-to-end tests to use the CLI as a host. - -### CLI - -We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details. -- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version. -- P2P sync and Object Storage are also supported in the CLI. - - Yes, we have finally managed to 'get one file'. - - Also, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can do it. - -- Now binary files are also supported in the CLI. - -### Refactored or internal changes - -- ServiceFileAccessBase now correctly handles the reading of binary files. -- HeadlessAPIService now correctly provides the online status (always online) to the plug-in. -- Non-worker version of bgWorker now correctly handles some functions. -- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`. -- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`. -- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`. -- Offline change scanner and the local database preparation have been separated. -- Set default priority for processFileEvent and processSynchroniseResult for the place to add hooks. -- ControlService now provides the readiness for processing operations. -- DatabaseService is now able to modify database opening options on derived classes. -- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`. -- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported. -- Journal Replicator now yields true after the replication is done. - -### R&D - -- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing. -- We can see the code in `src/apps/webapp` for the browser version. - - -## 0.25.52-patched-3 - -16th March, 2026 - -### Fixed - -- Fixed flaky timing issues in P2P synchronisation. -- Fixed more binary file handling issues in CLI. - -### Tests - -- Rewrite P2P end-to-end tests to use the CLI as host. - - -## 0.25.52-patched-2 - -14th March, 2026 - -### Fixed - -- No longer unexpected `Unhandled Rejections` during P2P operations (waiting acceptance). -- Fixed an issue where conflicts cannot be resolved in Journal Sync - -### CLI new features - -- `mirror` command has been added to the CLI. This command is intended to mirror the storage to the local database. -- `p2p-sync`, `p2p-peers`, and `p2p-host` commands have been added to the CLI. These commands are intended for P2P synchronisation. - - Yes, no more need for a [LiveSync PeerServer](https://github.com/vrtmrz/livesync-serverpeer) for virtual environments! The CLI can handle it by itself. - -## 0.25.52-patched-1 - -12th March, 2026 - -### Fixed - -- Fixed Journal Sync had not been working on some timing, due to a compatibility issue (for a long time). -- ServiceFileAccessBase now correctly handles the reading of binary files. -- HeadlessAPIService now correctly provides the online status (always online) to the plug-in. -- Non-worker version of bgWorker now correctly handles some functions. - -### Refactored - -- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`. -- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`. -- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`. -- Offline change scanner and the local database preparation have been separated. -- Set default priority for processFileEvent and processSynchroniseResult for the place to add hooks. -- ControlService now provides the readiness for processing operations. -- DatabaseService is now able to modify database opening options on derived classes. -- Now `useOfflineScanner`, `useCheckRemoteSize`, and `useRedFlagFeatures` are set from `main.ts`, instead of `LiveSyncBaseCore`. - -### Internal API changes - -- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported. -- Journal Replicator now yields true after the replication is done. - -### CLI - -We have previously developed FileSystem LiveSync and various other components in a separate repository, but updates have been significantly delayed, and we have been plagued by compatibility issues. Now, a CLI tool using the same core logic is emerging. This does not directly manipulate the file system, but it offers a more convenient way of working and can also communicate with Object Storage. We can also resolve conflicts. Please refer to the code in `src/apps/cli` for the [self-hosted-livesync-cli](./src/apps/cli/README.md) for more details. - -- Add `self-hosted-livesync-cli` to `src/apps/cli` as a headless and dedicated version. -- Add more tests. -- Object Storage support has also been confirmed (and fixed) in CLI. - - Yes, we have finally managed to 'get one file'. -- Now binary files are also supported in the CLI. - -### R&D - -- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing. -- We can see the code in `src/apps/webapp` for the browser version. - - -## 0.25.52 - -9th March, 2026 - -Excuses: Too much `I`. -Whilst I had a fever, I could not figure it out at all, but once I felt better, I spotted the problem in about thirty seconds. I apologise for causing you concern. I am grateful for your patience. -I would like to devise a mechanism for running simple test scenarios. Now that we have got the Obsidian CLI up and running, it seems the perfect opportunity. - -To improve the bus factor, we really need to organise the source code more thoroughly. Your cooperation and contributions would be greatly appreciated. - -### Fixed - -- No longer unexpected deletion-propagation occurs when the parent directory is not empty (#813). - -### Revert reversions - -- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature. - -## 0.25.51 - -7th March, 2026 - -### Reverted - -- Reverted to ModuleRedFlag and ModuleInitializerFile to the previous version because of some unexpected issues. (#813) - - I will re-implement them in the future with better design and tests. - -## 0.25.50 - -3rd March, 2026 - -Note: 0.25.49 has been skipped because of too verbose logging (credentials are logged in verbose level, but I realised that could lead to unexpected exposure on issue reporting). Please bump to 0.25.50 to get the fix if you are on 0.25.49. (No expected behaviour changes except the logging). - -### Fixed - -- No longer deleted files are not clickable in the Global History pane. -- Diff view now uses more specific classes (#803). -- A message of configuration mismatching slightly added for better understanding. - - Now it says `When replication is initiated manually via the command palette or ribbon, a dialogue box will open to address this.` to make it clear that the user can fix the issue by themselves. - -### Refactored - -- `ModuleRedFlag` has been refactored to `serviceFeatures/redFlag` and also tested. -- `ModuleInitializerFile` has been refactored to `lib/serviceFeatures/offlineScanner` and also tested. - -## 0.25.48 - -2nd March, 2026 - -No behavioural changes except unidentified faults. Please report if you find any unexpected behaviour after this update. - -### Refactored - -- Many storage-related functions have been refactored for better maintainability and testability. - - Now all platform-specific logics are supplied as adapters, and the core logic has become platform-agnostic. - - Quite a number of tests have been added for the core logic, and the platform-specific logics are also tested with mocked adapters. - -## 0.25.47 - -27th February, 2026 - -Phew, the financial year is still not over yet, but I have got some time to work on the plug-in again! - -### Fixed and refactored - -- Fixed the inexplicable behaviour when retrieving chunks from the network. - - The chunk manager has been layered to be responsible for its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`, and `ArrivalWaitLayer`. - - All layers have been tested now! - - `LayeredChunkManager` has been implemented to manage these layers. Also tested. - - `EntryManager` has been mostly rewritten and also tested. - -- Now we can configure `Never warn` for remote storage size notification again. - -### Tests - -- The following test has been added: - - `ConflictManager`. - -## 0.25.46 - -26th February, 2026 - -### Fixed - -- Unexpected errors no longer occurred when the plug-in was unloaded. -- Hidden File Sync now respects selectors. -- Registering protocol-handlers now works safely without causing unexpected errors. - -### Refactored - -- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and tests have also been added. -- Some unnecessary things have been removed. -- LiveSyncManagers has now explicit dependencies. -- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies. - - This is to avoid circular dependencies and clarify the ownership of the managers. -- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly. -- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage. - -## 0.25.45 - -25th February, 2026 - -As a result of recent refactoring, we are able to write tests more easily now! - -### Refactored - -- `ModuleTargetFilter`, which was responsible for checking if a file is a target file, has been ported to a serviceFeature. - - And also tests have been added. The middleware-style-power. -- `ModuleObsidianAPI` has been removed and implemented in `APIService` and `RemoteService`. -- Now `APIService` is responsible for the network-online-status, not `databaseService.managers.networkManager`. - -## 0.25.44 - -24th February, 2026 - -This release represents a significant architectural overhaul of the plug-in, focusing on modularity, testability, and stability. While many changes are internal, they pave the way for more robust features and easier maintenance. -However, as this update is very substantial, please do feel free to let me know if you encounter any issues. - -### Fixed - -- Ignore files (e.g., `.ignore`) are now handled efficiently. -- Replication & Database: - - Replication statistics are now correctly reset after switching replicators. -- Fixed `File already exists` for .md files has been merged (PR #802) So thanks @waspeer for the contribution! - -### Improved - -- Now we can configure network-error banners as icons, or hide them completely with the new `Network Warning Style` setting in the `General` pane of the settings dialogue. (#770, PR #804) - - Thanks so much to @A-wry! - -### Refactored - -#### Architectural Overhaul: - -- A major transition from Class-based Modules to a Service/Middleware architecture has begun. - - Many modules (for example, `ModulePouchDB`, `ModuleLocalDatabaseObsidian`, `ModuleKeyValueDB`) have been removed or integrated into specific Services (`database`, `keyValueDB`, etc.). - - Reduced reliance on dynamic binding and inverted dependencies; dependencies are now explicit. - - `ObsidianLiveSyncPlugin` properties (`replicator`, `localDatabase`, `storageAccess`, etc.) have been moved to their respective services for better separation of concerns. - - In this refactoring, the Service will henceforth, as a rule, cease to use setHandler, that is to say, simple lazy binding. - - They will be implemented directly in the service. - - However, not everything will be middlewarised. Modules that maintain state or make decisions based on the results of multiple handlers are permitted. -- Lifecycle: - - Application LifeCycle now starts in `Main` rather than `ServiceHub` or `ObsidianMenuModule`, ensuring smoother startup coordination. - -#### New Services & Utilities: - -- Added a `control` service to orchestrate other services (for example, handling stop/start logic during settings realisation). -- Added `UnresolvedErrorManager` to handle and display unresolved errors in a unified way. -- Added `logUtils` to unify logging injection and formatting. -- `VaultService.isTargetFile` now uses multiple, distinct checkers for better extensibility. - -#### Code Separation: - -- Separated Obsidian-specific logic from base logic for `StorageEventManager` and `FileAccess` modules. -- Moved reactive state values and statistics from the main plug-in instance to the services responsible for them. - -#### Internal Cleanups: - -- Many functions have been renamed for clarity (for example, `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`). -- Added `override` keywords to overridden items and removed dynamic binding for clearer code inheritance. -- Moved common functions to the common library. - -#### Dependencies: - -- Bumped dependencies simply to a point where they can be considered problem-free (by human-powered-artefacts-diff). - - Svelte, terser, and more something will be bumped later. They have a significant impact on the diff and paint it totally. - - You may be surprised, but when I bump the library, I am actually checking for any unintended code. - -## 0.25.43-patched-9 a.k.a. 0.25.44-rc1 - -We are finally ready for release. I think I will go ahead and release it after using it for a few days. - -### Fixed - -- Hidden file synchronisation now works! -- Now Hidden file synchronisation respects `.ignore` files. -- Replicator initialisation during rebuilding now works correctly. - -### Refactored - -- Some methods naming have been changed for better clarity, i.e., `_isTargetFileByLocalDB` is now `_isTargetAcceptedByLocalDB`. - -### Follow-up tasks memo (After 0.25.44) - -Going forward, functionality that does not span multiple events is expected to be implemented as middleware-style functions rather than modules based on classes. - -Consequently, the existing modules will likely be gradually dismantled. -For reference, `ModuleReplicator.ts` has extracted several functionalities as functions. - -However, this does not negate object-oriented design. Where lifecycles and state are present, and the Liskov Substitution Principle can be upheld, we design using classes. After all, a visible state is preferable to a hidden state. In other words, the handler still accepts both functions and member methods, so formally there is no change. - -As undertaking this for everything would be a bit longer task, I intend to release it at this stage. - -Note: I left using `setHandler`s that as a mark of `need to be refactored`. Basically, they should be implemented in the service itself. That is because it is just a mis-designed, separated implementation. - -## 0.25.43-patched-8 - -I really must thank you all. You know that it seems we have just a little more to do. -Note: This version is not fully tested yet. Be careful to use this. Very dogfood-y one. - -### Fixed - -- Now the device name is saved correctly. - -### Refactored - -- Add `override` keyword to all overridden items. -- More dynamic binding has been removed. -- The number of inverted dependencies has decreased much more. -- Some check-logic; i.e., like pre-replication check is now separated into check functions and added to the service as handlers, layered. - - This may help with better testing and better maintainability. - - -## 0.25.43-patched-7 - -19th February, 2026 - -Right then, let us make a decision already. - -Last time, since I found a bug, I ended up doing a few other things as well, but next time I intend to release it with just the bug fix. It is quite substantial, after all. - -Customisation Sync has mostly been verified. Hidden file synchronisation has not been done yet. - -Vite's build system is not in the production. However, I possibly migrate to it in the future. - -And, the `daily-progress` will be tidied on releasing 0.25.44. Do not worry! - -### Fixed - -- Fixed an issue where the StorageEventManager was not correctly loading the settings. -- Replication statistics are now correctly reset after switching replicators. - -### Refactored - -- Now, many reactive values which keep the state or statistics of the plugin are moved to the services which have the responsibility for these states. -- `serviceFeatures` are now able to be added to the services; this is not a class module, but a function which accepts dependencies and returns an addHandler-able function. This is for better separation of concerns, better maintainability, and testability. -- `control` service; is a meta-service which is responsible for orchestrating services has been added. - - Don't you think stopping replication or something occurs during `settingService.realiseSetting` is quite weird? It may be done by the control service, which can orchestrate the setting service and the replicator service. - - -- Some functions on services have been moved. e.g., `getSystemVaultName` is now on the API service. -- Setting Service is now responsible for the setting, no longer using dynamic binding for the modules. - -## 0.25.43-patched-6 - -18th February, 2026 - -Let me confess that I have lied about `now all ambiguous properties`... I have found some more implicit calling. - -Note: I have not checked hidden file sync and customisation sync yet. Please report if you find any unexpected behaviour in these features. - -### Fixed - -- Now ReplicatorService responds to database reset and database initialisation events to dispose of the active replicator. - - Fixes some unlocking issues during rebuilding. - -### Refactored - -- Now `StorageEventManagerBase` is separated from `StorageEventManagerObsidian` following their concerns. - - No longer using `ObsidianFileAccess` indirectly during checking duplicated-file events. - - Last event memorisation is now moved into the StorageAccessManager, just like the file processing interlocking. - - These methods, i.e., `ObsidianFileAccess.touch`. `StorageEventManager.recentlyTouched`, and `StorageEventManager.touch` are still available, but simply call the StorageAccessManager's methods. -- Now `FileAccessBase` is separated from `FileAccessObsidian` following their concerns. - -## 0.25.43-patched-5 - -17th February, 2026 - -Yes, we mostly have got refactored! - -### Refactored - -- Following properties of `ObsidianLiveSyncPlugin` are now initialised more explicitly: - - - property : what is responsible - - `storageAccess` : `ServiceFileAccessObsidian` - - `databaseFileAccess` : `ServiceDatabaseFileAccess` - - `fileHandler` : `ServiceFileHandler` - - `rebuilder` : `ServiceRebuilder` - - Not so long from now, ServiceFileAccessObsidian might be abstracted to a more general FileAccessService, and make more testable and maintainable. - - These properties are initialised in `initialiseServiceModules` on `ObsidianLiveSyncPlugin`. - - They are `ServiceModule`s. - - Which means they do not use dynamic binding themselves, but they use bound services. - - ServiceModules are in src/lib/src/serviceModules for common implementations, and src/serviceModules for Obsidian-specific implementations. - - Hence, now all ambiguous properties of `ObsidianLiveSyncPlugin` are initialised explicitly. We can proceed to testing. - - Well, I will release v0.25.44 after testing this. - -- Conflict service is now responsible for `resolveAllConflictedFilesByNewerOnes` function, which has been in the rebuilder. -- New functions `updateSettings`, and `applyPartial` have been added to the setting service. We should use these functions instead of directly writing the settings on `ObsidianLiveSyncPlugin.setting`. -- Some interfaces for services have been moved to src/lib/src/interfaces. -- `RemoteService.tryResetDatabase` and `tryCreateDatabase` are now moved to the replicator service. - - You know that these functions are surely performed by the replicator. - - Probably, most of the functions in `RemoteService` should be moved to the replicator service, but for now, these two functions are moved as they are the most related ones, to rewrite the rebuilder service. -- Common functions are gradually moved to the common library. -- Now, binding functions on modules have been delayed until the services and service modules are initialised, to avoid fragile behaviour. - -## 0.25.43-patched-4 - -16th February, 2026 - -I have been working on it little by little in my spare time. Sorry for the delayed response for issues! ! However, thanks for your patience, we seems the `revert to 0.25.43` is not necessary, and I will keep going with this version. - -### Refactored - -- No longer `DatabaseService` is an injectable service. It is now actually a service which has its own handlers. No dynamic binding for necessary functions. -- Now the following properties of `ObsidianLiveSyncPlugin` belong to each service: - - `replicator` : `services.replicator` (still we can access `ObsidianLiveSyncPlugin.replicator` for the active replicator) -- A Handy class `UnresolvedErrorManager` has been added, which is responsible for managing unresolved errors and their handlers (we will see `unresolved errors` on a red-background-banner in the editor when they occur). - - This manager can be used to handle unresolved errors in a unified way, and it can also be used to display notifications or something when unresolved errors occur. - -## 0.25.43-patched-3 - -16th February, 2026 - -### Refactored - -- Now following properties of `ObsidianLiveSyncPlugin` belong to each service: - - property : service (still we can access these properties from `ObsidianLiveSyncPlugin` for better usability, but probably we should access these from services to clarify the dependencies) - - `localDatabase` : `services.database` - - `managers` : `services.database` - - `simpleStore` : `services.keyValueDB` - - `kvDB`: `services.keyValueDB` -- Initialising modules, addOns, and services are now explicitly separated in the `_startUp` function of the main plug-in class. -- LiveSyncLocalDB now depends more explicitly on specified services, not the whole `ServiceHub`. -- New service `keyValueDB` has been added. This had been separated from the `database` service. -- Non-trivial modules, such as `ModuleExtraSyncObsidian` (which only holds deviceAndVaultName), are simply implemented in the service. -- Add `logUtils` for unifying logging method injection and formatting. This utility is able to accept the API service for log writing. -- `ModuleKeyValueDB` has been removed, and its functionality is now implemented in the `keyValueDB` service. -- `ModulePouchDB` and `ModuleLocalDatabaseObsidian` have been removed, and their functionality is now implemented in the `database` service. - - Please be aware that you have overridden createPouchDBInstance or something by dynamic binding; you should now override the createPouchDBInstance in the database service instead of using the module. - - You can refer to the `DirectFileManipulatorV2` for an example of how to override the createPouchDBInstance function in the database service. - -## 0.25.43-patched-2 - -14th February, 2026 - -### Fixed - -- Application LifeCycle has now started in Main, not ServiceHub. - - Indeed, ServiceHub cannot be known other things in main have got ready, so it is quite natural to start the lifecycle in main. - -## 0.25.43-patched-1 - -13th February, 2026 - -**NOTE: Hidden File Sync and Customisation Sync may not work in this version.** - -Just a heads-up: this is a patch version, which is essentially a beta release. Do not worry about the following memos, as they are indeed freaking us out. I trust that you have thought this was too large; you're right. - -If this cannot be stable, I will revert to 0.24.43 and try again. - -### Refactored - -- Now resolving unexpected and inexplicable dependency order issues... -- The function which is able to implement to the service is now moved to each service. - - AppLifecycleService.performRestart -- VaultService.isTargetFile is now using multiple checkers instead of a single function. - - This change allows better separation of concerns and easier extension in the future. -- Application LifeCycle has now started in ServiceHub, not ObsidianMenuModule. - - - It was in a QUITE unexpected place..., isn't it? - - Instead of, we should call `await this.services.appLifecycle.onReady()` in other platforms. - - As in the browser platform, it will be called at `DOMContentLoaded` event. - -- ModuleTargetFilter, which is responsible for parsing ignore files, has been refined. - - This should be separated to a TargetFilter and an IgnoreFileFilter for better maintainability. -- Using `API.addCommand` or some Obsidian API and shimmer APIs, Many modules have been refactored to be derived to AbstractModule from AbstractObsidianModule, to clarify the dependencies. (we should make `app` usage clearer...) -- Fixed initialising `storageAccess` too late in `FileAccessObsidian` module (I am still wondering why it worked before...). -- Remove some redundant overrides in modules. - -### Planned - -- Some services have an ambiguous name, such as `Injectable`. These will be renamed in the future for better clarity. -- Following properties of `ObsidianLiveSyncPlugin` should be initialised more explicitly: - - property : where it is initialised currently - - `localDatabase` : `ModuleLocalDatabaseObsidian` - - `managers` : `ModuleLocalDatabaseObsidian` - - `replicator` : `ModuleReplicator` - - `simpleStore` : `ModuleKeyValueDB` - - `storageAccess` : `ModuleFileAccessObsidian` - - `databaseFileAccess` : `ModuleDatabaseFileAccess` - - `fileHandler` : `ModuleFileHandler` - - `rebuilder` : `ModuleRebuilder` - - `kvDB`: `ModuleKeyValueDB` - - And I think that having a feature in modules directly is not good for maintainability, these should be separated to some module (loader) and implementation (not only service, but also independent something). -- Plug-in statuses such as requestCount, responseCount... should be moved to a status service or somewhere for better separation of concerns. - -## 0.25.43 - -5th, February, 2026 - -### Fixed - -- Encryption/decryption issues when using Object Storage as remote have been fixed. - - Now the plug-in falls back to V1 encryption/decryption when V2 fails (if not configured as ForceV1). - - This may fix the issue reported in #772. - -### Notice - -Quite a few packages have been updated in this release. Please report if you find any unexpected behaviour after this update. - -## 0.25.42 - -2nd, February, 2026 - -This release is identical to 0.25.41-patched-3, except for the version number. - -### Refactored - -- Now the service context is `protected` instead of `private` in `ServiceBase`. - - This change allows derived classes to access the context directly. -- Some dynamically bound services have been moved to services for better dependency management. -- `WebPeer` has been moved to the main repository from the sub repository `livesync-commonlib` for correct dependency management. -- Migrated from the outdated, unstable platform abstraction layer to services. - - A bit more services will be added in the future for better maintainability. - -## 0.25.41 - -24th January, 2026 - -### Fixed - -- No longer `No available splitter for settings!!` errors occur after fetching old remote settings while rebuilding local database. (#748) - -### Improved - -- Boot sequence warning is now kept in the in-editor notification area. - -### New feature - -- We can now set the maximum modified time for reflect events in the settings. (for #754) - - This setting can be configured from `Patches` -> `Remediation` in the settings dialogue. - - Enabling this setting will restrict the propagation from the database to storage to only those changes made before the specified date and time. - - This feature is primarily intended for recovery purposes. After placing `redflag.md` in an empty vault and importing the Self-hosted LiveSync configuration, please perform this configuration, and then fetch the local database from the remote. - - This feature is useful when we want to prevent recent unwanted changes from being reflected in the local storage. - -### Refactored - -- Module to service refactoring has been started for better maintainability: - - UI module has been moved to UI service. - -### Behaviour change - -- Default chunk splitter version has been changed to `Rabin-Karp` for new installations. - -## 0.25.40 - -23rd January, 2026 - -### Fixed - -- Fixed an issue where some events were not triggered correctly after the refactoring in 0.25.39. - -## 0.25.39 - -23rd January, 2026 - -Also no behaviour changes or fixes in this release. Just refactoring for better maintainability. Thank you for your patience! I will address some of the reported issues soon. -However, this is not a minor refactoring, so please be careful. Let me know if you find any unexpected behaviour after this update. - -### Refactored - -- Rewrite the service's binding/handler assignment systems -- Removed loopholes that allowed traversal between services to clarify dependencies. -- Consolidated the hidden state-related state, the handler, and the addition of bindings to the handler into a single object. - - Currently, functions that can have handlers added implement either addHandler or setHandler directly on the function itself. - I understand there are differing opinions on this, but for now, this is how it stands. -- Services now possess a Context. Please ensure each platform has a class that inherits from ServiceContext. -- To permit services to be dynamically bound, the services themselves are now defined by interfaces. - -## 0.25.38 - -17th January, 2026 - -### Fixed - -- Fixed an issue where indexedDB would not close correctly on some environments, causing unexpected errors during database operations. - -## 0.25.37 - -15th January, 2026 - -Thank you for your patience until my return! - -This release contains minor changes discovered and fixed during test implementation. -There are no changes affecting usage. - -### Refactored - -- Logging system has been slightly refactored to improve maintainability. -- Some import statements have been unified. - -## 0.25.36 - -25th December, 2025 - -### Improved - -- Now the garbage collector (V3) has been implemented. (Beta) - - This garbage collector ensures that all devices are synchronised to the latest progress to prevent inconsistencies. - - In other words, it makes sure that no new conflicts would have arisen. - - This feature requires additional information (via node information), but it should be more reliable. - - This feature requires all devices have v0.25.36 or later. - - After the garbage collector runs, the database size may be reduced (Compaction will be run automatically after GC). - - We should have an administrative privilege on the remote database to run this garbage collector. -- Now the plug-in and device information is stored in the remote database. - - This information is used for the garbage collector (V3). - - Some additional features may be added in the future using this information. - -## 0.25.35 - -24th December, 2025 - -Sorry for a small release! I would like to keep things moving along like this if possible. After all, the holidays seem to be starting soon. I will be doubled by my business until the 27th though, indeed. - -### Fixed - -- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764). - -## 0.25.34 - -10th December, 2025 - -### Behaviour change - -- The plug-in automatically fetches the missing chunks even if `Fetch chunks on demand` is disabled. - - This change is to avoid loss of data when receiving a bulk of revisions. - - This can be prevented by enabling `Use Only Local Chunks` in the settings. -- Storage application now saved during each event and restored on startup. -- Synchronisation result application is also now saved during each event and restored on startup. - - These may avoid some unexpected loss of data when the editor crashes. - -### Fixed - -- Now the plug-in waits for the application of pended batch changes before the synchronisation starts. - - This may avoid some unexpected loss or unexpected conflicts. - Plug-in sends custom headers correctly when RequestAPI is used. -- No longer causing unexpected chunk creation during `Reset synchronisation on This Device` with bucket sync. - -### Refactored - -- Synchronisation result application process has been refactored. -- Storage application process has been refactored. - - Please report if you find any unexpected behaviour after this update. A bit of large refactoring. - -## 0.25.33 - -05th December, 2025 - -### New feature - -- We can analyse the local database with the `Analyse database usage` command. - - This command makes a TSV-style report of the database usage, which can be pasted into spreadsheet applications. - - The report contains the number of unique chunks and shared chunks for each document revision. - - Unique chunks indicate the actual consumption. - - Shared chunks indicate the reference counts from other chunks with no consumption. - - We can find which notes or files are using large amounts of storage in the database. Or which notes cannot share chunks effectively. - - This command is useful when optimising the database size or investigating an unexpectedly large database size. -- We can reset the notification threshold and check the remote usage at once with the `Reset notification threshold and check the remote database usage` command. -- Commands are available from the Command Palette, or `Hatch` pane in the settings dialogue. - -### Fixed - -- Now the plug-in resets the remote size notification threshold after rebuild. - -## 0.25.32 - -02nd December, 2025 - -Now I am back from a short (?) break! Thank you all for your patience. (It is nothing major, but the first half of the year has finally come to an end). -Anyway, I will release the things a bit by bit. I think that we need a rehabilitation or getting gears in again. - -### Improved - -- Now the plugin warns when we are in several file-related situations that may cause unexpected behaviour (#300). - - These errors are displayed alongside issues such as file size exceeding limits. - - Such situations include: - - When the document has a name which is not supported by some file systems. - - When the vault has the same file names with different letter cases. - -## 0.25.31 - -18th November, 2025 - -### Fixed - -- Now fetching configuration from the server can handle the empty remote correctly (reported on #756). -- No longer asking to switch adapters during rebuilding. - -# 0.25 - -(0.25.0 through 0.25.30) - -Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) - -After reading Issue #668, I conducted another self-review of the E2EE-related code. In retrospect, it was clearly written by someone inexperienced, which is understandable, but it is still rather embarrassing. Three years is certainly enough time for growth. - -I have now rewritten the E2EE code to be more robust and easier to understand. It is significantly more readable and should be easier to maintain in the future. The performance issue, previously considered a concern, has been addressed by introducing a master key and deriving keys using HKDF. This approach is both fast and robust, and it provides protection against rainbow table attacks. (In addition, this implementation has been [a dedicated package on the npm registry](https://github.com/vrtmrz/octagonal-wheels), and tested in 100% branch-coverage). - -As a result, this is the first time in a while that forward compatibility has been broken. We have also taken the opportunity to change all metadata to use encryption rather than obfuscation. Furthermore, the `Dynamic Iteration Count` setting is now redundant and has been moved to the `Patches` pane in the settings. Thanks to Rabin-Karp, the eden setting is also no longer necessary and has been relocated accordingly. Therefore, v0.25.0 represents a legitimate and correct evolution. - ---- - -## 0.25.30 - -17th November, 2025 - -So sorry for the quick follow-up release, due to a humble mistake in a quick causing a matter. - -### Fixed - -- Now we can save settings correctly again (#756). - -## ~~0.25.28~~ 0.25.29 - -(0.25.28 was skipped due to a packaging issue.) - -17th November, 2025 - -### New feature - -- We can now configure hidden file synchronisation to always overwrite with the latest version (#579). - -### Fixed - -- Timing dependency issues during initialisation have been mitigated (#714) - -### Improved - -- Error logs now contain stack-traces for better inspection. - -## 0.25.27 - -12th November, 2025 - -### Improved - -- Now we can switch the database adapter between IndexedDB and IDB without rebuilding (#747). - - Just a local migration will be required, but faster than a full rebuild. -- No longer checking for the adapter by `Doctor`. - -### Changes - -- The default adapter is reverted to IDB to avoid memory leaks (#747). - -### Fixed (?) - -- Reverted QR code library to v1.4.4 (To make sure #752). - -## 0.25.26 - -07th November, 2025 - -### Improved - -- Some JWT notes have been added to the setting dialogue (#742). - -### Fixed - -- No longer wrong values encoded into the QR code. -- We can acknowledge why the QR codes have not been generated. - - Probably too large a dataset to encode. When this happens, please consider using Setup-URI via text instead of QR code, or reduce the settings temporarily. - -### Refactored - -- Some dependencies have been updated. -- Internal functions have been modularised into `octagonal-wheels` packages and are well tested. - - `dataobject/Computed` for caching computed values. - - `encodeAnyArray/decodeAnyArray` for encoding and decoding any array-like data into compact strings (#729). -- Fixed importing from the parent project in library codes. (#729). - -## 0.25.25 - -06th November, 2025 - -### Fixed - -#### JWT Authentication - -- Now we can use JWT Authentication ES512 correctly (#742). -- Several misdirections in the Setting dialogues have been fixed (i.e., seconds and minutes confusion...). -- The key area in the Setting dialogue has been enlarged and accepts newlines correctly. -- Caching of JWT tokens now works correctly - - Tokens are now cached and reused until they expire. - - They will be kept until 10% of the expiration duration is remaining or 10 seconds, whichever is longer (but at a maximum of 1 minute). -- JWT settings are now correctly displayed on the Setting dialogue. - -And, tips about JWT Authentication on CouchDB have been added to the documentation (docs/tips/jwt-on-couchdb.md). - -#### Other fixes - -- Receiving non-latest revisions no longer causes unexpected overwrites. - - On receiving revisions that made conflicting changes, we are still able to handle them. - -### Improved - -- No longer duplicated message notifications are shown when a connection to the remote server fails. - - Instead, a single notification is shown, and it will be kept on the notification area inside the editor until the situation is resolved. -- The notification area is no longer imposing, distracting, and overwhelming. - - With a pale background, but bordered and with icons. - -## 0.25.24 - -04th November, 2025 - -(Beta release notes have been consolidated to this note). - -### Guidance and UI improvements! - -Since several issues were pointed out, our setup procedure had been quite `system-oriented`. This is not good for users. Therefore, I have changed the procedure to be more `goal-oriented`. I have made extensive use of Svelte, resulting in a very straightforward setup. -While I would like to accelerate documentation and i18n adoption, I do not want to confuse everyone who's already working on it. Therefore, I have decided to release a Beta version at this stage. Significant changes are not expected from this point onward, so I will proceed to stabilise the codebase. (However, this is significant). - -### TURN server support and important notice - -TURN server settings are only necessary if you are behind a strict NAT or firewall that prevents direct P2P -connections. In most cases, you do not need to set up a TURN server. - -Using public TURN servers may have privacy implications, as your data will be relayed through third-party -servers. Even if your data are encrypted, your existence may be known to them. Please ensure you trust the TURN -server provider before using their services. Also your `network administrator` too. You should consider setting -up your own TURN server for your FQDN, if possible. - -### New features - -- We can use the TURN server for P2P connections now. - -### Fixed - -- P2P Replication got more robust and stable. - - Update [Trystero](https://github.com/dmotz/trystero) to the official v0.22.0! - - Fixed a bug that caused P2P connections to drop or (unwanted reconnection to the relay server) unexpectedly in some environments. - - Now, the connection status is more accurately reported. - - While in the background, the connection to the signalling server is now disconnected to save resources. - - When returning to the foreground, it will not reconnect automatically for safety. Please reconnect manually. -- All connection configurations should be edited in each dedicated dialogue now. -- No longer will larger files create chunks during preparing `Reset Synchronisation on This Device`. -- Now hidden file synchronisation respects the filters correctly (#631, #735) - - And `ignore-files` settings are also respected and surely read during the start-up. - -### Behaviour changes - -- The setup wizard is now more `goal-oriented`. Brand-new screens are introduced. -- `Fetch everything` and `Rebuild everything` are now `Reset Synchronisation on This Device` and `Overwrite Server Data with This Device's Files`. -- Remote configuration and E2EE settings are now separated into each modal dialogue. - - Remote configuration is now more straightforward. And if we need the rebuild (No... `Overwrite Server Data with This Device's Files`), it is now clearly indicated. -- Peer-to-Peer settings are also separated into their own modal dialogue (still in progress, and we need to open a P2P pane, still). -- Setup-URI, and Report for the Issue are now not copied to the clipboard automatically. Instead, there are copy-dialogue and buttons to copy them explicitly. - - This is to avoid confusion for users who do not want to use these features. -- No longer optional features are introduced during the setup, or `Reset Synchronisation on This Device`, `Overwrite Server Data with This Device's Files`. - - This is to avoid confusion for users who do not want to use these features. Instead, we will be informed that optional features are available after the setup is completed. -- We cannot perform `Fetch everything` and `Rebuild everything` (Removed, so the old name) without restarting Obsidian now. - -### Miscellaneous - -- Setup QR Code generation is separated into a src/lib/src/API/processSetting.ts file. Please use it as a subrepository if you want to generate QR codes in your own application. -- Setup-URI is also separated into a src/lib/src/API/processSetting.ts -- Some direct access to web APIs is now wrapped into the services layer. - -### Dependency updates - -- Many dependencies are updated. Please see `package.json`. - - This is the hardest part of this update. I read most of the changes in the dependencies. If you find any extra information, please let me know. -- As upgrading TypeScript, Fixed many UInt8Array and Uint8Array type mismatches. -- - -### Breaking changes - -- Sending configuration via Peer-to-Peer connection is not compatible with older versions. - - Please upgrade all devices to v0.25.24.beta1 or later to use this feature again. - - This is due to security improvements in the encryption scheme. - -## 0.25.23 - -26th October, 2025 - -The next version we are preparing (you know that as 0.25.23.beta1) is now still on beta, resulting in this rather unfortunate versioning situation. Apologies for the confusion. The next v0.25.23.beta2 will be v0.25.24.beta1. In other words, this is a v0.25.22.patch-1 actually, but possibly not allowed by Obsidian's rule. -(Perhaps we ought to declare 1.0.0 with a little more confidence. The current minor part has been effectively a major one for a long time. If it were 1.22.1 and 1.23.0.beta1, no confusion ). - -### Fixed - -- We are now able to enable optional features correctly again (#732). -- No longer oversized files have been processed, furthermore. - - - Before creating a chunk, the file is verified as the target. - - The behaviour upon receiving replication has been changed as follows: - - If the remote file is oversized, it is ignored. - - If not, but while the local file is oversized, it is also ignored. - -- We are now able to enable optional features correctly again (#732). -- No longer oversized files have been processed, furthermore. - - Before creating a chunk, the file is verified as the target. - - The behaviour upon receiving replication has been changed as follows: - - If the remote file is oversized, it is ignored. - - If not, but while the local file is oversized, it is also ignored. - -## 0.25.22 - -15th October, 2025 - -### Fixed - -- Fixed a bug that caused wrong event bindings and flag inversion (#727) - - This caused following issues: - - In some cases, settings changes were not applied or saved correctly. - - Automatic synchronisation did not begin correctly. - -### Improved - -- Too large diffs are not shown in the file comparison view, due to performance reasons. - -### Notes - -- The checking algorithm implemented in 0.25.20 is also raised as PR (#237). And completely I merged it manually. - - Sorry for lacking merging this PR, and let me say thanks to the great contribution, @bioluks ! -- Known issues: - - Sync on Editor save seems not to work correctly in some cases. - - I am investigating this issue. If you have any information, please let me know. - -## 0.25.21 - -13th October, 2025 - -This release including 0.25.21.beta1 and 0.25.21.beta2. - -Apologies for taking a little time. I was seriously tackling this. -(Of course, being caught up in an unfamiliar structure due to personnel changes on my workplace played a part, but fortunately I have returned to a place where I can do research and development rather than production. Completely beside the point, though). -Now then, this time, moving away from 'convention over configuration', I have changed to a mechanism for manually binding events. This makes it much easier to leverage IDE assistance. -And, also, we are ready to separate `Features` and `APIs` from `Module`. Features are still in the module, but APIs will be moved to a Service layer. This will make it easier to maintain and extend the codebase in the future. - -If you have found any issues, please let me know. I am now on the following: - -- GitHub [Issues](https://github.com/vrtmrz/obsidian-livesync/issues) Excellent! May the other contributors will help you too. -- Twitter [@vorotamoroz](https://twitter.com/vorotamoroz) Quickest! -- Matrix [@vrtmrz:matrix.org](https://matrix.to/#/@vrtmrz:matrix.org) Also quick, and if you need to keep it private! - I am creating rooms too, but I'm struggling to figure out how to use them effectively because I cannot tell the difference of use-case between them and discussions. However, if you want to use Discord, this is a answer; We should on E2E encrypted platform. - -## 0.25.21.beta2 - -8th October, 2025 - -### Fixed - -- Fixed wrong event type bindings (which caused some events not to be handled correctly). -- Fixed detected a timing issue in StorageEventManager - - When multiple events for the same file are fired in quick succession, metadata has been kept older information. This induces unexpected wrong notifications and write prevention. - -## 0.25.21.beta1 - -6th October, 2025 - -### Refactored - -- Event handling now does not rely on 'convention over configuration'. - - Services.ts now have a proper event handler registration system. - -## 0.25.20 - -26th September, 2025 - -### Fixed - -- Chunk fetching no longer reports errors when the fetched chunk could not be saved (#710). - - Just using the fetched chunk temporarily. -- Chunk fetching reports errors when the fetched chunk is surely corrupted (#710, #712). -- It no longer detects files that the plug-in has modified. - - It may reduce unnecessary file comparisons and unexpected file states. - -### Improved - -- Now checking the remote database configuration respecting the CouchDB version (#714). - -## 0.25.19 - -18th September, 2025 - -### Improved - -- Now encoding/decoding for chunk data and encryption/decryption are performed in native functions (if they were available). - - This uses Uint8Array.fromBase64 and Uint8Array.toBase64, which are natively available in iOS 18.2+ and Android with Chrome 140+. - - In Android, WebView is by default updated with Chrome, so it should be available in most cases. - - Note that this is not available in Desktop yet (due to being based on Electron). We are staying tuned for future updates. - - This realised by an external(?) package [octagonal-wheels](https://github.com/vrtmrz/octagonal-wheels). Therefore, this update only updates the dependency. - -## 0.25.18 - -17th September, 2025 - -### Fixed - -- Property encryption detection now works correctly (On Self-hosted LiveSync, it was not broken, but as a library, it was not working correctly). -- Initialising the chunk splitter is now surely performed. -- DirectFileManipulator now works fine (as a library) - - Old `DirectFileManipulatorV1` is now removed. - -### Refactored - -- Removed some unnecessary intermediate files. - -## 0.25.17 - -16th September, 2025 - -### Fixed - -- No longer information-level logs have produced during toggling `Show only notifications` in the settings (#708). -- Ignoring filters for Hidden file sync now works correctly (#709). - -### Refactored - -- Removed some unnecessary intermediate files. - -## 0.25.16 - -4th September, 2025 - -### Improved - -- Improved connectivity for P2P connections -- The connection to the signalling server can now be disconnected while in the background or when explicitly disconnected. - - These features use a patch that has not been incorporated upstream. - - This patch is available at [vrtmrz/trystero](https://github.com/vrtmrz/trystero). - -## 0.25.15 - -3rd September, 2025 - -### Improved - -- Now we can configure `forcePathStyle` for bucket synchronisation (#707). - -## 0.25.14 - -2nd September, 2025 - -### Fixed - -- Opening IndexedDB handling has been ensured. -- Migration check of corrupted files detection has been fixed. - - Now informs us about conflicted files as non-recoverable, but noted so. - - No longer errors on not-found files. - -## 0.25.13 - -1st September, 2025 - -### Fixed - -- Conflict resolving dialogue now properly displays the changeset name instead of A or B (#691). - -## 0.25.12 - -29th August, 2025 - -### Fixed - -- Fixed an issue with automatic synchronisation starting (#702). - -## 0.25.11 - -28th August, 2025 - -### Fixed - -- Automatic translation detection on the first launch now works correctly (#630). -- No errors are shown during synchronisations in offline (if not explicitly requested) (#699). -- Missing some checking during automatic-synchronisation now works correctly. - -## 0.25.10 - -26th August, 2025 - -### New experimental feature - -- We can perform Garbage Collection (Beta2) without rebuilding the entire database, and also fetch the database. - - Note that this feature is very experimental and should be used with caution. - - This feature requires disabling `Fetch chunks on demand`. - -### Fixed - -- Resetting the bucket now properly clears all uploaded files. - -### Refactored - -- Some files have been moved to better reflect their purpose and improve maintainability. -- The extensive LiveSyncLocalDB has been split into separate files for each role. - -### Fixed - -- Unexpected `Failed to obtain PBKDF2 salt` or similar errors during bucket-synchronisation no longer occur. -- Unexpected long delays for chunk-missing documents when using bucket-synchronisation have been resolved. -- Fetched remote chunks are now properly stored in the local database if `Fetch chunks on demand` is enabled. -- The 'fetch' dialogue's message has been refined. -- No longer overwriting any corrupted documents to the storage on boot-sequence. - -### Refactored - -- Type errors have been corrected. - -## 0.25.9 - -20th August, 2025 - -### Fixed - -- CORS Checking messages now use replacements. -- Configuring CORS setting via the UI now respects the existing rules. -- Now startup-checking works correctly again, performs migration check serially and then it will also fix starting LiveSync or start-up sync. (#696) -- Statusline in editor now supported 'Bases'. - -## 0.25.8 - -18th August, 2025 - -### New feature - -- Insecure chunk detection has been implemented. - - A notification dialogue will be shown if any insecure chunks are detected; these may have been created by v0.25.6 due to its issue. If this dialogue appears, please ensure you rebuild the database after backing it up. - -## 0.25.7 - -15th August, 2025 - -**Since the release of 0.25.6, there are two large problem. Please update immediately.** - -- We may have corrupted some documents during the migration process. **Please check your documents on the wizard.** -- Due to a chunk ID assignment issue, some data has not been encrypted. **Please rebuild the database using Rebuild Everything** if you have enabled E2EE. - -**_So, If you have enabled E2EE, please perform `Rebuild everything`. If not, please check your documents on the wizard._** - -In next version, insecure chunk detection will be implemented. - -### Fixed - -- Off-loaded chunking have been fixed to ensure proper functionality (#693). -- Chunk document ID assignment has been fixed. -- Replication prevention message during version up detection has been improved (#686). -- `Keep A` and `Keep B` on Conflict resolving dialogue has been renamed to `Use Base` and `Use Conflicted` (#691). - -### Improved - -- Metadata and content-size unmatched documents are now detected and reported, prevented to be applied to the storage. - - This behaviour can be configured in `Patch` -> `Edge case addressing (Behaviour)` -> `Process files even if seems to be corrupted` - - Note: this toggle is for the direct-database-manipulation users. - -### New Features - -- `Scan for Broken files` has been implemented on `Hatch` -> `TroubleShooting`. - -### Refactored - -- Off-loaded processes have been refactored for the better maintainability. - - Files prefixed `bg.worker` are now work on the worker threads. - - Files prefixed `bgWorker.` are now also controls these worker threads. (I know what you want to say... I will rename them). -- Removed unused code. - -## ~~0.25.5~~ 0.25.6 - -(0.25.5 has been withdrawn due to a bug in the `Fetch chunks on demand` feature). - -9th August, 2025 - -### Fixed - -- Storage scanning no longer occurs when `Suspend file watching` is enabled (including boot-sequence). - - This change improves safety when troubleshooting or fetching the remote database. -- `Fetch chunks on demand` is now working again (if you installed 0.25.5, other versions are not affected). - -### Improved - -- Saving notes and files now consumes less memory. - - Data is no longer fully buffered in memory and written at once; instead, it is now written in each over-2MB increments. -- Chunk caching is now more efficient. - - Chunks are now managed solely by their count (still maintained as LRU). If memory usage becomes excessive, they will be automatically released by the system-runtime. - - Reverse-indexing is also no longer used. It is performed as scanning caches and act also as a WeakRef thinning. -- Both of them (may) are effective for #692, #680, and some more. - -### Changed - -- `Incubate Chunks in Document` (also known as `Eden`) is now fully sunset. - - Existing chunks can still be read, but new ones will no longer be created. -- The `Compute revisions for chunks` setting has also been removed. - - This feature is now always enabled and is no longer configurable (restoring the original behaviour). -- As mentioned, `Memory cache size (by total characters)` has been removed. - - The `Memory cache size (by total items)` setting is now the only option available (but it has 10x ratio compared to the previous version). - -### Refactored - -- A significant refactoring of the core codebase is underway. - - This is part of our ongoing efforts to improve code maintainability, readability, and to unify interfaces. - - Previously, complex files posed a risk due to a low bus factor. Fortunately, as our devices have become faster and more capable, we can now write code that is clearer and more maintainable (And not so much costs on performance). - - Hashing functions have been refactored into the `HashManager` class and its derived classes. - - Chunk splitting functions have been refactored into the `ContentSplitterCore` class and its derived classes. - - Change tracking functions have been refactored into the `ChangeManager` class. - - Chunk read/write functions have been refactored into the `ChunkManager` class. - - Fetching chunks on demand is now handled separately from the `ChunkManager` and chunk reading functions. Chunks are queued by the `ChunkManager` and then processed by the `ChunkFetcher`, simplifying the process and reducing unnecessary complexity. - - Then, local database access via `LiveSyncLocalDB` has been refactored to use the new classes. -- References to external sources from `commonlib` have been corrected. -- Type definitions in `types.ts` have been refined. -- Unit tests are being added incrementally. - - I am using `Deno` for testing, to simplify testing and coverage reporting. - - While this is not identical to the Obsidian environment, `jest` may also have limitations. It is certainly better than having no tests. - - In other words, recent manual scenario testing has highlighted some shortcomings. - - `pouchdb-test`, used for testing PouchDB with Deno, has been added, utilising the `memory` adapter. - -Side note: Although class-oriented programming is sometimes considered an outdated style, However, I have come to re-evaluate it as valuable from the perspectives of maintainability and readability. - -## 0.25.4 - -29th July, 2025 - -### Fixed - -- The PBKDF2Salt is no longer corrupted when attempting replication while the device is offline. (#686) - - If this issue has already occurred, please use `Maintenance` -> `Rebuilding Operations (Remote Only)` -> `Overwrite Remote` and `Send` to resolve it. - - Please perform this operation on the device that is most reliable. - - I am so sorry for the inconvenience; there are no patching workarounds. The rebuilding operation is the only solution. - - This issue only affects the encryption of the remote database and does not impact the local databases on any devices. - - (Preventing synchronisation is by design and expected behaviour, even if it is sometimes inconvenient. This is also why we should avoid using workarounds; it is, admittedly, an excuse). - - In any case, we can unlock the remote from the warning dialogue on receiving devices. We are performing replication, instead of simple synchronisation at the expense of a little complexity (I would love to express thank you again for your every effort to manage and maintain the settings! Your all understanding saves our notes). - - This process may require considerable time and bandwidth (as usual), so please wait patiently and ensure a stable network connection. - -### Side note - -The PBKDF2Salt will be referred to as the `Security Seed`, and it is used to derive the encryption key for replication. Therefore, it should be stored on the server prior to synchronisation. We apologise for the lack of explanation in previous updates! - -## 0.25.3 - -22nd July, 2025 - -### Fixed - -- Now the `Doctor` at migration will save the configuration. - -## 0.25.2 ~~0.25.1~~ - -(0.25.1 was missed due to a mistake in the versioning process). -19th July, 2025 - -### Refined and New Features - -- Fetching the remote database on `RedFlag` now also retrieves remote configurations optionally. - - This is beneficial if we have already set up another device and wish to use the same configuration. We will see a much less frequent `Unmatched` dialogue. -- The setup wizard using Set-up URI and QR code has been improved. - - The message is now more user-friendly. - - The obsolete method (manual setting application) has been removed. - - The `Cancel` button has been added to the setup wizard. - - We can now fetch the remote configuration from the server if it exists, which is useful for adding new devices. - - Mostly same as a `RedFlag` fetching remote configuration. - - We can also use the `Doctor` to check and fix the imported (and fetched) configuration before applying it. - -### Changes - -- The Set-up URI is now encrypted with a new encryption algorithm (mostly the same as `V2`). - - The new Set-up URI is not compatible with version 0.24.x or earlier. - -## 0.25.0 - -### Fixed - -- The encryption algorithm now uses HKDF with a master key. - - This is more robust and faster than the previous implementation. - - It is now more secure against rainbow table attacks. - - The previous implementation can still be used via `Patches` -> `End-to-end encryption algorithm` -> `Force V1`. - - Note that `V1: Legacy` can decrypt V2, but produces V1 output. -- `Fetch everything from the remote` now works correctly. - - It no longer creates local database entries before synchronisation. -- Extra log messages during QR code decoding have been removed. - -### Changed - -- The following settings have been moved to the `Patches` pane: - - `Remote Database Tweak` - - `Incubate Chunks in Document` - - `Data Compression` - -### Behavioural and API Changes - -- `DirectFileManipulatorV2` now requires new settings (as you may already know, E2EEAlgorithm). -- The database version has been increased to `12` from `10`. - - If an older version is detected, we will be notified and synchronisation will be paused until the update is acknowledged. (It has been a long time since this behaviour was last encountered; we always err on the side of caution, even if it is less convenient.) - -### Refactored - -- `couchdb_utils.ts` has been separated into several explicitly named files. -- Some missing functions in `bgWorker.mock.ts` have been added. - -## 0.24.0 - -I know that we have been waiting for a long time. It is finally released! - -Over the past three years since the inception of the plugin, various features have been implemented to address diverse user needs. This is truly honourable, and I am grateful for your years of support. However, this process has led to an increasingly disorganised codebase, with features becoming entangled. Consequently, this has led to a situation where bugs can go unnoticed and resolving one issue may inadvertently introduce another. - -In 0.24.0, I reorganised the previously jumbled main codebase into clearly defined modules. Although I had assumed that the total size of the code would not increase, I discovered that it has in fact increased. While the complexity is still considerable, the refactoring has improved the clarity of the code's structure. Additionally, while testing the release candidates, we still found many bugs to fix, which helped to make this plug-in robust and stable. Therefore, we are now ready to use the updated plug-in, and in addition to that, proceed to the next step. - -This is also the first step towards a fully-fledged-fancy LiveSync, not just a plug-in from Obsidian. Of course, it will still be a plug-in primarily and foremost, but this development marks a significant step towards the self-hosting concept. - -Finally, I would like to once again express my respect and gratitude to all of you. My gratitude extends to all of the dev testers! Your contributions have certainly made the plug-in robust and stable! - -Thank you, and I hope your troubles will be resolved! - ---- - -## 0.24.31 - -10th July, 2025 - -### Fixed - -- The description of `Enable Developers' Debug Tools.` has been refined. - - Now performance impact is more clearly stated. -- Automatic conflict checking and resolution has been improved. - - It now works parallelly for each other file, instead of sequentially. It makes significantly faster on first synchronisation when with local files information. -- Resolving conflicts dialogue will not be shown for the multiple files at once. - - It will be shown for each file, one by one. - -## 0.24.30 - -9th July, 2025 - -### New Feature - -- New chunking algorithm `V3: Fine deduplication` has been added, and will be recommended after updates. - - The Rabin-Karp algorithm is used for efficient chunking. - - This will be the default in the new installations. - - It is more robust and faster than the previous one. - - We can change it in the `Advanced` pane of the settings. -- New language `ko` (Korean) has been added. - - Thank you for your contribution, [@ellixspace](https://x.com/ellixspace)! - - Any contributions are welcome, from any route. Please let me know if I seem to be unaware of this. It is often the case that I am not really aware of it. -- Chinese (Simplified) translation has been updated. - - Thank you for your contribution, [@52sanmao](https://github.com/52sanmao)! - -### Fixed - -- Numeric settings are now never lost the focus during value changing. -- Doctor now redacts more sensitive information on error reports. - -### Improved - -- All translations have been rewritten into YAML format, to easier to manage and contribute. - - We can write them with comments, newlines, and other YAML features. -- Doctor recommendations are now shown in a user-friendly notation. - - We can now see the recommended as `V3: Fine deduplication` instead of `v3-rabin-karp`. - -### Refactored - -- Never-ending `ObsidianLiveSyncSettingTab.ts` has finally been separated into each pane's file. -- Some commented-out code has been removed. - -### Acknowledgement - -- Jun Murakami, Shun Ishiguro, and Yoshihiro Oyama. 2012. Implementation and Evaluation of a Cache Deduplication Mechanism with Content-Defined Chunking. In _IPSJ SIG Technical Report_, Vol.2012-ARC-202, No.4. Information Processing Society of Japan, 1-7. - -## 0.24.29 - -20th June, 2025 - -### Fixed - -- Synchronisation with buckets now works correctly, regardless of whether a prefix is set or the bucket has been (re-) initialised (#664). -- An information message is now displayed again, during any automatic synchronisation is enabled (#662). - -### Tidied up - -- Importing paths have been tidied up. - -## 0.24.28 - -15th June, 2025 - -### Fixed - -- Batch Update is no longer available in LiveSync mode to avoid unexpected behaviour. (#653) -- Now compatible with Cloudflare R2 again for bucket synchronisation. - - @edo-bari-ikutsu, thank you for [your contribution](https://github.com/vrtmrz/livesync-commonlib/pull/12)! -- Prevention of broken behaviour due to database connection failures added (#649). - -## 0.24.27 - -10th June, 2025 - -### Improved - -- We can use prefix for path for the Bucket synchronisation. - - For example, if you set the `vaultName/` as a prefix for the bucket in the root directory, all data will be transferred to the bucket under the `vaultName/` directory. -- The "Use Request API to avoid `inevitable` CORS problem" option is now promoted to the normal setting, not a niche patch. - -### Fixed - -- Now switching replicators applied immediately, without the need to restart Obsidian. - -### Tidied up - -- Some dependencies have been updated to the latest version. - -## 0.24.26 - -14th May, 2025 - -This update introduces an option to circumvent Cross-Origin Resource Sharing -(CORS) constraints for CouchDB requests, by leveraging Obsidian's native request -API. The implementation of such a feature had previously been deferred due to -significant security considerations. - -CORS is a vital security mechanism, enabling servers like CouchDB -- which -functions as a sophisticated REST API -- to control access from different -origins, thereby ensuring secure communication across trust boundaries. I had -long hesitated to offer a CORS circumvention method, as it deviates from -security best practices; My preference was for users to configure CORS correctly -on the server-side. - -However, this policy has shifted due to specific reports of intractable -CORS-related configuration issues, particularly within enterprise proxy -environments where proxy servers can unpredictably alter or block -communications. Given that a primary objective of the "Self-hosted LiveSync" -plugin is to facilitate secure Obsidian usage within stringent corporate -settings, addressing these 'unavoidable' user-reported problems became -essential. Mostly raison d'être of this plugin. - -Consequently, the option "Use Request API to avoid `inevitable` CORS problem" -has been implemented. Users are strongly advised to enable this _only_ when -operating within a trusted environment. We can enable this option in the `Patch` pane. - -However, just to whisper, this is tremendously fast. - -### New Features - -- Automatic display-language changing according to the Obsidian language - setting. - - We will be asked on the migration or first startup. - - **Note: Please revert to the default language if you report any issues.** - - Not all messages are translated yet. We welcome your contribution! -- Now we can limit files to be synchronised even in the hidden files. -- "Use Request API to avoid `inevitable` CORS problem" has been implemented. - - Less secure, please use it only if you are sure that you are in the trusted - environment and be able to ignore the CORS. No `Web viewer` or similar tools - are recommended. (To avoid the origin forged attack). If you are able to - configure the server setting, always that is recommended. -- `Show status icon instead of file warnings banner` has been implemented. - - If enabled, the ⛔ icon will be shown inside the status instead of the file - warnings banner. No details will be shown. - -### Improved - -- All regular expressions can be inverted by prefixing `!!` now. - -### Fixed - -- No longer unexpected files will be gathered during hidden file sync. -- No longer broken `\n` and new-line characters during the bucket - synchronisation. -- We can purge the remote bucket again if we using MinIO instead of AWS S3 or - Cloudflare R2. -- Purging the remote bucket is now more reliable. - - 100 files are purged at a time. -- Some wrong messages have been fixed. - -### Behaviour changed - -- Entering into the deeper directories to gather the hidden files is now limited - by `/` or `\/` prefixed ignore filters. (It means that directories are scanned - deeper than before). - - However, inside the these directories, the files are still limited by the - ignore filters. - -### Etcetera - -- Some code has been tidied up. -- Trying less warning-suppressing and be more safer-coding. -- Dependent libraries have been updated to the latest version. -- Some build processes have been separated to `pre` and `post` processes. - -## 0.24.25 - -22nd April, 2025 - -### Improved - -- Peer-to-peer synchronisation has been got more robust. - -### Fixed - -- No longer broken falsy values in settings during set-up by the QR code - generation. - -### Refactored - -- Some `window` references now have pointed to `globalThis`. -- Some sloppy-import has been fixed. -- A server side implementation `Synchromesh` has been suffixed with `deno` - instead of `server` now. - -## 0.24.24 - -15th April, 2025 - -### Fixed - -- No longer broken JSON files including `\n`, during the bucket synchronisation. - (#623) -- Custom headers and JWT tokens are now correctly sent to the server during - configuration checking. (#624) - -### Improved - -- Bucket synchronisation has been enhanced for better performance and - reliability. - - Now less duplicated chunks are sent to the server. Note: If you have - encountered about too less chunks, please let me know. However, you can send - it to the server by `Overwrite remote`. - - Fetching conflicted files from the server is now more reliable. - - Dependent libraries have been updated to the latest version. - - Also, let me know if you have encountered any issues with this update. - Especially you are using a device that has been in use for a little - longer. - -## 0.24.23 - -10th April, 2025 - -### New Feature - -- Now, we can send custom headers to the server. - - They can be sent to either CouchDB or Object Storage. -- Authentication with JWT in CouchDB is now supported. - - I will describe steps later, but please refer to the - [CouchDB document](https://docs.couchdb.org/en/stable/config/auth.html#authentication-configuration). - - A JWT keypair for testing can be generated in the setting dialogue. - -### Improved - -- The QR Code for set-up can be shown also from the setting dialogue now. -- Conflict checking for preventing unexpected overwriting on the boot-up process - has been quite faster. - -### Fixed - -- Some bugs on Dev and Testing modules have been fixed. - -## 0.24.22 ~~0.24.21~~ - -1st April, 2025 - -(Really sorry for the confusion. I have got a miss at releasing...). - -### Fixed - -- No longer conflicted files are handled in the boot-up process. No more - unexpected overwriting. - - It ignores `Always overwrite with a newer file`, and always be prevented for - the safety. Please pick it manually or open the file. -- Some log messages on conflict resolution has been corrected. -- Automatic merge notifications, displayed on the grounds of `same`, have been - degraded to logs. - -### Improved - -- Now we can fetch the remote database with keeping local files completely - intact. - - In new option, all files are stored into the local database before the - fetching, and will be merged automatically or detected as conflicts. -- The dialogue presenting options when performing `Fetch` are now more - informative. - -### Refactored - -- Some class methods have been fixed its arguments to be more consistent. -- Types have been defined for some conditional results. - -## 0.24.20 - -24th March, 2025 - -### Improved - -- Now we can see the detail of `TypeError` using Obsidian API during remote - database access. - -### Behaviour and default changed - -- **NOW INDEED AND ACTUALLY** `Compute revisions for chunks` are backed into - enabled again. it is necessary for garbage collection of chunks. - - As far as existing users are concerned, this will not automatically change, - but the Doctor will inform us. - -## 0.24.19 - -5th March, 2025 - -### New Feature - -- Now we can generate a QR Code for transferring the configuration to another device. - - This QR Code can be scanned by the camera app or something QR Code Reader of another device, and via Obsidian URL, the configuration will be transferred. - - Note: This QR Code is not encrypted. So, please be careful when transferring the configuration. - -## 0.24.18 - -28th February, 2025 - -### Fixed - -- Now no chunk creation errors will be raised after switching `Compute revisions for chunks`. -- Some invisible file can be handled correctly (e.g., `writing-goals-history.csv`). -- Fetching configuration from the server is now saves the configuration immediately (if we are not in the wizard). - -### Improved - -- Mismatched configuration dialogue is now more informative, and rewritten to more user-friendly. -- Applying configuration mismatch is now without rebuilding (at our own risks). -- Now, rebuilding is decided more fine grained. - -### Improved internally - -- Translations can be nested. i.e., task:`Some procedure`, check: `%{task} checking`, checkfailed: `%{check} failed` produces `Some procedure checking failed`. - - Max to 10 levels of nesting - -## 0.24.17 - -27th February, 2025 - -Confession. I got the default values wrong. So scary and sorry. - -## 0.24.16 - -### Improved - -#### Peer-to-Peer - -- Now peer-to-peer synchronisation checks the settings are compatible with each other. - - No longer unexpected database broken, phew. -- Peer-to-peer synchronisation now handles the platform and detects pseudo-clients. - - Pseudo clients will not decrypt/encrypt anything, just relay the data. Hence, always settings are not compatible. Therefore, we have to accept the incompatibility for pseudo clients. - -#### General - -- New migration method has been implemented, that called `Doctor`. - - - `Doctor` checks the difference between the ideal and actual values and encourages corrective action. To facilitate our decision, the reasons for this and the recommendations are also presented. - - This can be used not only during migration. We can invoke the doctor from the settings for trouble-shooting. - -- The minimum interval for replication to be caused when an event occurs can now be configurable. -- Some detail note has been added and change nuance about the `Report` in the setting dialogue, which had less informative. - -### Behaviour and default changed - -- `Compute revisions for chunks` are backed into enabled again. it is necessary for garbage collection of chunks. - - As far as existing users are concerned, this will not automatically change, but the Doctor will inform us. - -### Refactored - -- Platform specific codes are more separated. No longer `node` modules were used in the browser and Obsidian. - -## 0.24.15 - -### Fixed - -- Now, even without WeakRef, Polyfill is used and the whole thing works without error. However, if you can switch WebView Engine, it is recommended to switch to a WebView Engine that supports WeakRef. - -## 0.24.14 - -### Fixed - -- Resolving conflicts of JSON files (and sensibly merging them) is now working fine, again! - - And, failure logs are more informative. -- More robust to release the event listeners on unwatching the local database. - -### Refactored - -- JSON file conflict resolution dialogue has been rewritten into svelte v5. -- Upgrade eslint. -- Remove unnecessary pragma comments for eslint. - -## 0.24.13 - -Sorry for the lack of replies. The ones that were not good are popping up, so I am just going to go ahead and get this one... However, they realised that refactoring and restructuring is about clarifying the problem. Your patience and understanding is much appreciated. - -### Fixed - -#### General Replication - -- No longer unexpected errors occur when the replication is stopped during for some reason (e.g., network disconnection). - -#### Peer-to-Peer Synchronisation - -- Set-up process will not receive data from unexpected sources. -- No longer resource leaks while enabling the `broadcasting changes` -- Logs are less verbose. -- Received data is now correctly dispatched to other devices. -- `Timeout` error now more informative. -- No longer timeout error occurs for reporting the progress to other devices. -- Decision dialogues for the same thing are not shown multiply at the same time anymore. -- Disconnection of the peer-to-peer synchronisation is now more robust and less error-prone. - -#### Webpeer - -- Now we can toggle Peers' configuration. - -### Refactored - -- Cross-platform compatibility layer has been improved. -- Common events are moved to the common library. -- Displaying replication status of the peer-to-peer synchronisation is separated from the main-log-logic. -- Some file names have been changed to be more consistent. - -## 0.24.12 - -I created a SPA called [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) (well, right... I will think of a name again), which replaces the server when using Peer-to-Peer synchronisation. This is a pseudo-client that appears to other devices as if it were one of the clients. . As with the client, it receives and sends data without storing it as a file. -And, this is just a single web page, without any server-side code. It is a static web page that can be hosted on any static web server, such as GitHub Pages, Netlify, or Vercel. All you have to do is to open the page and enter several items, and leave it open. - -### Fixed - -- No longer unnecessary acknowledgements are sent when starting peer-to-peer synchronisation. - -### Refactored - -- Platform impedance-matching-layer has been improved. - - And you can see the actual usage of this on [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) that a pseudo client for peer-to-peer synchronisation. -- Some UIs have been got isomorphic among Obsidian and web applications (for `webpeer`). - -## 0.24.11 - -Peer-to-peer synchronisation has been implemented! - -Until now, I have not provided a synchronisation server. More people may not -even know that I have shut down the test server. I confess that this is a bit -repetitive, but I confess it is a cautionary tale. This is out of a sense of -self-discipline that someone has occurred who could see your data. Even if the -'someone' is me. I should not be unaware of its superiority, even though -well-meaning and am a servant of all. (Half joking, but also serious). However, -now I can provide you with a signalling server. Because, to the best of my -knowledge, it is only the network that is connected to your device. Also, this -signalling server is just a Nostr relay, not my implementation. You can run your -implementation, which you consider trustworthy, on a trustworthy server. You do -not even have to trust me. Mate, it is great, isn't it? For your information, -strfry is running on my signalling server. - -Nevertheless, that being said, to be more honest, I still have not decided what -to do with this signalling server if too much traffic comes in. - -Note: Already you have noticed this, but let me mention it again, this is a -significantly large update. If you have noticed anything, please let me know. I -will try to fix it as soon as possible (Some address is on my -[profile](https://github.com/vrtmrz)). - -### Improved - -- New Translation: `es` (Spanish) by @zeedif (Thank you so much)! -- Now all of messages can be selectable and copyable, also on the iPhone, iPad, and Android devices. Now we can copy or share the messages easily. - -### New Feature - -- Peer-to-Peer Synchronisation has been implemented! - - This feature is still in early beta, and it is recommended to use it with caution. - - However, it is a significant step towards the self-hosting concept. It is now possible to synchronise your data without using any remote database or storage. It is a direct connection between your devices. - - Note: We should keep the device online to synchronise the data. It is not a background synchronisation. Also it needs a signalling server to establish the connection. But, the signalling server is used only for establishing the connection, and it does not store any data. - -### Fixed - -- No longer memory or resource leaks when the plug-in is disabled. -- Now deleted chunks are correctly detected on conflict resolution, and we are guided to resurrect them. -- Hanging issue during the initial synchronisation has been fixed. -- Some unnecessary logs have been removed. -- Now all modal dialogues are correctly closed when the plug-in is disabled. - -### Refactor - -- Several interfaces have been moved to the separated library. -- Translations have been moved to each language file, and during the build, they are merged into one file. -- Non-mobile friendly code has been removed and replaced with the safer code. - - (Now a days, mostly server-side engine can use webcrypto, so it will be rewritten in the future more). -- Started writing Platform impedance-matching-layer. -- Svelte has been updated to v5. -- Some function have got more robust type definitions. -- Terser optimisation has slightly improved. -- During the build, analysis meta-file of the bundled codes will be generated. - -## 0.24.10 - -### Fixed - -- Fixed the issue which the filename is shown as `undefined`. -- Fixed the issue where files transferred at short intervals were not reflected. - -### Improved - -- Add more translations: `ja-JP` (Japanese) by @kohki-shikata (Thank you so much)! - -### Internal - -- Some files have been prettified. - -## 0.24.9 - -Skipped. - -## 0.24.8 - -### Fixed - -- Some parallel-processing tasks are now performed more safely. -- Some error messages has been fixed. - -### Improved - -- Synchronisation is now more efficient and faster. -- Saving chunks is a bit more robust. - -### New Feature - -- We can remove orphaned chunks again, now! - - Without rebuilding the database! - - Note: Please synchronise devices completely before removing orphaned chunks. - - Note2: Deleted files are using chunks, if you want to remove them, please commit the deletion first. (`Commit File Deletion`) - - Note3: If you lost some chunks, do not worry. They will be resurrected if not so much time has passed. Try `Resurrect deleted chunks`. - - Note4: This feature is still beta. Please report any issues you encounter. - - Note5: Please disable `On demand chunk fetching`, and enable `Compute revisions for each chunk` before using this feature. - - These settings is going to be default in the future. - -## 0.24.7 - -### Fixed (Security) - -- Assigning IDs to chunks has been corrected for more safety. - - Before version 0.24.6, there were possibilities in End-to-End encryption where a brute-force attack could be carried out against an E2EE passphrase via a chunk ID if a zero-byte file was present. Now the chunk ID should be assigned more safely, and not all of passphrases are used for generating the chunk ID. - - This is a security fix, and it is recommended to update and rebuild database to this version as soon as possible. - - Note: It keeps the compatibility with the previous versions, but the chunk ID will be changed for the new files and modified files. Hence, deduplication will not work for the files which are modified after the update. It is recommended to rebuild the database to avoid the potential issues, and reduce the database size. - - Note2: This fix is only for with E2EE. Plain synchronisation is not affected by this issue. - -### Fixed - -- Now the conflict resolving dialogue is automatically closed after the conflict has been resolved (and transferred from other devices; or written by some other resolution). -- Resolving conflicts by timestamp is now working correctly. - - It also fixes customisation sync. - -### Improved - -- Notifications can be suppressed for the hidden files update now. -- No longer uses the old-xxhash and sha1 for generating the chunk ID. Chunk ID is now generated with the new algorithm (Pure JavaScript hash implementation; which is using Murmur3Hash and FNV-1a now used). - -## 0.24.6 - -### Fixed (Quick Fix) - -- Fixed the issue of log is not displayed on the log pane if the pane has not been shown on startup. - - This release is only for it. However, fixing this had been necessary to report any other issues. - -## 0.24.5 - -### Fixed - -- Fixed incorrect behaviour when comparing objects with undefined as a property value. - -### Improved - -- The status line and the log summary are now displayed more smoothly and efficiently. - - This improvement has also been applied to the logs displayed in the log pane. - -## 0.24.4 - -### Fixed - -- Fixed so many inefficient and buggy modules inherited from the past. - -### Improved - -- Tasks are now executed in an efficient asynchronous library. -- On-demand chunk fetching is now more efficient and keeps the interval between requests. - - This will reduce the load on the server and the network. - - And, safe for the Cloudant. - -## 0.24.3 - -### Improved - -- Many messages have been improved for better understanding as thanks to the fine works of @Volkor3-16! Thank you so much! -- Documentations also have been updated to reflect the changes in the messages. -- Now the style of In-Editor Status has been solid for some Android devices. - -## 0.24.2 - -### Rewritten - -- Hidden File Sync is now respects the file changes on the storage. Not simply comparing modified times. - - This makes hidden file sync more robust and reliable. - -### Fixed - -- `Scan hidden files before replication` is now configurable again. -- Some unexpected errors are now handled more gracefully. -- Meaningless event passing during boot sequence is now prevented. -- Error handling for non-existing files has been fixed. -- Hidden files will not be batched to avoid the potential error. - - This behaviour had been causing the error in the previous versions in specific situations. -- The log which checking automatic conflict resolution is now in verbose level. -- Replication log (skipping non-targetting files) shows the correct information. -- The dialogue that asking enabling optional feature during `Rebuild Everything` now prevents to show the `overwrite` option. - - The rebuilding device is the first, meaningless. -- Files with different modified time but identical content are no longer processed repeatedly. -- Some unexpected errors which caused after terminating plug-in are now avoided. -- - -### Improved - -- JSON files are now more transferred efficiently. - - Now the JSON files are transferred in more fine chunks, which makes the transfer more efficient. - -## 0.24.1 - -### Fixed - -- Vault History can show the correct information of match-or-not for each file and database even if it is a binary file. -- `Sync settings via markdown` is now hidden during the setup wizard. -- Verify and Fix will ignore the hidden files if the hidden file sync is disabled. - -#### New feature - -- Now we can fetch the tweaks from the remote database while the setting dialogue and wizard are processing. - -### Improved - -- More things are moved to the modules. - - Includes the Main codebase. Now `main.ts` is almost stub. -- EventHub is now more robust and typesafe. - -## 0.24.0 - -### Improved - -- The welcome message is now more simple to encourage the use of the Setup-URI. - - The secondary message is also simpler to guide users to Minimal Setup. - - But Setup-URI will be recommended again, due to its importance. - - These dialogues contain a link to the documentation which can be clicked. -- The minimal setup is more minimal now. And, the setup is more user-friendly. - - Now the Configuration of the remote database is checked more robustly, but we can ignore the warning and proceed with the setup. -- Before we are asked about each feature, we are asked if we want to use optional features in the first place. - - This is to prevent the user from being overwhelmed by the features. - - And made it clear that it is not recommended for new users. -- Many messages have been improved for better understanding. - - Ridiculous messages have been (carefully) refined. - - Dialogues are more informative and friendly. - - A lot of messages have been mostly rewritten, leveraging Markdown. - - Especially auto-closing dialogues are now explicitly labelled: `To stop the countdown, tap anywhere on the dialogue`. -- Now if the is plugin configured to ignore some events, we will get a chance to fix it, in addition to the warning. - - And why that has happened is also explained in the dialogue. -- A note relating to device names has been added to Customisation Sync on the setting dialogue. -- We can verify and resolve also the hidden files now. - -### Fixed - -- We can resolve the conflict of the JSON file correctly now. -- Verifying files between the local database and storage is now working correctly. -- While restarting the plug-in, the shown dialogues will be automatically closed to avoid unexpected behaviour. -- Replicated documents that the local device has configured to ignore are now correctly ignored. -- The chunks of the document on the local device during the first transfer will be created correctly. - - And why we should create them is now explained in the dialogue. -- If optional features have been enabled in the wizard, `Enable advanced features` will be toggled correctly. - The hidden file sync is now working correctly. - Now the deletion of hidden files is correctly synchronised. -- Customisation Sync is now working correctly together with hidden file sync. -- No longer database suffix is stored in the setting sharing markdown. -- A fair number of bugs have been fixed. - -### Changed - -- Some default settings have been changed for an easier new user experience. - - Preventing the meaningless migration of the settings. - -### Tiding - -- The codebase has been reorganised into clearly defined modules. -- Commented-out codes have been gradually removed. - -### 0.23.0 - -Incredibly new features! - -Now, we can use object storage (MinIO, S3, R2 or anything you like) for synchronising! Moreover, despite that, we can use all the features as if we were using CouchDB. -Note: As this is a pretty experimental feature, hence we have some limitations. - -- This is built on the append-only architecture. It will not shrink used storage if we do not perform a rebuild. -- A bit fragile. However, our version x.yy.0 is always so. -- When the first synchronisation, the entire history to date is transferred. For this reason, it is preferable to do this under the WiFi network. -- Do not worry, from the second synchronisation, we always transfer only differences. - -I hope this feature empowers users to maintain independence and self-host their data, offering an alternative for those who prefer to manage their own storage solutions and avoid being stuck on the right side of a sudden change in business model. - -Of course, I use Self-hosted MinIO for testing and recommend this. It is for the same reason as using CouchDB. -- open, controllable, auditable and indeed already audited by numerous eyes. - -Let me write one more acknowledgement. - -I have a lot of respect for that plugin, even though it is sometimes treated as if it is a competitor, remotely-save. I think it is a great architecture that embodies a different approach to my approach of recreating history. This time, with all due respect, I have used some of its code as a reference. -Hooray for open source, and generous licences, and the sharing of knowledge by experts. - -#### Version history - -- 0.23.23: - - Refined: - - Setting dialogue very slightly refined. - - The hodgepodge inside the `Hatch` pane has been sorted into more explicit categorised panes. - - Now we have new panes for: - - `Selector` - - `Advanced` - - `Power users` - - `Patches (Edge case)` - - Applying the settings will now be more informative. - - The header bar will be shown for applying the settings which needs a database rebuild. - - Applying methods are now more clearly navigated. - - Definitely, drastic change. I hope this will be more user-friendly. However, if you notice any issues, please let me know. I hope that nothing missed. - - New features: - - Word-segmented chunk building on users language - - Chunks can now be built with word-segmented data, enhancing efficiency for markdown files which contains the multiple sentences in a single line. - - This feature is enabled by default through `Use Segmented-splitter`. - - (Default: Disabled, Please be relived, I have learnt). - - Fixed: - - Sending chunks on `Send chunk in bulk` are now buffered to avoid the out-of-memory error. - - `Send chunk in bulk` is back to default disabled. (Sorry, not applied to the migrated users; I did not think we should deepen the wound any further "automatically"). - - Merging conflicts of JSON files are now works fine even if it contains `null`. - - Development: - - Implemented the logic for automatically generating the stub of document for the setting dialogue. -- 0.23.22: - - Fixed: - - Case-insensitive file handling - - Full-lower-case files are no longer created during database checking. - - Bulk chunk transfer - - The default value will automatically adjust to an acceptable size when using IBM Cloudant. -- 0.23.21: - - New Features: - - Case-insensitive file handling - - Files can now be handled case-insensitively. - - This behaviour can be modified in the settings under `Handle files as Case-Sensitive` (Default: Prompt, Enabled for previous behaviour). - - Improved chunk revision fixing - - Revisions for chunks can now be fixed for faster chunk creation. - - This can be adjusted in the settings under `Compute revisions for chunks` (Default: Prompt, Enabled for previous behaviour). - - Bulk chunk transfer - - Chunks can now be transferred in bulk during uploads. - - This feature is enabled by default through `Send chunks in bulk`. - - Creation of missing chunks without - - Missing chunks can be created without storing notes, enhancing efficiency for first synchronisation or after prolonged periods without synchronisation. - - Improvements: - - File status scanning on the startup - - Quite significant performance improvements. - - No more missing scans of some files. - - Status in editor enhancements - - Significant performance improvements in the status display within the editor. - - Notifications for files that will not be synchronised will now be properly communicated. - - Encryption and Decryption - - These processes are now performed in background threads to ensure fast and stable transfers. - - Verify and repair all files - - Got faster through parallel checking. - - Migration on update - - Migration messages and wizards have become more helpful. - - Behavioural changes: - - Chunk size adjustments - - Large chunks will no longer be created for older, stable files, addressing storage consumption issues. - - Flag file automation - - Confirmation will be shown and we can cancel it. - - Fixed: - - Database File Scanning - - All files in the database will now be enumerated correctly. - - Miscellaneous - - Dependency updated. - - Now, tree shaking is left to terser, from esbuild. -- 0.23.20: - - Fixed: - - Customisation Sync now checks the difference while storing or applying the configuration. - - No longer storing the same configuration multiple times. - - Time difference in the dialogue has been fixed. - - Remote Storage Limit Notification dialogue has been fixed, now the chosen value is saved. - - Improved: - - The Enlarging button on the enlarging threshold dialogue now displays the new value. -- 0.23.19: - - Not released. -- 0.23.18: - - New feature: - - Per-file-saved customization sync has been shipped. - - We can synchronise plug-igs etc., more smoothly. - - Default: disabled. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost compatibility with old versions. - - Customisation sync has got beta3. - - We can set `Flag` to each item to select the newest, automatically. - - This configuration is per device. - - Improved: - - Start-up speed has been improved. - - Fixed: - - On the customisation sync dialogue, buttons are kept within the screen. - - No more unnecessary entries on `data.json` for customisation sync. - - Selections are no longer lost while updating customisation items. - - Tidied on source codes: - - Many typos have been fixed. - - Some unnecessary type casting removed. -- 0.23.17: - - Improved: - - Overall performance has been improved by using PouchDB 9.0.0. - - Configuration mismatch detection is refined. We can resolve mismatches more smoothly and naturally. - More detail is on `troubleshooting.md` on the repository. - - Fixed: - - Customisation Sync will be disabled when a corrupted configuration is detected. - Therefore, the Device Name can be changed even in the event of a configuration mismatch. - - New feature: - - We can get a notification about the storage usage of the remote database. - - Default: We will be asked. - - If the remote storage usage approaches the configured value, we will be asked whether we want to Rebuild or increase the limit. -- 0.23.16: - - Maintenance Update: - - Library refining (Phase 1 - step 2). There are no significant changes on the user side. - - Including the following fixes of potentially problems: - - the problem which the path had been obfuscating twice has been resolved. - - Note: Potential problems of the library; which has not happened in Self-hosted LiveSync for some reasons. -- 0.23.15: - - Maintenance Update: - - Library refining (Phase 1). There are no significant changes on the user side. -- 0.23.14: - - Fixed: - - No longer batch-saving ignores editor inputs. - - The file-watching and serialisation processes have been changed to the one which is similar to previous implementations. - - We can configure the settings (Especially about text-boxes) even if we have configured the device name. - - Improved: - - We can configure the delay of batch-saving. - - Default: 5 seconds, the same as the previous hard-coded value. (Note: also, the previous behaviour was not correct). - - Also, we can configure the limit of delaying batch-saving. - - The performance of showing status indicators has been improved. -- 0.23.13: - - Fixed: - - No longer files have been trimmed even delimiters have been continuous. - - Fixed the toggle title to `Do not split chunks in the background` from `Do not split chunks in the foreground`. - - Non-configured item mismatches are no longer detected. -- 0.23.12: - - Improved: - - Now notes will be split into chunks in the background thread to improve smoothness. - - Default enabled, to disable, toggle `Do not split chunks in the foreground` on `Hatch` -> `Compatibility`. - - If you want to process very small notes in the foreground, please enable `Process small files in the foreground` on `Hatch` -> `Compatibility`. - - We can use a `splitting-limit-capped chunk splitter`; which performs more simple and make less amount of chunks. - - Default disabled, to enable, toggle `Use splitting-limit-capped chunk splitter` on `Sync settings` -> `Performance tweaks` - - Tidied - - Some files have been separated into multiple files to make them more explicit in what they are responsible for. -- 0.23.11: - - Fixed: - - Now we _surely_ can set the device name and enable customised synchronisation. - - Unnecessary dialogue update processes have been eliminated. - - Customisation sync no longer stores half-collected files. - - No longer hangs up when removing or renaming files with the `Sync on Save` toggle enabled. - - Improved: - - Customisation sync now performs data deserialization more smoothly. - - New translations have been merged. -- 0.23.10 - - Fixed: - - No longer configurations have been locked in the minimal setup. -- 0.23.9 - - Fixed: - - No longer unexpected parallel replication is performed. - - Now we can set the device name and enable customised synchronisation again. -- 0.23.8 - - New feature: - - Now we are ready for i18n. - - Patch or PR of `rosetta.ts` are welcome! - - The setting dialogue has been refined. Very controllable, clearly displayed disabled items, and ready to i18n. - - Fixed: - - Many memory leaks have been rescued. - - Chunk caches now work well. - - Many trivial but potential bugs are fixed. - - No longer error messages will be shown on retrieving checkpoint or server information. - - Now we can check and correct tweak mismatch during the setup - - Improved: - - Customisation synchronisation has got more smoother. - - Tidied - - Practically unused functions have been removed or are being prepared for removal. - - Many of the type-errors and lint errors have been corrected. - - Unused files have been removed. - - Note: - - From this version, some test files have been included. However, they are not enabled and released in the release build. - - To try them, please run Self-hosted LiveSync in the dev build. -- 0.23.7 - - Fixed: - - No longer missing tasks which have queued as the same key (e.g., for the same operation to the same file). - - This occurs, for example, with hidden files that have been changed multiple times in a very short period of time, such as `appearance.json`. Thanks for the report! - - Some trivial issues have been fixed. - - New feature: - - Reloading Obsidian can be scheduled until that file and database operations are stable. -- 0.23.6: - - Fixed: - - Now the remote chunks could be decrypted even if we are using `Incubate chunks in Document`. (The note of 0.23.6 has been fixed). - - Chunk retrieving with `Incubate chunks in document` got more efficiently. - - No longer task processor misses the completed tasks. - - Replication is no longer started automatically during changes in window visibility (e.g., task switching on the desktop) when off-focused. -- 0.23.5: - - New feature: - - Now we can check configuration mismatching between clients before synchronisation. - - Default: enabled / Preferred: enabled / We can disable this by the `Do not check configuration mismatch before replication` toggle in the `Hatch` pane. - - It detects configuration mismatches and prevents synchronisation failures and wasted storage. - - Now we can perform remote database compaction from the `Maintenance` pane. - - Fixed: - - We can detect the bucket could not be reachable. - - Note: - - Known inexplicable behaviour: Recently, (Maybe while enabling `Incubate chunks in Document` and `Fetch chunks on demand` or some more toggles), our customisation sync data is sometimes corrupted. It will be addressed by the next release. -- 0.23.4 - - Fixed: - - No longer experimental configuration is shown on the Minimal Setup. - - New feature: - - We can now use `Incubate Chunks in Document` to reduce non-well-formed chunks. - - Default: disabled / Preferred: enabled in all devices. - - When we enabled this toggle, newly created chunks are temporarily kept within the document, and graduated to become independent chunks once stabilised. - - The [design document](https://github.com/vrtmrz/obsidian-livesync/blob/3925052f9290b3579e45a4b716b3679c833d8ca0/docs/design_docs_of_keep_newborn_chunks.md) has been also available.. -- 0.23.3 - - Fixed: No longer unwanted `\f` in journal sync. -- 0.23.2 - - Sorry for all the fixes to experimental features. (These things were also critical for dogfooding). The next release would be the main fixes! Thank you for your patience and understanding! - - Fixed: - - Journal Sync will not hang up during big replication, especially the initial one. - - All changes which have been replicated while rebuilding will not be postponed (Previous behaviour). - - Improved: - - Now Journal Sync works efficiently in download and parse, or pack and upload. - - Less server storage and faster packing/unpacking usage by the new chunk format. -- 0.23.1 - - - Fixed: - - Now journal synchronisation considers untransferred each from sent and received. - - Journal sync now handles retrying. - - Journal synchronisation no longer considers the synchronisation of chunks as revision updates (Simply ignored). - - Journal sync now splits the journal pack to prevent mobile device rebooting. - - Maintenance menus which had been on the command palette are now back in the maintain pane on the setting dialogue. - - Improved: - - Now all changes which have been replicated while rebuilding will be postponed. - -- 0.23.0 - - New feature: - - Now we can use Object Storage. - -### 0.22.0 - -A few years passed since Self-hosted LiveSync was born, and our codebase had been very complicated. This could be patient now, but it should be a tremendous hurt. -Therefore at v0.22.0, for future maintainability, I refined task scheduling logic totally. - -Of course, I think this would be our suffering in some cases. However, I would love to ask you for your cooperation and contribution. - -Sorry for being absent so much long. And thank you for your patience! - -Note: we got a very performance improvement. -Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please. - -#### Version history - -- 0.22.19 - - Fixed: - - No longer data corrupting due to false BASE64 detections. - - Improved: - - A bit more efficient in Automatic data compression. -- 0.22.18 - - New feature (Very Experimental): - - Now we can use `Automatic data compression` to reduce amount of traffic and the usage of remote database. - - Please make sure all devices are updated to v0.22.18 before trying this feature. - - If you are using some other utilities which connected to your vault, please make sure that they have compatibilities. - - Note: Setting `File Compression` on the remote database works for shrink the size of remote database. Please refer the [Doc](https://docs.couchdb.org/en/stable/config/couchdb.html#couchdb/file_compression). -- 0.22.17: - - Fixed: - - Error handling on booting now works fine. - - Replication is now started automatically in LiveSync mode. - - Batch database update is now disabled in LiveSync mode. - - No longer automatically reconnection while off-focused. - - Status saves are thinned out. - - Now Self-hosted LiveSync waits for all files between the local database and storage to be surely checked. - - Improved: - - The job scheduler is now more robust and stable. - - The status indicator no longer flickers and keeps zero for a while. - - No longer meaningless frequent updates of status indicators. - - Now we can configure regular expression filters in handy UI. Thank you so much, @eth-p! - - `Fetch` or `Rebuild everything` is now more safely performed. - - Minor things - - Some utility function has been added. - - Customisation sync now less wrong messages. - - Digging the weeds for eradication of type errors. -- 0.22.16: - - Fixed: - - Fixed the issue that binary files were sometimes corrupted. - - Fixed customisation sync data could be corrupted. - - Improved: - - Now the remote database costs lower memory. - - This release requires a brief wait on the first synchronisation, to track the latest changeset again. - - Description added for the `Device name`. - - Refactored: - - Many type-errors have been resolved. - - Obsolete file has been deleted. -- 0.22.15: - - Improved: - Faster start-up by removing too many logs which indicates normality - By streamlined scanning of customised synchronisation extra phases have been deleted. - ... To continue on to `updates_old.md`. -- 0.22.14: - - New feature: - - We can disable the status bar in the setting dialogue. - - Improved: - - Now some files are handled as correct data type. - - Customisation sync now uses the digest of each file for better performance. - - The status in the Editor now works performant. - - Refactored: - - Common functions have been ready and the codebase has been organised. - - Stricter type checking following TypeScript updates. - - Remove old iOS workaround for simplicity and performance. -- 0.22.13: - - Improved: - - Now using HTTP for the remote database URI warns of an error (on mobile) or notice (on desktop). - - Refactored: - - Dependencies have been polished. -- 0.22.12: - - Changed: - - The default settings has been changed. - - Improved: - - Default and preferred settings are applied on completion of the wizard. - - Fixed: - - Now Initialisation `Fetch` will be performed smoothly and there will be fewer conflicts. - - No longer stuck while Handling transferred or initialised documents. -- 0.22.11: - - Fixed: - - `Verify and repair all files` is no longer broken. - - New feature: - - Now `Verify and repair all files` is able to... - - Restore if the file only in the local database. - - Show the history. - - Improved: - - Performance improved. -- 0.22.10 - - Fixed: - - No longer unchanged hidden files and customisations are saved and transferred now. - - File integrity of vault history indicates the integrity correctly. - - Improved: - - In the report, the schema of the remote database URI is now printed. -- 0.22.9 - - Fixed: - - Fixed a bug on `fetch chunks on demand` that could not fetch the chunks on demand. - - Improved: - - `fetch chunks on demand` works more smoothly. - - Initialisation `Fetch` is now more efficient. - - Tidied: - - Removed some meaningless codes. -- 0.22.8 - - Fixed: - - Now fetch and unlock the locked remote database works well again. - - No longer crash on symbolic links inside hidden folders. - - Improved: - - Chunks are now created more efficiently. - - Splitting old notes into a larger chunk. - - Better performance in saving notes. - - Network activities are indicated as an icon. - - Less memory used for binary processing. - - Tidied: - - Cleaned unused functions up. - - Sorting out the codes that have become nonsense. - - Changed: - - Now no longer `fetch chunks on demand` needs `Pacing replication` - - The setting `Do not pace synchronization` has been deleted. -- 0.22.7 - - Fixed: - - No longer deleted hidden files were ignored. - - The document history dialogue is now able to process the deleted revisions. - - Deletion of a hidden file is now surely performed even if the file is already conflicted. -- 0.22.6 - - Fixed: - - Fixed a problem with synchronisation taking a long time to start in some cases. - - The first synchronisation after update might take a bit longer. - - Now we can disable E2EE encryption. - - Improved: - - `Setup Wizard` is now more clear. - - `Minimal Setup` is now more simple. - - Self-hosted LiveSync now be able to use even if there are vaults with the same name. - - Database suffix will automatically added. - - Now Self-hosted LiveSync waits until set-up is complete. - - Show reload prompts when possibly recommended while settings. - - New feature: - - A guidance dialogue prompting for settings will be shown after the installation. - - Changed - - `Open setup URI` is now `Use the copied setup URI` - - `Copy setup URI` is now `Copy current settings as a new setup URI` - - `Setup Wizard` is now `Minimal Setup` - - `Check database configuration` is now `Check and Fix database configuration` -- 0.22.5 - - Fixed: - - Some description of settings have been refined - - New feature: - - TroubleShooting is now shown in the setting dialogue. -- 0.22.4 - - Fixed: - - Now the result of conflict resolution could be surely written into the storage. - - Deleted files can be handled correctly again in the history dialogue and conflict dialogue. - - Some wrong log messages were fixed. - - Change handling now has become more stable. - - Some event handling became to be safer. - - Improved: - - Dumping document information shows conflicts and revisions. - - The timestamp-only differences can be surely cached. - - Timestamp difference detection can be rounded by two seconds. - - Refactored: - - A bit of organisation to write the test. -- 0.22.3 - - Fixed: - - No longer detects storage changes which have been caused by Self-hosted LiveSync itself. - - Setting sync file will be detected only if it has been configured now. - - And its log will be shown only while the verbose log is enabled. - - Customisation file enumeration has got less blingy. - - Deletion of files is now reliably synchronised. - - Fixed and improved: - - In-editor-status is now shown in the following areas: - - Note editing pane (Source mode and live-preview mode). - - New tab pane. - - Canvas pane. -- 0.22.2 - - Fixed: - - Now the results of resolving conflicts are surely synchronised. - - Modified: - - Some setting items got new clear names. (`Sync Settings` -> `Targets`). - - New feature: - - We can limit the synchronising files by their size. (`Sync Settings` -> `Targets` -> `Maximum file size`). - - It depends on the size of the newer one. - - At Obsidian 1.5.3 on mobile, we should set this to around 50MB to avoid restarting Obsidian. - - Now the settings could be stored in a specific markdown file to synchronise or switch it (`General Setting` -> `Share settings via markdown`). - - [Screwdriver](https://github.com/vrtmrz/obsidian-screwdriver) is quite good, but mostly we only need this. - - Customisation of the obsoleted device is now able to be deleted at once. - - We have to put the maintenance mode in at the Customisation sync dialogue. -- 0.22.1 - - New feature: - - We can perform automatic conflict resolution for inactive files, and postpone only manual ones by `Postpone manual resolution of inactive files`. - - Now we can see the image in the document history dialogue. - - We can see the difference of the image, in the document history dialogue. - - And also we can highlight differences. - - Improved: - - Hidden file sync has been stabilised. - - Now automatically reloads the conflict-resolution dialogue when new conflicted revisions have arrived. - - Fixed: - - No longer periodic process runs after unloading the plug-in. - - Now the modification of binary files is surely stored in the storage. -- 0.22.0 - - Refined: - - Task scheduling logics has been rewritten. - - Screen updates are also now efficient. - - Possibly many bugs and fragile behaviour has been fixed. - - Status updates and logging have been thinned out to display. - - Fixed: - - Remote-chunk-fetching now works with keeping request intervals - - New feature: - - We can show only the icons in the editor. - - Progress indicators have been more meaningful: - - 📥 Unprocessed transferred items - - 📄 Working database operation - - 💾 Working write storage processes - - ⏳ Working read storage processes - - 🛫 Pending read storage processes - - ⚙️ Working or pending storage processes of hidden files - - 🧩 Waiting chunks - - 🔌 Working Customisation items (Configuration, snippets and plug-ins) - -... To continue on to `updates_old.md`. - -### 0.21.0 - -The E2EE encryption V2 format has been reverted. That was probably the cause of the glitch. -Instead, to maintain efficiency, files are treated with Blob until just before saving. Along with this, the old-fashioned encryption format has also been discontinued. -There are both forward and backwards compatibilities, with recent versions. However, unfortunately, we lost compatibility with filesystem-livesync or some. -It will be addressed soon. Please be patient if you are using filesystem-livesync with E2EE. - -- 0.21.5 - - Improved: - - Now all revisions will be shown only its first a few letters. - - Now ID of the documents is shown in the log with the first 8 letters. - - Fixed: - - Check before modifying files has been implemented. - - Content change detection has been improved. -- 0.21.4 - - This release had been skipped. -- 0.21.3 - - Implemented: - - Now we can use SHA1 for hash function as fallback. -- 0.21.2 - - IMPORTANT NOTICE: **0.21.1 CONTAINS A BUG WHILE REBUILDING THE DATABASE. IF YOU HAVE BEEN REBUILT, PLEASE MAKE SURE THAT ALL FILES ARE SANE.** - - This has been fixed in this version. - - Fixed: - - No longer files are broken while rebuilding. - - Now, Large binary files can be written correctly on a mobile platform. - - Any decoding errors now make zero-byte files. - - Modified: - - All files are processed sequentially for each. -- 0.21.1 - - Fixed: - - No more infinity loops on larger files. - - Show message on decode error. - - Refactored: - - Fixed to avoid obsolete global variables. -- 0.21.0 - - Changes and performance improvements: - - Now the saving files are processed by Blob. - - The V2-Format has been reverted. - - New encoding format has been enabled in default. - - WARNING: Since this version, the compatibilities with older Filesystem LiveSync have been lost. - -## 0.20.0 - -At 0.20.0, Self-hosted LiveSync has changed the binary file format and encrypting format, for efficient synchronisation. -The dialogue will be shown and asks us to decide whether to keep v1 or use v2. Once we have enabled v2, all subsequent edits will be saved in v2. Therefore, devices running 0.19 or below cannot understand this and they might say that decryption error. Please update all devices. -Then we will have an impressive performance. - -Of course, these are very impactful changes. If you have any questions or troubled things, please feel free to open an issue and mention me. - -Note: if you want to roll it back to v1, please enable `Use binary and encryption version 1` on the `Hatch` pane and perform the `rebuild everything` once. - -Extra but notable information: - -This format change gives us the ability to detect some `marks` in the binary files as same as text files. Therefore, we can split binary files and some specific sort of them (i.e., PDF files) at the specific character. It means that editing the middle of files could be detected with marks. - -Now only a few chunks are transferred, even if we add a comment to the PDF or put new files into the ZIP archives. - -- 0.20.7 - - Fixed - - To better replication, path obfuscation is now deterministic even if with E2EE. - Note: Compatible with previous database without any conversion. Only new files will be obfuscated in deterministic. -- 0.20.6 - - Fixed - - Now empty file could be decoded. - - Local files are no longer pre-saved before fetching from a remote database. - - No longer deadlock while applying customisation sync. - - Configuration with multiple files is now able to be applied correctly. - - Deleting folder propagation now works without enabling the use of a trash bin. -- 0.20.5 - - Fixed - - Now the files which having digit or character prefixes in the path will not be ignored. -- 0.20.4 - - Fixed - - The text-input-dialogue is no longer broken. - - Finally, we can use the Setup URI again on mobile. -- 0.20.3 - - New feature: - - We can launch Customization sync from the Ribbon if we enabled it. - - Fixed: - - Setup URI is now back to the previous spec; be encrypted by V1. - - It may avoid the trouble with iOS 17. - - The Settings dialogue is now registered at the beginning of the start-up process. - - We can change the configuration even though LiveSync could not be launched in normal. - - Improved: - - Enumerating documents has been faster. -- 0.20.2 - - New feature: - - We can delete all data of customization sync from the `Delete all customization sync data` on the `Hatch` pane. - - Fixed: - - Prevent keep restarting on iOS by yielding microtasks. -- 0.20.1 - - Fixed: - - No more UI freezing and keep restarting on iOS. - - Diff of Non-markdown documents are now shown correctly. - - Improved: - - Performance has been a bit improved. - - Customization sync has gotten faster. - - However, We lost forward compatibility again (only for this feature). Please update all devices. - - Misc - - Terser configuration has been more aggressive. -- 0.20.0 - - Improved: - - A New binary file handling implemented - - A new encrypted format has been implemented - - Now the chunk sizes will be adjusted for efficient sync - - Fixed: - - levels of exception in some logs have been fixed - - Tidied: - - Some Lint warnings have been suppressed. - -### 0.19.0 - -#### Customization sync - -Since `Plugin and their settings` have been broken, so I tried to fix it, not just fix it, but fix it the way it should be. - -Now, we have `Customization sync`. - -It is a real shame that the compatibility between these features has been broken. However, this new feature is surely useful and I believe that worth getting over the pain. -We can use the new feature with the same configuration. Only the menu on the command palette has been changed. The dialog can be opened by `Show customization sync dialog`. - -I hope you will give it a try. - -#### Minors - -- 0.19.1 - - Fixed: Fixed hidden file handling on Linux - - Improved: Now customization sync works more smoothly. -- 0.19.2 - - Fixed: - - Fixed garbage collection error while unreferenced chunks exist many. - - Fixed filename validation on Linux. - - Improved: - - Showing status is now thinned for performance. - - Enhance caching while collecting chunks. -- 0.19.3 - - Improved: - - Now replication will be paced by collecting chunks. If synchronisation has been deadlocked, please enable `Do not pace synchronization` once. -- 0.19.4 - - Improved: - - Reduced remote database checking to improve speed and reduce bandwidth. - - Fixed: - - Chunks which previously misinterpreted are now interpreted correctly. - - No more missing chunks which not be found forever, except if it has been actually missing. - - Deleted file detection on hidden file synchronising now works fine. - - Now the Customisation sync is surely quiet while it has been disabled. -- 0.19.5 - - Fixed: - - Now hidden file synchronisation would not be hanged, even if so many files exist. - - Improved: - - Customisation sync works more smoothly. - - Note: Concurrent processing has been rollbacked into the original implementation. As a result, the total number of processes is no longer shown next to the hourglass icon. However, only the processes that are running concurrently are shown. -- 0.19.6 - - Fixed: - - Logging has been tweaked. - - No more too many planes and rockets. - - The batch database update now surely only works in non-live mode. - - Internal things: - - Some frameworks has been upgraded. - - Import declaration has been fixed. - - Improved: - - The plug-in now asks to enable a new adaptor, when rebuilding, if it is not enabled yet. - - The setting dialogue refined. - - Configurations for compatibilities have been moved under the hatch. - - Made it clear that disabled is the default. - - Ambiguous names configuration have been renamed. - - Items that have no meaning in the settings are no longer displayed. - - Some items have been reordered for clarity. - - Each configuration has been grouped. -- 0.19.7 - - Fixed: - - The initial pane of Setting dialogue is now changed to General Settings. - - The Setup Wizard is now able to flush existing settings and get into the mode again. -- 0.19.8 - - New feature: - - Vault history: A tab has been implemented to give a birds-eye view of the changes that have occurred in the vault. - - Improved: - - Now the passphrases on the dialogue masked out. Thank you @antoKeinanen! - - Log dialogue is now shown as one of tabs. - - Fixed: - - Some minor issues has been fixed. -- 0.19.9 - - New feature (For fixing a problem): - - We can fix the database obfuscated and plain paths that have been mixed up. - - Improvements - - Customisation Sync performance has been improved. -- 0.19.10 - - Fixed - - Fixed the issue about fixing the database. -- 0.19.11 - - Improvements: - - Hashing ChunkID has been improved. - - Logging keeps 400 lines now. - - Refactored: - - Import statement has been fixed about types. -- 0.19.12 - - Improved: - - Boot-up performance has been improved. - - Customisation sync performance has been improved. - - Synchronising performance has been improved. -- 0.19.13 - - Implemented: - - Database clean-up is now in beta 2! - We can shrink the remote database by deleting unused chunks, with keeping history. - Note: Local database is not cleaned up totally. We have to `Fetch` again to let it done. - **Note2**: Still in beta. Please back your vault up anything before. - - Fixed: - - The log updates are not thinned out now. -- 0.19.14 - - Fixed: - - Internal documents are now ignored. - - Merge dialogue now respond immediately to button pressing. - - Periodic processing now works fine. - - The checking interval of detecting conflicted has got shorter. - - Replication is now cancelled while cleaning up. - - The database locking by the cleaning up is now carefully unlocked. - - Missing chunks message is correctly reported. - - New feature: - - Suspend database reflecting has been implemented. - - This can be disabled by `Fetch database with previous behaviour`. - - Now fetch suspends the reflecting database and storage changes temporarily to improve the performance. - - We can choose the action when the remote database has been cleaned - - Merge dialogue now show `↲` before the new line. - - Improved: - - Now progress is reported while the cleaning up and fetch process. - - Cancelled replication is now detected. -- 0.19.15 - - Fixed: - - Now storing files after cleaning up is correct works. - - Improved: - - Cleaning the local database up got incredibly fastened. - Now we can clean instead of fetching again when synchronising with the remote which has been cleaned up. -- 0.19.16 - - Many upgrades on this release. I have tried not to let that happen, if something got corrupted, please feel free to notify me. - - New feature: - - (Beta) ignore files handling - We can use `.gitignore`, `.dockerignore`, and anything you like to filter the synchronising files. - - Fixed: - - Buttons on lock-detected-dialogue now can be shown in narrow-width devices. - - Improved: - - Some constant has been flattened to be evaluated. - - The usage of the deprecated API of obsidian has been reduced. - - Now the indexedDB adapter will be enabled while the importing configuration. - - Misc: - - Compiler, framework, and dependencies have been upgraded. - - Due to standing for these impacts (especially in esbuild and svelte,) terser has been introduced. - Feel free to notify your opinion to me! I do not like to obfuscate the code too. -- 0.19.17 - - Fixed: - - Now nested ignore files could be parsed correctly. - - The unexpected deletion of hidden files in some cases has been corrected. - - Hidden file change is no longer reflected on the device which has made the change itself. - - Behaviour changed: - - From this version, the file which has `:` in its name should be ignored even if on Linux devices. -- 0.19.18 - - Fixed: - - Now the empty (or deleted) file could be conflict-resolved. -- 0.19.19 - - Fixed: - - Resolving conflicted revision has become more robust. - - LiveSync now try to keep local changes when fetching from the rebuilt remote database. - Local changes now have been kept as a revision and fetched things will be new revisions. - - Now, all files will be restored after performing `fetch` immediately. -- 0.19.20 - - New feature: - - `Sync on Editor save` has been implemented - - We can start synchronisation when we save from the Obsidian explicitly. - - Now we can use the `Hidden file sync` and the `Customization sync` cooperatively. - - We can exclude files from `Hidden file sync` which is already handled in Customization sync. - - We can ignore specific plugins in Customization sync. - - Now the message of leftover conflicted files accepts our click. - - We can open `Resolve all conflicted files` in an instant. - - Refactored: - - Parallelism functions made more explicit. - - Type errors have been reduced. - - Fixed: - - Now documents would not be overwritten if they are conflicted. - It will be saved as a new conflicted revision. - - Some error messages have been fixed. - - Missing dialogue titles have been shown now. - - We can click close buttons on mobile now. - - Conflicted Customisation sync files will be resolved automatically by their modified time. -- 0.19.21 - - Fixed: - - Hidden files are no longer handled in the initial replication. - - Report from `Making report` fixed - - No longer contains customisation sync information. - - Version of LiveSync has been added. -- 0.19.22 - - Fixed: - - Now the synchronisation will begin without our interaction. - - No longer puts the configuration of the remote database into the log while checking configuration. - - Some outdated description notes have been removed. - - Options that are meaningless depending on other settings configured are now hidden. - - Scan for hidden files before replication - - Scan customization periodically -- 0.19.23 - -Improved: - - We can open the log pane also from the command palette now. - - Now, the hidden file scanning interval could be configured to 0. - - `Check database configuration` now points out that we do not have administrator permission. - -### 0.18.0 - -#### Now, paths of files in the database can now be obfuscated. (Experimental Feature) - -At before v0.18.0, Self-hosted LiveSync used the path of files, to detect and resolve conflicts. In naive. The ID of the document stored in the CouchDB was naturally the filename. -However, it means a sort of lacking confidentiality. If the credentials of the database have been leaked, the attacker (or an innocent bystander) can read the path of files. So we could not use confidential things in the filename in some environments. -Since v0.18.0, they can be obfuscated. so it is no longer possible to decipher the path from the ID. Instead of that, it costs a bit CPU load than before, and the data structure has been changed a bit. - -We can configure the `Path Obfuscation` in the `Remote database configuration` pane. -Note: **When changing this configuration, we need to rebuild both of the local and the remote databases**. - -#### Minors - -- 0.18.1 - - Fixed: - - Some messages are fixed (Typo) - - File type detection now works fine! -- 0.18.2 - - Improved: - - The setting pane has been refined. - - We can enable `hidden files sync` with several initial behaviours; `Merge`, `Fetch` remote, and `Overwrite` remote. - - No longer `Touch hidden files`. -- 0.18.3 - - Fixed Pop-up is now correctly shown after hidden file synchronisation. -- 0.18.4 - - Fixed: - - `Fetch` and `Rebuild database` will work more safely. - - Case-sensitive renaming now works fine. - Revoked the logic which was made at #130, however, looks fine now. -- 0.18.5 - - - Improved: - - Actions for maintaining databases moved to the `🎛️Maintain databases`. - - Clean-up of unreferenced chunks has been implemented on an **experimental**. - - This feature requires enabling `Use new adapter`. - - Be sure to fully all devices synchronised before perform it. - - After cleaning up the remote, all devices will be locked out. If we are sure had it be synchronised, we can perform only cleaning-up locally. If not, we have to perform `Fetch`. - -- 0.18.6 - - New features: - - Now remote database cleaning-up will be detected automatically. - - A solution selection dialogue will be shown if synchronisation is rejected after cleaning or rebuilding the remote database. - - During fetching or rebuilding, we can configure `Hidden file synchronisation` on the spot. - - It let us free from conflict resolution on initial synchronising. - -### 0.17.0 - -- 0.17.0 has no surfaced changes but the design of saving chunks has been changed. They have compatibility but changing files after upgrading makes different chunks than before 0.16.x. - Please rebuild databases once if you have been worried about storage usage. - - - Improved: - - - Splitting markdown - - Saving chunks - - - Changed: - - Chunk ID numbering rules - -#### Minors - -- 0.17.1 - - - Fixed: Now we can verify and repair the database. - - Refactored inside. - -- 0.17.2 - - - New feature - - We can merge conflicted documents automatically if sensible. - - Fixed - - Writing to the storage will be pended while they have conflicts after replication. - -- 0.17.3 - - - Now we supported canvas! And conflicted JSON files are also synchronised with merging its content if they are obvious. - -- 0.17.4 - - - Canvases are now treated as a sort of plain text file. now we transfer only the metadata and chunks that have differences. - -- 0.17.5 Now `read chunks online` had been fixed, and a new feature: `Use dynamic iteration count` to reduce the load on encryption/decryption. - Note: `Use dynamic iteration count` is not compatible with earlier versions. -- 0.17.6 Now our renamed/deleted files have been surely deleted again. -- 0.17.7 - - Fixed: - - Fixed merging issues. - - Fixed button styling. - - Changed: - - Conflict checking on synchronising has been enabled for every note in default. -- 0.17.8 - - Improved: Performance improved. Prebuilt PouchDB is no longer used. - - Fixed: Merging hidden files is also fixed. - - New Feature: Now we can synchronise automatically after merging conflicts. -- 0.17.9 - - Fixed: Conflict merge of internal files is no longer broken. - - Improved: Smoother status display inside the editor. -- 0.17.10 - - Fixed: Large file synchronising has been now addressed! - Note: When synchronising large files, we have to set `Chunk size` to lower than 50, disable `Read chunks online`, `Batch size` should be set 50-100, and `Batch limit` could be around 20. -- 0.17.11 - - Fixed: - - Performance improvement - - Now `Chunk size` can be set to under one hundred. - - New feature: - - The number of transfers required before replication stabilises is now displayed. -- 0.17.12: Skipped. -- 0.17.13 - - Fixed: Document history is now displayed again. - - Reorganised: Many files have been refactored. -- 0.17.14: Skipped. -- 0.17.15 - - Improved: - - Confidential information has no longer stored in data.json as is. - - Synchronising progress has been shown in the notification. - - We can commit passphrases with a keyboard. - - Configuration which had not been saved yet is marked now. - - Now the filename is shown on the Conflict resolving dialog - - Fixed: - - Hidden files have been synchronised again. - - Rename of files has been fixed again. - And, minor changes have been included. -- 0.17.16: - - Improved: - - Plugins and their settings no longer need scanning if changes are monitored. - - Now synchronising plugins and their settings are performed parallelly and faster. - - We can place `redflag2.md` to rebuild the database automatically while the boot sequence. - - Experimental: - - We can use a new adapter on PouchDB. This will make us smoother. - - Note: Not compatible with the older version. - - Fixed: - - The default batch size is smaller again. - - Plugins and their setting can be synchronised again. - - Hidden files and plugins are correctly scanned while rebuilding. - - Files with the name started `_` are also being performed conflict-checking. -- 0.17.17 - - Fixed: Now we can merge JSON files even if we failed to compare items like null. -- 0.17.18 - - Fixed: Fixed lack of error handling. -- 0.17.19 - - Fixed: Error reporting has been ensured. -- 0.17.20 - - Improved: Changes of hidden files will be notified to Obsidian. -- 0.17.21 - - Fixed: Skip patterns now handle capital letters. - - Improved - - New configuration to avoid exceeding throttle capacity. - - We have been grateful to @karasevm! - - The conflicted `data.json` is no longer merged automatically. - - This behaviour is not configurable, unlike the `Use newer file if conflicted` of normal files. -- 0.17.22 - - Fixed: - - Now hidden files will not be synchronised while we are not configured. - - Some processes could start without waiting for synchronisation to complete, but now they will wait for. - - Improved - - Now, by placing `redflag3.md`, we can discard the local database and fetch again. - - The document has been updated! Thanks to @hilsonp! -- 0.17.23 - - Improved: - - Now we can preserve the logs into the file. - - Note: This option will be enabled automatically also when we flagging a red flag. - - File names can now be made platform-appropriate. - - Refactored: - - Some redundant implementations have been sorted out. -- 0.17.24 - - New feature: - - If any conflicted files have been left, they will be reported. - - Fixed: - - Now the name of the conflicting file is shown on the conflict-resolving dialogue. - - Hidden files are now able to be merged again. - - No longer error caused at plug-in being loaded. - - Improved: - - Caching chunks are now limited in total size of cached chunks. -- 0.17.25 - - Fixed: - - Now reading error will be reported. -- 0.17.26 - - Fixed(Urgent): - - The modified document will be reflected in the storage now. -- 0.17.27 - - Improved: - - Now, the filename of the conflicted settings will be shown on the merging dialogue - - The plugin data can be resolved when conflicted. - - The semaphore status display has been changed to count only. - - Applying to the storage will be concurrent with a few files. -- 0.17.28 - -Fixed: - - Some messages have been refined. - - Boot sequence has been speeded up. - - Opening the local database multiple times in a short duration has been suppressed. - - Older migration logic. - - Note: If you have used 0.10.0 or lower and have not upgraded, you will need to run 0.17.27 or earlier once or reinstall Obsidian. -- 0.17.29 - - Fixed: - - Requests of reading chunks online are now split into a reasonable(and configurable) size. - - No longer error message will be shown on Linux devices with hidden file synchronisation. - - Improved: - - The interval of reading chunks online is now configurable. - - Boot sequence has been speeded up, more. - - Misc: - - Messages on the boot sequence will now be more detailed. If you want to see them, please enable the verbose log. - - Logs became be kept for 1000 lines while the verbose log is enabled. -- 0.17.30 - - Implemented: - - `Resolve all conflicted files` has been implemented. - - Fixed: - - Fixed a problem about reading chunks online when a file has more chunks than the concurrency limit. - - Rollbacked: - - Logs are kept only for 100 lines, again. -- 0.17.31 - - Fixed: - - Now `redflag3` can be run surely. - - Synchronisation can now be aborted. - - Note: The synchronisation flow has been rewritten drastically. Please do not haste to inform me if you have noticed anything. -- 0.17.32 - - Fixed: - - Now periodic internal file scanning works well. - - The handler of Window-visibility-changed has been fixed. - - And minor fixes possibly included. - - Refactored: - - Unused logic has been removed. - - Some utility functions have been moved into suitable files. - - Function names have been renamed. -- 0.17.33 - - Maintenance update: Refactored; the responsibilities that `LocalDatabase` had were shared. (Hoping) No changes in behaviour. -- 0.17.34 - - Fixed: The `Fetch` that was broken at 0.17.33 has been fixed. - - Refactored again: Internal file sync, plug-in sync and Set up URI have been moved into each file. - -### 0.16.0 - -- Now hidden files need not be scanned. Changes will be detected automatically. - - If you want it to back to its previous behaviour, please disable `Monitor changes to internal files`. - - Due to using an internal API, this feature may become unusable with a major update. If this happens, please disable this once. - -#### Minors - -- 0.16.1 Added missing log updates. -- 0.16.2 Fixed many problems caused by combinations of `Sync On Save` and the tracking logic that changed at 0.15.6. -- 0.16.3 - - Fixed detection of IBM Cloudant (And if there are some issues, be fixed automatically). - - A configuration information reporting tool has been implemented. -- 0.16.4 Fixed detection failure. Please set the `Chunk size` again when using a self-hosted database. -- 0.16.5 - - Fixed - - Conflict detection and merging now be able to treat deleted files. - - Logs while the boot-up sequence has been tidied up. - - Fixed incorrect log entries. - - New Feature - - The feature of automatically deleting old expired metadata has been implemented. - We can configure it in `Delete old metadata of deleted files on start-up` in the `General Settings` pane. -- 0.16.6 - - Fixed - - Automatic (temporary) batch size adjustment has been restored to work correctly. - - Chunk splitting has been backed to the previous behaviour for saving them correctly. - - Improved - - Corrupted chunks will be detected automatically. - - Now on the case-insensitive system, `aaa.md` and `AAA.md` will be treated as the same file or path at applying changesets. -- 0.16.7 Nothing has been changed except toolsets, framework library, and as like them. Please inform me if something had been getting strange! -- 0.16.8 Now we can synchronise without `bad_request:invalid UTF-8 JSON` even while end-to-end encryption has been disabled. - -Note: -Before 0.16.5, LiveSync had some issues making chunks. In this case, synchronisation had became been always failing after a corrupted one should be made. After 0.16.6, the corrupted chunk is automatically detected. Sorry for troubling you but please do `rebuild everything` when this plug-in notified so. - -### 0.15.0 - -- Outdated configuration items have been removed. -- Setup wizard has been implemented! - -I appreciate for reviewing and giving me advice @Pouhon158! - -#### Minors - -- 0.15.1 Missed the stylesheet. -- 0.15.2 The wizard has been improved and documented! -- 0.15.3 Fixed the issue about locking/unlocking remote database while rebuilding in the wizard. -- 0.15.4 Fixed issues about asynchronous processing (e.g., Conflict check or hidden file detection) -- 0.15.5 Add new features for setting Self-hosted LiveSync up more easier. -- 0.15.6 File tracking logic has been refined. -- 0.15.7 Fixed bug about renaming file. -- 0.15.8 Fixed bug about deleting empty directory, weird behaviour on boot-sequence on mobile devices. -- 0.15.9 Improved chunk retrieving, now chunks are retrieved in batch on continuous requests. -- 0.15.10 Fixed: - - The boot sequence has been corrected and now boots smoothly. - - Auto applying of batch save will be processed earlier than before. - -### 0.14.1 - -- The target selecting filter was implemented. - Now we can set what files are synchronised by regular expression. -- We can configure the size of chunks. - We can use larger chunks to improve performance. - (This feature can not be used with IBM Cloudant) -- Read chunks online. - Now we can synchronise only metadata and retrieve chunks on demand. It reduces local database size and time for replication. -- Added this note. -- Use local chunks in preference to remote them if present, - -#### Recommended configuration for Self-hosted CouchDB - -- Set chunk size to around 100 to 250 (10MB - 25MB per chunk) -- _Set batch size to 100 and batch limit to 20 (0.14.2)_ -- Be sure to `Read chunks online` checked. - -#### Minors - -- 0.14.2 Fixed issue about retrieving files if synchronisation has been interrupted or failed -- 0.14.3 New test items have been added to `Check database configuration`. -- 0.14.4 Fixed issue of importing configurations. -- 0.14.5 Auto chunk size adjusting implemented. -- 0.14.6 Change Target to ES2018 -- 0.14.7 Refactor and fix typos. -- 0.14.8 Refactored again. There should be no change in behaviour, but please let me know if there is any. - -### 0.13.0 - -- The metadata of the deleted files will be kept on the database by default. If you want to delete this as the previous version, please turn on `Delete metadata of deleted files.`. And, if you have upgraded from the older version, please ensure every device has been upgraded. -- Please turn on `Delete metadata of deleted files.` if you are using livesync-classroom or filesystem-livesync. -- We can see the history of deleted files. -- `Pick file to show` was renamed to `Pick a file to show. -- Files in the `Pick a file to show` are now ordered by their modified date descent. -- Update information became to be shown on the major upgrade. - -#### Minors - -- 0.13.1 Fixed on conflict resolution. -- 0.13.2 Fixed file deletion failures. -- 0.13.4 - - Now, we can synchronise hidden files that conflicted on each devices. - - We can search for conflicting docs. - - Pending processes can now be run at any time. - - Performance improved on synchronising large numbers of files at once. +This compatibility page remains at its previous path so existing links continue to work. diff --git a/utils/bench/splitPiecesRabinKarp.ts b/utils/bench/splitPiecesRabinKarp.ts index 1c4642c7..d5ee4e5c 100644 --- a/utils/bench/splitPiecesRabinKarp.ts +++ b/utils/bench/splitPiecesRabinKarp.ts @@ -2,15 +2,19 @@ import { glob } from "glob"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { promises as fs } from "node:fs"; -import { isPlainText, shouldSplitAsPlainText } from "../../src/lib/src/string_and_binary/path"; -import { splitPiecesRabinKarp } from "../../src/lib/src/string_and_binary/chunks"; +import { isPlainText, shouldSplitAsPlainText } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/path"; +import { splitPiecesRabinKarp } from "@vrtmrz/livesync-commonlib/compat/string_and_binary/chunks"; import { PREFERRED_BASE, PREFERRED_JOURNAL_SYNC, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, -} from "../../src/lib/src/common/models/setting.const.preferred"; -import { type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, MAX_DOC_SIZE_BIN } from "../../src/lib/src/common/types"; +} from "@vrtmrz/livesync-commonlib/settings"; +import { + type ObsidianLiveSyncSettings, + DEFAULT_SETTINGS, + MAX_DOC_SIZE_BIN, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; async function blobFromString(content: string): Promise { return new Blob([content], { type: "text/plain" }); diff --git a/utils/commonlib-package-boundary.unit.spec.ts b/utils/commonlib-package-boundary.unit.spec.ts new file mode 100644 index 00000000..dc254759 --- /dev/null +++ b/utils/commonlib-package-boundary.unit.spec.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +type PackageJson = Partial< + Record<"dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies", Record> +>; + +const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as PackageJson; + +describe("Commonlib package ownership", () => { + it("does not retain a host-owned Trystero dependency", () => { + const directDependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + ...packageJson.optionalDependencies, + ...packageJson.peerDependencies, + }; + expect(directDependencies).not.toHaveProperty("@trystero-p2p/nostr"); + }); +}); diff --git a/utils/couchdb/couchdb-init.sh b/utils/couchdb/couchdb-init.sh index 323c13f3..3e41c3f8 100755 --- a/utils/couchdb/couchdb-init.sh +++ b/utils/couchdb/couchdb-init.sh @@ -1,33 +1,26 @@ #!/bin/bash -if [[ -z "$hostname" ]]; then - echo "ERROR: Hostname missing" - exit 1 -fi -if [[ -z "$username" ]]; then - echo "ERROR: Username missing" +set -euo pipefail + +if ! command -v deno >/dev/null 2>&1; then + echo "ERROR: Deno is required to run the Commonlib-backed CouchDB provisioning tool." >&2 exit 1 fi -if [[ -z "$password" ]]; then - echo "ERROR: Password missing" - exit 1 -fi -if [[ -z "$node" ]]; then - echo "INFO: defaulting to _local" - node=_local +script_url="${provision_script_url:-https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/couchdb/provision.ts}" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || true)" +deno_dependency_options=() +if [[ -n "$script_dir" && -f "$script_dir/provision.ts" ]]; then + script_url="$script_dir/provision.ts" + lockfile="$script_dir/../flyio/deno.lock" + deno_config="$script_dir/../flyio/deno.jsonc" + if [[ -f "$lockfile" && -f "$deno_config" ]]; then + deno_dependency_options+=("--config=$deno_config" --frozen "--lock=$lockfile") + fi fi -echo "-- Configuring CouchDB by REST APIs... -->" - -until (curl -X POST "${hostname}/_cluster_setup" -H "Content-Type: application/json" -d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd_auth/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/httpd/WWW-Authenticate" -H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/httpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/max_http_request_size" -H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/couchdb/max_document_size" -H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/cors/credentials" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done -until (curl -X PUT "${hostname}/_node/${node}/_config/cors/origins" -H "Content-Type: application/json" -d '"app://obsidian.md,capacitor://localhost,http://localhost"' --user "${username}:${password}"); do sleep 5; done - -echo "<-- Configuring CouchDB by REST APIs Done!" +exec deno run \ + --minimum-dependency-age=0 \ + "${deno_dependency_options[@]}" \ + --allow-env \ + --allow-net \ + "$script_url" diff --git a/utils/couchdb/livesync-commonlib.ts b/utils/couchdb/livesync-commonlib.ts new file mode 100644 index 00000000..b1a3a30a --- /dev/null +++ b/utils/couchdb/livesync-commonlib.ts @@ -0,0 +1,5 @@ +// Keep CouchDB database-version negotiation isolated from Setup URI generation. +// The exact release must match utils/livesync-commonlib-version.ts; the setup +// tool suite checks every static specifier before release. +export { checkRemoteVersion } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/negotiation"; +export { PouchDB } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/pouchdb-browser"; diff --git a/utils/couchdb/provision.test.ts b/utils/couchdb/provision.test.ts new file mode 100644 index 00000000..c4942d16 --- /dev/null +++ b/utils/couchdb/provision.test.ts @@ -0,0 +1,83 @@ +import { provisionCouchDB } from "./provision.ts"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("configures CouchDB and delegates database-version initialisation", async () => { + const requests: Array<{ url: string; method: string; body: string }> = []; + const initialisations: Array<[string, string, string]> = []; + await provisionCouchDB( + { + hostname: "https://couch.example.test/", + username: "alice", + password: "secret", + database: "notes", + retryCount: 1, + retryDelayMs: 0, + }, + { + fetch: async (input, init) => { + requests.push({ + url: String(input), + method: init?.method ?? "GET", + body: String(init?.body ?? ""), + }); + return new Response("{}", { status: 201 }); + }, + sleep: async () => {}, + initialiseDatabaseVersion: async (...args) => { + initialisations.push(args); + }, + }, + ); + + assert( + requests[0].url.endsWith("/_cluster_setup"), + "cluster setup was not first", + ); + assert( + requests.some((request) => + request.url.endsWith("/_config/cors/origins") && + request.body.includes("app://obsidian.md") + ), + "the Obsidian CORS origins were not configured", + ); + assert( + requests.at(-1)?.url === "https://couch.example.test/notes", + "the requested database was not created last", + ); + assert( + initialisations.length === 1, + "database-version initialisation was not delegated once", + ); + assert( + initialisations[0][0] === "https://couch.example.test/notes", + "database-version initialisation used the wrong URL", + ); +}); + +Deno.test("leaves database creation to the client when no database is supplied", async () => { + let initialised = false; + await provisionCouchDB( + { + hostname: "http://127.0.0.1:5984", + username: "admin", + password: "secret", + retryCount: 1, + retryDelayMs: 0, + }, + { + fetch: async () => new Response("{}", { status: 200 }), + sleep: async () => {}, + initialiseDatabaseVersion: async () => { + initialised = true; + }, + }, + ); + + assert( + !initialised, + "database-version initialisation ran without a database", + ); +}); diff --git a/utils/couchdb/provision.ts b/utils/couchdb/provision.ts new file mode 100644 index 00000000..21b6283b --- /dev/null +++ b/utils/couchdb/provision.ts @@ -0,0 +1,249 @@ +import { checkRemoteVersion, PouchDB } from "./livesync-commonlib.ts"; + +export interface CouchDBProvisioningOptions { + hostname: string; + username: string; + password: string; + node?: string; + database?: string; + origins?: string; + retryCount?: number; + retryDelayMs?: number; +} + +interface ProvisioningDependencies { + fetch: typeof fetch; + sleep: (milliseconds: number) => Promise; + initialiseDatabaseVersion: ( + databaseURL: string, + username: string, + password: string, + ) => Promise; +} + +const DEFAULT_ORIGINS = + "app://obsidian.md,capacitor://localhost,http://localhost"; + +function requireValue(value: string, name: string): string { + const trimmed = value.trim(); + if (!trimmed) throw new Error(`${name} is required`); + return trimmed; +} + +function normaliseHostname(hostname: string): string { + const parsed = new URL(requireValue(hostname, "hostname")); + parsed.pathname = parsed.pathname.replace(/\/+$/, ""); + parsed.search = ""; + parsed.hash = ""; + return parsed.href.replace(/\/$/, ""); +} + +function validateDatabaseName(database: string): string { + const trimmed = database.trim(); + if (!/^[a-z][a-z0-9_$()+-]*$/.test(trimmed)) { + throw new Error( + "database must begin with a lower-case letter and contain only lower-case letters, digits, _, $, (, ), +, or -", + ); + } + return trimmed; +} + +function basicAuthorisation(username: string, password: string): string { + return `Basic ${btoa(`${username}:${password}`)}`; +} + +async function requestWithRetry( + dependencies: ProvisioningDependencies, + label: string, + url: string, + init: RequestInit, + accept: (response: Response, body: string) => boolean, + retryCount: number, + retryDelayMs: number, +): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= retryCount; attempt++) { + try { + const response = await dependencies.fetch(url, init); + const body = await response.text(); + if (accept(response, body)) return; + const error = new Error( + `${label} failed with HTTP ${response.status}: ${body}`, + ); + if (response.status < 500) throw error; + lastError = error; + } catch (error) { + lastError = error; + if ( + error instanceof Error && + error.message.startsWith(`${label} failed with HTTP 4`) + ) { + throw error; + } + } + if (attempt < retryCount) await dependencies.sleep(retryDelayMs); + } + throw lastError instanceof Error + ? lastError + : new Error(`${label} failed after ${retryCount} attempts`); +} + +export async function initialiseLiveSyncDatabaseVersion( + databaseURL: string, + username: string, + password: string, +): Promise { + const database = new PouchDB(databaseURL, { + adapter: "http", + auth: { username, password }, + skip_setup: true, + }); + try { + const compatible = await checkRemoteVersion( + database, + async () => false, + ); + if (!compatible) { + throw new Error( + "the remote database uses an incompatible LiveSync database version", + ); + } + } finally { + await database.close(); + } +} + +export async function provisionCouchDB( + options: CouchDBProvisioningOptions, + overrides: Partial = {}, +): Promise { + const hostname = normaliseHostname(options.hostname); + const username = requireValue(options.username, "username"); + const password = requireValue(options.password, "password"); + const node = encodeURIComponent(options.node?.trim() || "_local"); + const origins = options.origins?.trim() || DEFAULT_ORIGINS; + const retryCount = options.retryCount ?? 12; + const retryDelayMs = options.retryDelayMs ?? 5_000; + if (!Number.isInteger(retryCount) || retryCount < 1) { + throw new Error("retryCount must be a positive integer"); + } + if (!Number.isFinite(retryDelayMs) || retryDelayMs < 0) { + throw new Error("retryDelayMs must be zero or greater"); + } + + const dependencies: ProvisioningDependencies = { + fetch, + sleep: (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds)), + initialiseDatabaseVersion: initialiseLiveSyncDatabaseVersion, + ...overrides, + }; + const headers = { + "Content-Type": "application/json", + Authorization: basicAuthorisation(username, password), + }; + const configure = async ( + label: string, + path: string, + body: string, + method = "PUT", + accept: (response: Response, body: string) => boolean = (response) => + response.ok, + ) => + await requestWithRetry( + dependencies, + label, + `${hostname}${path}`, + { method, headers, body }, + accept, + retryCount, + retryDelayMs, + ); + + await configure( + "single-node cluster setup", + "/_cluster_setup", + JSON.stringify({ + action: "enable_single_node", + username, + password, + bind_address: "0.0.0.0", + port: 5984, + singlenode: true, + }), + "POST", + (response, body) => + response.ok || + ((response.status === 400 || response.status === 409) && + /already|finished/i.test(body)), + ); + + const settings: Array<[string, string, string]> = [ + ["require authenticated HTTP users", "chttpd/require_valid_user", '"true"'], + [ + "require authenticated HTTP users for authentication", + "chttpd_auth/require_valid_user", + '"true"', + ], + [ + "set the HTTP authentication challenge", + "httpd/WWW-Authenticate", + '"Basic realm=\\"couchdb\\""', + ], + ["enable HTTP CORS", "httpd/enable_cors", '"true"'], + ["enable clustered HTTP CORS", "chttpd/enable_cors", '"true"'], + [ + "set the maximum HTTP request size", + "chttpd/max_http_request_size", + '"4294967296"', + ], + [ + "set the maximum document size", + "couchdb/max_document_size", + '"50000000"', + ], + ["enable CORS credentials", "cors/credentials", '"true"'], + ["set allowed CORS origins", "cors/origins", JSON.stringify(origins)], + ]; + for (const [label, key, body] of settings) { + await configure(label, `/_node/${node}/_config/${key}`, body); + } + + if (options.database?.trim()) { + const database = validateDatabaseName(options.database); + const databaseURL = `${hostname}/${encodeURIComponent(database)}`; + await requestWithRetry( + dependencies, + "create database", + databaseURL, + { method: "PUT", headers }, + (response) => response.ok || response.status === 412, + retryCount, + retryDelayMs, + ); + await dependencies.initialiseDatabaseVersion( + databaseURL, + username, + password, + ); + } +} + +function optionalNumber(name: string): number | undefined { + const value = Deno.env.get(name)?.trim(); + return value ? Number(value) : undefined; +} + +if (import.meta.main) { + await provisionCouchDB({ + hostname: Deno.env.get("hostname") ?? "", + username: Deno.env.get("username") ?? "", + password: Deno.env.get("password") ?? "", + node: Deno.env.get("node"), + database: Deno.env.get("database"), + origins: Deno.env.get("origins"), + retryCount: optionalNumber("retry_count"), + retryDelayMs: optionalNumber("retry_delay_ms"), + }); + console.log("CouchDB provisioning completed."); +} diff --git a/utils/flyio/deno.lock b/utils/flyio/deno.lock index 30d3c5eb..7ca3f1f5 100644 --- a/utils/flyio/deno.lock +++ b/utils/flyio/deno.lock @@ -1,25 +1,855 @@ { - "version": "4", + "version": "5", "specifiers": { - "npm:octagonal-wheels@0.1.11": "0.1.11" + "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4": "0.1.0-rc.4" }, "npm": { - "idb@8.0.0": { - "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" - }, - "octagonal-wheels@0.1.11": { - "integrity": "sha512-KsXfpziFHmlLEBe5VAXFz9OyyjJEEdSg7xxASqdzmbe5oo9dhcOeWGrQfyipRJwHAhlFkI4vEf8JCSgkcyRxYg==", + "@aws-sdk/checksums@3.1000.18": { + "integrity": "sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==", "dependencies": [ - "idb", - "xxhash-wasm@0.4.2", - "xxhash-wasm-102@npm:xxhash-wasm@1.0.2" + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" ] }, - "xxhash-wasm@0.4.2": { - "integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==" + "@aws-sdk/client-s3@3.1090.0": { + "integrity": "sha512-R6GX9cd1jljwzZ8xFmgAI/hHCuX1MobIKBdsymv7WL9SENvO9Vgz9KOR6avTnu0Ao+w1LmxnTe+jqmZXEn7Q/Q==", + "dependencies": [ + "@aws-sdk/checksums", + "@aws-sdk/core", + "@aws-sdk/credential-provider-node", + "@aws-sdk/middleware-sdk-s3", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] }, - "xxhash-wasm@1.0.2": { - "integrity": "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==" + "@aws-sdk/core@3.975.3": { + "integrity": "sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==", + "dependencies": [ + "@aws-sdk/types", + "@aws-sdk/xml-builder", + "@aws/lambda-invoke-store", + "@smithy/core", + "@smithy/signature-v4", + "@smithy/types", + "bowser", + "tslib" + ] + }, + "@aws-sdk/credential-provider-env@3.972.59": { + "integrity": "sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-http@3.972.61": { + "integrity": "sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-ini@3.973.4": { + "integrity": "sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-login", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-login@3.972.66": { + "integrity": "sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-node@3.972.70": { + "integrity": "sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==", + "dependencies": [ + "@aws-sdk/credential-provider-env", + "@aws-sdk/credential-provider-http", + "@aws-sdk/credential-provider-ini", + "@aws-sdk/credential-provider-process", + "@aws-sdk/credential-provider-sso", + "@aws-sdk/credential-provider-web-identity", + "@aws-sdk/types", + "@smithy/core", + "@smithy/credential-provider-imds", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-process@3.972.59": { + "integrity": "sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-sso@3.973.3": { + "integrity": "sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/token-providers", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/credential-provider-web-identity@3.972.65": { + "integrity": "sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/middleware-sdk-s3@3.972.64": { + "integrity": "sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/nested-clients@3.997.33": { + "integrity": "sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/signature-v4-multi-region", + "@aws-sdk/types", + "@smithy/core", + "@smithy/fetch-http-handler", + "@smithy/node-http-handler", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/signature-v4-multi-region@3.996.41": { + "integrity": "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==", + "dependencies": [ + "@aws-sdk/types", + "@smithy/signature-v4", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/token-providers@3.1088.0": { + "integrity": "sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==", + "dependencies": [ + "@aws-sdk/core", + "@aws-sdk/nested-clients", + "@aws-sdk/types", + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/types@3.974.2": { + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@aws-sdk/xml-builder@3.972.36": { + "integrity": "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@aws/lambda-invoke-store@0.3.0": { + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==" + }, + "@noble/secp256k1@3.1.0": { + "integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==" + }, + "@smithy/core@3.29.5": { + "integrity": "sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==", + "dependencies": [ + "@smithy/types", + "tslib" + ] + }, + "@smithy/credential-provider-imds@4.4.10": { + "integrity": "sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/fetch-http-handler@5.6.7": { + "integrity": "sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/md5-js@4.4.10": { + "integrity": "sha512-XI5xhWxRkWuiLNj0/Z30vLTPpZe9UQcg57Ox/n4vzGmFiHSrD+xAmx6ubEcWt6u69IOH+4LAucpaZk5AMxB8oQ==", + "dependencies": [ + "@smithy/core", + "tslib" + ] + }, + "@smithy/middleware-apply-body-checksum@4.5.10": { + "integrity": "sha512-1qFwlILFq+QnI1oqZIBpLda1W6oCkI667GPTyAn0eRan72ddQ/zA1CoKiIezTHLQKxINpPvDKVwsw/znLmKEbg==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/node-http-handler@4.9.7": { + "integrity": "sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/signature-v4@5.6.6": { + "integrity": "sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ==", + "dependencies": [ + "@smithy/core", + "@smithy/types", + "tslib" + ] + }, + "@smithy/types@4.16.1": { + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", + "dependencies": [ + "tslib" + ] + }, + "@smithy/util-retry@4.5.10": { + "integrity": "sha512-hYu5ieq8myuO29xCQV0IIhicRm1aO0lcNBeMcF1mVVAVQC0oylPGKFliMq5NbxtdOvRxJWCMuir2gwKI91f1lw==", + "dependencies": [ + "@smithy/core", + "tslib" + ] + }, + "@trystero-p2p/core@0.25.3": { + "integrity": "sha512-lQKNq/ha+vF6kQZrpaXJGzlzxLF/Fhizoy5dwJUYQlkqRrbPup/Jov2EpPX/CPr2X+f79HxVzonkeni3MxmCuQ==" + }, + "@trystero-p2p/nostr@0.25.3": { + "integrity": "sha512-nZV9Fl/GXuhIkJSQ+wIkdMoRLO1oSTUL/+vZorukaojeAdOpT/61e8b9Vzbt8L1ktMTaGPEJHw2BG/B5BCwBuw==", + "dependencies": [ + "@noble/secp256k1", + "@trystero-p2p/core" + ] + }, + "@vrtmrz/livesync-commonlib@0.1.0-rc.4": { + "integrity": "sha512-u4FdbjnYg7lAf38z7eUv4eq4vxEdrl4rFMxiDZiJ7T701awKiflkXGIJQnaHdLaMa3zkRBU15qqEo7GtextmlA==", + "dependencies": [ + "@aws-sdk/client-s3", + "@smithy/fetch-http-handler", + "@smithy/md5-js", + "@smithy/middleware-apply-body-checksum", + "@smithy/types", + "@smithy/util-retry", + "@trystero-p2p/nostr", + "diff-match-patch", + "events", + "fflate", + "idb", + "markdown-it", + "minimatch", + "octagonal-wheels", + "pouchdb-adapter-http", + "pouchdb-adapter-idb", + "pouchdb-adapter-indexeddb", + "pouchdb-adapter-memory", + "pouchdb-core", + "pouchdb-errors", + "pouchdb-find", + "pouchdb-mapreduce", + "pouchdb-merge", + "pouchdb-replication", + "pouchdb-utils", + "qrcode-generator", + "transform-pouch", + "xxhash-wasm-102@npm:xxhash-wasm@1.1.0" + ] + }, + "abstract-leveldown@2.7.2": { + "integrity": "sha512-+OVvxH2rHVEhWLdbudP6p0+dNMXu8JA1CbhP19T8paTYAcX7oJ4OVjT+ZUVpv7mITxXHqDMej+GdqXBmXkw09w==", + "dependencies": [ + "xtend" + ], + "deprecated": true + }, + "abstract-leveldown@6.2.3": { + "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "dependencies": [ + "buffer", + "immediate", + "level-concat-iterator", + "level-supports", + "xtend" + ], + "deprecated": true + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "balanced-match@4.0.4": { + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bowser@2.14.1": { + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + }, + "brace-expansion@5.0.7": { + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dependencies": [ + "balanced-match" + ] + }, + "buffer@5.7.1": { + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dependencies": [ + "base64-js", + "ieee754" + ] + }, + "core-util-is@1.0.3": { + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "deferred-leveldown@5.3.0": { + "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "dependencies": [ + "abstract-leveldown@6.2.3", + "inherits" + ], + "deprecated": true + }, + "diff-match-patch@1.0.5": { + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, + "double-ended-queue@2.1.0-0": { + "integrity": "sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==" + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "errno@0.1.8": { + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dependencies": [ + "prr" + ], + "bin": true + }, + "events@3.3.0": { + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fetch-cookie@2.2.0": { + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "dependencies": [ + "set-cookie-parser", + "tough-cookie" + ] + }, + "fflate@0.8.3": { + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==" + }, + "functional-red-black-tree@1.0.1": { + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==" + }, + "idb@8.0.3": { + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==" + }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "immediate@3.3.0": { + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "isarray@0.0.1": { + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "level-codec@9.0.2": { + "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "dependencies": [ + "buffer" + ], + "deprecated": true + }, + "level-concat-iterator@2.0.1": { + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "deprecated": true + }, + "level-errors@2.0.1": { + "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "dependencies": [ + "errno" + ], + "deprecated": true + }, + "level-iterator-stream@4.0.2": { + "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "dependencies": [ + "inherits", + "readable-stream@3.6.2", + "xtend" + ] + }, + "level-supports@1.0.1": { + "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "dependencies": [ + "xtend" + ] + }, + "levelup@4.4.0": { + "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "dependencies": [ + "deferred-leveldown", + "level-errors", + "level-iterator-stream", + "level-supports", + "xtend" + ], + "deprecated": true + }, + "linkify-it@5.0.2": { + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dependencies": [ + "uc.micro" + ] + }, + "ltgt@2.2.1": { + "integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==" + }, + "markdown-it@14.3.0": { + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dependencies": [ + "argparse", + "entities", + "linkify-it", + "mdurl", + "punycode.js", + "uc.micro" + ], + "bin": true + }, + "mdurl@2.0.0": { + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "memdown@1.4.1": { + "integrity": "sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w==", + "dependencies": [ + "abstract-leveldown@2.7.2", + "functional-red-black-tree", + "immediate", + "inherits", + "ltgt", + "safe-buffer@5.1.2" + ], + "deprecated": true + }, + "minimatch@10.2.5": { + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dependencies": [ + "brace-expansion" + ] + }, + "node-fetch@2.6.9": { + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dependencies": [ + "whatwg-url" + ] + }, + "octagonal-wheels@0.1.51": { + "integrity": "sha512-KTlfqKPjobHJg/t3A539srnFf+VHr1aXkHSmsNDDpiI5UFC7FamZ95dWpJfGE2EI/HULR5hveQDgkazmz8SAcg==", + "dependencies": [ + "idb" + ] + }, + "pouchdb-abstract-mapreduce@9.0.0": { + "integrity": "sha512-SnTtqwAEiAa3uxKbc1J7LfiBViwEkKe2xkK92zxyTXPqWBvMnh4UU3GXxx7GrXTM4L9llsQ3lSjpbH4CNqG1Mw==", + "dependencies": [ + "pouchdb-binary-utils", + "pouchdb-collate", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-mapreduce-utils", + "pouchdb-md5", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-http@9.0.0": { + "integrity": "sha512-2eL008XeRZkdyp3hMHHOhdIPqK9H6Mn4SLlQvit4zCbqnOFfAswzPjUmHULGMbDUCrQBTu6y82FnV6NHXv9kgw==", + "dependencies": [ + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-idb@9.0.0": { + "integrity": "sha512-2oLlgwMyOQwdKuzrEmOv8T7jFVgX7JgT4Cr81zX3eiiRClp7xXGgjv41ZRdVCAbM530sIN8BudafaQRVFKRVmA==", + "dependencies": [ + "pouchdb-adapter-utils", + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-json", + "pouchdb-merge", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-indexeddb@9.0.0": { + "integrity": "sha512-/mcCbnVR0VKwtVZWKf8lVSdADLD0yApjFudu4d+0jeLWAeBSGZBRKYlogz2PGs4uTA7GVc2TXjVCNGUdkCM9ZQ==", + "dependencies": [ + "pouchdb-adapter-utils", + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-md5", + "pouchdb-merge", + "pouchdb-utils" + ] + }, + "pouchdb-adapter-leveldb-core@9.0.0": { + "integrity": "sha512-b3ZGPtVXyivGL5SK3AIDG7PrNsZdoDpGFkmTytDTtctkVhxOg71gnXXP+CrupENPqSNG/eGbKW4w+bbMpxy6aA==", + "dependencies": [ + "double-ended-queue", + "levelup", + "pouchdb-adapter-utils", + "pouchdb-binary-utils", + "pouchdb-core", + "pouchdb-errors", + "pouchdb-json", + "pouchdb-md5", + "pouchdb-merge", + "pouchdb-utils", + "sublevel-pouchdb", + "through2" + ] + }, + "pouchdb-adapter-memory@9.0.0": { + "integrity": "sha512-XbCwJ5f5U9dGdkiDikzYjTebdPHuA6Ghylx1Pq0lDe4y6l8R9xhjDSUy56pJ8G2F4Z+8QdB5FBY9EQoFlFSXWQ==", + "dependencies": [ + "memdown", + "pouchdb-adapter-leveldb-core" + ] + }, + "pouchdb-adapter-utils@9.0.0": { + "integrity": "sha512-hmbm4ey0HL0vtoY1tRTPIt2FfYjvMh3DWoGGSxXDTS73qTFQ+Fhhi5I0AnN9PcD2omfKQAVXiYks4kkMvlAHqA==", + "dependencies": [ + "pouchdb-binary-utils", + "pouchdb-errors", + "pouchdb-md5", + "pouchdb-merge", + "pouchdb-utils" + ] + }, + "pouchdb-binary-utils@9.0.0": { + "integrity": "sha512-2OMtgDZi82vqs+zNDE0YiYjOaWkYCUcZJZKK3WkRr+XYRu+2B7umJrnygJFhUwoGedBbHSrlQBLhdNV3F1AX1A==" + }, + "pouchdb-changes-filter@9.0.0": { + "integrity": "sha512-ig0fo0WLgIjAniFJ19Uw1Y+oxiypqC+Skhd8BCETRVXOhLBzueRwEQR4thffyo0UayYVqldJfSR5wHSDvEVk/A==", + "dependencies": [ + "pouchdb-errors", + "pouchdb-selector-core", + "pouchdb-utils" + ] + }, + "pouchdb-checkpointer@9.0.0": { + "integrity": "sha512-yu1OlWw78oTHKOkg1GoxxF2qB7YUsjK3rUDJOChMs/sVlZwOTZ4mGdWFPBr3udxSGvR77E+g89kpdmAWhPpHvA==", + "dependencies": [ + "pouchdb-collate", + "pouchdb-utils" + ] + }, + "pouchdb-collate@9.0.0": { + "integrity": "sha512-TrnEDNZEmIIl+W3xKUO8h+geqVLQ90oZe5ujPkl8myUzpREULWXWQBnV5EzPXVEKDBpJlb8T3I6oy/zdWGQpdA==" + }, + "pouchdb-core@9.0.0": { + "integrity": "sha512-98SJgs8bqXhr4gMGuOTR8yVeLlMYy797zlOtdlvlXIxIicvocyA8ColhVVhdBXPNOGxT2HwReIMywdIVAgibpg==", + "dependencies": [ + "pouchdb-changes-filter", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-merge", + "pouchdb-utils", + "uuid" + ] + }, + "pouchdb-errors@9.0.0": { + "integrity": "sha512-961PSMLhW0UqqdJ566g+CdLZ5pkBJRd6l4WWpCDdD0USvE4xYfYGzv43w7nZZBw1k3Xdy092yqPge7yX/tfnyw==" + }, + "pouchdb-fetch@9.0.0": { + "integrity": "sha512-TbE3cUcAJQrwb9kr44tDP0X+NAbcqgjsTvcL30L4xzBNJeCPTIRjukYX80s154SHJUXBxcWRiPsMmNqpXsjfCA==", + "dependencies": [ + "fetch-cookie", + "node-fetch" + ] + }, + "pouchdb-find@9.0.0": { + "integrity": "sha512-vvVhq4eEOmSkwSRwf2NBYtdhURB7ryJ7sUI4WDN00GuLUj2g8jAXBJuZIryVgdYt/5S5cfn70iRL6Eow+LFhpA==", + "dependencies": [ + "pouchdb-abstract-mapreduce", + "pouchdb-collate", + "pouchdb-errors", + "pouchdb-fetch", + "pouchdb-md5", + "pouchdb-selector-core", + "pouchdb-utils" + ] + }, + "pouchdb-generate-replication-id@9.0.0": { + "integrity": "sha512-wetxjU0W/qNYtfHIoKwBO73ddUr0/eqzYOkoKHSFXCgOzYmTglDeqXiVY9LPysRXTgaHUJPKC5LoknZZw7e+Dw==", + "dependencies": [ + "pouchdb-collate", + "pouchdb-md5" + ] + }, + "pouchdb-json@9.0.0": { + "integrity": "sha512-aI41mYVyI195GXuT1Ys7mLIB/Mvrz11ihoTP6km6hYqVgSuaUxuZcFUozlyTJiZXr7H5kdhNgclhlVnjir4JAA==", + "dependencies": [ + "vuvuzela" + ] + }, + "pouchdb-mapreduce-utils@9.0.0": { + "integrity": "sha512-Bjh8W6QXqp1j7MKmHhYYp5cYlcQsm5drD8Jd/F+ZlfNt18uiD2SQXWzGM5797+tiW/LszFGb8ttw0uHWjxufCQ==", + "dependencies": [ + "pouchdb-utils" + ] + }, + "pouchdb-mapreduce@9.0.0": { + "integrity": "sha512-ZD8PleQ9atzQAzT2LZWsvooUVEfsen5QGv/SDfci20IleCaFW2A2q7OERrqY0YWKDCCNRsWhPWPmsFvZC9K8DQ==", + "dependencies": [ + "pouchdb-abstract-mapreduce", + "pouchdb-mapreduce-utils", + "pouchdb-utils" + ] + }, + "pouchdb-md5@9.0.0": { + "integrity": "sha512-58xUYBvW3/s+aH0j4uOhhN8yCk0LQ254cxBzI/gbKA9PrfwHpe4zrr0L/ia5ml3A30oH1f8aTnuVMwWDkFcuww==", + "dependencies": [ + "pouchdb-binary-utils", + "spark-md5" + ] + }, + "pouchdb-merge@9.0.0": { + "integrity": "sha512-Xh+TgOZCkGoZpI589btKf/cTiuQ5CsnPl9YpdW4h0cAPusniN6XNsR62F+/HbL9wirI6XTEPHUrk7MsQbk3S3A==", + "dependencies": [ + "pouchdb-utils" + ] + }, + "pouchdb-replication@9.0.0": { + "integrity": "sha512-EZ68KJ3ZUWuPe35NxP6WnRw8J6Zudf0j/tZ/6mOSrCcp3EbtBNt8Ke2FaAThUgiFahVnHD5Y8nd53EGs2DLygg==", + "dependencies": [ + "pouchdb-checkpointer", + "pouchdb-errors", + "pouchdb-generate-replication-id", + "pouchdb-utils" + ] + }, + "pouchdb-selector-core@9.0.0": { + "integrity": "sha512-ZYHYsdoedwm8j5tYofz+3+uUSK8i+7tRCBb01T0OuqDQb17+w5mzjHF8Ppi160xdPUPaWCo1Un+nLWGJzkmA3g==", + "dependencies": [ + "pouchdb-collate", + "pouchdb-utils" + ] + }, + "pouchdb-utils@9.0.0": { + "integrity": "sha512-xWZE5c+nAslgmLC8JBZbky8AYgdz7pKtv7KTSi6CD2tuQD0WyNKib0YnhZndeE84dksTeZlqlg56RQHsHoB2LQ==", + "dependencies": [ + "pouchdb-errors", + "pouchdb-md5", + "uuid" + ] + }, + "pouchdb-wrappers@5.0.0": { + "integrity": "sha512-fXqsVn+rmlPtxaAIGaQP5TkiaT39OMwvMk+ScLLtHrmfXD2KBO6fe/qBl38N/rpTn0h/A058dPN4fLAHt550zA==" + }, + "prr@1.0.1": { + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" + }, + "psl@1.15.0": { + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": [ + "punycode" + ] + }, + "punycode.js@2.3.1": { + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" + }, + "punycode@2.3.1": { + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "qrcode-generator@1.5.2": { + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==" + }, + "querystringify@2.2.0": { + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "readable-stream@1.1.14": { + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": [ + "core-util-is", + "inherits", + "isarray", + "string_decoder@0.10.31" + ] + }, + "readable-stream@3.6.2": { + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": [ + "inherits", + "string_decoder@1.3.0", + "util-deprecate" + ] + }, + "requires-port@1.0.0": { + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safe-buffer@5.1.2": { + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-buffer@5.2.1": { + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "set-cookie-parser@2.7.2": { + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "spark-md5@3.0.2": { + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==" + }, + "string_decoder@0.10.31": { + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "string_decoder@1.3.0": { + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": [ + "safe-buffer@5.2.1" + ] + }, + "sublevel-pouchdb@9.0.0": { + "integrity": "sha512-pX4r8+F7wuts0C81kUJ341h4bl2aRe7qV572FE8X1FMz9VkKlmi2nPD1vfeiOJXz5Y09I4MHjGULAbqvTfQZEQ==", + "dependencies": [ + "level-codec", + "ltgt", + "readable-stream@1.1.14" + ] + }, + "through2@3.0.2": { + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dependencies": [ + "inherits", + "readable-stream@3.6.2" + ] + }, + "tough-cookie@4.1.4": { + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": [ + "psl", + "punycode", + "universalify", + "url-parse" + ] + }, + "tr46@0.0.3": { + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "transform-pouch@2.0.0": { + "integrity": "sha512-nDZovo0U5o0UdMNL93fMQgGjrwH9h4F/a7qqRTnF6cVA+FfgyXiJPTrSuD+LmWSO7r2deZt0P0oeCD8hkgxl5g==", + "dependencies": [ + "pouchdb-wrappers" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "uc.micro@2.1.0": { + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "universalify@0.2.0": { + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" + }, + "url-parse@1.5.10": { + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": [ + "querystringify", + "requires-port" + ] + }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": true, + "bin": true + }, + "vuvuzela@1.0.3": { + "integrity": "sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ==" + }, + "webidl-conversions@3.0.1": { + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url@5.0.0": { + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": [ + "tr46", + "webidl-conversions" + ] + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "xxhash-wasm@1.1.0": { + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==" } } } diff --git a/utils/flyio/deploy-server.sh b/utils/flyio/deploy-server.sh index 967c1760..9c59329c 100755 --- a/utils/flyio/deploy-server.sh +++ b/utils/flyio/deploy-server.sh @@ -1,8 +1,15 @@ #!/bin/bash ## Script for deploy and automatic setup CouchDB onto fly.io. -## We need Deno for generating the Setup-URI. +## Deno is used for Commonlib-backed provisioning and Setup URI generation. -source setenv.sh $@ +set -euo pipefail + +if ! command -v deno >/dev/null 2>&1; then + echo "ERROR: Deno 2 is required for CouchDB provisioning and Setup URI generation." >&2 + exit 1 +fi + +source setenv.sh "$@" export hostname="https://$appname.fly.dev" @@ -14,30 +21,16 @@ echo "region : $region" echo "" echo "-- START DEPLOYING --> " -set -e fly launch --name=$appname --env="COUCHDB_USER=$username" --copy-config=true --detach --no-deploy --region ${region} --yes fly secrets set COUCHDB_PASSWORD=$password fly deploy -set +e ../couchdb/couchdb-init.sh -# flyctl deploy echo "OK!" -if command -v deno >/dev/null 2>&1; then - echo "Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri." - echo "Passphrase of setup-uri will be printed only one time. Keep it safe!" - echo "--- configured ---" - echo "database : ${database}" - echo "E2EE passphrase: ${passphrase}" - echo "--- setup uri ---" - deno run -A generate_setupuri.ts -else - echo "Setup finished! Here is the configured values (reprise)!" - echo "-- YOUR CONFIGURATION --" - echo "URL : $hostname" - echo "username: $username" - echo "password: $password" - echo "-- YOUR CONFIGURATION --" - echo "If we had Deno, we would got the setup uri directly!" -fi +echo "Setup finished. The Commonlib-generated Setup URI follows." +echo "Its passphrase is printed only once, so store it safely." +echo "--- configured ---" +echo "database: ${database}" +echo "--- setup URI ---" +deno run --minimum-dependency-age=0 --allow-env generate_setupuri.ts diff --git a/utils/flyio/generate_setupuri.test.ts b/utils/flyio/generate_setupuri.test.ts new file mode 100644 index 00000000..ec36f369 --- /dev/null +++ b/utils/flyio/generate_setupuri.test.ts @@ -0,0 +1,72 @@ +import { + decodeSettingsFromSetupURI, + DEFAULT_SETTINGS, +} from "../setup/livesync-commonlib.ts"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("generates a current self-hosted Setup URI through the published Commonlib contract", async () => { + const scriptPath = new URL("./generate_setupuri.ts", import.meta.url); + const command = new Deno.Command(Deno.execPath(), { + args: ["run", "-A", scriptPath.pathname], + env: { + hostname: "https://couch.example.test", + username: "alice", + password: "couch-secret", + database: "notes", + passphrase: "vault-secret", + uri_passphrase: "setup-secret", + }, + stdout: "piped", + stderr: "piped", + }); + + const result = await command.output(); + const stdout = new TextDecoder().decode(result.stdout); + const stderr = new TextDecoder().decode(result.stderr); + assert(result.success, `generator failed:\n${stdout}\n${stderr}`); + + const setupURI = stdout.match(/obsidian:\/\/setuplivesync\?settings=\S+/) + ?.[0]; + assert(setupURI, `generator did not print a Setup URI:\n${stdout}`); + + const decoded = await decodeSettingsFromSetupURI(setupURI, "setup-secret"); + assert(decoded, "Commonlib could not decode the generated Setup URI"); + const effectiveSettings = { ...DEFAULT_SETTINGS, ...decoded }; + assert( + effectiveSettings.isConfigured, + "the CouchDB Setup URI left the imported device unconfigured", + ); + assert( + effectiveSettings.customChunkSize === 60, + "the Setup URI did not use the current self-hosted chunk-size recommendation", + ); + assert( + effectiveSettings.chunkSplitterVersion === "v3-rabin-karp", + "the Setup URI did not use the current chunk splitter", + ); + assert( + effectiveSettings.E2EEAlgorithm === "v2", + "the Setup URI did not use the current E2EE algorithm", + ); + assert( + !Object.hasOwn(decoded, "doNotUseFixedRevisionForChunks"), + "the Setup URI serialised the obsolete fixed-revision compatibility setting", + ); + + const profiles = Object.values(decoded.remoteConfigurations ?? {}); + assert( + profiles.length === 1, + "the Setup URI did not contain exactly one CouchDB remote profile", + ); + assert( + decoded.activeConfigurationId === profiles[0].id, + "the CouchDB remote profile was not selected", + ); + assert( + profiles[0].uri.startsWith("sls+https://"), + "the selected remote profile was not a CouchDB connection URI", + ); +}); diff --git a/utils/flyio/generate_setupuri.ts b/utils/flyio/generate_setupuri.ts index 390c2e1c..5df0e7d6 100644 --- a/utils/flyio/generate_setupuri.ts +++ b/utils/flyio/generate_setupuri.ts @@ -1,175 +1,6 @@ -import { encrypt } from "npm:octagonal-wheels@0.1.30/encryption/encryption"; +import { runSetupURIGenerator } from "../setup/generate_setup_uri.ts"; -const noun = [ - "waterfall", - "river", - "breeze", - "moon", - "rain", - "wind", - "sea", - "morning", - "snow", - "lake", - "sunset", - "pine", - "shadow", - "leaf", - "dawn", - "glitter", - "forest", - "hill", - "cloud", - "meadow", - "sun", - "glade", - "bird", - "brook", - "butterfly", - "bush", - "dew", - "dust", - "field", - "fire", - "flower", - "firefly", - "feather", - "grass", - "haze", - "mountain", - "night", - "pond", - "darkness", - "snowflake", - "silence", - "sound", - "sky", - "shape", - "surf", - "thunder", - "violet", - "water", - "wildflower", - "wave", - "water", - "resonance", - "sun", - "log", - "dream", - "cherry", - "tree", - "fog", - "frost", - "voice", - "paper", - "frog", - "smoke", - "star", -]; -const adjectives = [ - "autumn", - "hidden", - "bitter", - "misty", - "silent", - "empty", - "dry", - "dark", - "summer", - "icy", - "delicate", - "quiet", - "white", - "cool", - "spring", - "winter", - "patient", - "twilight", - "dawn", - "crimson", - "wispy", - "weathered", - "blue", - "billowing", - "broken", - "cold", - "damp", - "falling", - "frosty", - "green", - "long", - "late", - "lingering", - "bold", - "little", - "morning", - "muddy", - "old", - "red", - "rough", - "still", - "small", - "sparkling", - "thrumming", - "shy", - "wandering", - "withered", - "wild", - "black", - "young", - "holy", - "solitary", - "fragrant", - "aged", - "snowy", - "proud", - "floral", - "restless", - "divine", - "polished", - "ancient", - "purple", - "lively", - "nameless", -]; -function friendlyString() { - return `${adjectives[Math.floor(Math.random() * adjectives.length)]}-${noun[Math.floor(Math.random() * noun.length)]}`; -} - -const uri_passphrase = `${Deno.env.get("uri_passphrase") ?? friendlyString()}`; - -const URIBASE = "obsidian://setuplivesync?settings="; -async function main() { - const conf = { - couchDB_URI: `${Deno.env.get("hostname")}`, - couchDB_USER: `${Deno.env.get("username")}`, - couchDB_PASSWORD: `${Deno.env.get("password")}`, - couchDB_DBNAME: `${Deno.env.get("database")}`, - syncOnStart: true, - gcDelay: 0, - periodicReplication: true, - syncOnFileOpen: true, - encrypt: true, - passphrase: `${Deno.env.get("passphrase")}`, - usePathObfuscation: true, - batchSave: true, - batch_size: 50, - batches_limit: 50, - useHistory: true, - disableRequestURI: true, - customChunkSize: 50, - syncAfterMerge: false, - concurrencyOfReadChunksOnline: 100, - minimumIntervalOfReadChunksOnline: 100, - handleFilenameCaseSensitive: false, - doNotUseFixedRevisionForChunks: false, - settingVersion: 10, - notifyThresholdOfRemoteStorageSize: 800, - }; - const encryptedConf = encodeURIComponent(await encrypt(JSON.stringify(conf), uri_passphrase, false)); - const theURI = `${URIBASE}${encryptedConf}`; - console.log("\nYour passphrase of Setup-URI is: ", uri_passphrase); - console.log("This passphrase is never shown again, so please note it in a safe place."); - console.log(theURI); -} -await main(); +await runSetupURIGenerator({ + ...Deno.env.toObject(), + remote_type: "couchdb", +}); diff --git a/utils/flyio/setenv.sh b/utils/flyio/setenv.sh index 1c1f4c1f..50e2dff3 100755 --- a/utils/flyio/setenv.sh +++ b/utils/flyio/setenv.sh @@ -1,19 +1,23 @@ random_num() { - echo $RANDOM + echo "$RANDOM" } random_noun() { nouns=("waterfall" "river" "breeze" "moon" "rain" "wind" "sea" "morning" "snow" "lake" "sunset" "pine" "shadow" "leaf" "dawn" "glitter" "forest" "hill" "cloud" "meadow" "sun" "glade" "bird" "brook" "butterfly" "bush" "dew" "dust" "field" "fire" "flower" "firefly" "feather" "grass" "haze" "mountain" "night" "pond" "darkness" "snowflake" "silence" "sound" "sky" "shape" "surf" "thunder" "violet" "water" "wildflower" "wave" "water" "resonance" "sun" "log" "dream" "cherry" "tree" "fog" "frost" "voice" "paper" "frog" "smoke" "star") - echo ${nouns[$(($RANDOM % ${#nouns[*]}))]} + echo "${nouns[$((RANDOM % ${#nouns[*]}))]}" } random_adjective() { adjectives=("autumn" "hidden" "bitter" "misty" "silent" "empty" "dry" "dark" "summer" "icy" "delicate" "quiet" "white" "cool" "spring" "winter" "patient" "twilight" "dawn" "crimson" "wispy" "weathered" "blue" "billowing" "broken" "cold" "damp" "falling" "frosty" "green" "long" "late" "lingering" "bold" "little" "morning" "muddy" "old" "red" "rough" "still" "small" "sparkling" "thrumming" "shy" "wandering" "withered" "wild" "black" "young" "holy" "solitary" "fragrant" "aged" "snowy" "proud" "floral" "restless" "divine" "polished" "ancient" "purple" "lively" "nameless") - echo ${adjectives[$(($RANDOM % ${#adjectives[*]}))]} + echo "${adjectives[$((RANDOM % ${#adjectives[*]}))]}" +} + +random_secret() { + deno eval 'const bytes=crypto.getRandomValues(new Uint8Array(24));console.log(btoa(String.fromCharCode(...bytes)).replaceAll("+","-").replaceAll("/","_").replace(/=+$/, ""));' } cp ./fly.template.toml ./fly.toml -if [ "$1" = "renew" ]; then +if [ "${1:-}" = "renew" ]; then unset appname unset username unset password @@ -22,9 +26,9 @@ if [ "$1" = "renew" ]; then unset region fi -[ -z $appname ] && export appname=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $username ] && export username=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $password ] && export password=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $database ] && export database="obsidiannotes" -[ -z $passphrase ] && export passphrase=$(random_adjective)-$(random_noun)-$(random_num) -[ -z $region ] && export region="nrt" +[ -z "${appname:-}" ] && export appname="$(random_adjective)-$(random_noun)-$(random_num)" +[ -z "${username:-}" ] && export username="$(random_adjective)-$(random_noun)-$(random_num)" +[ -z "${password:-}" ] && export password="$(random_secret)" +[ -z "${database:-}" ] && export database="obsidiannotes" +[ -z "${passphrase:-}" ] && export passphrase="$(random_secret)" +[ -z "${region:-}" ] && export region="nrt" diff --git a/utils/flyio/setenv.test.sh b/utils/flyio/setenv.test.sh new file mode 100644 index 00000000..82886754 --- /dev/null +++ b/utils/flyio/setenv.test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +fixture_dir="$(mktemp -d)" +trap 'rm -rf "$fixture_dir"' EXIT +cp "$(dirname "$0")/setenv.sh" "$(dirname "$0")/fly.template.toml" "$fixture_dir/" + +( + cd "$fixture_dir" + appname="" + username="" + password="" + database="" + passphrase="" + region="" + source ./setenv.sh keep + + [[ "$password" =~ ^[A-Za-z0-9_-]{32}$ ]] || { + echo "generated CouchDB password is not a 32-character base64url secret" >&2 + exit 1 + } + [[ "$passphrase" =~ ^[A-Za-z0-9_-]{32}$ ]] || { + echo "generated Vault encryption passphrase is not a 32-character base64url secret" >&2 + exit 1 + } +) diff --git a/utils/livesync-commonlib-version.test.ts b/utils/livesync-commonlib-version.test.ts new file mode 100644 index 00000000..8ffacdc3 --- /dev/null +++ b/utils/livesync-commonlib-version.test.ts @@ -0,0 +1,27 @@ +import { LIVESYNC_COMMONLIB_VERSION } from "./livesync-commonlib-version.ts"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("standalone utilities select one exact Commonlib registry version", async () => { + const facadeURLs = [ + new URL("./setup/livesync-commonlib.ts", import.meta.url), + new URL("./couchdb/livesync-commonlib.ts", import.meta.url), + ]; + const selectedVersions = ( + await Promise.all(facadeURLs.map((url) => Deno.readTextFile(url))) + ).flatMap((source) => + [...source.matchAll( + /npm:@vrtmrz\/livesync-commonlib@([^/"]+)\//gu, + )].map((match) => match[1]) + ); + + assert(selectedVersions.length > 0, "no Commonlib npm specifier was found"); + assert( + selectedVersions.every((version) => version === LIVESYNC_COMMONLIB_VERSION), + `Commonlib specifiers do not all select ${LIVESYNC_COMMONLIB_VERSION}: ${ + selectedVersions.join(", ") + }`, + ); +}); diff --git a/utils/livesync-commonlib-version.ts b/utils/livesync-commonlib-version.ts new file mode 100644 index 00000000..945f0039 --- /dev/null +++ b/utils/livesync-commonlib-version.ts @@ -0,0 +1,5 @@ +// The standalone Deno utilities deliberately remain pinned to one immutable +// Commonlib registry release. Static npm specifiers cannot interpolate this +// value, so livesync-commonlib-version.test.ts verifies the domain-specific +// facades against it. +export const LIVESYNC_COMMONLIB_VERSION = "0.1.0-rc.4"; diff --git a/utils/readme.md b/utils/readme.md index 6fe52ac9..df4c3c64 100644 --- a/utils/readme.md +++ b/utils/readme.md @@ -1,167 +1,84 @@ - # Utilities -Here are some useful things. -## couchdb +These utilities support self-hosted CouchDB provisioning and Setup URI generation. They consume an exact immutable `@vrtmrz/livesync-commonlib` registry version declared in `livesync-commonlib-version.ts`; the utility lockfile records its resolved package integrity. This selection is independent of the plug-in's runtime dependency and advances only when the utility behaviour is revalidated against a newer package. The setup-tool test keeps the domain-specific static npm specifiers aligned with that declaration. Update the declaration, the two facades, and the lockfile together when selecting a newer release. -### couchdb-init.sh -This script can configure CouchDB with the necessary settings by REST APIs. +## CouchDB provisioning -#### Materials -- Mandatory: curl +`couchdb/couchdb-init.sh` is a Bash wrapper for the Deno provisioning tool. Deno 2 is required. The tool configures single-node CouchDB, authenticated access, CORS for Obsidian, and the request and document size limits used by Self-hosted LiveSync. -#### Usage +Set `database` to create a database as part of provisioning. The tool then uses Commonlib's database negotiation contract to initialise and verify the LiveSync database version. If `database` is omitted, only the CouchDB server is configured and the first LiveSync client remains responsible for creating its database. ```sh -export hostname=http://localhost:5984/ +export hostname=http://localhost:5984 export username=couchdb-admin-username export password=couchdb-admin-password -./couchdb-init.sh +export database=obsidiannotes +./couchdb/couchdb-init.sh ``` -curl result will be shown, however, all of them can be ignored if the script has been run completely. +Optional variables are: -## fly.io +- `node`, which defaults to `_local`; +- `origins`, which defaults to the supported Obsidian desktop, mobile, and local origins; +- `retry_count`, which defaults to `12`; and +- `retry_delay_ms`, which defaults to `5000`. -### deploy-server.sh +Authentication and other non-retryable HTTP failures stop immediately. Network and server failures are retried within the configured bound. -A fully automated CouchDB deployment script. We can deploy CouchDB onto fly.io. The only we need is an account of it. +## Setup URI generation -All omitted configurations will be determined at random. (And, it is preferred). The region is configured to `nrt`. -If Japan is not close to you, please choose a region closer to you. However, the deployed database will work if you leave it at all. +`setup/generate_setup_uri.ts` creates current CouchDB, Object Storage, and P2P configurations from Commonlib's new-Vault and remote-specific presets. It stores the connection as the selected remote profile and encodes the result with Commonlib's Setup URI contract. Set `remote_type` to `couchdb`, `s3`, or `p2p`; CouchDB is the default. -#### Materials -- Mandatory: curl, flyctl -- Recommended: deno +The existing `flyio/generate_setupuri.ts` path remains a CouchDB-only compatibility wrapper for the Fly.io deployment script. + +### CouchDB -#### Usage ```sh -#export appname= -#export username= -#export password= -#export database= -#export passphrase= -export region=nrt #pick your nearest location +export hostname=https://couch.example.com +export username=couchdb-admin-username +export password=couchdb-admin-password +export database=obsidiannotes +export passphrase=a-strong-vault-encryption-passphrase +export uri_passphrase=a-separate-setup-uri-passphrase # Optional +deno run --minimum-dependency-age=0 --config=./flyio/deno.jsonc --frozen --lock=./flyio/deno.lock --allow-env ./setup/generate_setup_uri.ts +``` + +If `uri_passphrase` is omitted, the tool generates and prints a cryptographically random one. Store the Setup URI and its passphrase separately. The `passphrase` value protects synchronised Vault data and must also be stored safely. + +### Object Storage + +```sh +export remote_type=s3 +export endpoint=https://objects.example.com +export access_key= +export secret_key= +export bucket=vault-data +export region=auto +export bucket_prefix=team-a # Optional +export passphrase= +deno run --minimum-dependency-age=0 --config=./flyio/deno.jsonc --frozen --lock=./flyio/deno.lock --allow-env ./setup/generate_setup_uri.ts +``` + +Optional Object Storage variables are `use_custom_request_handler`, `force_path_style`, and `bucket_custom_headers`. + +### P2P + +```sh +export remote_type=p2p +export passphrase= +deno run --minimum-dependency-age=0 --config=./flyio/deno.jsonc --frozen --lock=./flyio/deno.lock --allow-env ./setup/generate_setup_uri.ts +``` + +If `p2p_room_id` or `p2p_passphrase` is omitted, the tool generates it with Commonlib's room-ID contract or cryptographically secure randomness. Optional variables are `p2p_relays`, `p2p_app_id`, `p2p_auto_start`, and `p2p_auto_broadcast`. Auto-start and auto-broadcast retain Commonlib's disabled defaults unless explicitly enabled. A peer name is deliberately absent because it identifies one device and must not be copied to another device through a Setup URI. + +## Fly.io deployment + +`flyio/deploy-server.sh` deploys CouchDB through `flyctl`, provisions the selected database through the tool above, and prints a Commonlib-generated Setup URI. Both `flyctl` and Deno 2 are required. + +```sh +export region=nrt # Choose a nearby Fly.io region. +cd flyio ./deploy-server.sh ``` -The result of this command is as follows. - -``` --- YOUR CONFIGURATION -- -URL : https://young-darkness-25342.fly.dev -username: billowing-cherry-22580 -password: misty-dew-13571 -region : nrt - --- START DEPLOYING --> -An existing fly.toml file was found -Using build strategies '[the "couchdb:latest" docker image]'. Remove [build] from fly.toml to force a rescan -Creating app in /home/vorotamoroz/dev/obsidian-livesync/utils/flyio -We're about to launch your app on Fly.io. Here's what you're getting: - -Organization: vorotamoroz (fly launch defaults to the personal org) -Name: young-darkness-25342 (specified on the command line) -Region: Tokyo, Japan (specified on the command line) -App Machines: shared-cpu-1x, 256MB RAM (specified on the command line) -Postgres: (not requested) -Redis: (not requested) - -Created app 'young-darkness-25342' in organization 'personal' -Admin URL: https://fly.io/apps/young-darkness-25342 -Hostname: young-darkness-25342.fly.dev -Wrote config file fly.toml -Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml -Platform: machines -✓ Configuration is valid -Your app is ready! Deploy with `flyctl deploy` -Secrets are staged for the first deployment -==> Verifying app config -Validating /home/vorotamoroz/dev/obsidian-livesync/utils/flyio/fly.toml -Platform: machines -✓ Configuration is valid ---> Verified app config -==> Building image -Searching for image 'couchdb:latest' remotely... -image found: img_ox20prk63084j1zq - -Watch your deployment at https://fly.io/apps/young-darkness-25342/monitoring - -Provisioning ips for young-darkness-25342 - Dedicated ipv6: 2a09:8280:1::37:fde9 - Shared ipv4: 66.241.124.163 - Add a dedicated ipv4 with: fly ips allocate-v4 - -Creating a 1 GB volume named 'couchdata' for process group 'app'. Use 'fly vol extend' to increase its size -This deployment will: - * create 1 "app" machine - -No machines in group app, launching a new machine - -WARNING The app is not listening on the expected address and will not be reachable by fly-proxy. -You can fix this by configuring your app to listen on the following addresses: - - 0.0.0.0:5984 -Found these processes inside the machine with open listening sockets: - PROCESS | ADDRESSES ------------------*--------------------------------------- - /.fly/hallpass | [fdaa:0:73b9:a7b:22e:3851:7f28:2]:22 - -Finished launching new machines - -NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling - -------- -Checking DNS configuration for young-darkness-25342.fly.dev - -Visit your newly deployed app at https://young-darkness-25342.fly.dev/ --- Configuring CouchDB by REST APIs... --> -curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to young-darkness-25342.fly.dev:443 -{"ok":true} -"" -"" -"" -"" -"" -"" -"" -"" -"" -<-- Configuring CouchDB by REST APIs Done! -OK! -Setup finished! Also, we can set up Self-hosted LiveSync instantly, by the following setup uri. -Passphrase of setup-uri will be printed only one time. Keep it safe! ---- configured --- -database : obsidiannotes -E2EE passphrase: dark-wildflower-26467 ---- setup uri --- -obsidian://setuplivesync?settings=%5B%22gZkBwjFbLqxbdSIbJymU%2FmTPBPAKUiHVGDRKYiNnKhW0auQeBgJOfvnxexZtMCn8sNiIUTAlxNaMGF2t%2BCEhpJoeCP%2FO%2BrwfN5LaNDQyky1Uf7E%2B64A5UWyjOYvZDOgq4iCKSdBAXp9oO%2BwKh4MQjUZ78vIVvJp8Mo6NWHfm5fkiWoAoddki1xBMvi%2BmmN%2FhZatQGcslVb9oyYWpZocduTl0a5Dv%2FQviGwlYQ%2F4NY0dVDIoOdvaYS%2FX4GhNAnLzyJKMXhPEJHo9FvR%2FEOBuwyfMdftV1SQUZ8YDCuiR3T7fh7Kn1c6OFgaFMpFm%2BWgIJ%2FZpmAyhZFpEcjpd7ty%2BN9kfd9gQsZM4%2BYyU9OwDd2DahVMBWkqoV12QIJ8OlJScHHdcUfMW5ex%2F4UZTWKNEHJsigITXBrtq11qGk3rBfHys8O0vY6sz%2FaYNM3iAOsR1aoZGyvwZm4O6VwtzK8edg0T15TL4O%2B7UajQgtCGxgKNYxb8EMOGeskv7NifYhjCWcveeTYOJzBhnIDyRbYaWbkAXQgHPBxzJRkkG%2FpBPfBBoJarj7wgjMvhLJ9xtL4FbP6sBNlr8jtAUCoq4L7LJcRNF4hlgvjJpL2BpFZMzkRNtUBcsRYR5J%2BM1X2buWi2BHncbSiRRDKEwNOQkc%2FmhMJjbAn%2F8eNKRuIICOLD5OvxD7FZNCJ0R%2BWzgrzcNV%22%2C%22ec7edc900516b4fcedb4c7cc01000000%22%2C%22fceb5fe54f6619ee266ed9a887634e07%22%5D - -Your passphrase of Setup-URI is: patient-haze -This passphrase is never shown again, so please note it in a safe place. -``` - -All we have to do is copy the setup-URI (`obsidian`://...`) and open it from Self-hosted LiveSync on Obsidian. - -If you did not install Deno, configurations will be printed again, instead of the setup-URI. In this case, we should configure it manually. - -### delete-server.sh - -The pair script of `deploy-server.sh`. We can delete the deployed server by this with fly.toml. - -#### Materials - -- Mandatory: flyctl, jq -- Recommended: none - -#### Usage -```sh -./delete-server.sh -``` - -``` -App 'young-darkness-25342 is going to be scaled according to this plan: - -1 machines for group 'app' on region 'nrt' of size 'shared-cpu-1x' -Executing scale plan - Destroyed e28667eec57158 group:app region:nrt size:shared-cpu-1x -Destroyed app young-darkness-25342 -``` \ No newline at end of file +Set `appname`, `username`, `password`, `database`, `passphrase`, or `region` before running the script to override its generated values. Use `delete-server.sh` to remove the Fly.io application described by the generated `fly.toml`. diff --git a/utils/release-notes.mjs b/utils/release-notes.mjs index 8c917ae6..ed2ef2f8 100644 --- a/utils/release-notes.mjs +++ b/utils/release-notes.mjs @@ -86,7 +86,8 @@ function prepare(version) { } // Keep a fresh empty Unreleased section above the newly dated release notes. - const releasedBody = `${unreleased.body.trim()}\n\n`; + const releaseSuffix = unreleased.end < markdown.length ? "\n\n" : "\n"; + const releasedBody = `${unreleased.body.trim()}${releaseSuffix}`; const releaseDate = process.env.RELEASE_DATE || formatReleaseDate(); const replacement = `## Unreleased\n\n## ${version}\n\n${releaseDate}\n\n${releasedBody}`; const nextMarkdown = markdown.slice(0, unreleased.headingStart) + replacement + markdown.slice(unreleased.end); diff --git a/utils/release-pr-body.mjs b/utils/release-pr-body.mjs new file mode 100644 index 00000000..18baa602 --- /dev/null +++ b/utils/release-pr-body.mjs @@ -0,0 +1,99 @@ +import { pathToFileURL } from "node:url"; + +/** + * Escape a value for use as inline Markdown code. + * + * Use a fence longer than any backtick run in the value so that branch names + * supplied by a caller cannot break the surrounding Markdown. + * + * @param {string} value + * @returns {string} + */ +function inlineCode(value) { + const backtickRuns = value.match(/`+/g) ?? []; + const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1)); + const fence = "`".repeat(fenceLength); + const padding = value.startsWith("`") || value.endsWith("`") ? " " : ""; + return `${fence}${padding}${value}${padding}${fence}`; +} + +/** + * Render the reader-facing checklist for a draft release pull request. + * + * The version decides whether the release commit is an immutable SemVer + * pre-release or a stable version staged through a GitHub pre-release. The + * base branch is included explicitly because integration previews can target + * a reviewed integration branch rather than `main`. + * + * @param {string} version + * @param {string} baseBranch + * @returns {string} + */ +export function renderReleasePrBody(version, baseBranch) { + const selectedVersion = version.trim(); + const selectedBaseBranch = baseBranch.trim(); + if (selectedVersion.length === 0) throw new Error("A release version is required."); + if (selectedBaseBranch.length === 0) throw new Error("A base branch is required."); + + const versionCode = inlineCode(selectedVersion); + const baseBranchCode = inlineCode(selectedBaseBranch); + const isPrerelease = selectedVersion.includes("-"); + const purpose = isPrerelease + ? `an immutable pre-release for BRAT validation without replacing the latest stable release` + : `a stable version which will first be staged as a GitHub pre-release for BRAT validation`; + const finaliseInstruction = isPrerelease + ? "Run the finalise release workflow with this PR's fixed head SHA and `prerelease=true`" + : "Run the finalise release workflow with this PR's fixed head SHA, `prerelease=true`, and `publish_cli=false`"; + const publicationInstruction = isPrerelease + ? "Publish the GitHub Release as a pre-release without replacing the latest stable release, while keeping this pull request in draft" + : "Publish the GitHub Release initially as a pre-release without replacing the latest stable release, while keeping this pull request in draft"; + const assetInstruction = isPrerelease + ? "Confirm the draft GitHub Release assets and the published CLI image, if selected" + : "Confirm the draft GitHub Release assets; keep stable CLI publication deferred until BRAT validation passes"; + const holdInstruction = isPrerelease + ? `Publishing and validating this pre-release does not unblock this pull request. Keep it in draft and unmerged, and leave ${baseBranchCode} unchanged.` + : `Publishing the GitHub pre-release does not unblock this pull request. Keep it in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation. Promotion remains on hold until the exact release commit has been integrated into the repository's default branch.`; + const completionInstructions = isPrerelease + ? [ + "- [ ] Keep this pre-release pull request unmerged; close it only through a separate maintainer action", + ] + : [ + `- [ ] After BRAT validation passes, mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`, + "- [ ] Integrate the exact release commit through the reviewed branch chain into the repository's default branch", + "- [ ] Confirm the default branch contains the exact release metadata, then remove the pre-release designation and make this 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", + ]; + + return [ + `This release pull request prepares Self-hosted LiveSync ${versionCode} from ${baseBranchCode} as ${purpose}.`, + "", + "> [!IMPORTANT]", + "> **Merge intentionally on hold**", + ">", + `> ${holdInstruction}`, + "", + "## Release checklist", + "", + "- [ ] Review and polish `updates.md`", + "- [ ] Confirm the release date", + "- [ ] Confirm `manifest.json`, `versions.json`, workspace package versions, and the locked Commonlib package version", + "- [ ] Confirm CI has passed", + `- [ ] ${finaliseInstruction}`, + `- [ ] ${assetInstruction}`, + `- [ ] ${publicationInstruction}`, + "- [ ] Validate the exact published release with BRAT", + ...completionInstructions, + "", + ].join("\n"); +} + +const invokedPath = process.argv[1]; +if (invokedPath !== undefined && pathToFileURL(invokedPath).href === import.meta.url) { + const [, , version, baseBranch] = process.argv; + try { + process.stdout.write(renderReleasePrBody(version ?? "", baseBranch ?? "")); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + } +} diff --git a/utils/release-process.unit.spec.ts b/utils/release-process.unit.spec.ts index 94c9c125..8f6edf90 100644 --- a/utils/release-process.unit.spec.ts +++ b/utils/release-process.unit.spec.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { renderReleasePrBody } from "./release-pr-body.mjs"; import { ensureTags } from "./release-tags.mjs"; const releaseNotesScript = fileURLToPath(new URL("./release-notes.mjs", import.meta.url)); @@ -13,6 +14,7 @@ const workspaceUpdateScript = fileURLToPath(new URL("../update-workspaces.mjs", const prepareReleaseWorkflow = fileURLToPath(new URL("../.github/workflows/prepare-release.yml", import.meta.url)); const finaliseReleaseWorkflow = fileURLToPath(new URL("../.github/workflows/finalise-release.yml", import.meta.url)); const releaseWorkflow = fileURLToPath(new URL("../.github/workflows/release.yml", import.meta.url)); +const cliDockerWorkflow = fileURLToPath(new URL("../.github/workflows/cli-docker.yml", import.meta.url)); const temporaryDirectories: string[] = []; afterEach(() => { @@ -41,6 +43,14 @@ function runNode(script: string, args: string[], cwd: string, env: Record = {}) { const tags = new Map(Object.entries(initialTags)); const git = (args: string[], allowMissing = false): string | undefined => { @@ -96,6 +106,24 @@ describe("release notes", () => { expect(validated.status, validated.stderr).toBe(0); }); + it("ends rotated notes with one newline when Unreleased is the final release section", () => { + const directory = createReleaseFixture(); + writeFileSync( + join(directory, "updates.md"), + "# 1.0\n\n## Unreleased\n\n### Fixed\n\n- Preserved file content.\n", + "utf8" + ); + + const prepared = runNode(releaseNotesScript, ["prepare", "1.0.0-beta.0"], directory, { + RELEASE_DATE: "22nd July, 2026", + }); + + expect(prepared.status, prepared.stderr).toBe(0); + expect(readFileSync(join(directory, "updates.md"), "utf8")).toBe( + "# 1.0\n\n## Unreleased\n\n## 1.0.0-beta.0\n\n22nd July, 2026\n\n### Fixed\n\n- Preserved file content.\n" + ); + }); + it("rejects an empty Unreleased section unless explicitly allowed", () => { const directory = createReleaseFixture(); writeFileSync( @@ -130,43 +158,114 @@ describe("release notes", () => { }); describe("release workflow", () => { - it("regenerates and stages fallback type definitions", () => { + it("uses the locked Commonlib package instead of generated fallback declarations", () => { const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); + const body = renderReleasePrBody("1.0.0-beta.0", "integration"); - expect(workflow).toContain("npm run build:lib:types"); - expect(workflow).toMatch(/git add[^\n]*_types/); + expect(workflow).not.toContain("npm run build:lib:types"); + expect(workflow).not.toMatch(/git add[^\n]*_types/); + expect(workflow).toMatch(/git add[^\n]*package-lock\.json/); + expect(body).toContain("locked Commonlib package version"); }); - it("installs Deno before post-processing fallback type definitions", () => { + it("reruns the version lifecycle when the integration branch already selects the release version", () => { const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); - const setupDeno = workflow.indexOf("denoland/setup-deno@v2"); - const buildTypes = workflow.indexOf("npm run build:lib:types"); - expect(setupDeno).toBeGreaterThan(-1); - expect(setupDeno).toBeLessThan(buildTypes); + expect(workflow).toContain('npm version "${VERSION}" --no-git-tag-version --allow-same-version'); }); - it("keeps the release PR in draft until BRAT validation", () => { + it("generates the release PR body from the selected version and base branch", () => { const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); - expect(workflow).toContain("Merge intentionally on hold"); - expect(workflow).toContain( - "Publish the GitHub Release as the latest stable release while keeping this pull request in draft" + expect(workflow).toContain('node utils/release-pr-body.mjs "${VERSION}" "${BASE_BRANCH}"'); + expect(workflow).not.toContain("leave \\`main\\`"); + expect(workflow).not.toContain("latest stable release"); + }); + + it("keeps an immutable pre-release out of its base branch after BRAT validation", () => { + const prerelease = renderReleasePrBody("1.0.0-rc.0", "common-library-package-boundary"); + + expect(prerelease).toContain("Merge intentionally on hold"); + expect(prerelease).toContain("Self-hosted LiveSync `1.0.0-rc.0`"); + expect(prerelease).toContain("leave `common-library-package-boundary` unchanged"); + expect(prerelease).toContain("prerelease=true"); + expect(prerelease).toContain( + "Publish the GitHub Release as a pre-release without replacing the latest stable release" ); - expect(workflow).toContain("Validate the published release with BRAT"); - expect(workflow).toContain("Mark this pull request ready and merge it with a merge commit"); + expect(prerelease).toContain("Validate the exact published release with BRAT"); + expect(prerelease).toContain("Keep this pre-release pull request unmerged"); + expect(prerelease).toContain("close it only through a separate maintainer action"); + expect(prerelease).not.toContain("Mark this pull request ready and merge it"); }); - it("explicitly dispatches publishing workflows after creating tags", () => { + it("publishes a stable version initially as a GitHub pre-release for BRAT validation", () => { + const stable = renderReleasePrBody("1.0.0", "main"); + + expect(stable).toContain("prerelease=true"); + expect(stable).toContain("publish_cli=false"); + expect(stable).toContain( + "Publish the GitHub Release initially as a pre-release without replacing the latest stable release" + ); + expect(stable).toContain( + "After BRAT validation passes, mark this pull request ready and merge it into `main` with a merge commit" + ); + expect(stable).toContain( + "Integrate the exact release commit through the reviewed branch chain into the repository's default branch" + ); + expect(stable).toContain( + "Confirm the default branch contains the exact release metadata, then remove the pre-release designation and make this exact release the latest stable release" + ); + expect(stable).toContain( + "Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate" + ); + expect(stable.indexOf("After BRAT validation passes")).toBeLessThan( + stable.indexOf("Integrate the exact release commit") + ); + expect(stable.indexOf("Integrate the exact release commit")).toBeLessThan( + stable.indexOf("Confirm the default branch contains the exact release metadata") + ); + expect(stable.indexOf("Confirm the default branch contains the exact release metadata")).toBeLessThan( + stable.indexOf("Create the stable CLI tag") + ); + expect(stable).not.toContain("prerelease=false"); + }); + + it("summarises immutable pre-releases separately from stable versions awaiting promotion", () => { + const workflow = readFileSync(finaliseReleaseWorkflow, "utf8"); + + expect(workflow).toContain('if [[ "${VERSION}" == *-* ]]; then'); + expect(workflow).toContain( + "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action." + ); + expect(workflow).toContain( + "After BRAT validation, merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch." + ); + expect(workflow).toContain( + "Only after the default branch contains the exact release metadata, remove the pre-release designation and make this exact release the latest stable release." + ); + expect(workflow).toContain( + 'if [[ "${VERSION}" != *-* && "${PRERELEASE}" == "true" && "${PUBLISH_CLI}" == "true" ]]; then' + ); + expect(workflow).toContain( + "A stable version staged as a pre-release must use publish_cli=false so that the CLI latest and major-minor image tags do not advance before BRAT validation." + ); + }); + + it("dispatches the selected plug-in and CLI workflows explicitly", () => { const workflow = readFileSync(finaliseReleaseWorkflow, "utf8"); expect(workflow).toContain("actions: write"); expect(workflow).toContain('node utils/release-tags.mjs ensure "${VERSION}" "${EXPECTED_HEAD_SHA}"'); expect(workflow).toContain('git push --atomic origin "refs/tags/${VERSION}" "refs/tags/${VERSION}-cli"'); expect(workflow).not.toContain("Tag already exists"); - expect(workflow).toContain("gh workflow run release.yml"); + expect(workflow).toContain("PUBLISH_CLI: ${{ inputs.publish_cli }}"); + expect(workflow).toContain('if [[ "${PUBLISH_CLI}" == "true" ]]; then'); expect(workflow).toContain("gh workflow run cli-docker.yml"); - expect(workflow).toContain("dry_run=false"); + expect(workflow).toContain('--ref "${VERSION}-cli"'); + expect(workflow).toContain("--field dry_run=false"); + expect(workflow).toContain("--field force=false"); + expect(workflow).toContain("gh workflow run release.yml"); + expect(workflow).toContain("explicitly dispatched the CLI container workflow"); }); it("publishes only by explicit dispatch and validates the selected release", () => { @@ -178,6 +277,29 @@ describe("release workflow", () => { expect(workflow).toContain('TAG_SHA="$(git rev-parse "refs/tags/${TAG}^{commit}")"'); expect(workflow).not.toContain("Get Version"); }); + + it("supports a pre-release plug-in without creating or publishing a CLI release", () => { + const workflow = readFileSync(finaliseReleaseWorkflow, "utf8"); + + expect(workflow).toContain("prerelease:"); + expect(workflow).toContain("publish_cli:"); + expect(workflow).toContain('--field prerelease="${PRERELEASE}"'); + expect(workflow).toContain("--plugin-only"); + }); + + it("does not attach an unsupported release archive", () => { + const workflow = readFileSync(releaseWorkflow, "utf8"); + + expect(workflow).not.toContain("zip -r"); + expect(workflow).not.toContain("${{ github.event.repository.name }}.zip"); + }); + + it("does not promote a pre-release CLI image to stable moving tags", () => { + const workflow = readFileSync(cliDockerWorkflow, "utf8"); + + expect(workflow).toContain('if [[ "${VERSION}" == *-* ]]; then'); + expect(workflow).toContain('TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli"'); + }); }); describe("release tags", () => { @@ -205,6 +327,16 @@ describe("release tags", () => { ); expect(tags.has("0.25.84")).toBe(false); }); + + it("can create only the plug-in tag for a review release", () => { + const head = "a".repeat(40); + const { git, tags } = createTagGit(head); + + ensureTags("1.0.0-rc.0", head, git, () => undefined, { pluginOnly: true }); + + expect(tags.get("1.0.0-rc.0")).toBe(head); + expect(tags.has("1.0.0-rc.0-cli")).toBe(false); + }); }); describe("version bump", () => { @@ -225,6 +357,59 @@ describe("version bump", () => { "0.25.81": "1.7.2", }); }); + + it("runs release metadata scripts when the selected version is already the package version", () => { + const directory = makeTemporaryDirectory(); + const workspaces = ["src/apps/cli", "src/apps/webpeer", "src/apps/webapp"]; + writeJson(directory, "package.json", { + name: "release-lifecycle-fixture", + version: "1.0.0-beta.0", + private: true, + workspaces, + scripts: { + version: `node ${JSON.stringify(versionBumpScript)} && node ${JSON.stringify(workspaceUpdateScript)}`, + }, + }); + writeJson(directory, "manifest.json", { version: "1.0.0-alpha.9", minAppVersion: "1.7.2" }); + writeJson(directory, "versions.json", { "0.25.83": "1.7.2" }); + const lockPackages: Record = { + "": { version: "1.0.0-beta.0", workspaces }, + }; + for (const workspace of ["cli", "webpeer", "webapp"]) { + writeJson(directory, `src/apps/${workspace}/package.json`, { + name: `release-lifecycle-${workspace}`, + version: `1.0.0-alpha.9-${workspace}`, + }); + lockPackages[`src/apps/${workspace}`] = { version: `1.0.0-alpha.9-${workspace}` }; + } + writeJson(directory, "package-lock.json", { + name: "release-lifecycle-fixture", + version: "1.0.0-beta.0", + lockfileVersion: 3, + requires: true, + packages: lockPackages, + }); + + const result = runNpm(["version", "1.0.0-beta.0", "--no-git-tag-version", "--allow-same-version"], directory); + + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(readFileSync(join(directory, "manifest.json"), "utf8"))).toMatchObject({ + version: "1.0.0-beta.0", + minAppVersion: "1.7.2", + }); + expect(JSON.parse(readFileSync(join(directory, "versions.json"), "utf8"))).toEqual({ + "0.25.83": "1.7.2", + "1.0.0-beta.0": "1.7.2", + }); + const packageLock = JSON.parse(readFileSync(join(directory, "package-lock.json"), "utf8")); + expect(packageLock.version).toBe("1.0.0-beta.0"); + expect(packageLock.packages[""].version).toBe("1.0.0-beta.0"); + for (const workspace of ["cli", "webpeer", "webapp"]) { + const packageJson = JSON.parse(readFileSync(join(directory, `src/apps/${workspace}/package.json`), "utf8")); + expect(packageJson.version).toBe(`1.0.0-beta.0-${workspace}`); + expect(packageLock.packages[`src/apps/${workspace}`].version).toBe(`1.0.0-beta.0-${workspace}`); + } + }); }); describe("workspace version update", () => { diff --git a/utils/release-tags.mjs b/utils/release-tags.mjs index 739e75e0..62e4fc5d 100644 --- a/utils/release-tags.mjs +++ b/utils/release-tags.mjs @@ -36,10 +36,10 @@ function resolveTag(tag, runGit) { return runGit(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}^{commit}`], true); } -export function ensureTags(version, expectedRevision, runGit = git, log = console.log) { +export function ensureTags(version, expectedRevision, runGit = git, log = console.log, options = {}) { assertVersion(version); const expectedCommit = resolveCommit(expectedRevision, runGit); - const tags = [version, `${version}-cli`]; + const tags = options.pluginOnly ? [version] : [version, `${version}-cli`]; const existing = tags.map((tag) => ({ tag, commit: resolveTag(tag, runGit) })); for (const { tag, commit } of existing) { @@ -60,13 +60,13 @@ export function ensureTags(version, expectedRevision, runGit = git, log = consol const isMain = process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href; if (isMain) { - const [command, version, expectedRevision] = process.argv.slice(2); - if (command !== "ensure" || !version || !expectedRevision) { - fail("Usage: node utils/release-tags.mjs ensure "); + const [command, version, expectedRevision, mode] = process.argv.slice(2); + if (command !== "ensure" || !version || !expectedRevision || (mode && mode !== "--plugin-only")) { + fail("Usage: node utils/release-tags.mjs ensure [--plugin-only]"); } try { - ensureTags(version, expectedRevision); + ensureTags(version, expectedRevision, git, console.log, { pluginOnly: mode === "--plugin-only" }); } catch (error) { fail(error instanceof Error ? error.message : String(error)); } diff --git a/utils/setup/generate_setup_uri.test.ts b/utils/setup/generate_setup_uri.test.ts new file mode 100644 index 00000000..725725bf --- /dev/null +++ b/utils/setup/generate_setup_uri.test.ts @@ -0,0 +1,123 @@ +import { + decodeSettingsFromSetupURI, + DEFAULT_SETTINGS, +} from "./livesync-commonlib.ts"; +import { generateSetupURI } from "./generate_setup_uri.ts"; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("generates an Object Storage Setup URI with a selected S3 profile", async () => { + const generated = await generateSetupURI({ + remote_type: "s3", + endpoint: "https://objects.example.test", + access_key: "access-key", + secret_key: "secret-key", + bucket: "vault-data", + region: "auto", + bucket_prefix: "team-a", + passphrase: "vault-secret", + uri_passphrase: "setup-secret", + }); + const decoded = await decodeSettingsFromSetupURI( + generated.setupURI, + generated.setupPassphrase, + ); + assert(decoded, "Commonlib could not decode the Object Storage Setup URI"); + const effective = { ...DEFAULT_SETTINGS, ...decoded }; + assert( + effective.isConfigured, + "the Setup URI left the imported device unconfigured", + ); + assert( + effective.customChunkSize === 10, + "the journal chunk-size preset was not applied", + ); + assert( + effective.liveSync, + "Object Storage was not configured for live journal synchronisation", + ); + assert( + effective.endpoint === "https://objects.example.test", + "the endpoint was not preserved", + ); + assert( + effective.bucketPrefix === "team-a", + "the bucket prefix was not preserved", + ); + + const profiles = Object.values(decoded.remoteConfigurations ?? {}); + assert( + profiles.length === 1, + "the Setup URI did not contain exactly one Object Storage profile", + ); + assert( + decoded.activeConfigurationId === profiles[0].id, + "the Object Storage profile was not selected", + ); + assert( + profiles[0].uri.startsWith("sls+s3://"), + "the selected profile was not an S3 connection URI", + ); +}); + +Deno.test("generates a random-room P2P Setup URI without copying a device identity", async () => { + const generated = await generateSetupURI({ + remote_type: "p2p", + passphrase: "vault-secret", + uri_passphrase: "setup-secret", + }); + const decoded = await decodeSettingsFromSetupURI( + generated.setupURI, + generated.setupPassphrase, + ); + assert(decoded, "Commonlib could not decode the P2P Setup URI"); + const effective = { ...DEFAULT_SETTINGS, ...decoded }; + assert( + /^\d{3}-\d{3}-\d{3}-[a-z0-9]{3}$/.test(effective.P2P_roomID), + "Commonlib did not generate the expected random room ID", + ); + assert( + /^[A-Za-z0-9_-]{32}$/.test(effective.P2P_passphrase), + "the generated P2P passphrase was not a 32-character base64url secret", + ); + assert( + !effective.P2P_AutoStart, + "P2P auto-start was enabled without an explicit request", + ); + assert( + !effective.P2P_AutoBroadcast, + "P2P auto-broadcast was enabled without an explicit request", + ); + assert( + effective.customChunkSize === 0, + "the P2P profile inherited the self-hosted CouchDB chunk-size recommendation", + ); + assert( + effective.sendChunksBulkMaxSize === 1, + "the P2P profile did not retain the conservative manual resend size", + ); + assert( + !Object.hasOwn(decoded, "P2P_DevicePeerName"), + "the Setup URI copied a device-specific P2P peer name", + ); + + const profiles = Object.values(decoded.remoteConfigurations ?? {}); + assert( + profiles.length === 1, + "the Setup URI did not contain exactly one P2P profile", + ); + assert( + decoded.activeConfigurationId === profiles[0].id, + "the P2P profile was not selected as the main remote", + ); + assert( + decoded.P2P_ActiveRemoteConfigurationId === profiles[0].id, + "the P2P profile was not selected for P2P features", + ); + assert( + profiles[0].uri.startsWith("sls+p2p://"), + "the selected profile was not a P2P connection URI", + ); +}); diff --git a/utils/setup/generate_setup_uri.ts b/utils/setup/generate_setup_uri.ts new file mode 100644 index 00000000..07c4f1cd --- /dev/null +++ b/utils/setup/generate_setup_uri.ts @@ -0,0 +1,189 @@ +import { + createNewVaultSettings, + encodeSettingsToSetupURI, + generateP2PRoomId, + type ObsidianLiveSyncSettings, + P2P_DEFAULT_SETTINGS, + PREFERRED_BASE, + PREFERRED_JOURNAL_SYNC, + PREFERRED_SETTING_SELF_HOSTED, + upsertRemoteConfigurationInPlace, +} from "./livesync-commonlib.ts"; + +export type SetupRemoteType = "couchdb" | "s3" | "p2p"; +export type SetupGeneratorEnvironment = Readonly< + Record +>; + +export interface GeneratedSetupURI { + remoteType: SetupRemoteType; + setupURI: string; + setupPassphrase: string; +} + +function requireValue( + environment: SetupGeneratorEnvironment, + name: string, +): string { + const value = environment[name]?.trim(); + if (!value) throw new Error(`${name} is required`); + return value; +} + +function optionalBoolean( + environment: SetupGeneratorEnvironment, + name: string, + fallback: boolean, +): boolean { + const value = environment[name]?.trim().toLowerCase(); + if (!value) return fallback; + if (value === "true" || value === "1") return true; + if (value === "false" || value === "0") return false; + throw new Error(`${name} must be true, false, 1, or 0`); +} + +export function generateSecret(): string { + const bytes = crypto.getRandomValues(new Uint8Array(24)); + return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll( + "/", + "_", + ).replace(/=+$/, ""); +} + +function applyEncryptedVaultSettings( + settings: ObsidianLiveSyncSettings, + environment: SetupGeneratorEnvironment, +): void { + Object.assign(settings, { + isConfigured: true, + encrypt: true, + passphrase: requireValue(environment, "passphrase"), + usePathObfuscation: true, + }); +} + +function createCouchDBSettings( + environment: SetupGeneratorEnvironment, +): ObsidianLiveSyncSettings { + const settings = createNewVaultSettings(); + Object.assign(settings, PREFERRED_SETTING_SELF_HOSTED, { + couchDB_URI: requireValue(environment, "hostname"), + couchDB_USER: requireValue(environment, "username"), + couchDB_PASSWORD: requireValue(environment, "password"), + couchDB_DBNAME: requireValue(environment, "database"), + batchSave: true, + periodicReplication: true, + syncOnStart: true, + syncOnFileOpen: true, + syncAfterMerge: true, + }); + applyEncryptedVaultSettings(settings, environment); + upsertRemoteConfigurationInPlace(settings, "couchdb", { activate: true }); + return settings; +} + +function createObjectStorageSettings( + environment: SetupGeneratorEnvironment, +): ObsidianLiveSyncSettings { + const settings = createNewVaultSettings(); + Object.assign(settings, PREFERRED_JOURNAL_SYNC, { + endpoint: requireValue(environment, "endpoint"), + accessKey: requireValue(environment, "access_key"), + secretKey: requireValue(environment, "secret_key"), + bucket: requireValue(environment, "bucket"), + region: environment.region?.trim() || "auto", + bucketPrefix: environment.bucket_prefix?.trim() || "", + bucketCustomHeaders: environment.bucket_custom_headers?.trim() || "", + useCustomRequestHandler: optionalBoolean( + environment, + "use_custom_request_handler", + false, + ), + forcePathStyle: optionalBoolean(environment, "force_path_style", true), + liveSync: true, + }); + applyEncryptedVaultSettings(settings, environment); + upsertRemoteConfigurationInPlace(settings, "s3", { activate: true }); + return settings; +} + +function createP2PSettings( + environment: SetupGeneratorEnvironment, +): ObsidianLiveSyncSettings { + const settings = createNewVaultSettings(); + Object.assign(settings, PREFERRED_BASE, P2P_DEFAULT_SETTINGS, { + P2P_Enabled: true, + P2P_roomID: environment.p2p_room_id?.trim() || generateP2PRoomId(), + P2P_passphrase: environment.p2p_passphrase?.trim() || generateSecret(), + P2P_relays: environment.p2p_relays?.trim() || + P2P_DEFAULT_SETTINGS.P2P_relays, + P2P_AppID: environment.p2p_app_id?.trim() || P2P_DEFAULT_SETTINGS.P2P_AppID, + P2P_AutoStart: optionalBoolean( + environment, + "p2p_auto_start", + P2P_DEFAULT_SETTINGS.P2P_AutoStart, + ), + P2P_AutoBroadcast: optionalBoolean( + environment, + "p2p_auto_broadcast", + P2P_DEFAULT_SETTINGS.P2P_AutoBroadcast, + ), + }); + applyEncryptedVaultSettings(settings, environment); + upsertRemoteConfigurationInPlace(settings, "p2p", { + activate: true, + activateForP2P: true, + }); + return settings; +} + +function parseRemoteType( + environment: SetupGeneratorEnvironment, +): SetupRemoteType { + const remoteType = environment.remote_type?.trim().toLowerCase() || "couchdb"; + if (remoteType === "couchdb" || remoteType === "s3" || remoteType === "p2p") { + return remoteType; + } + throw new Error("remote_type must be couchdb, s3, or p2p"); +} + +export function createSetupSettings( + environment: SetupGeneratorEnvironment, +): { remoteType: SetupRemoteType; settings: ObsidianLiveSyncSettings } { + const remoteType = parseRemoteType(environment); + if (remoteType === "couchdb") { + return { remoteType, settings: createCouchDBSettings(environment) }; + } + if (remoteType === "s3") { + return { remoteType, settings: createObjectStorageSettings(environment) }; + } + return { remoteType, settings: createP2PSettings(environment) }; +} + +export async function generateSetupURI( + environment: SetupGeneratorEnvironment, +): Promise { + const setupPassphrase = environment.uri_passphrase?.trim() || + generateSecret(); + const { remoteType, settings } = createSetupSettings(environment); + const setupURI = await encodeSettingsToSetupURI(settings, setupPassphrase, [ + "pluginSyncExtendedSetting", + "doNotUseFixedRevisionForChunks", + ], true); + return { remoteType, setupURI: setupURI.trim(), setupPassphrase }; +} + +export async function runSetupURIGenerator( + environment: SetupGeneratorEnvironment = Deno.env.toObject(), +): Promise { + const generated = await generateSetupURI(environment); + console.log(`\nGenerated ${generated.remoteType} Setup URI.`); + console.log( + "Your passphrase for the Setup URI is:", + generated.setupPassphrase, + ); + console.log("This passphrase is never shown again, so store it safely."); + console.log(generated.setupURI); +} + +if (import.meta.main) await runSetupURIGenerator(); diff --git a/utils/setup/livesync-commonlib.ts b/utils/setup/livesync-commonlib.ts new file mode 100644 index 00000000..d8091d4d --- /dev/null +++ b/utils/setup/livesync-commonlib.ts @@ -0,0 +1,18 @@ +// Keep Setup URI generation on its own static Commonlib module graph. This is +// intentionally separate from the CouchDB facade so that a raw-URL invocation +// does not load the PouchDB browser adapter. +export { + decodeSettingsFromSetupURI, + encodeSettingsToSetupURI, +} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/API/processSetting"; +export { generateP2PRoomId } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/common/utils"; +export { upsertRemoteConfigurationInPlace } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/remote-configurations"; +export { + createNewVaultSettings, + DEFAULT_SETTINGS, + P2P_DEFAULT_SETTINGS, + PREFERRED_BASE, + PREFERRED_JOURNAL_SYNC, + PREFERRED_SETTING_SELF_HOSTED, +} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings"; +export type { ObsidianLiveSyncSettings } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings"; diff --git a/utilsdeno/README.md b/utilsdeno/README.md index dc46fc49..d0b2a76e 100644 --- a/utilsdeno/README.md +++ b/utilsdeno/README.md @@ -32,7 +32,7 @@ Converts standard global variable usages to compatibility wrappers to ensure saf * **Targets**: `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `requestAnimationFrame`, `cancelAnimationFrame`, `localStorage`, `navigator`, `location`, `window`, `globalThis`, and `document`. * **Actions**: * Replaces global namespace references (like `window` and `globalThis`) with `compatGlobal`. - * Replaces `document` with `_activeDocument` (from `@lib/common/coreEnvFunctions.ts`). + * Replaces `document` with `_activeDocument` from the Commonlib compatibility entry. * Injects or updates the necessary imports in modified files. * **Command**: ```bash @@ -86,29 +86,15 @@ Scans the codebase and logs all occurrences of explicit `any` types. ``` ### 6. Import Normalisation (`normalise-imports.ts`) -Ensures that all import statements are standardised across the codebase, resolving paths to aliases such as `@lib/` and `@/` where applicable. +Ensures that internal plug-in import statements are standardised to the `@/` alias where applicable. Commonlib imports remain explicit package subpaths and are not rewritten. * **Command**: ```bash deno run --allow-read --allow-write --allow-env normalise-imports.ts ``` -### 7. CLI Node.js Import Redirection (`refactor-cli-node-imports.ts`) -Redirects direct Node.js built-in module imports (like `fs` and `path`) within the CLI codebase to use a single barrel file (`src/apps/cli/node-compat.ts`). - -* **Actions**: - * Finds imports of Node.js built-in APIs (`fs`, `fs/promises`, `path`, and `readline/promises`) in CLI source files. - * Replaces them with imports from the local `node-compat.ts` barrel file. - * This eliminates duplicate browser-targeted linter warnings on Node.js built-ins in the CLI workspace, keeping linter ignores consolidated. -* **Command**: - ```bash - deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts - ``` - ---- - ## Safety and Exclusions * **Tests Excluded**: All scripts automatically skip files located in `_test/` or `testdeno/` folders, as well as files ending with `.spec.ts` or `.test.ts`. -* **Submodule Caution**: Some tools will run against the `src/lib/` submodule. Ensure you verify changes inside the submodule prior to committing. +* **Package Boundary**: These tools operate on this repository only. Changes to Commonlib belong in its own repository and must be validated with its package checks. * **Verification**: Always run `npm run check` and `npm run test:unit` after performing refactoring tasks to verify that type safety and tests remain intact. diff --git a/utilsdeno/normalise-imports.ts b/utilsdeno/normalise-imports.ts index 11c3d153..c23ee0eb 100644 --- a/utilsdeno/normalise-imports.ts +++ b/utilsdeno/normalise-imports.ts @@ -1,4 +1,4 @@ -// Normalise import and export paths in the codebase to use @lib/ and @/ aliases correctly. +// Normalise import and export paths in the codebase to use the @/ alias correctly. // Use this script by running `deno run --allow-read --allow-write normalise-imports.ts` from the utilsdeno directory. // Set the --run flag to apply changes: `deno run --allow-read --allow-write normalise-imports.ts --run` // Set the --all-alias flag to also normalise sibling/child imports (starting with ./): `deno run --allow-read --allow-write normalise-imports.ts --all-alias` @@ -39,12 +39,9 @@ function toPosixPath(filePath: string): string { const posixProjectRoot = toPosixPath(projectRoot); const posixSrc = `${posixProjectRoot}/src`; -const posixLibSrc = `${posixProjectRoot}/src/lib/src`; -const posixSubrepo = `${posixProjectRoot}/src/lib`; console.log(`Project Root: ${posixProjectRoot}`); console.log(`Source Directory: ${posixSrc}`); -console.log(`Library Source Directory: ${posixLibSrc}`); console.log(""); let modifiedFilesCount = 0; @@ -87,7 +84,7 @@ for (const sourceFile of project.getSourceFiles()) { // Determine if it is an internal import. const isRelative = moduleSpecifier.startsWith("."); - const isAlias = moduleSpecifier.startsWith("@/") || moduleSpecifier.startsWith("@lib/"); + const isAlias = moduleSpecifier.startsWith("@/"); if (!isRelative && !isAlias) { // Skip external packages/modules. @@ -96,9 +93,7 @@ for (const sourceFile of project.getSourceFiles()) { // Resolve path to an absolute POSIX path. let resolvedPath = ""; - if (moduleSpecifier.startsWith("@lib/")) { - resolvedPath = `${posixLibSrc}/${moduleSpecifier.slice(5)}`; - } else if (moduleSpecifier.startsWith("@/")) { + if (moduleSpecifier.startsWith("@/")) { resolvedPath = `${posixSrc}/${moduleSpecifier.slice(2)}`; } else { // Relative path. @@ -107,14 +102,9 @@ for (const sourceFile of project.getSourceFiles()) { resolvedPath = toPosixPath(path.normalize(resolvedPath)); - // Keep relative sibling/child imports unchanged (e.g. ./utils) unless: - // 1. --all-alias is set, OR - // 2. the import crosses the subrepository boundary (src/lib/) + // Keep relative sibling/child imports unchanged (e.g. ./utils) unless --all-alias is set. const isSibling = isRelative && !moduleSpecifier.startsWith(".."); - const importerInsideSubrepo = posixFilePath.startsWith(posixSubrepo + "/"); - const targetInsideSubrepo = resolvedPath.startsWith(posixSubrepo + "/"); - const crossesSubrepo = importerInsideSubrepo !== targetInsideSubrepo; - if (isSibling && !allAlias && !crossesSubrepo) { + if (isSibling && !allAlias) { continue; } @@ -126,18 +116,7 @@ for (const sourceFile of project.getSourceFiles()) { moduleSpecifier.endsWith(".svelte") || moduleSpecifier.endsWith(".d.ts"); - if (resolvedPath.startsWith(posixLibSrc + "/")) { - let rel = resolvedPath.slice(posixLibSrc.length + 1); - if (!hasExtension && (rel.endsWith(".ts") || rel.endsWith(".js"))) { - // Strip extension if the original import did not have one. - if (rel.endsWith(".ts") && !rel.endsWith(".d.ts")) { - rel = rel.slice(0, -3); - } else if (rel.endsWith(".js")) { - rel = rel.slice(0, -3); - } - } - newSpecifier = `@lib/${rel}`; - } else if (resolvedPath.startsWith(posixSrc + "/")) { + if (resolvedPath.startsWith(posixSrc + "/")) { let rel = resolvedPath.slice(posixSrc.length + 1); if (!hasExtension && (rel.endsWith(".ts") || rel.endsWith(".js"))) { // Strip extension if the original import did not have one. diff --git a/utilsdeno/refactor-cli-node-imports.ts b/utilsdeno/refactor-cli-node-imports.ts deleted file mode 100644 index 36f88536..00000000 --- a/utilsdeno/refactor-cli-node-imports.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Refactor Node.js imports in the CLI application to use the barrel compatibility file. -// Use this script by running `deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts` from the utilsdeno directory. -// Run with --run flag to apply changes. -import { Project, SyntaxKind, Node } from "npm:ts-morph"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const isDryRun = !Deno.args.includes("--run"); - -if (isDryRun) { - console.log("=== DRY RUN MODE ==="); - console.log( - "To apply changes, run with: deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts --run\n" - ); -} else { - console.log("=== RUN MODE: WILL MODIFY FILES ==="); -} - -const project = new Project({ tsConfigFilePath: "../tsconfig.json" }); -project.addSourceFilesAtPaths("../src/apps/cli/**/*.ts"); - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const projectRoot = path.resolve(__dirname, ".."); -const nodeCompatPath = path.resolve(projectRoot, "src", "apps", "cli", "node-compat.ts"); - -function toPosixPath(filePath: string): string { - return filePath.replace(/\\/g, "/"); -} - -const posixProjectRoot = toPosixPath(projectRoot); -const posixSrc = `${posixProjectRoot}/src`; - -function getRelativeImportPath(fromFile: string, toFile: string): string { - let rel = path.relative(path.dirname(fromFile), toFile); - rel = rel.replace(/\\/g, "/"); - if (!rel.startsWith(".") && !rel.startsWith("/")) { - rel = "./" + rel; - } - if (rel.endsWith(".ts")) { - rel = rel.slice(0, -3); - } - return rel; -} - -let modifiedFilesCount = 0; - -for (const sourceFile of project.getSourceFiles()) { - const filePath = sourceFile.getFilePath(); - const posixFilePath = toPosixPath(filePath); - - // Only process CLI source files under src/apps/cli/ - if (!posixFilePath.includes("/src/apps/cli/")) continue; - if ( - posixFilePath.endsWith("node-compat.ts") || - posixFilePath.endsWith("vite.config.ts") || - posixFilePath.endsWith(".spec.ts") || - posixFilePath.endsWith(".test.ts") || - posixFilePath.includes("/_test/") || - posixFilePath.includes("/testdeno/") || - posixFilePath.includes("/test/") - ) { - continue; - } - - const importDeclarations = sourceFile.getImportDeclarations(); - const targetImports: any[] = []; - const namedImportsToAdd: string[] = []; - - for (const impDecl of importDeclarations) { - const specifier = impDecl.getModuleSpecifierValue(); - - // Check if it's a Node.js built-in module we want to redirect - let exportedName = ""; - if (specifier === "fs/promises" || specifier === "node:fs/promises") { - exportedName = "fsPromises"; - } else if (specifier === "fs" || specifier === "node:fs") { - exportedName = "fs"; - } else if (specifier === "path" || specifier === "node:path") { - exportedName = "path"; - } else if (specifier === "node:readline/promises") { - exportedName = "readline"; - } - - if (exportedName) { - const localName = impDecl.getNamespaceImport()?.getText() || impDecl.getDefaultImport()?.getText(); - if (localName) { - targetImports.push({ impDecl, exportedName, localName }); - } - } - } - - if (targetImports.length > 0) { - console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`); - - for (const { impDecl, exportedName, localName } of targetImports) { - const { line } = sourceFile.getLineAndColumnAtPos(impDecl.getStart()); - console.log(` Line ${line}: Redirecting "${impDecl.getText()}"`); - - if (exportedName === localName) { - namedImportsToAdd.push(exportedName); - } else { - namedImportsToAdd.push(`${exportedName} as ${localName}`); - } - - if (!isDryRun) { - impDecl.remove(); - } - } - - const relImportPath = getRelativeImportPath(filePath, nodeCompatPath); - console.log(` Adding: import { ${namedImportsToAdd.join(", ")} } from "${relImportPath}"`); - - if (!isDryRun) { - sourceFile.addImportDeclaration({ - namedImports: namedImportsToAdd, - moduleSpecifier: relImportPath, - }); - } - - modifiedFilesCount++; - } -} - -console.log(`\nTotal files to modify: ${modifiedFilesCount}`); - -if (!isDryRun) { - project.saveSync(); - console.log("All changes successfully saved."); -} else { - console.log("Dry run complete. No changes were written to files."); -} diff --git a/utilsdeno/refactor-globals.ts b/utilsdeno/refactor-globals.ts index bd368dfd..ea54a1f4 100644 --- a/utilsdeno/refactor-globals.ts +++ b/utilsdeno/refactor-globals.ts @@ -32,7 +32,6 @@ function toPosixPath(filePath: string): string { const posixProjectRoot = toPosixPath(projectRoot); const posixSrc = `${posixProjectRoot}/src`; -const posixLibSrc = `${posixProjectRoot}/src/lib`; const TARGET_GLOBALS = new Set([ "setTimeout", @@ -191,7 +190,7 @@ for (const sourceFile of project.getSourceFiles()) { if (requiredImports.length > 0) { const existingImport = sourceFile.getImportDeclarations().find((imp) => { const spec = imp.getModuleSpecifierValue(); - return spec === "@lib/common/coreEnvFunctions" || spec === "@lib/common/coreEnvFunctions.ts"; + return spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions" || spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; }); if (existingImport) { @@ -206,7 +205,7 @@ for (const sourceFile of project.getSourceFiles()) { } else { sourceFile.addImportDeclaration({ namedImports: requiredImports, - moduleSpecifier: "@lib/common/coreEnvFunctions.ts", + moduleSpecifier: "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", }); } } diff --git a/utilsdeno/refactor-import-utils.ts b/utilsdeno/refactor-import-utils.ts deleted file mode 100644 index 96e45dfd..00000000 --- a/utilsdeno/refactor-import-utils.ts +++ /dev/null @@ -1,187 +0,0 @@ -// Delete references to utils.ts and replace them with new imports based on the importMap. -// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-import-utils.ts` from the utilsdeno directory. -import { Project } from "npm:ts-morph"; - -const isDryRun = !Deno.args.includes("--run"); - -if (isDryRun) { - console.log("=== DRY RUN MODE ==="); - console.log( - "To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-import-utils.ts --run\n" - ); -} - -// const project = new Project({ tsConfigFilePath: "../src/apps/cli/tsconfig.json" }); -const project = new Project({ tsConfigFilePath: "../tsconfig.json" }); - -const importMap = new Map(); - -const targetFiles = [ - "utils.concurrency.ts", - "utils.timer.ts", - "utils.notations.ts", - "utils.database.ts", - "utils.regexp.ts", - "utils.settings.ts", - "utils.patch.ts", - "utils.misc.ts", -]; - -// 1. Map exports from our newly created subfiles -for (const sourceFile of project.getSourceFiles()) { - const filePath = sourceFile.getFilePath(); - const fileName = sourceFile.getBaseName(); - if (filePath.includes("src/lib/src/common/") && targetFiles.includes(fileName)) { - const exports = sourceFile.getExportedDeclarations(); - for (const [name] of exports) { - const relativePath = filePath.split("src/lib/src/")[1].replace(/\.ts$/, ""); - importMap.set(name, `@lib/${relativePath}`); - } - } -} - -// 2. Map exports/imports of octagonal-wheels in utils.ts -const utilsFile = project.getSourceFile("src/lib/src/common/utils.ts"); -if (utilsFile) { - // Parse imports from octagonal-wheels - for (const imp of utilsFile.getImportDeclarations()) { - const moduleSpec = imp.getModuleSpecifierValue(); - if (moduleSpec.startsWith("octagonal-wheels")) { - for (const namedImport of imp.getNamedImports()) { - importMap.set(namedImport.getName(), moduleSpec); - } - } - } - // Parse export declarations from octagonal-wheels - for (const exp of utilsFile.getExportDeclarations()) { - const moduleSpec = exp.getModuleSpecifierValue(); - if (moduleSpec && moduleSpec.startsWith("octagonal-wheels")) { - for (const namedExport of exp.getNamedExports()) { - importMap.set(namedExport.getName(), moduleSpec); - } - } - } -} - -console.log(`Built importMap with ${importMap.size} mappings.\n`); - -let modifiedFilesCount = 0; - -// 3. Loop through all source files and replace imports -for (const sourceFile of project.getSourceFiles()) { - let fileModified = false; - const imports = sourceFile.getImportDeclarations(); - - for (const imp of imports) { - const moduleSpec = imp.getModuleSpecifierValue(); - const isUtilsImport = - moduleSpec === "@lib/common/utils" || - moduleSpec === "@lib/common/utils.ts" || - moduleSpec.endsWith("/common/utils") || - moduleSpec.endsWith("/common/utils.ts"); - - if (isUtilsImport) { - const namedImports = imp.getNamedImports(); - const defaultImport = imp.getDefaultImport(); - - const importsToReplace: Record = {}; - for (const namedImport of namedImports) { - const name = namedImport.getName(); - let newPath = importMap.get(name); - if (newPath) { - // If original ended with .ts and the new path starts with @lib, keep .ts - if (moduleSpec.endsWith(".ts") && newPath.startsWith("@lib/")) { - newPath = newPath + ".ts"; - } - if (!importsToReplace[newPath]) { - importsToReplace[newPath] = []; - } - importsToReplace[newPath].push({ - name, - newPath, - isTypeOnly: namedImport.isTypeOnly() || imp.isTypeOnly(), - }); - } - } - - if (Object.keys(importsToReplace).length > 0 || (defaultImport && importMap.has(defaultImport.getText()))) { - fileModified = true; - - console.log(`File: ${sourceFile.getFilePath().split("obsidian-livesync/")[1]}`); - console.log(` Old: ${imp.getText()}`); - } - - if (!isDryRun) { - // Apply replacements - for (const newPath in importsToReplace) { - const isTypeOnly = importsToReplace[newPath].filter((i) => i.isTypeOnly); - if (isTypeOnly.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isTypeOnly.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: true, - }); - } - const isValueImport = importsToReplace[newPath].filter((i) => !i.isTypeOnly); - if (isValueImport.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isValueImport.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: false, - }); - } - for (const { name } of importsToReplace[newPath]) { - const namedImport = imp.getNamedImports().find((ni) => ni.getName() === name); - if (namedImport) { - namedImport.remove(); - } - } - } - } else { - // In dry run, just print what it would do - for (const newPath in importsToReplace) { - const names = importsToReplace[newPath].map((i) => i.name).join(", "); - console.log(` -> Would import { ${names} } from "${newPath}"`); - } - } - - if (defaultImport) { - const name = defaultImport.getText(); - let newPath = importMap.get(name); - if (newPath) { - if (moduleSpec.endsWith(".ts") && newPath.startsWith("@lib/")) { - newPath = newPath + ".ts"; - } - if (!isDryRun) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - defaultImport: name, - moduleSpecifier: newPath, - isTypeOnly: imp.isTypeOnly(), - }); - imp.removeDefaultImport(); - } else { - console.log(` -> Would import default ${name} from "${newPath}"`); - } - } - } - - if (!isDryRun) { - if (imp.getNamedImports().length === 0 && !imp.getDefaultImport()) { - imp.remove(); - } - } - } - } - if (fileModified) { - modifiedFilesCount++; - } -} - -console.log(`\nTotal files to modify: ${modifiedFilesCount}`); - -if (!isDryRun) { - project.saveSync(); - console.log("All changes successfully saved."); -} else { - console.log("Dry run complete. No changes were written to files."); -} diff --git a/utilsdeno/refactor-imports.ts b/utilsdeno/refactor-imports.ts deleted file mode 100644 index b7d7a6a2..00000000 --- a/utilsdeno/refactor-imports.ts +++ /dev/null @@ -1,155 +0,0 @@ -// Delete references to types.ts and replace them with new imports based on the importMap. It will also split imports if some are type-only and some are value imports. -// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-imports.ts` from the utilsdeno directory. It will read all source files, find imports from types.ts, and replace them with the new paths based on the importMap. Make sure to review the changes before saving, as it will modify your source files. -import { Project } from "npm:ts-morph"; - -const isDryRun = !Deno.args.includes("--run"); - -if (isDryRun) { - console.log("=== DRY RUN MODE ==="); - console.log( - "To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-import-utils.ts --run\n" - ); -} - -// const project = new Project({ tsConfigFilePath: "../src/apps/cli/tsconfig.json" }); -const project = new Project({ tsConfigFilePath: "../tsconfig.json" }); - -const importMap = new Map(); -// Build a map of types moved out of Models. -// Under src/lib/src/common/models. -for (const sourceFile of project.getSourceFiles()) { - if (sourceFile.getFilePath().includes("src/lib/src/common/models")) { - const exports = sourceFile.getExportedDeclarations(); - for (const [name, declarations] of exports) { - for (const declaration of declarations) { - if ( - // declaration.getKindName() === "TypeAliasDeclaration" || - // declaration.getKindName() === "InterfaceDeclaration" || - // declaration.getKindName() === "EnumDeclaration" || - true - ) { - // console.log(`Found type export in ${sourceFile.getFilePath()}:`, name); - const relativePath = sourceFile.getFilePath().split("src/lib/src/")[1].replace(/\.ts$/, ""); - importMap.set(name, `@lib/${relativePath}`); - } - } - } - } -} -// Extras - -importMap.set("LOG_LEVEL_NOTICE", "@lib/common/logger"); -importMap.set("LOG_LEVEL_VERBOSE", "@lib/common/logger"); -importMap.set("LOG_LEVEL_INFO", "@lib/common/logger"); -importMap.set("LOG_LEVEL_DEBUG", "@lib/common/logger"); -importMap.set("LOG_LEVEL_URGENT", "@lib/common/logger"); -importMap.set("LOG_LEVEL", "@lib/common/logger"); -importMap.set("Logger", "@lib/common/logger"); - -// console.log("Import map:", importMap); - -// Loop through all files that import from types.ts. -for (const sourceFile of project.getSourceFiles()) { - const imports = sourceFile.getImportDeclarations(); - // if import from types.ts and the file is pointing `/lib/src/common/types.ts` (resolved), then we will check if the imported names exist in the importMap, if yes, we will replace the import path with the new path from importMap. - - for (const imp of imports) { - const moduleSpecifier = imp.getModuleSpecifierValue(); - if (moduleSpecifier.endsWith("types") || moduleSpecifier.endsWith("types.ts")) { - const filePath = sourceFile.getFilePath(); - const lineNumber = imp.getStartLineNumber(); - const resolvedModule = imp.getModuleSpecifierSourceFile(); - if (!resolvedModule || !resolvedModule.getFilePath().includes("/lib/src/common/types.ts")) { - continue; - } - - // Collect imports from types.ts. - const namedImports = imp.getNamedImports(); - const defaultImport = imp.getDefaultImport(); - console.log(`Found import in ${filePath} at line ${lineNumber}:`, { - namedImports: namedImports.map((ni) => ni.getText()), - defaultImport: defaultImport ? defaultImport.getText() : null, - }); - // Group imports by their names and generate new import paths based on the importMap - const importsToReplace: Record = {}; - for (const namedImport of namedImports) { - const name = namedImport.getName(); - const newPath = importMap.get(name); - if (newPath) { - console.log( - `Will replace import of ${name} in ${filePath} at line ${lineNumber} with new path:`, - newPath - ); - if (!importsToReplace[newPath]) { - importsToReplace[newPath] = []; - } - importsToReplace[newPath].push({ - name, - newPath, - isTypeOnly: namedImport.isTypeOnly() || imp.isTypeOnly(), - }); - } - } - - // For each import, generate a new path from importMap and replace it. - // Split the import when it needs to become multiple imports. - - for (const newPath in importsToReplace) { - // First, handle type-only imports. - const isTypeOnly = importsToReplace[newPath].filter((i) => i.isTypeOnly); - if (isTypeOnly.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isTypeOnly.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: true, - }); - } - // Then, handle non-type-only imports. - const isValueImport = importsToReplace[newPath].filter((i) => !i.isTypeOnly); - if (isValueImport.length > 0) { - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - namedImports: isValueImport.map((i) => i.name), - moduleSpecifier: newPath, - isTypeOnly: false, - }); - } - // Remove the replaced named imports from the old import. - for (const { name } of importsToReplace[newPath]) { - const namedImport = imp.getNamedImports().find((ni) => ni.getName() === name); - if (namedImport) { - namedImport.remove(); - } - } - } - // If there is also a default import and it exists in importMap, replace it too. - if (defaultImport) { - const name = defaultImport.getText(); - const newPath = importMap.get(name); - - if (newPath) { - console.log( - `Replacing default import of ${name} in ${filePath} at line ${lineNumber} with new path:`, - newPath - ); - // Add the new import statement. - sourceFile.insertImportDeclaration(imp.getChildIndex(), { - defaultImport: name, - moduleSpecifier: newPath, - isTypeOnly: imp.isTypeOnly(), - }); - // Remove the default import from the old import. - imp.removeDefaultImport(); - } - } - if (imp.getNamedImports().length === 0 && !imp.getDefaultImport()) { - // Delete the entire import statement if nothing remains. - imp.remove(); - } - } - } -} - -// Save everything at the end. -if (!isDryRun) { - project.saveSync(); -} diff --git a/utilsdeno/refactor-styles.ts b/utilsdeno/refactor-styles.ts index c9546d78..95f46d82 100644 --- a/utilsdeno/refactor-styles.ts +++ b/utilsdeno/refactor-styles.ts @@ -32,7 +32,6 @@ function toPosixPath(filePath: string): string { const posixProjectRoot = toPosixPath(projectRoot); const posixSrc = `${posixProjectRoot}/src`; -const posixLibSrc = `${posixProjectRoot}/src/lib`; function matchStyleAccess(node: Node): { element: Node; propertyName: string; isComputed: boolean } | undefined { if (Node.isPropertyAccessExpression(node)) { diff --git a/utilsdeno/types-add-ignore.ts b/utilsdeno/types-add-ignore.ts deleted file mode 100644 index 3a546525..00000000 --- a/utilsdeno/types-add-ignore.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Project, SyntaxKind } from "npm:ts-morph"; - -function processFile(filePath: string, origin: string, repoHash: string): string { - const project = new Project(); - const sourceFile = project.addSourceFileAtPath(filePath); - let updated = false; - - // 0. insert a commit hash comment at the top of the file - sourceFile.insertText(0, `// @ts-nocheck\n// REPO: ${origin} Commit hash: ${repoHash}\n`); - updated = true; - - // 1. Replacements for Uint8Array and DataView - let sourceText = sourceFile.getFullText(); - if (sourceText.includes("Uint8Array") || sourceText.includes("DataView")) { - sourceText = sourceText.replace(/Uint8Array/g, "Uint8Array"); - sourceText = sourceText.replace(/DataView/g, "DataView"); - sourceFile.replaceWithText(sourceText); - updated = true; - } - - // 2. Remove EventEmitter import from "events" and declare class EventEmitter inline - const imports = sourceFile.getImportDeclarations(); - imports.forEach((importDecl) => { - if (importDecl.getModuleSpecifierValue() === "events") { - const defaultImport = importDecl.getDefaultImport(); - if (defaultImport && defaultImport.getText() === "EventEmitter") { - importDecl.remove(); - sourceFile.addClass({ - name: "EventEmitter", - isExported: false, - methods: [ - { - name: "on", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "once", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "off", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "emit", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "args", isRestParameter: true, type: "any[]" }, - ], - returnType: "boolean", - }, - { - name: "addListener", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "removeListener", - parameters: [ - { name: "event", type: "string | symbol" }, - { name: "listener", type: "(...args: any[]) => void" }, - ], - returnType: "this", - }, - { - name: "removeAllListeners", - parameters: [{ name: "event", isOptional: true, type: "string | symbol" }], - returnType: "this", - }, - ], - }); - updated = true; - } - } - }); - - // 3. Collect targets for inline disable comments - const targetAnyLines = new Set(); - const targetEmptyObjectLines = new Set(); - const targetEmptyInterfaceLines = new Set(); - const targetDuplicateEnumLines = new Set(); - - // 3.1. 'any' type nodes - const anyTypeNodes = sourceFile.getDescendantsOfKind(SyntaxKind.AnyKeyword); - anyTypeNodes.forEach((anyNode: any) => { - const { line } = sourceFile.getLineAndColumnAtPos(anyNode.getStart()); - targetAnyLines.add(line - 1); - }); - - // 3.2. Empty object type literals {} - const typeLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.TypeLiteral); - typeLiterals.forEach((node) => { - if (node.getMembers().length === 0) { - const { line } = sourceFile.getLineAndColumnAtPos(node.getStart()); - targetEmptyObjectLines.add(line - 1); - } - }); - - // 3.3. Empty interfaces - const interfaces = sourceFile.getInterfaces(); - interfaces.forEach((node) => { - if (node.getMembers().length === 0) { - const { line } = sourceFile.getLineAndColumnAtPos(node.getStart()); - targetEmptyInterfaceLines.add(line - 1); - } - }); - - // 3.4. Duplicate enum member values - const enums = sourceFile.getEnums(); - enums.forEach((enumDecl) => { - const values = new Set(); - enumDecl.getMembers().forEach((member) => { - const initValue = member.getInitializer()?.getText(); - if (initValue) { - if (values.has(initValue)) { - const { line } = sourceFile.getLineAndColumnAtPos(member.getStart()); - targetDuplicateEnumLines.add(line - 1); - } else { - values.add(initValue); - } - } - }); - }); - - // 4. Inject ignore comments line by line - const finalSourceText = sourceFile.getFullText(); - const lineBreak = finalSourceText.includes("\r\n") ? "\r\n" : "\n"; - const lines = finalSourceText.split(/\r?\n/); - - // 4.1. Add inline disable to lines that contain 'any' - for (const lineIndex of targetAnyLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line @typescript-eslint/no-explicit-any")) continue; - lines[lineIndex] = `${line} // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration`; - updated = true; - } - - // 4.2. Add inline disable to lines that contain empty object {} - for (const lineIndex of targetEmptyObjectLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue; - lines[lineIndex] = - `${line} // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/ban-types -- Empty object type`; - updated = true; - } - - // 4.3. Add inline disable to lines that contain empty interface - for (const lineIndex of targetEmptyInterfaceLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue; - lines[lineIndex] = - `${line} // eslint-disable-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface -- Empty interface`; - updated = true; - } - - // 4.4. Add inline disable to lines with duplicate enums - for (const lineIndex of targetDuplicateEnumLines) { - const line = lines[lineIndex]; - if (!line) continue; - if (line.includes("eslint-disable-line") || line.includes("eslint-disable-next-line")) continue; - lines[lineIndex] = - `${line} // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value`; - updated = true; - } - - const updatedSourceText = lines.join(lineBreak); - if (updated) { - console.log(`Processed file: ${filePath}`); - } - return updatedSourceText; -} - -const targetDir = `./_types`; - -async function processDir(dirPath: string) { - for await (const entry of Deno.readDir(dirPath)) { - if (entry.isDirectory) { - await processDir(`${dirPath}/${entry.name}`); - } - if (entry.isFile && entry.name.endsWith(".d.ts")) { - const filePath = `${dirPath}/${entry.name}`; - console.log(`Processing: ${filePath}`); - const updatedContent = processFile(filePath, repoRemoteOriginStr, gitCommitHashStr); - // Write the file. To revert, regenerate it with npm run lib:build:types. - await Deno.writeTextFile(filePath, updatedContent); - } - } -} - -const subDir = "./src/lib/"; -const repoRemoteOrigins = new Deno.Command("git", { - args: ["remote", "get-url", "origin"], - cwd: subDir, - stdout: "piped", -}).outputSync().stdout; -const repoRemoteOriginStr = new TextDecoder().decode(repoRemoteOrigins).trim(); -console.log(`STAMP: Git remote origin: ${repoRemoteOriginStr}`); -const gitCommitHashSub = new Deno.Command("git", { - args: ["rev-parse", "--short", "HEAD"], - cwd: subDir, - stdout: "piped", -}).outputSync().stdout; -const gitCommitHashStr = new TextDecoder().decode(gitCommitHashSub).trim(); -console.log(`STAMP: Git commit hash: ${gitCommitHashStr}`); -await processDir(targetDir); diff --git a/versions.json b/versions.json index bcfe6d36..6ffd7703 100644 --- a/versions.json +++ b/versions.json @@ -1,9 +1,16 @@ { "0.25.61": "1.7.2", "0.25.60": "1.7.2", - "1.0.1": "0.9.12", - "1.0.0": "0.9.7", "0.25.81": "1.7.2", "0.25.82": "1.7.2", - "0.25.83": "1.7.2" + "0.25.83": "1.7.2", + "1.0.0-beta.0": "1.7.2", + "1.0.0-beta.1": "1.7.2", + "1.0.0-beta.2": "1.7.2", + "1.0.0-beta.3": "1.7.2", + "1.0.0-beta.4": "1.7.2", + "1.0.0-beta.5": "1.7.2", + "1.0.0-rc.0": "1.7.2", + "1.0.0-rc.1": "1.7.2", + "1.0.0": "1.7.2" } diff --git a/vite.config.ts b/vite.config.ts index a7c47aee..8b009f0a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -130,7 +130,6 @@ export default defineConfig(({ mode }) => { resolve: { alias: { "@": path.resolve(__dirname, "./src"), - "@lib": path.resolve(__dirname, "./src/lib/src"), src: path.resolve(__dirname, "./src"), }, }, diff --git a/vitest.config.common.ts b/vitest.config.common.ts index 73f023bf..0a7d7375 100644 --- a/vitest.config.common.ts +++ b/vitest.config.common.ts @@ -96,7 +96,6 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - "@lib": path.resolve(__dirname, "./src/lib/src"), src: path.resolve(__dirname, "./src"), }, }, diff --git a/vitest.config.e2e-runner.ts b/vitest.config.e2e-runner.ts new file mode 100644 index 00000000..ce2c2743 --- /dev/null +++ b/vitest.config.e2e-runner.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/e2e-obsidian/runner/*.test.ts"], + }, +}); diff --git a/vitest.config.p2p.ts b/vitest.config.p2p.ts deleted file mode 100644 index 9478db52..00000000 --- a/vitest.config.p2p.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @file vitest.config.p2p.ts - * @description Configuration for running browser-based Peer-to-Peer (P2P) replication tests - * in Playwright (Chromium) using Trystero and Nostr relays. - * This is executed via the `npm run test:p2p` command (which runs `test/suitep2p/run-p2p-tests.sh` internally). - */ -import { defineConfig, mergeConfig } from "vitest/config"; -import { playwright } from "@vitest/browser-playwright"; -import viteConfig from "./vitest.config.common"; -import path from "path"; -import { existsSync, readFileSync } from "node:fs"; -import { parseEnv } from "node:util"; -import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./test/lib/commands"; - -// P2P test environment variables -// Configure these in .env or .test.env, or inject via shell before running tests. -// Shell-injected values take precedence over dotenv files. -// -// Required: -// P2P_TEST_ROOM_ID - Shared room identifier for peers to discover each other -// P2P_TEST_PASSPHRASE - Encryption passphrase shared between test peers -// -// Optional: -// P2P_TEST_HOST_PEER_NAME - Name used to identify the host peer (default varies) -// P2P_TEST_RELAY - Nostr relay server URL used for peer signalling/discovery -// P2P_TEST_APP_ID - Application ID scoping the P2P session -// P2P_TEST_HANDOFF_FILE - File path used to pass state between up/down test phases -// -// General test options (also read from env): -// ENABLE_DEBUGGER - Set to "true" to attach a debugger and pause before tests -// ENABLE_UI - Set to "true" to open a visible browser window during tests -const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined); -const defEnv = loadEnvFile(".env"); -const testEnv = loadEnvFile(".test.env"); -// Merge: dotenv files < process.env (so shell-injected vars like P2P_TEST_* take precedence) -const p2pEnv: Record = {}; -if (process.env.P2P_TEST_ROOM_ID) p2pEnv.P2P_TEST_ROOM_ID = process.env.P2P_TEST_ROOM_ID; -if (process.env.P2P_TEST_PASSPHRASE) p2pEnv.P2P_TEST_PASSPHRASE = process.env.P2P_TEST_PASSPHRASE; -if (process.env.P2P_TEST_HOST_PEER_NAME) p2pEnv.P2P_TEST_HOST_PEER_NAME = process.env.P2P_TEST_HOST_PEER_NAME; -if (process.env.P2P_TEST_RELAY) p2pEnv.P2P_TEST_RELAY = process.env.P2P_TEST_RELAY; -if (process.env.P2P_TEST_APP_ID) p2pEnv.P2P_TEST_APP_ID = process.env.P2P_TEST_APP_ID; -if (process.env.P2P_TEST_HANDOFF_FILE) p2pEnv.P2P_TEST_HANDOFF_FILE = process.env.P2P_TEST_HANDOFF_FILE; -const env = Object.assign({}, defEnv, testEnv, p2pEnv); -const debuggerEnabled = env?.ENABLE_DEBUGGER === "true"; -const enableUI = env?.ENABLE_UI === "true"; -const headless = !debuggerEnabled && !enableUI; - -export default mergeConfig( - viteConfig, - defineConfig({ - resolve: { - alias: { - obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"), - }, - }, - test: { - env: env, - testTimeout: 240000, - hookTimeout: 240000, - fileParallelism: false, - isolate: true, - watch: false, - // Run all CLI-host P2P test files (*.p2p.test.ts, *.p2p-up.test.ts, *.p2p-down.test.ts) - include: ["test/suitep2p/**/*.p2p*.test.ts"], - browser: { - isolate: true, - // Only grantClipboardPermissions is needed; no openWebPeer/acceptWebPeer - commands: { - grantClipboardPermissions, - writeHandoffFile, - readHandoffFile, - }, - provider: playwright({ - launchOptions: { - args: [ - "--js-flags=--expose-gc", - "--allow-insecure-localhost", - "--disable-web-security", - "--ignore-certificate-errors", - ], - }, - }), - enabled: true, - screenshotFailures: false, - instances: [ - { - execArgv: ["--js-flags=--expose-gc"], - browser: "chromium", - headless, - isolate: true, - inspector: debuggerEnabled ? { waitForDebugger: true, enabled: true } : undefined, - printConsoleTrace: true, - onUnhandledError(error) { - const msg = error.message || ""; - if (msg.includes("Cannot create so many PeerConnections")) { - return false; - } - }, - }, - ], - headless, - fileParallelism: false, - ui: debuggerEnabled || enableUI ? true : false, - }, - }, - }) -); diff --git a/vitest.config.rpc-unit.ts b/vitest.config.rpc-unit.ts deleted file mode 100644 index d3c175e7..00000000 --- a/vitest.config.rpc-unit.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @file vitest.config.rpc-unit.ts - * @description Configuration for running RPC-specific unit tests (such as RpcRoom and transport layers) in Node.js, - * enforcing coverage thresholds on the RPC sub-module. - * This can be run manually to verify RPC-specific coverage, or is matched by the glob patterns in `npm run test:unit`. - */ -import { defineConfig, mergeConfig } from "vitest/config"; -import viteConfig from "./vitest.config.common"; - -export default mergeConfig( - viteConfig, - defineConfig({ - resolve: { - alias: { - obsidian: "", - }, - }, - test: { - name: "rpc-unit-tests", - include: ["src/lib/src/rpc/**/*.unit.spec.ts"], - exclude: ["test/**"], - coverage: { - include: ["src/lib/src/rpc/**/*.ts"], - exclude: ["**/*.unit.spec.ts", "**/index.ts"], - provider: "v8", - reporter: ["text", "json", "html", ["text", { file: "coverage-rpc-text.txt" }]], - thresholds: { - lines: 90, - functions: 90, - branches: 75, - statements: 90, - }, - }, - }, - }) -); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index a62992d3..00000000 --- a/vitest.config.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @file vitest.config.ts - * @description Configuration for running browser-based end-to-end (E2E) integration tests - * using Playwright (Chromium) to test replication and synchronisation scenarios. - * This is executed when running the full test suite via `npm run test` or `npm run test:full`. - */ -import { defineConfig, mergeConfig } from "vitest/config"; -import { playwright } from "@vitest/browser-playwright"; -import viteConfig from "./vitest.config.common"; -import path from "path"; -import { existsSync, readFileSync } from "node:fs"; -import { parseEnv } from "node:util"; -import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands"; - -const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined); -const defEnv = loadEnvFile(".env"); -const testEnv = loadEnvFile(".test.env"); -const env = Object.assign({}, defEnv, testEnv); -const debuggerEnabled = env?.ENABLE_DEBUGGER === "true"; -const enableUI = env?.ENABLE_UI === "true"; -const headless = !debuggerEnabled && !enableUI; -export default mergeConfig( - viteConfig, - defineConfig({ - resolve: { - alias: { - obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"), - }, - }, - test: { - env: env, - testTimeout: 40000, - hookTimeout: 50000, - fileParallelism: false, - isolate: true, - watch: false, - - // environment: "browser", - include: ["test/**/*.test.ts"], - coverage: { - include: ["src/**/*.ts", "src/lib/src/**/*.ts", "src/**/*.svelte"], - exclude: ["**/*.test.ts", "src/lib/**"], - provider: "v8", - reporter: ["text", "json", "html"], - // ignoreEmptyLines: true, - }, - browser: { - isolate: true, - commands: { - grantClipboardPermissions, - openWebPeer, - closeWebPeer, - acceptWebPeer, - }, - provider: playwright({ - launchOptions: { - args: ["--js-flags=--expose-gc"], - // chromiumSandbox: true, - }, - }), - enabled: true, - screenshotFailures: false, - instances: [ - { - execArgv: ["--js-flags=--expose-gc"], - browser: "chromium", - headless, - isolate: true, - inspector: debuggerEnabled - ? { - waitForDebugger: true, - enabled: true, - } - : undefined, - printConsoleTrace: debuggerEnabled, - onUnhandledError(error) { - // Ignore certain errors - const msg = error.message || ""; - if (msg.includes("Cannot create so many PeerConnections")) { - return false; - } - }, - }, - ], - headless, - fileParallelism: false, - ui: debuggerEnabled || enableUI ? true : false, - }, - }, - }) -); diff --git a/vitest.config.unit.ts b/vitest.config.unit.ts index 3012a075..2b1b243e 100644 --- a/vitest.config.unit.ts +++ b/vitest.config.unit.ts @@ -20,7 +20,7 @@ export default mergeConfig( // maxConcurrency: 2, name: "unit-tests", include: ["**/*unit.test.ts", "**/*.unit.spec.ts"], - exclude: ["test/**", "src/apps/**/testdeno/**"], + exclude: ["node_modules/**", "test/**", "src/apps/**/testdeno/**"], coverage: { include: ["src/**/*.ts"], exclude: [ @@ -28,12 +28,8 @@ export default mergeConfig( "**/*unit.test.ts", "**/*.unit.spec.ts", "test/**", - "src/lib/**/*.test.ts", "**/_*", "src/apps/**/testdeno/**", - // "src/apps/**", - // "src/cli/**", - "src/lib/src/cli/**", "**/*_obsolete.ts", ...importOnlyFiles, ],
a
  • + {renderIcon(item)} + {item.title} +