diff --git a/.gitignore b/.gitignore index a6bced9..c443c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ cov_profile/** coverage src/apps/cli/dist/* _testdata/** -utils/bench/splitResults.csv \ No newline at end of file +utils/bench/splitResults.csv +.eslintcache \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a14a132 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,65 @@ +# AI Coding Assistant Instructions (AGENTS.md) + +When working on this repository (writing code, comments, documentation, or commits), you MUST follow these guidelines to maintain consistency. + +## Required Reference Files + +Before making changes to documentation, user-facing text, or settings: +1. Read [docs/terms.md](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. + +--- + +## Documentation and User-Facing Text Rules + +Always adhere to the following stylistic and spelling rules: + +1. **British English Spelling**: + - Write all documentation and user-facing messages in British English. If in doubt, the BBC News Styleguide may be useful as a reference. + - **Traditional Spelling (Trad-spelling)**: Use `-ise` and `-isation` suffixes instead of `-ize` and `-ization` (for example: 'initialisation', 'synchronisation', and 'organisation'). + - **Oxford Comma**: Use the serial (Oxford) comma to separate items in lists of three or more (for example: 'settings, snippets, and themes' instead of 'settings, snippets and themes'). + - **Logical Punctuation**: Place punctuation marks (such as commas and full stops) outside quotation marks unless they are part of the quoted text itself (for example: write 'dialogue', not 'dialogue,'). + +2. **No Contractions**: + - Do not use contractions in general text or documentation (for example: write "do not" instead of "don't", "cannot" instead of "can't", and "is not" instead of "isn't"). + +3. **Quotation Style**: + - Prefer single quotation marks (`'`) over double quotation marks (`"`) in general documentation text, unless the context requires double quotes (for example, inside JSON code blocks). + +4. **Specific Terminology and Spelling**: + - Use **'dialogue'** in documentation, user-facing messages, and general text. Use **'dialog'** only inside source code (e.g. class names, methods). + - Use the hyphenated form **'plug-in'** in user-facing text. Use **'plugin'** only in codebase files, configuration settings, or technical contexts. + +--- + +## Technical & Architecture Rules + +1. **Database Structure**: + - Remember that Self-hosted LiveSync splits files into **Metadata** (file properties, size, paths) and **Chunks** (actual content). Do not store raw content in the metadata document directly. +2. **Setup and Recovery**: + - **Fast Setup (Simple Fetch)** is the preferred flow for initial replication on secondary devices. It utilises stream-based replication for high speed and delays local file reflection to suppress temporary synchronisation warnings. + - **Flag files** (such as `redflag.md`, `redflag2.md`, and `redflag3.md`) at the root of the vault control the boot-up sequence and trigger automated fetch/rebuild tasks. +3. **Subrepositories**: + - 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. +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. + - `webapp`: A Web-based application. + - `webpeer`: A Web-based peer utility. + +--- + +## Development & Verification Commands + +Before submitting code, you should run verification scripts locally to ensure correct syntax and function. + +1. **Lint and Type Checking**: + - Run `npm run check` to perform code verification. This runs type-checking (`tsc-check`), ESLint (`lint`), and Svelte checks (`svelte-check`). +2. **Unit Tests**: + - Run `npm run test:unit` to execute fast local unit tests. + - Run `npm run test` or `npm run test:full` for full testing suites (including dockerised services). +3. **Build**: + - Run `npm run build` to compile the production bundle (`main.js`). + - Run `npm run dev` for the development watch/build task. diff --git a/README.md b/README.md index ed2540e..3f1ecdc 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. Documentations is available for [Peer-to-Peer Synchronisation](./docs/p2p_sync_updates_2026.md). +Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling you to synchronise your notes directly between devices without relying on a server. Documentation is available for [Peer-to-Peer Synchronisation](./docs/p2p_sync_updates_2026.md). ![obsidian_live_sync_demo](https://user-images.githubusercontent.com/45774780/137355323-f57a8b09-abf2-4501-836c-8cb7d2ff24a3.gif) @@ -25,17 +25,17 @@ Additionally, it supports peer-to-peer synchronisation using WebRTC, enabling yo - 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 blog site). This is also peer-to-peer. Feel free to use it. + - A pre-built instance is available at [fancy-syncing.vrtmrz.net/webpeer](https://fancy-syncing.vrtmrz.net/webpeer/) (hosted on the vrtmrz's blog site). This is also peer-to-peer. Feel free to use it. - For more information, refer to the [English explanatory article](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync-en.html) or the [Japanese explanatory article](https://fancy-syncing.vrtmrz.net/blog/0034-p2p-sync). This plug-in may be particularly useful for researchers, engineers, and developers who need to keep their notes fully self-hosted for security reasons. It is also suitable for anyone seeking the peace of mind that comes with knowing their notes remain entirely private. >[!IMPORTANT] > - Before installing or upgrading this plug-in, please back up your vault. -> - Do not enable this plug-in alongside another synchronisation solution at the same time (including iCloud and Obsidian Sync). +> - Do not enable this plug-in alongside another synchronisation solution (including iCloud and Obsidian Sync). > - For backups, we also provide a plug-in called [Differential ZIP Backup](https://github.com/vrtmrz/diffzip). -## How to use +## How to Use ### 3-minute setup - CouchDB on fly.io @@ -43,54 +43,55 @@ This plug-in may be particularly useful for researchers, engineers, and develope [![LiveSync Setup onto Fly.io SpeedRun 2024 using Google Colab](https://img.youtube.com/vi/7sa_I1832Xc/0.jpg)](https://www.youtube.com/watch?v=7sa_I1832Xc) -1. [Setup CouchDB on fly.io](docs/setup_flyio.md) +1. [Set up CouchDB on fly.io](docs/setup_flyio.md) 2. Configure plug-in in [Quick Setup](docs/quick_setup.md) -### Manually Setup +### Manual Setup -1. Setup the server - 1. [Setup CouchDB on fly.io](docs/setup_flyio.md) - 2. [Setup your CouchDB](docs/setup_own_server.md) +1. Set up the server + 1. [Set up CouchDB on fly.io](docs/setup_flyio.md) + 2. [Set up your CouchDB](docs/setup_own_server.md) 2. Configure plug-in in [Quick Setup](docs/quick_setup.md) > [!TIP] -> Fly.io is no longer free. Fortunately, despite some issues, we can still use IBM Cloudant. Refer to [Setup IBM Cloudant](docs/setup_cloudant.md). -> And also, we can use peer-to-peer synchronisation without a server. Or very cheap Object Storage -- Cloudflare R2 can be used for free. -> HOWEVER, most importantly, we can use the server that we trust. Therefore, please set up your own server. -> CouchDB can be run on a Raspberry Pi. (But please be careful about the security of your server). +> Fly.io is no longer free. Fortunately, we can still use IBM Cloudant despite some limitations. Refer to [Set up IBM Cloudant](docs/setup_cloudant.md). +> We can also use peer-to-peer synchronisation without a server. Alternatively, cheap object storage like Cloudflare R2 can be used for free. +> However, most importantly, we can use a server that we trust. Therefore, please set up your own server. +> CouchDB can also be run on a Raspberry Pi (please be mindful of your server's security). -## Information in StatusBar +## Information in the Status Bar -Synchronization status is shown in the status bar with the following icons. +Synchronisation status is shown in the status bar with the following icons. - Activity Indicator - 📲 Network request - Status - ⏹ Stopped - 💀 LiveSync enabled. Waiting for changes - - ⚡ Synchronization in progress + - ⚡ Synchronisation in progress - ⚠ An error occurred -- Statistical indicator +- Statistical Indicators - ↑ Uploaded chunks and metadata - ↓ Downloaded chunks and metadata -- Progress indicator +- Progress Indicators - 📥 Unprocessed transferred items - 📄 Working database operation - 💟 Working write storage processes - ⏳ Working read storage processes - 🛫 Pending read storage processes - 📬 Batched read storage processes - - ⚙ Working or pending storage processes of hidden files + - ⚙ Working or pending storage processes for hidden files - 🧩 Waiting chunks - - 🔌 Working Customisation items (Configuration, snippets, and plug-ins) + - 🔌 Working customisation items (configuration, snippets, and plug-ins) -To prevent file and database corruption, please wait to stop Obsidian until all progress indicators have disappeared as possible (The plugin will also try to resume, though). Especially in case of if you have deleted or renamed files. +To prevent file and database corruption, please avoid closing Obsidian until all progress indicators have disappeared as much as possible (although the plug-in will attempt to resume if interrupted). This is especially important if you have deleted or renamed files. ## Tips and Troubleshooting -If you are having problems getting the plugin working see: [Tips and Troubleshooting](docs/troubleshooting.md). +- If you want a faster and simpler initial replication when setting up subsequent devices, see the [Fast Setup Guide](docs/tips/fast-setup.md). +- If you are having problems getting the plug-in working, see [Tips and Troubleshooting](docs/troubleshooting.md). ## Acknowledgements -The project has been in continual progress and harmony thanks to: +The project has been in continual progress and harmony thanks to the following: - Many [Contributors](https://github.com/vrtmrz/obsidian-livesync/graphs/contributors). - Many [GitHub Sponsors](https://github.com/sponsors/vrtmrz#sponsors). - JetBrains Community Programs / Support for Open-Source Projects. JetBrains logo @@ -98,7 +99,7 @@ The project has been in continual progress and harmony thanks to: May those who have contributed be honoured and remembered for their kindness and generosity. ## Development Guide -Please refer to [Development Guide](devs.md) for development setup, testing infrastructure, code conventions, and more. +Please refer to the [Development Guide](devs.md) for development setup, testing infrastructure, code conventions, and more. ## License diff --git a/README_ja.md b/README_ja.md index 3ec21b7..47e3f5a 100644 --- a/README_ja.md +++ b/README_ja.md @@ -78,7 +78,8 @@ NDAや類䌌の契玄や矩務、倫理を守る必芁のある、研究者、 ## Tips and Troubleshooting -䜕かこたったら、[Tips and Troubleshooting](docs/troubleshooting.md)をご参照ください。 +- 2台目以降のセットアップ時に、初期同期をより迅速か぀簡単に行うには、[ファストセットアップガむド](docs/tips/fast-setup_ja.md)をご参照ください。 +- 䜕かこたったら、[Tips and Troubleshooting](docs/troubleshooting.md)をご参照ください。 ## License diff --git a/devs.md b/devs.md index d9d9ef1..3363c22 100644 --- a/devs.md +++ b/devs.md @@ -3,6 +3,76 @@ Self-hosted LiveSync is an Obsidian plugin for synchronising vaults across devices using CouchDB, MinIO/S3, or peer-to-peer WebRTC. The codebase uses a modular architecture with TypeScript, Svelte, and PouchDB. +## Build & Development Workflow + +### Environment Setup + +#### First-time Setup + +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 +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. +```bash +git checkout --recurse-submodules 0.25.70-patch1 # tag or branch name +npm ci +npm run build +``` + +### Commands + +```bash +npm run test:unit # Run unit tests with vitest (or `npm run test:unit:coverage` for coverage) +npm run check # TypeScript and svelte type checking +npm run dev # Development build with auto-rebuild (uses .env for test vault paths) +npm run build # Production build +npm run buildDev # Development build (one-time) +npm run 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. +``` + +### Tips + +We can use CLI's E2E test command instead of `npm test`. + +### Auto-copy to test vaults + +To facilitate development and testing, the build process can automatically copy the built plugin to specified test vault + +- Create `.env` file with `PATHS_TEST_INSTALL` pointing to test vault plug-in directories (`:` separated on Unix, `;` on Windows) +- Development builds auto-copy to these paths on build whilst `npm run dev` is running (watch mode) + +### Testing Infrastructure + +- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~ + - This is now obsolete, migrated to vitest. +- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests. + - Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`). + +- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services: + ```bash + npm run test:docker-all:start # Start all test services + npm run test:full # Run tests with coverage + npm run test:docker-all:stop # Stop services + ``` + If some services are not needed, start only required ones (e.g., `test:docker-couchdb:start`) + Note that if services are already running, starting script will fail. Please stop them first. + +- **Test Structure**: + - `test/suite/` - Integration tests for sync operations + - `test/unit/` - Unit tests (via vitest, as harness is browser-based) + - `test/harness/` - Mock implementations (e.g., `obsidian-mock.ts`) + + + ## Architecture ### Module System @@ -17,7 +87,7 @@ The plugin uses a dynamic module system to reduce coupling and improve maintaina - `coreObsidian/` - Obsidian-specific core (e.g., `ModuleFileAccessObsidian`) - `essential/` - Required modules (e.g., `ModuleMigration`, `ModuleKeyValueDB`) - `features/` - Optional features (e.g., `ModuleLog`, `ModuleObsidianSettings`) - - `extras/` - Development/testing tools (e.g., `ModuleDev`, `ModuleIntegratedTest`) + - `extras/` - Development/testing tools (e.g., `ModuleDev`, ~~`ModuleIntegratedTest`~~) - **Services**: Core services (e.g., `database`, `replicator`, `storageAccess`) are registered in `ServiceHub` and accessed by modules. They provide an extension point for add new behaviour without modifying existing code. - For example, checks before the replication can be added to the `replication.onBeforeReplicate` handler, and the handlers can be return `false` to prevent replication-starting. `vault.isTargetFile` also can be used to prevent processing specific files. - **ServiceModule**: A new type of module that directly depends on services. @@ -47,48 +117,6 @@ Hence, the new feature should be implemented as follows: - **Development code**: Use `.dev.ts` suffix (replaced with `.prod.ts` in production) - **Path aliases**: `@/*` maps to `src/*`, `@lib/*` maps to `src/lib/src/*` -## Build & Development Workflow - -### Commands - -```bash -npm run test:unit # Run unit tests with vitest (or `npm run test:unit:coverage` for coverage) -npm run check # TypeScript and svelte type checking -npm run dev # Development build with auto-rebuild (uses .env for test vault paths) -npm run build # Production build -npm run buildDev # Development build (one-time) -npm run bakei18n # Pre-build step: compile i18n resources (YAML → JSON → TS) -npm test # Run vitest tests (requires Docker services) -``` - -### Environment Setup - -- Clone with submodules: `git clone --recurse-submodules ` -- If you already cloned without them, run: `git submodule update --init --recursive` -- The shared common library is provided by the `src/lib` submodule, and builds will fail if it is missing -- Create `.env` file with `PATHS_TEST_INSTALL` pointing to test vault plug-in directories (`:` separated on Unix, `;` on Windows) -- Development builds auto-copy to these paths on build - -### Testing Infrastructure - -- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~ - - This is now obsolete, migrated to vitest. -- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests. - - Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`). - -- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services: - ```bash - npm run test:docker-all:start # Start all test services - npm run test:full # Run tests with coverage - npm run test:docker-all:stop # Stop services - ``` - If some services are not needed, start only required ones (e.g., `test:docker-couchdb:start`) - Note that if services are already running, starting script will fail. Please stop them first. -- **Test Structure**: - - `test/suite/` - Integration tests for sync operations - - `test/unit/` - Unit tests (via vitest, as harness is browser-based) - - `test/harness/` - Mock implementations (e.g., `obsidian-mock.ts`) - ## Code Conventions ### Internationalisation (i18n) @@ -156,17 +184,17 @@ export class ModuleExample extends AbstractObsidianModule { ## Beta Policy -- Beta versions are denoted by appending `+patchedN` to the base version number. +- 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. + - 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`. + - 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 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. diff --git a/docs/p2p_sync_updates_2026.md b/docs/p2p_sync_updates_2026.md index b6f3903..9507360 100644 --- a/docs/p2p_sync_updates_2026.md +++ b/docs/p2p_sync_updates_2026.md @@ -53,7 +53,7 @@ This command synchronises with every peer whose **SYNC** toggle is enabled in th *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 plugin more stable across different platforms (Mobile, Desktop, and Web). +- **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. diff --git a/docs/quick_setup.md b/docs/quick_setup.md index c6e1aef..f56f575 100644 --- a/docs/quick_setup.md +++ b/docs/quick_setup.md @@ -2,7 +2,7 @@ [Japanese docs](./quick_setup_ja.md) - [Chinese docs](./quick_setup_cn.md). -The plugin has so many configuration options to deal with different circumstances. However, only a few settings are required in the normal cases. Therefore, `The Setup wizard` has been implemented to simplify the setup. +The plug-in has so many configuration options to deal with different circumstances. However, only a few settings are required in the normal cases. Therefore, `The Setup wizard` has been implemented to simplify the setup. ![](../images/quick_setup_1.png) @@ -10,7 +10,7 @@ There are three methods to set up Self-hosted LiveSync. 1. [Using setup URIs](#1-using-setup-uris) *(Recommended)* 2. [Minimal setup](#2-minimal-setup) -3. [Full manually setup the and Enable on this dialogue](#3-manually-setup) +3. [Fully manual setup and enabling on this dialogue](#3-manually-setup) ## At the first device @@ -24,7 +24,7 @@ There are three methods to set up Self-hosted LiveSync. In this procedure, [this video](https://youtu.be/7sa_I1832Xc?t=146) may help us. -1. Click `Use` button (Or launch `Use the copied setup URI` from Command palette). +1. Click the `Use` button (or launch the `Use the copied setup URI (Formerly Open setup URI)` command from the command palette). 2. Paste the Setup URI into the dialogue 3. Type the passphrase of the Setup URI 4. Answer `yes` for `Importing LiveSync's conf, OK?`. @@ -107,23 +107,27 @@ Note: If you are going to use Object Storage, you cannot select `LiveSync`. Select any synchronisation methods we want to use and `Apply`. If database initialisation is required, it will be performed at this time. When `All done!` is displayed, we are ready to synchronise. -The dialogue of `Copy settings as a new setup URI` will be open automatically. Please input a passphrase to encrypt the new `Setup URI`. (This passphrase is to encrypt the setup URI, not the vault). +The dialogue of `Copy current settings as a new setup URI` will open automatically. Please input a passphrase to encrypt the new `Setup URI`. (This passphrase is to encrypt the setup URI, not the vault). ![](../images/quick_setup_10.png) The Setup URI will be copied to the clipboard, please make a note(Not in Obsidian) of this. >[!TIP] -We can copy this in any time by `Copy current settings as a new setup URI`. +We can copy this at any time by running the "Copy settings as a new setup URI" command from the command palette (or clicking the "Copy the current settings to a Setup URI" button in the settings UI). ### 3. Manually setup It is strongly recommended to perform a "minimal set-up" first and set up the other contents after making sure has been synchronised. However, if you have some specific reasons to configure it manually, please click the `Enable` button of `Enable LiveSync on this device as the set-up was completed manually`. -And, please copy the setup URI by `Copy current settings as a new setup URI` and make a note(Not in Obsidian) of this. +And, please copy the setup URI by running the "Copy settings as a new setup URI" command (or using the "Copy the current settings to a Setup URI" button) and make a note(Not in Obsidian) of this. ## At the subsequent device After installing Self-hosted LiveSync on the first device, we should have a setup URI. **The first choice is to use it**. Please share it with the device you want to setup. It is completely same as [Using setup URIs on the first device](#1-using-setup-uris). Please refer it. + +> [!TIP] +> **Fast Setup (Simple Fetch)** +> In recent versions, when you import a Setup URI or trigger a Fetch All, the plug-in boots in scheduled fetch mode and runs a simplified **Fast Setup** process. This allows you to choose your sync strategy with a single dialogue and performs initial synchronisation in one step. Refer to the [Fast Setup Guide](./tips/fast-setup.md) for more details. diff --git a/docs/quick_setup_ja.md b/docs/quick_setup_ja.md index 8af0779..06c2644 100644 --- a/docs/quick_setup_ja.md +++ b/docs/quick_setup_ja.md @@ -1,6 +1,6 @@ # Quick setup このプラグむンには、いろいろな状況に察応するための非垞に倚くの蚭定オプションがありたす。しかし、実際に䜿甚する蚭定項目はそれほど倚くはありたせん。そこで、初期蚭定を簡略化するために、「セットアップりィザヌド」を実装しおいたす。 -※なお、次のデバむスからは、`Copy setup URI`ず`Open setup URI`を䜿っおセットアップしおください。 +※なお、次のデバむスからは、`珟圚の蚭定をセットアップURIにコピヌ`ず`セットアップURIで接続`を䜿っおセットアップしおください。 ## Wizardの䜿い方 @@ -71,7 +71,8 @@ Fixボタンがなくなり、すべおチェックマヌクになれば完了 ![](../images/quick_setup_9_1.png) Presetsから、いずれかの同期方法を遞び`Apply`を行うず、必芁に応じおロヌカル・リモヌトのデヌタベヌスを初期化・構築したす。 -All done! ず衚瀺されれば完了です。自動的に、`Copy setup URI`が開き、`Setup URI`を暗号化するパスフレヌズを聞かれたす。 +「All done!」日本語環境では「完了」ず衚瀺されれば完了です。自動的に、「珟圚の蚭定をセットアップURIにコピヌ」のダむアログが開き、Setup URIを暗号化するためのパスフレヌズを求められたすこのパスフレヌズはSetup URIを暗号化するためのもので、Vault自䜓の暗号化キヌではありたせん。 +パスフレヌズを入力するず、クリップボヌドにSetup URIが保存されたすので、これを2台目以降のデバむスに䜕らかの方法で転送しおください。 ![](../images/quick_setup_10.png) @@ -79,10 +80,14 @@ All done! ず衚瀺されれば完了です。自動的に、`Copy setup URI`が クリップボヌドにSetup URIが保存されたすので、これを2台目以降のデバむスに䜕らかの方法で転送しおください。 # 2台目以降の蚭定方法 -2台目の端末にSelf-hosted LiveSyncをむンストヌルしたあず、コマンドパレットから`Open setup URI`を遞択し、転送したsetup URIを入力したす。その埌、パスフレヌズを入力するずセットアップ甚のりィザヌドが開きたす。 +2台目の端末にSelf-hosted LiveSyncをむンストヌルしたあず、コマンドパレットから`Use the copied setup URI (Formerly Open setup URI)`を遞択し、転送したsetup URIを入力したす。その埌、パスフレヌズを入力するずセットアップ甚のりィザヌドが開きたす。 䞋蚘のように答えおください。 - `Importing LiveSync's conf, OK?` に `Yes` - `How would you like to set it up?` に `Set it up as secondary or subsequent device` -これで蚭定が反映され、レプリケヌションが開始されたす。 \ No newline at end of file +これで蚭定が反映され、レプリケヌションが開始されたす。 + +> [!TIP] +> **ファストセットアップ (Fast Setup)** +> 近幎のバヌゞョンでは、セットアップURIの読み蟌みやデヌタの党取埗Fetch Allを実行した際、より簡単に同期戊略を遞択しお即座に初期同期を完了できる **ファストセットアップ (Simple Fetch)** フロヌが利甚できたす。詳现は [ファストセットアップガむド](./tips/fast-setup_ja.md) をご参照ください。 \ No newline at end of file diff --git a/docs/settings.md b/docs/settings.md index 29fbcd6..e0f2ca5 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -12,7 +12,7 @@ There are many settings in Self-hosted LiveSync. This document describes each se | 🛰 | [3. Remote Configuration](#3-remote-configuration) | | 🔄 | [4. Sync Settings](#4-sync-settings) | | 🚊 | [5. Selector (Advanced)](#5-selector-advanced) | -| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) | +| 🔌 | [6. Customisation sync (Advanced)](#6-customisation-sync-advanced) | | 🧰 | [7. Hatch](#7-hatch) | | 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) | | 💪 | [9. Power users (Power User)](#9-power-users-power-user) | @@ -68,7 +68,7 @@ Following panes will be shown when you enable this setting. | Icon | Description | | :--: | ------------------------------------------------------------------ | | 🚊 | [5. Selector (Advanced)](#5-selector-advanced) | -| 🔌 | [6. Customization sync (Advanced)](#6-customization-sync-advanced) | +| 🔌 | [6. Customisation sync (Advanced)](#6-customisation-sync-advanced) | | 🔧 | [8. Advanced (Advanced)](#8-advanced-advanced) | #### Enable poweruser features @@ -120,6 +120,18 @@ Setting key: showStatusOnStatusbar We can show the status of synchronisation on the status bar. (Default: On) +#### Show status icon instead of file warnings banner + +Setting key: hideFileWarningNotice + +If enabled, the ⛔ icon will be shown inside the status instead of the file warnings banner. No details will be shown. + +#### Network warning style + +Setting key: networkWarningStyle + +How to display network errors when the sync server is unreachable. + ### 2. Logging #### Show only notifications @@ -138,11 +150,19 @@ Show verbose log. Please enable when you report the logs ### 1. Remote Server +Self-hosted LiveSync supports multiple remote connection profiles under **Remote Server** -> **Remote Databases**. This allows you to save and switch between multiple databases or bucket configurations in a single vault. + +- **➕ Add new connection**: Create a new connection profile by launching the setup dialogue. +- **📥 Import connection**: Paste a connection string (e.g., `sls+https://...`, `sls+s3://...`, `sls+p2p://...`) to import a remote configuration profile. +- **🔧 Configure**: Open the setup dialogue to edit settings for the selected connection profile. +- **✅ Activate**: Select and activate this profile as the current active remote. +- **🗑 Delete**: Remove this connection profile from the list. + #### Remote Type Setting key: remoteType -Remote server type +The active remote server type. This is automatically projected to the legacy configuration when you activate a connection profile. ### 2. Notification @@ -172,6 +192,14 @@ Setting key: usePathObfuscation In default, the path of the file is not obfuscated to improve the performance. If you enable this, the path of the file will be obfuscated. This is useful when you want to hide the path of the file. +#### Encryption Algorithm + +Setting key: E2EEAlgorithm + +The encryption algorithm version used for end-to-end encryption. +- `v2` (V2: AES-256-GCM With HKDF): Recommended and default version. +- `forceV1` or `""` (V1: Legacy): Older legacy encryption. Only use this if you have an existing vault encrypted in the legacy format. + #### Use dynamic iteration count (Experimental) Setting key: useDynamicIterationCount @@ -192,30 +220,62 @@ Fetch necessary settings from already configured remote server. ### 5. Minio,S3,R2 +These settings are configured within the S3/MinIO/R2 Setup dialogue when adding (`➕`) or editing (`🔧`) an Object Storage connection profile. + #### Endpoint URL Setting key: endpoint +The URL of the remote storage endpoint. +Note: Only Secure (HTTPS) connections can be used on Obsidian Mobile. + #### Access Key Setting key: accessKey +The Access Key ID used for authentication. + #### Secret Key Setting key: secretKey +The Secret Access Key used for authentication. + #### Region Setting key: region +The storage region (e.g., `us-east-1`, or `auto` for Cloudflare R2). + #### Bucket Name Setting key: bucket +The name of the bucket to store synchronised files. + #### Use Custom HTTP Handler Setting key: useCustomRequestHandler -Enable this if your Object Storage doesn't support CORS + +This option is labeled **Use internal API** in the setup dialogue. Enable this if your Object Storage does not support CORS. It uses Obsidian's internal API to communicate with the S3 server, which is not compliant with web standards but can bypass CORS restrictions. Note that this might break in future Obsidian versions. + +#### File prefix on the bucket + +Setting key: bucketPrefix + +This option is labeled **Folder Prefix** in the setup dialogue. Effectively a directory. Should end with `/`. e.g., `vault-name/`. Leave blank to store data at the root of the bucket. + +#### Enable forcePathStyle + +Setting key: forcePathStyle + +This option is labeled **Use Path-Style Access** in the setup dialogue. If enabled, the forcePathStyle option will be used for bucket operations. + +#### Custom Headers + +Setting key: bucketCustomHeaders + +Custom HTTP headers to include in every request sent to the Object Storage bucket. Specify them in the format `Header-Name: Value`, with each header on a new line. #### Test Connection @@ -223,24 +283,82 @@ Enable this if your Object Storage doesn't support CORS ### 6. CouchDB +These settings are configured within the CouchDB Setup dialogue when adding (`➕`) or editing (`🔧`) a CouchDB connection profile. + #### Server URI Setting key: couchDB_URI +The URI of the CouchDB server. +Note: Only Secure (HTTPS) connections can be used on Obsidian Mobile. The URI must not end with a trailing slash. + #### Username Setting key: couchDB_USER -username + +The username used to authenticate with CouchDB. #### Password Setting key: couchDB_PASSWORD -password + +The password used to authenticate with CouchDB. #### Database Name Setting key: couchDB_DBNAME +The name of the database. +Note: The database name cannot contain capital letters, spaces, or special characters other than `_$()+/-`, and cannot start with an underscore (`_`). + +#### Use Request API to avoid inevitable CORS problem + +Setting key: useRequestAPI + +This option is labeled **Use Internal API** in the setup dialogue. If enabled, Obsidian's internal request API will be used to bypass CORS restrictions. This is a workaround that may not be compliant with web standards and is less secure. Note that this might break in future Obsidian versions. + +#### Custom Headers + +Setting key: couchDB_CustomHeaders + +Custom HTTP headers to include in every request sent to the CouchDB server. Specify them in the format `Header-Name: Value`, with each header on a new line. + +#### Use JWT Authentication + +Setting key: useJWT + +Enable JSON Web Token (JWT) authentication for CouchDB. This is an experimental feature and has not been thoroughly verified. + +#### JWT Algorithm + +Setting key: jwtAlgorithm + +The algorithm used to sign the JWT. Supported algorithms: `HS256`, `HS512`, `ES256`, `ES512`. + +#### JWT Expiration Duration (minutes) + +Setting key: jwtExpDuration + +Token expiration duration in minutes. Set to 0 to disable expiration. + +#### JWT Key + +Setting key: jwtKey + +The secret key (for HS256/HS512) or the PKCS#8 PEM-formatted private key (for ES256/ES512) used to sign the JWT. + +#### JWT Key ID (kid) + +Setting key: jwtKid + +The Key ID (`kid`) header parameter included in the JWT. + +#### JWT Subject (sub) + +Setting key: jwtSub + +The subject (`sub`) claim of the JWT, which should match your CouchDB username. + #### Test Database Connection Open database connection. If the remote database is not found and you have permission to create a database, the database will be created. @@ -251,26 +369,100 @@ Checks and fixes any potential issues with the database config. #### Apply Settings +### 7. Peer-to-Peer (P2P) Synchronisation + +#### Enable P2P Synchronisation + +Setting key: P2P_Enabled + +Enable direct peer-to-peer synchronisation via WebRTC. + +#### Relay URL + +Setting key: P2P_relays + +The WebSocket relay server URL(s) used for coordinating P2P connections via WebRTC. Multiple URLs can be separated by commas. + +#### Group ID + +Setting key: P2P_roomID + +The room ID or Group ID used to identify your group of synchronising devices. All devices you wish to synchronise must use the same Group ID. You can enter any custom string or generate a random Group ID. + +#### Passphrase + +Setting key: P2P_passphrase + +The password or passphrase used to authenticate and encrypt P2P communication. All devices must use the same passphrase. + +#### Device Peer ID + +Setting key: P2P_DevicePeerName + +The peer name or identifier of this device in the P2P network. This should be unique within your group of devices. + +#### Automatically start P2P connection on launch + +Setting key: P2P_AutoStart + +This option is labeled **Auto Start P2P Connection** in the setup dialogue. If enabled, the P2P connection will start automatically when the plug-in launches. + +#### Automatically broadcast changes to connected peers + +Setting key: P2P_AutoBroadcast + +This option is labeled **Auto Broadcast Changes** in the setup dialogue. If enabled, changes will be automatically broadcasted to connected peers, requesting them to fetch the changes. + +#### TURN Server URLs (comma-separated) + +Setting key: P2P_turnServers + +A comma-separated list of TURN/STUN server URLs. Used to relay P2P connections when direct WebRTC connection fails due to strict NAT or firewalls. In most cases, these can be left blank. + +#### TURN Username + +Setting key: P2P_turnUsername + +The username for authentication with the TURN server. + +#### TURN Credential + +Setting key: P2P_turnCredential + +The password or credential for authentication with the TURN server. + ## 4. Sync Settings -### 1. Synchronization Preset +### 1. Synchronisation Preset #### Presets Setting key: preset Apply preset configuration -### 2. Synchronization Method +### 2. Synchronisation Method #### Sync Mode Setting key: syncMode +The trigger mechanism for synchronisation. +- **LiveSync** (`LIVESYNC`): Real-time, continuous, bidirectional synchronisation. + Note: This requires a CouchDB or WebRTC P2P remote server. It is not supported for S3-compatible Object Storage. +- **Periodic Sync** (`PERIODIC`): Synchronisation is performed at regular intervals specified by the **Periodic Sync interval** setting. +- **On Events** (`ONEVENTS`): Synchronisation is triggered by specific events (such as save, file open, or startup) configured via the toggles below. + #### Periodic Sync interval Setting key: periodicReplicationInterval Interval (sec) +#### Minimum interval for syncing + +Setting key: syncMinimumInterval + +The minimum interval for automatic synchronisation on event. + #### Sync on Save Setting key: syncOnSave @@ -323,7 +515,7 @@ Move remotely deleted files to the trash, instead of deleting. #### Keep empty folder Setting key: doNotDeleteFolder -Should we keep folders that don't have any files inside? +Should we keep folders that do not have any files inside? ### 5. Conflict resolution (Advanced) @@ -360,7 +552,7 @@ Setting key: notifyAllSettingSyncFile ### 7. Hidden Files (Advanced) -#### Hidden file synchronization +#### Hidden file synchronisation #### Enable Hidden files sync @@ -373,6 +565,12 @@ Setting key: syncInternalFilesBeforeReplication Setting key: syncInternalFilesInterval Seconds, 0 to disable +#### Suppress notification of hidden files change + +Setting key: suppressNotifyHiddenFilesChange + +If enabled, the notification of hidden files change will be suppressed. + ## 5. Selector (Advanced) ### 1. Normal Files @@ -406,42 +604,42 @@ Comma separated `.gitignore, .dockerignore` #### Add default patterns -## 6. Customization sync (Advanced) +## 6. Customisation sync (Advanced) -### 1. Customization Sync +### 1. Customisation Sync #### Device name Setting key: deviceAndVaultName -Unique name between all synchronized devices. To edit this setting, please disable customization sync once. +Unique name between all synchronised devices. To edit this setting, please disable customisation sync once. -#### Per-file-saved customization sync +#### Per-file-saved customisation sync Setting key: usePluginSyncV2 -If enabled per-filed efficient customization sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enabled this, we lost a compatibility with old versions. +If enabled, per-file efficient customisation sync will be used. We need a small migration when enabling this. And all devices should be updated to v0.23.18. Once we enable this, we lose compatibility with old versions. -#### Enable customization sync +#### Enable customisation sync Setting key: usePluginSync -#### Scan customization automatically +#### Scan customisation automatically Setting key: autoSweepPlugins -Scan customization before replicating. +Scan customisation before replicating. -#### Scan customization periodically +#### Scan customisation periodically Setting key: autoSweepPluginsPeriodic -Scan customization every 1 minute. +Scan customisation every 1 minute. -#### Notify customized +#### Notify customised Setting key: notifyPluginOrSettingUpdated -Notify when other device has newly customized. +Notify when another device has newly customised. #### Open -Open the dialog +Open the dialogue ## 7. Hatch @@ -456,14 +654,18 @@ Warning! This will have a serious impact on performance. And the logs will not b ### 2. Scram Switches +Emergency controls to suspend synchronisation processes in order to prevent database corruption. If a critical mismatch or sync error occurs, the plug-in may automatically enter a Scram state and suspend operations. + #### Suspend file watching Setting key: suspendFileWatching -Stop watching for file changes. + +Stop watching for local file changes. #### Suspend database reflecting Setting key: suspendParseReplicationResult + Stop reflecting database changes to storage files. ### 3. Recovery and Repair @@ -486,7 +688,7 @@ Compare the content of files between on local database and storage. If not match #### Back to non-configured -#### Delete all customization sync data +#### Delete all customisation sync data ## 8. Advanced (Advanced) @@ -507,6 +709,12 @@ Setting key: hashCacheMaxAmount Setting key: customChunkSize +#### Chunk Splitter + +Setting key: chunkSplitterVersion + +Select the chunk splitter version; V3 is the most efficient. If you experience issues, please choose Default or Legacy. + #### Use splitting-limit-capped chunk splitter Setting key: enableChunkSplitterV2 @@ -532,6 +740,12 @@ Setting key: concurrencyOfReadChunksOnline Setting key: minimumIntervalOfReadChunksOnline +#### Maximum size of chunks to send in one request + +Setting key: sendChunksBulkMaxSize + +Limit the maximum size of chunks to send in a single bulk request (MB). + ## 9. Power users (Power User) ### 1. Remote Database Tweak @@ -639,7 +853,7 @@ If this enabled, All files are handled as case-Sensitive (Previous behaviour). ### 4. Compatibility (Internal API Usage) -#### Scan changes on customization sync +#### Scan changes on customisation sync Setting key: watchInternalFileChanges Do not use internal API @@ -664,7 +878,13 @@ Setting key: doNotSuspendOnFetching #### Keep empty folder Setting key: doNotDeleteFolder -Should we keep folders that don't have any files inside? +Should we keep folders that do not have any files inside? + +#### Process files even if seems to be corrupted + +Setting key: processSizeMismatchedFiles + +Enable this setting to process files with size mismatches, which can sometimes be created by certain external APIs or integrations. ### 7. Edge case addressing (Processing) @@ -684,17 +904,25 @@ If enabled, the file under 1kb will be processed in the UI thread. Setting key: disableCheckingConfigMismatch +### 9. Remediation + +#### Maximum file modification time for reflected file events + +Setting key: maxMTimeForReflectEvents + +Files with modification times greater than this value (in seconds since the Unix epoch) will not have their events reflected. Set to 0 to disable this limit. + ## 11. Maintenance ### 1. Scram! #### Lock Server -Lock the remote server to prevent synchronization with other devices. +Lock the remote server to prevent synchronisation with other devices. #### Emergency restart -Disables all synchronization and restart. +Disables all synchronisation and restart. ### 2. Syncing @@ -712,17 +940,13 @@ Initialise journal sent history. On the next sync, every item except this device ### 3. Rebuilding Operations (Local) -#### Fetch from remote +#### Reset Synchronisation on This Device Restore or reconstruct local database from remote. -#### Fetch rebuilt DB (Save local documents before) - -Restore or reconstruct local database from remote database but use local chunks. - ### 4. Total Overhaul -#### Rebuild everything +#### Overwrite Server Data with This Device's Files Rebuild local and remote database with local files. @@ -752,7 +976,7 @@ Delete all data on the remote server. #### Run database cleanup -Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Rebuild everything' under Total Overhaul. +Attempt to shrink the database by deleting unused chunks. This may not work consistently. Use the 'Overwrite Server Data with This Device's Files' under Reset Synchronisation information. ### 7. Reset diff --git a/docs/settings_ja.md b/docs/settings_ja.md index d7b9cf3..8d75dd7 100644 --- a/docs/settings_ja.md +++ b/docs/settings_ja.md @@ -3,23 +3,133 @@ # このプラグむンの蚭定項目 ## Remote Database Configurations -同期先のデヌタベヌス蚭定を行いたす。䜕らかの同期が有効になっおいる堎合は線集できないため、同期を解陀しおから行っおください。 +同期先のデヌタベヌス蚭定Remote Serverを行いたす。 -### URI -CouchDBのURIを入力したす。Cloudantの堎合は「External Endpoint(preferred)」になりたす。 -**スラッシュで終わっおはいけたせん。** -こちらにデヌタベヌス名を含めおもかたいたせん。 +珟圚のバヌゞョンでは、耇数のリモヌト接続蚭定接続プロファむルを登録・管理し、切り替えお䜿甚するこずが可胜です「Remote Databases」リスト。 -### Username -ナヌザヌ名を入力したす。このナヌザヌは管理者暩限があるこずが望たしいです。 +- **➕ 新芏接続を远加 (Add new connection)**: 新しい接続蚭定を䜜成し、各セットアップダむアログを起動したす。 +- **📥 接続をむンポヌト (Import connection)**: 接続文字列`sls+https://...`、`sls+s3://...`、`sls+p2p://...`などを貌り付けおむンポヌトしたす。 +- **🔧 蚭定 (Configure)**: セットアップダむアログを開き、遞択した接続プロファむルの蚭定を線集したす。 +- **✅ 有効化 (Activate)**: 遞択したプロファむルをアクティブな同期先ずしお有効化したす。 +- **🗑 削陀 (Delete)**: 接続プロファむルを䞀芧から削陀したす。 -### Password -パスワヌドを入力したす。 +これらの接続プロファむルを远加・線集する際、遞択したデヌタベヌスの皮類CouchDB、S3互換オブゞェクトストレヌゞ、P2Pなどに応じたセットアップダむアログが開きたす。 -### Database Name -同期するデヌタベヌス名を入力したす。 -⚠存圚しない堎合は、テストや接続を行った際、自動的に䜜成されたす[^1]。 -[^1]:暩限がない堎合は自動䜜成には倱敗したす。 +䜕らかの同期が有効になっおいる堎合は線集できないため、同期を解陀しおから行っおください。 + +### CouchDB の蚭定 +CouchDBの各蚭定項目は、接続プロファむルを远加 (➕) たたは蚭定 (🔧) する際に開く **CouchDB セットアップダむアログ** 内で蚭定したす。 + +#### URI +蚭定キヌ: couchDB_URI + +CouchDBの接続先URIです。ダむアログ内では **URL** ず衚蚘されたす。Cloudantの堎合は「External Endpoint (preferred)」になりたす。 +泚意: Obsidian Mobileではセキュア接続 (HTTPS) のみが䜿甚可胜です。たた、末尟にスラッシュ`/`を付けおはいけたせん。 + +#### Username +蚭定キヌ: couchDB_USER + +CouchDBのログむンナヌザヌ名です。ダむアログ内では **Username** ず衚蚘されたす。このナヌザヌには管理者暩限があるこずが望たしいです。 + +#### Password +蚭定キヌ: couchDB_PASSWORD + +CouchDBのログむンパスワヌドです。ダむアログ内では **Password** ず衚蚘されたす。 + +#### Database Name +蚭定キヌ: couchDB_DBNAME + +同期先のデヌタベヌス名です。ダむアログ内では **Database Name** ず衚蚘されたす。 +泚意: デヌタベヌス名には倧文字、スペヌス、および䞀郚の特殊文字`_$()+/-` 以倖は䜿甚できたせん。たた、アンダヌスコア`_`から始めるこずはできたせん。存圚しない堎合は、接続テスト時たたは蚭定適甚時に自動䜜成されたす䜜成暩限が必芁です。 + +#### CORS回避のためにRequest APIを䜿甚する +蚭定キヌ: useRequestAPI + +この項目はセットアップダむアログ内では **Use Internal API** ず衚蚘されたす。有効な堎合、䞍可避なCORS問題を回避するためにObsidianの内郚Request APIを䜿甚したす。これはWeb暙準に準拠しおいない回避策であり、すべおの環境での動䜜を保蚌するものではありたせん。安党性が䜎䞋する可胜性がある点にご泚意ください。将来のObsidianのアップデヌトによっお動䜜しなくなる可胜性がありたす。 + +#### カスタムヘッダヌ +蚭定キヌ: couchDB_CustomHeaders + +CouchDBサヌバヌに送信するすべおのリク゚ストに含めるカスタムHTTPヘッダヌを蚭定したす。ダむアログ内では **Custom Headers** ず衚蚘されたす。`ヘッダヌ名: 倀` の圢匏で、1行に1぀ず぀入力しおください。 + +#### JWT認蚌の䜿甚 (実隓的機胜) +蚭定キヌ: useJWT + +CouchDBでのJSON Web Token (JWT) 認蚌を有効にしたす。ダむアログ内では **Use JWT Authentication** ず衚蚘されたす。十分に怜蚌されおいない実隓的機胜であるため、ご泚意ください。 + +#### JWTアルゎリズム +蚭定キヌ: jwtAlgorithm + +JWTの眲名に䜿甚するアルゎリズムを遞択したす。ダむアログ内では **JWT Algorithm** ず衚蚘されたす。察応アルゎリズム: `HS256`, `HS512`, `ES256`, `ES512` + +#### JWT有効期限 (分) +蚭定キヌ: jwtExpDuration + +トヌクンの有効期限を分単䜍で指定したす。ダむアログ内では **JWT Expiration Duration (minutes)** ず衚蚘されたす。`0` を指定するず有効期限は無効になりたす。 + +#### JWTキヌ +蚭定キヌ: jwtKey + +JWTの眲名に䜿甚する秘密鍵たたはプラむベヌトキヌを指定したす。ダむアログ内では **JWT Key** ず衚蚘されたす。`HS256/HS512` の堎合は共通鍵を、`ES256/ES512` の堎合は pkcs8 PEM圢匏の秘密鍵を入力しおください。 + +#### JWTキヌID (kid) +蚭定キヌ: jwtKid + +JWTヘッダヌに含めるキヌIDを指定したす。ダむアログ内では **JWT Key ID (kid)** ず衚蚘されたす。 + +#### JWTサブゞェクト (sub) +蚭定キヌ: jwtSub + +JWTのサブゞェクト (CouchDBナヌザヌ名) を指定したす。ダむアログ内では **JWT Subject (sub)** ず衚蚘されたす。 + +### Object Storage (Minio, S3, R2) の蚭定 +Object Storageの各蚭定項目は、接続プロファむルを远加 (➕) たたは蚭定 (🔧) する際に開く **S3/MinIO/R2 セットアップダむアログ** 内で蚭定したす。 + +#### ゚ンドポむントURL +蚭定キヌ: endpoint + +S3互換ストレヌゞの゚ンドポむントURLです。ダむアログ内では **Endpoint URL** ず衚蚘されたす。 +泚意: Obsidian Mobileではセキュア接続 (HTTPS) のみが䜿甚可胜です。 + +#### アクセスキヌ ID +蚭定キヌ: accessKey + +認蚌に䜿甚するアクセスキヌIDです。ダむアログ内では **Access Key ID** ず衚蚘されたす。 + +#### シヌクレットアクセスキヌ +蚭定キヌ: secretKey + +認蚌に䜿甚するシヌクレットアクセスキヌです。ダむアログ内では **Secret Access Key** ず衚蚘されたす。 + +#### リヌゞョン +蚭定キヌ: region + +ストレヌゞのリヌゞョンを指定したす䟋: `us-east-1`、Cloudflare R2の堎合は `auto`。ダむアログ内では **Region** ず衚蚘されたす。 + +#### バケット名 +蚭定キヌ: bucket + +同期デヌタを保存するバケット名です。ダむアログ内では **Bucket Name** ず衚蚘されたす。 + +#### カスタムHTTPハンドラヌを䜿甚する +蚭定キヌ: useCustomRequestHandler + +この項目はセットアップダむアログ内では **Use internal API** ず衚蚘されたす。オブゞェクトストレヌゞがCORSをサポヌトしおいない堎合に有効にしたす。Obsidianの内郚APIを䜿甚しおS3サヌバヌず通信するこずでCORS制玄を回避したす。Web暙準には準拠しおいないため、将来のObsidianのアップデヌトによっお動䜜しなくなる可胜性がありたす。 + +#### バケット内のファむルプレフィックス +蚭定キヌ: bucketPrefix + +この項目はセットアップダむアログ内では **Folder Prefix** ず衚蚘されたす。実質的なディレクトリ指定です。末尟は `/` である必芁がありたす䟋`vault-name/`。バケットのルヌトに保存する堎合は空欄のたたにしおください。 + +#### forcePathStyleを有効にする +蚭定キヌ: forcePathStyle + +この項目はセットアップダむアログ内では **Use Path-Style Access** ず衚蚘されたす。有効な堎合、バケット操䜜でforcePathStyleオプションを䜿甚したす。 + +#### カスタムヘッダヌ +蚭定キヌ: bucketCustomHeaders + +オブゞェクトストレヌゞバケットに送信するすべおのリク゚ストに含めるカスタムHTTPヘッダヌを蚭定したす。ダむアログ内では **Custom Headers** ず衚蚘されたす。`ヘッダヌ名: 倀` の圢匏で、1行に1぀ず぀入力しおください。 @@ -30,6 +140,18 @@ CouchDBのURIを入力したす。Cloudantの堎合は「External Endpoint(prefe ### Passphrase 暗号化を行う際に䜿甚するパスフレヌズです。充分に長いものを䜿甚しおください。 +### パスの難読化 +蚭定キヌ: usePathObfuscation + +ダむアログ内では **Obfuscate Properties** ず衚蚘されたす。有効な堎合、リモヌトサヌバヌ䞊でのファむルパスやフォルダ名を難読化暗号化したす。これによりプラむバシヌが向䞊したすが、パフォヌマンスがわずかに䜎䞋する可胜性がありたす。 + +### 暗号化アルゎリズム +蚭定キヌ: E2EEAlgorithm + +ダむアログ内では **Encryption Algorithm** ず衚蚘されたす。゚ンドツヌ゚ンド暗号化に䜿甚する暗号化アルゎリズムのバヌゞョンを遞択したす。 +- `v2` (V2: AES-256-GCM With HKDF): 掚奚されるデフォルトのバヌゞョンです。 +- `forceV1` たたは `""` (V1: Legacy): レガシヌな暗号化バヌゞョンです。叀いバヌゞョンで暗号化された既存の保管庫Vaultを同期する堎合にのみ䜿甚しおください。 + ### Apply End to End 暗号化を行うに圓たっお、異なるパスフレヌズで暗号化された同䞀の内容を入手されるこずは避けるべきです。たた、Self-hosted LiveSyncはコンテンツのcrc32を重耇回避に䜿甚しおいるため、その点でも攻撃が有効になっおしたいたす。 @@ -53,12 +175,66 @@ End to End 暗号化を行うに圓たっお、異なるパスフレヌズで暗 どちらのオペレヌションも、実行するずすべおの同期蚭定が無効化されたす。 + + ### Test Database connection 䞊蚘の蚭定でデヌタベヌスに接続できるか確認したす。 ### Check database configuration ここから盎接CouchDBの蚭定を確認・倉曎できたす。 +### Peer-to-Peer (P2P) 同期の蚭定 + +#### P2P同期を有効にする +蚭定キヌ: P2P_Enabled + +WebRTCを介したデバむス間での盎接的なP2P同期を有効にしたす。ダむアログ内では **Enabled** ず衚蚘されたす。 + +#### リレヌサヌバヌのURL +蚭定キヌ: P2P_relays + +WebRTCによるP2P接続を仲介・調敎するためのWebSocketリレヌサヌバヌのURLを指定したす。ダむアログ内では **Relay URL** ず衚蚘されたす。耇数のURLを指定する堎合はカンマで区切りたす。ダむアログ内のボタンをクリックするず、デフォルトのリレヌサヌバヌを蚭定できたす。 + +#### グルヌプID +蚭定キヌ: P2P_roomID + +同期するデバむス矀を識別するためのルヌムIDたたはグルヌプIDを指定したす。ダむアログ内では **Group ID** ず衚蚘されたす。同期させたいすべおのデバむスで同じグルヌプIDを指定する必芁がありたす。任意のカスタム文字列を入力するか、ランダム生成ボタンで生成できたす。 + +#### パスフレヌズ +蚭定キヌ: P2P_passphrase + +P2P通信の認蚌および暗号化に䜿甚するパスワヌドパスフレヌズを指定したす。ダむアログ内では **Passphrase** ず衚蚘されたす。同期するすべおのデバむスで同じパスフレヌズを指定する必芁がありたす。 + +#### デバむス名 +蚭定キヌ: P2P_DevicePeerName + +P2Pネットワヌク䞊でこのデバむスを識別するための名前を指定したす。ダむアログ内では **Device Peer ID** ず衚蚘されたす。グルヌプ内のデバむス間で重耇しない䞀意の倀を蚭定しおください。 + +#### 起動時のP2P自動接続開始 +蚭定キヌ: P2P_AutoStart + +有効な堎合、プラグむンの起動時に自動的にP2P接続を開始したす。ダむアログ内では **Auto Start P2P Connection** ず衚蚘されたす。 + +#### 接続枈みピアぞの倉曎の自動ブロヌドキャスト +蚭定キヌ: P2P_AutoBroadcast + +有効な堎合、ロヌカルでの倉曎が接続枈みのピアに自動的にブロヌドキャストされたす。ダむアログ内では **Auto Broadcast Changes** ず衚蚘されたす。通知されたピアは倉曎の取埗を開始したす。 + +#### TURNサヌバヌのURL (カンマ区切り) +蚭定キヌ: P2P_turnServers + +ダむアログ内では **TURN Server URLs (comma-separated)** ず衚蚘されたす。厳しいNATやファむアりォヌルがある環境で、WebRTCの盎接接続が確立できない堎合にP2P接続を䞭継するためのTURN/STUNサヌバヌのURLをカンマ区切りで指定したす。通垞は空欄のたたで問題ありたせん。 + +#### TURNナヌザヌ名 +蚭定キヌ: P2P_turnUsername + +TURNサヌバヌでの認蚌に䜿甚するナヌザヌ名を蚭定したす。ダむアログ内では **TURN Username** ず衚蚘されたす。 + +#### TURNパスワヌド +蚭定キヌ: P2P_turnCredential + +TURNサヌバヌでの認蚌に䜿甚するパスワヌドクレデンシャルを蚭定したす。ダむアログ内では **TURN Credential** ず衚蚘されたす。 + ## Local Database Configurations 端末内に䜜成されるデヌタベヌスの蚭定です。 @@ -71,7 +247,8 @@ End to End 暗号化を行うに圓たっお、異なるパスフレヌズで暗 このオプションはLiveSyncず同時には䜿甚できたせん。 ### minimum chunk size ず LongLine threshold -チャンクの分割に぀いおの蚭定です。 +チャンクの分割に぀いおの蚭定です。※珟圚これらの項目はUIから盎接蚭定するこずはできたせんデフォルト倀で自動凊理されたす。 + Self-hosted LiveSyncは䞀぀のチャンクのサむズを最䜎minimum chunk size文字確保した䞊で、できるだけ効率的に同期できるよう、ノヌトを分割しおチャンクを䜜成したす。 これは、同期を行う際に、䞀定の文字数で分割した堎合、先頭の方を線集するず、その埌の分割䜍眮がすべおずれ、結果ずしおほがたるごずのファむルのファむル送受信を行うこずになっおいた問題を避けるために実装されたした。 具䜓的には、先頭から順に盎近の䞋蚘の箇所を怜玢し、䞀番長く切れたものを䞀぀のチャンクずしたす。 @@ -88,6 +265,11 @@ Self-hosted LiveSyncは䞀぀のチャンクのサむズを最䜎minimum chunk s 改行文字ず#を陀き、すべお●に眮換しおも、アルゎリズムは有効に働きたす。 デフォルトは20文字ず、250文字です。 +### チャンクスプリッタヌ +蚭定キヌ: chunkSplitterVersion + +チャンク分割アルゎリズムを遞択したす。V3が最も効率的です。問題が発生した堎合はDefaultたたはLegacyに蚭定しおください。 + ## General Settings 䞀般的な蚭定です。 @@ -97,18 +279,35 @@ Self-hosted LiveSyncは䞀぀のチャンクのサむズを最䜎minimum chunk s ### Vervose log 詳现なログをログに出力したす。 +### ファむル譊告バナヌの代わりにステヌタスアむコンを衚瀺 +蚭定キヌ: hideFileWarningNotice + +有効な堎合、ファむル譊告バナヌの代わりにステヌタス衚瀺内に ⛔ アむコンが衚瀺されたす詳现情報は非衚瀺になりたす。 + +### ネットワヌク譊告のスタむル +蚭定キヌ: networkWarningStyle + +同期サヌバヌに接続できない堎合のネットワヌク゚ラヌの衚瀺方法。 + ## Sync setting 同期に関する蚭定です。 -### LiveSync -LiveSyncを行いたす。 -他の同期方法では、同期の順序が「バヌゞョン確認を行い、ロックが行われおいないか確認した埌、リモヌトの倉曎を受信した埌、デバむスの倉曎を送信する」ずいう挙動になりたす。 +### 同期モヌド (Sync Mode) +蚭定キヌ: syncMode -### Periodic Sync -定期的に同期を行いたす。 +同期凊理を実行するトリガヌずなる条件を蚭定したす。 +- **LiveSync** (`LIVESYNC`): リアルタむムか぀継続的な双方向同期を行いたす。 + 泚意: このモヌドには CouchDB たたは WebRTC P2P リモヌトサヌバヌが必芁です。S3互換オブゞェクトストレヌゞではサポヌトされおいたせん。 +- **Periodic Sync** (`PERIODIC`): **Periodic Sync Interval** で指定した䞀定の間隔ごずに同期凊理を実行したす。 +- **On Events** (`ONEVENTS`): ファむルの保存、ファむルを開く、起動時など、特定のむベントが発生した際に同期をトリガヌしたす詳现は䞋郚の蚭定スむッチで制埡したす。 ### Periodic Sync Interval -定期的に同期を行う堎合の間隔です。 +定期的に同期を行う堎合の間隔秒単䜍です。 + +### 同期の最小間隔 +蚭定キヌ: syncMinimumInterval + +むベント時の自動同期の最小間隔ミリ秒。 ### Sync on Save ファむルが保存されたずきに同期を行いたす。 @@ -146,6 +345,11 @@ Self-hosted LiveSyncは通垞、フォルダ内のファむルがすべお削陀 - Scan hidden files periodicaly. このオプションを有効にするず、n秒おきに隠しファむルをスキャンしたす。 +#### 非衚瀺ファむルの倉曎通知を抑制 +蚭定キヌ: suppressNotifyHiddenFilesChange + +有効な堎合、非衚瀺ファむルの倉曎に関する通知を抑制したす。 + 隠しファむルは胜動的に怜出されないため、スキャンが必芁です。 スキャンでは、ファむルず共にファむルの倉曎時刻を保存したす。もしファむルが消された堎合は、その事実も保存したす。このファむルを蚘録した゚ントリヌがレプリケヌションされた際、ストレヌゞよりも新しい堎合はストレヌゞに反映されたす。 @@ -176,6 +380,45 @@ Self-hosted LiveSyncはPouchDBを䜿甚し、リモヌトず[このプロトコ ### Batch limit 䞀床に凊理するBatchの数です。デフォルトは40です。 +### 1回のリク゚ストで送信するチャンクの最倧サむズ +蚭定キヌ: sendChunksBulkMaxSize + +メガバむトMB単䜍で指定したす。 + +## Customisation Sync (カスタマむズ同期) +プラグむン、ホットキヌ、テヌマ、スニペットなどのObsidianのカスタマむズ蚭定を同期する機胜です以前は **Plugin Sync** ず呌ばれおいたした。 + +### デバむス名 (Device name) +蚭定キヌ: deviceAndVaultName + +同期するすべおのデバむス間で䞀意ずなるデバむス名です。この蚭定を線集するには、䞀床カスタマむズ同期を無効にする必芁がありたす。 + +### ファむル保存ごずのカスタマむズ同期 (Per-file-saved customisation sync) +蚭定キヌ: usePluginSyncV2 + +有効な堎合、ファむルごずの効率的なカスタマむズ同期が䜿甚されたす。有効にする際には簡単な移行䜜業が必芁であり、すべおのデバむスを v0.23.18 以降にアップデヌトする必芁がありたす。この機胜を有効にするず、叀いバヌゞョンずの互換性が倱われたす。 + +### カスタマむズ同期を有効にする (Enable customisation sync) +蚭定キヌ: usePluginSync + +テヌマ、スニペット、ホットキヌ、プラグむン蚭定などの同期を有効にしたす。 +泚意: 安党䞊の理由から、この機胜を䜿甚するにぱンドツヌ゚ンド暗号化End-to-End Encryptionが有効になっおいる必芁がありたす。 + +### カスタマむズの自動スキャン (Scan customisation automatically) +蚭定キヌ: autoSweepPlugins + +レプリケヌション同期凊理を実行する前に、カスタマむズ蚭定の倉曎をスキャンしたす。 + +### 定期的なカスタマむズのスキャン (Scan customisation periodically) +蚭定キヌ: autoSweepPluginsPeriodic + +1分ごずにカスタマむズ蚭定の倉曎を定期的にスキャンしたす。 + +### カスタマむズ曎新の通知 (Notify customised) +蚭定キヌ: notifyPluginOrSettingUpdated + +他のデバむスで新しくカスタマむズ蚭定が曎新されたずきに通知を衚瀺したす。 + ## Miscellaneous その他の蚭定です ### Show status inside editor @@ -195,8 +438,8 @@ Self-hosted LiveSyncはPouchDBを䜿甚し、リモヌトず[このプロトコ ![CorruptedData](../images/lock_pattern1.png) デヌタベヌスがロックされおいお、端末が「解決枈み」ずマヌクされおいない堎合、譊告が衚瀺されたす。 他のデバむスで、End to End暗号化を有効にしたか、Drop Historyを行った等、他の端末がそのたた同期を行っおはいない状態に陥った堎合衚瀺されたす。 -暗号化を有効化した堎合は、パスフレヌズを蚭定しおApply and recieve、Drop Historyを行った堎合は、Drop and recieveを行うず自動的に解陀されたす。 -手動でこのロックを解陀する堎合は「mark this device as resolved」をクリックしおください。 +暗号化を有効化した堎合は、パスフレヌズを蚭定しお「このデバむスの同期状態をリセット」、たたは「このデバむスのファむルでサヌバヌデヌタを䞊曞き」を行うず自動的に解陀されたす。 +手動でこのロックを解陀する堎合は「I've made a backup, mark this device 'resolved'」をクリックしおください。 - パタヌン ![CorruptedData](../images/lock_pattern2.png) @@ -207,18 +450,52 @@ Self-hosted LiveSyncはPouchDBを䜿甚し、リモヌトず[このプロトコ ### Verify and repair all files Vault内のファむルを党お読み蟌み盎し、もし差分があったり、デヌタベヌスから正垞に読み蟌めなかったものに関しお、デヌタベヌスに反映したす。 -- Drop and send -デバむスずリモヌトのデヌタベヌスを砎棄し、ロックしおからデバむスのファむルでデヌタベヌスを構築埌、リモヌトに䞊曞きしたす。 -- Drop and receive -デバむスのデヌタベヌスを砎棄した埌、リモヌトから、操䜜しおいるデバむスに関しおロックを解陀し、デヌタを受信しお再構築したす。 +- このデバむスの同期状態をリセット (Reset Synchronisation on This Device) +ロヌカルのデヌタベヌスを砎棄し、リモヌトのデヌタから再構築したす。 +- このデバむスのファむルでサヌバヌデヌタを䞊曞き (Overwrite Server Data with This Device's Files) +ロヌカルおよびリモヌトのデヌタベヌスをこのデバむス䞊のファむルで再構築䞊曞きしたす。 ### Lock remote database リモヌトのデヌタベヌスをロックし、他の端末で同期を行おうずしおも゚ラヌずずもに同期がキャンセルされるように蚭定したす。これは、デヌタベヌスの再構築を行った堎合、自動的に蚭定されるものず同じものです。 䞇が䞀同期に䞍具合が発生しおいお、䜿甚しおいるデバむスのデヌタサヌバヌのデヌタを保護する堎合などに、緊急避難的に䜿甚しおください。 -### Suspend file watching -ファむルの曎新の監芖を止めたす。 +### Scram スむッチ (Scram Switches) +デヌタベヌスの砎損や予期しないデヌタ喪倱を防ぐために、同期凊理を緊急停止するためのスむッチです。重倧な蚭定䞍䞀臎や同期゚ラヌが発生した堎合、プラグむンは自動的に Scram 状態に移行し、同期動䜜を䞀時停止するこずがありたす。 + +#### ファむルの曎新監芖を䞀時停止 (Suspend file watching) +蚭定キヌ: suspendFileWatching + +ロヌカルファむル倉曎の監芖ず怜知を停止したす。 + +#### デヌタベヌス反映を䞀時停止 (Suspend database reflecting) +蚭定キヌ: suspendParseReplicationResult + +デヌタベヌスでの倉曎をストレヌゞファむルVault内のファむルぞ曞き戻す凊理を停止したす。 + +### 互換性メタデヌタ(Compatibility (Metadata)) + +#### 削陀枈みファむルのメタデヌタを保持しない (Do not keep metadata of deleted files.) +蚭定キヌ: deleteMetadataOfDeletedFiles + +ファむルを削陀した際に、そのファむルの同期履歎メタデヌタも即座にデヌタベヌスから削陀し、保持しないようにしたす。 + +#### 削陀枈みデヌタのメタデヌタをクリヌンナップする (Delete old metadata of deleted files on start-up) +蚭定キヌ: automaticallyDeleteMetadataOfDeletedFiles + +ファむルを削陀した際のメタデヌタを保持する期間日数を蚭定したす。指定した日数を経過した叀い削陀枈みファむルのメタデヌタは、プラグむン起動時にデヌタベヌスから自動的に削陀クリヌンナップされたす。`0` を指定するず自動削陀は無効になりたす。 + +### 砎損しおいる可胜性があるファむルも凊理する +蚭定キヌ: processSizeMismatchedFiles + +サむズ䞍䞀臎のあるファむルを凊理したす。特定のAPIや倖郚連携によっお䜜成されたファむルを同期する際に圹立ちたす。 + +### Remediation + +#### むベント反映時の最倧ファむル曎新日時 +蚭定キヌ: maxMTimeForReflectEvents + +この倀Unix゚ポックからの秒数より新しい曎新日時を持぀ファむルに぀いおは、むベントの反映を無芖したす。0を指定するず制限が無効になりたす。 ### Corrupted data ![CorruptedData](../images/corrupted_data.png) diff --git a/docs/setup_cloudant.md b/docs/setup_cloudant.md index 83ca3ea..a979cb9 100644 --- a/docs/setup_cloudant.md +++ b/docs/setup_cloudant.md @@ -13,7 +13,7 @@ In these instructions, create IBM Cloudant Instance for trial. 1. You can choose "Lite plan" for free. ![step 3](../instruction_images/cloudant_3.png) -1. Select Multitenant(it's the default) and the region as you like. +1. Select Multitenant (it is the default) and the region as you like. ![step 4](../instruction_images/cloudant_4.png) 1. Be sure to select "IAM and Legacy credentials" for "Authentication Method". @@ -28,20 +28,20 @@ In these instructions, create IBM Cloudant Instance for trial. 1. When all of the above steps have been done, open "Resource list" on the left pane. you can see the Cloudant instance in the "Service and software". Click it. ![step 8](../instruction_images/cloudant_8.png) -1. In resource details, there's information to connect from Self-hosted LiveSync. - Copy the "External Endpoint(preferred)" address. (\*1). We use this address later, with the database name. +1. In resource details, there is information to connect from Self-hosted LiveSync. + Copy the "External Endpoint (preferred)" address. (\*1). We use this address later, with the database name. ![step 9](../instruction_images/cloudant_9.png) ## Database setup 1. Hit the "Launch Dashboard" button, Cloudant dashboard will be shown. - Yes, it's almost CouchDB's fauxton. + Yes, it is almost CouchDB's fauxton. ![step 1](../instruction_images/couchdb_1.png) 1. First, you have to enable the CORS option. Hit the Account menu and open the "CORS" tab. Initially, "Origin Domains" is set to "Restrict to specific domains"., so set to "All domains(\*)" - _NOTE: of course We want to set "app://obsidian.md" but it's not acceptable on Cloudant._ + _NOTE: of course We want to set "app://obsidian.md" but it is not acceptable on Cloudant._ ![step 2](../instruction_images/couchdb_2.png) 1. Next, Open the "Databases" tab and hit the "Create Database" button. @@ -55,10 +55,10 @@ In these instructions, create IBM Cloudant Instance for trial. ### Credentials Setup -1. Back into IBM Cloud, Open the "Service credentials". You'll get an empty list, hit the "New credential" button. +1. Back into IBM Cloud, Open the "Service credentials". You will get an empty list, hit the "New credential" button. ![step 1](../instruction_images/credentials_1.png) -1. The dialog to create a credential will be shown. +1. The dialogue to create a credential will be shown. type any name or leave it default, hit the "Add" button. ![step 2](../instruction_images/credentials_2.png) _NOTE: This "name" is not related to your username that uses in Self-hosted LiveSync._ @@ -68,14 +68,14 @@ In these instructions, create IBM Cloudant Instance for trial. ![step 3](../instruction_images/credentials_3.png) The username and password pair is inside this JSON. "username" and "password" are so. - follow the figure, it's + follow the figure, it is "apikey-v2-2unu15184f7o8emr90xlqgkm2ncwhbltml6tgnjl9sd5"(\*3) and "c2c11651d75497fa3d3c486e4c8bdf27"(\*4) ## Self-hosted LiveSync settings ![Setting](../images/remote_db_setting.png) -The Setting should be as below: +The settings should be as follows: | Items | Value | example | | ------------- | ----- | ----------------------------------------------------------------- | diff --git a/docs/tech_info.md b/docs/tech_info.md index 97fc2cc..3aa24b1 100644 --- a/docs/tech_info.md +++ b/docs/tech_info.md @@ -1,8 +1,8 @@ # Designed architecture -## How does this plugin synchronize. +## How does this plug-in synchronise. -![Synchronization](../images/1.png) +![Synchronisation](../images/1.png) 1. When notes are created or modified, Obsidian raises some events. Self-hosted LiveSync catches these events and reflects changes into Local PouchDB. 2. PouchDB automatically or manually replicates changes to remote CouchDB. diff --git a/docs/tech_info_cn.md b/docs/tech_info_cn.md index c3dc1ea..c1d450c 100644 --- a/docs/tech_info_cn.md +++ b/docs/tech_info_cn.md @@ -2,7 +2,7 @@ ## 这䞪插件是怎么实现同步的. -![Synchronization](../images/1.png) +![Synchronisation](../images/1.png) 1. 圓笔记创建或修改时Obsidian䌚觊发事件。Self-hosted LiveSync捕获这些事件并将变曎同步至本地PouchDB 2. PouchDB通过自劚或手劚方匏将变曎同步至远皋CouchDB diff --git a/docs/tech_info_ja.md b/docs/tech_info_ja.md index f23c22d..faee6b6 100644 --- a/docs/tech_info_ja.md +++ b/docs/tech_info_ja.md @@ -2,7 +2,7 @@ ## 同期 -![Synchronization](../images/1.png) +![Synchronisation](../images/1.png) 1. ノヌトが曎新された際、Obsidianがむベントを発報したす。Obsidian-LiveSyncはそれをハンドリングしお、ロヌカルのPouchDBに倉曎を反映したす。 2. PouchDBは、リモヌトのCouchDBに差分をレプリケヌションしたす。 diff --git a/docs/terms.md b/docs/terms.md index e30a311..ac34ef0 100644 --- a/docs/terms.md +++ b/docs/terms.md @@ -2,23 +2,102 @@ ## Spelling and Vocabulary conventions -1. Almost all of the english words are written in British English. For example, "organisation" instead of "organization", "synchronisation" instead of "synchronization", etc. This convention originated from the author's personal preference but is now maintained for consistency. +All guidelines and conventions listed below are disclosed and maintained solely for the sake of documentation `consistency`. -2. Idiomatic terms, such as used in HTML, CSS, and JavaScript, are usually be aligned with the language used in the technology. For example, "color" instead of "colour", "program" instead of "programme", etc. Especially, terms which are used for attributes, properties, and methods are notable. +1. Almost all of the English words are written in British English. This convention originated from the author's personal preference. + - **Traditional Spelling (Trad-spelling)**: We prefer traditional British English spellings. In particular, we use `-ise` and `-isation` suffixes rather than the Oxford spelling `-ize` and `-ization` (for example, 'initialisation', 'synchronisation', and 'organisation'). + - **Oxford Comma**: We use the serial (Oxford) comma to separate items in lists of three or more (for example, 'settings, snippets, and themes' instead of 'settings, snippets and themes'). + - **Logical Punctuation**: We place punctuation marks (such as commas and full stops) outside quotation marks, unless the punctuation mark is part of the quoted text itself. For example, we write 'dialogue', not 'dialogue,'. + - **BBC News Styleguide**: If in wonder, the BBC News Styleguide may be useful as a reference. + +2. Idiomatic terms, such as those used in HTML, CSS, and JavaScript, are usually aligned with the language used in the technology. For example, "color" instead of "colour", "program" instead of "programme", etc. Especially, terms which are used for attributes, properties, and methods are notable. 3. We use `dialogue` in documentation for consistency. While `dialog` may appear in source code, particularly in class names, method names, and attributes (following technical conventions in No. 2), we consistently use `dialogue` for user-facing messages and general documentation text. This approach balances No. 1 with No. 2. -4. Contractions are not used. For example, "do not" instead of "don't", "cannot" instead of "can't", etc. especially `'d`. +4. Contractions are not used. For example, "do not" instead of "don't", "cannot" instead of "can't", etc., especially `'d`. - We may encounter difficulties with tenses. 5. However, try using affirmative forms, `Discard` instead of `Do not keep`, `Continue` instead of `Do not stop`, etc. - Some languages, such as Japanese, have a different meaning for `yes` and `no` between affirmative and negative questions. -## Terminology +6. Single quotation marks (`'`) are preferred over double quotation marks (`"`) in general documentation text, unless the context requires double quotes (for example, inside JSON code blocks). -- Self-hosted LiveSync - - This plug-in name. `Self-hosted` is one word. +### Terminology + +- Boot-up sequence (boot-sequence) + - The initialisation process of the plug-in when Obsidian starts. It starts with the loading of the plug-in, setting up core services, loading saved settings, and opening the local database. Once the layout is ready, the plug-in checks for the presence of flag files, runs configuration diagnostics, connects to the remote database, and begins file watching. The sequence finishes once the plug-in is fully ready and operational. +- Broken files (Size mismatch) + - A state where a file's metadata and the actual content stored in its chunks do not match, causing file retrieval or synchronisation failures. These mismatches can be detected and resolved by running validation tools such as `Verify and repair all files` on the Hatch pane. +- Chunk / Chunks + - Divided units of data stored in the database or object storage to facilitate efficient synchronisation. +- Compaction + - A database maintenance procedure that discards old historical document revisions to shrink the remote database size. +- Custom HTTP Handler / Use Internal API (CORS Bypass Settings) + - Settings used to bypass CORS restrictions by routing requests through Obsidian's native request APIs. There are two distinct settings under the hood depending on the remote server type: + - **For S3-compatible Object Storage (useCustomRequestHandler)**: Labeled as **"Use Custom HTTP Handler"** in the standard settings tab, **"Use internal API"** in the Svelte-based Setup Wizard dialogue, and represented as `useProxy` in the Setup URI's query parameters due to an unfortunate misunderstanding during development. + - **For CouchDB (useRequestAPI)**: Labeled as **"Use Request API to avoid `inevitable` CORS problem"** in the standard settings tab, **"Use Internal API"** in the Svelte-based Setup Wizard dialogue, and represented as `useRequestAPI` in the Setup URI's query parameters. +- Customisation Sync + - The feature that synchronises settings, snippets, themes, and plug-ins. Write with an "s" in documentation (`Customisation`), though technical configurations and links may use `customization`. +- Database Adapter (IDB vs. IndexedDB) + - The local database storage interface used by PouchDB. The `IDB` adapter is recommended since the older `IndexedDB` adapter is obsolete and known to cause memory leaks in `LiveSync` mode. Users can switch between these adapters without a full database rebuild, although a local data migration and an Obsidian restart are required. +- Database Suffix (additionalSuffixOfDatabaseName) + - A unique suffix appended to the database name to allow synchronising multiple vaults with the same name on the same remote server. +- E2EE Algorithm + - The cryptographic algorithm version used for end-to-end encryption. All devices in the synchronisation group must be configured with a compatible version (such as `V2` or `V1`). +- Eden (Eden Chunks) + - A performance optimisation where newly created chunks are held within the document until they stabilise, before graduating to independent chunks. +- Fast Setup (Simple Fetch) + - A simplified, automated initial synchronisation flow triggered when setting up subsequent devices or recovering a database. It bypasses the detailed step-by-step setup wizard dialogues, prompting the user with high-level data processing decisions and completing the initial download and local file scan in one continuous process. +- Flag files (redflag.md, redflag2.md, redflag3.md) + - Special Markdown files (or directories) placed at the root of the vault to stop the boot-up sequence or trigger recovery tasks. For instance, `redflag.md` suspends all processes, while `redflag2.md` (`flag_rebuild.md`) triggers a full database rebuild and `redflag3.md` (`flag_fetch.md`) discards the local database to fetch it again from the remote. +- Garbage Collection (GC) + - The process of identifying and purging unreferenced chunks (unused data) from local and remote databases to reclaim storage space. +- Hatch (Hatch pane) + - A dedicated troubleshooting and maintenance section in the plug-in settings, typically hidden behind a warning-labeled collapsible panel to prevent accidental misconfiguration. It contains diagnostic utilities, database reset controls, status reports, and advanced edge-case patches. +- Hidden File Sync + - The feature that synchronises files located in hidden directories (like `.obsidian`). +- JWT Authentication + - An experimental authentication option for CouchDB allowing secure token-based authentication instead of standard credentials. It requires a configured private key/secret, algorithm, expiration duration, subject, and key ID. - LiveSync - - Very confusing term. - - As shorten-form of `Self-hosted LiveSync`. - - As a name of synchronisation mode. This should be changed to `Continuos`, in contrast to `Periodic`. + - A very confusing term. + - As a shortened form of `Self-hosted LiveSync`. + - As the name of a synchronisation mode. This should be changed to `Continuous`, in contrast to `Periodic`. +- livesync-serverpeer / webpeer + - Pseudo-clients that assist in WebRTC peer-to-peer communication. +- Metadata (File metadata) + - A database document that stores properties of a file, including its filename, path, size, modification time, conflict history, and references (hashes) of the chunks that comprise the file's content. In Self-hosted LiveSync, metadata is stored separately from the actual file content to enable efficient synchronisation and versioning. +- OneShot Sync + - A single, immediate bidirectional synchronisation (pull then push) triggered on demand or on specific events, as opposed to continuous (live) replication. +- Overwrite Server Data with This Device's Files + - A maintenance operation (formerly known as `Rebuild everything`) that discards the remote database and reconstructs it by uploading all current local files as a fresh database, overwriting any remote changes. +- Path Obfuscation + - A privacy option that encrypts file paths and folder names on the remote server. +- plug-in + - We use the hyphenated form `plug-in` in user-facing messages and general documentation, while `plugin` may appear in codebase files, configuration settings, or technical contexts. +- Relay Server (P2P relays) + - A WebSocket-based coordination server used to establish direct WebRTC peer-to-peer connections. The default relay is provided by the plug-in author. +- Remediation (maxMTimeForReflectEvents) + - A recovery setting that restricts the propagation of changes from the database to local storage, ignoring any file events (such as accidental mass deletions) that occurred after a specified date and time. +- Reset Synchronisation on This Device + - A maintenance operation (formerly known as `Fetch everything`) that discards the local database and reconstructs it by downloading all data from the remote server. +- Scram (Scram Switches) + - Emergency controls in the settings that allow users to suspend file watching or database writes to prevent corruption. +- Segmenter (Segmented-splitter) + - A chunking method that divides files on semantic boundaries (such as paragraphs or sections) rather than arbitrary byte boundaries. +- Self-hosted LiveSync + - The name of this plug-in. `Self-hosted` is one word. +- Setting Doctor (Config Doctor) + - A diagnostic utility that checks for mismatches or suboptimal configurations, presenting users with ideal values and recommendation reasons to easily resolve issues during migration, configuration import, or general troubleshooting. +- Setup URI + - An encrypted representation of the plug-in's settings containing server configuration, which allows users to clone their configuration across devices securely using a passphrase. +- Streaming replication (Stream-based replication) + - A data transfer method that downloads database documents as a continuous stream of events. It is significantly faster than traditional chunk-by-chunk HTTP requests and is used during Fast Setup to retrieve remote metadata quickly. +- Sync Mode + - The replication trigger mechanism. Users can select from `On Events` (synchronising on local file changes), `Periodic and Events` (synchronising at fixed intervals as well as on events), or `LiveSync` (continuous, real-time synchronisation). +- TURN Server (WebRTC P2P) + - A server type (Traversal Using Relays around NAT) used as a fallback to relay traffic when direct WebRTC peer-to-peer connection is blocked by strict NAT or firewalls. +- Update Thinning (Batch database update) + - An optimisation that groups multiple local file edits together over a short delay before committing them to the local database, reducing the number of database write operations. +- WebRTC P2P (Peer-to-Peer) + - A synchronisation method enabling direct communication between devices without a central server database. + diff --git a/docs/tips/fast-setup.md b/docs/tips/fast-setup.md new file mode 100644 index 0000000..3e33e35 --- /dev/null +++ b/docs/tips/fast-setup.md @@ -0,0 +1,65 @@ +# Fast Setup (Simple Fetch) + +Fast Setup is a streamlined, user-friendly data retrieval and initialisation flow designed to simplify setting up secondary devices or recovering databases. + +Instead of guiding the user through the detailed multi-step setup wizard dialogues, Fast Setup prompts the user with high-level sync decisions and automates database download and local storage scanning in one continuous process. + +--- + +## How It Works + +When you import a **Setup URI** on a secondary device, or when a **Fetch All** operation is triggered (such as by placing a `redflag3.md` / `flag_fetch.md` flag file at the root of the vault), the plug-in schedules remote data retrieval. + +On the next startup, the plug-in boots in scheduled fetch mode and opens a simplified dialogue: **"Data retrieval scheduled"**. + +--- + +## Technical Characteristics + +Fast Setup leverages several backend optimisations to make the retrieval fast, safe, and clean: + +1. **Stream-based Replication for Speed** + - It fetches all remote metadata via stream reception, which is significantly faster than traditional chunk-by-chunk retrieval. +2. **Delayed File Reflection to Prevent Corrupted Warnings** + - By suspending file reflection during the download phase, it prevents the plug-in from raising temporary or false "corrupted data synchronisation" or "size mismatch" warnings that can occur during the chunk download process. +3. **Time-Based Comparison is Generally Sufficient** + - Since the vault is entering a fresh synchronisation or recovery state, comparing files based on their modification timestamps (newer-wins) is highly reliable and sufficient to reconcile files without needing complex manual conflict resolution. + +--- + +## Step-by-Step Guide + +### Step 1: Choose Data Processing Method +You will be prompted to choose how the retrieved remote data will interact with your existing local files: + +1. **Compare time and take newer (newer-wins)** + - Compares the modified time of files and accepts the newer version. + - **Recommended if:** You have been using Self-hosted LiveSync and have made changes on multiple devices that you want to merge. +2. **Overwrite all with remote files (remote-wins)** + - Remote data is treated as the source of truth. + - **Recommended if:** You are setting up a brand new device with an empty or clean vault. + - *Warning: This will overwrite local files with remote files. Please ensure you have a backup of your local vault before proceeding.* +3. **Use the detailed flow (legacy)** + - Switches back to the detailed, traditional setup wizard dialogues. + - **Recommended if:** You want full control over the step-by-step database setup options. + +### Step 2: Configure Conflict & Deletion Rules +Depending on your choice in Step 1, you will configure how to handle mismatches: + +#### If you chose "Compare time and take newer": +- **Delete local files if they were deleted on remote** + - Keeps your local vault clean by removing files that have already been deleted on other devices. +- **Recreate remote files even if they were deleted on remote** + - Preserves local files and uploads them back to the remote database, even if they were deleted on other devices. + +#### If you chose "Overwrite all with remote files": +- **Delete local files if not on remote** + - Removes local-only files so that your local vault matches the remote database exactly. +- **Keep local files even if not on remote** + - Retains all existing local-only files, although this may result in duplicates that you will need to clean up manually after synchronisation. + +### Step 3: Automated Synchronisation +Once you confirm your choices: +1. The plug-in performs a fast download of the remote database (`fetchLocalDBFast`). +2. It automatically runs a full scan (`synchroniseAllFilesBetweenDBandStorage`) in the foreground to reflect database changes in your local vault files immediately. +3. The plug-in finalises the process and resumes normal operational status. diff --git a/docs/tips/fast-setup_ja.md b/docs/tips/fast-setup_ja.md new file mode 100644 index 0000000..56be7d2 --- /dev/null +++ b/docs/tips/fast-setup_ja.md @@ -0,0 +1,66 @@ +# ファストセットアップ (Fast Setup / Simple Fetch) + +ファストセットアップは、2台目以降のデバむスのセットアップやデヌタベヌス再構築時のデヌタ取埗・初期化凊理を、盎感的か぀迅速に行うための簡略化された同期フロヌです。 + +埓来のセットアップりィザヌドにおける耇数の詳现なステップを螏むこずなく、同期の基本方針を遞択するだけで、デヌタベヌスのダりンロヌドずロヌカルファむルのスキャン・反映を䞀連のプロセスずしお自動的に実行したす。 + +--- + +## 仕組み + +2台目以降のデバむスで **セットアップURI** をむンポヌトした堎合や、手動でデヌタの再フェッチVaultルヌトに `redflag3.md` / `flag_fetch.md` を配眮する等が予玄された堎合、プラグむンはリモヌトデヌタベヌスからのデヌタ取埗スケゞュヌルを蚭定したす。 + +その埌の起動時、プラグむンはデヌタフェッチ予玄モヌドで立ち䞊がり、**「Data retrieval scheduledデヌタ取埗のスケゞュヌル」** ずいう簡略化されたダむアログを衚瀺したす。 + +--- + +## 技術的な特城 + +ファストセットアップは、高速か぀安党でクリヌンな凊理を実珟するために、以䞋の最適化を行っおいたす。 + +1. **高速なストリヌム受信** + - 党デヌタを取埗したすが、ストリヌム受信によるレプリケヌションを䜿甚するため、凊理が非垞に高速です。 +2. **ストレヌゞ反映の遅延による䞍芁な譊告の抑制** + - デヌタのダりンロヌド䞭にロヌカルファむルストレヌゞぞの曞き出し反映凊理をあえお遅延させるこずで、同期途䞭で発生しがちな「砎損デヌタ同期」や「サむズ䞍䞀臎」などの䞀時的な゚ラヌ譊告を抑え蟌みたす。 + - すべおのデヌタダりンロヌドが完了した埌に䞀括しおストレヌゞぞ曞き出すため、䞍必芁な譊告画面でナヌザヌを混乱させたせん。 +3. **時刻ベヌス比范の劥圓性** + - 初期セットアップやリカバリヌの段階この状態に移行した盎埌においおは、抂ねファむル曎新時刻タむムスタンプベヌスでの単玔比范を行うこずで、十分か぀劥圓な同期結果を埗るこずができたす。これにより耇雑な競合解決の手間を省きたす。 + +--- + +## 蚭定手順 + +### ステップ 1: デヌタの反映方法の遞択 +取埗したリモヌトデヌタを、既存のロヌカルファむルずどのように統合するかを遞択したす。 + +1. **Compare time and take newer (newer-wins)** + - ファむルの曎新日時を比范し、より新しい方を採甚したす。 + - **掚奚されるケヌス:** すでに Self-hosted LiveSync を䜿甚しおおり、耇数のデバむスで線集した倉曎内容をタむムスタンプに基づいお統合したい堎合。 +2. **Overwrite all with remote files (remote-wins)** + - リモヌトデヌタベヌスの内容を正Source of Truthずしお扱いたす。 + - **掚奚されるケヌス:** たったく新しいデバむスをセットアップする堎合空のVaultなど。 + - *譊告: ロヌカルにあるすべおのファむルがリモヌトの内容で䞊曞きされたす。重芁なデヌタがある堎合は、事前にバックアップを取埗しおください。* +3. **Use the detailed flow (legacy)** + - 埓来の詳现なセットアップりィザヌドダむアログに戻りたす。 + - **掚奚されるケヌス:** デヌタベヌスの構成オプションをステップバむステップで现かく制埡・確認したい堎合。 + +### ステップ 2: 競合および削陀ルヌルの構成 +ステップ 1 での遞択内容に応じお、ロヌカルずリモヌトの䞍䞀臎をどう凊理するかを蚭定したす。 + +#### 「Compare time and take newer」を遞択した堎合: +- **Delete local files if they were deleted on remote** + - 他のデバむスで削陀枈みのファむルをロヌカルからも削陀し、Vaultを同期・クリヌンな状態に保ちたす。 +- **Recreate remote files even if they were deleted on remote** + - 他のデバむスで削陀されたファむルであっおも、ロヌカルファむルを維持し、リモヌトデヌタベヌスに再床アップロヌドしたす。 + +#### 「Overwrite all with remote files」を遞択した堎合: +- **Delete local files if not on remote** + - リモヌトに存圚しないロヌカル専甚ファむルを削陀し、ロヌカルのVaultをリモヌトデヌタベヌスず完党に䞀臎させたす。 +- **Keep local files even if not on remote** + - リモヌトに存圚しないロヌカルファむルをそのたた残したす。ただし、同期埌に重耇ファむルが発生する可胜性があるため、その堎合は手動でクリヌンアップを行っおください。 + +### ステップ 3: 自動同期の実行 +遞択を確定するず、以䞋の凊理が順次実行されたす。 +1. リモヌトデヌタベヌスの高速ダりンロヌドを実行したす (`fetchLocalDBFast`)。 +2. ロヌカルファむルぞの倉曎反映のため、フォアグラりンドでフルスキャン (`synchroniseAllFilesBetweenDBandStorage`) を自動的に実行したす。 +3. 凊理が完了するず、プラグむンは通垞の動䜜状態ぞ埩垰したす。 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f463e36..36d1168 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -224,7 +224,7 @@ There are many cases where this is really unclear. One possibility is that the c - 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. +7. Perform `Reset Synchronisation on This Device` on the `🎛 Maintenance` pane. This mode is very fragile. Please be careful. @@ -236,16 +236,16 @@ not been stable. The new adapter has better performance and has a new feature like purging. Therefore, we should use new adapters and current default is so. However, when switching from an old adapter to a new adapter, some converting or -local database rebuilding is required, and it takes a few time. It was a long +local database rebuilding is required, and it takes some time. It was a long time ago now, but we once inconvenienced everyone in a hurry when we changed the format of our database. For these reasons, this toggle is automatically on if we have upgraded from vault which using an old adapter. -When you rebuild everything or fetch from the remote again, you will be asked to +When you overwrite server data with this device's files or reset synchronisation on this device again, you will be asked to switch this. Therefore, experienced users (especially those stable enough not to have to -rebuild the database) may have this toggle enabled in their Vault. Please +overwrite server data) may have this toggle enabled in their Vault. Please disable it when you have enough time. ### ZIP (or any extensions) files were not synchronised. Why? @@ -303,9 +303,9 @@ happened on other devices. This means that conflicts will happen in the past, after the time we have synchronised. Hence we cannot collect and delete the unused chunks even though if we are not currently referenced. -To shrink the database size, `Rebuild everything` only reliably and effectively. +To shrink the database size, `Overwrite Server Data with This Device's Files` is the only reliable and effective way. But do not worry, if we have synchronised well. We have the actual and real -files. Only it takes a bit of time and traffics. +files. Only it takes a bit of time and traffic. ### How to launch the DevTools @@ -373,47 +373,64 @@ without Obsidian. For example, if there is `redflag.md`, Self-hosted LiveSync suspends all database and storage processes. -### Flag Files +### Scram State and Flag Files (SCRAM Warning Loop) -The flag file is a simple Markdown file designed to prevent storage events and database events in self-hosted LiveSync. -Its very existence is significant; it may be left blank, or it may contain text; either is acceptable. +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. -This file is in Markdown format so that it can be placed in the Vault externally, even if Obsidian fails to launch. +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. -There are some options to use `redflag.md`. +#### Flag Files -| Filename | Human-Friendly Name | Description | -| ------------- | ------------------- | ------------------------------------------------------------------------------------ | -| `redflag.md` | - | Suspends all processes. | -| `redflag2.md` | `flag_rebuild.md` | Suspends all processes, and rebuild both local and remote databases by local files. | -| `redflag3.md` | `flag_fetch.md` | Suspends all processes, discard the local database, and fetch from the remote again. | +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). -When fetching everything remotely or performing a rebuild, restarting Obsidian -is performed once for safety reasons. At that time, Self-hosted LiveSync uses -these files to determine whether the process should be carried out. (The use of -normal markdown files is a trick to externally force cancellation in the event -of faults in the rebuild or fetch function itself, especially on mobile -devices). This mechanism is also used for set-up. And just for information, -these files are also not subject to synchronisation. +| 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. | -However, occasionally the deletion of files may fail. This should generally work -normally after restarting Obsidian. (As far as I can observe). +When resetting synchronisation on this device or overwriting server data, restarting Obsidian is performed once for safety reasons. At that time, Self-hosted LiveSync uses these files to determine whether the process should be carried out. (This mechanism is especially useful on mobile devices to force cancellation if the database rebuilding fails). These files are not subject to synchronisation. + +#### How to Resolve the Scram Loop + +If you cannot disable Scram, please follow these steps: +1. Close Obsidian completely. +2. Open your system's file manager and check the root directory of your vault. +3. Locate and delete any flag files (such as `redflag.md`, `redflag2.md`, or `redflag3.md`). +4. Launch Obsidian. +5. Go to the settings dialogue -> **Hatch** -> **Scram Switches**, and manually toggle **Suspend file watching** and **Suspend database reflecting** to `false` (disabled) if they have not been reset automatically. + +> [!TIP] +> This is the reason why flag files are standard `.md` files: it allows you to manage them externally. On mobile devices, you can use system file manager applications (such as the native **Files** app on iOS/iPadOS or **Files by Google** on Android) to find and delete these files to resolve a lock, or conversely, create/place a new `redflag.md` file (or directory) at the root of your vault to force-suspend synchronisation and stop Obsidian's boot-up sequence if you need to fix a database issue. + +### JWT Authentication Errors + +#### DataError when configuring JWT authentication + +If you encounter a `DataError:` with no additional information in the logs when configuring JWT authentication, this usually indicates a private key formatting issue. + +Self-hosted LiveSync requires the private key (for ES256/ES512 algorithms) to be in the **PKCS#8 PEM** format. Standard SEC1 EC private keys (which begin with `-----BEGIN EC PRIVATE KEY-----`) will trigger this error. + +To resolve this, convert your private key to PKCS#8 format using the following `openssl` command: +```bash +openssl pkcs8 -topk8 -inform PEM -nocrypt -in private.key -out pkcs8.key +``` +Then paste the contents of `pkcs8.key` (which begins with `-----BEGIN PRIVATE KEY-----`) into the JWT Key field. ### Old tips -- Rarely, a file in the database could be corrupted. The plugin will not write +- Rarely, a file in the database could be corrupted. The plug-in will not write to local storage when a file looks corrupted. If a local version of the file is on your device, the corruption could be fixed by editing the local file and - synchronizing it. But if the file does not exist on any of your devices, then + synchronising it. But if the file does not exist on any of your devices, then it can not be rescued. In this case, you can delete these items from the - settings dialog. + settings dialogue. - To stop the boot-up sequence (eg. for fixing problems on databases), you can put a `redflag.md` file (or directory) at the root of your vault. Tip for iOS: a redflag directory can be created at the root of the vault using the File application. -- Also, with `redflag2.md` placed, we can automatically rebuild both the local - and the remote databases during the boot-up sequence. With `redflag3.md`, we - can discard only the local database and fetch from the remote again. +- Also, with `redflag2.md` placed, we can automatically overwrite server data with this device's files during the boot-up sequence. With `redflag3.md`, we + can discard only the local database and reset synchronisation on this device. - Q: The database is growing, how can I shrink it down? A: each of the docs is saved with their past 100 revisions for detecting and resolving conflicts. Picturing that one device has been offline for a while, and comes online @@ -425,7 +442,7 @@ normally after restarting Obsidian. (As far as I can observe). So, We have to make the database again like an enlarged git repo if you want to solve the root of the problem. - And more technical Information is in the [Technical Information](tech_info.md) -- If you want to synchronize files without obsidian, you can use +- If you want to synchronise files without obsidian, you can use [filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync). - WebClipper is also available on Chrome Web Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf) diff --git a/eslint.config.mjs b/eslint.config.mjs index 35b0d25..65f81a3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,81 +3,132 @@ import obsidianmd from "eslint-plugin-obsidianmd"; import globals from "globals"; import { defineConfig, globalIgnores } from "eslint/config"; import * as sveltePlugin from "eslint-plugin-svelte"; - +import svelteParser from "svelte-eslint-parser"; +const warnWhileDev = "off"; // Change to "warn" to enable warnings for rules that are currently disabled. export default defineConfig([ globalIgnores([ - "**/node_modules/*", - "**/jest.config.js", + // Build outputs and legacy files + "**/build", + "coverage", + "**/main.js", + "main_org.js", + "pouchdb-browser.js", + "version-bump.mjs", + "package.json", + "**/*.json", + "**/.eslintrc.js.bak", + // Files from linked dependencies (those files should not exist for most people). + "modules/octagonal-wheels/dist/**/*", + + // Sub-projects (Exclude from root linting as they have different environments) + "src/apps/**/*", + "utils/**/*", + + // Specific exclusions from common library (src/lib) "src/lib/coverage", "src/lib/browsertest", - "**/test.ts", - "**/tests.ts", - "**/**test.ts", - "**/**.test.ts", - "**/*.unit.spec.ts", - "**/esbuild.*.mjs", - "**/terser.*.mjs", - "**/node_modules", - "**/build", - "**/.eslintrc.js.bak", - "src/lib/src/patches/pouchdb-utils", - "**/esbuild.config.mjs", - "**/rollup.config.js", - "modules/octagonal-wheels/rollup.config.js", - "modules/octagonal-wheels/dist/**/*", "src/lib/test", "src/lib/_tools", + "src/lib/src/patches/pouchdb-utils", "src/lib/src/cli", - "**/main.js", - "src/apps/**/*", - ".prettierrc.*.mjs", - ".prettierrc.mjs", - "*.config.mjs", - "src/apps/**/*", "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", + "**/esbuild.config.mjs", + "**/terser.*.mjs", + ".prettierrc.*.mjs", + ".prettierrc.mjs", + "*.config.mjs", + "vite.*", + "vitest.*", + // Testing files (Simplified patterns) + "test/**", + "**/*.test.ts", + "**/*.unit.spec.ts", + "**/test.ts", + "**/tests.ts", ]), ...sveltePlugin.configs["flat/base"], ...obsidianmd.configs.recommended, { files: ["**/*.ts"], + // ignores:["src/lib/**/*.ts"], // Exclude library files from root linting (they have different environments and rules). languageOptions: { - globals: { ...globals.browser }, + globals: { ...globals.browser, PouchDB: "readonly" }, parser: tsParser, parserOptions: { project: "./tsconfig.json", }, }, + linterOptions:{ + reportUnusedDisableDirectives: false, + }, rules: { + // -- Base rules (turned off in favour of TS specific versions or explicitly disabled). "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], "no-unused-labels": "off", - "@typescript-eslint/ban-ts-comment": "off", "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off", - "require-await": "error", - "obsidianmd/rule-custom-message": "off", // Temporary - "obsidianmd/ui/sentence-case": "off", // Temporary - "@typescript-eslint/require-await": "warn", - "@typescript-eslint/no-misused-promises": "warn", - "@typescript-eslint/no-floating-promises": "warn", - "no-async-promise-executor": "warn", + "require-await": "off", + // -- TypeScript specific rules + // @typescript-eslint/no-unsafe-* rules and @typescript-eslint/no-explicit-any: + // This project contains a lot of library-sh code where the use of `any` is often necessary and justified. + // Rules is now set to 'off' for a while. "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + // -- Reasonable rules. + "@typescript-eslint/no-deprecated": warnWhileDev, + "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/require-await": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", + + // -- 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, + + // -- General rules + "no-async-promise-executor": warnWhileDev, "no-constant-condition": ["error", { checkLoops: false }], + // -- Disabled rules + // no-undef: This option breaks the global declarations for the library files and is not worth the effort to fix at this time. + "no-undef": "off", + + // -- Plugin specific overrides + "obsidianmd/rule-custom-message": "off", + "obsidianmd/ui/sentence-case": "off", + "obsidianmd/no-plugin-as-component": "off", + + // -- Temporary overrides for migration + "obsidianmd/no-static-styles-assignment": "off", }, }, { files: ["**/*.svelte"], languageOptions: { + parser: svelteParser, parserOptions: { parser: tsParser, + extraFileExtensions: [".svelte"], }, }, rules: { - "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], - "obsidianmd/no-plugin-as-component": "off", // Temporary + // no-unused-vars: + // Svelte template's declarations have a lot of false positives and the rule is not worth the effort to fix at this time. + // it may improve in the future with some options as like ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],] + "no-unused-vars": "off", + "obsidianmd/no-plugin-as-component": "off", + "obsidianmd/ui/sentence-case": "off", }, }, ]); diff --git a/manifest.json b/manifest.json index 027fd64..fbc492e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "obsidian-livesync", "name": "Self-hosted LiveSync", - "version": "0.25.68", + "version": "0.25.73", "minAppVersion": "1.7.2", "description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.", "author": "vorotamoroz", "authorUrl": "https://github.com/vrtmrz", "isDesktopOnly": false -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index bd8f9bd..a23ad25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-livesync", - "version": "0.25.68", + "version": "0.25.73", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-livesync", - "version": "0.25.68", + "version": "0.25.73", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.808.0", @@ -15,6 +15,7 @@ "@smithy/middleware-apply-body-checksum": "^4.3.9", "@smithy/protocol-http": "^5.3.9", "@smithy/querystring-builder": "^4.2.9", + "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", "chokidar": "^4.0.0", "commander": "^14.0.3", @@ -25,7 +26,7 @@ "micromatch": "^4.0.0", "minimatch": "^10.2.2", "obsidian": "^1.12.3", - "octagonal-wheels": "^0.1.45", + "octagonal-wheels": "^0.1.46", "pouchdb-adapter-leveldb": "^9.0.0", "qrcode-generator": "^1.4.4", "werift": "^0.23.0", @@ -51,9 +52,9 @@ "@types/transform-pouch": "^1.0.6", "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", - "@vitest/browser": "^4.1.1", - "@vitest/browser-playwright": "^4.1.1", - "@vitest/coverage-v8": "^4.1.1", + "@vitest/browser": "^4.1.8", + "@vitest/browser-playwright": "^4.1.8", + "@vitest/coverage-v8": "^4.1.8", "dotenv-cli": "^11.0.0", "esbuild": "0.25.0", "esbuild-plugin-inline-worker": "^0.1.1", @@ -90,7 +91,7 @@ "typescript": "5.9.3", "vite": "^7.3.1", "vite-plugin-istanbul": "^8.0.0", - "vitest": "^4.1.1", + "vitest": "^4.1.8", "webdriverio": "^9.27.0", "yaml": "^2.8.2" } @@ -1851,9 +1852,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1932,9 +1933,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -3546,13 +3547,13 @@ } }, "node_modules/@smithy/core": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", - "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", + "@smithy/types": "^4.14.3", "tslib": "^2.6.2" }, "engines": { @@ -3719,12 +3720,12 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.3.3.tgz", - "integrity": "sha512-RRxYqjUa/n8dRVkbhyuiRarppLzt4H/AtMUEFmiHlDy8o4wrgqAdzxsk9naemzu6iX67ZV375fNmX7Q8dynGKw==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.3.6.tgz", + "integrity": "sha512-/cSYHP8jPffkhBClQzH9fAJujIh8dwMwg2swrVF4stXQsUWO5Oi2bwyaMUcBPIyulUI5IxaJFxd9C8UQX+YZsQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.3", + "@smithy/core": "^3.24.6", "tslib": "^2.6.2" }, "engines": { @@ -3988,9 +3989,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4052,12 +4053,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.3.3.tgz", - "integrity": "sha512-5xlgilVaX96HdVlLZymKUa7vOTZtisOTxBJloM2J4PeRqyAWBeFIq0DnIxQISvwxT4rgJAvk7rHhB+GlCCKe8g==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.3.6.tgz", + "integrity": "sha512-sms/ty2CJwHOiGzEaAVWizTVK5KusXpAYqCUeXIa+hWtNKLwjimH4z11mc07d0Fe3DT3lmZJIZWOMcVQ/N4hBQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.3", + "@smithy/core": "^3.24.6", "tslib": "^2.6.2" }, "engines": { @@ -4149,13 +4150,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", - "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.4.5.tgz", + "integrity": "sha512-W9Ovy9i02yGqtLlpqZNQuXNxXc5OPfXujnembxN/FxyBtGjJd8vKY0PQYEJ8FNybTOcXG+ZxsSsX23HOb3zQzg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.24.5", "tslib": "^2.6.2" }, "engines": { @@ -4194,12 +4194,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.3.tgz", - "integrity": "sha512-c1QpRBn3aMsoqE64dd4Imgjy8Pynfw+eR7GkjElquxUFSnezwYVaOFm8JcYa+Bo/5ssbEyPKcT3+4bmrWYh6eQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.6.tgz", + "integrity": "sha512-tAa4sePYB7mlJzdYbdBqdv37KwFKWixmM/r3ihcI0HFOVjf+a5oGvtcLXcGm4S1bY4DFsLAIOHgjubtp+oRufw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.24.3", + "@smithy/core": "^3.24.6", "tslib": "^2.6.2" }, "engines": { @@ -4994,47 +4994,46 @@ } }, "node_modules/@vitest/browser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.1.tgz", - "integrity": "sha512-gjjrFC4+kPVK/fN9URDJWrssU5Gqh8Az8pKG/NSfQ2V+ky8b/y1BgBg0Ug13+hOGp5pzInonmGRPn7vOgSLgzA==", + "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", - "peer": true, "dependencies": { "@blazediff/core": "1.9.1", - "@vitest/mocker": "4.1.1", - "@vitest/utils": "4.1.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.0.3", + "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.1.1" + "vitest": "4.1.8" } }, "node_modules/@vitest/browser-playwright": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.1.tgz", - "integrity": "sha512-dtVSBZZha2k/7P7EAXXrEAoxuIKl8Yv9f2Dk4GN/DGfmhf4DQvkvu+57okR2wq/gan1xppKjL/aBxK/kbYrbGw==", + "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.1", - "@vitest/mocker": "4.1.1", - "tinyrainbow": "^3.0.3" + "@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.1" + "vitest": "4.1.8" }, "peerDependenciesMeta": { "playwright": { @@ -5043,14 +5042,15 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.1.tgz", - "integrity": "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.1", + "@vitest/utils": "4.1.8", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -5058,14 +5058,14 @@ "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.1", - "vitest": "4.1.1" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -5074,41 +5074,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz", - "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/expect/node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@vitest/mocker": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz", - "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.1", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -5129,26 +5119,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz", - "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz", - "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.1", + "@vitest/utils": "4.1.8", "pathe": "^2.0.3" }, "funding": { @@ -5156,14 +5146,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", - "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -5172,9 +5162,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", - "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -5182,15 +5172,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", - "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", + "@vitest/pretty-format": "4.1.8", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -5223,9 +5213,9 @@ "license": "MIT" }, "node_modules/@wdio/config/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -5591,9 +5581,9 @@ "license": "MIT" }, "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -6400,6 +6390,16 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7882,9 +7882,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -7979,9 +7979,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -8045,9 +8045,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -8206,9 +8206,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -8408,9 +8408,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -9244,9 +9244,9 @@ "license": "MIT" }, "node_modules/globby/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -10257,10 +10257,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10928,9 +10938,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", - "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "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": { @@ -11491,9 +11501,9 @@ "license": "MIT" }, "node_modules/octagonal-wheels": { - "version": "0.1.45", - "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.45.tgz", - "integrity": "sha512-gXoCrwoUIXhmu57YN4BxAtBe+JaYNJNaXaZuVjqjopwYKpH5p2mn1om6KjA22rgGPiIJFXkse2U28FFXoT3/0Q==", + "version": "0.1.46", + "resolved": "https://registry.npmjs.org/octagonal-wheels/-/octagonal-wheels-0.1.46.tgz", + "integrity": "sha512-19eB7b/WNNrZ4Xghu93f+NVJsbRiaZaIIzU1rn5shxb6SzwVBoOVkNPJdCAsONl6C1MwjaGDrPUS8CBXvPHjPg==", "license": "MIT", "dependencies": { "idb": "^8.0.3" @@ -12650,9 +12660,9 @@ "license": "MIT" }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -13075,9 +13085,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "dev": true, "license": "ISC", "bin": { @@ -15249,9 +15259,9 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.0.tgz", + "integrity": "sha512-+t2Z/GwkZQDtu00813aP66ygViGtPHKhhoFZpQKpKrE+9jIgES+Zw+mFNaDWOVRKiuJjuqKHzD3B1sfGg8+ZOQ==", "dev": true, "license": "MIT", "engines": { @@ -16030,20 +16040,19 @@ } }, "node_modules/vitest": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz", - "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@vitest/expect": "4.1.1", - "@vitest/mocker": "4.1.1", - "@vitest/pretty-format": "4.1.1", - "@vitest/runner": "4.1.1", - "@vitest/snapshot": "4.1.1", - "@vitest/spy": "4.1.1", - "@vitest/utils": "4.1.1", + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -16054,7 +16063,7 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", + "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, @@ -16071,10 +16080,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.1", - "@vitest/browser-preview": "4.1.1", - "@vitest/browser-webdriverio": "4.1.1", - "@vitest/ui": "4.1.1", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -16098,6 +16109,12 @@ "@vitest/browser-webdriverio": { "optional": true }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, "@vitest/ui": { "optional": true }, @@ -16206,9 +16223,9 @@ } }, "node_modules/webdriver/node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 057971c..8b178cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-livesync", - "version": "0.25.68", + "version": "0.25.73", "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", @@ -19,13 +19,13 @@ "buildVite": "npx dotenv-cli -e .env -- vite build --mode production", "buildViteOriginal": "npx dotenv-cli -e .env -- vite build --mode original", "buildDev": "node esbuild.config.mjs dev", - "lint": "eslint src", + "lint": "eslint --cache --concurrency auto src", "svelte-check": "svelte-check --tsconfig ./tsconfig.json", "tsc-check": "tsc --noEmit", "pretty": "npm run prettyNoWrite -- --write --log-level error", "prettyCheck": "npm run prettyNoWrite -- --check", "prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ", - "check": "npm run lint && npm run svelte-check", + "check": "npm run tsc-check && npm run lint && npm run svelte-check", "unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/", "test": "vitest run", "test:unit": "vitest run --config vitest.config.unit.ts", @@ -80,9 +80,9 @@ "@types/transform-pouch": "^1.0.6", "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", - "@vitest/browser": "^4.1.1", - "@vitest/browser-playwright": "^4.1.1", - "@vitest/coverage-v8": "^4.1.1", + "@vitest/browser": "^4.1.8", + "@vitest/browser-playwright": "^4.1.8", + "@vitest/coverage-v8": "^4.1.8", "dotenv-cli": "^11.0.0", "esbuild": "0.25.0", "esbuild-plugin-inline-worker": "^0.1.1", @@ -119,7 +119,7 @@ "typescript": "5.9.3", "vite": "^7.3.1", "vite-plugin-istanbul": "^8.0.0", - "vitest": "^4.1.1", + "vitest": "^4.1.8", "webdriverio": "^9.27.0", "yaml": "^2.8.2" }, @@ -130,17 +130,18 @@ "@smithy/middleware-apply-body-checksum": "^4.3.9", "@smithy/protocol-http": "^5.3.9", "@smithy/querystring-builder": "^4.2.9", + "@smithy/util-retry": "^4.4.5", "@trystero-p2p/nostr": "^0.24.0", "chokidar": "^4.0.0", "commander": "^14.0.3", - "obsidian": "^1.12.3", "diff-match-patch": "^1.0.5", "fflate": "^0.8.2", "idb": "^8.0.3", "markdown-it": "^14.1.1", "micromatch": "^4.0.0", "minimatch": "^10.2.2", - "octagonal-wheels": "^0.1.45", + "obsidian": "^1.12.3", + "octagonal-wheels": "^0.1.46", "pouchdb-adapter-leveldb": "^9.0.0", "qrcode-generator": "^1.4.4", "werift": "^0.23.0", diff --git a/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts b/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts index c2f11e1..f6bc9e0 100644 --- a/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts +++ b/src/apps/cli/managers/CLIStorageEventManagerAdapter.ts @@ -105,7 +105,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter { private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile { return { - path: path.relative(this.basePath, filePath) as FilePath, + path: path.relative(this.basePath, filePath).replace(/\\/g, "/") as FilePath, stat: { ctime: stats?.ctimeMs ?? Date.now(), mtime: stats?.mtimeMs ?? Date.now(), @@ -117,7 +117,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter { private _toNodeFolder(dirPath: string): NodeFolder { return { - path: path.relative(this.basePath, dirPath) as FilePath, + path: path.relative(this.basePath, dirPath).replace(/\\/g, "/") as FilePath, isFolder: true, }; } diff --git a/src/apps/cli/vite.config.ts b/src/apps/cli/vite.config.ts index 11104cd..01992e1 100644 --- a/src/apps/cli/vite.config.ts +++ b/src/apps/cli/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import path from "node:path"; import { readFileSync } from "node:fs"; +const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/"); const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8")); const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8")); // https://vite.dev/config/ @@ -63,17 +64,14 @@ export default defineConfig({ resolve: { alias: { "@lib/worker/bgWorker.ts": "../../lib/src/worker/bgWorker.mock.ts", - "@lib/pouchdb/pouchdb-browser.ts": path.resolve(__dirname, "lib/pouchdb-node.ts"), + "@lib/pouchdb/pouchdb-browser.ts": resolve(__dirname, "lib/pouchdb-node.ts"), // The CLI runs on Node.js; force AWS XML builder to its CJS Node entry // so Vite does not resolve the browser DOMParser-based XML parser. - "@aws-sdk/xml-builder": path.resolve( - __dirname, - "../../../node_modules/@aws-sdk/xml-builder/dist-cjs/index.js" - ), + "@aws-sdk/xml-builder": resolve(__dirname, "../../../node_modules/@aws-sdk/xml-builder/dist-cjs/index.js"), // Force fflate to the Node CJS entry; browser entry expects Web Worker globals. - fflate: path.resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"), - "@": path.resolve(__dirname, "../../"), - "@lib": path.resolve(__dirname, "../../lib/src"), + fflate: resolve(__dirname, "../../../node_modules/fflate/lib/node.cjs"), + "@": resolve(__dirname, "../../"), + "@lib": resolve(__dirname, "../../lib/src"), "../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts", }, }, @@ -85,7 +83,7 @@ export default defineConfig({ minify: false, rollupOptions: { input: { - index: path.resolve(__dirname, "entrypoint.ts"), + index: resolve(__dirname, "entrypoint.ts"), }, external: (id) => { if (defaultExternal.includes(id)) return true; @@ -101,7 +99,7 @@ export default defineConfig({ }, }, lib: { - entry: path.resolve(__dirname, "entrypoint.ts"), + entry: resolve(__dirname, "entrypoint.ts"), formats: ["cjs"], fileName: "index", }, diff --git a/src/common/utils.ts b/src/common/utils.ts index b008f2c..829d608 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -128,7 +128,7 @@ export const _requestToCouchDBFetch = async ( username: string, password: string, path?: string, - body?: string | any, + body?: any, method?: string ) => { const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]); @@ -146,7 +146,7 @@ export const _requestToCouchDBFetch = async ( contentType: "application/json", body: JSON.stringify(body), }; - return await fetch(uri, requestParam); + return await _fetch(uri, requestParam); }; export const _requestToCouchDB = async ( @@ -214,6 +214,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.cons export { BASE_IS_NEW, EVEN, TARGET_IS_NEW }; // Why 2000? : ZIP FILE Does not have enough resolution. import { compareMTime } from "@lib/common/utils.ts"; +import { _fetch } from "@/lib/src/common/coreEnvFunctions.ts"; export { compareMTime }; function getKey(file: AnyEntry | string | UXFileInfoStub) { const key = typeof file == "string" ? file : stripAllPrefixes(file.path); diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index c98142c..585ef5c 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -68,9 +68,10 @@ import { ConflictResolveModal } from "../../modules/features/InteractiveConflict import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG, eventHub } from "../../common/events.ts"; import { PluginDialogModal } from "./PluginDialogModal.ts"; -import { $msg } from "src/lib/src/common/i18n.ts"; +import { $msg } from "@/lib/src/common/i18n.ts"; import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; import type { LiveSyncCore } from "../../main.ts"; +import { LiveSyncError } from "@lib/common/LSError.ts"; const d = "\u200b"; const d2 = "\n"; @@ -564,7 +565,7 @@ export class ConfigSync extends LiveSyncCommands { ...data, documentPath: this.getPath(wx), files: xFiles, - } as PluginDataExDisplay; + } satisfies PluginDataExDisplay; } return false; } @@ -1069,10 +1070,10 @@ export class ConfigSync extends LiveSyncCommands { } const baseDir = this.configDir; try { - if (!data.documentPath) throw "InternalError: Document path not exist"; + if (!data.documentPath) throw new LiveSyncError("InternalError: Document path not exist"); const dx = await this.localDatabase.getDBEntry(data.documentPath); if (dx == false) { - throw "Not found on database"; + throw new LiveSyncError("Not found on database"); } const loadedData = deserialize(getDocDataAsArray(dx.data), {}) as PluginDataEx; for (const f of loadedData.files) { @@ -1317,7 +1318,7 @@ export class ConfigSync extends LiveSyncCommands { } const docXDoc = await this.localDatabase.getDBEntryFromMeta(old, false, false); if (docXDoc == false) { - throw "Could not load the document"; + throw new LiveSyncError("Could not load the document"); } const dataSrc = getDocData(docXDoc.data); const dataStart = dataSrc.indexOf(DUMMY_END); diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index ead080a..3c086d6 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -50,6 +50,7 @@ import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "../../lib/src import { EVENT_SETTING_SAVED, eventHub } from "../../common/events.ts"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import type { LiveSyncCore } from "../../main.ts"; +import { tryGetFilePath } from "@lib/common/utils.doc.ts"; type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce"; declare global { @@ -317,7 +318,7 @@ export class HiddenFileSync extends LiveSyncCommands { this._fileInfoLastProcessed.set(file, key); } - async updateLastProcessedAsActualFile(file: FilePath, stat?: UXStat | null | undefined) { + async updateLastProcessedAsActualFile(file: FilePath, stat?: UXStat | null) { if (!stat) stat = await this.core.storageAccess.statHidden(file); this._fileInfoLastProcessed.set(file, this.statToKey(stat)); } @@ -411,10 +412,7 @@ export class HiddenFileSync extends LiveSyncCommands { } } - async updateLastProcessedAsActualDatabase( - file: FilePath, - doc?: MetaEntry | LoadedEntry | null | undefined | false - ) { + async updateLastProcessedAsActualDatabase(file: FilePath, doc?: MetaEntry | LoadedEntry | null | false) { const dbPath = addPrefix(file, ICHeader); if (!doc) doc = await this.localDatabase.getDBEntryMeta(dbPath); if (!doc) return; @@ -1050,7 +1048,7 @@ Offline Changed files: ${processFiles.length}`; } notifyProgress(); } catch (ex) { - this._log(`Failed to process storage change file:${file}`, logLevel); + this._log(`Failed to process storage change file:${tryGetFilePath(file)}`, logLevel); this._log(ex, LOG_LEVEL_VERBOSE); } }); @@ -1162,7 +1160,7 @@ Offline Changed files: ${files.length}`; await this.trackDatabaseFileModification(path, "[Scanning]", true, onlyNew, file); notifyProgress(); } catch (ex) { - this._log(`Failed to process database changes:${file}`); + this._log(`Failed to process database changes:${tryGetFilePath(file)}`); this._log(ex, LOG_LEVEL_VERBOSE); } return; @@ -1500,7 +1498,7 @@ Offline Changed files: ${files.length}`; } async storeInternalFileToDatabase(file: InternalFileInfo | UXFileInfo, forceWrite = false) { - const storeFilePath = stripAllPrefixes(file.path as FilePath); + const storeFilePath = stripAllPrefixes(file.path); const storageFilePath = file.path; if (await this.services.vault.isIgnoredByIgnoreFile(storageFilePath)) { return undefined; diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index fb8651c..54eb7aa 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -16,9 +16,8 @@ import { serialized } from "octagonal-wheels/concurrency/lock_v2"; import { arrayToChunkedArray } from "octagonal-wheels/collection"; import { EVENT_ANALYSE_DB_USAGE, EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events"; import type { LiveSyncCouchDBReplicator } from "@/lib/src/replication/couchdb/LiveSyncReplicator"; -import { delay, parseHeaderValues } from "@/lib/src/common/utils"; -import { generateCredentialObject } from "@/lib/src/replication/httplib"; -import { _requestToCouchDB } from "@/common/utils"; +import { delay } from "@/lib/src/common/utils"; +// import { _requestToCouchDB } from "@/common/utils"; const DB_KEY_SEQ = "gc-seq"; const DB_KEY_CHUNK_SET = "chunk-set"; const DB_KEY_DOC_USAGE_MAP = "doc-usage-map"; @@ -391,7 +390,7 @@ Note: **Make sure to synchronise all devices before deletion.** .map((revInfo) => db.get(doc._id, { rev: revInfo.rev })) ).then((docs) => docs.filter((doc) => doc)); for (const oldDoc of oldDocs) { - await processDoc(oldDoc as EntryDoc, false); + await processDoc(oldDoc, false); } } } catch (ex) { @@ -533,7 +532,7 @@ Success: ${successCount}, Errored: ${errored}`; const docMap = new Map>(); const info = await db.info(); // Total number of revisions to process (approximate) - const maxSeq = new Number(info.update_seq); + const maxSeq = Number.parseInt(`${info.update_seq ?? 0}`, 10); let processed = 0; let read = 0; let errored = 0; @@ -560,7 +559,7 @@ Success: ${successCount}, Errored: ${errored}`; }); docMap.set(id, set); } else if (doc.type === EntryTypes.CHUNK) { - const id = doc._id as DocumentID; + const id = doc._id; if (chunkMap.has(id)) { return; } @@ -759,68 +758,68 @@ Success: ${successCount}, Errored: ${errored}`; } } - /** - * Compact the database by temporarily setting the revision limit to 1. - * @returns - */ - async compactDatabaseWithRevLimit() { - // Temporarily set revs_limit to 1, perform compaction, and restore the original revs_limit. - // Very dangerous operation, so now suppressed. - return false; - const replicator = this.core.replicator as LiveSyncCouchDBReplicator; - const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true); - if (!remote) { - this._notice("Failed to connect to remote for compaction."); - return; - } - if (typeof remote == "string") { - this._notice(`Failed to connect to remote for compaction. ${remote}`); - return; - } - const customHeaders = parseHeaderValues(this.settings.couchDB_CustomHeaders); - const credential = generateCredentialObject(this.settings); - const request = async (path: string, method: string = "GET", body: any = undefined) => { - const req = await _requestToCouchDB( - this.settings.couchDB_URI.replace(/\/+$/, "") + - (this.settings.couchDB_DBNAME ? `/${this.settings.couchDB_DBNAME}` : ""), - credential, - window.origin, - path, - body, - method, - customHeaders - ); - return req; - }; - let revsLimit = ""; - const req = await request(`_revs_limit`, "GET"); - if (req.status == 200) { - revsLimit = req.text.trim(); - this._info(`Remote database _revs_limit: ${revsLimit}`); - } else { - this._notice(`Failed to get remote database _revs_limit. Status: ${req.status}`); - return; - } - const req2 = await request(`_revs_limit`, "PUT", 1); - if (req2.status == 200) { - this._info(`Set remote database _revs_limit to 1 for compaction.`); - } - try { - await this.compactDatabase(); - } finally { - // Restore revs_limit - if (revsLimit) { - const req3 = await request(`_revs_limit`, "PUT", parseInt(revsLimit)); - if (req3.status == 200) { - this._info(`Restored remote database _revs_limit to ${revsLimit}.`); - } else { - this._notice( - `Failed to restore remote database _revs_limit. Status: ${req3.status} / ${req3.text}` - ); - } - } - } - } + // /** + // * Compact the database by temporarily setting the revision limit to 1. + // * @returns + // */ + // async compactDatabaseWithRevLimit() { + // // Temporarily set revs_limit to 1, perform compaction, and restore the original revs_limit. + // // Very dangerous operation, so now suppressed. + // return Promise.resolve(false); + // const replicator = this.core.replicator as LiveSyncCouchDBReplicator; + // const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true); + // if (!remote) { + // this._notice("Failed to connect to remote for compaction."); + // return; + // } + // if (typeof remote == "string") { + // this._notice(`Failed to connect to remote for compaction. ${remote}`); + // return; + // } + // const customHeaders = parseHeaderValues(this.settings.couchDB_CustomHeaders); + // const credential = generateCredentialObject(this.settings); + // const request = async (path: string, method: string = "GET", body: any = undefined) => { + // const req = await _requestToCouchDB( + // this.settings.couchDB_URI.replace(/\/+$/, "") + + // (this.settings.couchDB_DBNAME ? `/${this.settings.couchDB_DBNAME}` : ""), + // credential, + // window.origin, + // path, + // body, + // method, + // customHeaders + // ); + // return req; + // }; + // let revsLimit = ""; + // const req = await request(`_revs_limit`, "GET"); + // if (req.status == 200) { + // revsLimit = req.text.trim(); + // this._info(`Remote database _revs_limit: ${revsLimit}`); + // } else { + // this._notice(`Failed to get remote database _revs_limit. Status: ${req.status}`); + // return; + // } + // const req2 = await request(`_revs_limit`, "PUT", 1); + // if (req2.status == 200) { + // this._info(`Set remote database _revs_limit to 1 for compaction.`); + // } + // try { + // await this.compactDatabase(); + // } finally { + // // Restore revs_limit + // if (revsLimit) { + // const req3 = await request(`_revs_limit`, "PUT", parseInt(revsLimit)); + // if (req3.status == 200) { + // this._info(`Restored remote database _revs_limit to ${revsLimit}.`); + // } else { + // this._notice( + // `Failed to restore remote database _revs_limit. Status: ${req3.status} / ${req3.text}` + // ); + // } + // } + // } + // } async gcv3() { if (!this.isAvailable()) return; const replicator = this.core.replicator as LiveSyncCouchDBReplicator; @@ -929,7 +928,7 @@ This may indicate that some devices have not completed synchronisation, which co usedChunks.add(chunkId); } } else if (doc.type === EntryTypes.CHUNK) { - allChunks.set(doc._id as DocumentID, doc._rev); + allChunks.set(doc._id, doc._rev); } } this._notice( diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte index 4d53651..0d8aad6 100644 --- a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte @@ -24,6 +24,7 @@ let serverInfo = $state(undefined); let replicatorStatus = $state(undefined); let roomSuffix = $state(extractP2PRoomSuffix(core?.services.setting.currentSettings()?.P2P_roomID ?? "")); + let useDiagRTC = $state(core?.services.setting.currentSettings()?.P2P_useDiagRTC ?? false); async function requestServerStatus() { await Promise.resolve(liveSyncReplicator.requestStatus()); @@ -48,6 +49,18 @@ } } + async function toggleDiagRTC() { + if (!core) { + return; + } + const next = !useDiagRTC; + await core.services.setting.updateSettings((settings) => { + settings.P2P_useDiagRTC = next; + return settings; + }, true); + useDiagRTC = next; + } + onMount(() => { const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => { serverInfo = status; @@ -58,6 +71,7 @@ }); const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => { roomSuffix = extractP2PRoomSuffix(settings?.P2P_roomID ?? ""); + useDiagRTC = settings?.P2P_useDiagRTC ?? false; }); fireAndForget(async () => { @@ -131,6 +145,48 @@ {/if} + + {#if core} +
+ + +
+ {/if} + + {#if serverInfo} +
+

Stats

+
+
+ Incoming: + {serverInfo.diag.totalNewConnections} +
+
+ Connected: + {serverInfo.diag.totalSuccessfulConnections} +
+
+ Failed: + {serverInfo.diag.totalFailedConnections} +
+
+ Closed: + {serverInfo.diag.totalClosedConnections} +
+
+
+ {/if} \ No newline at end of file diff --git a/src/lib b/src/lib index b9aaf3c..76d9167 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit b9aaf3c03a773cf54887da5826e52774239106b9 +Subproject commit 76d91674c235c1ccf991a14802c737e82e144ef1 diff --git a/src/main.ts b/src/main.ts index 6c5c3f3..3de86ce 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,8 +12,6 @@ import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidian import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts"; import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts"; import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts"; -import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts"; -import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts"; import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts"; import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts"; import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts"; @@ -156,8 +154,6 @@ export default class ObsidianLiveSyncPlugin extends Plugin { new ModuleInteractiveConflictResolver(this, core), new ModuleObsidianGlobalHistory(this, core), new ModuleDev(this, core), - new ModuleReplicateTest(this, core), - new ModuleIntegratedTest(this, core), new SetupManager(core), // this should be moved to core? new ModuleMigration(core), ]; diff --git a/src/modules/coreObsidian/UILib/dialogs.ts b/src/modules/coreObsidian/UILib/dialogs.ts index ce77bd9..d84bcab 100644 --- a/src/modules/coreObsidian/UILib/dialogs.ts +++ b/src/modules/coreObsidian/UILib/dialogs.ts @@ -1,9 +1,11 @@ import { ButtonComponent } from "@/deps.ts"; import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts"; import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts"; +import { compatGlobal, type CompatIntervalHandle } from "@lib/common/coreEnvFunctions.ts"; class AutoClosableModal extends Modal { _closeByUnload() { + // eslint-disable-next-line @typescript-eslint/unbound-method eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload); this.close(); } @@ -11,9 +13,11 @@ class AutoClosableModal extends Modal { constructor(app: App) { super(app); this._closeByUnload = this._closeByUnload.bind(this); + // eslint-disable-next-line @typescript-eslint/unbound-method eventHub.once(EVENT_PLUGIN_UNLOADED, this._closeByUnload); } override onClose() { + // eslint-disable-next-line @typescript-eslint/unbound-method eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload); } } @@ -121,7 +125,7 @@ export class PopoverSelectString extends FuzzySuggestModal { this.callback = undefined; } override onClose(): void { - setTimeout(() => { + compatGlobal.setTimeout(() => { if (this.callback) { this.callback(""); this.callback = undefined; @@ -139,7 +143,7 @@ export class MessageBox extends AutoClosableModal { isManuallyClosed = false; defaultAction: string | undefined; timeout: number | undefined; - timer: ReturnType | undefined = undefined; + timer: CompatIntervalHandle | undefined = undefined; defaultButtonComponent: ButtonComponent | undefined; wideButton: boolean; @@ -165,12 +169,12 @@ export class MessageBox extends AutoClosableModal { this.timeout = timeout; this.wideButton = wideButton; if (this.timeout) { - this.timer = setInterval(() => { + this.timer = compatGlobal.setInterval(() => { if (this.timeout === undefined) return; this.timeout--; if (this.timeout < 0) { if (this.timer) { - clearInterval(this.timer); + compatGlobal.clearInterval(this.timer); this.defaultButtonComponent?.setButtonText(`${defaultAction}`); this.timer = undefined; } @@ -213,7 +217,7 @@ export class MessageBox extends AutoClosableModal { if (this.timer) { labelWrapper.empty(); labelWrapper.style.display = "none"; - clearInterval(this.timer); + compatGlobal.clearInterval(this.timer); this.timer = undefined; this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`); } @@ -224,7 +228,7 @@ export class MessageBox extends AutoClosableModal { this.isManuallyClosed = true; this.result = button; if (this.timer) { - clearInterval(this.timer); + compatGlobal.clearInterval(this.timer); this.timer = undefined; } this.close(); @@ -247,7 +251,7 @@ export class MessageBox extends AutoClosableModal { const { contentEl } = this; contentEl.empty(); if (this.timer) { - clearInterval(this.timer); + compatGlobal.clearInterval(this.timer); this.timer = undefined; } if (this.isManuallyClosed) { diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 5504918..ef5614c 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -27,7 +27,6 @@ export class ObsHttpHandler extends FetchHttpHandler { this.requestTimeoutInMs = options === undefined ? undefined : options.requestTimeout; this.reverseProxyNoSignUrl = reverseProxyNoSignUrl; } - // eslint-disable-next-line require-await override async handle( request: HttpRequest, { abortSignal }: HttpHandlerOptions = {} diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.ts index 33f0485..638b960 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.ts @@ -13,6 +13,7 @@ import { hiddenFilesProcessingCount, } from "../../lib/src/mock_and_interop/stores.ts"; import type { LiveSyncCore } from "../../main.ts"; +import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; export class ModuleObsidianEvents extends AbstractObsidianModule { _everyOnloadStart(): Promise { @@ -79,11 +80,19 @@ export class ModuleObsidianEvents extends AbstractObsidianModule { this.watchWindowVisibility = this.watchWindowVisibility.bind(this); this.watchWorkspaceOpen = this.watchWorkspaceOpen.bind(this); this.watchOnline = this.watchOnline.bind(this); + // Already bound + // eslint-disable-next-line @typescript-eslint/unbound-method this.plugin.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); - this.plugin.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility); + // Already bound + // eslint-disable-next-line @typescript-eslint/unbound-method + this.plugin.registerDomEvent(activeDocument, "visibilitychange", this.watchWindowVisibility); this.plugin.registerDomEvent(window, "focus", () => this.setHasFocus(true)); this.plugin.registerDomEvent(window, "blur", () => this.setHasFocus(false)); + // Already bound + // eslint-disable-next-line @typescript-eslint/unbound-method this.plugin.registerDomEvent(window, "online", this.watchOnline); + // Already bound + // eslint-disable-next-line @typescript-eslint/unbound-method this.plugin.registerDomEvent(window, "offline", this.watchOnline); } @@ -222,9 +231,9 @@ export class ModuleObsidianEvents extends AbstractObsidianModule { ); }); this.plugin.registerInterval( - setInterval(() => { + compatGlobal.setInterval(() => { __tick.value++; - }, 1000) as unknown as number + }, 1000) ); let stableCheck = 3; diff --git a/src/modules/extras/ModuleDev.ts b/src/modules/extras/ModuleDev.ts index a69776a..0395c9e 100644 --- a/src/modules/extras/ModuleDev.ts +++ b/src/modules/extras/ModuleDev.ts @@ -8,7 +8,6 @@ import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts"; import { writable } from "svelte/store"; import type { FilePathWithPrefix } from "../../lib/src/common/types.ts"; import type { LiveSyncCore } from "../../main.ts"; - export class ModuleDev extends AbstractObsidianModule { _everyOnloadStart(): Promise { __onMissingTranslation(() => {}); @@ -98,6 +97,7 @@ export class ModuleDev extends AbstractObsidianModule { }); return Promise.resolve(true); } + async _everyOnLayoutReady(): Promise { if (!this.settings.enableDebugTools) return Promise.resolve(true); // if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) { @@ -111,7 +111,7 @@ export class ModuleDev extends AbstractObsidianModule { const filename = "test-create-conflict.md"; const content = `# Test create conflict\n\n`; const w = await this.core.databaseFileAccess.store({ - name: filename as FilePathWithPrefix, + name: filename, path: filename as FilePathWithPrefix, body: new Blob([content], { type: "text/markdown" }), stat: { diff --git a/src/modules/extras/ModuleIntegratedTest.ts b/src/modules/extras/ModuleIntegratedTest.ts deleted file mode 100644 index df5cc3a..0000000 --- a/src/modules/extras/ModuleIntegratedTest.ts +++ /dev/null @@ -1,446 +0,0 @@ -import { delay } from "octagonal-wheels/promises"; -import { LOG_LEVEL_NOTICE, REMOTE_MINIO, type FilePathWithPrefix } from "src/lib/src/common/types"; -import { shareRunningResult } from "octagonal-wheels/concurrency/lock"; -import { AbstractObsidianModule } from "../AbstractObsidianModule"; - -export class ModuleIntegratedTest extends AbstractObsidianModule { - async waitFor(proc: () => Promise, timeout = 10000): Promise { - await delay(100); - const start = Date.now(); - while (!(await proc())) { - if (timeout > 0) { - if (Date.now() - start > timeout) { - this._log(`Timeout`); - return false; - } - } - await delay(500); - } - return true; - } - waitWithReplicating(proc: () => Promise, timeout = 10000): Promise { - return this.waitFor(async () => { - await this.tryReplicate(); - return await proc(); - }, timeout); - } - async storageContentIsEqual(file: string, content: string): Promise { - try { - const fileContent = await this.readStorageContent(file as FilePathWithPrefix); - if (fileContent === content) { - return true; - } else { - // this._log(`Content is not same \n Expected:${content}\n Actual:${fileContent}`, LOG_LEVEL_VERBOSE); - return false; - } - } catch (e) { - this._log(`Error: ${e}`); - return false; - } - } - async assert(proc: () => Promise): Promise { - if (!(await proc())) { - this._log(`Assertion failed`); - return false; - } - return true; - } - async __orDie(key: string, proc: () => Promise): Promise | never { - if (!(await this._test(key, proc))) { - throw new Error(`${key}`); - } - return true; - } - tryReplicate() { - if (!this.settings.liveSync) { - return shareRunningResult("replicate-test", async () => { - await this.services.replication.replicate(); - }); - } - } - async readStorageContent(file: FilePathWithPrefix): Promise { - if (!(await this.core.storageAccess.isExistsIncludeHidden(file))) { - return undefined; - } - return await this.core.storageAccess.readHiddenFileText(file); - } - async __proceed(no: number, title: string): Promise { - const stepFile = "_STEP.md" as FilePathWithPrefix; - const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix; - const stepContent = `Step ${no}`; - await this.services.conflict.resolveByNewest(stepFile); - await this.core.storageAccess.writeFileAuto(stepFile, stepContent); - await this.__orDie(`Wait for acknowledge ${no}`, async () => { - if ( - !(await this.waitWithReplicating(async () => { - return await this.storageContentIsEqual(stepAckFile, stepContent); - }, 20000)) - ) - return false; - return true; - }); - return true; - } - async __join(no: number, title: string): Promise { - const stepFile = "_STEP.md" as FilePathWithPrefix; - const stepAckFile = "_STEP_ACK.md" as FilePathWithPrefix; - // const otherStepFile = `_STEP_${isLeader ? "R" : "L"}.md` as FilePathWithPrefix; - const stepContent = `Step ${no}`; - - await this.__orDie(`Wait for step ${no} (${title})`, async () => { - if ( - !(await this.waitWithReplicating(async () => { - return await this.storageContentIsEqual(stepFile, stepContent); - }, 20000)) - ) - return false; - return true; - }); - await this.services.conflict.resolveByNewest(stepAckFile); - await this.core.storageAccess.writeFileAuto(stepAckFile, stepContent); - await this.tryReplicate(); - return true; - } - - async performStep({ - step, - title, - isGameChanger, - proc, - check, - }: { - step: number; - title: string; - isGameChanger: boolean; - proc: () => Promise; - check: () => Promise; - }): Promise { - if (isGameChanger) { - await this.__proceed(step, title); - try { - await proc(); - } catch (e) { - this._log(`Error: ${e}`); - return false; - } - return await this.__orDie(`Step ${step} - ${title}`, async () => await this.waitWithReplicating(check)); - } else { - return await this.__join(step, title); - } - } - // // see scenario.md - // async testLeader(testMain: (testFileName: FilePathWithPrefix) => Promise): Promise { - - // } - // async testReceiver(testMain: (testFileName: FilePathWithPrefix) => Promise): Promise { - - // } - async nonLiveTestRunner( - isLeader: boolean, - testMain: (testFileName: FilePathWithPrefix, isLeader: boolean) => Promise - ): Promise { - const storage = this.core.storageAccess; - // const database = this.core.databaseFileAccess; - // const _orDie = this._orDie.bind(this); - const testCommandFile = "IT.md" as FilePathWithPrefix; - const textCommandResponseFile = "ITx.md" as FilePathWithPrefix; - let testFileName: FilePathWithPrefix; - this.addTestResult( - "-------Starting ... ", - true, - `Test as ${isLeader ? "Leader" : "Receiver"} command file ${testCommandFile}` - ); - if (isLeader) { - await this.__proceed(0, "start"); - } - await this.tryReplicate(); - - await this.performStep({ - step: 0, - title: "Make sure that command File Not Exists", - isGameChanger: isLeader, - proc: async () => await storage.removeHidden(testCommandFile), - check: async () => !(await storage.isExistsIncludeHidden(testCommandFile)), - }); - await this.performStep({ - step: 1, - title: "Make sure that command File Not Exists On Receiver", - isGameChanger: !isLeader, - proc: async () => await storage.removeHidden(textCommandResponseFile), - check: async () => !(await storage.isExistsIncludeHidden(textCommandResponseFile)), - }); - - await this.performStep({ - step: 2, - title: "Decide the test file name", - isGameChanger: isLeader, - proc: async () => { - testFileName = (Date.now() + "-" + Math.ceil(Math.random() * 1000) + ".md") as FilePathWithPrefix; - const testCommandFile = "IT.md" as FilePathWithPrefix; - await storage.writeFileAuto(testCommandFile, testFileName); - }, - check: () => Promise.resolve(true), - }); - await this.performStep({ - step: 3, - title: "Wait for the command file to be arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await storage.isExistsIncludeHidden(testCommandFile), - }); - - await this.performStep({ - step: 4, - title: "Send the response file", - isGameChanger: !isLeader, - proc: async () => { - await storage.writeHiddenFileAuto(textCommandResponseFile, "!"); - }, - check: () => Promise.resolve(true), - }); - await this.performStep({ - step: 5, - title: "Wait for the response file to be arrived", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => await storage.isExistsIncludeHidden(textCommandResponseFile), - }); - - await this.performStep({ - step: 6, - title: "Proceed to begin the test", - isGameChanger: isLeader, - proc: async () => {}, - check: () => Promise.resolve(true), - }); - await this.performStep({ - step: 6, - title: "Begin the test", - isGameChanger: !false, - proc: async () => {}, - check: () => { - return Promise.resolve(true); - }, - }); - // await this.step(0, isLeader, true); - try { - this.addTestResult("** Main------", true, ``); - if (isLeader) { - return await testMain(testFileName!, true); - } else { - const testFileName = await this.readStorageContent(testCommandFile); - this.addTestResult("testFileName", true, `Request client to use :${testFileName!}`); - return await testMain(testFileName! as FilePathWithPrefix, false); - } - } finally { - this.addTestResult("Teardown", true, `Deleting ${testFileName!}`); - await storage.removeHidden(testFileName!); - } - - return true; - // Make sure the - } - - async testBasic(filename: FilePathWithPrefix, isLeader: boolean): Promise { - const storage = this.core.storageAccess; - const database = this.core.databaseFileAccess; - - await this.addTestResult( - `---**Starting Basic Test**---`, - true, - `Test as ${isLeader ? "Leader" : "Receiver"} command file ${filename}` - ); - // if (isLeader) { - // await this._proceed(0); - // } - // await this.tryReplicate(); - - await this.performStep({ - step: 0, - title: "Make sure that file is not exist", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => !(await storage.isExists(filename)), - }); - - await this.performStep({ - step: 1, - title: "Write a file", - isGameChanger: isLeader, - proc: async () => await storage.writeFileAuto(filename, "Hello World"), - check: async () => await storage.isExists(filename), - }); - await this.performStep({ - step: 2, - title: "Make sure the file is arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await storage.isExists(filename), - }); - await this.performStep({ - step: 3, - title: "Update to Hello World 2", - isGameChanger: isLeader, - proc: async () => await storage.writeFileAuto(filename, "Hello World 2"), - check: async () => await this.storageContentIsEqual(filename, "Hello World 2"), - }); - await this.performStep({ - step: 4, - title: "Make sure the modified file is arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, "Hello World 2"), - }); - await this.performStep({ - step: 5, - title: "Update to Hello World 3", - isGameChanger: !isLeader, - proc: async () => await storage.writeFileAuto(filename, "Hello World 3"), - check: async () => await this.storageContentIsEqual(filename, "Hello World 3"), - }); - await this.performStep({ - step: 6, - title: "Make sure the modified file is arrived", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, "Hello World 3"), - }); - - const multiLineContent = `Line1:A -Line2:B -Line3:C -Line4:D`; - - await this.performStep({ - step: 7, - title: "Update to Multiline", - isGameChanger: isLeader, - proc: async () => await storage.writeFileAuto(filename, multiLineContent), - check: async () => await this.storageContentIsEqual(filename, multiLineContent), - }); - - await this.performStep({ - step: 8, - title: "Make sure the modified file is arrived", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, multiLineContent), - }); - - // While LiveSync, possibly cannot cause the conflict. - if (!this.settings.liveSync) { - // Step 9 Make Conflict But Resolvable - const multiLineContentL = `Line1:A -Line2:B -Line3:C! -Line4:D`; - const multiLineContentC = `Line1:A -Line2:bbbbb -Line3:C -Line4:D`; - - await this.performStep({ - step: 9, - title: "Progress to be conflicted", - isGameChanger: isLeader, - proc: async () => {}, - check: () => Promise.resolve(true), - }); - - await storage.writeFileAuto(filename, isLeader ? multiLineContentL : multiLineContentC); - - await this.performStep({ - step: 10, - title: "Update As Conflicted", - isGameChanger: !isLeader, - proc: async () => {}, - check: () => Promise.resolve(true), - }); - - await this.performStep({ - step: 10, - title: "Make sure Automatically resolved", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => (await database.getConflictedRevs(filename)).length === 0, - }); - await this.performStep({ - step: 11, - title: "Make sure Automatically resolved", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => (await database.getConflictedRevs(filename)).length === 0, - }); - - const sensiblyMergedContent = `Line1:A -Line2:bbbbb -Line3:C! -Line4:D`; - - await this.performStep({ - step: 12, - title: "Make sure Sensibly Merged on Leader", - isGameChanger: isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent), - }); - await this.performStep({ - step: 13, - title: "Make sure Sensibly Merged on Receiver", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => await this.storageContentIsEqual(filename, sensiblyMergedContent), - }); - } - await this.performStep({ - step: 14, - title: "Delete File", - isGameChanger: isLeader, - proc: async () => { - await storage.removeHidden(filename); - }, - check: async () => !(await storage.isExists(filename)), - }); - - await this.performStep({ - step: 15, - title: "Make sure File is deleted", - isGameChanger: !isLeader, - proc: async () => {}, - check: async () => !(await storage.isExists(filename)), - }); - this._log(`The Basic Test has been completed`, LOG_LEVEL_NOTICE); - return true; - } - - async testBasicEvent(isLeader: boolean) { - this.settings.liveSync = false; - await this.saveSettings(); - await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l))); - } - async testBasicLive(isLeader: boolean) { - this.settings.liveSync = true; - await this.saveSettings(); - await this._test("basic", async () => await this.nonLiveTestRunner(isLeader, (t, l) => this.testBasic(t, l))); - } - - async _everyModuleTestMultiDevice(): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - const isLeader = this.core.services.vault.vaultName().indexOf("recv") === -1; - this.addTestResult("-------", true, `Test as ${isLeader ? "Leader" : "Receiver"}`); - try { - this._log(`Starting Test`); - await this.testBasicEvent(isLeader); - if (this.settings.remoteType == REMOTE_MINIO) await this.testBasicLive(isLeader); - } catch (e) { - this._log(e); - this._log(`Error: ${e}`); - return Promise.resolve(false); - } - - return Promise.resolve(true); - } - override onBindFunction(core: typeof this.core, services: typeof core.services): void { - services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this)); - } -} diff --git a/src/modules/extras/ModuleReplicateTest.ts b/src/modules/extras/ModuleReplicateTest.ts deleted file mode 100644 index 394042a..0000000 --- a/src/modules/extras/ModuleReplicateTest.ts +++ /dev/null @@ -1,590 +0,0 @@ -// I intend to discontinue maintenance of this class. It seems preferable to test it externally. -import { delay } from "octagonal-wheels/promises"; -import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; -import { eventHub } from "../../common/events"; -import { getWebCrypto } from "../../lib/src/mods.ts"; -import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex"; -import { parseYaml, requestUrl, stringifyYaml } from "@/deps.ts"; -import type { FilePath } from "../../lib/src/common/types.ts"; -import { scheduleTask } from "octagonal-wheels/concurrency/task"; -import { getFileRegExp } from "../../lib/src/common/utils.ts"; -import type { LiveSyncCore } from "../../main.ts"; - -declare global { - interface LSEvents { - "debug-sync-status": string[]; - } -} - -export class ModuleReplicateTest extends AbstractObsidianModule { - testRootPath = "_test/"; - testInfoPath = "_testinfo/"; - - get isLeader() { - return ( - this.services.vault.getVaultName().indexOf("dev") >= 0 && - this.services.vault.vaultName().indexOf("recv") < 0 - ); - } - - get nameByKind() { - if (!this.isLeader) { - return "RECV"; - } else if (this.isLeader) { - return "LEADER"; - } - } - get pairName() { - if (this.isLeader) { - return "RECV"; - } else if (!this.isLeader) { - return "LEADER"; - } - } - - watchIsSynchronised = false; - - statusBarSyncStatus?: HTMLElement; - async readFileContent(file: string) { - try { - return await this.core.storageAccess.readHiddenFileText(file); - } catch { - return ""; - } - } - - async dumpList() { - if (this.settings.syncInternalFiles) { - this._log("Write file list (Include Hidden)"); - await this.__dumpFileListIncludeHidden("files.md"); - } else { - this._log("Write file list"); - await this.__dumpFileList("files.md"); - } - } - async _everyBeforeReplicate(showMessage: boolean): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - await this.dumpList(); - return true; - } - private _everyOnloadAfterLoadSettings(): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - this.addCommand({ - id: "dump-file-structure-normal", - name: `Dump Structure (Normal)`, - callback: () => { - void this.__dumpFileList("files.md").finally(() => { - void this.refreshSyncStatus(); - }); - }, - }); - this.addCommand({ - id: "dump-file-structure-ih", - name: "Dump Structure (Include Hidden)", - callback: () => { - const d = "files.md"; - void this.__dumpFileListIncludeHidden(d); - }, - }); - this.addCommand({ - id: "dump-file-structure-auto", - name: "Dump Structure", - callback: () => { - void this.dumpList(); - }, - }); - this.addCommand({ - id: "dump-file-test", - name: `Perform Test (Dev) ${this.isLeader ? "(Leader)" : "(Recv)"}`, - callback: () => { - void this.performTestManually(); - }, - }); - this.addCommand({ - id: "watch-sync-result", - name: `Watch sync result is matched between devices`, - callback: () => { - this.watchIsSynchronised = !this.watchIsSynchronised; - void this.refreshSyncStatus(); - }, - }); - this.app.vault.on("modify", async (file) => { - if (file.path.startsWith(this.testInfoPath)) { - await this.refreshSyncStatus(); - } else { - scheduleTask("dumpStatus", 125, async () => { - await this.dumpList(); - return true; - }); - } - }); - this.statusBarSyncStatus = this.plugin.addStatusBarItem(); - return Promise.resolve(true); - } - async getSyncStatusAsText() { - const fileMine = this.testInfoPath + this.nameByKind + "/" + "files.md"; - const filePair = this.testInfoPath + this.pairName + "/" + "files.md"; - const mine = parseYaml(await this.readFileContent(fileMine)); - const pair = parseYaml(await this.readFileContent(filePair)); - const result = [] as string[]; - if (mine.length != pair.length) { - result.push(`File count is different: ${mine.length} vs ${pair.length}`); - } - const filesAll = new Set([...mine.map((e: any) => e.path), ...pair.map((e: any) => e.path)]); - for (const file of filesAll) { - const mineFile = mine.find((e: any) => e.path == file); - const pairFile = pair.find((e: any) => e.path == file); - if (!mineFile || !pairFile) { - result.push(`File not found: ${file}`); - } else { - if (mineFile.size != pairFile.size) { - result.push(`Size is different: ${file} ${mineFile.size} vs ${pairFile.size}`); - } - if (mineFile.hash != pairFile.hash) { - result.push(`Hash is different: ${file} ${mineFile.hash} vs ${pairFile.hash}`); - } - } - } - eventHub.emitEvent("debug-sync-status", result); - return result.join("\n"); - } - - async refreshSyncStatus() { - if (this.watchIsSynchronised) { - // Normal Files - const syncStatus = await this.getSyncStatusAsText(); - if (syncStatus) { - this.statusBarSyncStatus!.setText(`Sync Status: Having Error`); - this._log(`Sync Status: Having Error\n${syncStatus}`, LOG_LEVEL_INFO); - } else { - this.statusBarSyncStatus!.setText(`Sync Status: Synchronised`); - } - } else { - this.statusBarSyncStatus!.setText(""); - } - } - - async __dumpFileList(outFile?: string) { - if (!this.core || !this.core.storageAccess) { - this._log("No storage access", LOG_LEVEL_INFO); - return; - } - const files = await this.core.storageAccess.getFiles(); - const out = [] as any[]; - const webcrypto = await getWebCrypto(); - for (const file of files) { - if (!(await this.services.vault.isTargetFile(file.path))) { - continue; - } - if (file.path.startsWith(this.testInfoPath)) continue; - const stat = await this.core.storageAccess.stat(file.path); - if (stat) { - const hashSrc = await this.core.storageAccess.readHiddenFileBinary(file.path); - const hash = await webcrypto.subtle.digest("SHA-1", hashSrc); - const hashStr = uint8ArrayToHexString(new Uint8Array(hash)); - const item = { - path: file.path, - name: file.name, - size: stat.size, - mtime: stat.mtime, - hash: hashStr, - }; - // const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`; - out.push(item); - } - } - out.sort((a, b) => a.path.localeCompare(b.path)); - if (outFile) { - outFile = this.testInfoPath + this.nameByKind + "/" + outFile; - await this.core.storageAccess.ensureDir(outFile); - await this.core.storageAccess.writeHiddenFileAuto(outFile, stringifyYaml(out)); - } else { - // console.dir(out); - } - this._log(`Dumped ${out.length} files`, LOG_LEVEL_INFO); - } - - async __dumpFileListIncludeHidden(outFile?: string) { - const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns"); - const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns"); - const out = [] as any[]; - const files = await this.core.storageAccess.getFilesIncludeHidden("", targetPatterns, ignorePatterns); - // console.dir(files); - const webcrypto = await getWebCrypto(); - for (const file of files) { - // if (!await this.core.$$isTargetFile(file)) { - // continue; - // } - if (file.startsWith(this.testInfoPath)) continue; - const stat = await this.core.storageAccess.statHidden(file); - if (stat) { - const hashSrc = await this.core.storageAccess.readHiddenFileBinary(file); - const hash = await webcrypto.subtle.digest("SHA-1", hashSrc); - const hashStr = uint8ArrayToHexString(new Uint8Array(hash)); - const item = { - path: file, - name: file.split("/").pop(), - size: stat.size, - mtime: stat.mtime, - hash: hashStr, - }; - // const fileLine = `-${file.path}:${stat.size}:${stat.mtime}:${hashStr}`; - out.push(item); - } - } - out.sort((a, b) => a.path.localeCompare(b.path)); - if (outFile) { - outFile = this.testInfoPath + this.nameByKind + "/" + outFile; - await this.core.storageAccess.ensureDir(outFile); - await this.core.storageAccess.writeHiddenFileAuto(outFile, stringifyYaml(out)); - } else { - // console.dir(out); - } - this._log(`Dumped ${out.length} files`, LOG_LEVEL_NOTICE); - } - - async collectTestFiles() { - const remoteTopDir = "https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/refs/heads/main/"; - const files = [ - "README.md", - "docs/adding_translations.md", - "docs/design_docs_of_journalsync.md", - "docs/design_docs_of_keep_newborn_chunks.md", - "docs/design_docs_of_prefixed_hidden_file_sync.md", - "docs/design_docs_of_sharing_tweak_value.md", - "docs/quick_setup_cn.md", - "docs/quick_setup_ja.md", - "docs/quick_setup.md", - "docs/settings_ja.md", - "docs/settings.md", - "docs/setup_cloudant_ja.md", - "docs/setup_cloudant.md", - "docs/setup_flyio.md", - "docs/setup_own_server_cn.md", - "docs/setup_own_server_ja.md", - "docs/setup_own_server.md", - "docs/tech_info_ja.md", - "docs/tech_info.md", - "docs/terms.md", - "docs/troubleshooting.md", - "images/1.png", - "images/2.png", - "images/corrupted_data.png", - "images/hatch.png", - "images/lock_pattern1.png", - "images/lock_pattern2.png", - "images/quick_setup_1.png", - "images/quick_setup_2.png", - "images/quick_setup_3.png", - "images/quick_setup_3b.png", - "images/quick_setup_4.png", - "images/quick_setup_5.png", - "images/quick_setup_6.png", - "images/quick_setup_7.png", - "images/quick_setup_8.png", - "images/quick_setup_9_1.png", - "images/quick_setup_9_2.png", - "images/quick_setup_10.png", - "images/remote_db_setting.png", - "images/write_logs_into_the_file.png", - ]; - for (const file of files) { - const remote = remoteTopDir + file; - const local = this.testRootPath + file; - try { - const f = (await requestUrl(remote)).arrayBuffer; - await this.core.storageAccess.ensureDir(local); - await this.core.storageAccess.writeHiddenFileAuto(local, f); - } catch (ex) { - this._log(`Could not fetch ${remote}`, LOG_LEVEL_VERBOSE); - this._log(ex, LOG_LEVEL_VERBOSE); - } - } - - await this.dumpList(); - } - - async waitFor(proc: () => Promise, timeout = 10000): Promise { - await delay(100); - const start = Date.now(); - while (!(await proc())) { - if (timeout > 0) { - if (Date.now() - start > timeout) { - this._log(`Timeout`); - return false; - } - } - await delay(500); - } - return true; - } - - async testConflictedManually1() { - await this.services.replication.replicate(); - - const commonFile = `Resolve! -*****, the amazing chocolatier!!`; - - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", commonFile); - } - - await this.services.replication.replicate(); - await this.services.replication.replicate(); - if ( - (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 1?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - - const fileA = `Resolve to KEEP THIS -Willy Wonka, Willy Wonka, the amazing chocolatier!!`; - - const fileB = `Resolve to DISCARD THIS -Charlie Bucket, Charlie Bucket, the amazing chocolatier!!`; - - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileA); - } else { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "wonka.md", fileB); - } - - if ( - (await this.core.confirm.askYesNoDialog("Ready to check the result of Manually 1?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - await this.services.replication.replicate(); - await this.services.replication.replicate(); - - if ( - !(await this.waitFor(async () => { - await this.services.replication.replicate(); - return ( - (await this.__assertStorageContent( - (this.testRootPath + "wonka.md") as FilePath, - fileA, - false, - true - )) == true - ); - }, 30000)) - ) { - return await this.__assertStorageContent((this.testRootPath + "wonka.md") as FilePath, fileA, false, true); - } - return true; - // We have to check the result - } - - async testConflictedManually2() { - await this.services.replication.replicate(); - - const commonFile = `Resolve To concatenate -ABCDEFG`; - - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", commonFile); - } - - await this.services.replication.replicate(); - await this.services.replication.replicate(); - if ( - (await this.core.confirm.askYesNoDialog("Ready to begin the test conflict Manually 2?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - - const fileA = `Resolve to Concatenate -ABCDEFGHIJKLMNOPQRSTYZ`; - - const fileB = `Resolve to Concatenate -AJKLMNOPQRSTUVWXYZ`; - - const concatenated = `Resolve to Concatenate -ABCDEFGHIJKLMNOPQRSTUVWXYZ`; - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileA); - } else { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "concat.md", fileB); - } - if ( - (await this.core.confirm.askYesNoDialog("Ready to test conflict Manually 2?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - await this.services.replication.replicate(); - await this.services.replication.replicate(); - - if ( - !(await this.waitFor(async () => { - await this.services.replication.replicate(); - return ( - (await this.__assertStorageContent( - (this.testRootPath + "concat.md") as FilePath, - concatenated, - false, - true - )) == true - ); - }, 30000)) - ) { - return await this.__assertStorageContent( - (this.testRootPath + "concat.md") as FilePath, - concatenated, - false, - true - ); - } - return true; - } - - async testConflictAutomatic() { - if (this.isLeader) { - const baseDoc = `Tasks! -- [ ] Task 1 -- [ ] Task 2 -- [ ] Task 3 -- [ ] Task 4 -`; - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", baseDoc); - } - await delay(100); - await this.services.replication.replicate(); - await this.services.replication.replicate(); - - if ( - (await this.core.confirm.askYesNoDialog("Ready to test conflict?", { - timeout: 30, - defaultOption: "Yes", - })) == "no" - ) { - return; - } - const mod1Doc = `Tasks! -- [ ] Task 1 -- [v] Task 2 -- [ ] Task 3 -- [ ] Task 4 -`; - - const mod2Doc = `Tasks! -- [ ] Task 1 -- [ ] Task 2 -- [v] Task 3 -- [ ] Task 4 -`; - if (this.isLeader) { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod1Doc); - } else { - await this.core.storageAccess.writeHiddenFileAuto(this.testRootPath + "task.md", mod2Doc); - } - - await this.services.replication.replicate(); - await this.services.replication.replicate(); - await delay(1000); - if ( - (await this.core.confirm.askYesNoDialog("Ready to check result?", { timeout: 30, defaultOption: "Yes" })) == - "no" - ) { - return; - } - await this.services.replication.replicate(); - await this.services.replication.replicate(); - const mergedDoc = `Tasks! -- [ ] Task 1 -- [v] Task 2 -- [v] Task 3 -- [ ] Task 4 -`; - return this.__assertStorageContent((this.testRootPath + "task.md") as FilePath, mergedDoc, false, true); - } - - // No longer tested - async checkConflictResolution() { - this._log("Before testing conflicted files, resolve all once", LOG_LEVEL_NOTICE); - await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); - await this.services.conflict.resolveAllConflictedFilesByNewerOnes(); - await this.services.replication.replicate(); - await delay(1000); - if (!(await this.testConflictAutomatic())) { - this._log("Conflict resolution (Auto) failed", LOG_LEVEL_NOTICE); - return false; - } - if (!(await this.testConflictedManually1())) { - this._log("Conflict resolution (Manual1) failed", LOG_LEVEL_NOTICE); - return false; - } - if (!(await this.testConflictedManually2())) { - this._log("Conflict resolution (Manual2) failed", LOG_LEVEL_NOTICE); - return false; - } - return true; - } - - async __assertStorageContent( - fileName: FilePath, - content: string, - inverted = false, - showResult = false - ): Promise { - try { - const fileContent = await this.core.storageAccess.readHiddenFileText(fileName); - let result = fileContent === content; - if (inverted) { - result = !result; - } - if (result) { - return true; - } else { - if (showResult) { - this._log(`Content is not same \n Expected:${content}\n Actual:${fileContent}`, LOG_LEVEL_VERBOSE); - } - return `Content is not same \n Expected:${content}\n Actual:${fileContent}`; - } - } catch (e) { - this._log(`Cannot assert storage content: ${e}`); - return false; - } - } - async performTestManually() { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - await this.checkConflictResolution(); - // await this.collectTestFiles(); - } - - // testResults = writable<[boolean, string, string][]>([]); - // testResults: string[] = []; - - // $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void { - // const logLine = `${name}: ${key} ${summary ?? ""}`; - // this.testResults.update((results) => { - // results.push([result, logLine, message ?? ""]); - // return results; - // }); - // } - private async _everyModuleTestMultiDevice(): Promise { - if (!this.settings.enableDebugTools) return Promise.resolve(true); - // this.core.$$addTestResult("DevModule", "Test", true); - // return Promise.resolve(true); - await this._test("Conflict resolution", async () => await this.checkConflictResolution()); - return this.testDone(); - } - override onBindFunction(core: LiveSyncCore, services: typeof core.services): void { - services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this)); - services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this)); - services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this)); - } -} diff --git a/src/modules/extras/devUtil/TestPane.svelte b/src/modules/extras/devUtil/TestPane.svelte index 6ea0eea..2d8f378 100644 --- a/src/modules/extras/devUtil/TestPane.svelte +++ b/src/modules/extras/devUtil/TestPane.svelte @@ -1,17 +1,15 @@
- -
-
- - - + +
+
+ + + -
-
-
- {#each messages as line} -
{line}
- {/each} -
+
+
+
+ {#each messages as line} +
{line}
+ {/each} +
diff --git a/src/modules/features/Log/LogPaneView.ts b/src/modules/features/Log/LogPaneView.ts index 5af45f6..c032705 100644 --- a/src/modules/features/Log/LogPaneView.ts +++ b/src/modules/features/Log/LogPaneView.ts @@ -2,7 +2,7 @@ import { WorkspaceLeaf } from "@/deps.ts"; import LogPaneComponent from "./LogPane.svelte"; import type ObsidianLiveSyncPlugin from "../../../main.ts"; import { SvelteItemView } from "../../../common/SvelteItemView.ts"; -import { $msg } from "src/lib/src/common/i18n.ts"; +import { $msg } from "@lib/common/i18n.ts"; import { mount } from "svelte"; export const VIEW_TYPE_LOG = "log-log"; //Log view diff --git a/src/modules/features/ModuleInteractiveConflictResolver.ts b/src/modules/features/ModuleInteractiveConflictResolver.ts index 0c04092..0ef7c12 100644 --- a/src/modules/features/ModuleInteractiveConflictResolver.ts +++ b/src/modules/features/ModuleInteractiveConflictResolver.ts @@ -88,7 +88,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule { return false; } } else { - this._log(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE); + this._log(`Merge: Something went wrong: ${filename}, (${toDelete as string})`, LOG_LEVEL_NOTICE); return false; } // In here, some merge has been processed. @@ -163,7 +163,7 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule { this._log(`There are no conflicting files`, LOG_LEVEL_VERBOSE); } } catch (e) { - this._log(`Error while scanning conflicted files: ${e}`, LOG_LEVEL_NOTICE); + this._log(`Error while scanning conflicted files...`, LOG_LEVEL_NOTICE); this._log(e, LOG_LEVEL_VERBOSE); return false; } diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index 1eb7945..062549a 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -29,7 +29,7 @@ import { addIcon, debounce, normalizePath, Notice, stringifyYaml, type Workspace import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger"; import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts"; import { serialized } from "octagonal-wheels/concurrency/lock"; -import { $msg } from "src/lib/src/common/i18n.ts"; +import { $msg } from "@lib/common/i18n.ts"; import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector.ts"; import type { LiveSyncCore } from "../../main.ts"; import { LiveSyncError } from "@lib/common/LSError.ts"; @@ -262,7 +262,7 @@ export class ModuleLog extends AbstractObsidianModule { this.statusDiv.remove(); // this.statusDiv.pa(); const container = mdv.view.containerEl; - container.insertBefore(this.statusDiv, container.lastChild); + container.appendChild(this.statusDiv); } } @@ -466,12 +466,14 @@ ${stringifyYaml(info)} this.observeForLogs(); - this.statusDiv = this.app.workspace.containerEl.createDiv({ cls: "livesync-status" }); - this.statusLine = this.statusDiv.createDiv({ cls: "livesync-status-statusline" }); - this.messageArea = this.statusDiv.createDiv({ cls: "livesync-status-messagearea" }); - this.logMessage = this.statusDiv.createDiv({ cls: "livesync-status-logmessage" }); - this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" }); - this.statusDiv.style.display = this.settings?.showStatusOnEditor ? "" : "none"; + if (this.settings.showStatusOnEditor) { + this.statusDiv = this.app.workspace.containerEl.createDiv({ cls: "livesync-status" }); + this.statusLine = this.statusDiv.createDiv({ cls: "livesync-status-statusline" }); + this.messageArea = this.statusDiv.createDiv({ cls: "livesync-status-messagearea" }); + this.logMessage = this.statusDiv.createDiv({ cls: "livesync-status-logmessage" }); + this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" }); + this.statusDiv.style.display = this.settings?.showStatusOnEditor ? "" : "none"; + } eventHub.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition()); if (this.settings?.showStatusOnStatusbar) { this.statusBar = this.services.API.addStatusBarItem(); @@ -516,7 +518,12 @@ ${stringifyYaml(info)} let errorInfo = ""; if (message instanceof Error) { if (message instanceof LiveSyncError) { - errorInfo = `${message.cause?.name}:${message.cause?.message}\n[StackTrace]: ${message.stack}\n[CausedBy]: ${message.cause?.stack}`; + if (message.cause && message.cause instanceof Error) { + const causedError = message.cause; + errorInfo = `${causedError?.name}:${causedError?.message}\n[StackTrace]: ${message.stack}\n[CausedBy]: ${causedError?.stack}`; + } else { + errorInfo = `${message.name}:${message.message}\n[StackTrace]: ${message.stack}`; + } } else { const thisStack = new Error().stack; errorInfo = `${message.name}:${message.message}\n[StackTrace]: ${message.stack}\n[LogCallStack]: ${thisStack}`; diff --git a/src/modules/features/SettingDialogue/LiveSyncSetting.ts b/src/modules/features/SettingDialogue/LiveSyncSetting.ts index 0a7f90e..894916e 100644 --- a/src/modules/features/SettingDialogue/LiveSyncSetting.ts +++ b/src/modules/features/SettingDialogue/LiveSyncSetting.ts @@ -8,12 +8,7 @@ import { type ValueComponent, } from "@/deps.ts"; import { unique } from "octagonal-wheels/collection"; -import { - LEVEL_ADVANCED, - LEVEL_POWER_USER, - statusDisplay, - type ConfigurationItem, -} from "../../../lib/src/common/types.ts"; +import { LEVEL_ADVANCED, LEVEL_POWER_USER, statusDisplay, type ConfigurationItem } from "@lib/common/types.ts"; import { createStub, type ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import { type AllSettingItemKey, @@ -23,7 +18,7 @@ import { type AllNumericItemKey, type AllBooleanItemKey, } from "./settingConstants.ts"; -import { $msg } from "src/lib/src/common/i18n.ts"; +import { $msg } from "@lib/common/i18n.ts"; import { findAttrFromParent, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts"; export class LiveSyncSetting extends Setting { diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 9469397..c06dcfe 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -62,6 +62,7 @@ import { paneAdvanced } from "./PaneAdvanced.ts"; import { panePowerUsers } from "./PanePowerUsers.ts"; import { panePatches } from "./PanePatches.ts"; import { paneMaintenance } from "./PaneMaintenance.ts"; +import { compatGlobal } from "@lib/common/coreEnvFunctions.ts"; // For creating a document const toc = new Set(); @@ -141,7 +142,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { async saveLocalSetting(key: keyof typeof OnDialogSettingsDefault) { if (key == "configPassphrase") { - localStorage.setItem("ls-setting-passphrase", this.editingSettings?.[key] ?? ""); + compatGlobal.localStorage.setItem("ls-setting-passphrase", this.editingSettings?.[key] ?? ""); return await Promise.resolve(); } if (key == "deviceAndVaultName") { @@ -180,7 +181,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { // if (runOnSaved) { const handlers = this.onSavedHandlers .filter((e) => appliedKeys.indexOf(e.key) !== -1) - .map((e) => e.handler(this.editingSettings[e.key as AllSettingItemKey])); + .map((e) => Promise.resolve(e.handler(this.editingSettings[e.key as AllSettingItemKey]))); await Promise.all(handlers); // } keys.forEach((e) => this.refreshSetting(e)); @@ -214,7 +215,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { reloadAllLocalSettings() { const ret = { ...OnDialogSettingsDefault }; - ret.configPassphrase = localStorage.getItem("ls-setting-passphrase") || ""; + ret.configPassphrase = compatGlobal.localStorage.getItem("ls-setting-passphrase") || ""; ret.preset = ""; ret.deviceAndVaultName = this.services.setting.getDeviceAndVaultName(); return ret; @@ -349,7 +350,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { createEl( el: HTMLElement, tag: T, - o?: string | DomElementInfo | undefined, + o?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[T]) => void, func?: OnUpdateFunc ) { @@ -361,7 +362,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { addEl( el: HTMLElement, tag: T, - o?: string | DomElementInfo | undefined, + o?: string | DomElementInfo, callback?: (el: HTMLElementTagNameMap[T]) => void, func?: OnUpdateFunc ) { @@ -647,7 +648,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.editingSettings.passphrase = ""; } await this.saveAllDirtySettings(); - await this.applyAllSettings(); + await Promise.resolve(this.applyAllSettings()); if (result == OPTION_FETCH) { await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, ""); this.services.appLifecycle.scheduleRestart(); @@ -738,6 +739,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { ); } setLevelClass(el, level); + // TODO: Refactor to use Obsidian's recommended way to create heading. + // eslint-disable-next-line obsidianmd/settings-tab/no-manual-html-headings el.createEl("h3", { text: title, cls: "sls-setting-pane-title" }); if (this.menuEl) { this.menuEl.createEl( diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index d9ca27c..18c878f 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -1,29 +1,16 @@ -import { stringifyYaml } from "../../../deps.ts"; import { - type ObsidianLiveSyncSettings, type FilePathWithPrefix, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, type LoadedEntry, - REMOTE_COUCHDB, - REMOTE_MINIO, type MetaEntry, type FilePath, - DEFAULT_SETTINGS, -} from "../../../lib/src/common/types.ts"; -import { - createBlob, - getFileRegExp, - isDocContentSame, - parseHeaderValues, - readAsBlob, -} from "../../../lib/src/common/utils.ts"; -import { Logger } from "../../../lib/src/common/logger.ts"; -import { isCloudantURI } from "../../../lib/src/pouchdb/utils_couchdb.ts"; -import { requestToCouchDBWithCredentials } from "../../../common/utils.ts"; -import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts"; -import { $msg } from "../../../lib/src/common/i18n.ts"; +} from "@lib/common/types.ts"; +import { createBlob, getFileRegExp, isDocContentSame, readAsBlob } from "@lib/common/utils.ts"; +import { Logger } from "@lib/common/logger.ts"; +import { addPrefix, shouldBeIgnored, stripAllPrefixes } from "@lib/string_and_binary/path.ts"; +import { $msg } from "@lib/common/i18n.ts"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts"; import { @@ -32,14 +19,13 @@ import { EVENT_REQUEST_RUN_DOCTOR, EVENT_REQUEST_RUN_FIX_INCOMPLETE, eventHub, -} from "../../../common/events.ts"; -import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts"; -import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts"; -import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts"; -import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts"; +} from "@/common/events.ts"; +import { ICHeader, ICXHeader, PSCHeader } from "@/common/types.ts"; +import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts"; +import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; -import { generateReport } from "@/common/reportTool.ts"; +import { isNotFoundError } from "@lib/common/utils.doc.ts"; export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void { // const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` }); // hatchWarn.addClass("op-warn-info"); @@ -160,14 +146,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, } if (!(await addOn.storeInternalFileToDatabase(file, true))) { Logger( - `Failed to store the file to the database (Hidden file): ${file}`, + `Failed to store the file to the database (Hidden file): ${file.path}`, LOG_LEVEL_NOTICE ); return; } } } else { - if (!(await this.core.fileHandler.storeFileToDB(file as FilePath, true))) { + if (!(await this.core.fileHandler.storeFileToDB(file, true))) { Logger( `Failed to store the file to the database: ${file}`, LOG_LEVEL_NOTICE @@ -406,8 +392,8 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE); Logger(ret, LOG_LEVEL_VERBOSE); } - } catch (ex: any) { - if (ex?.status == 404) { + } catch (ex: unknown) { + if (isNotFoundError(ex)) { // We can perform this safely if ((await this.core.localDatabase.putRaw(newDoc)).ok) { Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE); diff --git a/src/modules/features/SettingDialogue/PanePatches.ts b/src/modules/features/SettingDialogue/PanePatches.ts index 0631bbd..839f7b2 100644 --- a/src/modules/features/SettingDialogue/PanePatches.ts +++ b/src/modules/features/SettingDialogue/PanePatches.ts @@ -150,7 +150,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen xxhash64: "xxhash64 (Fastest)", "mixed-purejs": "PureJS fallback (Fast, W/O WebAssembly)", sha1: "Older fallback (Slow, W/O WebAssembly)", - } as Record, + } satisfies Record, }); this.addOnSaved("hashAlg", async () => { await this.core.localDatabase._prepareHashFunctions(); @@ -188,7 +188,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen } this.requestUpdate(); }; - text.inputEl.before((dateEl = document.createElement("span"))); + text.inputEl.before((dateEl = activeDocument.createElement("span"))); text.inputEl.type = "datetime-local"; if (this.editingSettings.maxMTimeForReflectEvents > 0) { const date = new Date(this.editingSettings.maxMTimeForReflectEvents); diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index e609e39..8fb94e9 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -5,6 +5,7 @@ import { DEFAULT_SETTINGS, LOG_LEVEL_NOTICE, type ObsidianLiveSyncSettings, + LOG_LEVEL_VERBOSE, } from "../../../lib/src/common/types.ts"; import { Menu } from "@/deps.ts"; import { $msg } from "../../../lib/src/common/i18n.ts"; @@ -288,7 +289,8 @@ export function paneRemoteConfig( try { parsed = ConnectionStringParser.parse(trimmedURI); } catch (ex) { - this.services.API.addLog(`Failed to import remote configuration: ${ex}`, LOG_LEVEL_NOTICE); + this.services.API.addLog(`Failed to import remote configuration!`, LOG_LEVEL_NOTICE); + this.services.API.addLog(ex, LOG_LEVEL_VERBOSE); return; } @@ -343,9 +345,10 @@ export function paneRemoteConfig( parsed = ConnectionStringParser.parse(config.uri); } catch (ex) { this.services.API.addLog( - `Failed to parse remote configuration '${config.id}' for editing: ${ex}`, + `Failed to parse remote configuration '${config.id}' for editing!`, LOG_LEVEL_NOTICE ); + this.services.API.addLog(ex, LOG_LEVEL_VERBOSE); return; } const workSettings = createBaseRemoteSettings(); @@ -452,9 +455,10 @@ export function paneRemoteConfig( parsed = ConnectionStringParser.parse(config.uri); } catch (ex) { this.services.API.addLog( - `Failed to parse remote configuration '${config.id}': ${ex}`, + `Failed to parse remote configuration '${config.id}' for fetching settings!`, LOG_LEVEL_NOTICE ); + this.services.API.addLog(ex, LOG_LEVEL_VERBOSE); return; } const workSettings = createBaseRemoteSettings(); diff --git a/src/modules/features/SettingDialogue/settingUtils.ts b/src/modules/features/SettingDialogue/settingUtils.ts index b965298..847ebc0 100644 --- a/src/modules/features/SettingDialogue/settingUtils.ts +++ b/src/modules/features/SettingDialogue/settingUtils.ts @@ -75,7 +75,7 @@ export function getSummaryFromPartialSettings(setting: Partial { - const newSetting = await this.dialogManager.openWithExplicitCancel(UseSetupURI, setupURI); + const newSetting = await this.dialogManager.openWithExplicitCancel( + UseSetupURI, + setupURI + ); if (newSetting === "cancelled") { this._log("Setup URI dialog cancelled.", LOG_LEVEL_NOTICE); return false; @@ -140,7 +160,10 @@ export class SetupManager extends AbstractModule { ): Promise { const originalSetting = JSON.parse(JSON.stringify(currentSetting)) as ObsidianLiveSyncSettings; const baseSetting = JSON.parse(JSON.stringify(originalSetting)) as ObsidianLiveSyncSettings; - const couchConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteCouchDB, originalSetting); + const couchConf = await this.dialogManager.openWithExplicitCancel< + SetupRemoteCouchDBResultType, + CouchDBConnection + >(SetupRemoteCouchDB, originalSetting); if (couchConf === "cancelled") { this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE); return await this.onOnboard(userMode); @@ -164,7 +187,10 @@ export class SetupManager extends AbstractModule { currentSetting: ObsidianLiveSyncSettings, activate = true ): Promise { - const bucketConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteBucket, currentSetting); + const bucketConf = await this.dialogManager.openWithExplicitCancel< + SetupRemoteBucketResultType, + BucketSyncSetting + >(SetupRemoteBucket, currentSetting); if (bucketConf === "cancelled") { this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE); return await this.onOnboard(userMode); @@ -188,14 +214,33 @@ export class SetupManager extends AbstractModule { currentSetting: ObsidianLiveSyncSettings, activate = true ): Promise { - const p2pConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteP2P, currentSetting); + const p2pConf = await this.dialogManager.openWithExplicitCancel( + SetupRemoteP2P, + currentSetting + ); if (p2pConf === "cancelled") { this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE); return await this.onOnboard(userMode); } const newSetting = { ...currentSetting, ...p2pConf } as ObsidianLiveSyncSettings; + // Apply remoteConfigurations + if (newSetting.P2P_ActiveRemoteConfigurationId) { + const id = newSetting.P2P_ActiveRemoteConfigurationId; + const merged = { + ...newSetting, + ...p2pConf, + } as ObsidianLiveSyncSettings; + const uri = ConnectionStringParser.serialize({ type: "p2p", settings: merged }); + newSetting.remoteConfigurations[id] = { + ...newSetting.remoteConfigurations[id], + uri, + isEncrypted: false, + }; + newSetting.P2P_ActiveRemoteConfigurationId = id; + } if (activate) { newSetting.remoteType = REMOTE_P2P; + newSetting.activeConfigurationId = newSetting.P2P_ActiveRemoteConfigurationId; } return await this.onConfirmApplySettingsFromWizard(newSetting, userMode, activate); } @@ -207,10 +252,13 @@ export class SetupManager extends AbstractModule { * @returns */ async onlyE2EEConfiguration(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings): Promise { - const e2eeConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteE2EE, currentSetting); + const e2eeConf = await this.dialogManager.openWithExplicitCancel( + SetupRemoteE2EE, + currentSetting + ); if (e2eeConf === "cancelled") { this._log("E2EE configuration cancelled.", LOG_LEVEL_NOTICE); - return await false; + return false; } const newSetting = { ...currentSetting, @@ -226,7 +274,10 @@ export class SetupManager extends AbstractModule { * @returns */ async onConfigureManually(originalSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise { - const e2eeConf = await this.dialogManager.openWithExplicitCancel(SetupRemoteE2EE, originalSetting); + const e2eeConf = await this.dialogManager.openWithExplicitCancel( + SetupRemoteE2EE, + originalSetting + ); if (e2eeConf === "cancelled") { this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE); return await this.onOnboard(userMode); @@ -245,7 +296,7 @@ export class SetupManager extends AbstractModule { * @returns */ async onSelectServer(currentSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise { - const method = await this.dialogManager.openWithExplicitCancel(SetupRemote); + const method = await this.dialogManager.openWithExplicitCancel(SetupRemote); if (method === "couchdb") { return await this.onCouchDBManualSetup(userMode, currentSetting, true); } else if (method === "bucket") { @@ -285,9 +336,9 @@ export class SetupManager extends AbstractModule { this._log("No changes in settings detected. Skipping applying settings from wizard.", LOG_LEVEL_NOTICE); return true; } - const patch = generatePatchObj(this.settings, newConf); - console.log(`Changes:`); - console.dir(patch); + // const patch = generatePatchObj(this.settings, newConf); + // console.log(`Changes:`); + // console.dir(patch); if (!activate) { extra(); await this.applySetting(newConf, UserMode.ExistingUser); @@ -304,7 +355,8 @@ export class SetupManager extends AbstractModule { this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE); return true; } else { - const userModeResult = await this.dialogManager.openWithExplicitCancel(OutroAskUserMode); + const userModeResult = + await this.dialogManager.openWithExplicitCancel(OutroAskUserMode); if (userModeResult === "new-user") { userMode = UserMode.NewUser; } else if (userModeResult === "existing-user") { @@ -321,7 +373,9 @@ export class SetupManager extends AbstractModule { } } const component = userMode === UserMode.NewUser ? OutroNewUser : OutroExistingUser; - const confirm = await this.dialogManager.openWithExplicitCancel(component); + const confirm = await this.dialogManager.openWithExplicitCancel< + OutroNewUserResultType | OutroExistingUserResultType + >(component); if (confirm === "cancelled") { this._log("User cancelled applying settings from wizard..", LOG_LEVEL_NOTICE); return false; @@ -347,10 +401,10 @@ export class SetupManager extends AbstractModule { */ async onPromptQRCodeInstruction(): Promise { - const qrResult = await this.dialogManager.open(ScanQRCode); + const qrResult = await this.dialogManager.open(ScanQRCode); this._log("QR Code dialog closed.", LOG_LEVEL_VERBOSE); // Result is not used, but log it for debugging. - this._log(`QR Code result: ${qrResult}`, LOG_LEVEL_VERBOSE); + this._log(qrResult, LOG_LEVEL_VERBOSE); // QR Code instruction dialog never yields settings directly. return false; } diff --git a/src/modules/features/SetupWizard/dialogs/FetchEverything.svelte b/src/modules/features/SetupWizard/dialogs/FetchEverything.svelte index e704782..9bb06d7 100644 --- a/src/modules/features/SetupWizard/dialogs/FetchEverything.svelte +++ b/src/modules/features/SetupWizard/dialogs/FetchEverything.svelte @@ -1,47 +1,30 @@ diff --git a/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte b/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte index ff39c62..3818a29 100644 --- a/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte +++ b/src/modules/features/SetupWizard/dialogs/OutroNewUser.svelte @@ -5,14 +5,13 @@ import Question from "@/lib/src/UI/components/Question.svelte"; import Instruction from "@/lib/src/UI/components/Instruction.svelte"; import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte"; - const TYPE_APPLY = "apply"; - const TYPE_CANCELLED = "cancelled"; - type ResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED; + import { TYPE_APPLY, TYPE_CANCELLED, type OutroNewUserResultType } from "./setupDialogTypes"; + type Props = { - setResult: (result: ResultType) => void; + setResult: (result: OutroNewUserResultType) => void; }; const { setResult }: Props = $props(); - // let userType = $state(TYPE_CANCELLED); + // let userType = $state(TYPE_CANCELLED); diff --git a/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte b/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte index d2cef32..4ac0f85 100644 --- a/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte +++ b/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte @@ -2,9 +2,9 @@ /** * Panel to check and fix CouchDB configuration issues */ - import type { ObsidianLiveSyncSettings } from "../../../../lib/src/common/types"; - import Decision from "../../../../lib/src/UI/components/Decision.svelte"; - import UserDecisions from "../../../../lib/src/UI/components/UserDecisions.svelte"; + import type { ObsidianLiveSyncSettings } from "@lib/common/types"; + import Decision from "@lib/UI/components/Decision.svelte"; + import UserDecisions from "@lib/UI/components/UserDecisions.svelte"; import { checkConfig, type ConfigCheckResult, type ResultError, type ResultErrorMessage } from "./utilCheckCouchDB"; type Props = { trialRemoteSetting: ObsidianLiveSyncSettings; diff --git a/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte b/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte index 93aa834..1fe06e3 100644 --- a/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte +++ b/src/modules/features/SetupWizard/dialogs/RebuildEverything.svelte @@ -10,29 +10,17 @@ import InfoNote from "@/lib/src/UI/components/InfoNote.svelte"; import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte"; import Check from "@/lib/src/UI/components/Check.svelte"; - const TYPE_CANCEL = "cancelled"; + import { + TYPE_CANCEL, + TYPE_BACKUP_DONE, + TYPE_BACKUP_SKIPPED, + TYPE_UNABLE_TO_BACKUP, + type RebuildEverythingResult, + type ResultTypeBackup, + } from "./setupDialogTypes"; - const TYPE_BACKUP_DONE = "backup_done"; - const TYPE_BACKUP_SKIPPED = "backup_skipped"; - const TYPE_UNABLE_TO_BACKUP = "unable_to_backup"; - - type ResultTypeBackup = - | typeof TYPE_BACKUP_DONE - | typeof TYPE_BACKUP_SKIPPED - | typeof TYPE_UNABLE_TO_BACKUP - | typeof TYPE_CANCEL; - - type ResultTypeExtra = { - preventFetchingConfig: boolean; - }; - type ResultType = - | { - backup: ResultTypeBackup; - extra: ResultTypeExtra; - } - | typeof TYPE_CANCEL; type Props = { - setResult: (result: ResultType) => void; + setResult: (result: RebuildEverythingResult) => void; }; const { setResult }: Props = $props(); diff --git a/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte b/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte index 57c0621..4a24f46 100644 --- a/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte +++ b/src/modules/features/SetupWizard/dialogs/ScanQRCode.svelte @@ -4,10 +4,10 @@ import Decision from "@/lib/src/UI/components/Decision.svelte"; import Instruction from "@/lib/src/UI/components/Instruction.svelte"; import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte"; - const TYPE_CLOSE = "close"; - type ResultType = typeof TYPE_CLOSE; + import { TYPE_CLOSE, type ScanQRCodeResultType } from "./setupDialogTypes"; + type Props = { - setResult: (_result: ResultType) => void; + setResult: (_result: ScanQRCodeResultType) => void; }; const { setResult }: Props = $props(); diff --git a/src/modules/features/SetupWizard/dialogs/SelectMethodExisting.svelte b/src/modules/features/SetupWizard/dialogs/SelectMethodExisting.svelte index b339d3e..982d60c 100644 --- a/src/modules/features/SetupWizard/dialogs/SelectMethodExisting.svelte +++ b/src/modules/features/SetupWizard/dialogs/SelectMethodExisting.svelte @@ -7,19 +7,19 @@ import Options from "@/lib/src/UI/components/Options.svelte"; import Instruction from "@/lib/src/UI/components/Instruction.svelte"; import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte"; - import InfoNote from "@/lib/src/UI/components/InfoNote.svelte"; - import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte"; - import Check from "@/lib/src/UI/components/Check.svelte"; - const TYPE_USE_SETUP_URI = "use-setup-uri"; - const TYPE_SCAN_QR_CODE = "scan-qr-code"; - const TYPE_CONFIGURE_MANUALLY = "configure-manually"; - const TYPE_CANCELLED = "cancelled"; - type ResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_SCAN_QR_CODE | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED; + import { + TYPE_USE_SETUP_URI, + TYPE_SCAN_QR_CODE, + TYPE_CONFIGURE_MANUALLY, + TYPE_CANCELLED, + type SelectMethodExistingResultType, + } from "./setupDialogTypes"; + type Props = { - setResult: (result: ResultType) => void; + setResult: (result: SelectMethodExistingResultType) => void; }; const { setResult }: Props = $props(); - let userType = $state(TYPE_CANCELLED); + let userType = $state(TYPE_CANCELLED); let proceedTitle = $derived.by(() => { if (userType === TYPE_USE_SETUP_URI) { return "Proceed with Setup URI"; diff --git a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte index b4dd9ea..36e90fc 100644 --- a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte +++ b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte @@ -7,18 +7,18 @@ import Options from "@/lib/src/UI/components/Options.svelte"; import Instruction from "@/lib/src/UI/components/Instruction.svelte"; import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte"; - import InfoNote from "@/lib/src/UI/components/InfoNote.svelte"; - import ExtraItems from "@/lib/src/UI/components/ExtraItems.svelte"; - import Check from "@/lib/src/UI/components/Check.svelte"; - const TYPE_USE_SETUP_URI = "use-setup-uri"; - const TYPE_CONFIGURE_MANUALLY = "configure-manually"; - const TYPE_CANCELLED = "cancelled"; - type ResultType = typeof TYPE_USE_SETUP_URI | typeof TYPE_CONFIGURE_MANUALLY | typeof TYPE_CANCELLED; + import { + TYPE_USE_SETUP_URI, + TYPE_CONFIGURE_MANUALLY, + TYPE_CANCELLED, + type SelectMethodNewUserResultType, + } from "./setupDialogTypes"; + type Props = { - setResult: (result: ResultType) => void; + setResult: (result: SelectMethodNewUserResultType) => void; }; const { setResult }: Props = $props(); - let userType = $state(TYPE_CANCELLED); + let userType = $state(TYPE_CANCELLED); let proceedTitle = $derived.by(() => { if (userType === TYPE_USE_SETUP_URI) { return "Proceed with Setup URI"; diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte index adcb87a..365f117 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte @@ -6,16 +6,19 @@ import Options from "@/lib/src/UI/components/Options.svelte"; import Instruction from "@/lib/src/UI/components/Instruction.svelte"; import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte"; - const TYPE_COUCHDB = "couchdb"; - const TYPE_BUCKET = "bucket"; - const TYPE_P2P = "p2p"; - const TYPE_CANCELLED = "cancelled"; - type ResultType = typeof TYPE_COUCHDB | typeof TYPE_BUCKET | typeof TYPE_P2P | typeof TYPE_CANCELLED; + import { + TYPE_COUCHDB, + TYPE_BUCKET, + TYPE_P2P, + TYPE_CANCELLED, + type SetupRemoteResultType, + } from "./setupDialogTypes"; + type Props = { - setResult: (result: ResultType) => void; + setResult: (result: SetupRemoteResultType) => void; }; const { setResult }: Props = $props(); - let userType = $state(TYPE_CANCELLED); + let userType = $state(TYPE_CANCELLED); let proceedTitle = $derived.by(() => { if (userType === TYPE_COUCHDB) { return "Continue to CouchDB setup"; diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte index f2270dc..7c04c38 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte @@ -13,19 +13,18 @@ DEFAULT_SETTINGS, PREFERRED_JOURNAL_SYNC, RemoteTypes, - } from "../../../../lib/src/common/types"; + } from "@lib/common/types"; import { onMount } from "svelte"; - import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; - import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils"; + import { getDialogContext, type GuestDialogProps } from "@lib/UI/svelteDialog"; + import { copyTo, pickBucketSyncSettings } from "@lib/common/utils"; + import { TYPE_CANCELLED, type SetupRemoteBucketResultType } from "./setupDialogTypes"; const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS); let syncSetting = $state({ ...default_setting }); - type ResultType = typeof TYPE_CANCELLED | BucketSyncSetting; - type Props = GuestDialogProps; - const TYPE_CANCELLED = "cancelled"; + type Props = GuestDialogProps; const { setResult, getInitialData }: Props = $props(); diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte index 671af71..39d54c4 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte @@ -14,20 +14,19 @@ RemoteTypes, type CouchDBConnection, type ObsidianLiveSyncSettings, - } from "../../../../lib/src/common/types"; - import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb"; + } from "@lib/common/types"; + import { isCloudantURI } from "@lib/pouchdb/utils_couchdb"; import { onMount } from "svelte"; - import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; - import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils"; + import { getDialogContext, type GuestDialogProps } from "@lib/UI/svelteDialog"; + import { copyTo, pickCouchDBSyncSettings } from "@lib/common/utils"; import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte"; + import { TYPE_CANCELLED, type SetupRemoteCouchDBResultType } from "./setupDialogTypes"; const default_setting = pickCouchDBSyncSettings(DEFAULT_SETTINGS); let syncSetting = $state({ ...default_setting }); - type ResultType = typeof TYPE_CANCELLED | CouchDBConnection; - const TYPE_CANCELLED = "cancelled"; - type Props = GuestDialogProps; + type Props = GuestDialogProps; const { setResult, getInitialData }: Props = $props(); onMount(() => { if (getInitialData) { @@ -181,7 +180,7 @@ autocapitalize="off" spellcheck="false" required - pattern="^[a-z0-9][a-z0-9_]*$" + pattern="^[a-z][a-z0-9_$()+/-]*$" bind:value={syncSetting.couchDB_DBNAME} /> diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte index 052e97d..f3c40ed 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte @@ -12,13 +12,13 @@ E2EEAlgorithmNames, E2EEAlgorithms, type EncryptionSettings, - } from "../../../../lib/src/common/types"; + } from "@lib/common/types"; import { onMount } from "svelte"; - import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog"; - import { copyTo, pickEncryptionSettings } from "../../../../lib/src/common/utils"; - const TYPE_CANCELLED = "cancelled"; - type ResultType = typeof TYPE_CANCELLED | EncryptionSettings; - type Props = GuestDialogProps; + import type { GuestDialogProps } from "@lib/UI/svelteDialog"; + import { copyTo, pickEncryptionSettings } from "@lib/common/utils"; + import { TYPE_CANCELLED, type SetupRemoteE2EEResultType } from "./setupDialogTypes"; + + type Props = GuestDialogProps; const { setResult, getInitialData }: Props = $props(); let default_encryption: EncryptionSettings = { encrypt: true, diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte index cd2020e..4e1067a 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte @@ -26,16 +26,14 @@ import { getDialogContext, type GuestDialogProps } from "@lib/UI/svelteDialog"; import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types"; import ExtraItems from "@lib/UI/components/ExtraItems.svelte"; + import { TYPE_CANCELLED, type SetupRemoteP2PResultType } from "./setupDialogTypes"; const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS); let syncSetting = $state({ ...default_setting }); const context = getDialogContext(); let error = $state(""); - const TYPE_CANCELLED = "cancelled"; - type SettingInfo = P2PConnectionInfo; - type ResultType = typeof TYPE_CANCELLED | SettingInfo; - type Props = GuestDialogProps; + type Props = GuestDialogProps; const { setResult, getInitialData }: Props = $props(); onMount(() => { diff --git a/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte b/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte index d6152b7..d1ed1ac 100644 --- a/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte +++ b/src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte @@ -1,6 +1,6 @@