Compare commits

..

43 Commits

Author SHA1 Message Date
vorotamoroz
46546e121f Refactor: Move webpeer from lib to main repository. 2026-01-26 09:17:01 +00:00
vorotamoroz
28146eec2c Refactor: Migrate the outdated, unstable platform abstraction layer to Services 2026-01-26 09:13:40 +00:00
vorotamoroz
3cd9b9e06d bump 2026-01-24 16:48:51 +09:00
vorotamoroz
7c43c61b85 ### Fixed
- No longer `No available splitter for settings!!` errors occur after fetching old remote settings while rebuilding local database.

### Improved

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

### New feature

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

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

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

Fixed:
- Remove obsoleted accessing to Obsidian's global.
- Avoiding errors in exceptional circumstances.
- Removal of several outdated and inefficient QueueProcessor implementations
  - Log output is now a bit efficient.
2026-01-07 04:45:19 +00:00
vorotamoroz
1ff1ac951b - Remove old comments
- Fix Importing paths
2026-01-07 04:37:30 +00:00
vorotamoroz
aa6d771d17 bump 2025-12-25 10:30:04 +00:00
vorotamoroz
512c238415 ### Improved
- Now the garbage collector (V3) has been implemented. (Beta)
- Now the plug-in and device information is stored in the remote database.
2025-12-25 10:29:19 +00:00
vorotamoroz
55ffeeda10 bump 2025-12-24 03:38:40 +00:00
vorotamoroz
65f18b4160 ### Fixed
- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764).
2025-12-24 03:37:41 +00:00
143 changed files with 12269 additions and 1485 deletions

View File

@@ -26,6 +26,7 @@
"**/tests.ts",
"**/**test.ts",
"**/**.test.ts",
"src/apps/**",
"esbuild.*.mjs",
"terser.*.mjs"
],

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

@@ -0,0 +1,56 @@
# Run tests by Harnessed CI
name: harness-ci
on:
workflow_dispatch:
inputs:
testsuite:
description: 'Run specific test suite (leave empty to run all)'
type: choice
options:
- ''
- 'suite/'
- 'suitep2p/'
default: ''
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install test dependencies (Playwright Chromium)
run: npm run test:install-dependencies
- name: Start test services (CouchDB + MinIO + Nostr Relay + WebPeer)
run: npm run test:docker-all:start
- name: Run tests suite
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suite/' }}
env:
CI: true
run: npm run test suite/
- name: Run P2P tests suite
if: ${{ inputs.testsuite == '' || inputs.testsuite == 'suitep2p/' }}
env:
CI: true
run: npm run test suitep2p/
- name: Stop test services
if: always()
run: npm run test:docker-all:stop

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

@@ -0,0 +1,33 @@
# Run Unit test without Harnesses
name: unit-ci
on:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install test dependencies (Playwright Chromium)
run: npm run test:install-dependencies
- name: Run unit tests suite
run: npm run test:unit

6
.gitignore vendored
View File

@@ -23,4 +23,8 @@ data.json
.env
# local config files
*.local
*.local
cov_profile/**
coverage

11
.test.env Normal file
View File

@@ -0,0 +1,11 @@
hostname=http://localhost:5989/
dbname=livesync-test-db2
minioEndpoint=http://127.0.0.1:9000
username=admin
password=testpassword
accessKey=minioadmin
secretKey=minioadmin
bucketName=livesync-test-bucket
# ENABLE_DEBUGGER=true
# PRINT_LIVESYNC_LOGS=true
# ENABLE_UI=true

View File

@@ -97,6 +97,9 @@ 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.
## License
Licensed under the MIT License.

140
devs.md Normal file
View File

@@ -0,0 +1,140 @@
# Self-hosted LiveSync Development Guide
## Project Overview
Self-hosted LiveSync is an Obsidian plugin for synchronising vaults across devices using CouchDB, MinIO/S3, or peer-to-peer WebRTC. The codebase uses a modular architecture with TypeScript, Svelte, and PouchDB.
## Architecture
### Module System
The plugin uses a dynamic module system to reduce coupling and improve maintainability:
- **Service Hub**: Central registry for services using dependency injection
- Services are registered, and accessed via `this.services` (in most modules)
- **Module Loading**: All modules extend `AbstractModule` or `AbstractObsidianModule` (which extends `AbstractModule`). These modules are loaded in main.ts and some modules
- **Module Categories** (by directory):
- `core/` - Platform-independent core functionality
- `coreObsidian/` - Obsidian-specific core (e.g., `ModuleFileAccessObsidian`)
- `essential/` - Required modules (e.g., `ModuleMigration`, `ModuleKeyValueDB`)
- `features/` - Optional features (e.g., `ModuleLog`, `ModuleObsidianSettings`)
- `extras/` - Development/testing tools (e.g., `ModuleDev`, `ModuleIntegratedTest`)
### Key Architectural Components
- **LiveSyncLocalDB** (`src/lib/src/pouchdb/`): Local PouchDB database wrapper
- **Replicators** (`src/lib/src/replication/`): CouchDB, Journal, and MinIO sync engines
- **Service Hub** (`src/modules/services/`): Central service registry using dependency injection
- **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools
### File Structure Conventions
- **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild)
- **Development code**: Use `.dev.ts` suffix (replaced with `.prod.ts` in production)
- **Path aliases**: `@/*` maps to `src/*`, `@lib/*` maps to `src/lib/src/*`
## Build & Development Workflow
### Commands
```bash
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
- 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`)
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright
- **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)
- **Translation Workflow**:
1. Edit YAML files in `src/lib/src/common/messagesYAML/` (human-editable)
2. Run `npm run bakei18n` to compile: YAML → JSON → TypeScript constants
3. Use `$t()`, `$msg()` functions for translations
You can also use `$f` for formatted messages with Tagged Template Literals.
- **Usage**:
```typescript
$msg("dialog.someKey"); // Typed key with autocomplete
$t("Some message"); // Direct translation
$f`Hello, ${userName}`; // Formatted message
```
- **Supported languages**: `def` (English), `de`, `es`, `ja`, `ko`, `ru`, `zh`, `zh-tw`
### File Path Handling
- Use tagged types from `types.ts`: `FilePath`, `FilePathWithPrefix`, `DocumentID`
- Prefix constants: `CHeader` (chunks), `ICHeader`/`ICHeaderEnd` (internal data)
- Path utilities in `src/lib/src/string_and_binary/path.ts`: `addPrefix()`, `stripAllPrefixes()`, `shouldBeIgnored()`
### Logging & Debugging
- Use `this._log(msg, LOG_LEVEL_INFO)` in modules (automatically prefixes with module name)
- Log levels: `LOG_LEVEL_DEBUG`, `LOG_LEVEL_VERBOSE`, `LOG_LEVEL_INFO`, `LOG_LEVEL_NOTICE`, `LOG_LEVEL_URGENT`
- LOG_LEVEL_NOTICE and above are reported to the user via Obsidian notices
- LOG_LEVEL_DEBUG is for debug only and not shown in default builds
- Dev mode creates `ls-debug/` folder in `.obsidian/` for debug outputs (e.g., missing translations)
- This causes pretty significant performance overhead.
## Common Patterns
### Module Implementation
```typescript
export class ModuleExample extends AbstractObsidianModule {
async _everyOnloadStart(): Promise<boolean> {
/* ... */
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
}
}
```
### Settings Management
- Settings defined in `src/lib/src/common/types.ts` (`ObsidianLiveSyncSettings`)
- Configuration metadata in `src/lib/src/common/settingConstants.ts`
- Use `this.services.setting.saveSettingData()` instead of using plugin methods directly
### Database Operations
- Local database operations through `LiveSyncLocalDB` (wraps PouchDB)
- Document types: `EntryDoc` (files), `EntryLeaf` (chunks), `PluginDataEntry` (plugin sync)
## Important Files
- [main.ts](src/main.ts) - Plugin entry point, module registration
- [esbuild.config.mjs](esbuild.config.mjs) - Build configuration with platform/dev file replacement
- [package.json](package.json) - Scripts reference and dependencies
## Contribution Guidelines
- Follow existing code style and conventions
- Please bump dependencies with care, check artifacts after updates, with diff-tools and only expected changes in the build output (to avoid unexpected vulnerabilities).
- When adding new features, please consider it has an OSS implementation, and avoid using proprietary services or APIs that may limit usage.
- For example, any functionality to connect to a new type of server is expected to either have an OSS implementation available for that server, or to be managed under some responsibilities and/or limitations without disrupting existing functionality, and scope for surveillance reduced by some means (e.g., by client-side encryption, auditing the server ourselves).

View File

@@ -40,7 +40,7 @@ export default [
"src/lib/test",
"src/lib/src/cli",
"**/main.js",
"src/lib/apps/webpeer/*",
"src/apps/**/*",
".prettierrc.*.mjs",
".prettierrc.mjs",
"*.config.mjs"

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.34",
"version": "0.25.41",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

5678
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.25.34",
"version": "0.25.41",
"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",
@@ -24,7 +24,33 @@
"prettyCheck": "npm run prettyNoWrite -- --check",
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
"check": "npm run lint && npm run svelte-check",
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/"
"unittest": "deno test -A --no-check --coverage=cov_profile --v8-flags=--expose-gc --trace-leaks ./src/",
"test": "vitest run",
"test:unit": "vitest run --config vitest.config.unit.ts",
"test:unit:coverage": "vitest run --config vitest.config.unit.ts --coverage",
"test:install-playwright": "npx playwright install chromium",
"test:install-dependencies": "npm run test:install-playwright",
"test:coverage": "vitest run --coverage",
"test:docker-couchdb:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-start.sh",
"test:docker-couchdb:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-init.sh",
"test:docker-couchdb:start": "npm run test:docker-couchdb:up && sleep 5 && npm run test:docker-couchdb:init",
"test:docker-couchdb:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/couchdb-stop.sh",
"test:docker-couchdb:stop": "npm run test:docker-couchdb:down",
"test:docker-s3:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-start.sh",
"test:docker-s3:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-init.sh",
"test:docker-s3:start": "npm run test:docker-s3:up && sleep 3 && npm run test:docker-s3:init",
"test:docker-s3:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/minio-stop.sh",
"test:docker-s3:stop": "npm run test:docker-s3:down",
"test:docker-p2p:up": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-start.sh",
"test:docker-p2p:init": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-init.sh",
"test:docker-p2p:down": "npx dotenv-cli -e .env -e .test.env -- ./test/shell/p2p-stop.sh",
"test:docker-p2p:stop": "npm run test:docker-p2p:down",
"test:docker-all:up": "npm run test:docker-couchdb:up && npm run test:docker-s3:up && npm run test:docker-p2p:up",
"test:docker-all:init": "npm run test:docker-couchdb:init && npm run test:docker-s3:init && npm run test:docker-p2p:init",
"test:docker-all:down": "npm run test:docker-couchdb:down && npm run test:docker-s3:down && npm run test:docker-p2p:down",
"test:docker-all:start": "npm run test:docker-all:up && sleep 5 && npm run test:docker-all:init",
"test:docker-all:stop": "npm run test:docker-all:down",
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop"
},
"keywords": [],
"author": "vorotamoroz",
@@ -49,7 +75,12 @@
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"@vitest/browser": "^4.0.16",
"@vitest/browser-playwright": "^4.0.16",
"@vitest/coverage-v8": "^4.0.16",
"builtin-modules": "5.0.0",
"dotenv": "^17.2.3",
"dotenv-cli": "^11.0.0",
"esbuild": "0.25.0",
"esbuild-plugin-inline-worker": "^0.1.1",
"esbuild-svelte": "^0.9.3",
@@ -59,6 +90,7 @@
"events": "^3.3.0",
"glob": "^11.0.3",
"obsidian": "^1.8.7",
"playwright": "^1.57.0",
"postcss": "^8.5.3",
"postcss-load-config": "^6.0.1",
"pouchdb-adapter-http": "^9.0.0",
@@ -81,6 +113,9 @@
"tslib": "^2.8.1",
"tsx": "^4.20.6",
"typescript": "5.9.3",
"vite": "^7.3.0",
"vitest": "^4.0.16",
"webdriverio": "^9.23.0",
"yaml": "^2.8.0"
},
"dependencies": {
@@ -94,7 +129,7 @@
"fflate": "^0.8.2",
"idb": "^8.0.3",
"minimatch": "^10.0.2",
"octagonal-wheels": "^0.1.44",
"octagonal-wheels": "^0.1.45",
"qrcode-generator": "^1.4.4",
"trystero": "^0.22.0",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"

24
src/apps/webpeer/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,30 @@
# A pseudo client for Self-hosted LiveSync Peer-to-Peer Sync mode
## What is it for?
This is a pseudo client for the Self-hosted LiveSync Peer-to-Peer Sync mode. It is a simple pure-client-side web-application that can be connected to the Self-hosted LiveSync in peer-to-peer.
As long as you have a browser, it starts up, so if you leave it opened some device, it can replace your existing remote servers such as CouchDB.
> [!IMPORTANT]
> Of course, it has not been fully tested. Rather, it was created to be tested.
This pseudo client actually receives the data from other devices, and sends if some device requests it. However, it does not store **files** in the local storage. If you want to purge the data, please purge the browser's cache and indexedDB, local storage, etc.
## How to use it?
We can build the application by running the following command:
```bash
$ deno task build
```
Then, open the `dist/index.html` in the browser. It can be configured as the same as the Self-hosted LiveSync (Same components are used[^1]).
## Some notes
I will launch this application in the github pages later, so will be able to use it without building it. However, that shares the origin. Hence, the application that your have built and deployed would be more secure.
[^1]: Congrats! I made it modular. Finally...

1101
src/apps/webpeer/deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="icon.svg" />
<link rel="manifest" href="manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Peer-to-Peer Daemon on Browser</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
{
"name": "webpeer",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
},
"dependencies": {},
"devDependencies": {
"eslint-plugin-svelte": "^3.12.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tsconfig/svelte": "^5.0.5",
"svelte": "5.41.1",
"svelte-check": "^4.3.3",
"typescript": "5.9.3",
"vite": "^7.3.0"
},
"imports": {
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts"
}
}

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 511.99998 511.99998"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-22.694448,-28.922305)">
<g
id="g4"
transform="matrix(4.6921194,0,0,4.6921194,-266.26061,-494.11652)">
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.2366;stroke-opacity:1"
id="rect2"
width="109.11913"
height="109.11913"
x="61.583057"
y="111.47176" />
<g
id="g3"
transform="matrix(0.77702959,0,0,0.77702959,22.523192,34.973874)">
<path
d="m 104.50787,75.245039 h -3.77394 l -10.90251,-29.352906 c 25.15963,-14.257127 33.96551,-46.12600067 20.12771,-71.285639 -14.25713,-25.159637 -46.126,-33.96551 -71.28564,-20.12771 -12.16049,6.709237 -21.38569,18.450401 -24.74031,31.868875 l -38.99744,-4.193274 c -2.93529,-18.4504 -20.12771,-31.449546 -38.578109,-28.514255 -16.773091,2.515964 -28.933582,16.773091 -28.933582,33.546184 0,5.8705823 1.677309,11.7411643 4.6126,17.1924183 l -46.964659,46.1260007 c -8.80587,-6.709236 -19.70838,-10.483182 -31.03022,-10.483182 -28.93358,0 -52.41591,23.482328 -52.41591,52.415908 0,28.933581 23.48233,52.415911 52.41591,52.415911 l 10.48319,62.89909 c -17.19242,7.54789 -25.15964,27.6756 -17.61175,44.86802 7.54789,17.19242 27.6756,25.15964 44.86802,17.61175 15.93444,-6.70924 23.90165,-24.32098 19.28905,-41.09408 l 36.900806,-19.28905 c 23.901654,26.41762 64.9957237,28.51425 91.832674,4.6126 13.41847,-12.16049 21.38569,-29.77224 21.38569,-47.80331 0,-4.6126 -0.41933,-9.64453 -1.67731,-14.25713 l 40.67475,-20.96636 c 12.57982,14.25713 33.96551,15.51511 48.22264,2.93529 14.25712,-12.57982 15.51511,-33.96551 2.93529,-48.222641 -7.96722,-6.70924 -17.19242,-10.90251 -26.83695,-10.90251 z m -223.92077,140.055311 c -5.45125,-5.45125 -12.99914,-8.80587 -20.54703,-9.64452 l -10.48319,-62.8991 c 10.06386,-3.35461 18.86973,-9.64452 25.15964,-18.03107 l 38.997438,20.54704 c -6.289909,16.77309 -5.031927,35.64282 3.354619,51.57725 z m 41.094077,-85.54276 -38.578107,-20.12771 c 1.67731,-5.45126 2.51596,-10.902511 2.51596,-16.773091 0,-10.90251 -3.35462,-21.38569 -9.64453,-30.191565 l 46.964658,-45.706673 c 15.934437,9.644527 36.900802,5.031927 46.545332,-10.9025087 1.25798,-2.096637 2.09663,-4.193273 2.93529,-6.28990997 l 38.99744,4.19327297 c 0.83865,15.0957817 8.38654,29.3529097 20.54703,38.5781097 l -34.3848403,64.157075 c -27.2562697,-10.902511 -58.7058147,-1.25798 -75.8982327,23.063 z m 148.861183,-20.12771 c 0,2.51596 0.41933,5.03193 1.25798,7.54789 l -38.57811,20.12771 c -4.6126,-9.2252 -11.74116,-16.77309 -20.12771,-22.64367 l 34.38484,-64.157077 c 5.45126,2.096637 11.32184,2.935291 17.19242,2.935291 3.35462,0 6.70924,-0.419327 10.06385,-0.838654 l 10.90251,29.352909 c -10.06385,5.87058 -15.51511,16.35376 -15.09578,27.675601 z"
id="path1-1"
style="overflow:hidden;stroke-width:4.19327"
transform="matrix(0.26458333,0,0,0.26458333,130.85167,139.42444)" />
<path
id="path1-8-1-7"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.073263;stroke-opacity:1"
d="m 140.38615,132.15285 c -0.55386,0 -1.00708,0.45307 -1.00708,1.00708 v 12.05537 c 0,0.5546 0.45322,1.00707 1.00708,1.00707 h 0.504 v 1.51154 h 2.01461 v -1.51154 h 10.07399 v 1.51154 h 2.01461 v -1.51154 h 0.50354 c 0.55461,0 1.00754,-0.45247 1.00754,-1.00707 v -12.05537 c 0,-0.55401 -0.45293,-1.00708 -1.00754,-1.00708 z m 0.504,1.51108 h 14.10321 v 11.04783 h -14.10321 z m 1.00753,1.00754 v 9.03321 h 12.0886 v -9.03321 z m 3.52524,1.99776 c 1.3854,0 2.51906,1.13321 2.51906,2.51861 0,1.38467 -1.13366,2.51816 -2.51906,2.51816 -1.38467,0 -2.51771,-1.13349 -2.51771,-2.51816 0,-1.3854 1.13304,-2.51861 2.51771,-2.51861 z m 7.05183,0 c 0.27767,0 0.504,0.22706 0.504,0.504 v 4.02968 c 0,0.27694 -0.22633,0.50309 -0.504,0.50309 -0.27693,0 -0.50354,-0.22615 -0.50354,-0.50309 v -4.02968 c 0,-0.27694 0.22661,-0.504 0.50354,-0.504 z m -7.55492,0.504 v 0.60461 c -0.42786,0.15092 -0.75526,0.50306 -0.90692,0.90601 h -0.60461 v 1.00753 h 0.60461 c 0.15166,0.42859 0.47906,0.756 0.90692,0.90692 v 0.60461 h 1.00753 v -0.60461 c 0.42786,-0.15092 0.75509,-0.50397 0.90601,-0.90692 h 0.60462 v -1.00753 h -0.60462 c -0.15092,-0.42786 -0.47815,-0.75509 -0.90601,-0.90601 v -0.60461 z" />
<path
id="path1-8-1-7-3"
style="fill:#ffffff;fill-opacity:1"
d="m 2576.8666,993.66142 c -7.56,0 -13.7458,6.19115 -13.7458,13.75308 v 164.5449 c 0,7.57 6.1858,13.7458 13.7458,13.7458 h 6.8838 v 20.6369 h 27.499 v -20.6369 h 137.5019 v 20.6369 h 27.4989 v -20.6369 h 6.8693 c 7.57,0 13.7531,-6.1758 13.7531,-13.7458 v -164.5449 c 0,-7.56193 -6.1831,-13.75308 -13.7531,-13.75308 z m 6.8838,20.62968 h 192.4998 v 150.799 h -192.4998 z m 13.7531,13.753 v 123.2929 h 164.9936 v -123.2929 z m 48.1141,27.2674 c 18.91,0 34.3827,15.4654 34.3827,34.3754 0,18.9 -15.4727,34.3755 -34.3827,34.3755 -18.9,0 -34.3682,-15.4755 -34.3682,-34.3755 0,-18.91 15.4682,-34.3754 34.3682,-34.3754 z m 96.2499,0 c 3.79,0 6.8838,3.0965 6.8838,6.8765 v 55.0051 c 0,3.78 -3.0938,6.8693 -6.8838,6.8693 -3.78,0 -6.8693,-3.0893 -6.8693,-6.8693 v -55.0051 c 0,-3.78 3.0893,-6.8765 6.8693,-6.8765 z m -103.1192,6.8765 v 8.2519 c -5.84,2.06 -10.3078,6.8705 -12.3778,12.3705 h -8.2518 v 13.7531 h 8.2518 c 2.07,5.85 6.5378,10.3178 12.3778,12.3778 v 8.2518 h 13.7531 v -8.2518 c 5.84,-2.06 10.3105,-6.8778 12.3705,-12.3778 h 8.2446 v -13.7531 h -8.2446 c -2.06,-5.84 -6.5305,-10.3105 -12.3705,-12.3705 v -8.2519 z"
transform="matrix(0.06289731,0,0,0.06289731,-82.022365,94.831671)" />
<path
id="path1-8-1"
style="fill:#ffffff;fill-opacity:1"
d="m 2576.8708,993.66021 c -7.56,0 -13.7505,6.18852 -13.7505,13.75049 v 164.5474 c 0,7.57 6.1905,13.7454 13.7505,13.7454 h 6.8778 v 20.6333 h 27.5009 v -20.6333 h 137.4996 v 20.6333 h 27.5009 v -20.6333 h 6.8727 c 7.57,0 13.7504,-6.1754 13.7504,-13.7454 v -164.5474 c 0,-7.56197 -6.1804,-13.75049 -13.7504,-13.75049 z m 6.8778,20.62819 h 192.5014 v 150.797 h -192.5014 z m 13.7504,13.7556 v 123.296 h 165.0005 v -123.296 z m 48.119,27.2668 c 18.91,0 34.3838,15.4687 34.3838,34.3787 0,18.9 -15.4738,34.3685 -34.3838,34.3685 -18.9,0 -34.3685,-15.4685 -34.3685,-34.3685 0,-18.91 15.4685,-34.3787 34.3685,-34.3787 z m 96.2533,0 c 3.79,0 6.8778,3.0977 6.8778,6.8777 v 55.0019 c 0,3.78 -3.0878,6.8676 -6.8778,6.8676 -3.78,0 -6.8727,-3.0876 -6.8727,-6.8676 v -55.0019 c 0,-3.78 3.0927,-6.8777 6.8727,-6.8777 z m -103.1209,6.8777 v 8.2523 c -5.84,2.06 -10.311,6.8709 -12.381,12.3709 h -8.2472 v 13.7505 h 8.2472 c 2.07,5.85 6.541,10.3159 12.381,12.3759 v 8.2523 h 13.7505 v -8.2523 c 5.84,-2.06 10.3108,-6.8759 12.3708,-12.3759 h 8.2473 v -13.7505 h -8.2473 c -2.06,-5.84 -6.5308,-10.3109 -12.3708,-12.3709 v -8.2523 z"
transform="matrix(0.08943055,0,0,0.08943055,-115.49313,85.768735)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,26 @@
{
"name": "WebPeer - Pseudo client for Self-hosted LiveSync Peer-to-Peer Replication",
"short_name": "WepPeer",
"description": "A web-based pseudo peer-to-peer replication client using as like server for background sync.",
"start_url": "./",
"display": "standalone",
"background_color": "#fff",
"theme_color": "#fff",
"orientation": "any",
"icons": [
{
"src": "./icon.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"maskable": true
}
],
"additional_icons": [
{
"src": "./icon.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"maskable": true
}
]
}

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import SyncMain from "./SyncMain.svelte";
</script>
<SyncMain></SyncMain>

View File

@@ -0,0 +1,23 @@
import { LOG_LEVEL_VERBOSE } from "@lib/common/types";
import { defaultLoggerEnv, setGlobalLogFunction } from "@lib/common/logger";
import { writable } from "svelte/store";
export const logs = writable([] as string[]);
let _logs = [] as string[];
const maxLines = 10000;
setGlobalLogFunction((msg, level) => {
console.log(msg);
const msgstr = typeof msg === "string" ? msg : JSON.stringify(msg);
const strLog = `${new Date().toISOString()}\u2001${msgstr}`;
_logs.push(strLog);
if (_logs.length > maxLines) {
_logs = _logs.slice(_logs.length - maxLines);
}
logs.set(_logs);
});
defaultLoggerEnv.minLogLevel = LOG_LEVEL_VERBOSE;
export const storeP2PStatusLine = writable("");

View File

@@ -0,0 +1,364 @@
import { PouchDB } from "@lib/pouchdb/pouchdb-browser";
import {
type EntryDoc,
type LOG_LEVEL,
type P2PSyncSetting,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
P2P_DEFAULT_SETTINGS,
REMOTE_P2P,
} from "@lib/common/types";
import { eventHub } from "@lib/hub/hub";
import type { Confirm } from "@lib/interfaces/Confirm";
import { LOG_LEVEL_INFO, Logger } from "@lib/common/logger";
import { storeP2PStatusLine } from "./CommandsShim";
import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type CommandShim,
type PeerStatus,
type PluginShim,
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
import {
closeP2PReplicator,
openP2PReplicator,
P2PLogCollector,
type P2PReplicatorBase,
} from "@lib/replication/trystero/P2PReplicatorCore";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
import { unique } from "octagonal-wheels/collection";
import { BrowserServiceHub } from "@lib/services/BrowserServices";
import { TrysteroReplicator } from "@lib/replication/trystero/TrysteroReplicator";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
import { Menu } from "@/lib/src/services/implements/browser/Menu";
function addToList(item: string, list: string) {
return unique(
list
.split(",")
.map((e) => e.trim())
.concat(item)
.filter((p) => p)
).join(",");
}
function removeFromList(item: string, list: string) {
return list
.split(",")
.map((e) => e.trim())
.filter((p) => p !== item)
.filter((p) => p)
.join(",");
}
export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
storeP2PStatusLine = reactiveSource("");
plugin!: PluginShim;
// environment!: IEnvironment;
confirm!: Confirm;
// simpleStoreAPI!: ISimpleStoreAPI;
db?: PouchDB.Database<EntryDoc>;
services: InjectableServiceHub<ServiceContext>;
getDB() {
if (!this.db) {
throw new Error("DB not initialized");
}
return this.db;
}
_simpleStore!: SimpleStore<any>;
async closeDB() {
if (this.db) {
await this.db.close();
this.db = undefined;
}
}
constructor() {
const browserServiceHub = new BrowserServiceHub<ServiceContext>();
this.services = browserServiceHub;
this.services.vault.getVaultName.setHandler(() => "p2p-livesync-web-peer");
}
async init() {
// const { simpleStoreAPI } = await getWrappedSynchromesh();
// this.confirm = confirm;
this.confirm = this.services.UI.confirm;
// this.environment = environment;
if (this.db) {
try {
await this.closeDB();
} catch (ex) {
Logger("Error closing db", LOG_LEVEL_VERBOSE);
Logger(ex, LOG_LEVEL_VERBOSE);
}
}
const repStore = this.services.database.openSimpleStore<any>("p2p-livesync-web-peer");
this._simpleStore = repStore;
let _settings = (await repStore.get("settings")) || ({ ...P2P_DEFAULT_SETTINGS } as P2PSyncSetting);
this.plugin = {
saveSettings: async () => {
await repStore.set("settings", _settings);
eventHub.emitEvent(EVENT_SETTING_SAVED, _settings);
},
get settings() {
return _settings;
},
set settings(newSettings: P2PSyncSetting) {
_settings = { ..._settings, ...newSettings };
},
rebuilder: null,
services: this.services,
// $$scheduleAppReload: () => {},
// $$getVaultName: () => "p2p-livesync-web-peer",
};
// const deviceName = this.getDeviceName();
const database_name = this.settings.P2P_AppID + "-" + this.settings.P2P_roomID + "p2p-livesync-web-peer";
this.db = new PouchDB<EntryDoc>(database_name);
setTimeout(() => {
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
void this.open();
}
}, 1000);
return this;
}
get settings() {
return this.plugin.settings;
}
_log(msg: any, level?: LOG_LEVEL): void {
Logger(msg, level);
}
_notice(msg: string, key?: string): void {
Logger(msg, LOG_LEVEL_NOTICE, key);
}
getSettings(): P2PSyncSetting {
return this.settings;
}
simpleStore(): SimpleStore<any> {
return this._simpleStore;
}
handleReplicatedDocuments(docs: EntryDoc[]): Promise<void> {
// No op. This is a client and does not need to process the docs
return Promise.resolve();
}
getPluginShim() {
return {};
}
getConfig(key: string) {
const vaultName = this.services.vault.getVaultName();
const dbKey = `${vaultName}-${key}`;
return localStorage.getItem(dbKey);
}
setConfig(key: string, value: string) {
const vaultName = this.services.vault.getVaultName();
const dbKey = `${vaultName}-${key}`;
localStorage.setItem(dbKey, value);
}
getDeviceName(): string {
return this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? this.plugin.services.vault.getVaultName();
}
getPlatform(): string {
return "pseudo-replicator";
}
m?: Menu;
afterConstructor(): void {
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
if (this.m) {
this.m.hide();
}
this.m = new Menu()
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => this.replicateFrom(peer)))
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => this.replicateTo(peer)))
.addSeparator()
// .addItem((item) => {
// item.setTitle("🔧 Get Configuration").onClick(async () => {
// await this.getRemoteConfig(peer);
// });
// })
// .addSeparator()
.addItem((item) => {
const mark = peer.syncOnConnect ? "checkmark" : null;
item.setTitle("Toggle Sync on connect")
.onClick(async () => {
await this.toggleProp(peer, "syncOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.watchOnConnect ? "checkmark" : null;
item.setTitle("Toggle Watch on connect")
.onClick(async () => {
await this.toggleProp(peer, "watchOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = peer.syncOnReplicationCommand ? "checkmark" : null;
item.setTitle("Toggle Sync on `Replicate now` command")
.onClick(async () => {
await this.toggleProp(peer, "syncOnReplicationCommand");
})
.setIcon(mark);
});
void this.m.showAtPosition({ x: event.x, y: event.y });
});
this.p2pLogCollector.p2pReplicationLine.onChanged((line) => {
storeP2PStatusLine.set(line.value);
});
}
_replicatorInstance?: TrysteroReplicator;
p2pLogCollector = new P2PLogCollector();
async open() {
await openP2PReplicator(this);
}
async close() {
await closeP2PReplicator(this);
}
enableBroadcastCastings() {
return this?._replicatorInstance?.enableBroadcastChanges();
}
disableBroadcastCastings() {
return this?._replicatorInstance?.disableBroadcastChanges();
}
async initialiseP2PReplicator(): Promise<TrysteroReplicator> {
await this.init();
try {
if (this._replicatorInstance) {
await this._replicatorInstance.close();
this._replicatorInstance = undefined;
}
if (!this.settings.P2P_AppID) {
this.settings.P2P_AppID = P2P_DEFAULT_SETTINGS.P2P_AppID;
}
const getInitialDeviceName = () =>
this.getConfig(SETTING_KEY_P2P_DEVICE_NAME) || this.services.vault.getVaultName();
const getSettings = () => this.settings;
const store = () => this.simpleStore();
const getDB = () => this.getDB();
const getConfirm = () => this.confirm;
const getPlatform = () => this.getPlatform();
const env = {
get db() {
return getDB();
},
get confirm() {
return getConfirm();
},
get deviceName() {
return getInitialDeviceName();
},
get platform() {
return getPlatform();
},
get settings() {
return getSettings();
},
processReplicatedDocs: async (docs: EntryDoc[]): Promise<void> => {
await this.handleReplicatedDocuments(docs);
// No op. This is a client and does not need to process the docs
},
get simpleStore() {
return store();
},
};
this._replicatorInstance = new TrysteroReplicator(env);
return this._replicatorInstance;
} catch (e) {
this._log(
e instanceof Error ? e.message : "Something occurred on Initialising P2P Replicator",
LOG_LEVEL_INFO
);
this._log(e, LOG_LEVEL_VERBOSE);
throw e;
}
}
get replicator() {
return this._replicatorInstance!;
}
async replicateFrom(peer: PeerStatus) {
await this.replicator.replicateFrom(peer.peerId);
}
async replicateTo(peer: PeerStatus) {
await this.replicator.requestSynchroniseToPeer(peer.peerId);
}
async getRemoteConfig(peer: PeerStatus) {
Logger(
`Requesting remote config for ${peer.name}. Please input the passphrase on the remote device`,
LOG_LEVEL_NOTICE
);
const remoteConfig = await this.replicator.getRemoteConfig(peer.peerId);
if (remoteConfig) {
Logger(`Remote config for ${peer.name} is retrieved successfully`);
const DROP = "Yes, and drop local database";
const KEEP = "Yes, but keep local database";
const CANCEL = "No, cancel";
const yn = await this.confirm.askSelectStringDialogue(
`Do you really want to apply the remote config? This will overwrite your current config immediately and restart.
And you can also drop the local database to rebuild from the remote device.`,
[DROP, KEEP, CANCEL] as const,
{
defaultAction: CANCEL,
title: "Apply Remote Config ",
}
);
if (yn === DROP || yn === KEEP) {
if (yn === DROP) {
if (remoteConfig.remoteType !== REMOTE_P2P) {
const yn2 = await this.confirm.askYesNoDialog(
`Do you want to set the remote type to "P2P Sync" to rebuild by "P2P replication"?`,
{
title: "Rebuild from remote device",
}
);
if (yn2 === "yes") {
remoteConfig.remoteType = REMOTE_P2P;
remoteConfig.P2P_RebuildFrom = peer.name;
}
}
}
this.plugin.settings = remoteConfig;
await this.plugin.saveSettings();
if (yn === DROP) {
await this.plugin.rebuilder.scheduleFetch();
} else {
await this.plugin.services.appLifecycle.scheduleRestart();
}
} else {
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
}
} else {
Logger(`Cannot retrieve remote config for ${peer.peerId}`);
}
}
async toggleProp(peer: PeerStatus, prop: "syncOnConnect" | "watchOnConnect" | "syncOnReplicationCommand") {
const settingMap = {
syncOnConnect: "P2P_AutoSyncPeers",
watchOnConnect: "P2P_AutoWatchPeers",
syncOnReplicationCommand: "P2P_SyncOnReplication",
} as const;
const targetSetting = settingMap[prop];
if (peer[prop]) {
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
await this.plugin.saveSettings();
} else {
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
await this.plugin.saveSettings();
}
}
}
export const cmdSyncShim = new P2PReplicatorShim();

View File

@@ -0,0 +1,112 @@
<script lang="ts">
import { storeP2PStatusLine, logs } from "./CommandsShim";
import P2PReplicatorPane from "@/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte";
import { onMount, tick } from "svelte";
import { cmdSyncShim } from "./P2PReplicatorShim";
import { eventHub } from "@lib/hub/hub";
import { EVENT_LAYOUT_READY } from "@lib/events/coreEvents";
let synchronised = $state(cmdSyncShim.init());
onMount(() => {
eventHub.emitEvent(EVENT_LAYOUT_READY);
return () => {
synchronised.then((e) => e.close());
};
});
let elP: HTMLDivElement;
logs.subscribe((log) => {
tick().then(() => elP?.scrollTo({ top: elP.scrollHeight }));
});
let statusLine = $state("");
storeP2PStatusLine.subscribe((status) => {
statusLine = status;
});
</script>
<main>
<div class="control">
{#await synchronised then cmdSync}
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync}></P2PReplicatorPane>
{:catch error}
<p>{error.message}</p>
{/await}
</div>
<div class="log">
<div class="status">
{statusLine}
</div>
<div class="logslist" bind:this={elP}>
{#each $logs as log}
<p>{log}</p>
{/each}
</div>
</div>
</main>
<style>
main {
display: flex;
flex-direction: row;
flex-grow: 1;
max-height: 100vh;
box-sizing: border-box;
}
@media (max-width: 900px) {
main {
flex-direction: column;
}
}
@media (device-orientation: portrait) {
main {
flex-direction: column;
}
}
.log {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding: 1em;
min-width: 50%;
}
@media (max-width: 900px) {
.log {
max-height: 50vh;
}
}
@media (device-orientation: portrait) {
.log {
max-height: 50vh;
}
}
.control {
padding: 1em 1em;
overflow-y: scroll;
flex-grow: 1;
}
.status {
flex-grow: 0;
/* max-height: 40px; */
/* height: 40px; */
flex-shrink: 0;
}
.logslist {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
/* padding: 1em; */
width: 100%;
overflow-y: scroll;
flex-grow: 1;
flex-shrink: 1;
/* max-height: calc(100% - 40px); */
}
p {
margin: 0;
white-space: pre-wrap;
text-align: left;
word-break: break-all;
}
</style>

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import { Menu } from "@/lib/src/services/implements/browser/Menu";
import { getDialogContext } from "@lib/services/implements/base/SvelteDialog";
let result = $state<string | boolean>("");
const context = getDialogContext();
async function testUI() {
const confirm = await context.services.confirm;
const ret = await confirm.askString("Your name", "What is your name?", "John Doe", false);
result = ret;
}
let resultPassword = $state<string | boolean>("");
async function testPassword() {
const confirm = await context.services.confirm;
const ret = await confirm.askString("passphrase", "?", "anythingonlyyouknow", true);
resultPassword = ret;
}
async function testMenu(event: MouseEvent) {
const m = new Menu()
.addItem((item) => item.setTitle("📥 Only Fetch").onClick(() => {}))
.addItem((item) => item.setTitle("📤 Only Send").onClick(() => {}))
.addSeparator()
.addItem((item) => {
item.setTitle("🔧 Get Configuration").onClick(async () => {
console.log("Get Configuration");
});
})
.addSeparator()
.addItem((item) => {
const mark = "checkmark";
item.setTitle("Toggle Sync on connect")
.onClick(async () => {
console.log("Toggle Sync on connect");
// await this.toggleProp(peer, "syncOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = null;
item.setTitle("Toggle Watch on connect")
.onClick(async () => {
console.log("Toggle Watch on connect");
// await this.toggleProp(peer, "watchOnConnect");
})
.setIcon(mark);
})
.addItem((item) => {
const mark = null;
item.setTitle("Toggle Sync on `Replicate now` command")
.onClick(async () => {})
.setIcon(mark);
});
m.showAtPosition({ x: event.x, y: event.y });
}
</script>
<main>
<h1>UI Test</h1>
<article>
<div>
<button onclick={() => testUI()}> String input </button>
{result}
</div>
<div>
<button onclick={() => testPassword()}> Password Input </button>
{resultPassword}
</div>
<div>
<button onclick={testMenu}>Menu</button>
</div>
</article>
</main>

View File

@@ -0,0 +1,112 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--background-primary: #ffffff;
--background-primary-alt: #e9e9e9;
--size-4-1: 0.25em;
--tag-background: #f0f0f0;
--tag-border-width: 1px;
--tag-border-color: #cfffdd;
--background-modifier-success: #d4f3e9;
--background-secondary: #f0f0f0;
--background-modifier-error: #f8d7da;
--background-modifier-error-hover: #f5c6cb;
--interactive-accent: #007bff;
--interactive-accent-hover: #0056b3;
--text-normal: #333;
--text-warning: #f0ad4e;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
padding: 0;
display: flex;
min-width: 320px;
min-height: 100vh;
box-sizing: border-box;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
margin: 0;
padding: 0;
text-align: center;
display: flex;
flex-grow: 1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
input,
select {
border-radius: 8px;
border: 1px solid #1a1a1a;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
transition: border-color 0.25s;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,9 @@
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
const app = mount(App, {
target: document.getElementById("app")!,
});
export default app;

View File

@@ -0,0 +1,9 @@
import { mount } from "svelte";
import "./app.css";
import App from "./UITest.svelte";
const app = mount(App, {
target: document.getElementById("app")!,
});
export default app;

2
src/apps/webpeer/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,7 @@
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
};

View File

@@ -0,0 +1,25 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"sourceRoot": "../",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"moduleDetection": "force",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}

View File

@@ -0,0 +1,4 @@
{
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
}
},
"include": ["vite.config.ts"]
}

16
src/apps/webpeer/ui.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Peer-to-Peer Daemon on Browser</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/uitest.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import path from "node:path";
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
resolve: {
alias: {
"@": path.resolve(__dirname, "../../"),
"@lib": path.resolve(__dirname, "../../lib/src"),
},
},
base: "./",
build: {
outDir: "dist",
emptyOutDir: true,
rollupOptions: {
input: {
index: "index.html",
// uitest: "uitest.html",
},
},
},
});

View File

@@ -1,43 +1,99 @@
import { deleteDB, type IDBPDatabase, openDB } from "idb";
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
import { serialized } from "octagonal-wheels/concurrency/lock";
import { Logger } from "octagonal-wheels/common/logger";
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
export const OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatabase> => {
export { OpenKeyValueDatabase } from "./KeyValueDBv2.ts";
export const _OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatabase> => {
if (dbKey in databaseCache) {
databaseCache[dbKey].close();
delete databaseCache[dbKey];
}
const storeKey = dbKey;
const dbPromise = openDB(dbKey, 1, {
upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
return db.createObjectStore(storeKey);
},
});
const db = await dbPromise;
databaseCache[dbKey] = db;
let db: IDBPDatabase<any> | null = null;
const _openDB = () => {
return serialized("keyvaluedb-" + dbKey, async () => {
const dbInstance = await openDB(dbKey, 1, {
upgrade(db, _oldVersion, _newVersion, _transaction, _event) {
return db.createObjectStore(storeKey);
},
blocking(currentVersion, blockedVersion, event) {
Logger(
`Blocking database open for ${dbKey}: currentVersion=${currentVersion}, blockedVersion=${blockedVersion}`
);
databaseCache[dbKey]?.close();
delete databaseCache[dbKey];
},
blocked(currentVersion, blockedVersion, event) {
Logger(
`Database open blocked for ${dbKey}: currentVersion=${currentVersion}, blockedVersion=${blockedVersion}`
);
},
terminated() {
Logger(`Database connection terminated for ${dbKey}`);
},
});
databaseCache[dbKey] = dbInstance;
return dbInstance;
});
};
const closeDB = () => {
if (db) {
db.close();
delete databaseCache[dbKey];
db = null;
}
};
db = await _openDB();
return {
async get<T>(key: IDBValidKey): Promise<T> {
if (!db) {
db = await _openDB();
databaseCache[dbKey] = db;
}
return await db.get(storeKey, key);
},
async set<T>(key: IDBValidKey, value: T) {
if (!db) {
db = await _openDB();
databaseCache[dbKey] = db;
}
return await db.put(storeKey, value, key);
},
async del(key: IDBValidKey) {
if (!db) {
db = await _openDB();
databaseCache[dbKey] = db;
}
return await db.delete(storeKey, key);
},
async clear() {
if (!db) {
db = await _openDB();
databaseCache[dbKey] = db;
}
return await db.clear(storeKey);
},
async keys(query?: IDBValidKey | IDBKeyRange, count?: number) {
if (!db) {
db = await _openDB();
databaseCache[dbKey] = db;
}
return await db.getAllKeys(storeKey, query, count);
},
close() {
delete databaseCache[dbKey];
return db.close();
return Promise.resolve(closeDB());
},
async destroy() {
delete databaseCache[dbKey];
db.close();
await deleteDB(dbKey);
// await closeDB();
await deleteDB(dbKey, {
blocked() {
console.warn(`Database delete blocked for ${dbKey}`);
},
});
},
};
};

154
src/common/KeyValueDBv2.ts Normal file
View File

@@ -0,0 +1,154 @@
import { LOG_LEVEL_VERBOSE, Logger } from "@/lib/src/common/logger";
import type { KeyValueDatabase } from "@/lib/src/interfaces/KeyValueDatabase";
import { deleteDB, openDB, type IDBPDatabase } from "idb";
import { serialized } from "octagonal-wheels/concurrency/lock";
const databaseCache = new Map<string, IDBKeyValueDatabase>();
export async function OpenKeyValueDatabase(dbKey: string): Promise<KeyValueDatabase> {
return await serialized(`OpenKeyValueDatabase-${dbKey}`, async () => {
const cachedDB = databaseCache.get(dbKey);
if (cachedDB) {
if (!cachedDB.isDestroyed) {
return cachedDB;
}
await cachedDB.ensuredDestroyed;
databaseCache.delete(dbKey);
}
const newDB = new IDBKeyValueDatabase(dbKey);
try {
await newDB.getIsReady();
databaseCache.set(dbKey, newDB);
return newDB;
} catch (e) {
databaseCache.delete(dbKey);
throw e;
}
});
}
export class IDBKeyValueDatabase implements KeyValueDatabase {
protected _dbPromise: Promise<IDBPDatabase<any>> | null = null;
protected dbKey: string;
protected storeKey: string;
protected _isDestroyed: boolean = false;
protected destroyedPromise: Promise<void> | null = null;
get isDestroyed() {
return this._isDestroyed;
}
get ensuredDestroyed(): Promise<void> {
if (this.destroyedPromise) {
return this.destroyedPromise;
}
return Promise.resolve();
}
async getIsReady(): Promise<boolean> {
await this.ensureDB();
return this.isDestroyed === false;
}
protected ensureDB() {
if (this._isDestroyed) {
throw new Error("Database is destroyed");
}
if (this._dbPromise) {
return this._dbPromise;
}
this._dbPromise = openDB(this.dbKey, undefined, {
upgrade: (db, _oldVersion, _newVersion, _transaction, _event) => {
if (!db.objectStoreNames.contains(this.storeKey)) {
return db.createObjectStore(this.storeKey);
}
},
blocking: (currentVersion, blockedVersion, event) => {
Logger(
`Blocking database open for ${this.dbKey}: currentVersion=${currentVersion}, blockedVersion=${blockedVersion}`,
LOG_LEVEL_VERBOSE
);
// This `this` is not this openDB instance, previously opened DB. Let it be closed in the terminated handler.
void this.closeDB(true);
},
blocked: (currentVersion, blockedVersion, event) => {
Logger(
`Database open blocked for ${this.dbKey}: currentVersion=${currentVersion}, blockedVersion=${blockedVersion}`,
LOG_LEVEL_VERBOSE
);
},
terminated: () => {
Logger(`Database connection terminated for ${this.dbKey}`, LOG_LEVEL_VERBOSE);
this._dbPromise = null;
},
}).catch((e) => {
this._dbPromise = null;
throw e;
});
return this._dbPromise;
}
protected async closeDB(setDestroyed: boolean = false) {
if (this._dbPromise) {
const tempPromise = this._dbPromise;
this._dbPromise = null;
try {
const dbR = await tempPromise;
dbR.close();
} catch (e) {
Logger(`Error closing database`);
Logger(e, LOG_LEVEL_VERBOSE);
}
}
this._dbPromise = null;
if (setDestroyed) {
this._isDestroyed = true;
this.destroyedPromise = Promise.resolve();
}
}
get DB(): Promise<IDBPDatabase<any>> {
if (this._isDestroyed) {
return Promise.reject(new Error("Database is destroyed"));
}
return this.ensureDB();
}
constructor(dbKey: string) {
this.dbKey = dbKey;
this.storeKey = dbKey;
}
async get<U>(key: IDBValidKey): Promise<U> {
const db = await this.DB;
return await db.get(this.storeKey, key);
}
async set<U>(key: IDBValidKey, value: U): Promise<IDBValidKey> {
const db = await this.DB;
await db.put(this.storeKey, value, key);
return key;
}
async del(key: IDBValidKey): Promise<void> {
const db = await this.DB;
return await db.delete(this.storeKey, key);
}
async clear(): Promise<void> {
const db = await this.DB;
return await db.clear(this.storeKey);
}
async keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise<IDBValidKey[]> {
const db = await this.DB;
return await db.getAllKeys(this.storeKey, query, count);
}
async close(): Promise<void> {
await this.closeDB();
}
async destroy(): Promise<void> {
this._isDestroyed = true;
this.destroyedPromise = (async () => {
await this.closeDB();
await deleteDB(this.dbKey, {
blocked: () => {
Logger(`Database delete blocked for ${this.dbKey}`);
},
});
})();
await this.destroyedPromise;
}
}

View File

@@ -1,4 +1,4 @@
import { ItemView } from "obsidian";
import { ItemView } from "@/deps.ts";
import { type mount, unmount } from "svelte";
export abstract class SvelteItemView extends ItemView {

View File

@@ -24,6 +24,7 @@ export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
export const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error";
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3";
export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
// export const EVENT_FILE_CHANGED = "file-changed";
@@ -46,6 +47,7 @@ declare global {
[EVENT_ON_UNRESOLVED_ERROR]: undefined;
[EVENT_ANALYSE_DB_USAGE]: undefined;
[EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined;
[EVENT_REQUEST_PERFORM_GC_V3]: undefined;
}
}

View File

@@ -24,6 +24,13 @@ export {
parseYaml,
ItemView,
WorkspaceLeaf,
Menu,
request,
getLanguage,
ButtonComponent,
TextComponent,
ToggleComponent,
DropdownComponent,
} from "obsidian";
export type {
DataWriteOptions,
@@ -32,6 +39,7 @@ export type {
RequestUrlResponse,
MarkdownFileInfo,
ListedFiles,
ValueComponent,
} from "obsidian";
import { normalizePath as normalizePath_ } from "obsidian";
const normalizePath = normalizePath_ as <T extends string | FilePath>(from: T) => T;

View File

@@ -1803,16 +1803,16 @@ export class ConfigSync extends LiveSyncCommands {
return files;
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.fileProcessing.handleOptionalFileEvent(this._anyProcessOptionalFileEvent.bind(this));
services.conflict.handleGetOptionalConflictCheckMethod(this._anyGetOptionalConflictCheckMethod.bind(this));
services.replication.handleProcessVirtualDocuments(this._anyModuleParsedReplicationResultItem.bind(this));
services.setting.handleOnRealiseSetting(this._everyRealizeSettingSyncMode.bind(this));
services.appLifecycle.handleOnResuming(this._everyOnResumeProcess.bind(this));
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
services.databaseEvents.handleDatabaseInitialised(this._everyOnDatabaseInitialized.bind(this));
services.setting.handleSuspendExtraSync(this._allSuspendExtraSync.bind(this));
services.setting.handleSuggestOptionalFeatures(this._allAskUsingOptionalSyncFeature.bind(this));
services.setting.handleEnableOptionalFeature(this._allConfigureOptionalSyncFeature.bind(this));
services.fileProcessing.processOptionalFileEvent.addHandler(this._anyProcessOptionalFileEvent.bind(this));
services.conflict.getOptionalConflictCheckMethod.addHandler(this._anyGetOptionalConflictCheckMethod.bind(this));
services.replication.processVirtualDocument.addHandler(this._anyModuleParsedReplicationResultItem.bind(this));
services.setting.onRealiseSetting.addHandler(this._everyRealizeSettingSyncMode.bind(this));
services.appLifecycle.onResuming.addHandler(this._everyOnResumeProcess.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
services.setting.suspendExtraSync.addHandler(this._allSuspendExtraSync.bind(this));
services.setting.suggestOptionalFeatures.addHandler(this._allAskUsingOptionalSyncFeature.bind(this));
services.setting.enableOptionalFeature.addHandler(this._allConfigureOptionalSyncFeature.bind(this));
}
}

View File

@@ -10,7 +10,7 @@
import { getDocData, timeDeltaToHumanReadable, unique } from "../../lib/src/common/utils";
import type ObsidianLiveSyncPlugin from "../../main";
// import { askString } from "../../common/utils";
import { Menu } from "obsidian";
import { Menu } from "@/deps.ts";
export let list: IPluginDataExDisplay[] = [];
export let thisTerm = "";

View File

@@ -10,7 +10,7 @@
pluginV2Progress,
} from "./CmdConfigSync.ts";
import PluginCombo from "./PluginCombo.svelte";
import { Menu, type PluginManifest } from "obsidian";
import { Menu, type PluginManifest } from "@/deps.ts";
import { unique } from "../../lib/src/common/utils";
import {
MODE_SELECTIVE,

View File

@@ -1914,16 +1914,16 @@ ${messageFetch}${messageOverwrite}${messageMerge}
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
// No longer needed on initialisation
// services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
services.fileProcessing.handleOptionalFileEvent(this._anyProcessOptionalFileEvent.bind(this));
services.conflict.handleGetOptionalConflictCheckMethod(this._anyGetOptionalConflictCheckMethod.bind(this));
services.replication.handleProcessOptionalSynchroniseResult(this._anyProcessOptionalSyncFiles.bind(this));
services.setting.handleOnRealiseSetting(this._everyRealizeSettingSyncMode.bind(this));
services.appLifecycle.handleOnResuming(this._everyOnResumeProcess.bind(this));
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
services.databaseEvents.handleDatabaseInitialised(this._everyOnDatabaseInitialized.bind(this));
services.setting.handleSuspendExtraSync(this._allSuspendExtraSync.bind(this));
services.setting.handleSuggestOptionalFeatures(this._allAskUsingOptionalSyncFeature.bind(this));
services.setting.handleEnableOptionalFeature(this._allConfigureOptionalSyncFeature.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.fileProcessing.processOptionalFileEvent.addHandler(this._anyProcessOptionalFileEvent.bind(this));
services.conflict.getOptionalConflictCheckMethod.addHandler(this._anyGetOptionalConflictCheckMethod.bind(this));
services.replication.processOptionalSynchroniseResult.addHandler(this._anyProcessOptionalSyncFiles.bind(this));
services.setting.onRealiseSetting.addHandler(this._everyRealizeSettingSyncMode.bind(this));
services.appLifecycle.onResuming.addHandler(this._everyOnResumeProcess.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
services.setting.suspendExtraSync.addHandler(this._allSuspendExtraSync.bind(this));
services.setting.suggestOptionalFeatures.addHandler(this._allAskUsingOptionalSyncFeature.bind(this));
services.setting.enableOptionalFeature.addHandler(this._allConfigureOptionalSyncFeature.bind(this));
}
}

View File

@@ -14,7 +14,11 @@ import { getNoFromRev } from "../../lib/src/pouchdb/LiveSyncLocalDB";
import { LiveSyncCommands } from "../LiveSyncCommands";
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
import { arrayToChunkedArray } from "octagonal-wheels/collection";
import { EVENT_ANALYSE_DB_USAGE, eventHub } from "@/common/events";
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";
const DB_KEY_SEQ = "gc-seq";
const DB_KEY_CHUNK_SET = "chunk-set";
const DB_KEY_DOC_USAGE_MAP = "doc-usage-map";
@@ -37,7 +41,16 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
await this.analyseDatabase();
},
});
this.plugin.addCommand({
id: "gc-v3",
name: "Garbage Collection V3 (advanced, beta)",
icon: "trash-2",
callback: async () => {
await this.gcv3();
},
});
eventHub.onEvent(EVENT_ANALYSE_DB_USAGE, () => this.analyseDatabase());
eventHub.onEvent(EVENT_REQUEST_PERFORM_GC_V3, () => this.gcv3());
}
async allChunks(includeDeleted: boolean = false) {
const p = this._progress("", LOG_LEVEL_NOTICE);
@@ -708,4 +721,246 @@ Success: ${successCount}, Errored: ${errored}`;
// Prompt to copy to clipboard
await this.services.UI.promptCopyToClipboard("Database Analysis data (TSV):", csv);
}
async compactDatabase() {
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true);
if (!remote) {
this._notice("Failed to connect to remote for compaction.", "gc-compact");
return;
}
if (typeof remote == "string") {
this._notice(`Failed to connect to remote for compaction. ${remote}`, "gc-compact");
return;
}
const compactResult = await remote.db.compact({
interval: 1000,
});
// Probably no need to wait, but just in case.
let timeout = 2 * 60 * 1000; // 2 minutes
do {
const status = await remote.db.info();
if ("compact_running" in status && status?.compact_running) {
this._notice("Compaction in progress on remote database...", "gc-compact");
await delay(2000);
timeout -= 2000;
if (timeout <= 0) {
this._notice("Compaction on remote database timed out.", "gc-compact");
break;
}
} else {
break;
}
} while (true);
if (compactResult && "ok" in compactResult) {
this._notice("Compaction on remote database completed successfully.", "gc-compact");
} else {
this._notice("Compaction on remote database failed.", "gc-compact");
}
}
/**
* 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.plugin.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 + (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.plugin.replicator as LiveSyncCouchDBReplicator;
// Start one-shot replication to ensure all changes are synced before GC.
const r0 = await replicator.openOneShotReplication(this.settings, false, false, "sync");
if (!r0) {
this._notice(
"Failed to start one-shot replication before Garbage Collection. Garbage Collection Cancelled."
);
return;
}
// Delete the chunk, but first verify the following:
// Fetch the list of accepted nodes from the replicator.
const OPTION_CANCEL = "Cancel Garbage Collection";
const info = await this.plugin.replicator.getConnectedDeviceList();
if (!info) {
this._notice("No connected device information found. Cancelling Garbage Collection.");
return;
}
const { accepted_nodes, node_info } = info;
//1. Compare accepted_nodes and node_info, and confirm whether it is acceptable to delete nodes not present in node_info.
const infoMissingNodes = [] as string[];
for (const node of accepted_nodes) {
if (!(node in node_info)) {
infoMissingNodes.push(node);
}
}
if (infoMissingNodes.length > 0) {
const message = `The following accepted nodes are missing its node information:\n- ${infoMissingNodes.join("\n- ")}\n\nThis indicates that they have not been connected for some time or have been left on an older version.
It is preferable to update all devices if possible. If you have any devices that are no longer in use, you can clear all accepted nodes by locking the remote once.`;
const OPTION_IGNORE = "Ignore and Proceed";
// const OPTION_DELETE = "Delete them and proceed";
const buttons = [OPTION_CANCEL, OPTION_IGNORE] as const;
const result = await this.plugin.confirm.askSelectStringDialogue(message, buttons, {
title: "Node Information Missing",
defaultAction: OPTION_CANCEL,
});
if (result === OPTION_CANCEL) {
this._notice("Garbage Collection cancelled by user.");
return;
} else if (result === OPTION_IGNORE) {
this._notice("Proceeding with Garbage Collection, ignoring missing nodes.");
}
}
//2. Check whether the progress values in NodeData are roughly the same (only the numerical part is needed).
const progressValues = Object.values(node_info)
.map((e) => e.progress.split("-")[0])
.map((e) => parseInt(e));
const maxProgress = Math.max(...progressValues);
const minProgress = Math.min(...progressValues);
const progressDifference = maxProgress - minProgress;
const OPTION_PROCEED = "Proceed Garbage Collection";
// - If they differ significantly, the node may not have completed synchronisation, potentially causing conflicts. Display a confirmation dialog as a precaution.
// - If they are not significantly different, display the standard confirmation dialogue message.
const detail = `> [!INFO]- The connected devices have been detected as follows:
${Object.entries(node_info)
.map(
([nodeId, nodeData]) =>
`> - Device: ${nodeData.device_name} (Node ID: ${nodeId})
> - Obsidian version: ${nodeData.app_version}
> - Plug-in version: ${nodeData.plugin_version}
> - Progress: ${nodeData.progress.split("-")[0]}`
)
.join("\n")}
`;
const message =
progressDifference != 0
? `Some devices have differing progress values (max: ${maxProgress}, min: ${minProgress}).
This may indicate that some devices have not completed synchronisation, which could lead to conflicts. Strongly recommend confirming that all devices are synchronised before proceeding.`
: `All devices have the same progress value (${maxProgress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.`;
const buttons = [OPTION_PROCEED, OPTION_CANCEL] as const;
const defaultAction = progressDifference != 0 ? OPTION_CANCEL : OPTION_PROCEED;
const result = await this.plugin.confirm.askSelectStringDialogue(message + "\n\n" + detail, buttons, {
title: "Garbage Collection Confirmation",
defaultAction,
});
if (result !== OPTION_PROCEED) {
this._notice("Garbage Collection cancelled by user.");
return;
}
this._notice("Proceeding with Garbage Collection.");
//- 3. Once OK is confirmed in the dialogue, execute the chunk deletion. This is performed on the local database and immediately reflected on the remote. After reflecting on the remote, perform compaction.
const gcStartTime = Date.now();
// Perform Garbage Collection (new implementation).
const localDatabase = this.localDatabase.localDatabase;
const usedChunks = new Set<DocumentID>();
const allChunks = new Map<DocumentID, string>();
const IDs = this.localDatabase.findEntryNames("", "", {});
let i = 0;
const doc_count = (await localDatabase.info()).doc_count;
for await (const id of IDs) {
const doc = await this.localDatabase.getRaw(id as DocumentID);
i++;
if (i % 100 == 0) {
this._notice(`Garbage Collection: Scanned ${i} / ~${doc_count} `, "gc-scanning");
}
if (!doc) continue;
if ("children" in doc) {
const children = (doc.children || []) as DocumentID[];
for (const chunkId of children) {
usedChunks.add(chunkId);
}
} else if (doc.type === EntryTypes.CHUNK) {
allChunks.set(doc._id as DocumentID, doc._rev);
}
}
this._notice(
`Garbage Collection: Scanning completed. Total chunks: ${allChunks.size}, Used chunks: ${usedChunks.size}`,
"gc-scanning"
);
const unusedChunks = [...allChunks.keys()].filter((e) => !usedChunks.has(e));
this._notice(`Garbage Collection: Found ${unusedChunks.length} unused chunks to delete.`, "gc-scanning");
const deleteChunkDocs = unusedChunks.map(
(chunkId) =>
({
_id: chunkId,
_deleted: true,
_rev: allChunks.get(chunkId),
}) as EntryLeaf
);
const response = await localDatabase.bulkDocs(deleteChunkDocs);
const deletedCount = response.filter((e) => "ok" in e).length;
const gcEndTime = Date.now();
this._notice(
`Garbage Collection completed. Deleted chunks: ${deletedCount} / ${unusedChunks.length}. Time taken: ${(gcEndTime - gcStartTime) / 1000} seconds.`
);
// Send changes to remote
const r = await replicator.openOneShotReplication(this.settings, false, false, "pushOnly");
// Wait for replication to complete
if (!r) {
this._notice("Failed to start replication after Garbage Collection.");
return;
}
// Perform compaction
await this.compactDatabase();
this.clearHash();
}
}

View File

@@ -29,7 +29,7 @@ import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
import type ObsidianLiveSyncPlugin from "../../main.ts";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts";
// import { getPlatformName } from "../../lib/src/PlatformAPIs/obsidian/Environment.ts";
import type { LiveSyncCore } from "../../main.ts";
import { TrysteroReplicator } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../lib/src/common/types.ts";
@@ -130,7 +130,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
const getDB = () => this.getDB();
const getConfirm = () => this.confirm;
const getPlatform = () => this.getPlatform();
const getPlatform = () => this.services.API.getPlatform();
const env = {
get db() {
return getDB();
@@ -166,9 +166,6 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
throw e;
}
}
getPlatform(): string {
return getPlatformName();
}
onunload(): void {
removeP2PReplicatorInstance();
@@ -271,11 +268,11 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.handleOnSuspending(this._everyBeforeSuspendProcess.bind(this));
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
services.setting.handleSuspendExtraSync(this._allSuspendExtraSync.bind(this));
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSuspending.addHandler(this._everyBeforeSuspendProcess.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
services.setting.suspendExtraSync.addHandler(this._allSuspendExtraSync.bind(this));
}
}

View File

@@ -1,4 +1,4 @@
import { Menu, WorkspaceLeaf } from "obsidian";
import { Menu, WorkspaceLeaf } from "@/deps.ts";
import ReplicatorPaneComponent from "./P2PReplicatorPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { mount } from "svelte";

Submodule src/lib updated: da14678633...7c275d50ae

View File

@@ -23,7 +23,6 @@ import type { IObsidianModule } from "./modules/AbstractObsidianModule.ts";
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
import { ModuleFileAccessObsidian } from "./modules/coreObsidian/ModuleFileAccessObsidian.ts";
import { ModuleInputUIObsidian } from "./modules/coreObsidian/ModuleInputUIObsidian.ts";
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
import { ModuleCheckRemoteSize } from "./modules/essentialObsidian/ModuleCheckRemoteSize.ts";
@@ -68,33 +67,10 @@ import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleE
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
import type { LiveSyncManagers } from "./lib/src/managers/LiveSyncManagers.ts";
import { ObsidianServiceHub } from "./modules/services/ObsidianServices.ts";
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts";
// function throwShouldBeOverridden(): never {
// throw new Error("This function should be overridden by the module.");
// }
// const InterceptiveAll = Promise.resolve(true);
// const InterceptiveEvery = Promise.resolve(true);
// const InterceptiveAny = Promise.resolve(undefined);
/**
* All $prefixed functions are hooked by the modules. Be careful to call them directly.
* Please refer to the module's source code to understand the function.
* $$ : Completely overridden functions.
* $all : Process all modules and return all results.
* $every : Process all modules until the first failure.
* $any : Process all modules until the first success.
* $ : Other interceptive points. You should manually assign the module
* All of above performed on injectModules function.
*
* No longer used! See AppLifecycleService in Services.ts.
* For a while, just commented out some previously used code. (sorry, some are deleted...)
* 'Convention over configuration' was a lie for me. At least, very lack of refactor-ability.
*
* Still some modules are separated, and connected by `ThroughHole` class.
* However, it is not a good design. I am going to manage the modules in a more explicit way.
*/
import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts";
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts";
// import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts";
export default class ObsidianLiveSyncPlugin
extends Plugin
@@ -108,7 +84,7 @@ export default class ObsidianLiveSyncPlugin
/**
* The service hub for managing all services.
*/
_services: InjectableServiceHub = new ObsidianServiceHub(this);
_services: InjectableServiceHub<ServiceContext> = new ObsidianServiceHub(this);
get services() {
return this._services;
}
@@ -161,7 +137,6 @@ export default class ObsidianLiveSyncPlugin
new ModuleObsidianSettingsAsMarkdown(this, this),
new ModuleObsidianSettingDialogue(this, this),
new ModuleLog(this, this),
new ModuleInputUIObsidian(this, this),
new ModuleObsidianMenu(this, this),
new ModuleRebuilder(this),
new ModuleSetupObsidian(this, this),
@@ -184,17 +159,15 @@ export default class ObsidianLiveSyncPlugin
}
throw new Error(`Module ${constructor} not found or not loaded.`);
}
// injected = injectModules(this, [...this.modules, ...this.addOns] as ICoreModule[]);
// <-- Module System
// Following are plugged by the modules.
settings!: ObsidianLiveSyncSettings;
localDatabase!: LiveSyncLocalDB;
managers!: LiveSyncManagers;
simpleStore!: SimpleStore<CheckPointInfo>;
replicator!: LiveSyncAbstractReplicator;
confirm!: Confirm;
get confirm(): Confirm {
return this.services.UI.confirm;
}
storageAccess!: StorageAccess;
databaseFileAccess!: DatabaseFileAccess;
fileHandler!: ModuleFileHandler;
@@ -232,436 +205,6 @@ export default class ObsidianLiveSyncPlugin
syncStatus: "CLOSED" as DatabaseConnectingStatus,
});
// --> Events
/*
LifeCycle of the plugin
0. onunload (Obsidian Kicks.)
1. onLiveSyncLoad
2. (event) EVENT_PLUGIN_LOADED
3. $everyOnloadStart
-- Load settings
-- Open database
--
3. $everyOnloadAfterLoadSettings
4. $everyOnload
5. (addOns) onload
--
onLiveSyncReady
-- $everyOnLayoutReady
-- EVENT_LAYOUT_READY
(initializeDatabase)
-- $everyOnFirstInitialize
-- realizeSettingSyncMode
-- waitForReplicationOnce (if syncOnStart and not LiveSync)
-- scanStat (Not waiting for the result)
---
Finalization
0. onunload (Obsidian Kicks.)
1. onLiveSyncUnload
2. (event) EVENT_PLUGIN_UNLOADED
3. $allStartOnUnload
4. $allOnUnload
5. (addOns) onunload
6. localDatabase.onunload
7. replicator.closeReplication
8. localDatabase.close
9. (event) EVENT_PLATFORM_UNLOADED
*/
// $everyOnLayoutReady(): Promise<boolean> {
// //TODO: AppLifecycleService.onLayoutReady
// return InterceptiveEvery;
// }
// $everyOnFirstInitialize(): Promise<boolean> {
// //TODO: AppLifecycleService.onFirstInitialize
// return InterceptiveEvery;
// }
// Some Module should call this function to start the plugin.
// $$onLiveSyncReady(): Promise<false | undefined> {
// //TODO: AppLifecycleService.onLiveSyncReady
// throwShouldBeOverridden();
// }
// $$wireUpEvents(): void {
// //TODO: AppLifecycleService.wireUpEvents
// throwShouldBeOverridden();
// }
// $$onLiveSyncLoad(): Promise<void> {
// //TODO: AppLifecycleService.onLoad
// throwShouldBeOverridden();
// }
// $$onLiveSyncUnload(): Promise<void> {
// //TODO: AppLifecycleService.onAppUnload
// throwShouldBeOverridden();
// }
// $allScanStat(): Promise<boolean> {
// //TODO: AppLifecycleService.scanStartupIssues
// return InterceptiveAll;
// }
// $everyOnloadStart(): Promise<boolean> {
// //TODO: AppLifecycleService.onInitialise
// return InterceptiveEvery;
// }
// $everyOnloadAfterLoadSettings(): Promise<boolean> {
// //TODO: AppLifecycleService.onApplyStartupLoaded
// return InterceptiveEvery;
// }
// $everyOnload(): Promise<boolean> {
// //TODO: AppLifecycleService.onLoaded
// return InterceptiveEvery;
// }
// $anyHandlerProcessesFileEvent(item: FileEventItem): Promise<boolean | undefined> {
// //TODO: FileProcessingService.processFileEvent
// return InterceptiveAny;
// }
// $allStartOnUnload(): Promise<boolean> {
// //TODO: AppLifecycleService.onBeforeUnload
// return InterceptiveAll;
// }
// $allOnUnload(): Promise<boolean> {
// //TODO: AppLifecycleService.onUnload
// return InterceptiveAll;
// }
// $$openDatabase(): Promise<boolean> {
// // DatabaseService.openDatabase
// throwShouldBeOverridden();
// }
// $$realizeSettingSyncMode(): Promise<void> {
// // SettingService.realiseSetting
// throwShouldBeOverridden();
// }
// $$performRestart() {
// // AppLifecycleService.performRestart
// throwShouldBeOverridden();
// }
// $$clearUsedPassphrase(): void {
// // SettingService.clearUsedPassphrase
// throwShouldBeOverridden();
// }
// $$decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
// // SettingService.decryptSettings
// throwShouldBeOverridden();
// }
// $$adjustSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings> {
// // SettingService.adjustSettings
// throwShouldBeOverridden();
// }
// $$loadSettings(): Promise<void> {
// // SettingService.loadSettings
// throwShouldBeOverridden();
// }
// $$saveDeviceAndVaultName(): void {
// // SettingService.saveDeviceAndVaultName
// throwShouldBeOverridden();
// }
// $$saveSettingData(): Promise<void> {
// // SettingService.saveSettingData
// throwShouldBeOverridden();
// }
// $anyProcessOptionalFileEvent(path: FilePath): Promise<boolean | undefined> {
// // FileProcessingService.processOptionalFileEvent
// return InterceptiveAny;
// }
// $everyCommitPendingFileEvent(): Promise<boolean> {
// // FileProcessingService.commitPendingFileEvent
// return InterceptiveEvery;
// }
// ->
// $anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | undefined | "newer"> {
// return InterceptiveAny;
// }
// $$queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
// // ConflictEventManager.queueCheckForConflictIfOpen
// throwShouldBeOverridden();
// }
// $$queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
// // ConflictEventManager.queueCheckForConflict
// throwShouldBeOverridden();
// }
// $$waitForAllConflictProcessed(): Promise<boolean> {
// // ConflictEventManager.ensureAllConflictProcessed
// throwShouldBeOverridden();
// }
//<-- Conflict Check
// $anyProcessOptionalSyncFiles(doc: LoadedEntry): Promise<boolean | undefined> {
// // ReplicationService.processOptionalSyncFile
// return InterceptiveAny;
// }
// $anyProcessReplicatedDoc(doc: MetaEntry): Promise<boolean | undefined> {
// // ReplicationService.processReplicatedDocument
// return InterceptiveAny;
// }
//---> Sync
// $$parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): void {
// // ReplicationService.parseSynchroniseResult
// throwShouldBeOverridden();
// }
// $anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>): Promise<boolean | undefined> {
// // ReplicationService.processVirtualDocument
// return InterceptiveAny;
// }
// $everyBeforeRealizeSetting(): Promise<boolean> {
// // SettingEventManager.beforeRealiseSetting
// return InterceptiveEvery;
// }
// $everyAfterRealizeSetting(): Promise<boolean> {
// // SettingEventManager.onSettingRealised
// return InterceptiveEvery;
// }
// $everyRealizeSettingSyncMode(): Promise<boolean> {
// // SettingEventManager.onRealiseSetting
// return InterceptiveEvery;
// }
// $everyBeforeSuspendProcess(): Promise<boolean> {
// // AppLifecycleService.onSuspending
// return InterceptiveEvery;
// }
// $everyOnResumeProcess(): Promise<boolean> {
// // AppLifecycleService.onResuming
// return InterceptiveEvery;
// }
// $everyAfterResumeProcess(): Promise<boolean> {
// // AppLifecycleService.onResumed
// return InterceptiveEvery;
// }
// $$fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<TweakValues | false> {
// //TODO:TweakValueService.fetchRemotePreferred
// throwShouldBeOverridden();
// }
// $$checkAndAskResolvingMismatchedTweaks(preferred: Partial<TweakValues>): Promise<[TweakValues | boolean, boolean]> {
// //TODO:TweakValueService.checkAndAskResolvingMismatched
// throwShouldBeOverridden();
// }
// $$askResolvingMismatchedTweaks(preferredSource: TweakValues): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
// //TODO:TweakValueService.askResolvingMismatched
// throwShouldBeOverridden();
// }
// $$checkAndAskUseRemoteConfiguration(
// settings: RemoteDBSettings
// ): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
// // TweakValueService.checkAndAskUseRemoteConfiguration
// throwShouldBeOverridden();
// }
// $$askUseRemoteConfiguration(
// trialSetting: RemoteDBSettings,
// preferred: TweakValues
// ): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
// // TweakValueService.askUseRemoteConfiguration
// throwShouldBeOverridden();
// }
// $everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
// // ReplicationService.beforeReplicate
// return InterceptiveEvery;
// }
// $$canReplicate(showMessage: boolean = false): Promise<boolean> {
// // ReplicationService.isReplicationReady
// throwShouldBeOverridden();
// }
// $$replicate(showMessage: boolean = false): Promise<boolean | void> {
// // ReplicationService.replicate
// throwShouldBeOverridden();
// }
// $$replicateByEvent(showMessage: boolean = false): Promise<boolean | void> {
// // ReplicationService.replicateByEvent
// throwShouldBeOverridden();
// }
// $everyOnDatabaseInitialized(showingNotice: boolean): Promise<boolean> {
// // DatabaseEventService.onDatabaseInitialised
// throwShouldBeOverridden();
// }
// $$initializeDatabase(
// showingNotice: boolean = false,
// reopenDatabase = true,
// ignoreSuspending: boolean = false
// ): Promise<boolean> {
// // DatabaseEventService.initializeDatabase
// throwShouldBeOverridden();
// }
// $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
// // ReplicationService.checkConnectionFailure
// return InterceptiveAny;
// }
// $$replicateAllToServer(
// showingNotice: boolean = false,
// sendChunksInBulkDisabled: boolean = false
// ): Promise<boolean> {
// // RemoteService.replicateAllToRemote
// throwShouldBeOverridden();
// }
// $$replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
// // RemoteService.replicateAllFromRemote
// throwShouldBeOverridden();
// }
// Remote Governing
// $$markRemoteLocked(lockByClean: boolean = false): Promise<void> {
// // RemoteService.markLocked;
// throwShouldBeOverridden();
// }
// $$markRemoteUnlocked(): Promise<void> {
// // RemoteService.markUnlocked;
// throwShouldBeOverridden();
// }
// $$markRemoteResolved(): Promise<void> {
// // RemoteService.markResolved;
// throwShouldBeOverridden();
// }
// <-- Remote Governing
// $$isFileSizeExceeded(size: number): boolean {
// // VaultService.isFileSizeTooLarge
// throwShouldBeOverridden();
// }
// $$performFullScan(showingNotice?: boolean, ignoreSuspending?: boolean): Promise<void> {
// // VaultService.scanVault
// throwShouldBeOverridden();
// }
// $anyResolveConflictByUI(
// filename: FilePathWithPrefix,
// conflictCheckResult: diff_result
// ): Promise<boolean | undefined> {
// // ConflictService.resolveConflictByUserInteraction
// return InterceptiveAny;
// }
// $$resolveConflictByDeletingRev(
// path: FilePathWithPrefix,
// deleteRevision: string,
// subTitle = ""
// ): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
// // ConflictService.resolveByDeletingRevision
// throwShouldBeOverridden();
// }
// $$resolveConflict(filename: FilePathWithPrefix): Promise<void> {
// // ConflictService.resolveConflict
// throwShouldBeOverridden();
// }
// $anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
// // ConflictService.resolveByNewest
// throwShouldBeOverridden();
// }
// $$resetLocalDatabase(): Promise<void> {
// // DatabaseService.resetDatabase;
// throwShouldBeOverridden();
// }
// $$tryResetRemoteDatabase(): Promise<void> {
// // RemoteService.tryResetDatabase;
// throwShouldBeOverridden();
// }
// $$tryCreateRemoteDatabase(): Promise<void> {
// // RemoteService.tryCreateDatabase;
// throwShouldBeOverridden();
// }
// $$isIgnoredByIgnoreFiles(file: string | UXFileInfoStub): Promise<boolean> {
// // VaultService.isIgnoredByIgnoreFiles
// throwShouldBeOverridden();
// }
// $$isTargetFile(file: string | UXFileInfoStub, keepFileCheckList = false): Promise<boolean> {
// // VaultService.isTargetFile
// throwShouldBeOverridden();
// }
// $$askReload(message?: string) {
// // AppLifecycleService.askRestart
// throwShouldBeOverridden();
// }
// $$scheduleAppReload() {
// // AppLifecycleService.scheduleRestart
// throwShouldBeOverridden();
// }
//--- Setup
// $allSuspendAllSync(): Promise<boolean> {
// // SettingEventManager.suspendAllSync
// return InterceptiveAll;
// }
// $allSuspendExtraSync(): Promise<boolean> {
// // SettingEventManager.suspendExtraSync
// return InterceptiveAll;
// }
// $allAskUsingOptionalSyncFeature(opt: { enableFetch?: boolean; enableOverwrite?: boolean }): Promise<boolean> {
// // SettingEventManager.suggestOptionalFeatures
// throwShouldBeOverridden();
// }
// $anyConfigureOptionalSyncFeature(mode: string): Promise<void> {
// // SettingEventManager.enableOptionalFeature
// throwShouldBeOverridden();
// }
// $$showView(viewType: string): Promise<void> {
// // UIManager.showWindow //
// throwShouldBeOverridden();
// }
// For Development: Ensure reliability MORE AND MORE. May the this plug-in helps all of us.
// $everyModuleTest(): Promise<boolean> {
// return InterceptiveEvery;
// }
// $everyModuleTestMultiDevice(): Promise<boolean> {
// return InterceptiveEvery;
// }
// $$addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
// throwShouldBeOverridden();
// }
// _isThisModuleEnabled(): boolean {
// return true;
// }
// $anyGetAppId(): Promise<string | undefined> {
// // APIService.getAppId
// return InterceptiveAny;
// }
// Plug-in's overrideable functions
onload() {
void this.services.appLifecycle.onLoad();
}

View File

@@ -2,138 +2,6 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "oct
import type { LOG_LEVEL } from "../lib/src/common/types";
import type { LiveSyncCore } from "../main";
import { __$checkInstanceBinding } from "../lib/src/dev/checks";
// import { unique } from "octagonal-wheels/collection";
// import type { IObsidianModule } from "./AbstractObsidianModule.ts";
// import type {
// ICoreModuleBase,
// AllInjectableProps,
// AllExecuteProps,
// EveryExecuteProps,
// AnyExecuteProps,
// ICoreModule,
// } from "./ModuleTypes";
// function isOverridableKey(key: string): key is keyof ICoreModuleBase {
// return key.startsWith("$");
// }
// function isInjectableKey(key: string): key is keyof AllInjectableProps {
// return key.startsWith("$$");
// }
// function isAllExecuteKey(key: string): key is keyof AllExecuteProps {
// return key.startsWith("$all");
// }
// function isEveryExecuteKey(key: string): key is keyof EveryExecuteProps {
// return key.startsWith("$every");
// }
// function isAnyExecuteKey(key: string): key is keyof AnyExecuteProps {
// return key.startsWith("$any");
// }
/**
* All $prefixed functions are hooked by the modules. Be careful to call them directly.
* Please refer to the module's source code to understand the function.
* $$ : Completely overridden functions.
* $all : Process all modules and return all results.
* $every : Process all modules until the first failure.
* $any : Process all modules until the first success.
* $ : Other interceptive points. You should manually assign the module
* All of above performed on injectModules function.
*/
// export function injectModules<T extends ICoreModule>(target: T, modules: ICoreModule[]) {
// const allKeys = unique([
// ...Object.keys(Object.getOwnPropertyDescriptors(target)),
// ...Object.keys(Object.getOwnPropertyDescriptors(Object.getPrototypeOf(target))),
// ]).filter((e) => e.startsWith("$")) as (keyof ICoreModule)[];
// const moduleMap = new Map<string, IObsidianModule[]>();
// for (const module of modules) {
// for (const key of allKeys) {
// if (isOverridableKey(key)) {
// if (key in module) {
// const list = moduleMap.get(key) || [];
// if (typeof module[key] === "function") {
// module[key] = module[key].bind(module) as any;
// }
// list.push(module);
// moduleMap.set(key, list);
// }
// }
// }
// }
// Logger(`Injecting modules for ${target.constructor.name}`, LOG_LEVEL_VERBOSE);
// for (const key of allKeys) {
// const modules = moduleMap.get(key) || [];
// if (isInjectableKey(key)) {
// if (modules.length == 0) {
// throw new Error(`No module injected for ${key}. This is a fatal error.`);
// }
// target[key] = modules[0][key]! as any;
// Logger(`[${modules[0].constructor.name}]: Injected ${key} `, LOG_LEVEL_VERBOSE);
// } else if (isAllExecuteKey(key)) {
// const modules = moduleMap.get(key) || [];
// target[key] = async (...args: any) => {
// for (const module of modules) {
// try {
// //@ts-ignore
// await module[key]!(...args);
// } catch (ex) {
// Logger(`[${module.constructor.name}]: All handler for ${key} failed`, LOG_LEVEL_VERBOSE);
// Logger(ex, LOG_LEVEL_VERBOSE);
// }
// }
// return true;
// };
// for (const module of modules) {
// Logger(`[${module.constructor.name}]: Injected (All) ${key} `, LOG_LEVEL_VERBOSE);
// }
// } else if (isEveryExecuteKey(key)) {
// target[key] = async (...args: any) => {
// for (const module of modules) {
// try {
// //@ts-ignore:2556
// const ret = await module[key]!(...args);
// if (ret !== undefined && !ret) {
// // Failed then return that falsy value.
// return ret;
// }
// } catch (ex) {
// Logger(`[${module.constructor.name}]: Every handler for ${key} failed`);
// Logger(ex, LOG_LEVEL_VERBOSE);
// }
// }
// return true;
// };
// for (const module of modules) {
// Logger(`[${module.constructor.name}]: Injected (Every) ${key} `, LOG_LEVEL_VERBOSE);
// }
// } else if (isAnyExecuteKey(key)) {
// //@ts-ignore
// target[key] = async (...args: any[]) => {
// for (const module of modules) {
// try {
// //@ts-ignore:2556
// const ret = await module[key](...args);
// // If truly value returned, then return that value.
// if (ret) {
// return ret;
// }
// } catch (ex) {
// Logger(`[${module.constructor.name}]: Any handler for ${key} failed`);
// Logger(ex, LOG_LEVEL_VERBOSE);
// }
// }
// return false;
// };
// for (const module of modules) {
// Logger(`[${module.constructor.name}]: Injected (Any) ${key} `, LOG_LEVEL_VERBOSE);
// }
// } else {
// Logger(`No injected handler for ${key} `, LOG_LEVEL_VERBOSE);
// }
// }
// Logger(`Injected modules for ${target.constructor.name}`, LOG_LEVEL_VERBOSE);
// return true;
// }
export abstract class AbstractModule {
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {

View File

@@ -346,7 +346,7 @@ export class ModuleDatabaseFileAccess extends AbstractModule implements Database
return ret;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.test.handleTest(this._everyModuleTest.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.test.test.addHandler(this._everyModuleTest.bind(this));
}
}

View File

@@ -266,7 +266,10 @@ export class ModuleFileHandler extends AbstractModule {
// Check the file is not corrupted
// (Zero is a special case, may be created by some APIs and it might be acceptable).
if (docRead.size != 0 && docRead.size !== readAsBlob(docRead).size) {
this._log(`File ${path} seems to be corrupted! Writing prevented.`, LOG_LEVEL_NOTICE);
this._log(
`File ${path} seems to be corrupted! Writing prevented. (${docRead.size} != ${readAsBlob(docRead).size})`,
LOG_LEVEL_NOTICE
);
return false;
}
}
@@ -433,8 +436,8 @@ export class ModuleFileHandler extends AbstractModule {
);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.fileProcessing.handleProcessFileEvent(this._anyHandlerProcessesFileEvent.bind(this));
services.replication.handleProcessSynchroniseResult(this._anyProcessReplicatedDoc.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.fileProcessing.processFileEvent.addHandler(this._anyHandlerProcessesFileEvent.bind(this));
services.replication.processSynchroniseResult.addHandler(this._anyProcessReplicatedDoc.bind(this));
}
}

View File

@@ -39,8 +39,8 @@ export class ModuleLocalDatabaseObsidian extends AbstractModule {
return this.localDatabase != null && this.localDatabase.isReady;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.database.handleIsDatabaseReady(this._isDatabaseReady.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.database.handleOpenDatabase(this._openDatabase.bind(this));
services.database.isDatabaseReady.setHandler(this._isDatabaseReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.database.openDatabase.setHandler(this._openDatabase.bind(this));
}
}

View File

@@ -32,10 +32,10 @@ export class ModulePeriodicProcess extends AbstractModule {
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnUnload(this._allOnUnload.bind(this));
services.setting.handleBeforeRealiseSetting(this._everyBeforeRealizeSetting.bind(this));
services.setting.handleSettingRealised(this._everyAfterRealizeSetting.bind(this));
services.appLifecycle.handleOnSuspending(this._everyBeforeSuspendProcess.bind(this));
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
services.appLifecycle.onUnload.addHandler(this._allOnUnload.bind(this));
services.setting.onBeforeRealiseSetting.addHandler(this._everyBeforeRealizeSetting.bind(this));
services.setting.onSettingRealised.addHandler(this._everyAfterRealizeSetting.bind(this));
services.appLifecycle.onSuspending.addHandler(this._everyBeforeSuspendProcess.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
}

View File

@@ -18,6 +18,6 @@ export class ModulePouchDB extends AbstractModule {
return new PouchDB(name, optionPass);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.database.handleCreatePouchDBInstance(this._createPouchDBInstance.bind(this));
services.database.createPouchDBInstance.setHandler(this._createPouchDBInstance.bind(this));
}
}

View File

@@ -194,6 +194,32 @@ Please enable them from the settings screen after setup is complete.`,
// await this.askUseNewAdapter();
this.core.settings.isConfigured = true;
this.core.settings.notifyThresholdOfRemoteStorageSize = DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize;
if (this.core.settings.maxMTimeForReflectEvents > 0) {
const date = new Date(this.core.settings.maxMTimeForReflectEvents);
const ask = `Your settings restrict file reflection times to no later than ${date}.
**This is a recovery configuration.**
This operation should only be performed on an empty vault.
Are you sure you wish to proceed?`;
const PROCEED = "I understand, proceed";
const CANCEL = "Cancel operation";
const CLEARANDPROCEED = "Clear restriction and proceed";
const choices = [PROCEED, CLEARANDPROCEED, CANCEL] as const;
const ret = await this.core.confirm.askSelectStringDialogue(ask, choices, {
title: "Confirm restricted fetch",
defaultAction: CANCEL,
timeout: 0,
});
if (ret == CLEARANDPROCEED) {
this.core.settings.maxMTimeForReflectEvents = 0;
await this.core.saveSettings();
}
if (ret == CANCEL) {
return;
}
}
await this.suspendReflectingDatabase();
await this.services.setting.realiseSetting();
await this.resetLocalDatabase();
@@ -275,10 +301,10 @@ Please enable them from the settings screen after setup is complete.`,
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.database.handleResetDatabase(this._resetLocalDatabase.bind(this));
services.remote.handleTryResetDatabase(this._tryResetRemoteDatabase.bind(this));
services.remote.handleTryCreateDatabase(this._tryCreateRemoteDatabase.bind(this));
services.setting.handleSuspendAllSync(this._allSuspendAllSync.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.database.resetDatabase.setHandler(this._resetLocalDatabase.bind(this));
services.remote.tryResetDatabase.setHandler(this._tryResetRemoteDatabase.bind(this));
services.remote.tryCreateDatabase.setHandler(this._tryCreateRemoteDatabase.bind(this));
services.setting.suspendAllSync.addHandler(this._allSuspendAllSync.bind(this));
}
}

View File

@@ -329,19 +329,19 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.handleGetActiveReplicator(this._getReplicator.bind(this));
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
services.databaseEvents.handleDatabaseInitialised(this._everyOnDatabaseInitialized.bind(this));
services.databaseEvents.handleOnResetDatabase(this._everyOnResetDatabase.bind(this));
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.handleParseSynchroniseResult(this._parseReplicationResult.bind(this));
services.appLifecycle.handleOnSuspending(this._everyBeforeSuspendProcess.bind(this));
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
services.replication.handleIsReplicationReady(this._canReplicate.bind(this));
services.replication.handleReplicate(this._replicate.bind(this));
services.replication.handleReplicateByEvent(this._replicateByEvent.bind(this));
services.remote.handleReplicateAllToRemote(this._replicateAllToServer.bind(this));
services.remote.handleReplicateAllFromRemote(this._replicateAllFromServer.bind(this));
services.appLifecycle.reportUnresolvedMessages(this._reportUnresolvedMessages.bind(this));
services.replicator.getActiveReplicator.setHandler(this._getReplicator.bind(this));
services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.parseSynchroniseResult.setHandler(this._parseReplicationResult.bind(this));
services.appLifecycle.onSuspending.addHandler(this._everyBeforeSuspendProcess.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.replication.isReplicationReady.setHandler(this._canReplicate.bind(this));
services.replication.replicate.setHandler(this._replicate.bind(this));
services.replication.replicateByEvent.setHandler(this._replicateByEvent.bind(this));
services.remote.replicateAllToRemote.setHandler(this._replicateAllToServer.bind(this));
services.remote.replicateAllFromRemote.setHandler(this._replicateAllFromServer.bind(this));
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportUnresolvedMessages.bind(this));
}
}

View File

@@ -36,7 +36,7 @@ export class ModuleReplicatorCouchDB extends AbstractModule {
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
}

View File

@@ -13,6 +13,6 @@ export class ModuleReplicatorMinIO extends AbstractModule {
return Promise.resolve(false);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
}
}

View File

@@ -28,7 +28,7 @@ export class ModuleReplicatorP2P extends AbstractModule {
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.handleGetNewReplicator(this._anyNewReplicator.bind(this));
services.appLifecycle.handleOnResumed(this._everyAfterResumeProcess.bind(this));
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
}

View File

@@ -174,12 +174,12 @@ export class ModuleTargetFilter extends AbstractModule {
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.vault.handleMarkFileListPossiblyChanged(this._markFileListPossiblyChanged.bind(this));
services.path.handleId2Path(this._id2path.bind(this));
services.path.handlePath2Id(this._path2id.bind(this));
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.vault.handleIsFileSizeTooLarge(this._isFileSizeExceeded.bind(this));
services.vault.handleIsIgnoredByIgnoreFile(this._isIgnoredByIgnoreFiles.bind(this));
services.vault.handleIsTargetFile(this._isTargetFile.bind(this));
services.vault.markFileListPossiblyChanged.setHandler(this._markFileListPossiblyChanged.bind(this));
services.path.id2path.setHandler(this._id2path.bind(this));
services.path.path2id.setHandler(this._path2id.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.vault.isFileSizeTooLarge.setHandler(this._isFileSizeExceeded.bind(this));
services.vault.isIgnoredByIgnoreFile.setHandler(this._isIgnoredByIgnoreFiles.bind(this));
services.vault.isTargetFile.setHandler(this._isTargetFile.bind(this));
}
}

View File

@@ -318,6 +318,20 @@ export class ReplicateResultProcessor {
*/
async parseDocumentChange(change: PouchDB.Core.ExistingDocument<EntryDoc>) {
try {
if (isAnyNote(change)) {
const docMtime = change.mtime ?? 0;
const maxMTime = this.replicator.settings.maxMTimeForReflectEvents;
if (maxMTime > 0 && docMtime > maxMTime) {
const docPath = getPath(change);
this.log(
`Processing ${docPath} has been skipped due to modification time (${new Date(
docMtime * 1000
).toISOString()}) exceeding the limit`,
LOG_LEVEL_INFO
);
return;
}
}
// If the document is a virtual document, process it in the virtual document processor.
if (await this.services.replication.processVirtualDocument(change)) return;
// If the document is version info, check compatibility and return.
@@ -342,7 +356,7 @@ export class ReplicateResultProcessor {
return;
} finally {
// Remove from processing queue
this._processingChanges.remove(change);
this._processingChanges = this._processingChanges.filter((e) => e !== change);
this.triggerTakeSnapshot();
}
}

View File

@@ -75,8 +75,8 @@ export class ModuleConflictChecker extends AbstractModule {
}
);
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.conflict.handleQueueCheckForIfOpen(this._queueConflictCheckIfOpen.bind(this));
services.conflict.handleQueueCheckFor(this._queueConflictCheck.bind(this));
services.conflict.handleEnsureAllProcessed(this._waitForAllConflictProcessed.bind(this));
services.conflict.queueCheckForIfOpen.setHandler(this._queueConflictCheckIfOpen.bind(this));
services.conflict.queueCheckFor.setHandler(this._queueConflictCheck.bind(this));
services.conflict.ensureAllProcessed.setHandler(this._waitForAllConflictProcessed.bind(this));
}
}

View File

@@ -213,8 +213,8 @@ export class ModuleConflictResolver extends AbstractModule {
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.conflict.handleResolveByDeletingRevision(this._resolveConflictByDeletingRev.bind(this));
services.conflict.handleResolve(this._resolveConflict.bind(this));
services.conflict.handleResolveByNewest(this._anyResolveConflictByNewest.bind(this));
services.conflict.resolveByDeletingRevision.setHandler(this._resolveConflictByDeletingRev.bind(this));
services.conflict.resolve.setHandler(this._resolveConflict.bind(this));
services.conflict.resolveByNewest.setHandler(this._anyResolveConflictByNewest.bind(this));
}
}

View File

@@ -9,10 +9,11 @@ import {
} from "../../lib/src/common/types.ts";
import { AbstractModule } from "../AbstractModule.ts";
import type { LiveSyncCore } from "../../main.ts";
import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog.ts";
import FetchEverything from "../features/SetupWizard/dialogs/FetchEverything.svelte";
import RebuildEverything from "../features/SetupWizard/dialogs/RebuildEverything.svelte";
import { extractObject } from "octagonal-wheels/object";
import { SvelteDialogManagerBase } from "@/lib/src/UI/svelteDialog.ts";
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase.ts";
export class ModuleRedFlag extends AbstractModule {
async isFlagFileExist(path: string) {
@@ -52,7 +53,10 @@ export class ModuleRedFlag extends AbstractModule {
await this.deleteFlagFile(FlagFilesOriginal.FETCH_ALL);
await this.deleteFlagFile(FlagFilesHumanReadable.FETCH_ALL);
}
dialogManager = new SvelteDialogManager(this.core);
// dialogManager = new SvelteDialogManagerBase(this.core);
get dialogManager(): SvelteDialogManagerBase<ServiceContext> {
return this.core.services.UI.dialogManager;
}
/**
* Adjust setting to remote if needed.
@@ -322,6 +326,6 @@ export class ModuleRedFlag extends AbstractModule {
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
super.onBindFunction(core, services);
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
}
}

View File

@@ -15,8 +15,8 @@ export class ModuleRemoteGovernor extends AbstractModule {
return await this.core.replicator.markRemoteResolved(this.settings);
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.remote.handleMarkLocked(this._markRemoteLocked.bind(this));
services.remote.handleMarkUnlocked(this._markRemoteUnlocked.bind(this));
services.remote.handleMarkResolved(this._markRemoteResolved.bind(this));
services.remote.markLocked.setHandler(this._markRemoteLocked.bind(this));
services.remote.markUnlocked.setHandler(this._markRemoteUnlocked.bind(this));
services.remote.markResolved.setHandler(this._markRemoteResolved.bind(this));
}
}

View File

@@ -285,11 +285,15 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.tweakValue.handleFetchRemotePreferred(this._fetchRemotePreferredTweakValues.bind(this));
services.tweakValue.handleCheckAndAskResolvingMismatched(this._checkAndAskResolvingMismatchedTweaks.bind(this));
services.tweakValue.handleAskResolvingMismatched(this._askResolvingMismatchedTweaks.bind(this));
services.tweakValue.handleCheckAndAskUseRemoteConfiguration(this._checkAndAskUseRemoteConfiguration.bind(this));
services.tweakValue.handleAskUseRemoteConfiguration(this._askUseRemoteConfiguration.bind(this));
services.replication.handleCheckConnectionFailure(this._anyAfterConnectCheckFailed.bind(this));
services.tweakValue.fetchRemotePreferred.setHandler(this._fetchRemotePreferredTweakValues.bind(this));
services.tweakValue.checkAndAskResolvingMismatched.setHandler(
this._checkAndAskResolvingMismatchedTweaks.bind(this)
);
services.tweakValue.askResolvingMismatched.setHandler(this._askResolvingMismatchedTweaks.bind(this));
services.tweakValue.checkAndAskUseRemoteConfiguration.setHandler(
this._checkAndAskUseRemoteConfiguration.bind(this)
);
services.tweakValue.askUseRemoteConfiguration.setHandler(this._askUseRemoteConfiguration.bind(this));
services.replication.checkConnectionFailure.addHandler(this._anyAfterConnectCheckFailed.bind(this));
}
}

View File

@@ -1,4 +1,4 @@
import { TFile, TFolder, type ListedFiles } from "obsidian";
import { TFile, TFolder, type ListedFiles } from "@/deps.ts";
import { SerializedFileAccess } from "./storageLib/SerializedFileAccess";
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
@@ -386,11 +386,11 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
super(plugin, core);
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.vault.handleIsStorageInsensitive(this._isStorageInsensitive.bind(this));
services.setting.handleShouldCheckCaseInsensitively(this._shouldCheckCaseInsensitive.bind(this));
services.appLifecycle.handleFirstInitialise(this._everyOnFirstInitialize.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.fileProcessing.handleCommitPendingFileEvents(this._everyCommitPendingFileEvent.bind(this));
services.vault.isStorageInsensitive.setHandler(this._isStorageInsensitive.bind(this));
services.setting.shouldCheckCaseInsensitively.setHandler(this._shouldCheckCaseInsensitive.bind(this));
services.appLifecycle.onFirstInitialise.addHandler(this._everyOnFirstInitialize.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.fileProcessing.commitPendingFileEvents.addHandler(this._everyCommitPendingFileEvent.bind(this));
}
}

View File

@@ -1,4 +1,4 @@
import { ButtonComponent } from "obsidian";
import { ButtonComponent } from "@/deps.ts";
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";

View File

@@ -169,7 +169,7 @@ export class SerializedFileAccess {
getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null {
//@ts-ignore
return app.vault.getAbstractFileByPathInsensitive(path);
return this.app.vault.getAbstractFileByPathInsensitive(path);
}
getAbstractFileByPath(path: FilePath | string): TAbstractFile | null {

View File

@@ -243,6 +243,9 @@ export class StorageEventManagerObsidian extends StorageEventManager {
async appendQueue(params: FileEvent[], ctx?: any) {
if (!this.core.settings.isConfigured) return;
if (this.core.settings.suspendFileWatching) return;
if (this.core.settings.maxMTimeForReflectEvents > 0) {
return;
}
this.core.services.vault.markFileListPossiblyChanged();
// Flag up to be reload
for (const param of params) {
@@ -497,7 +500,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
if (fei.type == "CREATE" || fei.type == "CHANGED") {
// 3.2. If true, set the queue, and wait for the waiting, or until timeout
// (since is copied from previous waiting if exists to limit the maximum wait time)
console.warn(`Since:`, previous?.since);
// console.warn(`Since:`, previous?.since);
const info = this._addWaiting(waitingKey, fei, previous?.since);
waitPromise = info.canProceed.promise;
} else if (fei.type == "DELETE") {
@@ -531,7 +534,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
"storage-event-manager-snapshot"
);
if (snapShot && Array.isArray(snapShot) && snapShot.length > 0) {
console.warn(`Restoring snapshot: ${snapShot.length} items`);
// console.warn(`Restoring snapshot: ${snapShot.length} items`);
Logger(`Restoring storage operation snapshot: ${snapShot.length} items`, LOG_LEVEL_VERBOSE);
// Restore the snapshot
// Note: Mark all items as skipBatchWait to prevent apply the off-line batch saving.

View File

@@ -1,6 +1,6 @@
import { unique } from "octagonal-wheels/collection";
import { throttle } from "octagonal-wheels/function";
import { eventHub } from "../../common/events.ts";
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts";
import { BASE_IS_NEW, compareFileFreshness, EVEN, getPath, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
import {
type FilePathWithPrefixLC,
@@ -13,6 +13,7 @@ import {
LOG_LEVEL_INFO,
LOG_LEVEL_DEBUG,
type UXFileInfoStub,
type LOG_LEVEL,
} from "../../lib/src/common/types.ts";
import { isAnyNote } from "../../lib/src/common/utils.ts";
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
@@ -21,30 +22,43 @@ import { withConcurrency } from "octagonal-wheels/iterable/map";
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
import type { LiveSyncCore } from "../../main.ts";
export class ModuleInitializerFile extends AbstractModule {
private _detectedErrors = new Set<string>();
private logDetectedError(message: string, logLevel: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) {
this._detectedErrors.add(message);
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
this._log(message, logLevel, key);
}
private resetDetectedError(message: string) {
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
this._detectedErrors.delete(message);
}
private async _performFullScan(showingNotice?: boolean, ignoreSuspending: boolean = false): Promise<boolean> {
this._log("Opening the key-value database", LOG_LEVEL_VERBOSE);
const isInitialized = (await this.core.kvDB.get<boolean>("initialized")) || false;
// synchronize all files between database and storage.
const ERR_NOT_CONFIGURED =
"LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.";
if (!this.settings.isConfigured) {
if (showingNotice) {
this._log(
"LiveSync is not configured yet. Synchronising between the storage and the local database is now prevented.",
LOG_LEVEL_NOTICE,
"syncAll"
);
}
this.logDetectedError(ERR_NOT_CONFIGURED, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll");
return false;
}
this.resetDetectedError(ERR_NOT_CONFIGURED);
const ERR_SUSPENDING =
"Now suspending file watching. Synchronising between the storage and the local database is now prevented.";
if (!ignoreSuspending && this.settings.suspendFileWatching) {
if (showingNotice) {
this._log(
"Now suspending file watching. Synchronising between the storage and the local database is now prevented.",
LOG_LEVEL_NOTICE,
"syncAll"
);
}
this.logDetectedError(ERR_SUSPENDING, showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "syncAll");
return false;
}
const MSG_IN_REMEDIATION = `Started in remediation Mode! (Max mtime for reflect events is set). Synchronising between the storage and the local database is now prevented.`;
this.resetDetectedError(ERR_SUSPENDING);
if (this.settings.maxMTimeForReflectEvents > 0) {
this.logDetectedError(MSG_IN_REMEDIATION, LOG_LEVEL_NOTICE, "syncAll");
return false;
}
this.resetDetectedError(MSG_IN_REMEDIATION);
if (showingNotice) {
this._log("Initializing", LOG_LEVEL_NOTICE, "syncAll");
@@ -383,10 +397,12 @@ export class ModuleInitializerFile extends AbstractModule {
if (this.localDatabase.isReady) {
await this.services.vault.scanVault(showingNotice, ignoreSuspending);
}
const ERR_INITIALISATION_FAILED = `Initializing database has been failed on some module!`;
if (!(await this.services.databaseEvents.onDatabaseInitialised(showingNotice))) {
this._log(`Initializing database has been failed on some module!`, LOG_LEVEL_NOTICE);
this.logDetectedError(ERR_INITIALISATION_FAILED, LOG_LEVEL_NOTICE);
return false;
}
this.resetDetectedError(ERR_INITIALISATION_FAILED);
this.services.appLifecycle.markIsReady();
// run queued event once.
await this.services.fileProcessing.commitPendingFileEvents();
@@ -396,8 +412,12 @@ export class ModuleInitializerFile extends AbstractModule {
return false;
}
}
private _reportDetectedErrors(): Promise<string[]> {
return Promise.resolve(Array.from(this._detectedErrors));
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.databaseEvents.handleInitialiseDatabase(this._initializeDatabase.bind(this));
services.vault.handleScanVault(this._performFullScan.bind(this));
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportDetectedErrors.bind(this));
services.databaseEvents.initialiseDatabase.setHandler(this._initializeDatabase.bind(this));
services.vault.scanVault.setHandler(this._performFullScan.bind(this));
}
}

View File

@@ -4,11 +4,14 @@ import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { AbstractModule } from "../AbstractModule.ts";
import type { LiveSyncCore } from "../../main.ts";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import type { InjectableServiceHub } from "@/lib/src/services/InjectableServices.ts";
import type { ObsidianDatabaseService } from "../services/ObsidianServices.ts";
export class ModuleKeyValueDB extends AbstractModule {
tryCloseKvDB() {
async tryCloseKvDB() {
try {
this.core.kvDB?.close();
await this.core.kvDB?.close();
return true;
} catch (e) {
this._log("Failed to close KeyValueDB", LOG_LEVEL_VERBOSE);
@@ -19,7 +22,7 @@ export class ModuleKeyValueDB extends AbstractModule {
async openKeyValueDB(): Promise<boolean> {
await delay(10);
try {
this.tryCloseKvDB();
await this.tryCloseKvDB();
await delay(10);
await yieldMicrotask();
this.core.kvDB = await OpenKeyValueDatabase(this.services.vault.getVaultName() + "-livesync-kv");
@@ -33,12 +36,12 @@ export class ModuleKeyValueDB extends AbstractModule {
}
return true;
}
_onDBUnload(db: LiveSyncLocalDB) {
if (this.core.kvDB) this.core.kvDB.close();
async _onDBUnload(db: LiveSyncLocalDB) {
if (this.core.kvDB) await this.core.kvDB.close();
return Promise.resolve(true);
}
_onDBClose(db: LiveSyncLocalDB) {
if (this.core.kvDB) this.core.kvDB.close();
async _onDBClose(db: LiveSyncLocalDB) {
if (this.core.kvDB) await this.core.kvDB.close();
return Promise.resolve(true);
}
@@ -50,32 +53,34 @@ export class ModuleKeyValueDB extends AbstractModule {
return Promise.resolve(true);
}
_getSimpleStore<T>(kind: string) {
const getDB = () => this.core.kvDB;
const prefix = `${kind}-`;
return {
get: async (key: string): Promise<T> => {
return await this.core.kvDB.get(`${prefix}${key}`);
return await getDB().get(`${prefix}${key}`);
},
set: async (key: string, value: any): Promise<void> => {
await this.core.kvDB.set(`${prefix}${key}`, value);
await getDB().set(`${prefix}${key}`, value);
},
delete: async (key: string): Promise<void> => {
await this.core.kvDB.del(`${prefix}${key}`);
await getDB().del(`${prefix}${key}`);
},
keys: async (
from: string | undefined,
to: string | undefined,
count?: number | undefined
): Promise<string[]> => {
const ret = this.core.kvDB.keys(
const ret = await getDB().keys(
IDBKeyRange.bound(`${prefix}${from || ""}`, `${prefix}${to || ""}`),
count
);
return (await ret)
return ret
.map((e) => e.toString())
.filter((e) => e.startsWith(prefix))
.map((e) => e.substring(prefix.length));
},
};
db: Promise.resolve(getDB()),
} satisfies SimpleStore<T>;
}
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.openKeyValueDB();
@@ -98,12 +103,12 @@ export class ModuleKeyValueDB extends AbstractModule {
}
return true;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.databaseEvents.handleOnUnloadDatabase(this._onDBUnload.bind(this));
services.databaseEvents.handleOnCloseDatabase(this._onDBClose.bind(this));
services.databaseEvents.handleOnDatabaseInitialisation(this._everyOnInitializeDatabase.bind(this));
services.databaseEvents.handleOnResetDatabase(this._everyOnResetDatabase.bind(this));
services.database.handleOpenSimpleStore(this._getSimpleStore.bind(this));
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.databaseEvents.onUnloadDatabase.addHandler(this._onDBUnload.bind(this));
services.databaseEvents.onCloseDatabase.addHandler(this._onDBClose.bind(this));
services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
(services.database as ObsidianDatabaseService).openSimpleStore.setHandler(this._getSimpleStore.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
}
}

View File

@@ -355,7 +355,7 @@ export class ModuleMigration extends AbstractModule {
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
super.onBindFunction(core, services);
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
services.appLifecycle.handleFirstInitialise(this._everyOnFirstInitialize.bind(this));
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onFirstInitialise.addHandler(this._everyOnFirstInitialize.bind(this));
}
}

View File

@@ -128,7 +128,7 @@ export class ModuleCheckRemoteSize extends AbstractObsidianModule {
return Promise.resolve(true);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnScanningStartupIssues(this._allScanStat.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.onScanningStartupIssues.addHandler(this._allScanStat.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -312,19 +312,34 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
private _reportUnresolvedMessages(): Promise<string[]> {
private _reportUnresolvedMessages(): Promise<(string | Error)[]> {
return Promise.resolve([...this._previousErrors]);
}
private _getAppVersion(): string {
const navigatorString = globalThis.navigator?.userAgent ?? "";
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
if (match && match.length >= 2) {
return match[1];
}
return "0.0.0";
}
private _getPluginVersion(): string {
return this.plugin.manifest.version;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
services.API.handleGetCustomFetchHandler(this._customFetchHandler.bind(this));
services.API.handleIsLastPostFailedDueToPayloadSize(this._getLastPostFailedBySize.bind(this));
services.remote.handleConnect(this._connectRemoteCouchDB.bind(this));
services.API.handleIsMobile(this._isMobile.bind(this));
services.vault.handleGetVaultName(this._getVaultName.bind(this));
services.vault.handleVaultName(this._vaultName.bind(this));
services.vault.handleGetActiveFilePath(this._getActiveFilePath.bind(this));
services.API.handleGetAppID(this._anyGetAppId.bind(this));
services.appLifecycle.reportUnresolvedMessages(this._reportUnresolvedMessages.bind(this));
services.API.getCustomFetchHandler.setHandler(this._customFetchHandler.bind(this));
services.API.isLastPostFailedDueToPayloadSize.setHandler(this._getLastPostFailedBySize.bind(this));
services.remote.connect.setHandler(this._connectRemoteCouchDB.bind(this));
services.API.isMobile.setHandler(this._isMobile.bind(this));
services.vault.getVaultName.setHandler(this._getVaultName.bind(this));
services.vault.vaultName.setHandler(this._vaultName.bind(this));
services.vault.getActiveFilePath.setHandler(this._getActiveFilePath.bind(this));
services.API.getAppID.setHandler(this._anyGetAppId.bind(this));
services.API.getAppVersion.setHandler(this._getAppVersion.bind(this));
services.API.getPluginVersion.setHandler(this._getPluginVersion.bind(this));
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportUnresolvedMessages.bind(this));
}
}

View File

@@ -67,6 +67,11 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
//@ts-ignore
if (!window.CodeMirrorAdapter) {
this._log("CodeMirrorAdapter is not available");
return;
}
//@ts-ignore
window.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
_this.app.commands.executeCommandById("editor:save-file");
@@ -239,10 +244,10 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
}
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.handlePerformRestart(this._performRestart.bind(this));
services.appLifecycle.handleAskRestart(this._askReload.bind(this));
services.appLifecycle.handleScheduleRestart(this._scheduleAppReload.bind(this));
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.performRestart.setHandler(this._performRestart.bind(this));
services.appLifecycle.askRestart.setHandler(this._askReload.bind(this));
services.appLifecycle.scheduleRestart.setHandler(this._scheduleAppReload.bind(this));
}
}

View File

@@ -131,8 +131,8 @@ export class ModuleObsidianMenu extends AbstractObsidianModule {
}
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.API.handleShowWindow(this._showView.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.API.showWindow.setHandler(this._showView.bind(this));
}
}

View File

@@ -12,7 +12,7 @@ export class ModuleExtraSyncObsidian extends AbstractObsidianModule {
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.setting.handleGetDeviceAndVaultName(this._getDeviceAndVaultName.bind(this));
services.setting.handleSetDeviceAndVaultName(this._setDeviceAndVaultName.bind(this));
services.setting.getDeviceAndVaultName.setHandler(this._getDeviceAndVaultName.bind(this));
services.setting.setDeviceAndVaultName.setHandler(this._setDeviceAndVaultName.bind(this));
}
}

View File

@@ -157,10 +157,10 @@ export class ModuleDev extends AbstractObsidianModule {
return this.testDone();
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
services.test.handleTest(this._everyModuleTest.bind(this));
services.test.handleAddTestResult(this._addTestResult.bind(this));
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.test.test.addHandler(this._everyModuleTest.bind(this));
services.test.addTestResult.setHandler(this._addTestResult.bind(this));
}
}

View File

@@ -441,6 +441,6 @@ Line4:D`;
return Promise.resolve(true);
}
onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.test.handleTestMultiDevice(this._everyModuleTestMultiDevice.bind(this));
services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this));
}
}

View File

@@ -4,7 +4,7 @@ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-w
import { eventHub } from "../../common/events";
import { getWebCrypto } from "../../lib/src/mods.ts";
import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex";
import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
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";
@@ -581,8 +581,8 @@ ABCDEFGHIJKLMNOPQRSTUVWXYZ`;
return this.testDone();
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.handleBeforeReplicate(this._everyBeforeReplicate.bind(this));
services.test.handleTestMultiDevice(this._everyModuleTestMultiDevice.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this));
services.test.testMultiDevice.addHandler(this._everyModuleTestMultiDevice.bind(this));
}
}

View File

@@ -1,4 +1,4 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { ItemView, WorkspaceLeaf } from "@/deps.ts";
import TestPaneComponent from "./TestPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import type { ModuleDev } from "../ModuleDev.ts";

View File

@@ -90,10 +90,8 @@ export class ConflictResolveModal extends Modal {
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
const date2 =
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
div2.setHTMLUnsafe(`
<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>
`);
div2.innerHTML = `<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>`;
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
).style.marginRight = "4px";
@@ -109,11 +107,10 @@ export class ConflictResolveModal extends Modal {
e.addEventListener("click", () => this.sendResponse(CANCELLED))
).style.marginRight = "4px";
diff = diff.replace(/\n/g, "<br>");
// div.innerHTML = diff;
if (diff.length > 100 * 1024) {
div.setText("(Too large diff to display)");
div.innerText = "(Too large diff to display)";
} else {
div.setHTMLUnsafe(diff);
div.innerHTML = diff;
}
}

View File

@@ -1,4 +1,4 @@
import { WorkspaceLeaf } from "obsidian";
import { WorkspaceLeaf } from "@/deps.ts";
import LogPaneComponent from "./LogPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";

View File

@@ -20,6 +20,6 @@ export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
void this.services.API.showWindow(VIEW_TYPE_GLOBAL_HISTORY);
}
onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -131,36 +131,42 @@ export class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
async _allScanStat(): Promise<boolean> {
const notes: { path: string; mtime: number }[] = [];
this._log(`Checking conflicted files`, LOG_LEVEL_VERBOSE);
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ path: getPath(doc), mtime: doc.mtime });
}
if (notes.length > 0) {
this.core.confirm.askInPopup(
`conflicting-detected-on-safety`,
`Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(() => this.allConflictCheck());
});
}
);
this._log(
`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`,
LOG_LEVEL_VERBOSE
);
for (const note of notes) {
this._log(`Conflicted: ${note.path}`);
try {
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
if (!("_conflicts" in doc)) continue;
notes.push({ path: getPath(doc), mtime: doc.mtime });
}
} else {
this._log(`There are no conflicting files`, LOG_LEVEL_VERBOSE);
if (notes.length > 0) {
this.core.confirm.askInPopup(
`conflicting-detected-on-safety`,
`Some files have been left conflicted! Press {HERE} to resolve them, or you can do it later by "Pick a file to resolve conflict`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(() => this.allConflictCheck());
});
}
);
this._log(
`Some files have been left conflicted! Please resolve them by "Pick a file to resolve conflict". The list is written in the log.`,
LOG_LEVEL_VERBOSE
);
for (const note of notes) {
this._log(`Conflicted: ${note.path}`);
}
} else {
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(e, LOG_LEVEL_VERBOSE);
return false;
}
return true;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnScanningStartupIssues(this._allScanStat.bind(this));
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.conflict.handleResolveByUserInteraction(this._anyResolveConflictByUI.bind(this));
services.appLifecycle.onScanningStartupIssues.addHandler(this._allScanStat.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.conflict.resolveByUserInteraction.addHandler(this._anyResolveConflictByUI.bind(this));
}
}

View File

@@ -15,7 +15,6 @@ import {
hiddenFilesEventCount,
hiddenFilesProcessingCount,
type LogEntry,
logStore,
logMessages,
} from "../../lib/src/mock_and_interop/stores.ts";
import { eventHub } from "../../lib/src/hub/hub.ts";
@@ -28,7 +27,6 @@ import {
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
import { addIcon, normalizePath, Notice } from "../../deps.ts";
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
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";
@@ -45,24 +43,21 @@ import {
// This module cannot be a core module because it depends on the Obsidian UI.
// DI the log again.
const recentLogEntries = reactiveSource<LogEntry[]>([]);
setGlobalLogFunction((message: any, level?: number, key?: string) => {
const messageX =
message instanceof Error
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
: message;
const entry = { message: messageX, level, key } as LogEntry;
logStore.enqueue(entry);
recentLogEntries.value = [...recentLogEntries.value, entry];
});
let recentLogs = [] as string[];
// Recent log splicer
const recentLogProcessor = new QueueProcessor(
(logs: string[]) => {
recentLogs = [...recentLogs, ...logs].splice(-200);
logMessages.value = recentLogs;
},
{ batchSize: 25, delay: 10, suspended: false, concurrentLimit: 1 }
).resumePipeLine();
function addLog(log: string) {
recentLogs = [...recentLogs, log].splice(-200);
logMessages.value = recentLogs;
}
// logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
const showDebugLog = false;
@@ -373,16 +368,12 @@ export class ModuleLog extends AbstractObsidianModule {
return Promise.resolve(true);
}
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
logStore
.pipeTo(
new QueueProcessor((logs) => logs.forEach((e) => this.__addLog(e.message, e.level, e.key)), {
suspended: false,
batchSize: 20,
concurrentLimit: 1,
delay: 0,
})
)
.startPipeline();
recentLogEntries.onChanged((entries) => {
if (entries.value.length === 0) return;
const newEntries = [...entries.value];
recentLogEntries.value = [];
newEntries.forEach((e) => this.__addLog(e.message, e.level, e.key));
});
eventHub.onEvent(EVENT_FILE_RENAMED, (data) => {
void this.setFileStatus();
});
@@ -464,7 +455,7 @@ export class ModuleLog extends AbstractObsidianModule {
if (this.settings?.writeLogToTheFile) {
this.writeLogToTheFile(now, vaultName, newMessage);
}
recentLogProcessor.enqueue(newMessage);
addLog(newMessage);
this.logLines.push({ ttl: now.getTime() + 3000, message: newMessage });
if (level >= LOG_LEVEL_NOTICE) {
@@ -504,9 +495,9 @@ export class ModuleLog extends AbstractObsidianModule {
}
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.handleOnSettingLoaded(this._everyOnloadAfterLoadSettings.bind(this));
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.appLifecycle.handleOnBeforeUnload(this._allStartOnUnload.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.appLifecycle.onBeforeUnload.addHandler(this._allStartOnUnload.bind(this));
}
}

View File

@@ -1,4 +1,4 @@
import { type TFile } from "obsidian";
import { type TFile } from "@/deps.ts";
import { eventHub } from "../../common/events.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "../../common/obsidianEvents.ts";
import type { FilePathWithPrefix, LoadedEntry, DocumentID } from "../../lib/src/common/types.ts";
@@ -52,6 +52,6 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
}
}
onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -14,7 +14,7 @@ import {
import { LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT } from "octagonal-wheels/common/logger";
import { $msg, setLang } from "../../lib/src/common/i18n.ts";
import { isCloudantURI } from "../../lib/src/pouchdb/utils_couchdb.ts";
import { getLanguage } from "obsidian";
import { getLanguage } from "@/deps.ts";
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../../lib/src/common/rosetta.ts";
import { decryptString, encryptString } from "@/lib/src/encryption/stringEncryption.ts";
import type { LiveSyncCore } from "../../main.ts";
@@ -323,13 +323,13 @@ export class ModuleObsidianSettings extends AbstractObsidianModule {
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
super.onBindFunction(core, services);
services.appLifecycle.handleLayoutReady(this._everyOnLayoutReady.bind(this));
services.setting.handleClearUsedPassphrase(this._clearUsedPassphrase.bind(this));
services.setting.handleDecryptSettings(this._decryptSettings.bind(this));
services.setting.handleAdjustSettings(this._adjustSettings.bind(this));
services.setting.handleLoadSettings(this._loadSettings.bind(this));
services.setting.handleCurrentSettings(this._currentSettings.bind(this));
services.setting.handleSaveDeviceAndVaultName(this._saveDeviceAndVaultName.bind(this));
services.setting.handleSaveSettingData(this._saveSettingData.bind(this));
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.setting.clearUsedPassphrase.setHandler(this._clearUsedPassphrase.bind(this));
services.setting.decryptSettings.setHandler(this._decryptSettings.bind(this));
services.setting.adjustSettings.setHandler(this._adjustSettings.bind(this));
services.setting.loadSettings.setHandler(this._loadSettings.bind(this));
services.setting.currentSettings.setHandler(this._currentSettings.bind(this));
services.setting.saveDeviceAndVaultName.setHandler(this._saveDeviceAndVaultName.bind(this));
services.setting.saveSettingData.setHandler(this._saveSettingData.bind(this));
}
}

View File

@@ -243,6 +243,6 @@ We can perform a command in this file.
}
}
onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -30,6 +30,6 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -195,6 +195,6 @@ export class ModuleSetupObsidian extends AbstractObsidianModule {
// }
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
}
}

View File

@@ -6,7 +6,7 @@ import {
ButtonComponent,
type TextAreaComponent,
type ValueComponent,
} from "obsidian";
} from "@/deps.ts";
import { unique } from "octagonal-wheels/collection";
import {
LEVEL_ADVANCED,

View File

@@ -1,4 +1,4 @@
import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events.ts";
import { LOG_LEVEL_NOTICE, Logger } from "../../../lib/src/common/logger.ts";
import { FlagFilesHumanReadable, FLAGMD_REDFLAG } from "../../../lib/src/common/types.ts";
import { fireAndForget } from "../../../lib/src/common/utils.ts";
@@ -187,92 +187,106 @@ export function paneMaintenance(
)
.addOnUpdate(this.onlyOnMinIO);
});
void addPanel(paneEl, "Garbage Collection (Beta2)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => {
void addPanel(paneEl, "Garbage Collection V3 (Beta)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => {
new Setting(paneEl)
.setName("Scan garbage")
.setDesc("Scan for garbage chunks in the database.")
.setName("Perform Garbage Collection")
.setDesc("Perform Garbage Collection to remove unused chunks and reduce database size.")
.addButton((button) =>
button
.setButtonText("Scan")
// .setWarning()
.setButtonText("Perform Garbage Collection")
.setDisabled(false)
.onClick(async () => {
await this.plugin
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
?.trackChanges(false, true);
})
)
.addButton((button) =>
button.setButtonText("Rescan").onClick(async () => {
await this.plugin
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
?.trackChanges(true, true);
})
);
new Setting(paneEl)
.setName("Collect garbage")
.setDesc("Remove all unused chunks from the local database.")
.addButton((button) =>
button
.setButtonText("Collect")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.plugin
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
?.performGC(true);
})
);
new Setting(paneEl)
.setName("Commit File Deletion")
.setDesc("Completely delete all deleted documents from the local database.")
.addButton((button) =>
button
.setButtonText("Delete")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.plugin
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
?.commitFileDeletion();
.onClick(() => {
this.closeSetting();
eventHub.emitEvent(EVENT_REQUEST_PERFORM_GC_V3);
})
);
});
void addPanel(paneEl, "Garbage Collection (Old and Experimental)", (e) => e, this.onlyOnP2POrCouchDB).then(
(paneEl) => {
new Setting(paneEl)
.setName("Remove all orphaned chunks")
.setDesc("Remove all orphaned chunks from the local database.")
.addButton((button) =>
button
.setButtonText("Remove")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.plugin
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
?.removeUnusedChunks();
})
);
// void addPanel(paneEl, "Garbage Collection (Beta2)", (e) => e, this.onlyOnP2POrCouchDB).then((paneEl) => {
// new Setting(paneEl)
// .setName("Scan garbage")
// .setDesc("Scan for garbage chunks in the database.")
// .addButton((button) =>
// button
// .setButtonText("Scan")
// // .setWarning()
// .setDisabled(false)
// .onClick(async () => {
// await this.plugin
// .getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
// ?.trackChanges(false, true);
// })
// )
// .addButton((button) =>
// button.setButtonText("Rescan").onClick(async () => {
// await this.plugin
// .getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
// ?.trackChanges(true, true);
// })
// );
// new Setting(paneEl)
// .setName("Collect garbage")
// .setDesc("Remove all unused chunks from the local database.")
// .addButton((button) =>
// button
// .setButtonText("Collect")
// .setWarning()
// .setDisabled(false)
// .onClick(async () => {
// await this.plugin
// .getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
// ?.performGC(true);
// })
// );
// new Setting(paneEl)
// .setName("Commit File Deletion")
// .setDesc("Completely delete all deleted documents from the local database.")
// .addButton((button) =>
// button
// .setButtonText("Delete")
// .setWarning()
// .setDisabled(false)
// .onClick(async () => {
// await this.plugin
// .getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
// ?.commitFileDeletion();
// })
// );
// });
// void addPanel(paneEl, "Garbage Collection (Old and Experimental)", (e) => e, this.onlyOnP2POrCouchDB).then(
// (paneEl) => {
// new Setting(paneEl)
// .setName("Remove all orphaned chunks")
// .setDesc("Remove all orphaned chunks from the local database.")
// .addButton((button) =>
// button
// .setButtonText("Remove")
// .setWarning()
// .setDisabled(false)
// .onClick(async () => {
// await this.plugin
// .getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
// ?.removeUnusedChunks();
// })
// );
new Setting(paneEl)
.setName("Resurrect deleted chunks")
.setDesc(
"If you have deleted chunks before fully synchronised and missed some chunks, you possibly can resurrect them."
)
.addButton((button) =>
button
.setButtonText("Try resurrect")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.plugin
.getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
?.resurrectChunks();
})
);
}
);
// new Setting(paneEl)
// .setName("Resurrect deleted chunks")
// .setDesc(
// "If you have deleted chunks before fully synchronised and missed some chunks, you possibly can resurrect them."
// )
// .addButton((button) =>
// button
// .setButtonText("Try resurrect")
// .setWarning()
// .setDisabled(false)
// .onClick(async () => {
// await this.plugin
// .getAddOn<LocalDatabaseMaintenance>(LocalDatabaseMaintenance.name)
// ?.resurrectChunks();
// })
// );
// }
// );
void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => {
new Setting(paneEl)

View File

@@ -173,7 +173,62 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
void addPanel(paneEl, "Compatibility (Trouble addressed)").then((paneEl) => {
new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch");
});
void addPanel(paneEl, "Remediation").then((paneEl) => {
let dateEl: HTMLSpanElement;
new Setting(paneEl)
.addText((text) => {
const updateDateText = () => {
if (this.editingSettings.maxMTimeForReflectEvents == 0) {
dateEl.textContent = `No limit configured`;
} else {
const date = new Date(this.editingSettings.maxMTimeForReflectEvents);
dateEl.textContent = `Limit: ${date.toLocaleString()} (${this.editingSettings.maxMTimeForReflectEvents})`;
}
this.requestUpdate();
};
text.inputEl.before((dateEl = document.createElement("span")));
text.inputEl.type = "datetime-local";
if (this.editingSettings.maxMTimeForReflectEvents > 0) {
const date = new Date(this.editingSettings.maxMTimeForReflectEvents);
const isoString = date.toISOString().slice(0, 16);
text.setValue(isoString);
} else {
text.setValue("");
}
text.onChange((value) => {
if (value == "") {
this.editingSettings.maxMTimeForReflectEvents = 0;
updateDateText();
return;
}
const date = new Date(value);
if (!isNaN(date.getTime())) {
this.editingSettings.maxMTimeForReflectEvents = date.getTime();
}
updateDateText();
});
updateDateText();
return text;
})
.setAuto("maxMTimeForReflectEvents")
.addApplyButton(["maxMTimeForReflectEvents"]);
this.addOnSaved("maxMTimeForReflectEvents", async (key) => {
const buttons = ["Restart Now", "Later"] as const;
const reboot = await this.plugin.confirm.askSelectStringDialogue(
"Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?",
buttons,
{
title: "Remediation Setting Changed",
defaultAction: "Restart Now",
}
);
if (reboot !== "Later") {
Logger("Remediation setting changed. Restarting Obsidian...", LOG_LEVEL_NOTICE);
this.services.appLifecycle.performRestart();
}
});
});
void addPanel(paneEl, "Remote Database Tweak (In sunset)").then((paneEl) => {
// new Setting(paneEl).autoWireToggle("useEden").setClass("wizardHidden");
// const onlyUsingEden = visibleOnly(() => this.isConfiguredAs("useEden", true));

View File

@@ -12,7 +12,7 @@ import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts
import type { PageFunctions } from "./SettingPane.ts";
import { visibleOnly } from "./SettingPane.ts";
import { DEFAULT_SETTINGS } from "../../../lib/src/common/types.ts";
import { request } from "obsidian";
import { request } from "@/deps.ts";
import { SetupManager, UserMode } from "../SetupManager.ts";
export function paneSetup(
this: ObsidianLiveSyncSettingTab,

View File

@@ -9,7 +9,6 @@ import {
} from "../../lib/src/common/types.ts";
import { generatePatchObj, isObjectDifferent } from "../../lib/src/common/utils.ts";
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
import { SvelteDialogManager } from "./SetupWizard/ObsidianSvelteDialog.ts";
import Intro from "./SetupWizard/dialogs/Intro.svelte";
import SelectMethodNewUser from "./SetupWizard/dialogs/SelectMethodNewUser.svelte";
import SelectMethodExisting from "./SetupWizard/dialogs/SelectMethodExisting.svelte";
@@ -52,10 +51,13 @@ export const enum UserMode {
* Setup Manager to handle onboarding and configuration setup
*/
export class SetupManager extends AbstractObsidianModule {
/**
* Dialog manager for handling Svelte dialogs
*/
private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin);
// /**
// * Dialog manager for handling Svelte dialogs
// */
// private dialogManager: SvelteDialogManager = new SvelteDialogManager(this.plugin);
get dialogManager() {
return this.services.UI.dialogManager;
}
/**
* Starts the onboarding process

View File

@@ -1,141 +0,0 @@
import { eventHub, EVENT_PLUGIN_UNLOADED } from "@/common/events";
import { Modal } from "@/deps";
import type ObsidianLiveSyncPlugin from "@/main";
import { mount, unmount } from "svelte";
import DialogHost from "@lib/UI/DialogHost.svelte";
import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises";
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
import {
type DialogControlBase,
type DialogSvelteComponentBaseProps,
type ComponentHasResult,
setupDialogContext,
getDialogContext,
type SvelteDialogManagerBase,
} from "@/lib/src/UI/svelteDialog.ts";
export type DialogSvelteComponentProps = DialogSvelteComponentBaseProps & {
plugin: ObsidianLiveSyncPlugin;
services: ObsidianLiveSyncPlugin["services"];
};
export type DialogControls<T = any, U = any> = DialogControlBase<T, U> & {
plugin: ObsidianLiveSyncPlugin;
services: ObsidianLiveSyncPlugin["services"];
};
export type DialogMessageProps = Record<string, any>;
// type DialogSvelteComponent<T extends DialogSvelteComponentProps = DialogSvelteComponentProps> = Component<SvelteComponent<T>,any>;
export class SvelteDialog<T, U> extends Modal {
plugin: ObsidianLiveSyncPlugin;
mountedComponent?: ReturnType<typeof mount>;
component: ComponentHasResult<T, U>;
result?: T;
initialData?: U;
title: string = "Obsidian LiveSync - Setup Wizard";
constructor(plugin: ObsidianLiveSyncPlugin, component: ComponentHasResult<T, U>, initialData?: U) {
super(plugin.app);
this.plugin = plugin;
this.component = component;
this.initialData = initialData;
}
resolveResult() {
this.resultPromiseWithResolvers?.resolve(this.result);
this.resultPromiseWithResolvers = undefined;
}
resultPromiseWithResolvers?: PromiseWithResolvers<T | undefined>;
onOpen() {
const { contentEl } = this;
contentEl.empty();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const dialog = this;
if (this.resultPromiseWithResolvers) {
this.resultPromiseWithResolvers.reject("Dialog opened again");
}
const pr = promiseWithResolvers<any>();
eventHub.once(EVENT_PLUGIN_UNLOADED, () => {
if (this.resultPromiseWithResolvers === pr) {
pr.reject("Plugin unloaded");
this.close();
}
});
this.resultPromiseWithResolvers = pr;
this.mountedComponent = mount(DialogHost, {
target: contentEl,
props: {
onSetupContext: (props: DialogSvelteComponentBaseProps) => {
setupDialogContext({
...props,
plugin: this.plugin,
services: this.plugin.services,
});
},
setTitle: (title: string) => {
dialog.setTitle(title);
},
closeDialog: () => {
dialog.close();
},
setResult: (result: T) => {
this.result = result;
},
getInitialData: () => this.initialData,
mountComponent: this.component,
},
});
}
waitForClose(): Promise<T | undefined> {
if (!this.resultPromiseWithResolvers) {
throw new Error("Dialog not opened yet");
}
return this.resultPromiseWithResolvers.promise;
}
onClose() {
this.resolveResult();
fireAndForget(async () => {
if (this.mountedComponent) {
await unmount(this.mountedComponent);
}
});
}
}
export async function openSvelteDialog<T, U>(
plugin: ObsidianLiveSyncPlugin,
component: ComponentHasResult<T, U>,
initialData?: U
): Promise<T | undefined> {
const dialog = new SvelteDialog<T, U>(plugin, component, initialData);
dialog.open();
return await dialog.waitForClose();
}
export class SvelteDialogManager implements SvelteDialogManagerBase {
plugin: ObsidianLiveSyncPlugin;
constructor(plugin: ObsidianLiveSyncPlugin) {
this.plugin = plugin;
}
async open<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T | undefined> {
return await openSvelteDialog<T, U>(this.plugin, component, initialData);
}
async openWithExplicitCancel<T, U>(component: ComponentHasResult<T, U>, initialData?: U): Promise<T> {
for (let i = 0; i < 10; i++) {
const ret = await openSvelteDialog<T, U>(this.plugin, component, initialData);
if (ret !== undefined) {
return ret;
}
if (this.plugin.services.appLifecycle.hasUnloaded()) {
throw new Error("Operation cancelled due to app shutdown.");
}
Logger("Please select 'Cancel' explicitly to cancel this operation.", LOG_LEVEL_NOTICE);
}
throw new Error("Operation Forcibly cancelled by user.");
}
}
export function getObsidianDialogContext<T = any>(): DialogControls<T> {
return getDialogContext<T>() as DialogControls<T>;
}

View File

@@ -16,8 +16,7 @@
} from "../../../../lib/src/common/types";
import { onMount } from "svelte";
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
import { copyTo, pickBucketSyncSettings } from "../../../../lib/src/common/utils";
const default_setting = pickBucketSyncSettings(DEFAULT_SETTINGS);
@@ -39,7 +38,7 @@
}
});
let error = $state("");
const context = getObsidianDialogContext();
const context = getDialogContext();
const isEndpointSecure = $derived.by(() => {
return syncSetting.endpoint.trim().toLowerCase().startsWith("https://");
});

View File

@@ -17,9 +17,8 @@
} from "../../../../lib/src/common/types";
import { isCloudantURI } from "../../../../lib/src/pouchdb/utils_couchdb";
import { getObsidianDialogContext } from "../ObsidianSvelteDialog";
import { onMount } from "svelte";
import type { GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
import { getDialogContext, type GuestDialogProps } from "../../../../lib/src/UI/svelteDialog";
import { copyTo, pickCouchDBSyncSettings } from "../../../../lib/src/common/utils";
import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte";
@@ -40,7 +39,7 @@
});
let error = $state("");
const context = getObsidianDialogContext();
const context = getDialogContext();
function generateSetting() {
const connSetting: CouchDBConnection = {

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