mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-02-23 20:48:48 +00:00
Compare commits
33 Commits
0.25.36
...
refactor_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b97756d0cf | ||
|
|
acf4bc3737 | ||
|
|
88838872e7 | ||
|
|
7d3827d335 | ||
|
|
92d3a0cfa2 | ||
|
|
bba26624ad | ||
|
|
b82f497cab | ||
|
|
37f4d13e75 | ||
|
|
7965f5342c | ||
|
|
9cdc14dda8 | ||
|
|
4f46276ebf | ||
|
|
931d360fb1 | ||
|
|
f68c1855da | ||
|
|
dff654b6e5 | ||
|
|
7e85bcbf08 | ||
|
|
38a695ea12 | ||
|
|
a502b0cd0c | ||
|
|
934f708753 | ||
|
|
0e574c6cb1 | ||
|
|
7375a85b07 | ||
|
|
4c3393d8b2 | ||
|
|
02aa9319c3 | ||
|
|
1a72e46d53 | ||
|
|
d755579968 | ||
|
|
b74ee9df77 | ||
|
|
daa04bcea8 | ||
|
|
b96b2f24a6 | ||
|
|
5569ab62df | ||
|
|
d84b6c4f15 | ||
|
|
336f2c8a4d | ||
|
|
b52ceec36a | ||
|
|
1e6400cf79 | ||
|
|
1ff1ac951b |
56
.github/workflows/harness-ci.yml
vendored
Normal file
56
.github/workflows/harness-ci.yml
vendored
Normal 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
33
.github/workflows/unit-ci.yml
vendored
Normal 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
6
.gitignore
vendored
@@ -23,4 +23,8 @@ data.json
|
||||
.env
|
||||
|
||||
# local config files
|
||||
*.local
|
||||
*.local
|
||||
|
||||
cov_profile/**
|
||||
|
||||
coverage
|
||||
11
.test.env
Normal file
11
.test.env
Normal 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
|
||||
@@ -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
140
devs.md
Normal 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).
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.36",
|
||||
"version": "0.25.38",
|
||||
"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",
|
||||
|
||||
5664
package-lock.json
generated
5664
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.36",
|
||||
"version": "0.25.38",
|
||||
"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": {
|
||||
|
||||
@@ -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
154
src/common/KeyValueDBv2.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,11 +271,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 17884bf912...724d788e2b
462
src/main.ts
462
src/main.ts
@@ -70,31 +70,7 @@ 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 { ServiceContext } from "./lib/src/services/ServiceHub.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;
|
||||
}
|
||||
@@ -184,10 +160,6 @@ 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;
|
||||
@@ -232,436 +204,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();
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,10 +275,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ export class ReplicateResultProcessor {
|
||||
return;
|
||||
} finally {
|
||||
// Remove from processing queue
|
||||
this._processingChanges.remove(change);
|
||||
this._processingChanges = this._processingChanges.filter((e) => e !== change);
|
||||
this.triggerTakeSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +322,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,6 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements Con
|
||||
}
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||
services.appLifecycle.handleOnLoaded(this._everyOnload.bind(this));
|
||||
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -497,7 +497,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 +531,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.
|
||||
|
||||
@@ -397,7 +397,7 @@ export class ModuleInitializerFile extends AbstractModule {
|
||||
}
|
||||
}
|
||||
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||
services.databaseEvents.handleInitialiseDatabase(this._initializeDatabase.bind(this));
|
||||
services.vault.handleScanVault(this._performFullScan.bind(this));
|
||||
services.databaseEvents.initialiseDatabase.setHandler(this._initializeDatabase.bind(this));
|
||||
services.vault.scanVault.setHandler(this._performFullScan.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ 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";
|
||||
|
||||
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 +20,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 +34,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 +51,33 @@ 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));
|
||||
},
|
||||
};
|
||||
} satisfies SimpleStore<T>;
|
||||
}
|
||||
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
|
||||
return this.openKeyValueDB();
|
||||
@@ -99,11 +101,11 @@ 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));
|
||||
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.openSimpleStore.setHandler(this._getSimpleStore.bind(this));
|
||||
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ 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]);
|
||||
}
|
||||
|
||||
@@ -330,16 +330,16 @@ export class ModuleObsidianAPI extends AbstractObsidianModule {
|
||||
}
|
||||
|
||||
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.API.handleGetAppVersion(this._getAppVersion.bind(this));
|
||||
services.API.handleGetPluginVersion(this._getPluginVersion.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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ButtonComponent,
|
||||
type TextAreaComponent,
|
||||
type ValueComponent,
|
||||
} from "obsidian";
|
||||
} from "@/deps.ts";
|
||||
import { unique } from "octagonal-wheels/collection";
|
||||
import {
|
||||
LEVEL_ADVANCED,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -93,6 +93,9 @@
|
||||
keys: () => {
|
||||
return Promise.resolve(Array.from(map.keys()));
|
||||
},
|
||||
get db() {
|
||||
return Promise.resolve(this);
|
||||
},
|
||||
} as SimpleStore<any>;
|
||||
|
||||
const dummyPouch = new PouchDB<EntryDoc>("dummy");
|
||||
|
||||
@@ -16,6 +16,7 @@ import { AbstractModule } from "../AbstractModule.ts";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/base/APIBase.ts";
|
||||
import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts";
|
||||
import type { LiveSyncCore } from "../../main.ts";
|
||||
import { initialiseWorkerModule } from "@/lib/src/worker/bgWorker.ts";
|
||||
|
||||
export class ModuleLiveSyncMain extends AbstractModule {
|
||||
async _onLiveSyncReady() {
|
||||
@@ -80,6 +81,7 @@ export class ModuleLiveSyncMain extends AbstractModule {
|
||||
}
|
||||
|
||||
async _onLiveSyncLoad(): Promise<boolean> {
|
||||
initialiseWorkerModule();
|
||||
await this.services.appLifecycle.onWireUpEvents();
|
||||
// debugger;
|
||||
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core);
|
||||
@@ -206,17 +208,17 @@ export class ModuleLiveSyncMain extends AbstractModule {
|
||||
|
||||
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
|
||||
super.onBindFunction(core, services);
|
||||
services.appLifecycle.handleIsSuspended(this._isSuspended.bind(this));
|
||||
services.appLifecycle.handleSetSuspended(this._setSuspended.bind(this));
|
||||
services.appLifecycle.handleIsReady(this._isReady.bind(this));
|
||||
services.appLifecycle.handleMarkIsReady(this._markIsReady.bind(this));
|
||||
services.appLifecycle.handleResetIsReady(this._resetIsReady.bind(this));
|
||||
services.appLifecycle.handleHasUnloaded(this._isUnloaded.bind(this));
|
||||
services.appLifecycle.handleIsReloadingScheduled(this._isReloadingScheduled.bind(this));
|
||||
services.appLifecycle.handleOnReady(this._onLiveSyncReady.bind(this));
|
||||
services.appLifecycle.handleOnWireUpEvents(this._wireUpEvents.bind(this));
|
||||
services.appLifecycle.handleOnLoad(this._onLiveSyncLoad.bind(this));
|
||||
services.appLifecycle.handleOnAppUnload(this._onLiveSyncUnload.bind(this));
|
||||
services.setting.handleRealiseSetting(this._realizeSettingSyncMode.bind(this));
|
||||
services.appLifecycle.isSuspended.setHandler(this._isSuspended.bind(this));
|
||||
services.appLifecycle.setSuspended.setHandler(this._setSuspended.bind(this));
|
||||
services.appLifecycle.isReady.setHandler(this._isReady.bind(this));
|
||||
services.appLifecycle.markIsReady.setHandler(this._markIsReady.bind(this));
|
||||
services.appLifecycle.resetIsReady.setHandler(this._resetIsReady.bind(this));
|
||||
services.appLifecycle.hasUnloaded.setHandler(this._isUnloaded.bind(this));
|
||||
services.appLifecycle.isReloadingScheduled.setHandler(this._isReloadingScheduled.bind(this));
|
||||
services.appLifecycle.onReady.addHandler(this._onLiveSyncReady.bind(this));
|
||||
services.appLifecycle.onWireUpEvents.addHandler(this._wireUpEvents.bind(this));
|
||||
services.appLifecycle.onLoad.addHandler(this._onLiveSyncLoad.bind(this));
|
||||
services.appLifecycle.onAppUnload.addHandler(this._onLiveSyncUnload.bind(this));
|
||||
services.setting.realiseSetting.setHandler(this._realizeSettingSyncMode.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Dynamic Load Modules
|
||||
|
||||
## Introduction
|
||||
|
||||
Self-hosted LiveSync has gradually but steadily become very feature-rich and they have created a very heavy `Main` class. This is very difficult to understand and maintain especially new contributors or futures contributors.
|
||||
And some of the features are not used by all users, we should limit the inter-dependencies between modules. And also inter-effects between modules.
|
||||
Hence, to make the code more readable and maintainable, I decided to split the code into multiple modules.
|
||||
|
||||
I also got a little greedy here, but I have an another objective now, which is to reduce the difficulty when porting to other platforms.
|
||||
|
||||
Therefore, almost all feature of the plug-in can be implemented as a module. And the `Main` class will be responsible for loading these modules.
|
||||
|
||||
## Modules
|
||||
|
||||
### Sorts
|
||||
|
||||
Modules can be sorted into two categories in some sorts:
|
||||
|
||||
- `CoreModule` and `ObsidianModule`
|
||||
- `Core`, `Essential`, and `Feature` ...
|
||||
|
||||
### How it works
|
||||
|
||||
After instancing `Core` and Modules, you should call `injectModules`. Then, the specific function will be injected into the stub of it of `Core` class by following rules:
|
||||
|
||||
| Function prefix | Description |
|
||||
| --------------- | ----------------------------------------------------------------- |
|
||||
| `$$` | 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. |
|
||||
|
||||
Note1: `Core` class should implement the same function as the module. If not, the module will be ignored.
|
||||
|
||||
And, basically, the Module has a `Core` class as `core` property. You should call any of inject functions by `this.core.$xxxxxx`. This rule is also applied to the function which implemented itself. Because some other modules possibly injects the function again, for the specific purpose.
|
||||
|
||||
### CoreModule
|
||||
|
||||
This Module is independent from Obsidian, and can be used in any platform. However, it can be call (or use) functions which has implemented in `ObsidianModule`.
|
||||
To porting, you should implement shim functions for `ObsidianModule`.
|
||||
|
||||
### ObsidianModule
|
||||
|
||||
(TBW)
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ServiceContext, type ServiceInstances } from "@/lib/src/services/ServiceHub.ts";
|
||||
import {
|
||||
InjectableAPIService,
|
||||
InjectableAppLifecycleService,
|
||||
InjectableConflictService,
|
||||
InjectableDatabaseEventService,
|
||||
InjectableDatabaseService,
|
||||
InjectableFileProcessingService,
|
||||
InjectablePathService,
|
||||
@@ -17,79 +19,96 @@ import { InjectableServiceHub } from "../../lib/src/services/InjectableServices.
|
||||
import { ConfigServiceBrowserCompat } from "../../lib/src/services/Services.ts";
|
||||
import type ObsidianLiveSyncPlugin from "../../main.ts";
|
||||
import { ObsidianUIService } from "./ObsidianUIService.ts";
|
||||
import type { App, Plugin } from "@/deps";
|
||||
|
||||
export class ObsidianServiceContext extends ServiceContext {
|
||||
app: App;
|
||||
plugin: Plugin;
|
||||
liveSyncPlugin: ObsidianLiveSyncPlugin;
|
||||
constructor(app: App, plugin: Plugin, liveSyncPlugin: ObsidianLiveSyncPlugin) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.liveSyncPlugin = liveSyncPlugin;
|
||||
}
|
||||
}
|
||||
|
||||
// All Services will be migrated to be based on Plain Services, not Injectable Services.
|
||||
// This is a migration step.
|
||||
|
||||
export class ObsidianAPIService extends InjectableAPIService {
|
||||
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
|
||||
getPlatform(): string {
|
||||
return "obsidian";
|
||||
}
|
||||
}
|
||||
export class ObsidianPathService extends InjectablePathService {}
|
||||
export class ObsidianDatabaseService extends InjectableDatabaseService {}
|
||||
export class ObsidianPathService extends InjectablePathService<ObsidianServiceContext> {}
|
||||
export class ObsidianDatabaseService extends InjectableDatabaseService<ObsidianServiceContext> {}
|
||||
export class ObsidianDatabaseEventService extends InjectableDatabaseEventService<ObsidianServiceContext> {}
|
||||
|
||||
// InjectableReplicatorService
|
||||
export class ObsidianReplicatorService extends InjectableReplicatorService {}
|
||||
export class ObsidianReplicatorService extends InjectableReplicatorService<ObsidianServiceContext> {}
|
||||
// InjectableFileProcessingService
|
||||
export class ObsidianFileProcessingService extends InjectableFileProcessingService {}
|
||||
export class ObsidianFileProcessingService extends InjectableFileProcessingService<ObsidianServiceContext> {}
|
||||
// InjectableReplicationService
|
||||
export class ObsidianReplicationService extends InjectableReplicationService {}
|
||||
export class ObsidianReplicationService extends InjectableReplicationService<ObsidianServiceContext> {}
|
||||
// InjectableRemoteService
|
||||
export class ObsidianRemoteService extends InjectableRemoteService {}
|
||||
export class ObsidianRemoteService extends InjectableRemoteService<ObsidianServiceContext> {}
|
||||
// InjectableConflictService
|
||||
export class ObsidianConflictService extends InjectableConflictService {}
|
||||
export class ObsidianConflictService extends InjectableConflictService<ObsidianServiceContext> {}
|
||||
// InjectableAppLifecycleService
|
||||
export class ObsidianAppLifecycleService extends InjectableAppLifecycleService {}
|
||||
export class ObsidianAppLifecycleService extends InjectableAppLifecycleService<ObsidianServiceContext> {}
|
||||
// InjectableSettingService
|
||||
export class ObsidianSettingService extends InjectableSettingService {}
|
||||
export class ObsidianSettingService extends InjectableSettingService<ObsidianServiceContext> {}
|
||||
// InjectableTweakValueService
|
||||
export class ObsidianTweakValueService extends InjectableTweakValueService {}
|
||||
export class ObsidianTweakValueService extends InjectableTweakValueService<ObsidianServiceContext> {}
|
||||
// InjectableVaultService
|
||||
export class ObsidianVaultService extends InjectableVaultService {}
|
||||
export class ObsidianVaultService extends InjectableVaultService<ObsidianServiceContext> {}
|
||||
// InjectableTestService
|
||||
export class ObsidianTestService extends InjectableTestService {}
|
||||
|
||||
export class ObsidianConfigService extends ConfigServiceBrowserCompat {}
|
||||
export class ObsidianTestService extends InjectableTestService<ObsidianServiceContext> {}
|
||||
export class ObsidianConfigService extends ConfigServiceBrowserCompat<ObsidianServiceContext> {}
|
||||
|
||||
// InjectableServiceHub
|
||||
|
||||
export class ObsidianServiceHub extends InjectableServiceHub {
|
||||
protected _api: ObsidianAPIService = new ObsidianAPIService(this._serviceBackend, this._throughHole);
|
||||
protected _path: ObsidianPathService = new ObsidianPathService(this._serviceBackend, this._throughHole);
|
||||
protected _database: ObsidianDatabaseService = new ObsidianDatabaseService(this._serviceBackend, this._throughHole);
|
||||
protected _replicator: ObsidianReplicatorService = new ObsidianReplicatorService(
|
||||
this._serviceBackend,
|
||||
this._throughHole
|
||||
);
|
||||
protected _fileProcessing: ObsidianFileProcessingService = new ObsidianFileProcessingService(
|
||||
this._serviceBackend,
|
||||
this._throughHole
|
||||
);
|
||||
protected _replication: ObsidianReplicationService = new ObsidianReplicationService(
|
||||
this._serviceBackend,
|
||||
this._throughHole
|
||||
);
|
||||
protected _remote: ObsidianRemoteService = new ObsidianRemoteService(this._serviceBackend, this._throughHole);
|
||||
protected _conflict: ObsidianConflictService = new ObsidianConflictService(this._serviceBackend, this._throughHole);
|
||||
protected _appLifecycle: ObsidianAppLifecycleService = new ObsidianAppLifecycleService(
|
||||
this._serviceBackend,
|
||||
this._throughHole
|
||||
);
|
||||
protected _setting: ObsidianSettingService = new ObsidianSettingService(this._serviceBackend, this._throughHole);
|
||||
protected _tweakValue: ObsidianTweakValueService = new ObsidianTweakValueService(
|
||||
this._serviceBackend,
|
||||
this._throughHole
|
||||
);
|
||||
protected _vault: ObsidianVaultService = new ObsidianVaultService(this._serviceBackend, this._throughHole);
|
||||
protected _test: ObsidianTestService = new ObsidianTestService(this._serviceBackend, this._throughHole);
|
||||
|
||||
private _plugin: ObsidianLiveSyncPlugin;
|
||||
export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceContext> {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
const config = new ObsidianConfigService();
|
||||
super({
|
||||
ui: new ObsidianUIService(plugin),
|
||||
const context = new ObsidianServiceContext(plugin.app, plugin, plugin);
|
||||
|
||||
const API = new ObsidianAPIService(context);
|
||||
const appLifecycle = new ObsidianAppLifecycleService(context);
|
||||
const conflict = new ObsidianConflictService(context);
|
||||
const database = new ObsidianDatabaseService(context);
|
||||
const fileProcessing = new ObsidianFileProcessingService(context);
|
||||
const replication = new ObsidianReplicationService(context);
|
||||
const replicator = new ObsidianReplicatorService(context);
|
||||
const remote = new ObsidianRemoteService(context);
|
||||
const setting = new ObsidianSettingService(context);
|
||||
const tweakValue = new ObsidianTweakValueService(context);
|
||||
const vault = new ObsidianVaultService(context);
|
||||
const test = new ObsidianTestService(context);
|
||||
const databaseEvents = new ObsidianDatabaseEventService(context);
|
||||
const path = new ObsidianPathService(context);
|
||||
const ui = new ObsidianUIService(context);
|
||||
const config = new ObsidianConfigService(context, vault);
|
||||
// Using 'satisfies' to ensure all services are provided
|
||||
const serviceInstancesToInit = {
|
||||
appLifecycle: appLifecycle,
|
||||
conflict: conflict,
|
||||
database: database,
|
||||
databaseEvents: databaseEvents,
|
||||
fileProcessing: fileProcessing,
|
||||
replication: replication,
|
||||
replicator: replicator,
|
||||
remote: remote,
|
||||
setting: setting,
|
||||
tweakValue: tweakValue,
|
||||
vault: vault,
|
||||
test: test,
|
||||
ui: ui,
|
||||
path: path,
|
||||
API: API,
|
||||
config: config,
|
||||
});
|
||||
this._plugin = plugin;
|
||||
} satisfies Required<ServiceInstances<ObsidianServiceContext>>;
|
||||
|
||||
super(context, serviceInstancesToInit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { UIService } from "../../lib/src/services/Services";
|
||||
import type ObsidianLiveSyncPlugin from "../../main";
|
||||
import type { Plugin } from "@/deps";
|
||||
import { SvelteDialogManager } from "../features/SetupWizard/ObsidianSvelteDialog";
|
||||
import DialogueToCopy from "../../lib/src/UI/dialogues/DialogueToCopy.svelte";
|
||||
import type { ObsidianServiceContext } from "./ObsidianServices";
|
||||
import type ObsidianLiveSyncPlugin from "@/main";
|
||||
|
||||
export class ObsidianUIService extends UIService {
|
||||
export class ObsidianUIService extends UIService<ObsidianServiceContext> {
|
||||
private _dialogManager: SvelteDialogManager;
|
||||
private _plugin: ObsidianLiveSyncPlugin;
|
||||
private _plugin: Plugin;
|
||||
private _liveSyncPlugin: ObsidianLiveSyncPlugin;
|
||||
get dialogManager() {
|
||||
return this._dialogManager;
|
||||
}
|
||||
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||
super();
|
||||
this._dialogManager = new SvelteDialogManager(plugin);
|
||||
this._plugin = plugin;
|
||||
constructor(context: ObsidianServiceContext) {
|
||||
super(context);
|
||||
this._liveSyncPlugin = context.liveSyncPlugin;
|
||||
this._dialogManager = new SvelteDialogManager(this._liveSyncPlugin);
|
||||
this._plugin = context.plugin;
|
||||
}
|
||||
|
||||
async promptCopyToClipboard(title: string, value: string): Promise<boolean> {
|
||||
const param = {
|
||||
title: title,
|
||||
@@ -25,13 +30,15 @@ export class ObsidianUIService extends UIService {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
showMarkdownDialog<T extends string[]>(
|
||||
title: string,
|
||||
contentMD: string,
|
||||
buttons: T,
|
||||
defaultAction?: (typeof buttons)[number]
|
||||
): Promise<(typeof buttons)[number] | false> {
|
||||
return this._plugin.confirm.askSelectStringDialogue(contentMD, buttons, {
|
||||
// TODO: implement `confirm` to this service
|
||||
return this._liveSyncPlugin.confirm.askSelectStringDialogue(contentMD, buttons, {
|
||||
title,
|
||||
defaultAction: defaultAction ?? buttons[0],
|
||||
timeout: 0,
|
||||
|
||||
147
test/harness/harness.ts
Normal file
147
test/harness/harness.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { App } from "@/deps.ts";
|
||||
import ObsidianLiveSyncPlugin from "@/main";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
import { LOG_LEVEL_VERBOSE, setGlobalLogFunction } from "@lib/common/logger";
|
||||
import { SettingCache } from "./obsidian-mock";
|
||||
import { delay, promiseWithResolvers } from "octagonal-wheels/promises";
|
||||
import { EVENT_LAYOUT_READY, eventHub } from "@/common/events";
|
||||
import { EVENT_PLATFORM_UNLOADED } from "@/lib/src/PlatformAPIs/base/APIBase";
|
||||
import { env } from "../suite/variables";
|
||||
|
||||
export type LiveSyncHarness = {
|
||||
app: App;
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
dispose: () => Promise<void>;
|
||||
disposalPromise: Promise<void>;
|
||||
isDisposed: () => boolean;
|
||||
};
|
||||
const isLiveSyncLogEnabled = env?.PRINT_LIVESYNC_LOGS === "true";
|
||||
function overrideLogFunction(vaultName: string) {
|
||||
setGlobalLogFunction((msg, level, key) => {
|
||||
if (!isLiveSyncLogEnabled) {
|
||||
return;
|
||||
}
|
||||
if (level && level < LOG_LEVEL_VERBOSE) {
|
||||
return;
|
||||
}
|
||||
if (msg instanceof Error) {
|
||||
console.error(msg.stack);
|
||||
} else {
|
||||
console.log(
|
||||
`[${vaultName}] :: [${key ?? "Global"}][${level ?? 1}]: ${msg instanceof Error ? msg.stack : msg}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateHarness(
|
||||
paramVaultName?: string,
|
||||
settings?: Partial<ObsidianLiveSyncSettings>
|
||||
): Promise<LiveSyncHarness> {
|
||||
// return await serialized("harness-generation-lock", async () => {
|
||||
// Dispose previous harness to avoid multiple harness running at the same time
|
||||
// if (previousHarness && !previousHarness.isDisposed()) {
|
||||
// console.log(`Previous harness detected, waiting for disposal...`);
|
||||
// await previousHarness.disposalPromise;
|
||||
// previousHarness = null;
|
||||
// await delay(100);
|
||||
// }
|
||||
const vaultName = paramVaultName ?? "TestVault" + Date.now();
|
||||
const setting = {
|
||||
...DEFAULT_SETTINGS,
|
||||
...settings,
|
||||
};
|
||||
overrideLogFunction(vaultName);
|
||||
//@ts-ignore Mocked in harness
|
||||
const app = new App(vaultName);
|
||||
// setting and vault name
|
||||
SettingCache.set(app, setting);
|
||||
SettingCache.set(app.vault, vaultName);
|
||||
|
||||
//@ts-ignore
|
||||
const manifest_version = `${MANIFEST_VERSION || "0.0.0-harness"}`;
|
||||
overrideLogFunction(vaultName);
|
||||
const manifest = {
|
||||
id: "obsidian-livesync",
|
||||
name: "Self-hosted LiveSync (Harnessed)",
|
||||
version: manifest_version,
|
||||
minAppVersion: "0.15.0",
|
||||
description: "Testing",
|
||||
author: "vrtmrz",
|
||||
authorUrl: "",
|
||||
isDesktopOnly: false,
|
||||
};
|
||||
|
||||
const plugin = new ObsidianLiveSyncPlugin(app, manifest);
|
||||
overrideLogFunction(vaultName);
|
||||
// Initial load
|
||||
await delay(100);
|
||||
await plugin.onload();
|
||||
let isDisposed = false;
|
||||
const waitPromise = promiseWithResolvers<void>();
|
||||
eventHub.once(EVENT_PLATFORM_UNLOADED, async () => {
|
||||
console.log(`Harness for vault '${vaultName}' disposed.`);
|
||||
await delay(100);
|
||||
eventHub.offAll();
|
||||
isDisposed = true;
|
||||
waitPromise.resolve();
|
||||
});
|
||||
eventHub.once(EVENT_LAYOUT_READY, () => {
|
||||
plugin.app.vault.trigger("layout-ready");
|
||||
});
|
||||
const harness: LiveSyncHarness = {
|
||||
app,
|
||||
plugin,
|
||||
dispose: async () => {
|
||||
await plugin.onunload();
|
||||
return waitPromise.promise;
|
||||
},
|
||||
disposalPromise: waitPromise.promise,
|
||||
isDisposed: () => isDisposed,
|
||||
};
|
||||
await delay(100);
|
||||
console.log(`Harness for vault '${vaultName}' is ready.`);
|
||||
// previousHarness = harness;
|
||||
return harness;
|
||||
}
|
||||
export async function waitForReady(harness: LiveSyncHarness): Promise<void> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (harness.plugin.services.appLifecycle.isReady()) {
|
||||
console.log("App Lifecycle is ready");
|
||||
return;
|
||||
}
|
||||
await delay(100);
|
||||
}
|
||||
throw new Error(`Initialisation Timed out!`);
|
||||
}
|
||||
|
||||
export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await delay(25);
|
||||
const processing =
|
||||
harness.plugin.databaseQueueCount.value +
|
||||
harness.plugin.processingFileEventCount.value +
|
||||
harness.plugin.pendingFileEventCount.value +
|
||||
harness.plugin.totalQueued.value +
|
||||
harness.plugin.batched.value +
|
||||
harness.plugin.processing.value +
|
||||
harness.plugin.storageApplyingCount.value;
|
||||
|
||||
if (processing === 0) {
|
||||
if (i > 0) {
|
||||
console.log(`Idle after ${i} loops`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function waitForClosed(harness: LiveSyncHarness): Promise<void> {
|
||||
await delay(100);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (harness.plugin.services.appLifecycle.hasUnloaded()) {
|
||||
console.log("App Lifecycle has unloaded");
|
||||
return;
|
||||
}
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
995
test/harness/obsidian-mock.ts
Normal file
995
test/harness/obsidian-mock.ts
Normal file
@@ -0,0 +1,995 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
export const SettingCache = new Map<any, any>();
|
||||
//@ts-ignore obsidian global
|
||||
globalThis.activeDocument = document;
|
||||
|
||||
declare const hostPlatform: string | undefined;
|
||||
|
||||
// import { interceptFetchForLogging } from "../harness/utils/intercept";
|
||||
// interceptFetchForLogging();
|
||||
globalThis.process = {
|
||||
platform: (hostPlatform || "win32") as any,
|
||||
} as any;
|
||||
console.warn(`[Obsidian Mock] process.platform is set to ${globalThis.process.platform}`);
|
||||
export class TAbstractFile {
|
||||
vault: Vault;
|
||||
path: string;
|
||||
name: string;
|
||||
parent: TFolder | null;
|
||||
|
||||
constructor(vault: Vault, path: string, name: string, parent: TFolder | null) {
|
||||
this.vault = vault;
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export class TFile extends TAbstractFile {
|
||||
stat: {
|
||||
ctime: number;
|
||||
mtime: number;
|
||||
size: number;
|
||||
} = { ctime: Date.now(), mtime: Date.now(), size: 0 };
|
||||
|
||||
get extension(): string {
|
||||
return this.name.split(".").pop() || "";
|
||||
}
|
||||
|
||||
get basename(): string {
|
||||
const parts = this.name.split(".");
|
||||
if (parts.length > 1) parts.pop();
|
||||
return parts.join(".");
|
||||
}
|
||||
}
|
||||
|
||||
export class TFolder extends TAbstractFile {
|
||||
children: TAbstractFile[] = [];
|
||||
|
||||
get isRoot(): boolean {
|
||||
return this.path === "" || this.path === "/";
|
||||
}
|
||||
}
|
||||
|
||||
export class EventRef {}
|
||||
|
||||
// class StorageMap<T, U> extends Map<T, U> {
|
||||
// constructor(saveName?: string) {
|
||||
// super();
|
||||
// if (saveName) {
|
||||
// this.saveName = saveName;
|
||||
// void this.restore(saveName);
|
||||
// }
|
||||
// }
|
||||
// private saveName: string = "";
|
||||
// async restore(saveName: string) {
|
||||
// this.saveName = saveName;
|
||||
// const db = await OpenKeyValueDatabase(saveName);
|
||||
// const data = await db.get<{ [key: string]: U }>("data");
|
||||
// if (data) {
|
||||
// for (const key of Object.keys(data)) {
|
||||
// this.set(key as any as T, data[key]);
|
||||
// }
|
||||
// }
|
||||
// db.close();
|
||||
// return this;
|
||||
// }
|
||||
// saving: boolean = false;
|
||||
// async save() {
|
||||
// if (this.saveName === "") {
|
||||
// return;
|
||||
// }
|
||||
// if (this.saving) {
|
||||
// return;
|
||||
// }
|
||||
// try {
|
||||
// this.saving = true;
|
||||
|
||||
// const db = await OpenKeyValueDatabase(this.saveName);
|
||||
// const data: { [key: string]: U } = {};
|
||||
// for (const [key, value] of this.entries()) {
|
||||
// data[key as any as string] = value;
|
||||
// }
|
||||
// await db.set("data", data);
|
||||
// db.close();
|
||||
// } finally {
|
||||
// this.saving = false;
|
||||
// }
|
||||
// }
|
||||
// set(key: T, value: U): this {
|
||||
// super.set(key, value);
|
||||
// void this.save();
|
||||
// return this;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
export class Vault {
|
||||
adapter: DataAdapter;
|
||||
vaultName: string = "MockVault";
|
||||
private files: Map<string, TAbstractFile> = new Map();
|
||||
private contents: Map<string, string | ArrayBuffer> = new Map();
|
||||
private root: TFolder;
|
||||
private listeners: Map<string, Set<Function>> = new Map();
|
||||
|
||||
constructor(vaultName?: string) {
|
||||
if (vaultName) {
|
||||
this.vaultName = vaultName;
|
||||
}
|
||||
this.files = new Map();
|
||||
this.contents = new Map();
|
||||
this.adapter = new DataAdapter(this);
|
||||
this.root = new TFolder(this, "", "", null);
|
||||
this.files.set("", this.root);
|
||||
}
|
||||
|
||||
getAbstractFileByPath(path: string): TAbstractFile | null {
|
||||
if (path === "/") path = "";
|
||||
const file = this.files.get(path);
|
||||
return file || null;
|
||||
}
|
||||
getAbstractFileByPathInsensitive(path: string): TAbstractFile | null {
|
||||
const lowerPath = path.toLowerCase();
|
||||
for (const [p, file] of this.files.entries()) {
|
||||
if (p.toLowerCase() === lowerPath) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getFiles(): TFile[] {
|
||||
return Array.from(this.files.values()).filter((f) => f instanceof TFile);
|
||||
}
|
||||
|
||||
async _adapterRead(path: string): Promise<string | null> {
|
||||
await Promise.resolve();
|
||||
const file = this.contents.get(path);
|
||||
if (typeof file === "string") {
|
||||
return file;
|
||||
}
|
||||
if (file instanceof ArrayBuffer) {
|
||||
return new TextDecoder().decode(file);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async _adapterReadBinary(path: string): Promise<ArrayBuffer | null> {
|
||||
await Promise.resolve();
|
||||
const file = this.contents.get(path);
|
||||
if (file instanceof ArrayBuffer) {
|
||||
return file;
|
||||
}
|
||||
if (typeof file === "string") {
|
||||
return new TextEncoder().encode(file).buffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async read(file: TFile): Promise<string> {
|
||||
await Promise.resolve();
|
||||
const content = this.contents.get(file.path);
|
||||
if (typeof content === "string") return content;
|
||||
if (content instanceof ArrayBuffer) {
|
||||
return new TextDecoder().decode(content);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async readBinary(file: TFile): Promise<ArrayBuffer> {
|
||||
await Promise.resolve();
|
||||
const content = this.contents.get(file.path);
|
||||
if (content instanceof ArrayBuffer) return content;
|
||||
if (typeof content === "string") {
|
||||
return new TextEncoder().encode(content).buffer;
|
||||
}
|
||||
return new ArrayBuffer(0);
|
||||
}
|
||||
|
||||
private async _create(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<TFile> {
|
||||
if (this.files.has(path)) throw new Error("File already exists");
|
||||
const name = path.split("/").pop() || "";
|
||||
const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
||||
let parent = this.getAbstractFileByPath(parentPath);
|
||||
if (!parent || !(parent instanceof TFolder)) {
|
||||
parent = await this.createFolder(parentPath);
|
||||
}
|
||||
|
||||
const file = new TFile(this, path, name, parent as TFolder);
|
||||
file.stat.size = typeof data === "string" ? new TextEncoder().encode(data).length : data.byteLength;
|
||||
file.stat.ctime = options?.ctime ?? Date.now();
|
||||
file.stat.mtime = options?.mtime ?? Date.now();
|
||||
this.files.set(path, file);
|
||||
this.contents.set(path, data);
|
||||
(parent as TFolder).children.push(file);
|
||||
// console.dir(this.files);
|
||||
|
||||
this.trigger("create", file);
|
||||
return file;
|
||||
}
|
||||
async create(path: string, data: string, options?: DataWriteOptions): Promise<TFile> {
|
||||
return await this._create(path, data, options);
|
||||
}
|
||||
async createBinary(path: string, data: ArrayBuffer, options?: DataWriteOptions): Promise<TFile> {
|
||||
return await this._create(path, data, options);
|
||||
}
|
||||
|
||||
async _modify(file: TFile, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<void> {
|
||||
await Promise.resolve();
|
||||
this.contents.set(file.path, data);
|
||||
file.stat.mtime = options?.mtime ?? Date.now();
|
||||
file.stat.ctime = options?.ctime ?? file.stat.ctime ?? Date.now();
|
||||
file.stat.size = typeof data === "string" ? data.length : data.byteLength;
|
||||
console.warn(`[Obsidian Mock ${this.vaultName}] Modified file at path: '${file.path}'`);
|
||||
this.files.set(file.path, file);
|
||||
this.trigger("modify", file);
|
||||
}
|
||||
async modify(file: TFile, data: string, options?: DataWriteOptions): Promise<void> {
|
||||
return await this._modify(file, data, options);
|
||||
}
|
||||
async modifyBinary(file: TFile, data: ArrayBuffer, options?: DataWriteOptions): Promise<void> {
|
||||
return await this._modify(file, data, options);
|
||||
}
|
||||
|
||||
async createFolder(path: string): Promise<TFolder> {
|
||||
if (path === "") return this.root;
|
||||
if (this.files.has(path)) {
|
||||
const f = this.files.get(path);
|
||||
if (f instanceof TFolder) return f;
|
||||
throw new Error("Path is a file");
|
||||
}
|
||||
const name = path.split("/").pop() || "";
|
||||
const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
|
||||
const parent = await this.createFolder(parentPath);
|
||||
const folder = new TFolder(this, path, name, parent);
|
||||
this.files.set(path, folder);
|
||||
parent.children.push(folder);
|
||||
return folder;
|
||||
}
|
||||
|
||||
async delete(file: TAbstractFile, force?: boolean): Promise<void> {
|
||||
await Promise.resolve();
|
||||
this.files.delete(file.path);
|
||||
this.contents.delete(file.path);
|
||||
if (file.parent) {
|
||||
file.parent.children = file.parent.children.filter((c) => c !== file);
|
||||
}
|
||||
this.trigger("delete", file);
|
||||
}
|
||||
|
||||
async trash(file: TAbstractFile, system: boolean): Promise<void> {
|
||||
await Promise.resolve();
|
||||
return this.delete(file);
|
||||
}
|
||||
|
||||
on(name: string, callback: (...args: any[]) => any, ctx?: any): EventRef {
|
||||
if (!this.listeners.has(name)) {
|
||||
this.listeners.set(name, new Set());
|
||||
}
|
||||
const boundCallback = ctx ? callback.bind(ctx) : callback;
|
||||
this.listeners.get(name)!.add(boundCallback);
|
||||
return { name, callback: boundCallback } as any;
|
||||
}
|
||||
|
||||
off(name: string, callback: any) {
|
||||
this.listeners.get(name)?.delete(callback);
|
||||
}
|
||||
|
||||
offref(ref: EventRef) {
|
||||
const { name, callback } = ref as any;
|
||||
this.off(name, callback);
|
||||
}
|
||||
|
||||
trigger(name: string, ...args: any[]) {
|
||||
this.listeners.get(name)?.forEach((cb) => cb(...args));
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.vaultName;
|
||||
}
|
||||
}
|
||||
|
||||
export class DataAdapter {
|
||||
vault: Vault;
|
||||
constructor(vault: Vault) {
|
||||
this.vault = vault;
|
||||
}
|
||||
stat(path: string): Promise<{ ctime: number; mtime: number; size: number }> {
|
||||
const file = this.vault.getAbstractFileByPath(path);
|
||||
if (file && file instanceof TFile) {
|
||||
return Promise.resolve({
|
||||
ctime: file.stat.ctime,
|
||||
mtime: file.stat.mtime,
|
||||
size: file.stat.size,
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error("File not found"));
|
||||
}
|
||||
async list(path: string): Promise<{ files: string[]; folders: string[] }> {
|
||||
await Promise.resolve();
|
||||
const abstractFile = this.vault.getAbstractFileByPath(path);
|
||||
if (abstractFile instanceof TFolder) {
|
||||
const files: string[] = [];
|
||||
const folders: string[] = [];
|
||||
for (const child of abstractFile.children) {
|
||||
if (child instanceof TFile) files.push(child.path);
|
||||
else if (child instanceof TFolder) folders.push(child.path);
|
||||
}
|
||||
return { files, folders };
|
||||
}
|
||||
return { files: [], folders: [] };
|
||||
}
|
||||
async _write(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise<void> {
|
||||
const file = this.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
if (typeof data === "string") {
|
||||
await this.vault.modify(file, data, options);
|
||||
} else {
|
||||
await this.vault.modifyBinary(file, data, options);
|
||||
}
|
||||
} else {
|
||||
if (typeof data === "string") {
|
||||
await this.vault.create(path, data, options);
|
||||
} else {
|
||||
await this.vault.createBinary(path, data, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
async write(path: string, data: string, options?: DataWriteOptions): Promise<void> {
|
||||
return await this._write(path, data, options);
|
||||
}
|
||||
async writeBinary(path: string, data: ArrayBuffer, options?: DataWriteOptions): Promise<void> {
|
||||
return await this._write(path, data, options);
|
||||
}
|
||||
|
||||
async read(path: string): Promise<string> {
|
||||
const file = this.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) return await this.vault.read(file);
|
||||
throw new Error("File not found");
|
||||
}
|
||||
async readBinary(path: string): Promise<ArrayBuffer> {
|
||||
const file = this.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) return await this.vault.readBinary(file);
|
||||
throw new Error("File not found");
|
||||
}
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
await Promise.resolve();
|
||||
return this.vault.getAbstractFileByPath(path) !== null;
|
||||
}
|
||||
async mkdir(path: string): Promise<void> {
|
||||
await this.vault.createFolder(path);
|
||||
}
|
||||
async remove(path: string): Promise<void> {
|
||||
const file = this.vault.getAbstractFileByPath(path);
|
||||
if (file) await this.vault.delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
class Events {
|
||||
_eventEmitter = new EventTarget();
|
||||
_events = new Map<any, any>();
|
||||
_eventTarget(cb: any) {
|
||||
const x = this._events.get(cb);
|
||||
if (x) {
|
||||
return x;
|
||||
}
|
||||
const callback = (evt: any) => {
|
||||
x(evt?.detail ?? undefined);
|
||||
};
|
||||
this._events.set(cb, callback);
|
||||
return callback;
|
||||
}
|
||||
on(name: string, cb: any, ctx?: any) {
|
||||
this._eventEmitter.addEventListener(name, this._eventTarget(cb));
|
||||
}
|
||||
trigger(name: string, args: any) {
|
||||
const evt = new CustomEvent(name, {
|
||||
detail: args,
|
||||
});
|
||||
this._eventEmitter.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
|
||||
class Workspace extends Events {
|
||||
getActiveFile() {
|
||||
return null;
|
||||
}
|
||||
getMostRecentLeaf() {
|
||||
return null;
|
||||
}
|
||||
|
||||
onLayoutReady(cb: any) {
|
||||
// cb();
|
||||
// console.log("[Obsidian Mock] Workspace onLayoutReady registered");
|
||||
// this._eventEmitter.addEventListener("layout-ready", () => {
|
||||
// console.log("[Obsidian Mock] Workspace layout-ready event triggered");
|
||||
setTimeout(() => {
|
||||
cb();
|
||||
}, 200);
|
||||
// });
|
||||
}
|
||||
getLeavesOfType() {
|
||||
return [];
|
||||
}
|
||||
getLeaf() {
|
||||
return { setViewState: () => Promise.resolve(), revealLeaf: () => Promise.resolve() };
|
||||
}
|
||||
revealLeaf() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
containerEl: HTMLElement = document.createElement("div");
|
||||
}
|
||||
export class App {
|
||||
vaultName: string = "MockVault";
|
||||
constructor(vaultName?: string) {
|
||||
if (vaultName) {
|
||||
this.vaultName = vaultName;
|
||||
}
|
||||
this.vault = new Vault(this.vaultName);
|
||||
}
|
||||
vault: Vault;
|
||||
workspace: Workspace = new Workspace();
|
||||
metadataCache: any = {
|
||||
on: (name: string, cb: any, ctx?: any) => {},
|
||||
getFileCache: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
export class Plugin {
|
||||
app: App;
|
||||
manifest: any;
|
||||
settings: any;
|
||||
commands: Map<string, any> = new Map();
|
||||
constructor(app: App, manifest: any) {
|
||||
this.app = app;
|
||||
this.manifest = manifest;
|
||||
}
|
||||
async loadData(): Promise<any> {
|
||||
await Promise.resolve();
|
||||
return SettingCache.get(this.app) ?? {};
|
||||
}
|
||||
async saveData(data: any): Promise<void> {
|
||||
await Promise.resolve();
|
||||
SettingCache.set(this.app, data);
|
||||
}
|
||||
onload() {}
|
||||
onunload() {}
|
||||
addSettingTab(tab: any) {}
|
||||
addCommand(command: any) {
|
||||
this.commands.set(command.id, command);
|
||||
}
|
||||
addStatusBarItem() {
|
||||
return {
|
||||
setText: () => {},
|
||||
setClass: () => {},
|
||||
addClass: () => {},
|
||||
};
|
||||
}
|
||||
addRibbonIcon() {
|
||||
const icon = {
|
||||
setAttribute: () => icon,
|
||||
addClass: () => icon,
|
||||
onclick: () => {},
|
||||
};
|
||||
return icon;
|
||||
}
|
||||
registerView(type: string, creator: any) {}
|
||||
registerObsidianProtocolHandler(handler: any) {}
|
||||
registerEvent(handler: any) {}
|
||||
registerDomEvent(target: any, eventName: string, handler: any) {}
|
||||
}
|
||||
|
||||
export class Notice {
|
||||
constructor(message: string) {
|
||||
console.log("Notice:", message);
|
||||
}
|
||||
}
|
||||
|
||||
export class Modal {
|
||||
app: App;
|
||||
contentEl: HTMLElement;
|
||||
titleEl: HTMLElement;
|
||||
modalEl: HTMLElement;
|
||||
isOpen: boolean = false;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
this.contentEl = document.createElement("div");
|
||||
this.contentEl.className = "modal-content";
|
||||
this.titleEl = document.createElement("div");
|
||||
this.titleEl.className = "modal-title";
|
||||
this.modalEl = document.createElement("div");
|
||||
this.modalEl.className = "modal";
|
||||
this.modalEl.style.display = "none";
|
||||
this.modalEl.appendChild(this.titleEl);
|
||||
this.modalEl.appendChild(this.contentEl);
|
||||
}
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.modalEl.style.display = "block";
|
||||
if (!this.modalEl.parentElement) {
|
||||
document.body.appendChild(this.modalEl);
|
||||
}
|
||||
this.onOpen();
|
||||
}
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.modalEl.style.display = "none";
|
||||
this.onClose();
|
||||
}
|
||||
onOpen() {}
|
||||
onClose() {}
|
||||
setPlaceholder(p: string) {}
|
||||
setTitle(t: string) {
|
||||
this.titleEl.textContent = t;
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginSettingTab {
|
||||
app: App;
|
||||
plugin: Plugin;
|
||||
containerEl: HTMLElement;
|
||||
constructor(app: App, plugin: Plugin) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.containerEl = document.createElement("div");
|
||||
}
|
||||
display() {}
|
||||
}
|
||||
|
||||
export function normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
export const Platform = {
|
||||
isDesktop: true,
|
||||
isMobile: false,
|
||||
};
|
||||
|
||||
export class Menu {
|
||||
addItem(cb: (item: MenuItem) => any) {
|
||||
cb(new MenuItem());
|
||||
return this;
|
||||
}
|
||||
showAtMouseEvent(evt: MouseEvent) {}
|
||||
}
|
||||
export class MenuItem {
|
||||
setTitle(title: string) {
|
||||
return this;
|
||||
}
|
||||
setIcon(icon: string) {
|
||||
return this;
|
||||
}
|
||||
onClick(cb: (evt: MouseEvent) => any) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
export class MenuSeparator {}
|
||||
|
||||
export class Component {
|
||||
load() {}
|
||||
unload() {}
|
||||
}
|
||||
|
||||
export class ButtonComponent extends Component {
|
||||
buttonEl: HTMLButtonElement = document.createElement("button");
|
||||
private clickHandler: ((evt: MouseEvent) => any) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.buttonEl = document.createElement("button");
|
||||
this.buttonEl.type = "button";
|
||||
}
|
||||
|
||||
setButtonText(text: string) {
|
||||
this.buttonEl.textContent = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
setCta() {
|
||||
this.buttonEl.classList.add("mod-cta");
|
||||
return this;
|
||||
}
|
||||
|
||||
onClick(cb: (evt: MouseEvent) => any) {
|
||||
this.clickHandler = cb;
|
||||
this.buttonEl.removeEventListener("click", this.clickHandler);
|
||||
this.buttonEl.addEventListener("click", (evt) => cb(evt as MouseEvent));
|
||||
return this;
|
||||
}
|
||||
|
||||
setClass(c: string) {
|
||||
this.buttonEl.classList.add(c);
|
||||
return this;
|
||||
}
|
||||
|
||||
setTooltip(tooltip: string) {
|
||||
this.buttonEl.title = tooltip;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled: boolean) {
|
||||
this.buttonEl.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class TextComponent extends Component {
|
||||
inputEl: HTMLInputElement = document.createElement("input");
|
||||
private changeHandler: ((value: string) => any) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inputEl = document.createElement("input");
|
||||
this.inputEl.type = "text";
|
||||
}
|
||||
|
||||
onChange(cb: (value: string) => any) {
|
||||
this.changeHandler = cb;
|
||||
this.inputEl.removeEventListener("change", this.handleChange);
|
||||
this.inputEl.addEventListener("change", this.handleChange);
|
||||
this.inputEl.addEventListener("input", (evt) => {
|
||||
const target = evt.target as HTMLInputElement;
|
||||
cb(target.value);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleChange = (evt: Event) => {
|
||||
if (this.changeHandler) {
|
||||
const target = evt.target as HTMLInputElement;
|
||||
this.changeHandler(target.value);
|
||||
}
|
||||
};
|
||||
|
||||
setValue(v: string) {
|
||||
this.inputEl.value = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
setPlaceholder(p: string) {
|
||||
this.inputEl.placeholder = p;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled: boolean) {
|
||||
this.inputEl.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ToggleComponent extends Component {
|
||||
inputEl: HTMLInputElement = document.createElement("input");
|
||||
private changeHandler: ((value: boolean) => any) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inputEl = document.createElement("input");
|
||||
this.inputEl.type = "checkbox";
|
||||
}
|
||||
|
||||
onChange(cb: (value: boolean) => any) {
|
||||
this.changeHandler = cb;
|
||||
this.inputEl.addEventListener("change", (evt) => {
|
||||
const target = evt.target as HTMLInputElement;
|
||||
cb(target.checked);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(v: boolean) {
|
||||
this.inputEl.checked = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled: boolean) {
|
||||
this.inputEl.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class DropdownComponent extends Component {
|
||||
selectEl: HTMLSelectElement = document.createElement("select");
|
||||
private changeHandler: ((value: string) => any) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selectEl = document.createElement("select");
|
||||
}
|
||||
|
||||
addOption(v: string, d: string) {
|
||||
const option = document.createElement("option");
|
||||
option.value = v;
|
||||
option.textContent = d;
|
||||
this.selectEl.appendChild(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
addOptions(o: Record<string, string>) {
|
||||
for (const [value, display] of Object.entries(o)) {
|
||||
this.addOption(value, display);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
onChange(cb: (value: string) => any) {
|
||||
this.changeHandler = cb;
|
||||
this.selectEl.addEventListener("change", (evt) => {
|
||||
const target = evt.target as HTMLSelectElement;
|
||||
cb(target.value);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(v: string) {
|
||||
this.selectEl.value = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled: boolean) {
|
||||
this.selectEl.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class SliderComponent extends Component {
|
||||
inputEl: HTMLInputElement = document.createElement("input");
|
||||
private changeHandler: ((value: number) => any) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.inputEl = document.createElement("input");
|
||||
this.inputEl.type = "range";
|
||||
}
|
||||
|
||||
onChange(cb: (value: number) => any) {
|
||||
this.changeHandler = cb;
|
||||
this.inputEl.addEventListener("change", (evt) => {
|
||||
const target = evt.target as HTMLInputElement;
|
||||
cb(parseFloat(target.value));
|
||||
});
|
||||
this.inputEl.addEventListener("input", (evt) => {
|
||||
const target = evt.target as HTMLInputElement;
|
||||
cb(parseFloat(target.value));
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
setValue(v: number) {
|
||||
this.inputEl.value = String(v);
|
||||
return this;
|
||||
}
|
||||
|
||||
setMin(min: number) {
|
||||
this.inputEl.min = String(min);
|
||||
return this;
|
||||
}
|
||||
|
||||
setMax(max: number) {
|
||||
this.inputEl.max = String(max);
|
||||
return this;
|
||||
}
|
||||
|
||||
setStep(step: number) {
|
||||
this.inputEl.step = String(step);
|
||||
return this;
|
||||
}
|
||||
|
||||
setDisabled(disabled: boolean) {
|
||||
this.inputEl.disabled = disabled;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Setting {
|
||||
nameEl: HTMLElement;
|
||||
descEl: HTMLElement;
|
||||
controlEl: HTMLElement;
|
||||
infoEl: HTMLElement;
|
||||
|
||||
constructor(containerEl: HTMLElement) {
|
||||
this.nameEl = containerEl.createDiv();
|
||||
this.descEl = containerEl.createDiv();
|
||||
this.controlEl = containerEl.createDiv();
|
||||
this.infoEl = containerEl.createDiv();
|
||||
}
|
||||
setName(name: string) {
|
||||
this.nameEl.setText(name);
|
||||
return this;
|
||||
}
|
||||
setDesc(desc: string) {
|
||||
this.descEl.setText(desc);
|
||||
return this;
|
||||
}
|
||||
setClass(c: string) {
|
||||
this.controlEl.addClass(c);
|
||||
return this;
|
||||
}
|
||||
addText(cb: (text: TextComponent) => any) {
|
||||
const component = new TextComponent();
|
||||
this.controlEl.appendChild(component.inputEl);
|
||||
cb(component);
|
||||
return this;
|
||||
}
|
||||
addToggle(cb: (toggle: ToggleComponent) => any) {
|
||||
const component = new ToggleComponent();
|
||||
cb(component);
|
||||
return this;
|
||||
}
|
||||
addButton(cb: (btn: ButtonComponent) => any) {
|
||||
const btn = new ButtonComponent();
|
||||
this.controlEl.appendChild(btn.buttonEl);
|
||||
cb(btn);
|
||||
return this;
|
||||
}
|
||||
addDropdown(cb: (dropdown: DropdownComponent) => any) {
|
||||
const component = new DropdownComponent();
|
||||
cb(component);
|
||||
return this;
|
||||
}
|
||||
addSlider(cb: (slider: SliderComponent) => any) {
|
||||
const component = new SliderComponent();
|
||||
cb(component);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLElement extensions
|
||||
if (typeof HTMLElement !== "undefined") {
|
||||
const proto = HTMLElement.prototype as any;
|
||||
proto.createDiv = function (o?: any) {
|
||||
const div = document.createElement("div");
|
||||
if (o?.cls) div.addClass(o.cls);
|
||||
if (o?.text) div.setText(o.text);
|
||||
this.appendChild(div);
|
||||
return div;
|
||||
};
|
||||
proto.createEl = function (tag: string, o?: any) {
|
||||
const el = document.createElement(tag);
|
||||
if (o?.cls) el.addClass(o.cls);
|
||||
if (o?.text) el.setText(o.text);
|
||||
this.appendChild(el);
|
||||
return el;
|
||||
};
|
||||
proto.createSpan = function (o?: any) {
|
||||
return this.createEl("span", o);
|
||||
};
|
||||
proto.empty = function () {
|
||||
this.innerHTML = "";
|
||||
};
|
||||
proto.setText = function (t: string) {
|
||||
this.textContent = t;
|
||||
};
|
||||
proto.addClass = function (c: string) {
|
||||
this.classList.add(c);
|
||||
};
|
||||
proto.removeClass = function (c: string) {
|
||||
this.classList.remove(c);
|
||||
};
|
||||
proto.toggleClass = function (c: string, b: boolean) {
|
||||
this.classList.toggle(c, b);
|
||||
};
|
||||
proto.hasClass = function (c: string) {
|
||||
return this.classList.contains(c);
|
||||
};
|
||||
}
|
||||
|
||||
export class Editor {}
|
||||
|
||||
export class FuzzySuggestModal<T> {
|
||||
constructor(app: App) {}
|
||||
setPlaceholder(p: string) {}
|
||||
open() {}
|
||||
close() {}
|
||||
private __dummy(_: T): never {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
}
|
||||
export class MarkdownRenderer {
|
||||
static render(app: App, md: string, el: HTMLElement, path: string, component: Component) {
|
||||
el.innerHTML = md;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
export class MarkdownView {}
|
||||
export class TextAreaComponent extends Component {}
|
||||
export class ItemView {}
|
||||
export class WorkspaceLeaf {}
|
||||
|
||||
export function sanitizeHTMLToDom(html: string) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
return div;
|
||||
}
|
||||
|
||||
export function addIcon() {}
|
||||
export const debounce = (fn: any) => fn;
|
||||
export async function request(options: any) {
|
||||
const result = await requestUrl(options);
|
||||
return result.text;
|
||||
}
|
||||
|
||||
export async function requestUrl({
|
||||
body,
|
||||
headers,
|
||||
method,
|
||||
url,
|
||||
contentType,
|
||||
}: RequestUrlParam): Promise<RequestUrlResponse> {
|
||||
// console.log("[requestUrl] Mock called:", { method, url, contentType });
|
||||
const reqHeadersObj: Record<string, string> = {};
|
||||
for (const key of Object.keys(headers || {})) {
|
||||
reqHeadersObj[key.toLowerCase()] = headers[key];
|
||||
}
|
||||
if (contentType) {
|
||||
reqHeadersObj["content-type"] = contentType;
|
||||
}
|
||||
reqHeadersObj["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
||||
reqHeadersObj["Pragma"] = "no-cache";
|
||||
reqHeadersObj["Expires"] = "0";
|
||||
const result = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
...reqHeadersObj,
|
||||
},
|
||||
|
||||
body: body,
|
||||
});
|
||||
const headersObj: Record<string, string> = {};
|
||||
result.headers.forEach((value, key) => {
|
||||
headersObj[key] = value;
|
||||
});
|
||||
let json = undefined;
|
||||
let text = undefined;
|
||||
let arrayBuffer = undefined;
|
||||
try {
|
||||
const isJson = result.headers.get("content-type")?.includes("application/json");
|
||||
arrayBuffer = await result.arrayBuffer();
|
||||
const isText = result.headers.get("content-type")?.startsWith("text/");
|
||||
if (isText || isJson) {
|
||||
text = new TextDecoder().decode(arrayBuffer);
|
||||
}
|
||||
if (isJson) {
|
||||
json = await JSON.parse(text || "{}");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse response:", e);
|
||||
// ignore
|
||||
}
|
||||
return {
|
||||
status: result.status,
|
||||
headers: headersObj,
|
||||
text: text,
|
||||
json: json,
|
||||
arrayBuffer: arrayBuffer,
|
||||
};
|
||||
}
|
||||
export function stringifyYaml(obj: any) {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
export function parseYaml(s: string) {
|
||||
return JSON.parse(s);
|
||||
}
|
||||
export function getLanguage() {
|
||||
return "en";
|
||||
}
|
||||
export function setIcon(el: HTMLElement, icon: string) {}
|
||||
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||
}
|
||||
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer;
|
||||
}
|
||||
|
||||
export type DataWriteOptions = any;
|
||||
export type PluginManifest = any;
|
||||
export type RequestUrlParam = any;
|
||||
export type RequestUrlResponse = any;
|
||||
export type MarkdownFileInfo = any;
|
||||
export type ListedFiles = {
|
||||
files: string[];
|
||||
folders: string[];
|
||||
};
|
||||
|
||||
export type ValueComponent = any;
|
||||
51
test/harness/utils/intercept.ts
Normal file
51
test/harness/utils/intercept.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export function interceptFetchForLogging() {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (...params: any[]) => {
|
||||
const paramObj = params[0];
|
||||
const initObj = params[1];
|
||||
const url = typeof paramObj === "string" ? paramObj : paramObj.url;
|
||||
const method = initObj?.method || "GET";
|
||||
const headers = initObj?.headers || {};
|
||||
const body = initObj?.body || null;
|
||||
const headersObj: Record<string, string> = {};
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, key) => {
|
||||
headersObj[key] = value;
|
||||
});
|
||||
}
|
||||
console.dir({
|
||||
mockedFetch: {
|
||||
url,
|
||||
method,
|
||||
headers: headersObj,
|
||||
},
|
||||
});
|
||||
try {
|
||||
const res = await originalFetch.apply(globalThis, params as any);
|
||||
console.log(`[Obsidian Mock] Fetch response: ${res.status} ${res.statusText} for ${method} ${url}`);
|
||||
const resClone = res.clone();
|
||||
const contentType = resClone.headers.get("content-type") || "";
|
||||
const isJson = contentType.includes("application/json");
|
||||
if (isJson) {
|
||||
const data = await resClone.json();
|
||||
console.dir({ mockedFetchResponseJson: data });
|
||||
} else {
|
||||
const ab = await resClone.arrayBuffer();
|
||||
const text = new TextDecoder().decode(ab);
|
||||
const isText = /^text\//.test(contentType);
|
||||
if (isText) {
|
||||
console.dir({
|
||||
mockedFetchResponseText: ab.byteLength < 1000 ? text : text.slice(0, 1000) + "...(truncated)",
|
||||
});
|
||||
} else {
|
||||
console.log(`[Obsidian Mock] Fetch response is of content-type ${contentType}, not logging body.`);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} catch (e) {
|
||||
// console.error("[Obsidian Mock] Fetch error:", e);
|
||||
console.error(`[Obsidian Mock] Fetch failed for ${method} ${url}, error:`, e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
}
|
||||
145
test/lib/commands.ts
Normal file
145
test/lib/commands.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { P2PSyncSetting } from "@/lib/src/common/types";
|
||||
import { delay } from "octagonal-wheels/promises";
|
||||
import type { BrowserContext, Page } from "playwright";
|
||||
import type { Plugin } from "vitest/config";
|
||||
import type { BrowserCommand } from "vitest/node";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
export const grantClipboardPermissions: BrowserCommand = async (ctx) => {
|
||||
if (ctx.provider.name === "playwright") {
|
||||
await ctx.context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
console.log("Granted clipboard permissions");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let peerPage: Page | undefined;
|
||||
let peerPageContext: BrowserContext | undefined;
|
||||
let previousName = "";
|
||||
async function setValue(page: Page, selector: string, value: string) {
|
||||
const e = await page.waitForSelector(selector);
|
||||
await e.fill(value);
|
||||
}
|
||||
async function closePeerContexts() {
|
||||
const peerPageLocal = peerPage;
|
||||
const peerPageContextLocal = peerPageContext;
|
||||
if (peerPageLocal) {
|
||||
await peerPageLocal.close();
|
||||
}
|
||||
if (peerPageContextLocal) {
|
||||
await peerPageContextLocal.close();
|
||||
}
|
||||
}
|
||||
export const openWebPeer: BrowserCommand<[P2PSyncSetting, serverPeerName: string]> = async (
|
||||
ctx,
|
||||
setting: P2PSyncSetting,
|
||||
serverPeerName: string = "p2p-livesync-web-peer"
|
||||
) => {
|
||||
if (ctx.provider.name === "playwright") {
|
||||
const previousPage = ctx.page;
|
||||
if (peerPage !== undefined) {
|
||||
if (previousName === serverPeerName) {
|
||||
console.log(`WebPeer for ${serverPeerName} already opened`);
|
||||
return;
|
||||
}
|
||||
console.log(`Closing previous WebPeer for ${previousName}`);
|
||||
await closePeerContexts();
|
||||
}
|
||||
console.log(`Opening webPeer`);
|
||||
return serialized("webpeer", async () => {
|
||||
const browser = ctx.context.browser()!;
|
||||
const context = await browser.newContext();
|
||||
peerPageContext = context;
|
||||
peerPage = await context.newPage();
|
||||
previousName = serverPeerName;
|
||||
console.log(`Navigating...`);
|
||||
await peerPage.goto("http://localhost:8081");
|
||||
await peerPage.waitForLoadState();
|
||||
console.log(`Navigated!`);
|
||||
await setValue(peerPage, "#app > main [placeholder*=wss]", setting.P2P_relays);
|
||||
await setValue(peerPage, "#app > main [placeholder*=anything]", setting.P2P_roomID);
|
||||
await setValue(peerPage, "#app > main [placeholder*=password]", setting.P2P_passphrase);
|
||||
await setValue(peerPage, "#app > main [placeholder*=iphone]", serverPeerName);
|
||||
// await peerPage.getByTitle("Enable P2P Replicator").setChecked(true);
|
||||
await peerPage.getByRole("checkbox").first().setChecked(true);
|
||||
// (await peerPage.waitForSelector("Save and Apply")).click();
|
||||
await peerPage.getByText("Save and Apply").click();
|
||||
await delay(100);
|
||||
await peerPage.reload();
|
||||
await delay(500);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await delay(100);
|
||||
const btn = peerPage.getByRole("button").filter({ hasText: /^connect/i });
|
||||
if ((await peerPage.getByText(/disconnect/i).count()) > 0) {
|
||||
break;
|
||||
}
|
||||
await btn.click();
|
||||
}
|
||||
await previousPage.bringToFront();
|
||||
ctx.context.on("close", async () => {
|
||||
console.log("Browser context is closing, closing peer page if exists");
|
||||
await closePeerContexts();
|
||||
});
|
||||
console.log("Web peer page opened");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const closeWebPeer: BrowserCommand = async (ctx) => {
|
||||
if (ctx.provider.name === "playwright") {
|
||||
return serialized("webpeer", async () => {
|
||||
await closePeerContexts();
|
||||
peerPage = undefined;
|
||||
peerPageContext = undefined;
|
||||
previousName = "";
|
||||
console.log("Web peer page closed");
|
||||
});
|
||||
}
|
||||
};
|
||||
export const acceptWebPeer: BrowserCommand = async (ctx) => {
|
||||
if (peerPage) {
|
||||
// Detect dialogue
|
||||
const buttonsOnDialogs = await peerPage.$$("popup .buttons button");
|
||||
for (const b of buttonsOnDialogs) {
|
||||
const text = (await b.innerText()).toLowerCase();
|
||||
// console.log(`Dialog button found: ${text}`);
|
||||
if (text === "accept") {
|
||||
console.log("Accepting dialog");
|
||||
await b.click({ timeout: 300 });
|
||||
await delay(500);
|
||||
}
|
||||
}
|
||||
const buttons = peerPage.getByRole("button").filter({ hasText: /^accept$/i });
|
||||
const a = await buttons.all();
|
||||
for (const b of a) {
|
||||
await b.click({ timeout: 300 });
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export default function BrowserCommands(): Plugin {
|
||||
return {
|
||||
name: "vitest:custom-commands",
|
||||
config() {
|
||||
return {
|
||||
test: {
|
||||
browser: {
|
||||
commands: {
|
||||
grantClipboardPermissions,
|
||||
openWebPeer,
|
||||
closeWebPeer,
|
||||
acceptWebPeer,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
declare module "vitest/browser" {
|
||||
interface BrowserCommands {
|
||||
grantClipboardPermissions: () => Promise<void>;
|
||||
openWebPeer: (setting: P2PSyncSetting, serverPeerName: string) => Promise<void>;
|
||||
closeWebPeer: () => Promise<void>;
|
||||
acceptWebPeer: () => Promise<boolean>;
|
||||
}
|
||||
}
|
||||
70
test/lib/ui.ts
Normal file
70
test/lib/ui.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { page } from "vitest/browser";
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
|
||||
export async function waitForDialogShown(dialogText: string, timeout = 500) {
|
||||
const ttl = Date.now() + timeout;
|
||||
while (Date.now() < ttl) {
|
||||
try {
|
||||
await delay(50);
|
||||
const dialog = page
|
||||
.getByText(dialogText)
|
||||
.elements()
|
||||
.filter((e) => e.classList.contains("modal-title"))
|
||||
.filter((e) => e.checkVisibility());
|
||||
if (dialog.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export async function waitForDialogHidden(dialogText: string | RegExp, timeout = 500) {
|
||||
const ttl = Date.now() + timeout;
|
||||
while (Date.now() < ttl) {
|
||||
try {
|
||||
await delay(50);
|
||||
const dialog = page
|
||||
.getByText(dialogText)
|
||||
.elements()
|
||||
.filter((e) => e.classList.contains("modal-title"))
|
||||
.filter((e) => e.checkVisibility());
|
||||
if (dialog.length > 0) {
|
||||
// console.log(`Still exist ${dialogText.toString()}`);
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function waitForButtonClick(buttonText: string | RegExp, timeout = 500) {
|
||||
const ttl = Date.now() + timeout;
|
||||
while (Date.now() < ttl) {
|
||||
try {
|
||||
await delay(100);
|
||||
const buttons = page
|
||||
.getByText(buttonText)
|
||||
.elements()
|
||||
.filter((e) => e.checkVisibility() && e.tagName.toLowerCase() == "button");
|
||||
if (buttons.length == 0) {
|
||||
// console.log(`Could not found ${buttonText.toString()}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`Button detected: ${buttonText.toString()}`);
|
||||
// console.dir(buttons[0])
|
||||
await page.elementLocator(buttons[0]).click();
|
||||
await delay(100);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
21
test/lib/util.ts
Normal file
21
test/lib/util.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
|
||||
export async function waitTaskWithFollowups<T>(
|
||||
task: Promise<T>,
|
||||
followup: () => Promise<void>,
|
||||
timeout: number = 10000,
|
||||
interval: number = 1000
|
||||
): Promise<T> {
|
||||
const symbolNotCompleted = Symbol("notCompleted");
|
||||
const isCompleted = () => Promise.race([task, Promise.resolve(symbolNotCompleted)]);
|
||||
const ttl = Date.now() + timeout;
|
||||
do {
|
||||
const state = await isCompleted();
|
||||
if (state !== symbolNotCompleted) {
|
||||
return state;
|
||||
}
|
||||
await followup();
|
||||
await delay(interval);
|
||||
} while (Date.now() < ttl);
|
||||
throw new Error("Task did not complete in time");
|
||||
}
|
||||
33
test/shell/couchdb-init.sh
Executable file
33
test/shell/couchdb-init.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
if [[ -z "$hostname" ]]; then
|
||||
echo "ERROR: Hostname missing"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$username" ]]; then
|
||||
echo "ERROR: Username missing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$password" ]]; then
|
||||
echo "ERROR: Password missing"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$node" ]]; then
|
||||
echo "INFO: defaulting to _local"
|
||||
node=_local
|
||||
fi
|
||||
|
||||
echo "-- Configuring CouchDB by REST APIs... -->"
|
||||
|
||||
until (curl -X POST "${hostname}/_cluster_setup" -H "Content-Type: application/json" -d "{\"action\":\"enable_single_node\",\"username\":\"${username}\",\"password\":\"${password}\",\"bind_address\":\"0.0.0.0\",\"port\":5984,\"singlenode\":true}" --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd_auth/require_valid_user" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/httpd/WWW-Authenticate" -H "Content-Type: application/json" -d '"Basic realm=\"couchdb\""' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/httpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/enable_cors" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/chttpd/max_http_request_size" -H "Content-Type: application/json" -d '"4294967296"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/couchdb/max_document_size" -H "Content-Type: application/json" -d '"50000000"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/cors/credentials" -H "Content-Type: application/json" -d '"true"' --user "${username}:${password}"); do sleep 5; done
|
||||
until (curl -X PUT "${hostname}/_node/${node}/_config/cors/origins" -H "Content-Type: application/json" -d '"*"' --user "${username}:${password}"); do sleep 5; done
|
||||
|
||||
echo "<-- Configuring CouchDB by REST APIs Done!"
|
||||
3
test/shell/couchdb-start.sh
Executable file
3
test/shell/couchdb-start.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
docker run -d --name couchdb-test -p 5989:5984 -e COUCHDB_USER=$username -e COUCHDB_PASSWORD=$password couchdb:3.5.0
|
||||
3
test/shell/couchdb-stop.sh
Executable file
3
test/shell/couchdb-stop.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
docker stop couchdb-test
|
||||
docker rm couchdb-test
|
||||
47
test/shell/minio-init.sh
Executable file
47
test/shell/minio-init.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cat >/tmp/mybucket-rw.json <<EOF
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:GetBucketLocation","s3:ListBucket"],
|
||||
"Resource": ["arn:aws:s3:::$bucketName"]
|
||||
},
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:GetObject","s3:PutObject","s3:DeleteObject"],
|
||||
"Resource": ["arn:aws:s3:::$bucketName/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
# echo "<CORSConfiguration>
|
||||
# <CORSRule>
|
||||
# <AllowedOrigin>http://localhost:63315</AllowedOrigin>
|
||||
# <AllowedOrigin>http://localhost:63316</AllowedOrigin>
|
||||
# <AllowedOrigin>http://localhost</AllowedOrigin>
|
||||
# <AllowedMethod>GET</AllowedMethod>
|
||||
# <AllowedMethod>PUT</AllowedMethod>
|
||||
# <AllowedMethod>POST</AllowedMethod>
|
||||
# <AllowedMethod>DELETE</AllowedMethod>
|
||||
# <AllowedMethod>HEAD</AllowedMethod>
|
||||
# <AllowedHeader>*</AllowedHeader>
|
||||
# </CORSRule>
|
||||
# </CORSConfiguration>" > /tmp/cors.xml
|
||||
# docker run --rm --network host -v /tmp/mybucket-rw.json:/tmp/mybucket-rw.json --entrypoint=/bin/sh minio/mc -c "
|
||||
# mc alias set myminio $minioEndpoint $username $password
|
||||
# mc mb --ignore-existing myminio/$bucketName
|
||||
# mc admin policy create myminio my-custom-policy /tmp/mybucket-rw.json
|
||||
# echo 'Creating service account for user $username with access key $accessKey'
|
||||
# mc admin user svcacct add --access-key '$accessKey' --secret-key '$secretKey' myminio '$username'
|
||||
# mc admin policy attach myminio my-custom-policy --user '$accessKey'
|
||||
# echo 'Verifying policy and user creation:'
|
||||
# mc admin user svcacct info myminio '$accessKey'
|
||||
# "
|
||||
|
||||
docker run --rm --network host -v /tmp/mybucket-rw.json:/tmp/mybucket-rw.json --entrypoint=/bin/sh minio/mc -c "
|
||||
mc alias set myminio $minioEndpoint $accessKey $secretKey
|
||||
mc mb --ignore-existing myminio/$bucketName
|
||||
"
|
||||
2
test/shell/minio-start.sh
Executable file
2
test/shell/minio-start.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
docker run -d --name minio-test -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=$accessKey -e MINIO_ROOT_PASSWORD=$secretKey -e MINIO_SERVER_URL=$minioEndpoint minio/minio server /data --console-address ':9001'
|
||||
3
test/shell/minio-stop.sh
Executable file
3
test/shell/minio-stop.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
docker stop minio-test
|
||||
docker rm minio-test
|
||||
2
test/shell/p2p-init.sh
Executable file
2
test/shell/p2p-init.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
echo "P2P Init - No additional initialization required."
|
||||
8
test/shell/p2p-start.sh
Executable file
8
test/shell/p2p-start.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
script_dir=$(dirname "$0")
|
||||
webpeer_dir=$script_dir/../../src/lib/apps/webpeer
|
||||
|
||||
docker run -d --name relay-test -p 4000:8080 scsibug/nostr-rs-relay:latest
|
||||
npm run --prefix $webpeer_dir build
|
||||
docker run -d --name webpeer-test -p 8081:8043 -v $webpeer_dir/dist:/srv/http pierrezemb/gostatic
|
||||
5
test/shell/p2p-stop.sh
Executable file
5
test/shell/p2p-stop.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
docker stop relay-test
|
||||
docker rm relay-test
|
||||
docker stop webpeer-test
|
||||
docker rm webpeer-test
|
||||
129
test/suite/db_common.ts
Normal file
129
test/suite/db_common.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { compareMTime, EVEN } from "@/common/utils";
|
||||
import { TFile, type DataWriteOptions } from "@/deps";
|
||||
import type { FilePath } from "@/lib/src/common/types";
|
||||
import { isDocContentSame, readContent } from "@/lib/src/common/utils";
|
||||
import { waitForIdle, type LiveSyncHarness } from "../harness/harness";
|
||||
import { expect } from "vitest";
|
||||
|
||||
export const defaultFileOption = {
|
||||
mtime: new Date(2026, 0, 1, 0, 1, 2, 3).getTime(),
|
||||
} as const satisfies DataWriteOptions;
|
||||
export async function storeFile(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
content: string | Blob,
|
||||
deleteBeforeSend = false,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
if (deleteBeforeSend && harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
if (content instanceof Blob) {
|
||||
console.log(`Creating binary file ${path}`);
|
||||
await harness.app.vault.createBinary(path, await content.arrayBuffer(), fileOptions);
|
||||
} else {
|
||||
await harness.app.vault.create(path, content, fileOptions);
|
||||
}
|
||||
|
||||
// Ensure file is created
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
expect(compareMTime(file.stat.mtime, fileOptions?.mtime ?? defaultFileOption.mtime)).toBe(EVEN);
|
||||
if (content instanceof Blob) {
|
||||
const readContent = await harness.app.vault.readBinary(file);
|
||||
expect(await isDocContentSame(readContent, content)).toBe(true);
|
||||
} else {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
}
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
return file;
|
||||
}
|
||||
export async function readFromLocalDB(harness: LiveSyncHarness, path: string) {
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
return entry;
|
||||
}
|
||||
export async function readFromVault(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
isBinary: boolean = false,
|
||||
fileOptions = defaultFileOption
|
||||
): Promise<string | ArrayBuffer> {
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
// console.log(`MTime: ${file.stat.mtime}, Expected: ${fileOptions.mtime}`);
|
||||
if (fileOptions.mtime !== undefined) {
|
||||
expect(compareMTime(file.stat.mtime, fileOptions.mtime)).toBe(EVEN);
|
||||
}
|
||||
const content = isBinary ? await harness.app.vault.readBinary(file) : await harness.app.vault.read(file);
|
||||
return content;
|
||||
}
|
||||
|
||||
throw new Error("File not found in vault");
|
||||
}
|
||||
export async function checkStoredFileInDB(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
content: string | Blob,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
const entry = await readFromLocalDB(harness, path);
|
||||
if (entry === false) {
|
||||
throw new Error("DB Content not found");
|
||||
}
|
||||
const contentToCheck = content instanceof Blob ? await content.arrayBuffer() : content;
|
||||
const isDocSame = await isDocContentSame(readContent(entry), contentToCheck);
|
||||
if (fileOptions.mtime !== undefined) {
|
||||
expect(compareMTime(entry.mtime, fileOptions.mtime)).toBe(EVEN);
|
||||
}
|
||||
expect(isDocSame).toBe(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
export async function testFileWrite(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
content: string | Blob,
|
||||
skipCheckToBeWritten = false,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
const file = await storeFile(harness, path, content, false, fileOptions);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
const vaultFile = await readFromVault(harness, path, content instanceof Blob, fileOptions);
|
||||
expect(await isDocContentSame(vaultFile, content)).toBe(true);
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
if (skipCheckToBeWritten) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
await checkStoredFileInDB(harness, path, content);
|
||||
return Promise.resolve();
|
||||
}
|
||||
export async function testFileRead(
|
||||
harness: LiveSyncHarness,
|
||||
path: string,
|
||||
expectedContent: string | Blob,
|
||||
fileOptions = defaultFileOption
|
||||
) {
|
||||
await waitForIdle(harness);
|
||||
const file = await readFromVault(harness, path, expectedContent instanceof Blob, fileOptions);
|
||||
const isDocSame = await isDocContentSame(file, expectedContent);
|
||||
expect(isDocSame).toBe(true);
|
||||
// Check local database entry
|
||||
const entry = await readFromLocalDB(harness, path);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry === false) {
|
||||
throw new Error("DB Content not found");
|
||||
}
|
||||
const isDBDocSame = await isDocContentSame(readContent(entry), expectedContent);
|
||||
expect(isDBDocSame).toBe(true);
|
||||
return await Promise.resolve();
|
||||
}
|
||||
125
test/suite/onlylocaldb.test.ts
Normal file
125
test/suite/onlylocaldb.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { TFile } from "@/deps.ts";
|
||||
import { DEFAULT_SETTINGS, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
import { isDocContentSame, readContent } from "@/lib/src/common/utils";
|
||||
import { DummyFileSourceInisialised, generateBinaryFile, generateFile, init } from "../utils/dummyfile";
|
||||
|
||||
const localdb_test_setting = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
describe.skip("Plugin Integration Test (Local Database)", async () => {
|
||||
let harness: LiveSyncHarness;
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
harness = await generateHarness(vaultName, localdb_test_setting);
|
||||
await waitForReady(harness);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", async () => {
|
||||
expect(harness.plugin).toBeDefined();
|
||||
expect(harness.plugin.app).toBe(harness.app);
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
it("should have services initialized", async () => {
|
||||
expect(harness.plugin.services).toBeDefined();
|
||||
return await Promise.resolve();
|
||||
});
|
||||
it("should have local database initialized", async () => {
|
||||
expect(harness.plugin.localDatabase).toBeDefined();
|
||||
expect(harness.plugin.localDatabase.isReady).toBe(true);
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
it("should store the changes into the local database", async () => {
|
||||
const path = "test-store6.md";
|
||||
const content = "Hello, World!";
|
||||
if (harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
await harness.app.vault.create(path, content);
|
||||
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
|
||||
if (file instanceof TFile) {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
// await delay(100); // Wait a bit for the local database to process
|
||||
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
expect(readContent(entry)).toBe(content);
|
||||
}
|
||||
return await Promise.resolve();
|
||||
});
|
||||
test.each([10, 100, 1000, 10000, 50000, 100000])("should handle large file of size %i bytes", async (size) => {
|
||||
const path = `test-large-file-${size}.md`;
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
if (harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
await harness.app.vault.create(path, content);
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
const readContent = await harness.app.vault.read(file);
|
||||
expect(readContent).toBe(content);
|
||||
}
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
expect(readContent(entry)).toBe(content);
|
||||
}
|
||||
return await Promise.resolve();
|
||||
});
|
||||
|
||||
const binaryMap = Array.from({ length: 7 }, (_, i) => Math.pow(2, i * 4));
|
||||
test.each(binaryMap)("should handle binary file of size %i bytes", async (size) => {
|
||||
const path = `test-binary-file-${size}.bin`;
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
if (harness.app.vault.getAbstractFileByPath(path)) {
|
||||
console.log(`Deleting existing file ${path}`);
|
||||
await harness.app.vault.delete(harness.app.vault.getAbstractFileByPath(path) as TFile);
|
||||
}
|
||||
// Create file via vault
|
||||
await harness.app.vault.createBinary(path, await content.arrayBuffer());
|
||||
const file = harness.app.vault.getAbstractFileByPath(path);
|
||||
expect(file).toBeInstanceOf(TFile);
|
||||
if (file instanceof TFile) {
|
||||
const readContent = await harness.app.vault.readBinary(file);
|
||||
expect(await isDocContentSame(readContent, content)).toBe(true);
|
||||
}
|
||||
|
||||
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
|
||||
await waitForIdle(harness);
|
||||
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
expect(entry).not.toBe(false);
|
||||
if (entry) {
|
||||
const entryContent = await readContent(entry);
|
||||
if (!(entryContent instanceof ArrayBuffer)) {
|
||||
throw new Error("Entry content is not an ArrayBuffer");
|
||||
}
|
||||
// const expectedContent = await content.arrayBuffer();
|
||||
expect(await isDocContentSame(entryContent, content)).toBe(true);
|
||||
}
|
||||
return await Promise.resolve();
|
||||
});
|
||||
});
|
||||
275
test/suite/sync.senario.basic.ts
Normal file
275
test/suite/sync.senario.basic.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { afterAll, beforeAll, describe, expect, it, test } from "vitest";
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { RemoteTypes, type FilePath, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
|
||||
import {
|
||||
DummyFileSourceInisialised,
|
||||
FILE_SIZE_BINS,
|
||||
FILE_SIZE_MD,
|
||||
generateBinaryFile,
|
||||
generateFile,
|
||||
} from "../utils/dummyfile";
|
||||
import { checkStoredFileInDB, testFileRead, testFileWrite } from "./db_common";
|
||||
import { delay } from "@/lib/src/common/utils";
|
||||
import { commands } from "vitest/browser";
|
||||
import { closeReplication, performReplication, prepareRemote } from "./sync_common";
|
||||
import type { DataWriteOptions } from "@/deps.ts";
|
||||
|
||||
type MTimedDataWriteOptions = DataWriteOptions & { mtime: number };
|
||||
export type TestOptions = {
|
||||
setting: ObsidianLiveSyncSettings;
|
||||
fileOptions: MTimedDataWriteOptions;
|
||||
};
|
||||
function generateName(prefix: string, type: string, ext: string, size: number) {
|
||||
return `${prefix}-${type}-file-${size}.${ext}`;
|
||||
}
|
||||
export function syncBasicCase(label: string, { setting, fileOptions }: TestOptions) {
|
||||
describe("Replication Suite Tests - " + label, () => {
|
||||
const nameFile = (type: string, ext: string, size: number) => generateName("sync-test", type, ext, size);
|
||||
let serverPeerName = "";
|
||||
// TODO: Harness disposal may broke the event loop of P2P replication
|
||||
// so we keep the harnesses alive until all tests are done.
|
||||
// It may trystero's somethong, or not.
|
||||
let harnessUpload: LiveSyncHarness;
|
||||
let harnessDownload: LiveSyncHarness;
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// await commands.closeWebPeer();
|
||||
serverPeerName = "t-" + Date.now();
|
||||
setting.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
setting.P2P_AutoSyncPeers = serverPeerName;
|
||||
setting.P2P_DevicePeerName = "client-" + Date.now();
|
||||
await commands.openWebPeer(setting, serverPeerName);
|
||||
}
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
await commands.closeWebPeer();
|
||||
// await closeP2PReplicatorConnections(harnessUpload);
|
||||
}
|
||||
});
|
||||
|
||||
describe("Remote Database Initialization", () => {
|
||||
let harnessInit: LiveSyncHarness;
|
||||
const sync_test_setting_init = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Remote Database Initialization - Vault: ${vaultName}`);
|
||||
harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
expect(harnessInit.plugin).toBeDefined();
|
||||
expect(harnessInit.plugin.app).toBe(harnessInit.app);
|
||||
await waitForIdle(harnessInit);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harnessInit.plugin.services.replicator.getActiveReplicator()?.closeReplication();
|
||||
await harnessInit.dispose();
|
||||
await delay(1000);
|
||||
});
|
||||
|
||||
it("should reset remote database", async () => {
|
||||
// harnessInit = await generateHarness(vaultName, sync_test_setting_init);
|
||||
await waitForReady(harnessInit);
|
||||
await prepareRemote(harnessInit, sync_test_setting_init, true);
|
||||
});
|
||||
it("should be prepared for replication", async () => {
|
||||
await waitForReady(harnessInit);
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
const status = await harnessInit.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(sync_test_setting_init);
|
||||
console.log("Connected devices after reset:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Upload", () => {
|
||||
const sync_test_setting_upload = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Upload - Vault: ${vaultName}`);
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
sync_test_setting_upload.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
sync_test_setting_upload.P2P_AutoSyncPeers = serverPeerName;
|
||||
sync_test_setting_upload.P2P_DevicePeerName = "up-" + Date.now();
|
||||
}
|
||||
harnessUpload = await generateHarness(vaultName, sync_test_setting_upload);
|
||||
await waitForReady(harnessUpload);
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
await waitForIdle(harnessUpload);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessUpload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessUpload.plugin).toBeDefined();
|
||||
expect(harnessUpload.plugin.app).toBe(harnessUpload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessUpload.plugin.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessUpload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessUpload.plugin.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should prepare remote database", async () => {
|
||||
await prepareRemote(harnessUpload, sync_test_setting_upload, false);
|
||||
});
|
||||
|
||||
// describe("File Creation", async () => {
|
||||
it("should a file has been created", async () => {
|
||||
const content = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
// Perform replication
|
||||
// await harness.plugin.services.replication.replicate(true);
|
||||
});
|
||||
it("should different content of several files have been created correctly", async () => {
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-2", "md", 0), "Content B", false, fileOptions);
|
||||
await testFileWrite(harnessUpload, nameFile("test-diff-3", "md", 0), "Content C", false, fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should large file of size %i bytes has been created", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await testFileWrite(harnessUpload, path, content, false, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes has been created", async (size) => {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
const path = nameFile("binary", "bin", size);
|
||||
await testFileWrite(harnessUpload, path, content, true, fileOptions);
|
||||
const isTooLarge = harnessUpload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(true).toBe(true);
|
||||
} else {
|
||||
await checkStoredFileInDB(harnessUpload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
it("Replication after uploads", async () => {
|
||||
await performReplication(harnessUpload);
|
||||
await performReplication(harnessUpload);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Replication - Download", () => {
|
||||
// Download into a new vault
|
||||
const sync_test_setting_download = {
|
||||
...setting,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
beforeAll(async () => {
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
console.log(`BeforeAll - Replication Download - Vault: ${vaultName}`);
|
||||
if (setting.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
sync_test_setting_download.P2P_AutoAcceptingPeers = serverPeerName;
|
||||
sync_test_setting_download.P2P_AutoSyncPeers = serverPeerName;
|
||||
sync_test_setting_download.P2P_DevicePeerName = "down-" + Date.now();
|
||||
}
|
||||
harnessDownload = await generateHarness(vaultName, sync_test_setting_download);
|
||||
await waitForReady(harnessDownload);
|
||||
await prepareRemote(harnessDownload, sync_test_setting_download, false);
|
||||
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
await delay(1000);
|
||||
await performReplication(harnessDownload);
|
||||
await waitForIdle(harnessDownload);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeReplication(harnessDownload);
|
||||
});
|
||||
|
||||
it("should be instantiated and defined", () => {
|
||||
expect(harnessDownload.plugin).toBeDefined();
|
||||
expect(harnessDownload.plugin.app).toBe(harnessDownload.app);
|
||||
});
|
||||
|
||||
it("should have services initialized", () => {
|
||||
expect(harnessDownload.plugin.services).toBeDefined();
|
||||
});
|
||||
|
||||
it("should have local database initialized", () => {
|
||||
expect(harnessDownload.plugin.localDatabase).toBeDefined();
|
||||
expect(harnessDownload.plugin.localDatabase.isReady).toBe(true);
|
||||
});
|
||||
|
||||
it("should a file has been synchronised", async () => {
|
||||
const expectedContent = "Hello, World!";
|
||||
const path = nameFile("store", "md", 0);
|
||||
await testFileRead(harnessDownload, path, expectedContent, fileOptions);
|
||||
});
|
||||
it("should different content of several files have been synchronised", async () => {
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-1", "md", 0), "Content A", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-2", "md", 0), "Content B", fileOptions);
|
||||
await testFileRead(harnessDownload, nameFile("test-diff-3", "md", 0), "Content C", fileOptions);
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_MD)("should the file %i bytes had been synchronised", async (size) => {
|
||||
const content = Array.from(generateFile(size)).join("");
|
||||
const path = nameFile("large", "md", size);
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
|
||||
test.each(FILE_SIZE_BINS)("should binary file of size %i bytes had been synchronised", async (size) => {
|
||||
const path = nameFile("binary", "bin", size);
|
||||
|
||||
const isTooLarge = harnessDownload.plugin.services.vault.isFileSizeTooLarge(size);
|
||||
if (isTooLarge) {
|
||||
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
|
||||
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
|
||||
expect(entry).toBe(false);
|
||||
} else {
|
||||
const content = new Blob([...generateBinaryFile(size)], { type: "application/octet-stream" });
|
||||
await testFileRead(harnessDownload, path, content, fileOptions);
|
||||
}
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
if (harnessDownload) {
|
||||
await closeReplication(harnessDownload);
|
||||
await harnessDownload.dispose();
|
||||
await delay(1000);
|
||||
}
|
||||
if (harnessUpload) {
|
||||
await closeReplication(harnessUpload);
|
||||
await harnessUpload.dispose();
|
||||
await delay(1000);
|
||||
}
|
||||
});
|
||||
it("Wait for idle state", async () => {
|
||||
await delay(100);
|
||||
});
|
||||
});
|
||||
}
|
||||
50
test/suite/sync.single.test.ts
Normal file
50
test/suite/sync.single.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
import { settingBase } from "./variables.ts";
|
||||
const sync_test_setting_base = settingBase;
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
const remoteTypes = [RemoteTypes.REMOTE_COUCHDB];
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
const e2eeOptions = [false];
|
||||
// const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe.skip("Replication Suite Tests (Single)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
50
test/suite/sync.test.ts
Normal file
50
test/suite/sync.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
|
||||
import { defaultFileOption } from "./db_common";
|
||||
import { syncBasicCase } from "./sync.senario.basic.ts";
|
||||
import { settingBase } from "./variables.ts";
|
||||
const sync_test_setting_base = settingBase;
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO];
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
const e2eeOptions = [false, true];
|
||||
// const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Replication Suite Tests (Normal)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
117
test/suite/sync_common.ts
Normal file
117
test/suite/sync_common.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { expect } from "vitest";
|
||||
import { waitForIdle, type LiveSyncHarness } from "../harness/harness";
|
||||
import { LOG_LEVEL_INFO, RemoteTypes, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
|
||||
import { delay, fireAndForget } from "@/lib/src/common/utils";
|
||||
import { commands } from "vitest/browser";
|
||||
import { LiveSyncTrysteroReplicator } from "@/lib/src/replication/trystero/LiveSyncTrysteroReplicator";
|
||||
import { waitTaskWithFollowups } from "../lib/util";
|
||||
async function waitForP2PPeers(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// Wait for peers to connect
|
||||
const maxRetries = 20;
|
||||
let retries = maxRetries;
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
const p2pReplicator = await replicator.getP2PConnection(LOG_LEVEL_INFO);
|
||||
if (!p2pReplicator) {
|
||||
throw new Error("P2P Replicator is not initialized");
|
||||
}
|
||||
while (retries-- > 0) {
|
||||
fireAndForget(() => commands.acceptWebPeer());
|
||||
await delay(1000);
|
||||
const peers = p2pReplicator.knownAdvertisements;
|
||||
|
||||
if (peers && peers.length > 0) {
|
||||
console.log("P2P peers connected:", peers);
|
||||
return;
|
||||
}
|
||||
fireAndForget(() => commands.acceptWebPeer());
|
||||
console.log(`Waiting for any P2P peers to be connected... ${maxRetries - retries}/${maxRetries}`);
|
||||
console.dir(peers);
|
||||
await delay(1000);
|
||||
}
|
||||
console.log("Failed to connect P2P peers after retries");
|
||||
throw new Error("P2P peers did not connect in time.");
|
||||
}
|
||||
}
|
||||
export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
|
||||
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
|
||||
}
|
||||
replicator.closeReplication();
|
||||
await delay(30);
|
||||
replicator.closeReplication();
|
||||
await delay(1000);
|
||||
console.log("P2P replicator connections closed");
|
||||
// if (replicator instanceof LiveSyncTrysteroReplicator) {
|
||||
// replicator.closeReplication();
|
||||
// await delay(1000);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
export async function performReplication(harness: LiveSyncHarness) {
|
||||
await waitForP2PPeers(harness);
|
||||
await delay(500);
|
||||
const p = harness.plugin.services.replication.replicate(true);
|
||||
const task =
|
||||
harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P
|
||||
? waitTaskWithFollowups(
|
||||
p,
|
||||
() => {
|
||||
// Accept any peer dialogs during replication (fire and forget)
|
||||
fireAndForget(() => commands.acceptWebPeer());
|
||||
return Promise.resolve();
|
||||
},
|
||||
30000,
|
||||
500
|
||||
)
|
||||
: p;
|
||||
const result = await task;
|
||||
// await waitForIdle(harness);
|
||||
// if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
// await closeP2PReplicatorConnections(harness);
|
||||
// }
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function closeReplication(harness: LiveSyncHarness) {
|
||||
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
|
||||
return await closeP2PReplicatorConnections(harness);
|
||||
}
|
||||
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
|
||||
if (!replicator) {
|
||||
console.log("No active replicator to close");
|
||||
return;
|
||||
}
|
||||
await replicator.closeReplication();
|
||||
await waitForIdle(harness);
|
||||
console.log("Replication closed");
|
||||
}
|
||||
|
||||
export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianLiveSyncSettings, shouldReset = false) {
|
||||
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
|
||||
if (shouldReset) {
|
||||
await delay(1000);
|
||||
await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryResetRemoteDatabase(harness.plugin.settings);
|
||||
} else {
|
||||
await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.tryCreateRemoteDatabase(harness.plugin.settings);
|
||||
}
|
||||
await harness.plugin.services.replicator.getActiveReplicator()?.markRemoteResolved(harness.plugin.settings);
|
||||
// No exceptions should be thrown
|
||||
const status = await harness.plugin.services.replicator
|
||||
.getActiveReplicator()
|
||||
?.getRemoteStatus(harness.plugin.settings);
|
||||
console.log("Remote status:", status);
|
||||
expect(status).not.toBeFalsy();
|
||||
}
|
||||
}
|
||||
39
test/suite/variables.ts
Normal file
39
test/suite/variables.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { DoctorRegulation } from "@/lib/src/common/configForDoc";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
ChunkAlgorithms,
|
||||
AutoAccepting,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
export const env = (import.meta as any).env;
|
||||
export const settingBase = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
couchDB_URI: `${env.hostname}`,
|
||||
couchDB_DBNAME: `${env.dbname}`,
|
||||
couchDB_USER: `${env.username}`,
|
||||
couchDB_PASSWORD: `${env.password}`,
|
||||
bucket: `${env.bucketName}`,
|
||||
region: "us-east-1",
|
||||
endpoint: `${env.minioEndpoint}`,
|
||||
accessKey: `${env.accessKey}`,
|
||||
secretKey: `${env.secretKey}`,
|
||||
useCustomRequestHandler: true,
|
||||
forcePathStyle: true,
|
||||
bucketPrefix: "",
|
||||
usePluginSyncV2: true,
|
||||
chunkSplitterVersion: ChunkAlgorithms.RabinKarp,
|
||||
doctorProcessedVersion: DoctorRegulation.version,
|
||||
notifyThresholdOfRemoteStorageSize: 800,
|
||||
P2P_AutoAccepting: AutoAccepting.ALL,
|
||||
P2P_AutoBroadcast: true,
|
||||
P2P_AutoStart: true,
|
||||
P2P_Enabled: true,
|
||||
P2P_passphrase: "p2psync-test",
|
||||
P2P_roomID: "p2psync-test",
|
||||
P2P_DevicePeerName: "p2psync-test",
|
||||
P2P_relays: "ws://localhost:4000/",
|
||||
P2P_AutoAcceptingPeers: "p2p-livesync-web-peer",
|
||||
P2P_SyncOnReplication: "p2p-livesync-web-peer",
|
||||
} as ObsidianLiveSyncSettings;
|
||||
51
test/suitep2p/syncp2p.test.ts
Normal file
51
test/suitep2p/syncp2p.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Functional Test on Main Cases
|
||||
// This test suite only covers main functional cases of synchronisation. Event handling, error cases,
|
||||
// and edge, resolving conflicts, etc. will be covered in separate test suites.
|
||||
import { describe } from "vitest";
|
||||
import {
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
RemoteTypes,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "@/lib/src/common/types";
|
||||
|
||||
import { settingBase } from "../suite/variables.ts";
|
||||
import { defaultFileOption } from "../suite/db_common";
|
||||
import { syncBasicCase } from "../suite/sync.senario.basic.ts";
|
||||
|
||||
export const env = (import.meta as any).env;
|
||||
function* generateCase() {
|
||||
const sync_test_setting_base = settingBase;
|
||||
const passpharse = "thetest-Passphrase3+9-for-e2ee!";
|
||||
const REMOTE_RECOMMENDED = {
|
||||
[RemoteTypes.REMOTE_COUCHDB]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
[RemoteTypes.REMOTE_MINIO]: PREFERRED_JOURNAL_SYNC,
|
||||
[RemoteTypes.REMOTE_P2P]: PREFERRED_SETTING_SELF_HOSTED,
|
||||
};
|
||||
// const remoteTypes = [RemoteTypes.REMOTE_COUCHDB, RemoteTypes.REMOTE_MINIO, RemoteTypes.REMOTE_P2P];
|
||||
const remoteTypes = [RemoteTypes.REMOTE_P2P];
|
||||
// const e2eeOptions = [false, true];
|
||||
const e2eeOptions = [true];
|
||||
for (const remoteType of remoteTypes) {
|
||||
for (const useE2EE of e2eeOptions) {
|
||||
yield {
|
||||
setting: {
|
||||
...sync_test_setting_base,
|
||||
...REMOTE_RECOMMENDED[remoteType],
|
||||
remoteType,
|
||||
encrypt: useE2EE,
|
||||
passphrase: useE2EE ? passpharse : "",
|
||||
usePathObfuscation: useE2EE,
|
||||
} as ObsidianLiveSyncSettings,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("Replication Suite Tests (P2P)", async () => {
|
||||
const cases = Array.from(generateCase());
|
||||
const fileOptions = defaultFileOption;
|
||||
describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
syncBasicCase(`Remote: ${setting.remoteType}, E2EE: ${setting.encrypt}`, { setting, fileOptions });
|
||||
});
|
||||
});
|
||||
53
test/testtest/dummyfile.test.ts
Normal file
53
test/testtest/dummyfile.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { writeFile } from "../utils/fileapi.vite";
|
||||
import { DummyFileSourceInisialised, generateBinaryFile, generateFile } from "../utils/dummyfile";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("Test File Teet", async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
|
||||
it("should generate binary file correctly", async () => {
|
||||
const size = 5000;
|
||||
let generatedSize = 0;
|
||||
const chunks: Uint8Array[] = [];
|
||||
const generator = generateBinaryFile(size);
|
||||
const blob = new Blob([...generator], { type: "application/octet-stream" });
|
||||
const buf = await blob.arrayBuffer();
|
||||
const hexDump = new Uint8Array(buf)
|
||||
//@ts-ignore
|
||||
.toHex()
|
||||
.match(/.{1,32}/g)
|
||||
?.join("\n");
|
||||
const secondDummy = generateBinaryFile(size);
|
||||
const secondBlob = new Blob([...secondDummy], { type: "application/octet-stream" });
|
||||
const secondBuf = await secondBlob.arrayBuffer();
|
||||
const secondHexDump = new Uint8Array(secondBuf)
|
||||
//@ts-ignore
|
||||
.toHex()
|
||||
.match(/.{1,32}/g)
|
||||
?.join("\n");
|
||||
if (hexDump !== secondHexDump) {
|
||||
throw new Error("Generated binary files do not match");
|
||||
}
|
||||
expect(hexDump).toBe(secondHexDump);
|
||||
// await writeFile("test/testtest/dummyfile.test.bin", buf);
|
||||
// await writeFile("test/testtest/dummyfile.test.bin.hexdump.txt", hexDump || "");
|
||||
});
|
||||
it("should generate text file correctly", async () => {
|
||||
const size = 25000;
|
||||
let generatedSize = 0;
|
||||
let content = "";
|
||||
const generator = generateFile(size);
|
||||
const out = [...generator];
|
||||
// const blob = new Blob(out, { type: "text/plain" });
|
||||
content = out.join("");
|
||||
|
||||
const secondDummy = generateFile(size);
|
||||
const secondOut = [...secondDummy];
|
||||
const secondContent = secondOut.join("");
|
||||
if (content !== secondContent) {
|
||||
throw new Error("Generated text files do not match");
|
||||
}
|
||||
expect(content).toBe(secondContent);
|
||||
// await writeFile("test/testtest/dummyfile.test.txt", await blob.text());
|
||||
});
|
||||
});
|
||||
94
test/unit/dialog.test.ts
Normal file
94
test/unit/dialog.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
// Dialog Unit Tests
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { commands } from "vitest/browser";
|
||||
|
||||
import { generateHarness, waitForIdle, waitForReady, type LiveSyncHarness } from "../harness/harness";
|
||||
import { ChunkAlgorithms, DEFAULT_SETTINGS, type ObsidianLiveSyncSettings } from "@/lib/src/common/types";
|
||||
|
||||
import { DummyFileSourceInisialised } from "../utils/dummyfile";
|
||||
|
||||
import { page } from "vitest/browser";
|
||||
import { DoctorRegulation } from "@/lib/src/common/configForDoc";
|
||||
import { waitForDialogHidden, waitForDialogShown } from "../lib/ui";
|
||||
const env = (import.meta as any).env;
|
||||
const dialog_setting_base = {
|
||||
...DEFAULT_SETTINGS,
|
||||
isConfigured: true,
|
||||
handleFilenameCaseSensitive: false,
|
||||
couchDB_URI: `${env.hostname}`,
|
||||
couchDB_DBNAME: `${env.dbname}`,
|
||||
couchDB_USER: `${env.username}`,
|
||||
couchDB_PASSWORD: `${env.password}`,
|
||||
bucket: `${env.bucketName}`,
|
||||
region: "us-east-1",
|
||||
endpoint: `${env.minioEndpoint}`,
|
||||
accessKey: `${env.accessKey}`,
|
||||
secretKey: `${env.secretKey}`,
|
||||
useCustomRequestHandler: true,
|
||||
forcePathStyle: true,
|
||||
bucketPrefix: "",
|
||||
usePluginSyncV2: true,
|
||||
chunkSplitterVersion: ChunkAlgorithms.RabinKarp,
|
||||
doctorProcessedVersion: DoctorRegulation.version,
|
||||
notifyThresholdOfRemoteStorageSize: 800,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
function checkDialogVisibility(dialogText: string, shouldBeVisible: boolean): void {
|
||||
const dialog = page.getByText(dialogText);
|
||||
expect(dialog).toHaveClass(/modal-title/);
|
||||
if (!shouldBeVisible) {
|
||||
expect(dialog).not.toBeVisible();
|
||||
} else {
|
||||
expect(dialog).toBeVisible();
|
||||
}
|
||||
return;
|
||||
}
|
||||
function checkDialogShown(dialogText: string) {
|
||||
checkDialogVisibility(dialogText, true);
|
||||
}
|
||||
function checkDialogHidden(dialogText: string) {
|
||||
checkDialogVisibility(dialogText, false);
|
||||
}
|
||||
|
||||
describe("Dialog Tests", async () => {
|
||||
// describe.each(cases)("Replication Tests - Remote: $setting.remoteType, E2EE: $setting.encrypt", ({ setting }) => {
|
||||
const setting = dialog_setting_base;
|
||||
beforeAll(async () => {
|
||||
await DummyFileSourceInisialised;
|
||||
await commands.grantClipboardPermissions();
|
||||
});
|
||||
let harness: LiveSyncHarness;
|
||||
const vaultName = "TestVault" + Date.now();
|
||||
beforeAll(async () => {
|
||||
harness = await generateHarness(vaultName, setting);
|
||||
await waitForReady(harness);
|
||||
expect(harness.plugin).toBeDefined();
|
||||
expect(harness.plugin.app).toBe(harness.app);
|
||||
await waitForIdle(harness);
|
||||
});
|
||||
it("should show copy to clipboard dialog and confirm", async () => {
|
||||
const testString = "This is a test string to copy to clipboard.";
|
||||
const title = "Copy Test";
|
||||
const result = harness.plugin.services.UI.promptCopyToClipboard(title, testString);
|
||||
const isDialogShown = await waitForDialogShown(title, 500);
|
||||
expect(isDialogShown).toBe(true);
|
||||
const copyButton = page.getByText("📋");
|
||||
expect(copyButton).toBeDefined();
|
||||
expect(copyButton).toBeVisible();
|
||||
await copyButton.click();
|
||||
const copyResultButton = page.getByText("✔️");
|
||||
expect(copyResultButton).toBeDefined();
|
||||
expect(copyResultButton).toBeVisible();
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
expect(clipboardText).toBe(testString);
|
||||
const okButton = page.getByText("OK");
|
||||
expect(okButton).toBeDefined();
|
||||
expect(okButton).toBeVisible();
|
||||
await okButton.click();
|
||||
const resultValue = await result;
|
||||
expect(resultValue).toBe(true);
|
||||
// Check that the dialog is closed
|
||||
const isDialogHidden = await waitForDialogHidden(title, 500);
|
||||
expect(isDialogHidden).toBe(true);
|
||||
});
|
||||
});
|
||||
77
test/utils/dummyfile.ts
Normal file
77
test/utils/dummyfile.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { DEFAULT_SETTINGS } from "@/lib/src/common/types.ts";
|
||||
import { readFile } from "../utils/fileapi.vite.ts";
|
||||
let charset = "";
|
||||
export async function init() {
|
||||
console.log("Initializing dummyfile utils...");
|
||||
|
||||
charset = (await readFile("test/utils/testcharvariants.txt")).toString();
|
||||
console.log(`Loaded charset of length ${charset.length}`);
|
||||
console.log(charset);
|
||||
}
|
||||
export const DummyFileSourceInisialised = init();
|
||||
function* indexer(range: number = 1000, seed: number = 0): Generator<number, number, number> {
|
||||
let t = seed | 0;
|
||||
while (true) {
|
||||
t = (t + 0x6d2b79f5) | 0;
|
||||
let z = t;
|
||||
z = Math.imul(z ^ (z >>> 15), z | 1);
|
||||
z ^= z + Math.imul(z ^ (z >>> 7), z | 61);
|
||||
const float = ((z ^ (z >>> 14)) >>> 0) / 4294967296;
|
||||
yield Math.floor(float * range);
|
||||
}
|
||||
}
|
||||
|
||||
export function* generateFile(size: number): Generator<string> {
|
||||
const chunkSourceStr = charset;
|
||||
const chunkStore = [...chunkSourceStr]; // To support indexing avoiding multi-byte issues
|
||||
const bufSize = 1024;
|
||||
let buf = "";
|
||||
let generated = 0;
|
||||
const indexGen = indexer(chunkStore.length);
|
||||
while (generated < size) {
|
||||
const f = indexGen.next().value;
|
||||
buf += chunkStore[f];
|
||||
generated += 1;
|
||||
if (buf.length >= bufSize) {
|
||||
yield buf;
|
||||
buf = "";
|
||||
}
|
||||
}
|
||||
if (buf.length > 0) {
|
||||
yield buf;
|
||||
}
|
||||
}
|
||||
export function* generateBinaryFile(size: number): Generator<Uint8Array<ArrayBuffer>> {
|
||||
let generated = 0;
|
||||
const pattern = Array.from({ length: 256 }, (_, i) => i);
|
||||
const indexGen = indexer(pattern.length);
|
||||
const bufSize = 1024;
|
||||
const buf = new Uint8Array(bufSize);
|
||||
let bufIdx = 0;
|
||||
while (generated < size) {
|
||||
const f = indexGen.next().value;
|
||||
buf[bufIdx] = pattern[f];
|
||||
bufIdx += 1;
|
||||
generated += 1;
|
||||
if (bufIdx >= bufSize) {
|
||||
yield buf;
|
||||
bufIdx = 0;
|
||||
}
|
||||
}
|
||||
if (bufIdx > 0) {
|
||||
yield buf.subarray(0, bufIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// File size for markdown test files (10B to 1MB, roughly logarithmic scale)
|
||||
export const FILE_SIZE_MD = [10, 100, 1000, 10000, 100000, 1000000];
|
||||
// File size for test files (10B to 40MB, roughly logarithmic scale)
|
||||
export const FILE_SIZE_BINS = [
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
50000,
|
||||
100000,
|
||||
5000000,
|
||||
DEFAULT_SETTINGS.syncMaxSizeInMB * 1024 * 1024 + 1,
|
||||
];
|
||||
3
test/utils/fileapi.vite.ts
Normal file
3
test/utils/fileapi.vite.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { server } from "vitest/browser";
|
||||
const { readFile, writeFile } = server.commands;
|
||||
export { readFile, writeFile };
|
||||
17
test/utils/testcharvariants.txt
Normal file
17
test/utils/testcharvariants.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
國破山河在,城春草木深。
|
||||
感時花濺淚,恨別鳥驚心。
|
||||
烽火連三月,家書抵萬金。
|
||||
白頭搔更短,渾欲不勝簪。
|
||||
«Nel mezzo del cammin di nostra vita
|
||||
mi ritrovai per una selva oscura,
|
||||
ché la diritta via era smarrita.»
|
||||
Духовной жаждою томим,
|
||||
В пустыне мрачной я влачился, —
|
||||
И шестикрылый серафим
|
||||
На перепутье мне явился.
|
||||
Shall I compare thee to a summer’s day?
|
||||
Thou art more lovely and more temperate:
|
||||
Rough winds do shake the darling buds of May,
|
||||
And summer’s lease hath all too short a date:
|
||||
|
||||
📜🖋️ 🏺 🏛️ 春望𠮷ché🇷🇺АaRTLO🏳️🌈👨👩👧👦lʼanatraアイウエオ
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user