Compare commits

..

22 Commits

Author SHA1 Message Date
vorotamoroz
0dfd42259d 11th March, 2026
Now, Self-hosted LiveSync has finally begun to be split into the Self-hosted LiveSync plugin for Obsidian, and a properly abstracted version of it.
This may not offer much benefit to Obsidian plugin users, or might even cause a slight inconvenience, but I believe it will certainly help improve testability and make the ecosystem better.
However, I do not see the point in putting something with little benefit into beta, so I am handling this on the alpha branch. I would actually preferred to create an R&D branch, but I was not keen on the ampersand, and I feel it will eventually become a proper beta anyway.

### Refactored

- Separated `ObsidianLiveSyncPlugin` into `ObsidianLiveSyncPlugin` and `LiveSyncBaseCore`.
- Now `LiveSyncCore` indicates the type specified version of `LiveSyncBaseCore`.
- Referencing `plugin.xxx` has been rewritten to referencing the corresponding service or `core.xxx`.

### Internal API changes

- Storage Access APIs are now yielding Promises. This is to allow more limited storage platforms to be supported.

### R&D

- Browser-version of Self-hosted LiveSync is now in development. This is not intended for public use now, but I will eventually make it available for testing.
- We can see the code in `src/apps/webapp` for the browser version.
2026-03-11 05:47:00 +01:00
vorotamoroz
9cf630320c Merge remote-tracking branch 'refs/remotes/origin/main' 2026-03-09 10:38:17 +09:00
vorotamoroz
584adc9296 bump 2026-03-09 10:25:19 +09:00
vorotamoroz
f7dba6854f ### Fixed
- No longer unexpected deletion-propagation occurs when the parent directory is not empty (#813).

### Revert reversions
- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature.
2026-03-09 10:24:49 +09:00
vorotamoroz
1d83e0ee31 manifest.json を更新 2026-03-08 17:24:54 +09:00
vorotamoroz
d0244bd6d0 bump 2026-03-07 18:36:54 +09:00
vorotamoroz
79bb5e1c77 ### Reverted
- Reverted to ModuleRedFlag and ModuleInitializerFile to the previous version because of some unexpected issues. (#813)
2026-03-07 18:36:10 +09:00
vorotamoroz
3403712e24 Downgrade version from 0.25.50 to 0.25.48 for #813 2026-03-07 13:13:37 +09:00
vorotamoroz
8faa19629b bump 2026-03-03 13:39:13 +00:00
vorotamoroz
7ff9c666ce Fix: No more credentials logged 2026-03-03 13:34:18 +00:00
vorotamoroz
d8bc2806e0 bump 2026-03-03 13:27:23 +00:00
vorotamoroz
62f78b4028 Modify unit-ci to upload coverage 2026-03-03 13:22:17 +00:00
vorotamoroz
cf9d2720ce ### Fixed
- No longer deleted files are not clickable in the Global History pane.
- Diff view now uses more specific classes (#803).
- A message of configuration mismatching slightly added for better understanding.
    - Now it says `When replication is initiated manually via the command palette or ribbon, a dialogue box will open to address this.` to make it clear that the user can fix the issue by themselves.

### Refactored

- `ModuleRedFlag` has been refactored to `serviceFeatures/redFlag` and also tested.
- `ModuleInitializerFile` has been refactored to `lib/serviceFeatures/offlineScanner` and also tested.
2026-03-03 13:19:22 +00:00
vorotamoroz
09115dfe15 Fixed: Styles on the diff-dialogue has been qualified. 2026-03-02 10:52:14 +00:00
vorotamoroz
4cbb833e9d bump 2026-03-02 09:36:43 +00:00
vorotamoroz
7419d0d2a1 Add unit-ci hook to push 2026-03-02 09:15:29 +00:00
vorotamoroz
f3e83d4045 Refactored: changed the implementation from using overrides to injecting an adapter. 2026-03-02 09:06:23 +00:00
vorotamoroz
28e06a21e4 bump 2026-02-27 11:15:23 +00:00
vorotamoroz
e08fbbd223 ### Fixed and refactored
- Fixed the inexplicable behaviour when retrieving chunks from the network.
    - Chunk manager has been layered to responsible its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`,and `ArrivalWaitLayer`.
        - All layers have test now!
        - `LayeredChunkManager` has been implemented to manage these layers. Also tested.
    - `EntryManager` has been mostly rewritten, and also tested.

- Now we can configure `Never warn` for remote storage size notification, again.
2026-02-27 11:00:30 +00:00
vorotamoroz
a1e331d452 bump 2026-02-26 11:34:14 +00:00
vorotamoroz
646f8af680 ### Fixed
- Unexpected errors no longer occurred when the plug-in was unloaded.
- Hidden File Sync now respects selectors.
- Registering protocol-handlers now works safely without causing unexpected errors.

### Refactored
- LiveSyncManagers has now explicit dependencies.
- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies.
    - This is to avoid circular dependencies and clarify the ownership of the managers.
- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly.
- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage.
2026-02-26 11:30:57 +00:00
vorotamoroz
392f76fd36 ### Refactored
- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and also tests have been added.
- Some unnecessary things have been removed.
2026-02-26 08:59:54 +00:00
105 changed files with 6325 additions and 2766 deletions

View File

@@ -3,6 +3,10 @@ name: unit-ci
on:
workflow_dispatch:
push:
branches:
- main
- beta
permissions:
contents: read
@@ -26,8 +30,16 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Install test dependencies (Playwright Chromium)
run: npm run test:install-dependencies
# unit tests do not require Playwright, so we can skip installing its dependencies to save time
# - name: Install test dependencies (Playwright Chromium)
# run: npm run test:install-dependencies
- name: Run unit tests suite
run: npm run test:unit
- name: Run unit tests suite with coverage
run: npm run test:unit:coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/**

3
.gitignore vendored
View File

@@ -27,4 +27,5 @@ data.json
cov_profile/**
coverage
coverage
src/apps/cli/dist/*

View File

@@ -52,6 +52,7 @@ Hence, the new feature should be implemented as follows:
### Commands
```bash
npm run test:unit # Run unit tests with vitest (or `npm run test:unit:coverage` for coverage)
npm run check # TypeScript and svelte type checking
npm run dev # Development build with auto-rebuild (uses .env for test vault paths)
npm run build # Production build
@@ -67,8 +68,11 @@ npm test # Run vitest tests (requires Docker services)
### 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
- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~
- This is now obsolete, migrated to vitest.
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests.
- Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`).
- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services:
```bash
npm run test:docker-all:start # Start all test services

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.45",
"version": "0.25.52",
"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",

2639
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.25.45",
"version": "0.25.52",
"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",

287
src/LiveSyncBaseCore.ts Normal file
View File

@@ -0,0 +1,287 @@
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
import type { Confirm } from "./lib/src/interfaces/Confirm";
import type { DatabaseFileAccess } from "./lib/src/interfaces/DatabaseFileAccess";
import type { Rebuilder } from "./lib/src/interfaces/DatabaseRebuilder";
import type { IFileHandler } from "./lib/src/interfaces/FileHandler";
import type { StorageAccess } from "./lib/src/interfaces/StorageAccess";
import type { LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB";
import type { LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator";
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes";
import type { LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicatorEnv";
import type { LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator";
import { useCheckRemoteSize } from "./lib/src/serviceFeatures/checkRemoteSize";
import { useOfflineScanner } from "./lib/src/serviceFeatures/offlineScanner";
import { useTargetFilters } from "./lib/src/serviceFeatures/targetFilter";
import type { ServiceContext } from "./lib/src/services/base/ServiceBase";
import type { InjectableServiceHub } from "./lib/src/services/InjectableServices";
import { AbstractModule } from "./modules/AbstractModule";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB";
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker";
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain";
import type { ServiceModules } from "./lib/src/interfaces/ServiceModule";
import { useRedFlagFeatures } from "./serviceFeatures/redFlag";
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
export class LiveSyncBaseCore<
T extends ServiceContext = ServiceContext,
TCommands extends IMinimumLiveSyncCommands = IMinimumLiveSyncCommands,
>
implements
LiveSyncLocalDBEnv,
LiveSyncReplicatorEnv,
LiveSyncJournalReplicatorEnv,
LiveSyncCouchDBReplicatorEnv,
HasSettings<ObsidianLiveSyncSettings>
{
addOns = [] as TCommands[];
/**
* register an add-onn to the plug-in.
* Add-ons are features that are not essential to the core functionality of the plugin,
* @param addOn
*/
private _registerAddOn(addOn: TCommands) {
this.addOns.push(addOn);
this.services.appLifecycle.onUnload.addHandler(() => Promise.resolve(addOn.onunload()).then(() => true));
}
/**
* Get an add-on by its class name. Returns undefined if not found.
* @param cls
* @returns
*/
getAddOn<T extends TCommands>(cls: string) {
for (const addon of this.addOns) {
if (addon.constructor.name == cls) return addon as T;
}
return undefined;
}
constructor(
serviceHub: InjectableServiceHub<T>,
serviceModuleInitialiser: (
core: LiveSyncBaseCore<T, TCommands>,
serviceHub: InjectableServiceHub<T>
) => ServiceModules,
extraModuleInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => AbstractModule[],
addOnsInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => TCommands[],
featuresInitialiser: (core: LiveSyncBaseCore<T, TCommands>) => void
) {
this._services = serviceHub;
this._serviceModules = serviceModuleInitialiser(this, serviceHub);
const extraModules = extraModuleInitialiser(this);
this.registerModules(extraModules);
this.initialiseServiceFeatures();
featuresInitialiser(this);
const addOns = addOnsInitialiser(this);
for (const addOn of addOns) {
this._registerAddOn(addOn);
}
this.bindModuleFunctions();
}
/**
* The service hub for managing all services.
*/
_services: InjectableServiceHub<T> | undefined = undefined;
get services() {
if (!this._services) {
throw new Error("Services not initialised yet");
}
return this._services;
}
/**
* Service Modules
*/
protected _serviceModules: ServiceModules;
get serviceModules() {
return this._serviceModules;
}
/**
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
*/
private modules = [
// Move to registerModules
] as AbstractModule[];
/**
* Get a module by its class. Throws an error if not found.
* Mostly used for getting SetupManager.
* @param constructor
* @returns
*/
getModule<T extends AbstractModule>(constructor: new (...args: any[]) => T): T {
for (const module of this.modules) {
if (module.constructor === constructor) return module as T;
}
throw new Error(`Module ${constructor} not found or not loaded.`);
}
/**
* Register a module to the plug-in.
* @param module The module to register.
*/
private _registerModule(module: AbstractModule) {
this.modules.push(module);
}
public registerModules(extraModules: AbstractModule[] = []) {
this._registerModule(new ModuleLiveSyncMain(this));
this._registerModule(new ModuleConflictChecker(this));
this._registerModule(new ModuleReplicatorMinIO(this));
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleConflictResolver(this));
this._registerModule(new ModulePeriodicProcess(this));
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
this._registerModule(new ModuleBasicMenu(this));
for (const module of extraModules) {
this._registerModule(module);
}
// Test and Dev Modules
}
/**
* Bind module functions to services.
*/
public bindModuleFunctions() {
for (const module of this.modules) {
if (module instanceof AbstractModule) {
module.onBindFunction(this, this.services);
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
} else {
this.services.API.addLog(
`Module ${(module as any)?.constructor?.name ?? "unknown"} does not have onBindFunction, skipping binding.`,
LOG_LEVEL_INFO
);
}
}
}
/**
* @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user.
*/
get confirm(): Confirm {
return this.services.UI.confirm;
}
/**
* @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in.
*/
get settings() {
return this.services.setting.settings;
}
/**
* @obsolete Use services.setting.settings instead. Set the settings of the plug-in.
*/
set settings(value: ObsidianLiveSyncSettings) {
this.services.setting.settings = value;
}
/**
* @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in.
* @returns The current settings of the plug-in.
*/
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
/**
* @obsolete Use services.database.localDatabase instead. The local database instance.
*/
get localDatabase() {
return this.services.database.localDatabase;
}
/**
* @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database.
* @returns The PouchDB database instance.
*/
getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase;
}
/**
* @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc.
*/
get simpleStore() {
return this.services.keyValueDB.simpleStore as SimpleStore<CheckPointInfo>;
}
/**
* @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time.
*/
get replicator() {
return this.services.replicator.getActiveReplicator()!;
}
/**
* @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc.
*/
get kvDB() {
return this.services.keyValueDB.kvDB;
}
/// Modules which were relied on services
/**
* Storage Accessor for handling file operations.
* @obsolete Use serviceModules.storageAccess instead.
*/
get storageAccess(): StorageAccess {
return this.serviceModules.storageAccess;
}
/**
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
* @obsolete Use serviceModules.databaseFileAccess instead.
*/
get databaseFileAccess(): DatabaseFileAccess {
return this.serviceModules.databaseFileAccess;
}
/**
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
* @obsolete Use serviceModules.fileHandler instead.
*/
get fileHandler(): IFileHandler {
return this.serviceModules.fileHandler;
}
/**
* Rebuilder for handling database rebuilding operations.
* @obsolete Use serviceModules.rebuilder instead.
*/
get rebuilder(): Rebuilder {
return this.serviceModules.rebuilder;
}
// private initialiseServices<T extends ServiceContext>(serviceHub: InjectableServiceHub<T>) {
// this._services = serviceHub;
// }
/**
* Initialise ServiceFeatures.
* (Please refer `serviceFeatures` for more details)
*/
initialiseServiceFeatures() {
useRedFlagFeatures(this);
useOfflineScanner(this);
// enable target filter feature.
useTargetFilters(this);
useCheckRemoteSize(this);
}
}
export interface IMinimumLiveSyncCommands {
onunload(): void;
onload(): void | Promise<void>;
constructor: { name: string };
}

4
src/apps/webapp/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.DS_Store
*.log

View File

@@ -0,0 +1,34 @@
import type { UXFileInfoStub, UXFolderInfo } from "../../../lib/src/common/types";
import type { IConversionAdapter } from "../../../lib/src/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
/**
* Conversion adapter implementation for FileSystem API
*/
export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSAPIFolder> {
nativeFileToUXFileInfoStub(file: FSAPIFile): UXFileInfoStub {
const pathParts = file.path.split("/");
const name = pathParts[pathParts.length - 1] || file.handle.name;
return {
name: name,
path: file.path,
stat: file.stat,
isFolder: false,
};
}
nativeFolderToUXFolder(folder: FSAPIFolder): UXFolderInfo {
const pathParts = folder.path.split("/");
const name = pathParts[pathParts.length - 1] || folder.handle.name;
const parentPath = pathParts.slice(0, -1).join("/");
return {
name: name,
path: folder.path,
isFolder: true,
children: [],
parent: parentPath as any,
};
}
}

View File

@@ -0,0 +1,214 @@
import type { FilePath, UXStat } from "../../../lib/src/common/types";
import type { IFileSystemAdapter } from "../../../lib/src/serviceModules/adapters";
import { FSAPIPathAdapter } from "./FSAPIPathAdapter";
import { FSAPITypeGuardAdapter } from "./FSAPITypeGuardAdapter";
import { FSAPIConversionAdapter } from "./FSAPIConversionAdapter";
import { FSAPIStorageAdapter } from "./FSAPIStorageAdapter";
import { FSAPIVaultAdapter } from "./FSAPIVaultAdapter";
import type { FSAPIFile, FSAPIFolder, FSAPIStat } from "./FSAPITypes";
import { shareRunningResult } from "octagonal-wheels/concurrency/lock_v2";
/**
* Complete file system adapter implementation for FileSystem API
*/
export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSAPIFile, FSAPIFolder, FSAPIStat> {
readonly path: FSAPIPathAdapter;
readonly typeGuard: FSAPITypeGuardAdapter;
readonly conversion: FSAPIConversionAdapter;
readonly storage: FSAPIStorageAdapter;
readonly vault: FSAPIVaultAdapter;
private fileCache = new Map<string, FSAPIFile>();
private handleCache = new Map<string, FileSystemFileHandle>();
constructor(private rootHandle: FileSystemDirectoryHandle) {
this.path = new FSAPIPathAdapter();
this.typeGuard = new FSAPITypeGuardAdapter();
this.conversion = new FSAPIConversionAdapter();
this.storage = new FSAPIStorageAdapter(rootHandle);
this.vault = new FSAPIVaultAdapter(rootHandle);
}
private normalisePath(path: FilePath | string): string {
return this.path.normalisePath(path as string);
}
/**
* Get file handle for a given path
*/
private async getFileHandleByPath(p: FilePath | string): Promise<FileSystemFileHandle | null> {
const pathStr = p as string;
// Check cache first
const cached = this.handleCache.get(pathStr);
if (cached) return cached;
try {
const parts = pathStr.split("/").filter((part) => part !== "");
if (parts.length === 0) return null;
let currentHandle: FileSystemDirectoryHandle = this.rootHandle;
const fileName = parts[parts.length - 1];
// Navigate to the parent directory
for (let i = 0; i < parts.length - 1; i++) {
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
}
const fileHandle = await currentHandle.getFileHandle(fileName);
this.handleCache.set(pathStr, fileHandle);
return fileHandle;
} catch {
return null;
}
}
async getAbstractFileByPath(p: FilePath | string): Promise<FSAPIFile | null> {
const pathStr = this.normalisePath(p);
const cached = this.fileCache.get(pathStr);
if (cached) {
return cached;
}
return await this.refreshFile(pathStr);
}
/**
*
*/
async getAbstractFileByPathInsensitive(p: FilePath | string): Promise<FSAPIFile | null> {
const pathStr = this.normalisePath(p);
const exact = await this.getAbstractFileByPath(pathStr);
if (exact) {
return exact;
}
// TODO: Refactor: Very, Very heavy.
const lowerPath = pathStr.toLowerCase();
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
if (cachedPath.toLowerCase() === lowerPath) {
return cachedFile;
}
}
await this.scanDirectory();
for (const [cachedPath, cachedFile] of this.fileCache.entries()) {
if (cachedPath.toLowerCase() === lowerPath) {
return cachedFile;
}
}
return null;
}
async getFiles(): Promise<FSAPIFile[]> {
if (this.fileCache.size === 0) {
await this.scanDirectory();
}
return Array.from(this.fileCache.values());
}
async statFromNative(file: FSAPIFile): Promise<UXStat> {
// Refresh stat from the file handle
try {
const fileObject = await file.handle.getFile();
return {
size: fileObject.size,
mtime: fileObject.lastModified,
ctime: fileObject.lastModified,
type: "file",
};
} catch {
return file.stat;
}
}
async reconcileInternalFile(p: string): Promise<void> {
// No-op in webapp version
// This is used by Obsidian to sync internal file metadata
}
/**
* Refresh file cache for a specific path
*/
async refreshFile(p: string): Promise<FSAPIFile | null> {
const pathStr = this.normalisePath(p);
const handle = await this.getFileHandleByPath(pathStr);
if (!handle) {
this.fileCache.delete(pathStr);
this.handleCache.delete(pathStr);
return null;
}
const fileObject = await handle.getFile();
const file: FSAPIFile = {
path: pathStr as FilePath,
stat: {
size: fileObject.size,
mtime: fileObject.lastModified,
ctime: fileObject.lastModified,
type: "file",
},
handle: handle,
};
this.fileCache.set(pathStr, file);
this.handleCache.set(pathStr, handle);
return file;
}
/**
* Helper method to recursively scan directory and populate file cache
*/
async scanDirectory(relativePath: string = ""): Promise<void> {
return shareRunningResult("scanDirectory:" + relativePath, async () => {
try {
const parts = relativePath.split("/").filter((part) => part !== "");
let currentHandle = this.rootHandle;
for (const part of parts) {
currentHandle = await currentHandle.getDirectoryHandle(part);
}
// Use AsyncIterator instead of .values() for better compatibility
for await (const [name, entry] of (currentHandle as any).entries()) {
const entryPath = relativePath ? `${relativePath}/${name}` : name;
if (entry.kind === "directory") {
// Recursively scan subdirectories
await this.scanDirectory(entryPath);
} else if (entry.kind === "file") {
const fileHandle = entry as FileSystemFileHandle;
const fileObject = await fileHandle.getFile();
const file: FSAPIFile = {
path: entryPath as FilePath,
stat: {
size: fileObject.size,
mtime: fileObject.lastModified,
ctime: fileObject.lastModified,
type: "file",
},
handle: fileHandle,
};
this.fileCache.set(entryPath, file);
this.handleCache.set(entryPath, fileHandle);
}
}
} catch (error) {
console.error(`Error scanning directory ${relativePath}:`, error);
}
});
}
/**
* Clear all caches
*/
clearCache(): void {
this.fileCache.clear();
this.handleCache.clear();
}
}

View File

@@ -0,0 +1,18 @@
import type { FilePath } from "../../../lib/src/common/types";
import type { IPathAdapter } from "../../../lib/src/serviceModules/adapters";
import type { FSAPIFile } from "./FSAPITypes";
/**
* Path adapter implementation for FileSystem API
*/
export class FSAPIPathAdapter implements IPathAdapter<FSAPIFile> {
getPath(file: string | FSAPIFile): FilePath {
return (typeof file === "string" ? file : file.path) as FilePath;
}
normalisePath(p: string): string {
// Normalize path separators to forward slashes (like Obsidian)
// Remove leading/trailing slashes
return p.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
}
}

View File

@@ -0,0 +1,210 @@
import type { UXDataWriteOptions } from "../../../lib/src/common/types";
import type { IStorageAdapter } from "../../../lib/src/serviceModules/adapters";
import type { FSAPIStat } from "./FSAPITypes";
/**
* Storage adapter implementation for FileSystem API
*/
export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
constructor(private rootHandle: FileSystemDirectoryHandle) {}
/**
* Resolve a path to directory and file handles
*/
private async resolvePath(p: string): Promise<{
dirHandle: FileSystemDirectoryHandle;
fileName: string;
} | null> {
try {
const parts = p.split("/").filter((part) => part !== "");
if (parts.length === 0) {
return null;
}
let currentHandle = this.rootHandle;
const fileName = parts[parts.length - 1];
// Navigate to the parent directory
for (let i = 0; i < parts.length - 1; i++) {
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
}
return { dirHandle: currentHandle, fileName };
} catch {
return null;
}
}
/**
* Get file handle for a given path
*/
private async getFileHandle(p: string): Promise<FileSystemFileHandle | null> {
const resolved = await this.resolvePath(p);
if (!resolved) return null;
try {
return await resolved.dirHandle.getFileHandle(resolved.fileName);
} catch {
return null;
}
}
/**
* Get directory handle for a given path
*/
private async getDirectoryHandle(p: string): Promise<FileSystemDirectoryHandle | null> {
try {
const parts = p.split("/").filter((part) => part !== "");
if (parts.length === 0) {
return this.rootHandle;
}
let currentHandle = this.rootHandle;
for (const part of parts) {
currentHandle = await currentHandle.getDirectoryHandle(part);
}
return currentHandle;
} catch {
return null;
}
}
async exists(p: string): Promise<boolean> {
const fileHandle = await this.getFileHandle(p);
if (fileHandle) return true;
const dirHandle = await this.getDirectoryHandle(p);
return dirHandle !== null;
}
async trystat(p: string): Promise<FSAPIStat | null> {
// Try as file first
const fileHandle = await this.getFileHandle(p);
if (fileHandle) {
const file = await fileHandle.getFile();
return {
size: file.size,
mtime: file.lastModified,
ctime: file.lastModified,
type: "file",
};
}
// Try as directory
const dirHandle = await this.getDirectoryHandle(p);
if (dirHandle) {
return {
size: 0,
mtime: Date.now(),
ctime: Date.now(),
type: "folder",
};
}
return null;
}
async stat(p: string): Promise<FSAPIStat | null> {
return await this.trystat(p);
}
async mkdir(p: string): Promise<void> {
const parts = p.split("/").filter((part) => part !== "");
let currentHandle = this.rootHandle;
for (const part of parts) {
currentHandle = await currentHandle.getDirectoryHandle(part, { create: true });
}
}
async remove(p: string): Promise<void> {
const resolved = await this.resolvePath(p);
if (!resolved) return;
await resolved.dirHandle.removeEntry(resolved.fileName, { recursive: true });
}
async read(p: string): Promise<string> {
const fileHandle = await this.getFileHandle(p);
if (!fileHandle) {
throw new Error(`File not found: ${p}`);
}
const file = await fileHandle.getFile();
return await file.text();
}
async readBinary(p: string): Promise<ArrayBuffer> {
const fileHandle = await this.getFileHandle(p);
if (!fileHandle) {
throw new Error(`File not found: ${p}`);
}
const file = await fileHandle.getFile();
return await file.arrayBuffer();
}
async write(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const resolved = await this.resolvePath(p);
if (!resolved) {
throw new Error(`Invalid path: ${p}`);
}
// Ensure parent directory exists
await this.mkdir(p.split("/").slice(0, -1).join("/"));
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
}
async writeBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
const resolved = await this.resolvePath(p);
if (!resolved) {
throw new Error(`Invalid path: ${p}`);
}
// Ensure parent directory exists
await this.mkdir(p.split("/").slice(0, -1).join("/"));
const fileHandle = await resolved.dirHandle.getFileHandle(resolved.fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
}
async append(p: string, data: string, options?: UXDataWriteOptions): Promise<void> {
const existing = await this.exists(p);
if (existing) {
const currentContent = await this.read(p);
await this.write(p, currentContent + data, options);
} else {
await this.write(p, data, options);
}
}
async list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
const dirHandle = await this.getDirectoryHandle(basePath);
if (!dirHandle) {
return { files: [], folders: [] };
}
const files: string[] = [];
const folders: string[] = [];
// Use AsyncIterator instead of .values() for better compatibility
for await (const [name, entry] of (dirHandle as any).entries()) {
const entryPath = basePath ? `${basePath}/${name}` : name;
if (entry.kind === "directory") {
folders.push(entryPath);
} else if (entry.kind === "file") {
files.push(entryPath);
}
}
return { files, folders };
}
}

View File

@@ -0,0 +1,17 @@
import type { ITypeGuardAdapter } from "../../../lib/src/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
/**
* Type guard adapter implementation for FileSystem API
*/
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
isFile(file: any): file is FSAPIFile {
return (
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
);
}
isFolder(item: any): item is FSAPIFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
}
}

View File

@@ -0,0 +1,24 @@
import type { FilePath, UXStat } from "../../../lib/src/common/types";
/**
* FileSystem API file representation
*/
export type FSAPIFile = {
path: FilePath;
stat: UXStat;
handle: FileSystemFileHandle;
};
/**
* FileSystem API folder representation
*/
export type FSAPIFolder = {
path: FilePath;
isFolder: true;
handle: FileSystemDirectoryHandle;
};
/**
* FileSystem API stat type (compatible with UXStat)
*/
export type FSAPIStat = UXStat;

View File

@@ -0,0 +1,123 @@
import type { FilePath, UXDataWriteOptions } from "../../../lib/src/common/types";
import type { IVaultAdapter } from "../../../lib/src/serviceModules/adapters";
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
/**
* Vault adapter implementation for FileSystem API
*/
export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
constructor(private rootHandle: FileSystemDirectoryHandle) {}
async read(file: FSAPIFile): Promise<string> {
const fileObject = await file.handle.getFile();
return await fileObject.text();
}
async cachedRead(file: FSAPIFile): Promise<string> {
// No caching in webapp version, just read directly
return await this.read(file);
}
async readBinary(file: FSAPIFile): Promise<ArrayBuffer> {
const fileObject = await file.handle.getFile();
return await fileObject.arrayBuffer();
}
async modify(file: FSAPIFile, data: string, options?: UXDataWriteOptions): Promise<void> {
const writable = await file.handle.createWritable();
await writable.write(data);
await writable.close();
}
async modifyBinary(file: FSAPIFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
const writable = await file.handle.createWritable();
await writable.write(data);
await writable.close();
}
async create(p: string, data: string, options?: UXDataWriteOptions): Promise<FSAPIFile> {
const parts = p.split("/").filter((part) => part !== "");
const fileName = parts[parts.length - 1];
// Navigate to parent directory, creating as needed
let currentHandle = this.rootHandle;
for (let i = 0; i < parts.length - 1; i++) {
currentHandle = await currentHandle.getDirectoryHandle(parts[i], { create: true });
}
// Create the file
const fileHandle = await currentHandle.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
// Get file metadata
const fileObject = await fileHandle.getFile();
return {
path: p as FilePath,
stat: {
size: fileObject.size,
mtime: fileObject.lastModified,
ctime: fileObject.lastModified,
type: "file",
},
handle: fileHandle,
};
}
async createBinary(p: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<FSAPIFile> {
const parts = p.split("/").filter((part) => part !== "");
const fileName = parts[parts.length - 1];
// Navigate to parent directory, creating as needed
let currentHandle = this.rootHandle;
for (let i = 0; i < parts.length - 1; i++) {
currentHandle = await currentHandle.getDirectoryHandle(parts[i], { create: true });
}
// Create the file
const fileHandle = await currentHandle.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
// Get file metadata
const fileObject = await fileHandle.getFile();
return {
path: p as FilePath,
stat: {
size: fileObject.size,
mtime: fileObject.lastModified,
ctime: fileObject.lastModified,
type: "file",
},
handle: fileHandle,
};
}
async delete(file: FSAPIFile | FSAPIFolder, force = false): Promise<void> {
const parts = file.path.split("/").filter((part) => part !== "");
const name = parts[parts.length - 1];
// Navigate to parent directory
let currentHandle = this.rootHandle;
for (let i = 0; i < parts.length - 1; i++) {
currentHandle = await currentHandle.getDirectoryHandle(parts[i]);
}
// Remove the entry
await currentHandle.removeEntry(name, { recursive: force });
}
async trash(file: FSAPIFile | FSAPIFolder, force = false): Promise<void> {
// In webapp, trash is the same as delete (no recycle bin)
await this.delete(file, force);
}
trigger(name: string, ...data: any[]): any {
// No-op in webapp version (no event system yet)
return undefined;
}
}

209
src/apps/webapp/index.html Normal file
View File

@@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Self-hosted LiveSync WebApp</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 600px;
width: 100%;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
#status {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
}
#status.error {
background: #fee;
color: #c33;
border: 1px solid #fcc;
}
#status.warning {
background: #ffeaa7;
color: #d63031;
border: 1px solid #fdcb6e;
}
#status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
#status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.info-section {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.info-section h2 {
font-size: 18px;
margin-bottom: 15px;
color: #333;
}
.info-section ul {
list-style: none;
padding-left: 0;
}
.info-section li {
padding: 8px 0;
color: #666;
font-size: 14px;
}
.info-section li::before {
content: "•";
color: #667eea;
font-weight: bold;
display: inline-block;
width: 1em;
margin-left: -1em;
padding-right: 0.5em;
}
.feature-list {
margin-top: 20px;
}
.feature-list h3 {
font-size: 16px;
margin-bottom: 10px;
color: #444;
}
code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.footer {
margin-top: 30px;
text-align: center;
color: #999;
font-size: 12px;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.console-link {
margin-top: 20px;
text-align: center;
font-size: 13px;
color: #666;
}
@media (max-width: 600px) {
.container {
padding: 30px 20px;
}
h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🔄 Self-hosted LiveSync</h1>
<p class="subtitle">Browser-based Self-hosted LiveSync using FileSystem API</p>
<div id="status" class="info">
Initialising...
</div>
<div class="info-section">
<h2>About This Application</h2>
<ul>
<li>Runs entirely in your browser</li>
<li>Uses FileSystem API to access your local vault</li>
<li>Syncs with CouchDB server (like Obsidian plugin)</li>
<li>Settings stored in <code>.livesync/settings.json</code></li>
<li>Real-time file watching with FileSystemObserver (Chrome 124+)</li>
</ul>
</div>
<div class="info-section">
<h2>How to Use</h2>
<ul>
<li>Grant directory access when prompted</li>
<li>Create <code>.livesync/settings.json</code> in your vault folder. (Compatible with Obsidian's Self-hosted LiveSync)</li>
<li>Add your CouchDB connection details</li>
<li>Your files will be synced automatically</li>
</ul>
</div>
<div class="console-link">
💡 Open browser console (F12) for detailed logs
</div>
<div class="footer">
<p>
Powered by
<a href="https://github.com/vrtmrz/obsidian-livesync" target="_blank">
Self-hosted LiveSync
</a>
</p>
</div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>

345
src/apps/webapp/main.ts Normal file
View File

@@ -0,0 +1,345 @@
/**
* Self-hosted LiveSync WebApp
* Browser-based version of Self-hosted LiveSync plugin using FileSystem API
*/
import { BrowserServiceHub } from "../../lib/src/services/BrowserServices";
import { LiveSyncBaseCore } from "../../LiveSyncBaseCore";
import { ServiceContext } from "../../lib/src/services/base/ServiceBase";
import { initialiseServiceModulesFSAPI } from "./serviceModules/FSAPIServiceModules";
import type { ObsidianLiveSyncSettings } from "../../lib/src/common/types";
import type { BrowserAPIService } from "../../lib/src/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "../../lib/src/services/implements/injectable/InjectableSettingService";
// import { SetupManager } from "@/modules/features/SetupManager";
// import { ModuleObsidianSettingsAsMarkdown } from "@/modules/features/ModuleObsidianSettingAsMarkdown";
// import { ModuleSetupObsidian } from "@/modules/features/ModuleSetupObsidian";
// import { ModuleObsidianMenu } from "@/modules/essentialObsidian/ModuleObsidianMenu";
const SETTINGS_DIR = ".livesync";
const SETTINGS_FILE = "settings.json";
const DB_NAME = "livesync-webapp";
/**
* Default settings for the webapp
*/
const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
liveSync: false,
syncOnSave: true,
syncOnStart: false,
savingDelay: 200,
lessInformationInLog: false,
gcDelay: 0,
periodicReplication: false,
periodicReplicationInterval: 60,
isConfigured: false,
// CouchDB settings - user needs to configure these
couchDB_URI: "",
couchDB_USER: "",
couchDB_PASSWORD: "",
couchDB_DBNAME: "",
// Disable features not needed in webapp
usePluginSync: false,
autoSweepPlugins: false,
autoSweepPluginsPeriodic: false,
};
class LiveSyncWebApp {
private rootHandle: FileSystemDirectoryHandle | null = null;
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
async initialize() {
console.log("Self-hosted LiveSync WebApp");
console.log("Initializing...");
// Request directory access
await this.requestDirectoryAccess();
if (!this.rootHandle) {
throw new Error("Failed to get directory access");
}
console.log(`Vault directory: ${this.rootHandle.name}`);
// Create service context and hub
const context = new ServiceContext();
this.serviceHub = new BrowserServiceHub<ServiceContext>();
// Setup API service
(this.serviceHub.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
() => this.rootHandle?.name || "livesync-webapp"
);
// Setup settings handlers - save to .livesync folder
const settingService = this.serviceHub.setting as InjectableSettingService<ServiceContext>;
settingService.saveData.setHandler(async (data: ObsidianLiveSyncSettings) => {
try {
await this.saveSettingsToFile(data);
console.log("[Settings] Saved to .livesync/settings.json");
} catch (error) {
console.error("[Settings] Failed to save:", error);
}
});
settingService.loadData.setHandler(async (): Promise<ObsidianLiveSyncSettings | undefined> => {
try {
const data = await this.loadSettingsFromFile();
if (data) {
console.log("[Settings] Loaded from .livesync/settings.json");
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
}
} catch (error) {
console.log("[Settings] Failed to load, using defaults");
}
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
});
// Create LiveSync core
this.core = new LiveSyncBaseCore(
this.serviceHub,
(core, serviceHub) => {
return initialiseServiceModulesFSAPI(this.rootHandle!, core, serviceHub);
},
(core) => [
// new ModuleObsidianEvents(this, core),
// new ModuleObsidianSettingDialogue(this, core),
// new ModuleObsidianMenu(core),
// new ModuleSetupObsidian(core),
// new ModuleObsidianSettingsAsMarkdown(core),
// new ModuleLog(this, core),
// new ModuleObsidianDocumentHistory(this, core),
// new ModuleInteractiveConflictResolver(this, core),
// new ModuleObsidianGlobalHistory(this, core),
// new ModuleDev(this, core),
// new ModuleReplicateTest(this, core),
// new ModuleIntegratedTest(this, core),
// new SetupManager(core),
],
() => [],// No add-ons
() => [],
);
// Start the core
await this.start();
}
private async saveSettingsToFile(data: ObsidianLiveSyncSettings): Promise<void> {
if (!this.rootHandle) return;
try {
// Create .livesync directory if it doesn't exist
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR, { create: true });
// Create/overwrite settings.json
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (error) {
console.error("[Settings] Error saving to file:", error);
throw error;
}
}
private async loadSettingsFromFile(): Promise<Partial<ObsidianLiveSyncSettings> | null> {
if (!this.rootHandle) return null;
try {
const livesyncDir = await this.rootHandle.getDirectoryHandle(SETTINGS_DIR);
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE);
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
} catch (error) {
// File doesn't exist yet
return null;
}
}
private async requestDirectoryAccess() {
try {
// Check if we have a cached directory handle
const cached = await this.loadCachedDirectoryHandle();
if (cached) {
// Verify permission (cast to any for compatibility)
try {
const permission = await (cached as any).queryPermission({ mode: "readwrite" });
if (permission === "granted") {
this.rootHandle = cached;
console.log("[Directory] Using cached directory handle");
return;
}
} catch (e) {
// queryPermission might not be supported, try to use anyway
console.log("[Directory] Could not verify permission, requesting new access");
}
}
// Request new directory access
console.log("[Directory] Requesting directory access...");
this.rootHandle = await (window as any).showDirectoryPicker({
mode: "readwrite",
startIn: "documents",
});
// Save the handle for next time
await this.saveCachedDirectoryHandle(this.rootHandle);
console.log("[Directory] Directory access granted");
} catch (error) {
console.error("[Directory] Failed to get directory access:", error);
throw error;
}
}
private async saveCachedDirectoryHandle(handle: FileSystemDirectoryHandle) {
try {
// Use IndexedDB to store the directory handle
const db = await this.openHandleDB();
const transaction = db.transaction(["handles"], "readwrite");
const store = transaction.objectStore("handles");
await new Promise((resolve, reject) => {
const request = store.put(handle, "rootHandle");
request.onsuccess = resolve;
request.onerror = reject;
});
db.close();
} catch (error) {
console.error("[Directory] Failed to cache handle:", error);
}
}
private async loadCachedDirectoryHandle(): Promise<FileSystemDirectoryHandle | null> {
try {
const db = await this.openHandleDB();
const transaction = db.transaction(["handles"], "readonly");
const store = transaction.objectStore("handles");
const handle = await new Promise<FileSystemDirectoryHandle | null>((resolve, reject) => {
const request = store.get("rootHandle");
request.onsuccess = () => resolve(request.result || null);
request.onerror = reject;
});
db.close();
return handle;
} catch (error) {
console.error("[Directory] Failed to load cached handle:", error);
return null;
}
}
private async openHandleDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open("livesync-webapp-handles", 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains("handles")) {
db.createObjectStore("handles");
}
};
});
}
private async start() {
if (!this.core) {
throw new Error("Core not initialized");
}
try {
console.log("[Starting] Initializing LiveSync...");
const loadResult = await this.core.services.control.onLoad();
if (!loadResult) {
console.error("[Error] Failed to initialize LiveSync");
this.showError("Failed to initialize LiveSync");
return;
}
await this.core.services.control.onReady();
console.log("[Ready] LiveSync is running");
// Check if configured
const settings = this.core.services.setting.currentSettings();
if (!settings.isConfigured) {
console.warn("[Warning] LiveSync is not configured yet");
this.showWarning("Please configure CouchDB connection in settings");
} else {
console.log("[Info] LiveSync is configured and ready");
console.log(`[Info] Database: ${settings.couchDB_URI}/${settings.couchDB_DBNAME}`);
this.showSuccess("LiveSync is ready!");
}
// Scan the directory to populate file cache
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
if (fileAccess?.fsapiAdapter) {
console.log("[Scanning] Scanning vault directory...");
await fileAccess.fsapiAdapter.scanDirectory();
const files = await fileAccess.fsapiAdapter.getFiles();
console.log(`[Scanning] Found ${files.length} files`);
}
} catch (error) {
console.error("[Error] Failed to start:", error);
this.showError(`Failed to start: ${error}`);
}
}
async shutdown() {
if (this.core) {
console.log("[Shutdown] Shutting down...");
// Stop file watching
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
if (storageEventManager?.cleanup) {
await storageEventManager.cleanup();
}
await this.core.services.control.onUnload();
console.log("[Shutdown] Complete");
}
}
private showError(message: string) {
const statusEl = document.getElementById("status");
if (statusEl) {
statusEl.className = "error";
statusEl.textContent = `Error: ${message}`;
}
}
private showWarning(message: string) {
const statusEl = document.getElementById("status");
if (statusEl) {
statusEl.className = "warning";
statusEl.textContent = `Warning: ${message}`;
}
}
private showSuccess(message: string) {
const statusEl = document.getElementById("status");
if (statusEl) {
statusEl.className = "success";
statusEl.textContent = message;
}
}
}
// Initialize on load
const app = new LiveSyncWebApp();
window.addEventListener("load", async () => {
try {
await app.initialize();
} catch (error) {
console.error("Failed to initialize:", error);
}
});
// Handle page unload
window.addEventListener("beforeunload", () => {
void app.shutdown();
});
// Export for debugging
(window as any).livesyncApp = app;

View File

@@ -0,0 +1,281 @@
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "../../../lib/src/common/types";
import type { FileEventItem } from "../../../lib/src/common/types";
import type { IStorageEventManagerAdapter } from "../../../lib/src/managers/adapters";
import type {
IStorageEventTypeGuardAdapter,
IStorageEventPersistenceAdapter,
IStorageEventWatchAdapter,
IStorageEventStatusAdapter,
IStorageEventConverterAdapter,
IStorageEventWatchHandlers,
} from "../../../lib/src/managers/adapters";
import type { FileEventItemSentinel } from "../../../lib/src/managers/StorageEventManager";
import type { FSAPIFile, FSAPIFolder } from "../adapters/FSAPITypes";
/**
* FileSystem API-specific type guard adapter
*/
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
isFile(file: any): file is FSAPIFile {
return (
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
);
}
isFolder(item: any): item is FSAPIFolder {
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
}
}
/**
* FileSystem API-specific persistence adapter (IndexedDB-based snapshot)
*/
class FSAPIPersistenceAdapter implements IStorageEventPersistenceAdapter {
private dbName = "livesync-webapp-snapshot";
private storeName = "snapshots";
private snapshotKey = "file-events";
private async openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
async saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise<void> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
await new Promise<void>((resolve, reject) => {
const request = store.put(snapshot, this.snapshotKey);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
db.close();
} catch (error) {
console.error("Failed to save snapshot:", error);
}
}
async loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null> {
try {
const db = await this.openDB();
const transaction = db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const result = await new Promise<(FileEventItem | FileEventItemSentinel)[] | null>((resolve, reject) => {
const request = store.get(this.snapshotKey);
request.onsuccess = () => resolve(request.result || null);
request.onerror = () => reject(request.error);
});
db.close();
return result;
} catch {
return null;
}
}
}
/**
* FileSystem API-specific status adapter (console logging)
*/
class FSAPIStatusAdapter implements IStorageEventStatusAdapter {
private lastUpdate = 0;
private updateInterval = 5000; // Update every 5 seconds
updateStatus(status: { batched: number; processing: number; totalQueued: number }): void {
const now = Date.now();
if (now - this.lastUpdate > this.updateInterval) {
if (status.totalQueued > 0 || status.processing > 0) {
console.log(
`[StorageEventManager] Batched: ${status.batched}, Processing: ${status.processing}, Total Queued: ${status.totalQueued}`
);
}
this.lastUpdate = now;
}
}
}
/**
* FileSystem API-specific converter adapter
*/
class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile> {
toFileInfo(file: FSAPIFile, deleted?: boolean): UXFileInfoStub {
const pathParts = file.path.split("/");
const name = pathParts[pathParts.length - 1] || file.handle.name;
return {
name: name,
path: file.path,
stat: file.stat,
deleted: deleted,
isFolder: false,
};
}
toInternalFileInfo(p: FilePath): UXInternalFileInfoStub {
const pathParts = p.split("/");
const name = pathParts[pathParts.length - 1] || "";
return {
name: name,
path: p,
isInternal: true,
stat: undefined,
};
}
}
/**
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
*/
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
private observer: any = null; // FileSystemObserver type
constructor(private rootHandle: FileSystemDirectoryHandle) {}
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
// Use FileSystemObserver if available (Chrome 124+)
if (typeof (window as any).FileSystemObserver === "undefined") {
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
return Promise.resolve();
}
try {
const FileSystemObserver = (window as any).FileSystemObserver;
this.observer = new FileSystemObserver(async (records: any[]) => {
for (const record of records) {
const handle = record.root;
const changedHandle = record.changedHandle;
const relativePathComponents = record.relativePathComponents;
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
// Build relative path
const relativePath = relativePathComponents ? relativePathComponents.join("/") : "";
// Skip .livesync directory to avoid infinite loops
if (relativePath.startsWith(".livesync/") || relativePath === ".livesync") {
continue;
}
console.log(`[FileSystemObserver] ${type}: ${relativePath}`);
// Convert to our event handlers
try {
if (type === "appeared" || type === "modified") {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
stat: {
size: file.size,
mtime: file.lastModified,
ctime: file.lastModified,
type: "file" as const,
},
handle: changedHandle,
};
if (type === "appeared") {
await handlers.onCreate(fileInfo, undefined);
} else {
await handlers.onChange(fileInfo, undefined);
}
}
} else if (type === "disappeared") {
const fileInfo = {
path: relativePath as any,
stat: {
size: 0,
mtime: Date.now(),
ctime: Date.now(),
type: "file" as const,
},
handle: null as any,
};
await handlers.onDelete(fileInfo, undefined);
} else if (type === "moved") {
// Handle as delete + create
// Note: FileSystemObserver provides both old and new paths in some cases
// For simplicity, we'll treat it as a modification
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
stat: {
size: file.size,
mtime: file.lastModified,
ctime: file.lastModified,
type: "file" as const,
},
handle: changedHandle,
};
await handlers.onChange(fileInfo, undefined);
}
}
} catch (error) {
console.error(
`[FileSystemObserver] Error processing ${type} event for ${relativePath}:`,
error
);
}
}
});
// Start observing
await this.observer.observe(this.rootHandle, { recursive: true });
console.log("[FSAPIWatchAdapter] FileSystemObserver started successfully");
} catch (error) {
console.error("[FSAPIWatchAdapter] Failed to start FileSystemObserver:", error);
console.log("[FSAPIWatchAdapter] Falling back to manual sync mode");
}
return Promise.resolve();
}
async stopWatch(): Promise<void> {
if (this.observer) {
try {
this.observer.disconnect();
this.observer = null;
console.log("[FSAPIWatchAdapter] FileSystemObserver stopped");
} catch (error) {
console.error("[FSAPIWatchAdapter] Error stopping observer:", error);
}
}
}
}
/**
* Composite adapter for FileSystem API StorageEventManager
*/
export class FSAPIStorageEventManagerAdapter implements IStorageEventManagerAdapter<FSAPIFile, FSAPIFolder> {
readonly typeGuard: FSAPITypeGuardAdapter;
readonly persistence: FSAPIPersistenceAdapter;
readonly watch: FSAPIWatchAdapter;
readonly status: FSAPIStatusAdapter;
readonly converter: FSAPIConverterAdapter;
constructor(rootHandle: FileSystemDirectoryHandle) {
this.typeGuard = new FSAPITypeGuardAdapter();
this.persistence = new FSAPIPersistenceAdapter();
this.watch = new FSAPIWatchAdapter(rootHandle);
this.status = new FSAPIStatusAdapter();
this.converter = new FSAPIConverterAdapter();
}
}

View File

@@ -0,0 +1,39 @@
import {
StorageEventManagerBase,
type StorageEventManagerBaseDependencies,
} from "../../../lib/src/managers/StorageEventManager";
import { FSAPIStorageEventManagerAdapter } from "./FSAPIStorageEventManagerAdapter";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import type { ServiceContext } from "../../../lib/src/services/base/ServiceBase";
export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStorageEventManagerAdapter> {
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>;
private fsapiAdapter: FSAPIStorageEventManagerAdapter;
constructor(
rootHandle: FileSystemDirectoryHandle,
core: LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands>,
dependencies: StorageEventManagerBaseDependencies
) {
const adapter = new FSAPIStorageEventManagerAdapter(rootHandle);
super(adapter, dependencies);
this.fsapiAdapter = adapter;
this.core = core;
}
/**
* Override _watchVaultRawEvents for webapp-specific logic
* In webapp, we don't have internal files like Obsidian's .obsidian folder
*/
protected override async _watchVaultRawEvents(path: string) {
// No-op in webapp version
// Internal file handling is not needed
}
async cleanup() {
// Stop file watching
if (this.fsapiAdapter?.watch) {
await (this.fsapiAdapter.watch as any).stopWatch?.();
}
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "livesync-webapp",
"private": true,
"version": "0.0.1",
"type": "module",
"description": "Browser-based Obsidian LiveSync using FileSystem API",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {},
"devDependencies": {
"typescript": "5.9.3",
"vite": "^7.3.1"
},
"imports": {
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",
"@lib/worker/bgWorker.ts": "@lib/worker/bgWorker.mock.ts"
}
}

View File

@@ -0,0 +1,15 @@
import {
ServiceDatabaseFileAccessBase,
type ServiceDatabaseFileAccessDependencies,
} from "../../../lib/src/serviceModules/ServiceDatabaseFileAccessBase";
import type { DatabaseFileAccess } from "../../../lib/src/interfaces/DatabaseFileAccess";
/**
* FileSystem API-specific implementation of ServiceDatabaseFileAccess
* Same as Obsidian version, no platform-specific changes needed
*/
export class ServiceDatabaseFileAccessFSAPI extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {
constructor(services: ServiceDatabaseFileAccessDependencies) {
super(services);
}
}

View File

@@ -0,0 +1,104 @@
import type { InjectableServiceHub } from "../../../lib/src/services/implements/injectable/InjectableServiceHub";
import { ServiceRebuilder } from "../../../lib/src/serviceModules/Rebuilder";
import { ServiceFileHandler } from "../../../serviceModules/FileHandler";
import { StorageAccessManager } from "../../../lib/src/managers/StorageProcessingManager";
import type { ServiceModules } from "../../../types";
import type { LiveSyncBaseCore } from "../../../LiveSyncBaseCore";
import type { ServiceContext } from "../../../lib/src/services/base/ServiceBase";
import { FileAccessFSAPI } from "./FileAccessFSAPI";
import { ServiceFileAccessFSAPI } from "./ServiceFileAccessImpl";
import { ServiceDatabaseFileAccessFSAPI } from "./DatabaseFileAccess";
import { StorageEventManagerFSAPI } from "../managers/StorageEventManagerFSAPI";
/**
* Initialize service modules for FileSystem API webapp version
* This is the webapp equivalent of ObsidianLiveSyncPlugin.initialiseServiceModules
*
* @param rootHandle - The root FileSystemDirectoryHandle for the vault
* @param core - The LiveSyncBaseCore instance
* @param services - The service hub
* @returns ServiceModules containing all initialized service modules
*/
export function initialiseServiceModulesFSAPI(
rootHandle: FileSystemDirectoryHandle,
core: LiveSyncBaseCore<ServiceContext, any>,
services: InjectableServiceHub<ServiceContext>
): ServiceModules {
const storageAccessManager = new StorageAccessManager();
// FileSystem API-specific file access
const vaultAccess = new FileAccessFSAPI(rootHandle, {
storageAccessManager: storageAccessManager,
vaultService: services.vault,
settingService: services.setting,
APIService: services.API,
pathService: services.path,
});
// FileSystem API-specific storage event manager
const storageEventManager = new StorageEventManagerFSAPI(rootHandle, core, {
fileProcessing: services.fileProcessing,
setting: services.setting,
vaultService: services.vault,
storageAccessManager: storageAccessManager,
APIService: services.API,
});
// Storage access using FileSystem API adapter
const storageAccess = new ServiceFileAccessFSAPI({
API: services.API,
setting: services.setting,
fileProcessing: services.fileProcessing,
vault: services.vault,
appLifecycle: services.appLifecycle,
storageEventManager: storageEventManager,
storageAccessManager: storageAccessManager,
vaultAccess: vaultAccess,
});
// Database file access (platform-independent)
const databaseFileAccess = new ServiceDatabaseFileAccessFSAPI({
API: services.API,
database: services.database,
path: services.path,
storageAccess: storageAccess,
vault: services.vault,
});
// File handler (platform-independent)
const fileHandler = new (ServiceFileHandler as any)({
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: services.conflict,
setting: services.setting,
fileProcessing: services.fileProcessing,
vault: services.vault,
path: services.path,
replication: services.replication,
storageAccess: storageAccess,
});
// Rebuilder (platform-independent)
const rebuilder = new ServiceRebuilder({
API: services.API,
database: services.database,
appLifecycle: services.appLifecycle,
setting: services.setting,
remote: services.remote,
databaseEvents: services.databaseEvents,
replication: services.replication,
replicator: services.replicator,
UI: services.UI,
vault: services.vault,
fileHandler: fileHandler,
storageAccess: storageAccess,
control: services.control,
});
return {
rebuilder,
fileHandler,
databaseFileAccess,
storageAccess,
};
}

View File

@@ -0,0 +1,20 @@
import { FileAccessBase, type FileAccessBaseDependencies } from "../../../lib/src/serviceModules/FileAccessBase";
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
/**
* FileSystem API-specific implementation of FileAccessBase
* Uses FSAPIFileSystemAdapter for browser file operations
*/
export class FileAccessFSAPI extends FileAccessBase<FSAPIFileSystemAdapter> {
constructor(rootHandle: FileSystemDirectoryHandle, dependencies: FileAccessBaseDependencies) {
const adapter = new FSAPIFileSystemAdapter(rootHandle);
super(adapter, dependencies);
}
/**
* Expose the adapter for accessing scanDirectory and other methods
*/
get fsapiAdapter(): FSAPIFileSystemAdapter {
return this.adapter;
}
}

View File

@@ -0,0 +1,15 @@
import {
ServiceFileAccessBase,
type StorageAccessBaseDependencies,
} from "../../../lib/src/serviceModules/ServiceFileAccessBase";
import { FSAPIFileSystemAdapter } from "../adapters/FSAPIFileSystemAdapter";
/**
* FileSystem API-specific implementation of ServiceFileAccess
* Uses FSAPIFileSystemAdapter for platform-specific operations
*/
export class ServiceFileAccessFSAPI extends ServiceFileAccessBase<FSAPIFileSystemAdapter> {
constructor(services: StorageAccessBaseDependencies<FSAPIFileSystemAdapter>) {
super(services);
}
}

View File

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

View File

@@ -0,0 +1,32 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]
}
},
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,34 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import path from "node:path";
import { readFileSync } from "node:fs";
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
resolve: {
alias: {
"@": path.resolve(__dirname, "../../"),
"@lib": path.resolve(__dirname, "../../lib/src"),
},
},
base: "./",
build: {
outDir: "dist",
emptyOutDir: true,
rollupOptions: {
input: {
index: path.resolve(__dirname, "index.html"),
},
},
},
define: {
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"),
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"),
},
server: {
port: 3000,
open: true,
},
});

View File

@@ -2,6 +2,7 @@ import { PouchDB } from "@lib/pouchdb/pouchdb-browser";
import {
type EntryDoc,
type LOG_LEVEL,
type ObsidianLiveSyncSettings,
type P2PSyncSetting,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
@@ -39,6 +40,7 @@ import type { InjectableVaultServiceCompat } from "@lib/services/implements/inje
import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
import type { InjectableAPIService } from "@/lib/src/services/implements/injectable/InjectableAPIService";
import type { BrowserAPIService } from "@/lib/src/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@/lib/src/services/implements/injectable/InjectableSettingService";
function addToList(item: string, list: string) {
return unique(
@@ -87,6 +89,22 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
(this.services.API as BrowserAPIService<ServiceContext>).getSystemVaultName.setHandler(
() => "p2p-livesync-web-peer"
);
this.services.API.addLog.setHandler(Logger);
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
this._simpleStore = repStore;
let _settings = { ...P2P_DEFAULT_SETTINGS, additionalSuffixOfDatabaseName: "" } as ObsidianLiveSyncSettings;
this.services.setting.settings = _settings as any;
(this.services.setting as InjectableSettingService<any>).saveData.setHandler(async (data) => {
await repStore.set("settings", data);
eventHub.emitEvent(EVENT_SETTING_SAVED, data);
});
(this.services.setting as InjectableSettingService<any>).loadData.setHandler(async () => {
const settings = { ..._settings, ...((await repStore.get("settings")) as ObsidianLiveSyncSettings) };
return settings;
});
}
get settings() {
return this.services.setting.currentSettings() as P2PSyncSetting;
}
async init() {
// const { simpleStoreAPI } = await getWrappedSynchromesh();
@@ -102,23 +120,27 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
Logger(ex, LOG_LEVEL_VERBOSE);
}
}
const repStore = SimpleStoreIDBv2.open<any>("p2p-livesync-web-peer");
this._simpleStore = repStore;
let _settings = (await repStore.get("settings")) || ({ ...P2P_DEFAULT_SETTINGS } as P2PSyncSetting);
this.services.setting.settings = _settings as any;
await this.services.setting.loadSettings();
this.plugin = {
saveSettings: async () => {
await repStore.set("settings", _settings);
eventHub.emitEvent(EVENT_SETTING_SAVED, _settings);
},
get settings() {
return _settings;
},
set settings(newSettings: P2PSyncSetting) {
_settings = { ..._settings, ...newSettings };
},
rebuilder: null,
// saveSettings: async () => {
// await repStore.set("settings", _settings);
// eventHub.emitEvent(EVENT_SETTING_SAVED, _settings);
// },
// get settings() {
// return _settings;
// },
// set settings(newSettings: P2PSyncSetting) {
// _settings = { ..._settings, ...newSettings };
// },
// rebuilder: null,
// core: {
// settings: this.services.setting.settings,
// },
services: this.services,
core: {
services: this.services,
},
// $$scheduleAppReload: () => {},
// $$getVaultName: () => "p2p-livesync-web-peer",
};
@@ -132,9 +154,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
}, 1000);
return this;
}
get settings() {
return this.plugin.settings;
}
_log(msg: any, level?: LOG_LEVEL): void {
Logger(msg, level);
}
@@ -334,12 +354,11 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
}
}
}
this.plugin.settings = remoteConfig;
await this.plugin.saveSettings();
await this.services.setting.applyPartial(remoteConfig, true);
if (yn === DROP) {
await this.plugin.rebuilder.scheduleFetch();
// await this.plugin.rebuilder.scheduleFetch();
} else {
await this.plugin.services.appLifecycle.scheduleRestart();
await this.plugin.core.services.appLifecycle.scheduleRestart();
}
} else {
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
@@ -357,13 +376,18 @@ export class P2PReplicatorShim implements P2PReplicatorBase, CommandShim {
} as const;
const targetSetting = settingMap[prop];
const currentSettingAll = this.plugin.core.services.setting.currentSettings();
const currentSetting = {
[targetSetting]: currentSettingAll ? currentSettingAll[targetSetting] : "",
};
if (peer[prop]) {
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
await this.plugin.saveSettings();
// this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
// await this.plugin.saveSettings();
currentSetting[targetSetting] = removeFromList(peer.name, currentSetting[targetSetting]);
} else {
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
await this.plugin.saveSettings();
currentSetting[targetSetting] = addToList(peer.name, currentSetting[targetSetting]);
}
await this.plugin.core.services.setting.applyPartial(currentSetting, true);
}
}

View File

@@ -27,7 +27,7 @@
<main>
<div class="control">
{#await synchronised then cmdSync}
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync}></P2PReplicatorPane>
<P2PReplicatorPane plugin={cmdSync.plugin} {cmdSync} core={cmdSync.plugin.core}></P2PReplicatorPane>
{:catch error}
<p>{error.message}</p>
{/await}

View File

@@ -0,0 +1,45 @@
import { Logger } from "octagonal-wheels/common/logger";
import { fireAndForget } from "octagonal-wheels/promises";
import { eventHub, EVENT_PLUGIN_UNLOADED } from "./events";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
type PeriodicProcessorHost = NecessaryServices<"API" | "control", never>;
export class PeriodicProcessor {
_process: () => Promise<any>;
_timer?: number = undefined;
_core: PeriodicProcessorHost;
constructor(core: PeriodicProcessorHost, process: () => Promise<any>) {
// this._plugin = plugin;
this._core = core;
this._process = process;
eventHub.onceEvent(EVENT_PLUGIN_UNLOADED, () => {
this.disable();
});
}
async process() {
try {
await this._process();
} catch (ex) {
Logger(ex);
}
}
enable(interval: number) {
this.disable();
if (interval == 0) return;
this._timer = this._core.services.API.setInterval(
() =>
fireAndForget(async () => {
await this.process();
if (this._core.services?.control?.hasUnloaded()) {
this.disable();
}
}),
interval
);
}
disable() {
if (this._timer !== undefined) {
this._core.services.API.clearInterval(this._timer);
this._timer = undefined;
}
}
}

View File

@@ -1,5 +1,5 @@
import { eventHub } from "../lib/src/hub/hub";
import type ObsidianLiveSyncPlugin from "../main";
// import type ObsidianLiveSyncPlugin from "../main";
export const EVENT_PLUGIN_LOADED = "plugin-loaded";
export const EVENT_PLUGIN_UNLOADED = "plugin-unloaded";
@@ -24,12 +24,12 @@ export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3";
export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
// export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
// export const EVENT_FILE_CHANGED = "file-changed";
declare global {
interface LSEvents {
[EVENT_PLUGIN_LOADED]: ObsidianLiveSyncPlugin;
[EVENT_PLUGIN_LOADED]: undefined;
[EVENT_PLUGIN_UNLOADED]: undefined;
[EVENT_REQUEST_OPEN_PLUGIN_SYNC_DIALOG]: undefined;
[EVENT_REQUEST_OPEN_SETTINGS]: undefined;
@@ -44,7 +44,6 @@ declare global {
[EVENT_REQUEST_RUN_DOCTOR]: string;
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
[EVENT_ANALYSE_DB_USAGE]: undefined;
[EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined;
[EVENT_REQUEST_PERFORM_GC_V3]: undefined;
}
}

View File

@@ -24,13 +24,10 @@ import {
type UXFileInfoStub,
} from "../lib/src/common/types.ts";
export { ICHeader, ICXHeader } from "./types.ts";
import type ObsidianLiveSyncPlugin from "../main.ts";
import { writeString } from "../lib/src/string_and_binary/convert.ts";
import { fireAndForget } from "../lib/src/common/utils.ts";
import { sameChangePairs } from "./stores.ts";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "./events.ts";
import { AuthorizationHeaderGenerator } from "../lib/src/replication/httplib.ts";
import type { KeyValueDatabase } from "../lib/src/interfaces/KeyValueDatabase.ts";
@@ -126,47 +123,6 @@ export {
stripInternalMetadataPrefix,
} from "@lib/common/typeUtils.ts";
export class PeriodicProcessor {
_process: () => Promise<any>;
_timer?: number = undefined;
_plugin: ObsidianLiveSyncPlugin;
constructor(plugin: ObsidianLiveSyncPlugin, process: () => Promise<any>) {
this._plugin = plugin;
this._process = process;
eventHub.onceEvent(EVENT_PLUGIN_UNLOADED, () => {
this.disable();
});
}
async process() {
try {
await this._process();
} catch (ex) {
Logger(ex);
}
}
enable(interval: number) {
this.disable();
if (interval == 0) return;
this._timer = window.setInterval(
() =>
fireAndForget(async () => {
await this.process();
if (this._plugin.services?.control?.hasUnloaded()) {
this.disable();
}
}),
interval
);
this._plugin.registerInterval(this._timer);
}
disable() {
if (this._timer !== undefined) {
window.clearInterval(this._timer);
this._timer = undefined;
}
}
}
export const _requestToCouchDBFetch = async (
baseUri: string,
username: string,
@@ -257,20 +213,8 @@ export function requestToCouchDBWithCredentials(
import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
// Why 2000? : ZIP FILE Does not have enough resolution.
const resolution = 2000;
export function compareMTime(
baseMTime: number,
targetMTime: number
): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
const truncatedBaseMTime = ~~(baseMTime / resolution) * resolution;
const truncatedTargetMTime = ~~(targetMTime / resolution) * resolution;
// Logger(`Resolution MTime ${truncatedBaseMTime} and ${truncatedTargetMTime} `, LOG_LEVEL_VERBOSE);
if (truncatedBaseMTime == truncatedTargetMTime) return EVEN;
if (truncatedBaseMTime > truncatedTargetMTime) return BASE_IS_NEW;
if (truncatedBaseMTime < truncatedTargetMTime) return TARGET_IS_NEW;
throw new Error("Unexpected error");
}
import { compareMTime } from "@lib/common/utils.ts";
export { compareMTime };
function getKey(file: AnyEntry | string | UXFileInfoStub) {
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);
return key;
@@ -385,11 +329,6 @@ export function disposeAllMemo() {
_cached.clear();
}
export function displayRev(rev: string) {
const [number, hash] = rev.split("-");
return `${number}-${hash.substring(0, 6)}`;
}
export function getLogLevel(showNotice: boolean) {
return showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
}
@@ -458,3 +397,5 @@ export function onlyInNTimes(n: number, proc: (progress: number) => any) {
}
};
}
export { displayRev } from "@lib/common/utils.ts";

View File

@@ -50,17 +50,15 @@ import { LiveSyncCommands } from "../LiveSyncCommands.ts";
import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts";
import {
EVEN,
PeriodicProcessor,
disposeMemoObject,
isCustomisationSyncMetadata,
isMarkedAsSameChanges,
isPluginMetadata,
markChangesAreSame,
memoIfNotExist,
memoObject,
retrieveMemoObject,
scheduleTask,
} from "../../common/utils.ts";
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { pluginScanningCount } from "../../lib/src/mock_and_interop/stores.ts";
@@ -394,29 +392,32 @@ export type PluginDataEx = {
};
export class ConfigSync extends LiveSyncCommands {
constructor(plugin: ObsidianLiveSyncPlugin) {
super(plugin);
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
super(plugin, core);
pluginScanningCount.onChanged((e) => {
const total = e.value;
pluginIsEnumerating.set(total != 0);
});
}
get configDir() {
return this.core.services.API.getSystemConfigDir();
}
get kvDB() {
return this.plugin.kvDB;
return this.core.kvDB;
}
get useV2() {
return this.plugin.settings.usePluginSyncV2;
return this.core.settings.usePluginSyncV2;
}
get useSyncPluginEtc() {
return this.plugin.settings.usePluginEtc;
return this.core.settings.usePluginEtc;
}
isThisModuleEnabled() {
return this.plugin.settings.usePluginSync;
return this.core.settings.usePluginSync;
}
pluginDialog?: PluginDialogModal = undefined;
periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.scanAllConfigFiles(false));
periodicPluginSweepProcessor = new PeriodicProcessor(this.core, async () => await this.scanAllConfigFiles(false));
pluginList: IPluginDataExDisplay[] = [];
showPluginSyncModal() {
@@ -441,7 +442,7 @@ export class ConfigSync extends LiveSyncCommands {
this.hidePluginSyncModal();
this.periodicPluginSweepProcessor?.disable();
}
addRibbonIcon = this.plugin.addRibbonIcon.bind(this.plugin);
addRibbonIcon = this.services.API.addRibbonIcon.bind(this.services.API);
onload() {
addIcon(
"custom-sync",
@@ -449,7 +450,7 @@ export class ConfigSync extends LiveSyncCommands {
<path d="m272 166-9.38 9.38 9.38 9.38 9.38-9.38c1.96-1.93 5.11-1.9 7.03 0.058 1.91 1.94 1.91 5.04 0 6.98l-9.38 9.38 5.86 5.86-11.7 11.7c-8.34 8.35-21.4 9.68-31.3 3.19l-3.84 3.98c-8.45 8.7-20.1 13.6-32.2 13.6h-5.55v-9.95h5.55c9.43-0.0182 18.5-3.84 25-10.6l3.95-4.09c-6.54-9.86-5.23-23 3.14-31.3l11.7-11.7 5.86 5.86 9.38-9.38c1.96-1.93 5.11-1.9 7.03 0.0564 1.91 1.93 1.91 5.04 2e-3 6.98z"/>
</g>`
);
this.plugin.addCommand({
this.services.API.addCommand({
id: "livesync-plugin-dialog-ex",
name: "Show customization sync dialog",
callback: () => {
@@ -466,10 +467,9 @@ export class ConfigSync extends LiveSyncCommands {
filePath: string
): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "" {
if (filePath.split("/").length == 2 && filePath.endsWith(".json")) return "CONFIG";
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.app.vault.configDir}/themes/`))
return "THEME";
if (filePath.startsWith(`${this.app.vault.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET";
if (filePath.startsWith(`${this.app.vault.configDir}/plugins/`)) {
if (filePath.split("/").length == 4 && filePath.startsWith(`${this.configDir}/themes/`)) return "THEME";
if (filePath.startsWith(`${this.configDir}/snippets/`) && filePath.endsWith(".css")) return "SNIPPET";
if (filePath.startsWith(`${this.configDir}/plugins/`)) {
if (
filePath.endsWith("/styles.css") ||
filePath.endsWith("/manifest.json") ||
@@ -487,7 +487,7 @@ export class ConfigSync extends LiveSyncCommands {
return "";
}
isTargetPath(filePath: string): boolean {
if (!filePath.startsWith(this.app.vault.configDir)) return false;
if (!filePath.startsWith(this.configDir)) return false;
// Idea non-filter option?
return this.getFileCategory(filePath) != "";
}
@@ -856,7 +856,7 @@ export class ConfigSync extends LiveSyncCommands {
children: [],
eden: {},
};
const r = await this.plugin.localDatabase.putDBEntry(saving);
const r = await this.core.localDatabase.putDBEntry(saving);
if (r && r.ok) {
this._log(`Migrated ${v1Path} / ${f.filename} to ${v2Path}`, LOG_LEVEL_INFO);
const delR = await this.deleteConfigOnDatabase(v1Path);
@@ -998,16 +998,16 @@ export class ConfigSync extends LiveSyncCommands {
}
}
async applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise<boolean> {
const baseDir = this.app.vault.configDir;
const baseDir = this.configDir;
try {
if (content) {
// const dt = createBlob(content);
const filename = data.files[0].filename;
this._log(`Applying ${filename} of ${data.displayName || data.name}..`);
const path = `${baseDir}/${filename}` as FilePath;
await this.plugin.storageAccess.ensureDir(path);
await this.core.storageAccess.ensureDir(path);
// If the content has applied, modified time will be updated to the current time.
await this.plugin.storageAccess.writeHiddenFileAuto(path, content);
await this.core.storageAccess.writeHiddenFileAuto(path, content);
await this.storeCustomisationFileV2(path, this.services.setting.getDeviceAndVaultName());
} else {
const files = data.files;
@@ -1017,12 +1017,12 @@ export class ConfigSync extends LiveSyncCommands {
const path = `${baseDir}/${f.filename}` as FilePath;
this._log(`Applying ${f.filename} of ${data.displayName || data.name}..`);
// const contentEach = createBlob(f.data);
await this.plugin.storageAccess.ensureDir(path);
await this.core.storageAccess.ensureDir(path);
if (f.datatype == "newnote") {
let oldData;
try {
oldData = await this.plugin.storageAccess.readHiddenFileBinary(path);
oldData = await this.core.storageAccess.readHiddenFileBinary(path);
} catch (ex) {
this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE);
@@ -1033,11 +1033,11 @@ export class ConfigSync extends LiveSyncCommands {
this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
continue;
}
await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat);
await this.core.storageAccess.writeHiddenFileAuto(path, content, stat);
} else {
let oldData;
try {
oldData = await this.plugin.storageAccess.readHiddenFileText(path);
oldData = await this.core.storageAccess.readHiddenFileText(path);
} catch (ex) {
this._log(`Could not read the file ${f.filename}`, LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE);
@@ -1048,7 +1048,7 @@ export class ConfigSync extends LiveSyncCommands {
this._log(`The file ${f.filename} is already up-to-date`, LOG_LEVEL_VERBOSE);
continue;
}
await this.plugin.storageAccess.writeHiddenFileAuto(path, content, stat);
await this.core.storageAccess.writeHiddenFileAuto(path, content, stat);
}
this._log(`Applied ${f.filename} of ${data.displayName || data.name}..`);
await this.storeCustomisationFileV2(path, this.services.setting.getDeviceAndVaultName());
@@ -1067,7 +1067,7 @@ export class ConfigSync extends LiveSyncCommands {
if (data instanceof PluginDataExDisplayV2) {
return this.applyDataV2(data, content);
}
const baseDir = this.app.vault.configDir;
const baseDir = this.configDir;
try {
if (!data.documentPath) throw "InternalError: Document path not exist";
const dx = await this.localDatabase.getDBEntry(data.documentPath);
@@ -1080,12 +1080,12 @@ export class ConfigSync extends LiveSyncCommands {
try {
// console.dir(f);
const path = `${baseDir}/${f.filename}`;
await this.plugin.storageAccess.ensureDir(path);
await this.core.storageAccess.ensureDir(path);
if (!content) {
const dt = decodeBinary(f.data);
await this.plugin.storageAccess.writeHiddenFileAuto(path, dt);
await this.core.storageAccess.writeHiddenFileAuto(path, dt);
} else {
await this.plugin.storageAccess.writeHiddenFileAuto(path, content);
await this.core.storageAccess.writeHiddenFileAuto(path, content);
}
this._log(`Applying ${f.filename} of ${data.displayName || data.name}.. Done`);
} catch (ex) {
@@ -1174,7 +1174,7 @@ export class ConfigSync extends LiveSyncCommands {
(docs as AnyEntry).path ? (docs as AnyEntry).path : this.getPath(docs as AnyEntry)
);
}
if (this.isThisModuleEnabled() && this.plugin.settings.notifyPluginOrSettingUpdated) {
if (this.isThisModuleEnabled() && this.core.settings.notifyPluginOrSettingUpdated) {
if (!this.pluginDialog || (this.pluginDialog && !this.pluginDialog.isOpened())) {
const fragment = createFragment((doc) => {
doc.createEl("span", undefined, (a) => {
@@ -1232,13 +1232,13 @@ export class ConfigSync extends LiveSyncCommands {
recentProcessedInternalFiles = [] as string[];
async makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile> {
const stat = await this.plugin.storageAccess.statHidden(path);
const stat = await this.core.storageAccess.statHidden(path);
let version: string | undefined;
let displayName: string | undefined;
if (!stat) {
return false;
}
const contentBin = await this.plugin.storageAccess.readHiddenFileBinary(path);
const contentBin = await this.core.storageAccess.readHiddenFileBinary(path);
let content: string[];
try {
content = await arrayBufferToBase64(contentBin);
@@ -1267,7 +1267,7 @@ export class ConfigSync extends LiveSyncCommands {
}
const mtime = stat.mtime;
return {
filename: path.substring(this.app.vault.configDir.length + 1),
filename: path.substring(this.configDir.length + 1),
data: content,
mtime,
size: stat.size,
@@ -1282,12 +1282,12 @@ export class ConfigSync extends LiveSyncCommands {
const prefixedFileName = vf;
const id = await this.path2id(prefixedFileName);
const stat = await this.plugin.storageAccess.statHidden(path);
const stat = await this.core.storageAccess.statHidden(path);
if (!stat) {
return false;
}
const mtime = stat.mtime;
const content = await this.plugin.storageAccess.readHiddenFileBinary(path);
const content = await this.core.storageAccess.readHiddenFileBinary(path);
const contentBlob = createBlob([DUMMY_HEAD, DUMMY_END, ...(await arrayBufferToBase64(content))]);
// const contentBlob = createBlob(content);
try {
@@ -1308,7 +1308,7 @@ export class ConfigSync extends LiveSyncCommands {
eden: {},
};
} else {
if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) {
if (this.services.path.isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) {
this._log(
`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`,
LOG_LEVEL_DEBUG
@@ -1328,7 +1328,7 @@ export class ConfigSync extends LiveSyncCommands {
`STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`,
LOG_LEVEL_VERBOSE
);
markChangesAreSame(prefixedFileName, old.mtime, mtime + 1);
this.services.path.markChangesAreSame(prefixedFileName, old.mtime, mtime + 1);
return true;
}
saveData = {
@@ -1506,11 +1506,11 @@ export class ConfigSync extends LiveSyncCommands {
if (this._isMainSuspended()) return false;
if (!this.isThisModuleEnabled()) return false;
// if (!this.isTargetPath(path)) return false;
const stat = await this.plugin.storageAccess.statHidden(path);
const stat = await this.core.storageAccess.statHidden(path);
// Make sure that target is a file.
if (stat && stat.type != "file") return false;
const configDir = normalizePath(this.app.vault.configDir);
const configDir = normalizePath(this.configDir);
const synchronisedInConfigSync = Object.values(this.settings.pluginSyncExtendedSetting)
.filter((e) => e.mode != MODE_SELECTIVE && e.mode != MODE_SHINY)
.map((e) => e.files)
@@ -1676,7 +1676,7 @@ export class ConfigSync extends LiveSyncCommands {
}
async scanInternalFiles(): Promise<FilePath[]> {
const filenames = (await this.getFiles(this.app.vault.configDir, 2))
const filenames = (await this.getFiles(this.configDir, 2))
.filter((e) => e.startsWith("."))
.filter((e) => !e.startsWith(".trash"));
return filenames as FilePath[];
@@ -1707,7 +1707,7 @@ export class ConfigSync extends LiveSyncCommands {
choices.push(CHOICE_DISABLE);
choices.push(CHOICE_DISMISS);
const ret = await this.plugin.confirm.askSelectStringDialogue(message, choices, {
const ret = await this.core.confirm.askSelectStringDialogue(message, choices, {
defaultAction: CHOICE_DISMISS,
timeout: 40,
title: "Customisation sync",
@@ -1730,13 +1730,13 @@ export class ConfigSync extends LiveSyncCommands {
}
private _allSuspendExtraSync(): Promise<boolean> {
if (this.plugin.settings.usePluginSync || this.plugin.settings.autoSweepPlugins) {
if (this.core.settings.usePluginSync || this.core.settings.autoSweepPlugins) {
this._log(
"Customisation sync have been temporarily disabled. Please enable them after the fetching, if you need them.",
LOG_LEVEL_NOTICE
);
this.plugin.settings.usePluginSync = false;
this.plugin.settings.autoSweepPlugins = false;
this.core.settings.usePluginSync = false;
this.core.settings.autoSweepPlugins = false;
}
return Promise.resolve(true);
}
@@ -1747,14 +1747,20 @@ export class ConfigSync extends LiveSyncCommands {
}
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
if (mode == "DISABLE") {
this.plugin.settings.usePluginSync = false;
await this.plugin.saveSettings();
// this.plugin.settings.usePluginSync = false;
// await this.plugin.saveSettings();
await this.core.services.setting.applyPartial(
{
usePluginSync: false,
},
true
);
return;
}
if (mode == "CUSTOMIZE") {
if (!this.services.setting.getDeviceAndVaultName()) {
let name = await this.plugin.confirm.askString("Device name", "Please set this device name", `desktop`);
let name = await this.core.confirm.askString("Device name", "Please set this device name", `desktop`);
if (!name) {
if (Platform.isAndroidApp) {
name = "android-app";
@@ -1779,9 +1785,16 @@ export class ConfigSync extends LiveSyncCommands {
}
this.services.setting.setDeviceAndVaultName(name);
}
this.plugin.settings.usePluginSync = true;
this.plugin.settings.useAdvancedMode = true;
await this.plugin.saveSettings();
// this.core.settings.usePluginSync = true;
// this.core.settings.useAdvancedMode = true;
// await this.core.saveSettings();
await this.core.services.setting.applyPartial(
{
usePluginSync: true,
useAdvancedMode: true,
},
true
);
await this.scanAllConfigFiles(true);
}
}

View File

@@ -30,7 +30,8 @@
export let plugin: ObsidianLiveSyncPlugin;
export let isMaintenanceMode: boolean = false;
export let isFlagged: boolean = false;
const addOn = plugin.getAddOn<ConfigSync>(ConfigSync.name)!;
$: core = plugin.core;
const addOn = plugin.core.getAddOn<ConfigSync>(ConfigSync.name)!;
if (!addOn) {
Logger(`Could not load the add-on ${ConfigSync.name}`, LOG_LEVEL_INFO);
throw new Error(`Could not load the add-on ${ConfigSync.name}`);
@@ -334,13 +335,13 @@
Logger(`Could not find local item`, LOG_LEVEL_VERBOSE);
return;
}
const duplicateTermName = await plugin.confirm.askString("Duplicate", "device name", "");
const duplicateTermName = await core.confirm.askString("Duplicate", "device name", "");
if (duplicateTermName) {
if (duplicateTermName.contains("/")) {
Logger(`We can not use "/" to the device name`, LOG_LEVEL_NOTICE);
return;
}
const key = `${plugin.app.vault.configDir}/${local.files[0].filename}`;
const key = `${plugin.core.services.API.getSystemConfigDir()}/${local.files[0].filename}`;
await addOn.storeCustomizationFiles(key as FilePath, duplicateTermName);
await addOn.updatePluginList(false, addOn.filenameToUnifiedKey(key, duplicateTermName));
}

View File

@@ -23,7 +23,7 @@ export class PluginDialogModal extends Modal {
if (!this.component) {
this.component = mount(PluginPane, {
target: contentEl,
props: { plugin: this.plugin },
props: { plugin: this.plugin, core: this.plugin.core },
});
}
}

View File

@@ -22,19 +22,22 @@
import { normalizePath } from "../../deps";
import { HiddenFileSync } from "../HiddenFileSync/CmdHiddenFileSync.ts";
import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
export let plugin: ObsidianLiveSyncPlugin;
export let core :LiveSyncBaseCore;
// $: core = plugin.core;
$: hideNotApplicable = false;
$: thisTerm = plugin.services.setting.getDeviceAndVaultName();
$: thisTerm = core.services.setting.getDeviceAndVaultName();
const addOn = plugin.getAddOn(ConfigSync.name) as ConfigSync;
const addOn = core.getAddOn<ConfigSync>(ConfigSync.name)!;
if (!addOn) {
const msg =
"AddOn Module (ConfigSync) has not been loaded. This is very unexpected situation. Please report this issue.";
Logger(msg, LOG_LEVEL_NOTICE);
throw new Error(msg);
}
const addOnHiddenFileSync = plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
const addOnHiddenFileSync = core.getAddOn<HiddenFileSync>(HiddenFileSync.name) as HiddenFileSync;
if (!addOnHiddenFileSync) {
const msg =
"AddOn Module (HiddenFileSync) has not been loaded. This is very unexpected situation. Please report this issue.";
@@ -98,7 +101,7 @@
await requestUpdate();
}
async function replicate() {
await plugin.services.replication.replicate(true);
await core.services.replication.replicate(true);
}
function selectAllNewest(selectMode: boolean) {
selectNewestPulse++;
@@ -147,8 +150,8 @@
}
function applyAutomaticSync(key: string, direction: "pushForce" | "pullForce" | "safe") {
setMode(key, MODE_AUTOMATIC);
const configDir = normalizePath(plugin.app.vault.configDir);
const files = (plugin.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`);
const configDir = normalizePath(plugin.core.services.API.getSystemConfigDir());
const files = (plugin.core.settings.pluginSyncExtendedSetting[key]?.files ?? []).map((e) => `${configDir}/${e}`);
addOnHiddenFileSync.initialiseInternalFileSync(direction, true, files);
}
function askOverwriteModeForAutomatic(evt: MouseEvent, key: string) {
@@ -222,22 +225,22 @@
);
if (mode == MODE_SELECTIVE) {
automaticList.delete(key);
delete plugin.settings.pluginSyncExtendedSetting[key];
delete plugin.core.settings.pluginSyncExtendedSetting[key];
automaticListDisp = automaticList;
} else {
automaticList.set(key, mode);
automaticListDisp = automaticList;
if (!(key in plugin.settings.pluginSyncExtendedSetting)) {
plugin.settings.pluginSyncExtendedSetting[key] = {
if (!(key in plugin.core.settings.pluginSyncExtendedSetting)) {
plugin.core.settings.pluginSyncExtendedSetting[key] = {
key,
mode,
files: [],
};
}
plugin.settings.pluginSyncExtendedSetting[key].files = files;
plugin.settings.pluginSyncExtendedSetting[key].mode = mode;
plugin.core.settings.pluginSyncExtendedSetting[key].files = files;
plugin.core.settings.pluginSyncExtendedSetting[key].mode = mode;
}
plugin.services.setting.saveSettingData();
core.services.setting.saveSettingData();
}
function getIcon(mode: SYNC_MODE) {
if (mode in ICONS) {
@@ -250,7 +253,7 @@
let automaticListDisp = new Map<string, SYNC_MODE>();
// apply current configuration to the dialogue
for (const { key, mode } of Object.values(plugin.settings.pluginSyncExtendedSetting)) {
for (const { key, mode } of Object.values(plugin.core.settings.pluginSyncExtendedSetting)) {
automaticList.set(key, mode);
}
@@ -259,7 +262,7 @@
let displayKeys: Record<string, string[]> = {};
function computeDisplayKeys(list: IPluginDataExDisplay[]) {
const extraKeys = Object.keys(plugin.settings.pluginSyncExtendedSetting);
const extraKeys = Object.keys(plugin.core.settings.pluginSyncExtendedSetting);
return [
...list,
...extraKeys
@@ -321,7 +324,7 @@
$: {
pluginEntries = groupBy(filterList(list, ["PLUGIN_MAIN", "PLUGIN_DATA", "PLUGIN_ETC"]), "name");
}
let useSyncPluginEtc = plugin.settings.usePluginEtc;
let useSyncPluginEtc = plugin.core.settings.usePluginEtc;
</script>
<div class="buttonsWrap">

View File

@@ -143,7 +143,7 @@
</div>
{#if selectedObj != false}
<div class="op-scrollable json-source">
<div class="op-scrollable json-source ls-dialog">
{#each diffs as diff}
<span class={diff[0] == DIFF_DELETE ? "deleted" : diff[0] == DIFF_INSERT ? "added" : "normal"}
>{diff[1]}</span

View File

@@ -29,10 +29,7 @@ import {
} from "../../lib/src/common/utils.ts";
import {
compareMTime,
unmarkChanges,
isInternalMetadata,
markChangesAreSame,
PeriodicProcessor,
TARGET_IS_NEW,
scheduleTask,
getLogLevel,
@@ -43,6 +40,7 @@ import {
EVEN,
displayRev,
} from "../../common/utils.ts";
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
import { serialized, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { JsonResolveModal } from "../HiddenFileCommon/JsonResolveModal.ts";
import { LiveSyncCommands } from "../LiveSyncCommands.ts";
@@ -80,25 +78,25 @@ function getComparingMTime(
export class HiddenFileSync extends LiveSyncCommands {
isThisModuleEnabled() {
return this.plugin.settings.syncInternalFiles;
return this.core.settings.syncInternalFiles;
}
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(
this.plugin,
this.core,
async () => this.isThisModuleEnabled() && this._isDatabaseReady() && (await this.scanAllStorageChanges(false))
);
get kvDB() {
return this.plugin.kvDB;
return this.core.kvDB;
}
getConflictedDoc(path: FilePathWithPrefix, rev: string) {
return this.plugin.managers.conflictManager.getConflictedDoc(path, rev);
return this.core.localDatabase.managers.conflictManager.getConflictedDoc(path, rev);
}
onunload() {
this.periodicInternalFileScanProcessor?.disable();
}
onload() {
this.plugin.addCommand({
this.services.API.addCommand({
id: "livesync-sync-internal",
name: "(re)initialise hidden files between storage and database",
callback: () => {
@@ -107,7 +105,7 @@ export class HiddenFileSync extends LiveSyncCommands {
}
},
});
this.plugin.addCommand({
this.services.API.addCommand({
id: "livesync-scaninternal-storage",
name: "Scan hidden file changes on the storage",
callback: () => {
@@ -116,7 +114,7 @@ export class HiddenFileSync extends LiveSyncCommands {
}
},
});
this.plugin.addCommand({
this.services.API.addCommand({
id: "livesync-scaninternal-database",
name: "Scan hidden file changes on the local database",
callback: () => {
@@ -125,7 +123,7 @@ export class HiddenFileSync extends LiveSyncCommands {
}
},
});
this.plugin.addCommand({
this.services.API.addCommand({
id: "livesync-internal-scan-offline-changes",
name: "Scan and apply all offline hidden-file changes",
callback: () => {
@@ -244,13 +242,23 @@ export class HiddenFileSync extends LiveSyncCommands {
if (this.isThisModuleEnabled()) {
//system file
const filename = this.getPath(doc);
if (await this.services.vault.isTargetFile(filename)) {
// this.procInternalFile(filename);
await this.processReplicationResult(doc);
const unprefixedPath = stripAllPrefixes(filename);
// No need to check via vaultService
// if (!await this.services.vault.isTargetFile(unprefixedPath)) {
// this._log(`Skipped processing sync file:${unprefixedPath} (Not target)`, LOG_LEVEL_VERBOSE);
// return true;
// }
if (!(await this.isTargetFile(stripAllPrefixes(unprefixedPath)))) {
this._log(
`Skipped processing sync file:${unprefixedPath} (Not Hidden File Sync target)`,
LOG_LEVEL_VERBOSE
);
// We should return true, we made sure that document is a internalMetadata.
return true;
} else {
this._log(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE);
return false;
}
if (!(await this.processReplicationResult(doc))) {
this._log(`Failed to process sync file:${unprefixedPath}`, LOG_LEVEL_NOTICE);
// Do not yield false, this file had been processed.
}
}
return true;
@@ -259,7 +267,7 @@ export class HiddenFileSync extends LiveSyncCommands {
}
async loadFileWithInfo(path: FilePath): Promise<UXFileInfo> {
const stat = await this.plugin.storageAccess.statHidden(path);
const stat = await this.core.storageAccess.statHidden(path);
if (!stat)
return {
name: path.split("/").pop() ?? "",
@@ -274,7 +282,7 @@ export class HiddenFileSync extends LiveSyncCommands {
deleted: true,
body: createBlob(new Uint8Array(0)),
};
const content = await this.plugin.storageAccess.readHiddenFileAuto(path);
const content = await this.core.storageAccess.readHiddenFileAuto(path);
return {
name: path.split("/").pop() ?? "",
path,
@@ -296,7 +304,7 @@ export class HiddenFileSync extends LiveSyncCommands {
return `${doc.mtime}-${doc.size}-${doc._rev}-${doc._deleted || doc.deleted || false ? "-0" : "-1"}`;
}
async fileToStatKey(file: FilePath, stat: UXStat | null = null) {
if (!stat) stat = await this.plugin.storageAccess.statHidden(file);
if (!stat) stat = await this.core.storageAccess.statHidden(file);
return this.statToKey(stat);
}
@@ -310,7 +318,7 @@ export class HiddenFileSync extends LiveSyncCommands {
}
async updateLastProcessedAsActualFile(file: FilePath, stat?: UXStat | null | undefined) {
if (!stat) stat = await this.plugin.storageAccess.statHidden(file);
if (!stat) stat = await this.core.storageAccess.statHidden(file);
this._fileInfoLastProcessed.set(file, this.statToKey(stat));
}
@@ -352,38 +360,38 @@ export class HiddenFileSync extends LiveSyncCommands {
const dbMTime = getComparingMTime(db);
const storageMTime = getComparingMTime(stat);
if (dbMTime == 0 || storageMTime == 0) {
unmarkChanges(path);
this.services.path.unmarkChanges(path);
} else {
markChangesAreSame(path, getComparingMTime(db), getComparingMTime(stat));
this.services.path.markChangesAreSame(path, getComparingMTime(db), getComparingMTime(stat));
}
}
updateLastProcessedDeletion(path: FilePath, db: MetaEntry | LoadedEntry | false) {
unmarkChanges(path);
this.services.path.unmarkChanges(path);
if (db) this.updateLastProcessedDatabase(path, db);
this.updateLastProcessedFile(path, this.statToKey(null));
}
async ensureDir(path: FilePath) {
const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(path);
const isExists = await this.core.storageAccess.isExistsIncludeHidden(path);
if (!isExists) {
await this.plugin.storageAccess.ensureDir(path);
await this.core.storageAccess.ensureDir(path);
}
}
async writeFile(path: FilePath, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<UXStat | null> {
await this.plugin.storageAccess.writeHiddenFileAuto(path, data, opt);
const stat = await this.plugin.storageAccess.statHidden(path);
await this.core.storageAccess.writeHiddenFileAuto(path, data, opt);
const stat = await this.core.storageAccess.statHidden(path);
// this.updateLastProcessedFile(path, this.statToKey(stat));
return stat;
}
async __removeFile(path: FilePath): Promise<"OK" | "ALREADY" | false> {
try {
if (!(await this.plugin.storageAccess.isExistsIncludeHidden(path))) {
if (!(await this.core.storageAccess.isExistsIncludeHidden(path))) {
// Already deleted
// this.updateLastProcessedFile(path, this.statToKey(null));
return "ALREADY";
}
if (await this.plugin.storageAccess.removeHidden(path)) {
if (await this.core.storageAccess.removeHidden(path)) {
// this.updateLastProcessedFile(path, this.statToKey(null));
return "OK";
}
@@ -396,7 +404,7 @@ export class HiddenFileSync extends LiveSyncCommands {
async triggerEvent(path: FilePath) {
try {
// await this.app.vault.adapter.reconcileInternalFile(filename);
await this.plugin.storageAccess.triggerHiddenFile(path);
await this.core.storageAccess.triggerHiddenFile(path);
} catch (ex) {
this._log("Failed to call internal API(reconcileInternalFile)", LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE);
@@ -510,7 +518,7 @@ export class HiddenFileSync extends LiveSyncCommands {
LOG_LEVEL_VERBOSE
);
const taskNameAndMeta = [...files].map(
async (e) => [e, await this.plugin.storageAccess.statHidden(e)] as const
async (e) => [e, await this.core.storageAccess.statHidden(e)] as const
);
const nameAndMeta = await Promise.all(taskNameAndMeta);
const processFiles = nameAndMeta
@@ -552,7 +560,7 @@ Offline Changed files: ${processFiles.length}`;
}
try {
return await this.serializedForEvent(path, async () => {
let stat = await this.plugin.storageAccess.statHidden(path);
let stat = await this.core.storageAccess.statHidden(path);
// sometimes folder is coming.
if (stat != null && stat.type != "file") {
return false;
@@ -700,7 +708,7 @@ Offline Changed files: ${processFiles.length}`;
revFrom._revs_info
?.filter((e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo)
.first()?.rev ?? "";
const result = await this.plugin.managers.conflictManager.mergeObject(
const result = await this.localDatabase.managers.conflictManager.mergeObject(
doc.path,
commonBase,
doc._rev,
@@ -807,9 +815,9 @@ Offline Changed files: ${processFiles.length}`;
}
}
if (!keep && result) {
const isExists = await this.plugin.storageAccess.isExistsIncludeHidden(storageFilePath);
const isExists = await this.core.storageAccess.isExistsIncludeHidden(storageFilePath);
if (!isExists) {
await this.plugin.storageAccess.ensureDir(storageFilePath);
await this.core.storageAccess.ensureDir(storageFilePath);
}
const stat = await this.writeFile(storageFilePath, result);
if (!stat) {
@@ -886,7 +894,7 @@ Offline Changed files: ${processFiles.length}`;
* @returns An object containing the ignore and target filters.
*/
parseRegExpSettings() {
const regExpKey = `${this.plugin.settings.syncInternalFilesTargetPatterns}||${this.plugin.settings.syncInternalFilesIgnorePatterns}`;
const regExpKey = `${this.core.settings.syncInternalFilesTargetPatterns}||${this.core.settings.syncInternalFilesIgnorePatterns}`;
let ignoreFilter: CustomRegExp[];
let targetFilter: CustomRegExp[];
if (this.cacheFileRegExps.has(regExpKey)) {
@@ -894,8 +902,8 @@ Offline Changed files: ${processFiles.length}`;
ignoreFilter = cached[1];
targetFilter = cached[0];
} else {
ignoreFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
targetFilter = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
ignoreFilter = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
targetFilter = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
this.cacheFileRegExps.clear();
this.cacheFileRegExps.set(regExpKey, [targetFilter, ignoreFilter]);
}
@@ -933,7 +941,7 @@ Offline Changed files: ${processFiles.length}`;
* @returns An array of ignored file paths (lowercase).
*/
getCustomisationSynchronizationIgnoredFiles(): string[] {
const configDir = this.plugin.app.vault.configDir;
const configDir = this.services.API.getSystemConfigDir();
const key =
JSON.stringify(this.settings.pluginSyncExtendedSetting) + `||${this.settings.usePluginSync}||${configDir}`;
if (this.cacheCustomisationSyncIgnoredFiles.has(key)) {
@@ -1050,7 +1058,7 @@ Common untracked files: ${bothUntracked.length}`;
notifyProgress();
const rel = await semaphores.acquire();
try {
const fileStat = await this.plugin.storageAccess.statHidden(file);
const fileStat = await this.core.storageAccess.statHidden(file);
if (fileStat == null) {
// This should not be happened. But, if it happens, we should skip this.
this._log(`Unexpected error: Failed to stat file during applyOfflineChange :${file}`);
@@ -1198,7 +1206,7 @@ Offline Changed files: ${files.length}`;
// If notified about plug-ins, reloading Obsidian may not be necessary.
const updatePluginId = manifest.id;
const updatePluginName = manifest.name;
this.plugin.confirm.askInPopup(
this.core.confirm.askInPopup(
`updated-${updatePluginId}`,
`Files in ${updatePluginName} has been updated!\nPress {HERE} to reload ${updatePluginName}, or press elsewhere to dismiss this message.`,
(anchor) => {
@@ -1230,9 +1238,9 @@ Offline Changed files: ${files.length}`;
}
// If something changes left, notify for reloading Obsidian.
if (updatedFolders.indexOf(this.plugin.app.vault.configDir) >= 0) {
if (updatedFolders.indexOf(this.services.API.getSystemConfigDir()) >= 0) {
if (!this.services.appLifecycle.isReloadingScheduled()) {
this.plugin.confirm.askInPopup(
this.core.confirm.askInPopup(
`updated-any-hidden`,
`Some setting files have been modified\nPress {HERE} to schedule a reload of Obsidian, or press elsewhere to dismiss this message.`,
(anchor) => {
@@ -1250,7 +1258,7 @@ Offline Changed files: ${files.length}`;
if (this.settings.suppressNotifyHiddenFilesChange) {
return;
}
const configDir = this.plugin.app.vault.configDir;
const configDir = this.services.API.getSystemConfigDir();
if (!key.startsWith(configDir)) return;
const dirName = key.split("/").slice(0, -1).join("/");
this.queuedNotificationFiles.add(dirName);
@@ -1288,7 +1296,7 @@ Offline Changed files: ${files.length}`;
const eachProgress = onlyInNTimes(100, (progress) => p.log(`Checking ${progress}/${allFileNames.size}`));
for (const file of allFileNames) {
eachProgress();
const storageMTime = await this.plugin.storageAccess.statHidden(file);
const storageMTime = await this.core.storageAccess.statHidden(file);
const mtimeStorage = getComparingMTime(storageMTime);
const dbEntry = allDatabaseMap.get(file)!;
const mtimeDB = getComparingMTime(dbEntry);
@@ -1608,7 +1616,7 @@ Offline Changed files: ${files.length}`;
if (onlyNew) {
// Check the file is new or not.
const dbMTime = getComparingMTime(metaOnDB, includeDeletion); // metaOnDB.mtime;
const storageStat = await this.plugin.storageAccess.statHidden(storageFilePath);
const storageStat = await this.core.storageAccess.statHidden(storageFilePath);
const storageMTimeActual = storageStat?.mtime ?? 0;
const storageMTime =
storageMTimeActual == 0 ? this.getLastProcessedFileMTime(storageFilePath) : storageMTimeActual;
@@ -1662,7 +1670,7 @@ Offline Changed files: ${files.length}`;
async __checkIsNeedToWriteFile(storageFilePath: FilePath, content: string | ArrayBuffer): Promise<boolean> {
try {
const storageContent = await this.plugin.storageAccess.readHiddenFileAuto(storageFilePath);
const storageContent = await this.core.storageAccess.readHiddenFileAuto(storageFilePath);
const needWrite = !(await isDocContentSame(storageContent, content));
return needWrite;
} catch (ex) {
@@ -1674,7 +1682,7 @@ Offline Changed files: ${files.length}`;
async __writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise<false | UXStat> {
try {
const statBefore = await this.plugin.storageAccess.statHidden(storageFilePath);
const statBefore = await this.core.storageAccess.statHidden(storageFilePath);
const isExist = statBefore != null;
const writeContent = readContent(fileOnDB);
await this.ensureDir(storageFilePath);
@@ -1760,7 +1768,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
choices.push(CHOICE_MERGE);
choices.push(CHOICE_DISABLE);
const ret = await this.plugin.confirm.confirmWithMessage(
const ret = await this.core.confirm.confirmWithMessage(
"Hidden file sync",
message,
choices,
@@ -1779,12 +1787,12 @@ ${messageFetch}${messageOverwrite}${messageMerge}
}
private _allSuspendExtraSync(): Promise<boolean> {
if (this.plugin.settings.syncInternalFiles) {
if (this.core.settings.syncInternalFiles) {
this._log(
"Hidden file synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.",
LOG_LEVEL_NOTICE
);
this.plugin.settings.syncInternalFiles = false;
this.core.settings.syncInternalFiles = false;
}
return Promise.resolve(true);
}
@@ -1807,9 +1815,15 @@ ${messageFetch}${messageOverwrite}${messageMerge}
}
if (mode == "DISABLE" || mode == "DISABLE_HIDDEN") {
// await this.plugin.$allSuspendExtraSync();
this.plugin.settings.syncInternalFiles = false;
await this.plugin.saveSettings();
// await this.core.$allSuspendExtraSync();
await this.core.services.setting.applyPartial(
{
syncInternalFiles: false,
},
true
);
// this.core.settings.syncInternalFiles = false;
// await this.core.saveSettings();
return;
}
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
@@ -1820,10 +1834,17 @@ ${messageFetch}${messageOverwrite}${messageMerge}
} else if (mode == "MERGE") {
await this.initialiseInternalFileSync("safe", true);
}
this.plugin.settings.useAdvancedMode = true;
this.plugin.settings.syncInternalFiles = true;
await this.core.services.setting.applyPartial(
{
useAdvancedMode: true,
syncInternalFiles: true,
},
true
);
// this.plugin.settings.useAdvancedMode = true;
// this.plugin.settings.syncInternalFiles = true;
await this.plugin.saveSettings();
// await this.plugin.saveSettings();
this._log(`Done! Restarting the app is strongly recommended!`, LOG_LEVEL_NOTICE);
}
// <-- Configuration handling
@@ -1843,7 +1864,7 @@ ${messageFetch}${messageOverwrite}${messageMerge}
const files = fileNames.map(async (e) => {
return {
path: e,
stat: await this.plugin.storageAccess.statHidden(e), // this.plugin.vaultAccess.adapterStat(e)
stat: await this.core.storageAccess.statHidden(e), // this.plugin.vaultAccess.adapterStat(e)
};
});
const result: InternalFileInfo[] = [];
@@ -1948,5 +1969,6 @@ ${messageFetch}${messageOverwrite}${messageMerge}
services.setting.suspendExtraSync.addHandler(this._allSuspendExtraSync.bind(this));
services.setting.suggestOptionalFeatures.addHandler(this._allAskUsingOptionalSyncFeature.bind(this));
services.setting.enableOptionalFeature.addHandler(this._allConfigureOptionalSyncFeature.bind(this));
services.vault.isTargetFileInExtra.addHandler(this.isTargetFile.bind(this));
}
}

View File

@@ -16,18 +16,22 @@ import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils.ts";
let noticeIndex = 0;
export abstract class LiveSyncCommands {
/**
* @deprecated This class is deprecated. Please use core
*/
plugin: ObsidianLiveSyncPlugin;
core: LiveSyncCore;
get app() {
return this.plugin.app;
}
get settings() {
return this.plugin.settings;
return this.core.settings;
}
get localDatabase() {
return this.plugin.localDatabase;
return this.core.localDatabase;
}
get services() {
return this.plugin.services;
return this.core.services;
}
// id2path(id: DocumentID, entry?: EntryHasPath, stripPrefix?: boolean): FilePathWithPrefix {
@@ -41,9 +45,10 @@ export abstract class LiveSyncCommands {
return this.services.path.getPath(entry);
}
constructor(plugin: ObsidianLiveSyncPlugin) {
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
this.plugin = plugin;
this.onBindFunction(plugin, plugin.services);
this.core = core;
this.onBindFunction(this.core, this.core.services);
this._log = createInstanceLogFunction(this.constructor.name, this.services.API);
__$checkInstanceBinding(this);
}
@@ -51,7 +56,7 @@ export abstract class LiveSyncCommands {
abstract onload(): void | Promise<void>;
_isMainReady() {
return this.plugin.services.appLifecycle.isReady();
return this.services.appLifecycle.isReady();
}
_isMainSuspended() {
return this.services.appLifecycle.isSuspended();

View File

@@ -71,7 +71,7 @@ export class LocalDatabaseMaintenance extends LiveSyncCommands {
async confirm(title: string, message: string, affirmative = "Yes", negative = "No") {
return (
(await this.plugin.confirm.askSelectStringDialogue(message, [affirmative, negative], {
(await this.core.confirm.askSelectStringDialogue(message, [affirmative, negative], {
title,
defaultAction: affirmative,
})) === affirmative
@@ -302,7 +302,7 @@ Note: **Make sure to synchronise all devices before deletion.**
}
async scanUnusedChunks() {
const kvDB = this.plugin.kvDB;
const kvDB = this.core.kvDB;
const chunkSet = (await kvDB.get<Set<DocumentID>>(DB_KEY_CHUNK_SET)) || new Set();
const chunkUsageMap = (await kvDB.get<ChunkUsageMap>(DB_KEY_DOC_USAGE_MAP)) || new Map();
const KEEP_MAX_REVS = 10;
@@ -328,7 +328,7 @@ Note: **Make sure to synchronise all devices before deletion.**
async trackChanges(fromStart: boolean = false, showNotice: boolean = false) {
if (!this.isAvailable()) return;
const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
const kvDB = this.plugin.kvDB;
const kvDB = this.core.kvDB;
const previousSeq = fromStart ? "" : await kvDB.get<string>(DB_KEY_SEQ);
const chunkSet = (await kvDB.get<Set<DocumentID>>(DB_KEY_CHUNK_SET)) || new Set();
@@ -457,7 +457,7 @@ Are you ready to delete unused chunks?`;
const BUTTON_OK = `Yes, delete chunks`;
const BUTTON_CANCEL = "Cancel";
const result = await this.plugin.confirm.askSelectStringDialogue(
const result = await this.core.confirm.askSelectStringDialogue(
confirmMessage,
[BUTTON_OK, BUTTON_CANCEL] as const,
{
@@ -506,7 +506,7 @@ Are you ready to delete unused chunks?`;
const message = `Garbage Collection completed.
Success: ${successCount}, Errored: ${errored}`;
this._log(message, logLevel);
const kvDB = this.plugin.kvDB;
const kvDB = this.core.kvDB;
await kvDB.set(DB_KEY_CHUNK_SET, chunkSet);
}
@@ -723,7 +723,7 @@ Success: ${successCount}, Errored: ${errored}`;
}
async compactDatabase() {
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true);
if (!remote) {
this._notice("Failed to connect to remote for compaction.", "gc-compact");
@@ -767,7 +767,7 @@ Success: ${successCount}, Errored: ${errored}`;
// Temporarily set revs_limit to 1, perform compaction, and restore the original revs_limit.
// Very dangerous operation, so now suppressed.
return false;
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
const remote = await replicator.connectRemoteCouchDBWithSetting(this.settings, false, false, true);
if (!remote) {
this._notice("Failed to connect to remote for compaction.");
@@ -822,7 +822,7 @@ Success: ${successCount}, Errored: ${errored}`;
}
async gcv3() {
if (!this.isAvailable()) return;
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
// Start one-shot replication to ensure all changes are synced before GC.
const r0 = await replicator.openOneShotReplication(this.settings, false, false, "sync");
if (!r0) {
@@ -835,7 +835,7 @@ Success: ${successCount}, Errored: ${errored}`;
// Delete the chunk, but first verify the following:
// Fetch the list of accepted nodes from the replicator.
const OPTION_CANCEL = "Cancel Garbage Collection";
const info = await this.plugin.replicator.getConnectedDeviceList();
const info = await this.core.replicator.getConnectedDeviceList();
if (!info) {
this._notice("No connected device information found. Cancelling Garbage Collection.");
return;
@@ -855,7 +855,7 @@ It is preferable to update all devices if possible. If you have any devices that
const OPTION_IGNORE = "Ignore and Proceed";
// const OPTION_DELETE = "Delete them and proceed";
const buttons = [OPTION_CANCEL, OPTION_IGNORE] as const;
const result = await this.plugin.confirm.askSelectStringDialogue(message, buttons, {
const result = await this.core.confirm.askSelectStringDialogue(message, buttons, {
title: "Node Information Missing",
defaultAction: OPTION_CANCEL,
});
@@ -896,7 +896,7 @@ This may indicate that some devices have not completed synchronisation, which co
: `All devices have the same progress value (${maxProgress}). Your devices seem to be synchronised. And be able to proceed with Garbage Collection.`;
const buttons = [OPTION_PROCEED, OPTION_CANCEL] as const;
const defaultAction = progressDifference != 0 ? OPTION_CANCEL : OPTION_PROCEED;
const result = await this.plugin.confirm.askSelectStringDialogue(message + "\n\n" + detail, buttons, {
const result = await this.core.confirm.askSelectStringDialogue(message + "\n\n" + detail, buttons, {
title: "Garbage Collection Confirmation",
defaultAction,
});

View File

@@ -38,14 +38,14 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
storeP2PStatusLine = reactiveSource("");
getSettings(): P2PSyncSetting {
return this.plugin.settings;
return this.core.settings;
}
getDB() {
return this.plugin.localDatabase.localDatabase;
return this.core.localDatabase.localDatabase;
}
get confirm(): Confirm {
return this.plugin.confirm;
return this.core.confirm;
}
_simpleStore!: SimpleStore<any>;
@@ -53,8 +53,8 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
return this._simpleStore;
}
constructor(plugin: ObsidianLiveSyncPlugin) {
super(plugin);
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
super(plugin, core);
setReplicatorFunc(() => this._replicatorInstance);
addP2PEventHandlers(this);
this.afterConstructor();
@@ -72,7 +72,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_P2P) {
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin));
return Promise.resolve(new LiveSyncTrysteroReplicator(this.plugin.core));
}
return undefined!;
}
@@ -183,12 +183,12 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
}
private async _allSuspendExtraSync() {
this.plugin.settings.P2P_Enabled = false;
this.plugin.settings.P2P_AutoAccepting = AutoAccepting.NONE;
this.plugin.settings.P2P_AutoBroadcast = false;
this.plugin.settings.P2P_AutoStart = false;
this.plugin.settings.P2P_AutoSyncPeers = "";
this.plugin.settings.P2P_AutoWatchPeers = "";
this.plugin.core.settings.P2P_Enabled = false;
this.plugin.core.settings.P2P_AutoAccepting = AutoAccepting.NONE;
this.plugin.core.settings.P2P_AutoBroadcast = false;
this.plugin.core.settings.P2P_AutoStart = false;
this.plugin.core.settings.P2P_AutoSyncPeers = "";
this.plugin.core.settings.P2P_AutoWatchPeers = "";
return await Promise.resolve(true);
}
@@ -201,7 +201,10 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
}
async _everyOnloadStart(): Promise<boolean> {
this.plugin.registerView(VIEW_TYPE_P2P, (leaf) => new P2PReplicatorPaneView(leaf, this.plugin));
this.plugin.registerView(
VIEW_TYPE_P2P,
(leaf) => new P2PReplicatorPaneView(leaf, this.plugin.core, this.plugin)
);
this.plugin.addCommand({
id: "open-p2p-replicator",
name: "P2P Sync : Open P2P Replicator",

View File

@@ -20,17 +20,18 @@
import { type P2PReplicatorStatus } from "../../../lib/src/replication/trystero/TrysteroReplicator";
import { $msg as _msg } from "../../../lib/src/common/i18n";
import { SETTING_KEY_P2P_DEVICE_NAME } from "../../../lib/src/common/types";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
interface Props {
plugin: PluginShim;
cmdSync: CommandShim;
core: LiveSyncBaseCore;
}
let { plugin, cmdSync }: Props = $props();
let { cmdSync, core }: Props = $props();
// const cmdSync = plugin.getAddOn<P2PReplicator>("P2PReplicator")!;
setContext("getReplicator", () => cmdSync);
const initialSettings = { ...plugin.settings };
const currentSettings = () => core.services.setting.currentSettings() as P2PSyncSetting;
const initialSettings = { ...currentSettings() } as P2PSyncSetting;
let settings = $state<P2PSyncSetting>(initialSettings);
@@ -70,21 +71,33 @@
);
async function saveAndApply() {
const newSettings = {
...plugin.settings,
P2P_Enabled: eP2PEnabled,
P2P_relays: eRelay,
P2P_roomID: eRoomId,
P2P_passphrase: ePassword,
P2P_AppID: eAppId,
P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
P2P_AutoStart: eAutoStart,
P2P_AutoBroadcast: eAutoBroadcast,
};
plugin.settings = newSettings;
// const newSettings = {
// ...currentSettings(),
// P2P_Enabled: eP2PEnabled,
// P2P_relays: eRelay,
// P2P_roomID: eRoomId,
// P2P_passphrase: ePassword,
// P2P_AppID: eAppId,
// P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
// P2P_AutoStart: eAutoStart,
// P2P_AutoBroadcast: eAutoBroadcast,
// };
await core.services.setting.applyPartial(
{
P2P_Enabled: eP2PEnabled,
P2P_relays: eRelay,
P2P_roomID: eRoomId,
P2P_passphrase: ePassword,
P2P_AppID: eAppId,
P2P_AutoAccepting: eAutoAccept ? AutoAccepting.ALL : AutoAccepting.NONE,
P2P_AutoStart: eAutoStart,
P2P_AutoBroadcast: eAutoBroadcast,
},
true
);
cmdSync.setConfig(SETTING_KEY_P2P_DEVICE_NAME, eDeviceName);
deviceName = eDeviceName;
await plugin.saveSettings();
// await plugin.saveSettings();
}
async function revert() {
eP2PEnabled = settings.P2P_Enabled;
@@ -100,8 +113,9 @@
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
let replicatorInfo = $state<P2PReplicatorStatus | undefined>(undefined);
const applyLoadSettings = (d: P2PSyncSetting, force: boolean) => {
if(force){
const initDeviceName = cmdSync.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? plugin.services.vault.getVaultName();
if (force) {
const initDeviceName =
cmdSync.getConfig(SETTING_KEY_P2P_DEVICE_NAME) ?? core.services.vault.getVaultName();
deviceName = initDeviceName;
eDeviceName = initDeviceName;
}
@@ -124,7 +138,7 @@
closeServer();
});
const rx = eventHub.onEvent(EVENT_LAYOUT_READY, () => {
applyLoadSettings(plugin.settings, true);
applyLoadSettings(currentSettings(), true);
});
const r2 = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
serverInfo = status;
@@ -254,7 +268,7 @@
cmdSync.setConfig(initialDialogStatusKey, JSON.stringify(dialogStatus));
});
let isObsidian = $derived.by(() => {
return plugin.services.API.getPlatform() === "obsidian";
return core.services.API.getPlatform() === "obsidian";
});
</script>

View File

@@ -13,6 +13,7 @@ import {
EVENT_P2P_PEER_SHOW_EXTRA_MENU,
type PeerStatus,
} from "../../../lib/src/replication/trystero/P2PReplicatorPaneCommon.ts";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
export const VIEW_TYPE_P2P = "p2p-replicator";
function addToList(item: string, list: string) {
@@ -34,7 +35,8 @@ function removeFromList(item: string, list: string) {
}
export class P2PReplicatorPaneView extends SvelteItemView {
plugin: ObsidianLiveSyncPlugin;
// plugin: ObsidianLiveSyncPlugin;
core: LiveSyncBaseCore;
override icon = "waypoints";
title: string = "";
override navigation = false;
@@ -43,7 +45,7 @@ export class P2PReplicatorPaneView extends SvelteItemView {
return "waypoints";
}
get replicator() {
const r = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
const r = this.core.getAddOn<P2PReplicator>(P2PReplicator.name);
if (!r || !r._replicatorInstance) {
throw new Error("Replicator not found");
}
@@ -66,7 +68,7 @@ export class P2PReplicatorPaneView extends SvelteItemView {
const DROP = "Yes, and drop local database";
const KEEP = "Yes, but keep local database";
const CANCEL = "No, cancel";
const yn = await this.plugin.confirm.askSelectStringDialogue(
const yn = await this.core.confirm.askSelectStringDialogue(
`Do you really want to apply the remote config? This will overwrite your current config immediately and restart.
And you can also drop the local database to rebuild from the remote device.`,
[DROP, KEEP, CANCEL] as const,
@@ -78,7 +80,7 @@ And you can also drop the local database to rebuild from the remote device.`,
if (yn === DROP || yn === KEEP) {
if (yn === DROP) {
if (remoteConfig.remoteType !== REMOTE_P2P) {
const yn2 = await this.plugin.confirm.askYesNoDialog(
const yn2 = await this.core.confirm.askYesNoDialog(
`Do you want to set the remote type to "P2P Sync" to rebuild by "P2P replication"?`,
{
title: "Rebuild from remote device",
@@ -90,12 +92,14 @@ And you can also drop the local database to rebuild from the remote device.`,
}
}
}
this.plugin.settings = remoteConfig;
await this.plugin.saveSettings();
// this.plugin.settings = remoteConfig;
// await this.plugin.saveSettings();
await this.core.services.setting.applyPartial(remoteConfig);
if (yn === DROP) {
await this.plugin.rebuilder.scheduleFetch();
await this.core.rebuilder.scheduleFetch();
} else {
this.plugin.services.appLifecycle.scheduleRestart();
this.core.services.appLifecycle.scheduleRestart();
}
} else {
Logger(`Cancelled\nRemote config for ${peer.name} is not applied`, LOG_LEVEL_NOTICE);
@@ -113,19 +117,24 @@ And you can also drop the local database to rebuild from the remote device.`,
} as const;
const targetSetting = settingMap[prop];
const currentSettingAll = this.core.services.setting.currentSettings();
const currentSetting = {
[targetSetting]: currentSettingAll ? currentSettingAll[targetSetting] : "",
};
if (peer[prop]) {
this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
await this.plugin.saveSettings();
// this.plugin.settings[targetSetting] = removeFromList(peer.name, this.plugin.settings[targetSetting]);
// await this.plugin.saveSettings();
currentSetting[targetSetting] = removeFromList(peer.name, currentSetting[targetSetting]);
} else {
this.plugin.settings[targetSetting] = addToList(peer.name, this.plugin.settings[targetSetting]);
await this.plugin.saveSettings();
currentSetting[targetSetting] = addToList(peer.name, currentSetting[targetSetting]);
}
await this.plugin.saveSettings();
await this.core.services.setting.applyPartial(currentSetting, true);
}
m?: Menu;
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin) {
constructor(leaf: WorkspaceLeaf, core: LiveSyncBaseCore, plugin: ObsidianLiveSyncPlugin) {
super(leaf);
this.plugin = plugin;
// this.plugin = plugin;
this.core = core;
eventHub.onEvent(EVENT_P2P_PEER_SHOW_EXTRA_MENU, ({ peer, event }) => {
if (this.m) {
this.m.hide();
@@ -183,15 +192,15 @@ And you can also drop the local database to rebuild from the remote device.`,
}
}
instantiateComponent(target: HTMLElement) {
const cmdSync = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
const cmdSync = this.core.getAddOn<P2PReplicator>(P2PReplicator.name);
if (!cmdSync) {
throw new Error("Replicator not found");
}
return mount(ReplicatorPaneComponent, {
target: target,
props: {
plugin: cmdSync.plugin,
cmdSync: cmdSync,
core: this.core,
},
});
}

Submodule src/lib updated: 4af350bb67...7989f57e06

View File

@@ -1,399 +1,110 @@
import { Notice, Plugin, type App, type PluginManifest } from "./deps";
import {
type EntryDoc,
type ObsidianLiveSyncSettings,
type HasSettings,
LOG_LEVEL_INFO,
} from "./lib/src/common/types.ts";
import { type SimpleStore } from "./lib/src/common/utils.ts";
import { type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
import { type LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator.js";
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
import { type LiveSyncJournalReplicatorEnv } from "./lib/src/replication/journal/LiveSyncJournalReplicator.js";
import { type LiveSyncCouchDBReplicatorEnv } from "./lib/src/replication/couchdb/LiveSyncReplicator.js";
import type { CheckPointInfo } from "./lib/src/replication/journal/JournalSyncTypes.js";
import type { IObsidianModule } from "./modules/AbstractObsidianModule.ts";
import { ModuleDev } from "./modules/extras/ModuleDev.ts";
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
import { ModuleCheckRemoteSize } from "./modules/essentialObsidian/ModuleCheckRemoteSize.ts";
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver.ts";
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
import { ModuleLog } from "./modules/features/ModuleLog.ts";
import { ModuleRedFlag } from "./modules/coreFeatures/ModuleRedFlag.ts";
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
import { SetupManager } from "./modules/features/SetupManager.ts";
import type { StorageAccess } from "@lib/interfaces/StorageAccess.ts";
import type { Confirm } from "./lib/src/interfaces/Confirm.ts";
import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder.ts";
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts";
import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidianEvents.ts";
import { AbstractModule } from "./modules/AbstractModule.ts";
import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts";
import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts";
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
import { ModuleInitializerFile } from "./modules/essential/ModuleInitializerFile.ts";
import { ModuleReplicator } from "./modules/core/ModuleReplicator.ts";
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB.ts";
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO.ts";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess.ts";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker.ts";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks.ts";
import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts";
import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts";
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder.ts";
import type { IFileHandler } from "@lib/interfaces/FileHandler.ts";
import { ServiceDatabaseFileAccess } from "@/serviceModules/DatabaseFileAccess.ts";
import { ServiceFileAccessObsidian } from "@/serviceModules/ServiceFileAccessImpl.ts";
import { StorageAccessManager } from "@lib/managers/StorageProcessingManager.ts";
import { __$checkInstanceBinding } from "./lib/src/dev/checks.ts";
import { ServiceFileHandler } from "./serviceModules/FileHandler.ts";
import { FileAccessObsidian } from "./serviceModules/FileAccessObsidian.ts";
import { StorageEventManagerObsidian } from "./managers/StorageEventManagerObsidian.ts";
import { onLayoutReadyFeatures } from "./serviceFeatures/onLayoutReady.ts";
import type { ServiceModules } from "./types.ts";
import { useTargetFilters } from "@lib/serviceFeatures/targetFilter.ts";
import { setNoticeClass } from "@lib/mock_and_interop/wrapper.ts";
import type { ObsidianServiceContext } from "./lib/src/services/implements/obsidian/ObsidianServiceContext.ts";
import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts";
import { ModuleSetupObsidian } from "./modules/features/ModuleSetupObsidian.ts";
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
import { SetupManager } from "./modules/features/SetupManager.ts";
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
import { enableI18nFeature } from "./serviceFeatures/onLayoutReady/enablei18n.ts";
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
export default class ObsidianLiveSyncPlugin extends Plugin {
core: LiveSyncCore;
export default class ObsidianLiveSyncPlugin
extends Plugin
implements
LiveSyncLocalDBEnv,
LiveSyncReplicatorEnv,
LiveSyncJournalReplicatorEnv,
LiveSyncCouchDBReplicatorEnv,
HasSettings<ObsidianLiveSyncSettings>
{
/**
* The service hub for managing all services.
*/
_services: InjectableServiceHub<ServiceContext> | undefined = undefined;
get services() {
if (!this._services) {
throw new Error("Services not initialised yet");
}
return this._services;
}
/**
* Service Modules
*/
protected _serviceModules: ServiceModules;
get serviceModules() {
return this._serviceModules;
}
/**
* addOns: Non-essential and graphically features
*/
addOns = [] as LiveSyncCommands[];
/**
* The modules of the plug-in. Modules are responsible for specific features or functionalities of the plug-in, such as file handling, conflict resolution, replication, etc.
*/
private modules = [
// Move to registerModules
] as (IObsidianModule | AbstractModule)[];
/**
* register an add-onn to the plug-in.
* Add-ons are features that are not essential to the core functionality of the plugin,
* @param addOn
*/
private _registerAddOn(addOn: LiveSyncCommands) {
this.addOns.push(addOn);
this.services.appLifecycle.onUnload.addHandler(() => Promise.resolve(addOn.onunload()).then(() => true));
}
private registerAddOns() {
this._registerAddOn(new ConfigSync(this));
this._registerAddOn(new HiddenFileSync(this));
this._registerAddOn(new LocalDatabaseMaintenance(this));
this._registerAddOn(new P2PReplicator(this));
}
/**
* Get an add-on by its class name. Returns undefined if not found.
* @param cls
* @returns
*/
getAddOn<T extends LiveSyncCommands>(cls: string) {
for (const addon of this.addOns) {
if (addon.constructor.name == cls) return addon as T;
}
return undefined;
}
/**
* Get a module by its class. Throws an error if not found.
* Mostly used for getting SetupManager.
* @param constructor
* @returns
*/
getModule<T extends IObsidianModule>(constructor: new (...args: any[]) => T): T {
for (const module of this.modules) {
if (module.constructor === constructor) return module as T;
}
throw new Error(`Module ${constructor} not found or not loaded.`);
}
/**
* Register a module to the plug-in.
* @param module The module to register.
*/
private _registerModule(module: IObsidianModule) {
this.modules.push(module);
}
private registerModules() {
this._registerModule(new ModuleLiveSyncMain(this));
this._registerModule(new ModuleConflictChecker(this));
this._registerModule(new ModuleReplicatorMinIO(this));
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleConflictResolver(this));
this._registerModule(new ModulePeriodicProcess(this));
this._registerModule(new ModuleInitializerFile(this));
this._registerModule(new ModuleObsidianEvents(this, this));
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
this._registerModule(new ModuleObsidianSettingsAsMarkdown(this));
this._registerModule(new ModuleObsidianSettingDialogue(this, this));
this._registerModule(new ModuleLog(this, this));
this._registerModule(new ModuleObsidianMenu(this));
this._registerModule(new ModuleSetupObsidian(this));
this._registerModule(new ModuleObsidianDocumentHistory(this, this));
this._registerModule(new ModuleMigration(this));
this._registerModule(new ModuleRedFlag(this));
this._registerModule(new ModuleInteractiveConflictResolver(this, this));
this._registerModule(new ModuleObsidianGlobalHistory(this, this));
this._registerModule(new ModuleCheckRemoteSize(this));
// Test and Dev Modules
this._registerModule(new ModuleDev(this, this));
this._registerModule(new ModuleReplicateTest(this, this));
this._registerModule(new ModuleIntegratedTest(this, this));
this._registerModule(new SetupManager(this));
}
/**
* Bind module functions to services.
*/
private bindModuleFunctions() {
for (const module of this.modules) {
if (module instanceof AbstractModule) {
module.onBindFunction(this, this.services);
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
} else {
this.services.API.addLog(
`Module ${module.constructor.name} does not have onBindFunction, skipping binding.`,
LOG_LEVEL_INFO
);
}
}
}
/**
* @obsolete Use services.UI.confirm instead. The confirm function to show a confirmation dialog to the user.
*/
get confirm(): Confirm {
return this.services.UI.confirm;
}
/**
* @obsolete Use services.setting.currentSettings instead. The current settings of the plug-in.
*/
get settings() {
return this.services.setting.settings;
}
/**
* @obsolete Use services.setting.settings instead. Set the settings of the plug-in.
*/
set settings(value: ObsidianLiveSyncSettings) {
this.services.setting.settings = value;
}
/**
* @obsolete Use services.setting.currentSettings instead. Get the settings of the plug-in.
* @returns The current settings of the plug-in.
*/
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
/**
* @obsolete Use services.database.localDatabase instead. The local database instance.
*/
get localDatabase() {
return this.services.database.localDatabase;
}
/**
* @obsolete Use services.database.managers instead. The database managers, including entry manager, revision manager, etc.
*/
get managers() {
return this.services.database.managers;
}
/**
* @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database.
* @returns The PouchDB database instance.
*/
getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase;
}
/**
* @obsolete Use services.keyValueDB.simpleStore instead. A simple key-value store for storing non-file data, such as checkpoints, sync status, etc.
*/
get simpleStore() {
return this.services.keyValueDB.simpleStore as SimpleStore<CheckPointInfo>;
}
/**
* @obsolete Use services.replication.getActiveReplicator instead. Get the active replicator instance. Note that there can be multiple replicators, but only one can be active at a time.
*/
get replicator() {
return this.services.replicator.getActiveReplicator()!;
}
/**
* @obsolete Use services.keyValueDB.kvDB instead. Get the key-value database instance. This is used for storing large data that cannot be stored in the simple store, such as file metadata, etc.
*/
get kvDB() {
return this.services.keyValueDB.kvDB;
}
/// Modules which were relied on services
/**
* Storage Accessor for handling file operations.
* @obsolete Use serviceModules.storageAccess instead.
*/
get storageAccess(): StorageAccess {
return this.serviceModules.storageAccess;
}
/**
* Database File Accessor for handling file operations related to the database, such as exporting the database, importing from a file, etc.
* @obsolete Use serviceModules.databaseFileAccess instead.
*/
get databaseFileAccess(): DatabaseFileAccess {
return this.serviceModules.databaseFileAccess;
}
/**
* File Handler for handling file operations related to replication, such as resolving conflicts, applying changes from replication, etc.
* @obsolete Use serviceModules.fileHandler instead.
*/
get fileHandler(): IFileHandler {
return this.serviceModules.fileHandler;
}
/**
* Rebuilder for handling database rebuilding operations.
* @obsolete Use serviceModules.rebuilder instead.
*/
get rebuilder(): Rebuilder {
return this.serviceModules.rebuilder;
}
// requestCount = reactiveSource(0);
// responseCount = reactiveSource(0);
// totalQueued = reactiveSource(0);
// batched = reactiveSource(0);
// processing = reactiveSource(0);
// databaseQueueCount = reactiveSource(0);
// storageApplyingCount = reactiveSource(0);
// replicationResultCount = reactiveSource(0);
// pendingFileEventCount = reactiveSource(0);
// processingFileEventCount = reactiveSource(0);
// _totalProcessingCount?: ReactiveValue<number>;
// replicationStat = reactiveSource({
// sent: 0,
// arrived: 0,
// maxPullSeq: 0,
// maxPushSeq: 0,
// lastSyncPullSeq: 0,
// lastSyncPushSeq: 0,
// syncStatus: "CLOSED" as DatabaseConnectingStatus,
// });
private initialiseServices() {
this._services = new ObsidianServiceHub(this);
}
/**
* Initialise service modules.
*/
private initialiseServiceModules() {
private initialiseServiceModules(
core: LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>,
services: InjectableServiceHub<ObsidianServiceContext>
): ServiceModules {
const storageAccessManager = new StorageAccessManager();
// If we want to implement to the other platform, implement ObsidianXXXXXService.
const vaultAccess = new FileAccessObsidian(this.app, {
storageAccessManager: storageAccessManager,
vaultService: this.services.vault,
settingService: this.services.setting,
APIService: this.services.API,
vaultService: services.vault,
settingService: services.setting,
APIService: services.API,
pathService: services.path,
});
const storageEventManager = new StorageEventManagerObsidian(this, this, {
fileProcessing: this.services.fileProcessing,
setting: this.services.setting,
vaultService: this.services.vault,
const storageEventManager = new StorageEventManagerObsidian(this, core, {
fileProcessing: services.fileProcessing,
setting: services.setting,
vaultService: services.vault,
storageAccessManager: storageAccessManager,
APIService: this.services.API,
APIService: services.API,
});
const storageAccess = new ServiceFileAccessObsidian({
API: this.services.API,
setting: this.services.setting,
fileProcessing: this.services.fileProcessing,
vault: this.services.vault,
appLifecycle: this.services.appLifecycle,
API: services.API,
setting: services.setting,
fileProcessing: services.fileProcessing,
vault: services.vault,
appLifecycle: services.appLifecycle,
storageEventManager: storageEventManager,
storageAccessManager: storageAccessManager,
vaultAccess: vaultAccess,
});
const databaseFileAccess = new ServiceDatabaseFileAccess({
API: this.services.API,
database: this.services.database,
path: this.services.path,
API: services.API,
database: services.database,
path: services.path,
storageAccess: storageAccess,
vault: this.services.vault,
vault: services.vault,
});
const fileHandler = new ServiceFileHandler({
API: this.services.API,
API: services.API,
databaseFileAccess: databaseFileAccess,
conflict: this.services.conflict,
setting: this.services.setting,
fileProcessing: this.services.fileProcessing,
vault: this.services.vault,
path: this.services.path,
replication: this.services.replication,
conflict: services.conflict,
setting: services.setting,
fileProcessing: services.fileProcessing,
vault: services.vault,
path: services.path,
replication: services.replication,
storageAccess: storageAccess,
});
const rebuilder = new ServiceRebuilder({
API: this.services.API,
database: this.services.database,
appLifecycle: this.services.appLifecycle,
setting: this.services.setting,
remote: this.services.remote,
databaseEvents: this.services.databaseEvents,
replication: this.services.replication,
replicator: this.services.replicator,
UI: this.services.UI,
vault: this.services.vault,
API: services.API,
database: services.database,
appLifecycle: services.appLifecycle,
setting: services.setting,
remote: services.remote,
databaseEvents: services.databaseEvents,
replication: services.replication,
replicator: services.replicator,
UI: services.UI,
vault: services.vault,
fileHandler: fileHandler,
storageAccess: storageAccess,
control: this.services.control,
control: services.control,
});
return {
rebuilder,
@@ -407,20 +118,7 @@ export default class ObsidianLiveSyncPlugin
* @obsolete Use services.setting.saveSettingData instead. Save the settings to the disk. This is usually called after changing the settings in the code, to persist the changes.
*/
async saveSettings() {
await this.services.setting.saveSettingData();
}
/**
* Initialise ServiceFeatures.
* (Please refer `serviceFeatures` for more details)
*/
initialiseServiceFeatures() {
for (const feature of onLayoutReadyFeatures) {
const curriedFeature = () => feature(this);
this.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
}
// enable target filter feature.
useTargetFilters(this);
await this.core.services.setting.saveSettingData();
}
constructor(app: App, manifest: PluginManifest) {
@@ -428,26 +126,60 @@ export default class ObsidianLiveSyncPlugin
// Maybe no more need to setNoticeClass, but for safety, set it in the constructor of the main plugin class.
// TODO: remove this.
setNoticeClass(Notice);
this.initialiseServices();
this.registerModules();
this.registerAddOns();
this._serviceModules = this.initialiseServiceModules();
this.initialiseServiceFeatures();
this.bindModuleFunctions();
const serviceHub = new ObsidianServiceHub(this);
this.core = new LiveSyncBaseCore(
serviceHub,
(core, serviceHub) => {
return this.initialiseServiceModules(core, serviceHub);
},
(core) => {
const extraModules = [
new ModuleObsidianEvents(this, core),
new ModuleObsidianSettingDialogue(this, core),
new ModuleObsidianMenu(core),
new ModuleSetupObsidian(core),
new ModuleObsidianSettingsAsMarkdown(core),
new ModuleLog(this, core),
new ModuleObsidianDocumentHistory(this, core),
new ModuleInteractiveConflictResolver(this, core),
new ModuleObsidianGlobalHistory(this, core),
new ModuleDev(this, core),
new ModuleReplicateTest(this, core),
new ModuleIntegratedTest(this, core),
new SetupManager(core), // this should be moved to core?
new ModuleMigration(core),
];
return extraModules;
},
(core) => {
const addOns = [
new ConfigSync(this, core),
new HiddenFileSync(this, core),
new LocalDatabaseMaintenance(this, core),
new P2PReplicator(this, core),
];
return addOns;
},
(core) => {
//TODO Fix: useXXXX
const featuresInitialiser = enableI18nFeature;
const curriedFeature = () => featuresInitialiser(core);
core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
}
);
}
private async _startUp() {
if (!(await this.services.control.onLoad())) return;
const onReady = this.services.control.onReady.bind(this.services.control);
if (!(await this.core.services.control.onLoad())) return;
const onReady = this.core.services.control.onReady.bind(this.core.services.control);
this.app.workspace.onLayoutReady(onReady);
}
override onload() {
void this._startUp();
}
override onunload() {
return void this.services.control.onUnload();
return void this.core.services.control.onUnload();
}
}
// For now,
export type LiveSyncCore = ObsidianLiveSyncPlugin;

View File

@@ -0,0 +1,137 @@
import { TFile, TFolder } from "@/deps";
import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types";
import type { FileEventItem } from "@lib/common/types";
import type { IStorageEventManagerAdapter } from "@lib/managers/adapters";
import type {
IStorageEventTypeGuardAdapter,
IStorageEventPersistenceAdapter,
IStorageEventWatchAdapter,
IStorageEventStatusAdapter,
IStorageEventConverterAdapter,
IStorageEventWatchHandlers,
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
import type ObsidianLiveSyncPlugin from "@/main";
import type { LiveSyncCore } from "@/main";
import type { FileProcessingService } from "@lib/services/base/FileProcessingService";
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
/**
* Obsidian-specific type guard adapter
*/
class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter<TFile, TFolder> {
isFile(file: any): file is TFile {
if (file instanceof TFile) {
return true;
}
if (file && typeof file === "object" && "isFolder" in file) {
return !file.isFolder;
}
return false;
}
isFolder(item: any): item is TFolder {
if (item instanceof TFolder) {
return true;
}
if (item && typeof item === "object" && "isFolder" in item) {
return !!item.isFolder;
}
return false;
}
}
/**
* Obsidian-specific persistence adapter
*/
class ObsidianPersistenceAdapter implements IStorageEventPersistenceAdapter {
constructor(private core: LiveSyncCore) {}
async saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise<void> {
await this.core.kvDB.set("storage-event-manager-snapshot", snapshot);
}
async loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null> {
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
"storage-event-manager-snapshot"
);
return snapShot;
}
}
/**
* Obsidian-specific status adapter
*/
class ObsidianStatusAdapter implements IStorageEventStatusAdapter {
constructor(private fileProcessing: FileProcessingService) {}
updateStatus(status: { batched: number; processing: number; totalQueued: number }): void {
this.fileProcessing.batched.value = status.batched;
this.fileProcessing.processing.value = status.processing;
this.fileProcessing.totalQueued.value = status.totalQueued;
}
}
/**
* Obsidian-specific converter adapter
*/
class ObsidianConverterAdapter implements IStorageEventConverterAdapter<TFile> {
toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub {
return TFileToUXFileInfoStub(file, deleted);
}
toInternalFileInfo(path: FilePath): UXInternalFileInfoStub {
return InternalFileToUXFileInfoStub(path);
}
}
/**
* Obsidian-specific watch adapter
*/
class ObsidianWatchAdapter implements IStorageEventWatchAdapter {
constructor(private plugin: ObsidianLiveSyncPlugin) {}
beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
const plugin = this.plugin;
const boundHandlers = {
onCreate: handlers.onCreate.bind(handlers),
onChange: handlers.onChange.bind(handlers),
onDelete: handlers.onDelete.bind(handlers),
onRename: handlers.onRename.bind(handlers),
onRaw: handlers.onRaw.bind(handlers),
onEditorChange: handlers.onEditorChange?.bind(handlers),
};
plugin.registerEvent(plugin.app.vault.on("create", boundHandlers.onCreate));
plugin.registerEvent(plugin.app.vault.on("modify", boundHandlers.onChange));
plugin.registerEvent(plugin.app.vault.on("delete", boundHandlers.onDelete));
plugin.registerEvent(plugin.app.vault.on("rename", boundHandlers.onRename));
//@ts-ignore : Internal API
plugin.registerEvent(plugin.app.vault.on("raw", boundHandlers.onRaw));
if (boundHandlers.onEditorChange) {
plugin.registerEvent(plugin.app.workspace.on("editor-change", boundHandlers.onEditorChange));
}
return Promise.resolve();
}
}
/**
* Composite adapter for Obsidian StorageEventManager
*/
export class ObsidianStorageEventManagerAdapter implements IStorageEventManagerAdapter<TFile, TFolder> {
readonly typeGuard: ObsidianTypeGuardAdapter;
readonly persistence: ObsidianPersistenceAdapter;
readonly watch: ObsidianWatchAdapter;
readonly status: ObsidianStatusAdapter;
readonly converter: ObsidianConverterAdapter;
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, fileProcessing: FileProcessingService) {
this.typeGuard = new ObsidianTypeGuardAdapter();
this.persistence = new ObsidianPersistenceAdapter(core);
this.watch = new ObsidianWatchAdapter(plugin);
this.status = new ObsidianStatusAdapter(fileProcessing);
this.converter = new ObsidianConverterAdapter();
}
}

View File

@@ -1,210 +1,44 @@
import type { FileEventItem } from "@/common/types";
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync";
import type { FilePath, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "@lib/common/types";
import type { FileEvent } from "@lib/interfaces/StorageEventManager";
import { TFile, type TAbstractFile, TFolder } from "@/deps";
import { LOG_LEVEL_DEBUG } from "octagonal-wheels/common/logger";
import type { FilePath } from "@lib/common/types";
import type ObsidianLiveSyncPlugin from "@/main";
import type { LiveSyncCore } from "@/main";
import {
StorageEventManagerBase,
type FileEventItemSentinel,
type StorageEventManagerBaseDependencies,
} from "@lib/managers/StorageEventManager";
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } from "@lib/managers/StorageEventManager";
import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter";
export class StorageEventManagerObsidian extends StorageEventManagerBase {
plugin: ObsidianLiveSyncPlugin;
export class StorageEventManagerObsidian extends StorageEventManagerBase<ObsidianStorageEventManagerAdapter> {
core: LiveSyncCore;
// Necessary evil.
cmdHiddenFileSync: HiddenFileSync;
override isFile(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFile): boolean {
if (file instanceof TFile) {
return true;
}
if (super.isFile(file)) {
return true;
}
return !file.isFolder;
}
override isFolder(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFolder): boolean {
if (file instanceof TFolder) {
return true;
}
if (super.isFolder(file)) {
return true;
}
return !!file.isFolder;
}
// cmdHiddenFileSync: HiddenFileSync;
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) {
super(dependencies);
this.plugin = plugin;
const adapter = new ObsidianStorageEventManagerAdapter(plugin, core, dependencies.fileProcessing);
super(adapter, dependencies);
this.core = core;
this.cmdHiddenFileSync = this.plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync;
}
async beginWatch() {
await this.snapShotRestored;
const plugin = this.plugin;
this.watchVaultChange = this.watchVaultChange.bind(this);
this.watchVaultCreate = this.watchVaultCreate.bind(this);
this.watchVaultDelete = this.watchVaultDelete.bind(this);
this.watchVaultRename = this.watchVaultRename.bind(this);
this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this);
this.watchEditorChange = this.watchEditorChange.bind(this);
plugin.registerEvent(plugin.app.vault.on("modify", this.watchVaultChange));
plugin.registerEvent(plugin.app.vault.on("delete", this.watchVaultDelete));
plugin.registerEvent(plugin.app.vault.on("rename", this.watchVaultRename));
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
//@ts-ignore : Internal API
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
plugin.registerEvent(plugin.app.workspace.on("editor-change", this.watchEditorChange));
}
watchEditorChange(editor: any, info: any) {
if (!("path" in info)) {
return;
}
if (!this.shouldBatchSave) {
return;
}
const file = info?.file as TFile;
if (!file) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
if (!this.isWaiting(file.path as FilePath)) {
return;
}
const data = info?.data as string;
const fi: FileEvent = {
type: "CHANGED",
file: TFileToUXFileInfoStub(file),
cachedData: data,
};
void this.appendQueue([fi]);
}
watchVaultCreate(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
}
watchVaultChange(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
}
watchVaultDelete(file: TAbstractFile, ctx?: any) {
if (file instanceof TFolder) return;
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
// this._log(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
return;
}
const fileInfo = TFileToUXFileInfoStub(file, true);
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
}
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
// vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename').
if (file instanceof TFile) {
const fileInfo = TFileToUXFileInfoStub(file);
void this.appendQueue(
[
{
type: "DELETE",
file: {
path: oldFile as FilePath,
name: file.name,
stat: {
mtime: file.stat.mtime,
ctime: file.stat.ctime,
size: file.stat.size,
type: "file",
},
deleted: true,
},
skipBatchWait: true,
},
{ type: "CREATE", file: fileInfo, skipBatchWait: true },
],
ctx
);
}
}
// Watch raw events (Internal API)
watchVaultRawEvents(path: FilePath) {
if (this.storageAccess.isFileProcessing(path)) {
// this._log(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE);
return;
}
// Only for internal files.
if (!this.settings) return;
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
if (this.settings.useIgnoreFiles) {
// If it is one of ignore files, refresh the cached one.
// (Calling$$isTargetFile will refresh the cache)
void this.vaultService.isTargetFile(path).then(() => this._watchVaultRawEvents(path));
} else {
void this._watchVaultRawEvents(path);
}
}
async _watchVaultRawEvents(path: FilePath) {
/**
* Override _watchVaultRawEvents to add Obsidian-specific logic
*/
protected override async _watchVaultRawEvents(path: FilePath) {
if (!this.settings.syncInternalFiles && !this.settings.usePluginSync) return;
if (!this.settings.watchInternalFileChanges) return;
if (!path.startsWith(this.plugin.app.vault.configDir)) return;
if (!path.startsWith(this.core.services.API.getSystemConfigDir())) return;
if (path.endsWith("/")) {
// Folder
return;
}
const isTargetFile = await this.cmdHiddenFileSync.isTargetFile(path);
const isTargetFile = await this.vaultService.isTargetFileInExtra(path);
if (!isTargetFile) return;
void this.appendQueue(
[
{
type: "INTERNAL",
file: InternalFileToUXFileInfoStub(path),
file: this.adapter.converter.toInternalFileInfo(path),
skipBatchWait: true, // Internal files should be processed immediately.
},
],
null
);
}
async _saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]) {
await this.core.kvDB.set("storage-event-manager-snapshot", snapshot);
this._log(`Storage operation snapshot saved: ${snapshot.length} items`, LOG_LEVEL_DEBUG);
}
async _loadSnapshot() {
const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>(
"storage-event-manager-snapshot"
);
return snapShot;
}
updateStatus() {
const allFileEventItems = this.bufferedQueuedItems.filter((e): e is FileEventItem => "args" in e);
const allItems = allFileEventItems.filter((e) => !e.cancelled);
const totalItems = allItems.length + this.concurrentProcessing.waiting;
const processing = this.processingCount;
const batchedCount = this._waitingMap.size;
this.fileProcessing.batched.value = batchedCount;
this.fileProcessing.processing.value = processing;
this.fileProcessing.totalQueued.value = totalItems + batchedCount + processing;
}
}

View File

@@ -1,10 +1,16 @@
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types";
import type { LiveSyncCore } from "@/main";
import type { IMinimumLiveSyncCommands, LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
import type { ServiceContext } from "@/lib/src/services/base/ServiceBase";
export abstract class AbstractModule {
export abstract class AbstractModule<
T extends LiveSyncBaseCore<ServiceContext, IMinimumLiveSyncCommands> = LiveSyncBaseCore<
ServiceContext,
IMinimumLiveSyncCommands
>,
> {
_log = createInstanceLogFunction(this.constructor.name, this.services.API);
get services() {
if (!this.core._services) {
@@ -36,13 +42,13 @@ export abstract class AbstractModule {
return stripAllPrefixes(this.services.path.getPath(entry));
}
onBindFunction(core: LiveSyncCore, services: typeof core.services) {
onBindFunction(core: T, services: typeof core.services) {
// Override if needed.
}
constructor(public core: LiveSyncCore) {
constructor(public core: T) {
Logger(`[${this.constructor.name}] Loaded`, LOG_LEVEL_VERBOSE);
}
saveSettings = this.core.saveSettings.bind(this.core);
saveSettings = this.core.services.setting.saveSettingData.bind(this.core.services.setting);
addTestResult(key: string, value: boolean, summary?: string, message?: string) {
this.services.test.addTestResult(`${this.constructor.name}`, key, value, summary, message);

View File

@@ -1,4 +1,4 @@
import { PeriodicProcessor } from "../../common/utils";
import { PeriodicProcessor } from "@/common/PeriodicProcessor";
import type { LiveSyncCore } from "../../main";
import { AbstractModule } from "../AbstractModule";

View File

@@ -6,7 +6,8 @@ import { balanceChunkPurgedDBs } from "@lib/pouchdb/chunks";
import { purgeUnreferencedChunks } from "@lib/pouchdb/chunks";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
import { scheduleTask } from "../../common/utils";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import { $msg } from "../../lib/src/common/i18n";

View File

@@ -8,8 +8,7 @@ import {
type MetaEntry,
} from "@lib/common/types";
import type { ModuleReplicator } from "./ModuleReplicator";
import { isChunk, isValidPath } from "@/common/utils";
import type { LiveSyncCore } from "@/main";
import { isChunk } from "@/lib/src/common/typeUtils";
import {
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,
@@ -22,6 +21,7 @@ import { fireAndForget, isAnyNote, throttle } from "@lib/common/utils";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore_v2";
import { serialized } from "octagonal-wheels/concurrency/lock";
import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot";
type ReplicateResultProcessorState = {
@@ -54,7 +54,7 @@ export class ReplicateResultProcessor {
get services() {
return this.replicator.core.services;
}
get core(): LiveSyncCore {
get core(): LiveSyncBaseCore {
return this.replicator.core;
}
@@ -414,7 +414,7 @@ export class ReplicateResultProcessor {
if (await this.services.replication.processOptionalSynchroniseResult(dbDoc)) {
// Already processed
this.log(`Processed by other processor: ${docNote}`, LOG_LEVEL_DEBUG);
} else if (isValidPath(this.getPath(doc))) {
} else if (this.services.vault.isValidPath(this.getPath(doc))) {
// Apply to storage if the path is valid
await this.applyToStorage(doc as MetaEntry);
this.log(`Processed: ${docNote}`, LOG_LEVEL_DEBUG);

View File

@@ -11,13 +11,9 @@ import {
type diff_check_result,
type FilePathWithPrefix,
} from "../../lib/src/common/types";
import {
compareMTime,
displayRev,
isCustomisationSyncMetadata,
isPluginMetadata,
TARGET_IS_NEW,
} from "../../common/utils";
import { isCustomisationSyncMetadata, isPluginMetadata } from "@lib/common/typeUtils.ts";
import { TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
import { compareMTime, displayRev } from "@lib/common/utils.ts";
import diff_match_patch from "diff-match-patch";
import { stripAllPrefixes, isPlainText } from "../../lib/src/string_and_binary/path";
import { eventHub } from "../../common/events.ts";
@@ -214,7 +210,7 @@ export class ModuleConflictResolver extends AbstractModule {
private async _resolveAllConflictedFilesByNewerOnes() {
this._log(`Resolving conflicts by newer ones`, LOG_LEVEL_NOTICE);
const files = this.core.storageAccess.getFileNames();
const files = await this.core.storageAccess.getFileNames();
let i = 0;
for (const file of files) {

View File

@@ -0,0 +1,86 @@
import type { LiveSyncCore } from "@/main";
import { LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import { fireAndForget } from "octagonal-wheels/promises";
import { AbstractModule } from "../AbstractModule";
// Separated Module for basic menu commands, which are not related to obsidian specific features. It is expected to be used in other platforms with minimal changes.
// However, it is odd that it has here at all; it really ought to be in each respective feature. It will likely be moved eventually. Until now, addCommand pointed to Obsidian's version.
export class ModuleBasicMenu extends AbstractModule {
_everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
callback: async () => {
await this.services.replication.replicate();
},
});
this.addCommand({
id: "livesync-dump",
name: "Dump information of this doc ",
callback: () => {
const file = this.services.vault.getActiveFilePath();
if (!file) return;
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
},
});
this.addCommand({
id: "livesync-toggle",
name: "Toggle LiveSync",
callback: async () => {
if (this.settings.liveSync) {
this.settings.liveSync = false;
this._log("LiveSync Disabled.", LOG_LEVEL_NOTICE);
} else {
this.settings.liveSync = true;
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
}
await this.services.control.applySettings();
await this.services.setting.saveSettingData();
},
});
this.addCommand({
id: "livesync-suspendall",
name: "Toggle All Sync.",
callback: async () => {
if (this.services.appLifecycle.isSuspended()) {
this.services.appLifecycle.setSuspended(false);
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
} else {
this.services.appLifecycle.setSuspended(true);
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
}
await this.services.control.applySettings();
await this.services.setting.saveSettingData();
},
});
this.addCommand({
id: "livesync-scan-files",
name: "Scan storage and database again",
callback: async () => {
await this.services.vault.scanVault(true);
},
});
this.addCommand({
id: "livesync-runbatch",
name: "Run pended batch processes",
callback: async () => {
await this.services.fileProcessing.commitPendingFileEvents();
},
});
// TODO, Replicator is possibly one of features. It should be moved to features.
this.addCommand({
id: "livesync-abortsync",
name: "Abort synchronization immediately",
callback: () => {
this.core.replicator.terminateSync();
},
});
return Promise.resolve(true);
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -1,7 +1,7 @@
import { unique } from "octagonal-wheels/collection";
import { throttle } from "octagonal-wheels/function";
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts";
import { BASE_IS_NEW, compareFileFreshness, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
import { BASE_IS_NEW, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
import {
type FilePathWithPrefixLC,
type FilePathWithPrefix,
@@ -73,7 +73,7 @@ export class ModuleInitializerFile extends AbstractModule {
await this.collectDeletedFiles();
this._log("Collecting local files on the storage", LOG_LEVEL_VERBOSE);
const filesStorageSrc = this.core.storageAccess.getFiles();
const filesStorageSrc = await this.core.storageAccess.getFiles();
const _filesStorage = [] as typeof filesStorageSrc;
@@ -300,7 +300,7 @@ export class ModuleInitializerFile extends AbstractModule {
throw new Error(`Missing doc:${(file as any).path}`);
}
if ("path" in file) {
const w = this.core.storageAccess.getFileStub((file as any).path);
const w = await this.core.storageAccess.getFileStub((file as any).path);
if (w) {
file = w;
} else {
@@ -308,7 +308,7 @@ export class ModuleInitializerFile extends AbstractModule {
}
}
const compareResult = compareFileFreshness(file, doc);
const compareResult = this.services.path.compareFileFreshness(file, doc);
switch (compareResult) {
case BASE_IS_NEW:
if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) {
@@ -423,7 +423,7 @@ export class ModuleInitializerFile extends AbstractModule {
}
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportDetectedErrors.bind(this));
services.databaseEvents.initialiseDatabase.setHandler(this._initializeDatabase.bind(this));
services.vault.scanVault.setHandler(this._performFullScan.bind(this));
services.databaseEvents.initialiseDatabase.addHandler(this._initializeDatabase.bind(this));
services.vault.scanVault.addHandler(this._performFullScan.bind(this));
}
}

View File

@@ -8,7 +8,7 @@ import {
eventHub,
} from "../../common/events.ts";
import { AbstractModule } from "../AbstractModule.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
import { $msg } from "@lib/common/i18n.ts";
import { performDoctorConsultation, RebuildOptions } from "../../lib/src/common/configForDoc.ts";
import { isValidPath } from "../../common/utils.ts";
import { isMetaEntry } from "../../lib/src/common/types.ts";
@@ -40,7 +40,7 @@ export class ModuleMigration extends AbstractModule {
);
if (isModified) {
this.settings = settings;
await this.core.saveSettings();
await this.saveSettings();
}
if (!skipRebuild) {
if (shouldRebuild) {
@@ -231,7 +231,7 @@ export class ModuleMigration extends AbstractModule {
if (ret == FIX) {
for (const file of recoverable) {
// Overwrite the database with the files on the storage
const stubFile = this.core.storageAccess.getFileStub(file.path);
const stubFile = await this.core.storageAccess.getFileStub(file.path);
if (stubFile == null) {
Logger(`Could not find stub file for ${file.path}`, LOG_LEVEL_NOTICE);
continue;

View File

@@ -35,13 +35,13 @@ export class ModuleCheckRemoteSize extends AbstractModule {
);
if (ret == ANSWER_0) {
this.settings.notifyThresholdOfRemoteStorageSize = 0;
await this.core.saveSettings();
await this.saveSettings();
} else if (ret == ANSWER_800) {
this.settings.notifyThresholdOfRemoteStorageSize = 800;
await this.core.saveSettings();
await this.saveSettings();
} else if (ret == ANSWER_2000) {
this.settings.notifyThresholdOfRemoteStorageSize = 2000;
await this.core.saveSettings();
await this.saveSettings();
}
}
if (this.settings.notifyThresholdOfRemoteStorageSize > 0) {
@@ -88,7 +88,8 @@ export class ModuleCheckRemoteSize extends AbstractModule {
}),
LOG_LEVEL_NOTICE
);
await this.core.saveSettings();
// await this.core.saveSettings();
await this.core.services.setting.saveSettingData();
} else {
// Dismiss or Close the dialog
}

View File

@@ -1,10 +1,10 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { addIcon, type Editor, type MarkdownFileInfo, type MarkdownView } from "../../deps.ts";
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "../../lib/src/common/types.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
import type { LiveSyncCore } from "../../main.ts";
import { type Editor, type MarkdownFileInfo, type MarkdownView } from "@/deps.ts";
import { addIcon } from "@/deps.ts";
import { type FilePathWithPrefix } from "@lib/common/types.ts";
import { $msg } from "@lib/common/i18n.ts";
import type { LiveSyncCore } from "@/main.ts";
import { AbstractModule } from "../AbstractModule.ts";
// Obsidian specific menu commands.
export class ModuleObsidianMenu extends AbstractModule {
_everyOnloadStart(): Promise<boolean> {
// UI
@@ -22,22 +22,6 @@ export class ModuleObsidianMenu extends AbstractModule {
await this.services.replication.replicate(true);
}).addClass("livesync-ribbon-replicate");
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
callback: async () => {
await this.services.replication.replicate();
},
});
this.addCommand({
id: "livesync-dump",
name: "Dump information of this doc ",
callback: () => {
const file = this.services.vault.getActiveFilePath();
if (!file) return;
fireAndForget(() => this.localDatabase.getDBEntry(file, {}, true, false));
},
});
this.addCommand({
id: "livesync-checkdoc-conflicted",
name: "Resolve if conflicted.",
@@ -48,61 +32,6 @@ export class ModuleObsidianMenu extends AbstractModule {
},
});
this.addCommand({
id: "livesync-toggle",
name: "Toggle LiveSync",
callback: async () => {
if (this.settings.liveSync) {
this.settings.liveSync = false;
this._log("LiveSync Disabled.", LOG_LEVEL_NOTICE);
} else {
this.settings.liveSync = true;
this._log("LiveSync Enabled.", LOG_LEVEL_NOTICE);
}
await this.services.control.applySettings();
await this.services.setting.saveSettingData();
},
});
this.addCommand({
id: "livesync-suspendall",
name: "Toggle All Sync.",
callback: async () => {
if (this.services.appLifecycle.isSuspended()) {
this.services.appLifecycle.setSuspended(false);
this._log("Self-hosted LiveSync resumed", LOG_LEVEL_NOTICE);
} else {
this.services.appLifecycle.setSuspended(true);
this._log("Self-hosted LiveSync suspended", LOG_LEVEL_NOTICE);
}
await this.services.control.applySettings();
await this.services.setting.saveSettingData();
},
});
this.addCommand({
id: "livesync-scan-files",
name: "Scan storage and database again",
callback: async () => {
await this.services.vault.scanVault(true);
},
});
this.addCommand({
id: "livesync-runbatch",
name: "Run pended batch processes",
callback: async () => {
await this.services.fileProcessing.commitPendingFileEvents();
},
});
// TODO, Replicator is possibly one of features. It should be moved to features.
this.addCommand({
id: "livesync-abortsync",
name: "Abort synchronization immediately",
callback: () => {
this.core.replicator.terminateSync();
},
});
return Promise.resolve(true);
}

View File

@@ -1,3 +1,4 @@
// I intend to discontinue maintenance of this class. It seems preferable to test it externally.
import { delay } from "octagonal-wheels/promises";
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
@@ -169,7 +170,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule {
this._log("No storage access", LOG_LEVEL_INFO);
return;
}
const files = this.core.storageAccess.getFiles();
const files = await this.core.storageAccess.getFiles();
const out = [] as any[];
const webcrypto = await getWebCrypto();
for (const file of files) {
@@ -205,8 +206,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule {
}
async __dumpFileListIncludeHidden(outFile?: string) {
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
const out = [] as any[];
const files = await this.core.storageAccess.getFilesIncludeHidden("", targetPatterns, ignorePatterns);
// console.dir(files);

View File

@@ -9,6 +9,7 @@
import { writable } from "svelte/store";
export let plugin: ObsidianLiveSyncPlugin;
export let moduleDev: ModuleDev;
$: core = plugin.core;
let performanceTestResult = "";
let functionCheckResult = "";
let testRunning = false;
@@ -42,7 +43,7 @@
// performTest();
eventHub.onceEvent(EVENT_LAYOUT_READY, async () => {
if (await plugin.storageAccess.isExistsIncludeHidden("_AUTO_TEST.md")) {
if (await core.storageAccess.isExistsIncludeHidden("_AUTO_TEST.md")) {
new Notice("Auto test file found, running tests...");
fireAndForget(async () => {
await allTest();
@@ -57,14 +58,14 @@
function moduleMultiDeviceTest() {
if (moduleTesting) return;
moduleTesting = true;
plugin.services.test.testMultiDevice().finally(() => {
core.services.test.testMultiDevice().finally(() => {
moduleTesting = false;
});
}
function moduleSingleDeviceTest() {
if (moduleTesting) return;
moduleTesting = true;
plugin.services.test.test().finally(() => {
core.services.test.test().finally(() => {
moduleTesting = false;
});
}
@@ -72,8 +73,8 @@
if (moduleTesting) return;
moduleTesting = true;
try {
await plugin.services.test.test();
await plugin.services.test.testMultiDevice();
await core.services.test.test();
await core.services.test.testMultiDevice();
} finally {
moduleTesting = false;
}

View File

@@ -38,8 +38,8 @@ export function addDebugFileLog(message: any, stackLog = false) {
// const out = "--" + timestamp + "--\n" + messageContent + " " + (stack || "");
// const out
try {
await plugin.storageAccess.appendHiddenFile(
plugin.app.vault.configDir + "/ls-debug/" + outFile,
await plugin.core.storageAccess.appendHiddenFile(
plugin.core.services.API.getSystemConfigDir() + "/ls-debug/" + outFile,
JSON.stringify(out) + "\n"
);
} catch {

View File

@@ -48,7 +48,7 @@ async function formatPerfResults(items: NamedMeasureResult[]) {
}
export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
clearResult("trench");
const trench = new Trench(plugin.simpleStore);
const trench = new Trench(plugin.core.simpleStore);
const result = [] as NamedMeasureResult[];
result.push(
await measure("trench-short-string", async () => {
@@ -57,7 +57,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
})
);
{
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/10kb.png");
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/10kb.png");
const uint8Array = new Uint8Array(testBinary);
result.push(
await measure("trench-binary-10kb", async () => {
@@ -67,7 +67,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
);
}
{
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg");
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/100kb.jpeg");
const uint8Array = new Uint8Array(testBinary);
result.push(
await measure("trench-binary-100kb", async () => {
@@ -77,7 +77,7 @@ export async function perf_trench(plugin: ObsidianLiveSyncPlugin) {
);
}
{
const testBinary = await plugin.storageAccess.readHiddenFileBinary("testdata/1mb.png");
const testBinary = await plugin.core.storageAccess.readHiddenFileBinary("testdata/1mb.png");
const uint8Array = new Uint8Array(testBinary);
result.push(
await measure("trench-binary-1mb", async () => {

View File

@@ -15,6 +15,7 @@ import { isErrorOfMissingDoc } from "../../../lib/src/pouchdb/utils_couchdb.ts";
import { fireAndForget, getDocData, readContent } from "../../../lib/src/common/utils.ts";
import { isPlainText, stripPrefix } from "../../../lib/src/string_and_binary/path.ts";
import { scheduleOnceIfDuplicated } from "octagonal-wheels/concurrency/lock";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
function isImage(path: string) {
const ext = path.split(".").splice(-1)[0].toLowerCase();
@@ -46,8 +47,9 @@ function readDocument(w: LoadedEntry) {
}
export class DocumentHistoryModal extends Modal {
plugin: ObsidianLiveSyncPlugin;
core: LiveSyncBaseCore;
get services() {
return this.plugin.services;
return this.core.services;
}
range!: HTMLInputElement;
contentView!: HTMLDivElement;
@@ -66,6 +68,7 @@ export class DocumentHistoryModal extends Modal {
constructor(
app: App,
core: LiveSyncBaseCore,
plugin: ObsidianLiveSyncPlugin,
file: TFile | FilePathWithPrefix,
id?: DocumentID,
@@ -73,6 +76,7 @@ export class DocumentHistoryModal extends Modal {
) {
super(app);
this.plugin = plugin;
this.core = core;
this.file = file instanceof TFile ? getPathFromTFile(file) : file;
this.id = id;
this.initialRev = revision;
@@ -88,7 +92,7 @@ export class DocumentHistoryModal extends Modal {
if (!this.id) {
this.id = await this.services.path.path2id(this.file);
}
const db = this.plugin.localDatabase;
const db = this.core.localDatabase;
try {
const w = await db.getRaw(this.id, { revs_info: true });
this.revs_info = w._revs_info?.filter((e) => e?.status == "available") ?? [];
@@ -137,7 +141,7 @@ export class DocumentHistoryModal extends Modal {
}
async showExactRev(rev: string) {
const db = this.plugin.localDatabase;
const db = this.core.localDatabase;
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
this.currentText = "";
this.currentDeleted = false;
@@ -292,7 +296,7 @@ export class DocumentHistoryModal extends Modal {
return;
}
const d = readContent(this.currentDoc);
await this.plugin.storageAccess.writeHiddenFileAuto(pathToWrite, d);
await this.core.storageAccess.writeHiddenFileAuto(pathToWrite, d);
await focusFile(pathToWrite);
this.close();
});

View File

@@ -6,7 +6,9 @@
import { diff_match_patch } from "../../../deps.ts";
import { DocumentHistoryModal } from "../DocumentHistory/DocumentHistoryModal.ts";
import { isPlainText, stripAllPrefixes } from "../../../lib/src/string_and_binary/path.ts";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore.ts";
export let plugin: ObsidianLiveSyncPlugin;
export let core: LiveSyncBaseCore;
let showDiffInfo = false;
let showChunkCorrected = false;
@@ -44,12 +46,12 @@
let history = [] as HistoryData[];
let loading = false;
function getPath(entry: AnyEntry): FilePathWithPrefix {
return plugin.services.path.getPath(entry);
return core.services.path.getPath(entry);
}
async function fetchChanges(): Promise<HistoryData[]> {
try {
const db = plugin.localDatabase;
const db = core.localDatabase;
let result = [] as typeof history;
for await (const docA of db.findAllNormalDocs()) {
if (docA.mtime < range_from_epoch) {
@@ -112,11 +114,11 @@
}
if (rev == docA._rev) {
if (checkStorageDiff) {
const isExist = await plugin.storageAccess.isExistsIncludeHidden(
const isExist = await core.storageAccess.isExistsIncludeHidden(
stripAllPrefixes(getPath(docA))
);
if (isExist) {
const data = await plugin.storageAccess.readHiddenFileBinary(
const data = await core.storageAccess.readHiddenFileBinary(
stripAllPrefixes(getPath(docA))
);
const d = readAsBlob(doc);
@@ -189,7 +191,7 @@
onDestroy(() => {});
function showHistory(file: string, rev: string) {
new DocumentHistoryModal(plugin.app, plugin, file as unknown as FilePathWithPrefix, undefined, rev).open();
new DocumentHistoryModal(plugin.app, plugin.core, plugin, file as unknown as FilePathWithPrefix, undefined, rev).open();
}
function openFile(file: string) {
plugin.app.workspace.openLinkText(file, file);
@@ -250,7 +252,11 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-missing-attribute -->
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
{#if entry.isDeleted}
<span class="filename" style="text-decoration: line-through">{entry.filename}</span>
{:else}
<span class="filename"><a on:click={() => openFile(entry.path)}>{entry.filename}</a></span>
{/if}
</div>
</td>
<td>

View File

@@ -11,6 +11,7 @@ export class GlobalHistoryView extends SvelteItemView {
target: target,
props: {
plugin: this.plugin,
core: this.plugin.core,
},
});
}

View File

@@ -63,6 +63,7 @@ export class ConflictResolveModal extends Modal {
contentEl.createEl("span", { text: this.filename });
const div = contentEl.createDiv("");
div.addClass("op-scrollable");
div.addClass("ls-dialog");
let diff = "";
for (const v of this.result.diff) {
const x1 = v[0];
@@ -86,6 +87,7 @@ export class ConflictResolveModal extends Modal {
}
const div2 = contentEl.createDiv("");
div2.addClass("ls-dialog");
const date1 =
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
const date2 =

View File

@@ -254,8 +254,7 @@ export class ModuleLog extends AbstractObsidianModule {
}
// Case Sensitivity
if (this.services.vault.shouldCheckCaseInsensitively()) {
const f = this.core.storageAccess
.getFiles()
const f = (await this.core.storageAccess.getFiles())
.map((e) => e.path)
.filter((e) => e.toLowerCase() == thisFile.path.toLowerCase());
if (f.length > 1) {
@@ -405,8 +404,8 @@ export class ModuleLog extends AbstractObsidianModule {
this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" });
eventHub.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
if (this.settings?.showStatusOnStatusbar) {
this.statusBar = this.core.addStatusBarItem();
this.statusBar.addClass("syncstatusbar");
this.statusBar = this.services.API.addStatusBarItem();
this.statusBar?.addClass("syncstatusbar");
}
this.adjustStatusDivPosition();
return Promise.resolve(true);

View File

@@ -34,7 +34,7 @@ export class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
}
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
new DocumentHistoryModal(this.app, this.plugin, file, id).open();
new DocumentHistoryModal(this.app, this.core, this.plugin, file, id).open();
}
async fileHistory() {

View File

@@ -2,6 +2,7 @@ import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSe
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "../../common/events.ts";
import type { LiveSyncCore } from "@/main.ts";
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
settingTab!: ObsidianLiveSyncSettingTab;
@@ -29,7 +30,7 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
get appId() {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
override onBindFunction(core: typeof this.plugin, services: typeof core.services): void {
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -1,4 +1,4 @@
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE } from "../../lib/src/common/types.ts";
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types.ts";
import { configURIBase } from "../../common/types.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
import { fireAndForget } from "../../lib/src/common/utils.ts";
@@ -25,16 +25,24 @@ export class ModuleSetupObsidian extends AbstractModule {
private _setupManager!: SetupManager;
private _everyOnload(): Promise<boolean> {
this._setupManager = this.core.getModule(SetupManager);
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
if (conf.settings) {
await this._setupManager.onUseSetupURI(
UserMode.Unknown,
`${configURIBase}${encodeURIComponent(conf.settings)}`
);
} else if (conf.settingsQR) {
await this._setupManager.decodeQR(conf.settingsQR);
}
});
try {
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
if (conf.settings) {
await this._setupManager.onUseSetupURI(
UserMode.Unknown,
`${configURIBase}${encodeURIComponent(conf.settings)}`
);
} else if (conf.settingsQR) {
await this._setupManager.decodeQR(conf.settingsQR);
}
});
} catch (e) {
this._log(
"Failed to register protocol handler. This feature may not work in some environments.",
LOG_LEVEL_NOTICE
);
this._log(e, LOG_LEVEL_VERBOSE);
}
this.addCommand({
id: "livesync-setting-qr",
name: "Show settings as a QR code",

View File

@@ -86,8 +86,11 @@ export function createStub(name: string, key: string, value: string, panel: stri
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
plugin: ObsidianLiveSyncPlugin;
get core() {
return this.plugin.core;
}
get services() {
return this.plugin.services;
return this.core.services;
}
selectedScreen = "";
@@ -122,9 +125,9 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
continue;
}
//@ts-ignore
this.plugin.settings[k] = this.editingSettings[k];
this.core.settings[k] = this.editingSettings[k];
//@ts-ignore
this.initialSettings[k] = this.plugin.settings[k];
this.initialSettings[k] = this.core.settings[k];
}
keys.forEach((e) => this.refreshSetting(e));
}
@@ -164,14 +167,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
continue;
}
//@ts-ignore
this.plugin.settings[k] = this.editingSettings[k];
this.core.settings[k] = this.editingSettings[k];
//@ts-ignore
this.initialSettings[k] = this.plugin.settings[k];
this.initialSettings[k] = this.core.settings[k];
hasChanged = true;
}
if (hasChanged) {
await this.plugin.saveSettings();
await this.services.setting.saveSettingData();
}
// if (runOnSaved) {
@@ -231,7 +234,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
*/
reloadAllSettings(skipUpdate: boolean = false) {
const localSetting = this.reloadAllLocalSettings();
this._editingSettings = { ...this.plugin.settings, ...localSetting };
this._editingSettings = { ...this.core.settings, ...localSetting };
this._editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() };
this.initialSettings = { ...this.editingSettings };
if (!skipUpdate) this.requestUpdate();
@@ -242,7 +245,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
*/
refreshSetting(key: AllSettingItemKey) {
const localSetting = this.reloadAllLocalSettings();
if (key in this.plugin.settings) {
if (key in this.core.settings) {
if (key in localSetting) {
//@ts-ignore
this.initialSettings[key] = localSetting[key];
@@ -250,7 +253,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.editingSettings[key] = localSetting[key];
} else {
//@ts-ignore
this.initialSettings[key] = this.plugin.settings[key];
this.initialSettings[key] = this.core.settings[key];
//@ts-ignore
this.editingSettings[key] = this.initialSettings[key];
}
@@ -319,7 +322,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
closeSetting() {
// @ts-ignore
this.plugin.app.setting.close();
this.core.app.setting.close();
}
handleElement(element: HTMLElement, func: OnUpdateFunc) {
@@ -381,7 +384,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
requestReload() {
if (this.isShown) {
const newConf = this.plugin.settings;
const newConf = this.core.settings;
const keys = Object.keys(newConf) as (keyof ObsidianLiveSyncSettings)[];
let hasLoaded = false;
for (const k of keys) {
@@ -389,7 +392,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
// Something has changed
if (this.isDirty(k as AllSettingItemKey)) {
// And modified.
this.plugin.confirm.askInPopup(
this.core.confirm.askInPopup(
`config-reloaded-${k}`,
$msg("obsidianLiveSyncSettingTab.msgSettingModified", {
setting: getConfName(k as AllSettingItemKey),
@@ -457,7 +460,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.editingSettings.syncOnStart = false;
this.editingSettings.syncOnFileOpen = false;
this.editingSettings.syncAfterMerge = false;
this.plugin.replicator.closeReplication();
this.core.replicator.closeReplication();
await this.saveAllDirtySettings();
this.containerEl.addClass("isWizard");
this.inWizard = true;
@@ -514,8 +517,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (this.isConfiguredAs("syncOnStart", true)) return true;
if (this.isConfiguredAs("syncAfterMerge", true)) return true;
if (this.isConfiguredAs("syncOnFileOpen", true)) return true;
if (this.plugin?.replicator?.syncStatus == "CONNECTED") return true;
if (this.plugin?.replicator?.syncStatus == "PAUSED") return true;
if (this.core?.replicator?.syncStatus == "CONNECTED") return true;
if (this.core?.replicator?.syncStatus == "PAUSED") return true;
return false;
}
@@ -605,7 +608,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.saveAllDirtySettings();
this.closeSetting();
await delay(2000);
await this.plugin.rebuilder.$performRebuildDB(method);
await this.core.rebuilder.$performRebuildDB(method);
};
async confirmRebuild() {
if (!(await this.isPassphraseValid())) {
@@ -633,7 +636,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
if (result == OPTION_FETCH) {
if (!(await this.checkWorkingPassphrase())) {
if (
(await this.plugin.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
(await this.core.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
defaultOption: "No",
})) != "yes"
)
@@ -646,16 +649,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
await this.saveAllDirtySettings();
await this.applyAllSettings();
if (result == OPTION_FETCH) {
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG3_HR, "");
this.services.appLifecycle.scheduleRestart();
this.closeSetting();
// await rebuildDB("localOnly");
} else if (result == OPTION_REBUILD_BOTH) {
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG2_HR, "");
this.services.appLifecycle.scheduleRestart();
this.closeSetting();
} else if (result == OPTION_ONLY_SETTING) {
await this.plugin.saveSettings();
await this.services.setting.saveSettingData();
}
}
@@ -868,7 +871,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
getMinioJournalSyncClient() {
return new JournalSyncMinio(this.plugin.settings, this.plugin.simpleStore, this.plugin);
return new JournalSyncMinio(this.core.settings, this.core.simpleStore, this.core);
}
async resetRemoteBucket() {
const minioJournal = this.getMinioJournalSyncClient();

View File

@@ -165,7 +165,7 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
}
const obsidianInfo = {
navigator: navigator.userAgent,
fileSystem: this.plugin.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
fileSystem: this.core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
};
const msgConfig = `# ---- Obsidian info ----
${stringifyYaml(obsidianInfo)}
@@ -221,7 +221,7 @@ ${stringifyYaml({
void addPanel(paneEl, "Recovery and Repair").then((paneEl) => {
const addResult = async (path: string, file: FilePathWithPrefix | false, fileOnDB: LoadedEntry | false) => {
const storageFileStat = file ? await this.plugin.storageAccess.statHidden(file) : null;
const storageFileStat = file ? await this.core.storageAccess.statHidden(file) : null;
resultArea.appendChild(
this.createEl(resultArea, "div", {}, (el) => {
el.appendChild(this.createEl(el, "h6", { text: path }));
@@ -256,7 +256,7 @@ ${stringifyYaml({
this.createEl(el, "button", { text: "Storage -> Database" }, (buttonEl) => {
buttonEl.onClickEvent(async () => {
if (file.startsWith(".")) {
const addOn = this.plugin.getAddOn<HiddenFileSync>(HiddenFileSync.name);
const addOn = this.core.getAddOn<HiddenFileSync>(HiddenFileSync.name);
if (addOn) {
const file = (await addOn.scanInternalFiles()).find((e) => e.path == path);
if (!file) {
@@ -275,7 +275,7 @@ ${stringifyYaml({
}
}
} else {
if (!(await this.plugin.fileHandler.storeFileToDB(file as FilePath, true))) {
if (!(await this.core.fileHandler.storeFileToDB(file as FilePath, true))) {
Logger(
`Failed to store the file to the database: ${file}`,
LOG_LEVEL_NOTICE
@@ -293,7 +293,7 @@ ${stringifyYaml({
this.createEl(el, "button", { text: "Database -> Storage" }, (buttonEl) => {
buttonEl.onClickEvent(async () => {
if (fileOnDB.path.startsWith(ICHeader)) {
const addOn = this.plugin.getAddOn<HiddenFileSync>(HiddenFileSync.name);
const addOn = this.core.getAddOn<HiddenFileSync>(HiddenFileSync.name);
if (addOn) {
if (
!(await addOn.extractInternalFileFromDatabase(path as FilePath, true))
@@ -307,7 +307,7 @@ ${stringifyYaml({
}
} else {
if (
!(await this.plugin.fileHandler.dbToStorage(
!(await this.core.fileHandler.dbToStorage(
fileOnDB as MetaEntry,
null,
true
@@ -332,7 +332,7 @@ ${stringifyYaml({
const checkBetweenStorageAndDatabase = async (file: FilePathWithPrefix, fileOnDB: LoadedEntry) => {
const dataContent = readAsBlob(fileOnDB);
const content = createBlob(await this.plugin.storageAccess.readHiddenFileBinary(file));
const content = createBlob(await this.core.storageAccess.readHiddenFileBinary(file));
if (await isDocContentSame(content, dataContent)) {
Logger(`Compare: SAME: ${file}`);
} else {
@@ -348,7 +348,7 @@ ${stringifyYaml({
.setButtonText("Recreate all")
.setCta()
.onClick(async () => {
await this.plugin.fileHandler.createAllChunks(true);
await this.core.fileHandler.createAllChunks(true);
})
);
new Setting(paneEl)
@@ -377,21 +377,21 @@ ${stringifyYaml({
.setCta()
.onClick(async () => {
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
const ignorePatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.plugin.settings, "syncInternalFilesTargetPatterns");
this.plugin.localDatabase.clearCaches();
const ignorePatterns = getFileRegExp(this.core.settings, "syncInternalFilesIgnorePatterns");
const targetPatterns = getFileRegExp(this.core.settings, "syncInternalFilesTargetPatterns");
this.core.localDatabase.clearCaches();
Logger("Start verifying all files", LOG_LEVEL_NOTICE, "verify");
const files = this.plugin.settings.syncInternalFiles
? await this.plugin.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns)
: await this.plugin.storageAccess.getFileNames();
const files = this.core.settings.syncInternalFiles
? await this.core.storageAccess.getFilesIncludeHidden("/", targetPatterns, ignorePatterns)
: await this.core.storageAccess.getFileNames();
const documents = [] as FilePath[];
const adn = this.plugin.localDatabase.findAllDocs();
const adn = this.core.localDatabase.findAllDocs();
for await (const i of adn) {
const path = this.services.path.getPath(i);
if (path.startsWith(ICXHeader)) continue;
if (path.startsWith(PSCHeader)) continue;
if (!this.plugin.settings.syncInternalFiles && path.startsWith(ICHeader)) continue;
if (!this.core.settings.syncInternalFiles && path.startsWith(ICHeader)) continue;
documents.push(stripAllPrefixes(path));
}
const allPaths = [...new Set([...documents, ...files])];
@@ -411,8 +411,8 @@ ${stringifyYaml({
if (shouldBeIgnored(path)) {
return incProc();
}
const stat = (await this.plugin.storageAccess.isExistsIncludeHidden(path))
? await this.plugin.storageAccess.statHidden(path)
const stat = (await this.core.storageAccess.isExistsIncludeHidden(path))
? await this.core.storageAccess.statHidden(path)
: false;
const fileOnStorage = stat != null ? stat : false;
if (!(await this.services.vault.isTargetFile(path))) return incProc();
@@ -422,7 +422,7 @@ ${stringifyYaml({
try {
const isHiddenFile = path.startsWith(".");
const dbPath = isHiddenFile ? addPrefix(path, ICHeader) : path;
const fileOnDB = await this.plugin.localDatabase.getDBEntry(dbPath);
const fileOnDB = await this.core.localDatabase.getDBEntry(dbPath);
if (fileOnDB && this.services.vault.isFileSizeTooLarge(fileOnDB.size))
return incProc();
@@ -466,10 +466,10 @@ ${stringifyYaml({
.setDisabled(false)
.setWarning()
.onClick(async () => {
for await (const docName of this.plugin.localDatabase.findAllDocNames()) {
for await (const docName of this.core.localDatabase.findAllDocNames()) {
if (!docName.startsWith("f:")) {
const idEncoded = await this.services.path.path2id(docName as FilePathWithPrefix);
const doc = await this.plugin.localDatabase.getRaw(docName as DocumentID);
const doc = await this.core.localDatabase.getRaw(docName as DocumentID);
if (!doc) continue;
if (doc.type != "newnote" && doc.type != "plain") {
continue;
@@ -482,7 +482,7 @@ ${stringifyYaml({
// @ts-ignore
delete newDoc._rev;
try {
const obfuscatedDoc = await this.plugin.localDatabase.getRaw(idEncoded, {
const obfuscatedDoc = await this.core.localDatabase.getRaw(idEncoded, {
revs_info: true,
});
// Unfortunately we have to delete one of them.
@@ -499,14 +499,14 @@ ${stringifyYaml({
-32
);
}
const ret = await this.plugin.localDatabase.putRaw(newDoc, { force: true });
const ret = await this.core.localDatabase.putRaw(newDoc, { force: true });
if (ret.ok) {
Logger(
`${docName} has been converted as conflicted document`,
LOG_LEVEL_NOTICE
);
doc._deleted = true;
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
if ((await this.core.localDatabase.putRaw(doc)).ok) {
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
}
await this.services.conflict.queueCheckForIfOpen(docName as FilePathWithPrefix);
@@ -517,10 +517,10 @@ ${stringifyYaml({
} catch (ex: any) {
if (ex?.status == 404) {
// We can perform this safely
if ((await this.plugin.localDatabase.putRaw(newDoc)).ok) {
if ((await this.core.localDatabase.putRaw(newDoc)).ok) {
Logger(`${docName} has been converted`, LOG_LEVEL_NOTICE);
doc._deleted = true;
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
if ((await this.core.localDatabase.putRaw(doc)).ok) {
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
}
}
@@ -555,7 +555,7 @@ ${stringifyYaml({
.setWarning()
.onClick(async () => {
Logger(`Deleting customization sync data`, LOG_LEVEL_NOTICE);
const entriesToDelete = await this.plugin.localDatabase.allDocsRaw({
const entriesToDelete = await this.core.localDatabase.allDocsRaw({
startkey: "ix:",
endkey: "ix:\u{10ffff}",
include_docs: true,
@@ -564,7 +564,7 @@ ${stringifyYaml({
...e.doc,
_deleted: true,
}));
const r = await this.plugin.localDatabase.bulkDocsRaw(newData as any[]);
const r = await this.core.localDatabase.bulkDocsRaw(newData as any[]);
// Do not care about the result.
Logger(
`${r.length} items have been removed, to confirm how many items are left, please perform it again.`,

View File

@@ -11,8 +11,8 @@ export function paneMaintenance(
paneEl: HTMLElement,
{ addPanel }: PageFunctions
): void {
const isRemoteLockedAndDeviceNotAccepted = () => this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted;
const isRemoteLocked = () => this.plugin?.replicator?.remoteLocked;
const isRemoteLockedAndDeviceNotAccepted = () => this.core?.replicator?.remoteLockedAndDeviceNotAccepted;
const isRemoteLocked = () => this.core?.replicator?.remoteLocked;
// if (this.plugin?.replicator?.remoteLockedAndDeviceNotAccepted) {
this.createEl(
paneEl,
@@ -92,7 +92,7 @@ export function paneMaintenance(
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.plugin.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
await this.core.storageAccess.writeFileAuto(FLAGMD_REDFLAG, "");
this.services.appLifecycle.performRestart();
})
);
@@ -108,7 +108,7 @@ export function paneMaintenance(
.setCta()
.setDisabled(false)
.onClick(async () => {
await this.plugin.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
this.services.appLifecycle.performRestart();
})
);
@@ -121,7 +121,7 @@ export function paneMaintenance(
.setCta()
.setDisabled(false)
.onClick(async () => {
await this.plugin.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
this.services.appLifecycle.performRestart();
})
);
@@ -137,8 +137,8 @@ export function paneMaintenance(
.setWarning()
.setDisabled(false)
.onClick(async () => {
if (this.plugin.replicator instanceof LiveSyncCouchDBReplicator) {
await this.plugin.replicator.sendChunks(this.plugin.settings, undefined, true, 0);
if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
}
})
)
@@ -299,7 +299,7 @@ export function paneMaintenance(
.setButtonText("Perform")
.setDisabled(false)
.onClick(async () => {
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction");
if (await replicator.compactRemote(this.editingSettings)) {
Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction");

View File

@@ -31,7 +31,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
void addPanel(paneEl, "Compatibility (Database structure)").then((paneEl) => {
const migrateAllToIndexedDB = async () => {
const dbToName = this.plugin.localDatabase.dbname + SuffixDatabaseName + ExtraSuffixIndexedDB;
const dbToName = this.core.localDatabase.dbname + SuffixDatabaseName + ExtraSuffixIndexedDB;
const options = {
adapter: "indexeddb",
//@ts-ignore :missing def
@@ -42,18 +42,19 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
const openTo = () => {
return new PouchDB(dbToName, options);
};
if (await migrateDatabases("to IndexedDB", this.plugin.localDatabase.localDatabase, openTo)) {
if (await migrateDatabases("to IndexedDB", this.core.localDatabase.localDatabase, openTo)) {
Logger(
"Migration to IndexedDB completed. Obsidian will be restarted with new configuration immediately.",
LOG_LEVEL_NOTICE
);
this.plugin.settings.useIndexedDBAdapter = true;
await this.services.setting.saveSettingData();
// this.plugin.settings.useIndexedDBAdapter = true;
// await this.services.setting.saveSettingData();
await this.core.services.setting.applyPartial({ useIndexedDBAdapter: true }, true);
this.services.appLifecycle.performRestart();
}
};
const migrateAllToIDB = async () => {
const dbToName = this.plugin.localDatabase.dbname + SuffixDatabaseName;
const dbToName = this.core.localDatabase.dbname + SuffixDatabaseName;
const options = {
adapter: "idb",
auto_compaction: false,
@@ -62,13 +63,14 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
const openTo = () => {
return new PouchDB(dbToName, options);
};
if (await migrateDatabases("to IDB", this.plugin.localDatabase.localDatabase, openTo)) {
if (await migrateDatabases("to IDB", this.core.localDatabase.localDatabase, openTo)) {
Logger(
"Migration to IDB completed. Obsidian will be restarted with new configuration immediately.",
LOG_LEVEL_NOTICE
);
this.plugin.settings.useIndexedDBAdapter = false;
await this.services.setting.saveSettingData();
await this.core.services.setting.applyPartial({ useIndexedDBAdapter: false }, true);
// this.core.settings.useIndexedDBAdapter = false;
// await this.services.setting.saveSettingData();
this.services.appLifecycle.performRestart();
}
};
@@ -151,7 +153,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
} as Record<HashAlgorithm, string>,
});
this.addOnSaved("hashAlg", async () => {
await this.plugin.localDatabase._prepareHashFunctions();
await this.core.localDatabase._prepareHashFunctions();
});
});
void addPanel(paneEl, "Edge case addressing (Behaviour)").then((paneEl) => {
@@ -215,7 +217,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
this.addOnSaved("maxMTimeForReflectEvents", async (key) => {
const buttons = ["Restart Now", "Later"] as const;
const reboot = await this.plugin.confirm.askSelectStringDialogue(
const reboot = await this.core.confirm.askSelectStringDialogue(
"Restarting Obsidian is strongly recommended. Until restart, some changes may not take effect, and display may be inconsistent. Are you sure to restart now?",
buttons,
{

View File

@@ -68,7 +68,7 @@ export function paneRemoteConfig(
.addButton((button) =>
button
.onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onlyE2EEConfiguration(UserMode.Update, originalSettings);
updateE2EESummary();
@@ -79,7 +79,7 @@ export function paneRemoteConfig(
.addButton((button) =>
button
.onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onConfigureManually(originalSettings, UserMode.Update);
updateE2EESummary();
@@ -101,7 +101,7 @@ export function paneRemoteConfig(
.setButtonText("Change Remote and Setup")
.setCta()
.onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onSelectServer(originalSettings, UserMode.Update);
})
@@ -127,7 +127,7 @@ export function paneRemoteConfig(
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onCouchDBManualSetup(
UserMode.Update,
@@ -162,7 +162,7 @@ export function paneRemoteConfig(
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onBucketManualSetup(
UserMode.Update,
@@ -202,7 +202,7 @@ export function paneRemoteConfig(
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onP2PManualSetup(
UserMode.Update,

View File

@@ -35,7 +35,7 @@ export function paneSetup(
.setDesc($msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."))
.addButton((text) => {
text.setButtonText($msg("Rerun Wizard")).onClick(async () => {
const setupManager = this.plugin.getModule(SetupManager);
const setupManager = this.core.getModule(SetupManager);
await setupManager.onOnboard(UserMode.ExistingUser);
// await this.plugin.moduleSetupObsidian.onBoardingWizard(true);
});
@@ -86,14 +86,14 @@ export function paneSetup(
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard"))
.onClick(async () => {
if (
(await this.plugin.confirm.askYesNoDialog(
(await this.core.confirm.askYesNoDialog(
$msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
{ defaultOption: "No" }
)) == "yes"
) {
this.editingSettings = { ...this.editingSettings, ...DEFAULT_SETTINGS };
await this.saveAllDirtySettings();
this.plugin.settings = { ...DEFAULT_SETTINGS };
this.core.settings = { ...DEFAULT_SETTINGS };
await this.services.setting.saveSettingData();
await this.services.database.resetDatabase();
// await this.plugin.initializeDatabase();

View File

@@ -109,7 +109,7 @@ export function paneSyncSettings(
await this.rebuildDB("localOnly");
// this.resetEditingSettings();
if (
(await this.plugin.confirm.askYesNoDialog(
(await this.core.confirm.askYesNoDialog(
$msg("obsidianLiveSyncSettingTab.msgGenerateSetupURI"),
{
defaultOption: "Yes",

View File

@@ -80,7 +80,7 @@ export class ModuleLiveSyncMain extends AbstractModule {
initialiseWorkerModule();
await this.services.appLifecycle.onWireUpEvents();
// debugger;
eventHub.emitEvent(EVENT_PLUGIN_LOADED, this.core);
eventHub.emitEvent(EVENT_PLUGIN_LOADED);
this._log($msg("moduleLiveSyncMain.logLoadingPlugin"));
if (!(await this.services.appLifecycle.onInitialise())) {
this._log($msg("moduleLiveSyncMain.logPluginInitCancelled"), LOG_LEVEL_NOTICE);

View File

@@ -171,4 +171,18 @@ export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceCont
statusText: `${r.status}`,
});
}
override addStatusBarItem(): HTMLElement | undefined {
return this.context.plugin.addStatusBarItem();
}
override setInterval(handler: () => void, timeout: number): number {
const timerId = globalThis.setInterval(handler, timeout) as unknown as number;
this.context.plugin.registerInterval(timerId);
return timerId;
}
override getSystemConfigDir() {
return this.app.vault.configDir;
}
}

View File

@@ -1,7 +1,40 @@
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext";
import { normalizePath } from "@/deps";
import { PathService } from "@/lib/src/services/base/PathService";
import {
type BASE_IS_NEW,
type TARGET_IS_NEW,
type EVEN,
markChangesAreSame,
unmarkChanges,
compareFileFreshness,
isMarkedAsSameChanges,
} from "@/common/utils";
import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@/lib/src/common/types";
export class ObsidianPathService extends PathService<ObsidianServiceContext> {
override markChangesAreSame(
old: UXFileInfo | AnyEntry | FilePathWithPrefix,
newMtime: number,
oldMtime: number
): boolean | undefined {
return markChangesAreSame(old, newMtime, oldMtime);
}
override unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void {
return unmarkChanges(file);
}
override compareFileFreshness(
baseFile: UXFileInfoStub | AnyEntry | undefined,
checkTarget: UXFileInfo | AnyEntry | undefined
): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
return compareFileFreshness(baseFile, checkTarget);
}
override isMarkedAsSameChanges(
file: UXFileInfoStub | AnyEntry | FilePathWithPrefix,
mtimes: number[]
): undefined | typeof EVEN {
return isMarkedAsSameChanges(file, mtimes);
}
protected normalizePath(path: string): string {
return normalizePath(path);
}

View File

@@ -59,6 +59,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
path: path,
vault: vault,
setting: setting,
API: API,
});
const keyValueDB = new ObsidianKeyValueDBService(context, {
appLifecycle: appLifecycle,
@@ -77,7 +78,6 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
const replication = new ObsidianReplicationService(context, {
APIService: API,
appLifecycleService: appLifecycle,
databaseEventService: databaseEvents,
replicatorService: replicator,
settingService: setting,
fileProcessingService: fileProcessing,

View File

@@ -1,4 +1,4 @@
import { getPathFromTFile } from "@/common/utils";
import { getPathFromTFile, isValidPath } from "@/common/utils";
import { InjectableVaultService } from "@/lib/src/services/implements/injectable/InjectableVaultService";
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import type { FilePath } from "@/lib/src/common/types";
@@ -30,4 +30,7 @@ export class ObsidianVaultService extends InjectableVaultService<ObsidianService
if (this.isStorageInsensitive()) return false;
return super.shouldCheckCaseInsensitively(); // Check the setting
}
override isValidPath(path: string): boolean {
return isValidPath(path);
}
}

View File

@@ -0,0 +1,387 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
import { createInstanceLogFunction, type LogFunction } from "@lib/services/lib/logUtils";
import { FlagFilesHumanReadable, FlagFilesOriginal } from "@lib/common/models/redflag.const";
import FetchEverything from "../modules/features/SetupWizard/dialogs/FetchEverything.svelte";
import RebuildEverything from "../modules/features/SetupWizard/dialogs/RebuildEverything.svelte";
import { extractObject } from "octagonal-wheels/object";
import { REMOTE_MINIO } from "@lib/common/models/setting.const";
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
import { TweakValuesShouldMatchedTemplate } from "@lib/common/models/tweak.definition";
/**
* Flag file handler interface, similar to target filter pattern.
*/
interface FlagFileHandler {
priority: number;
check: () => Promise<boolean>;
handle: () => Promise<boolean>;
}
export async function isFlagFileExist(host: NecessaryServices<never, "storageAccess">, path: string) {
const redFlagExist = await host.serviceModules.storageAccess.isExists(
host.serviceModules.storageAccess.normalisePath(path)
);
if (redFlagExist) {
return true;
}
return false;
}
export async function deleteFlagFile(host: NecessaryServices<never, "storageAccess">, log: LogFunction, path: string) {
try {
const isFlagged = await host.serviceModules.storageAccess.isExists(
host.serviceModules.storageAccess.normalisePath(path)
);
if (isFlagged) {
await host.serviceModules.storageAccess.delete(path, true);
}
} catch (ex) {
log(`Could not delete ${path}`);
log(ex, LOG_LEVEL_VERBOSE);
}
}
/**
* Factory function to create a fetch all flag handler.
* All logic related to fetch all flag is encapsulated here.
*/
export function createFetchAllFlagHandler(
host: NecessaryServices<
"vault" | "fileProcessing" | "tweakValue" | "UI" | "setting" | "appLifecycle",
"storageAccess" | "rebuilder"
>,
log: LogFunction
): FlagFileHandler {
// Check if fetch all flag is active
const isFlagActive = async () =>
(await isFlagFileExist(host, FlagFilesOriginal.FETCH_ALL)) ||
(await isFlagFileExist(host, FlagFilesHumanReadable.FETCH_ALL));
// Cleanup fetch all flag files
const cleanupFlag = async () => {
await deleteFlagFile(host, log, FlagFilesOriginal.FETCH_ALL);
await deleteFlagFile(host, log, FlagFilesHumanReadable.FETCH_ALL);
};
// Handle the fetch all scheduled operation
const onScheduled = async () => {
const method = await host.services.UI.dialogManager.openWithExplicitCancel(FetchEverything);
if (method === "cancelled") {
log("Fetch everything cancelled by user.", LOG_LEVEL_NOTICE);
await cleanupFlag();
host.services.appLifecycle.performRestart();
return false;
}
const { vault, extra } = method;
const settings = await host.services.setting.currentSettings();
// If remote is MinIO, makeLocalChunkBeforeSync is not available. (because no-deduplication on sending).
const makeLocalChunkBeforeSyncAvailable = settings.remoteType !== REMOTE_MINIO;
const mapVaultStateToAction = {
identical: {
makeLocalChunkBeforeSync: makeLocalChunkBeforeSyncAvailable,
makeLocalFilesBeforeSync: false,
},
independent: {
makeLocalChunkBeforeSync: false,
makeLocalFilesBeforeSync: false,
},
unbalanced: {
makeLocalChunkBeforeSync: false,
makeLocalFilesBeforeSync: true,
},
cancelled: {
makeLocalChunkBeforeSync: false,
makeLocalFilesBeforeSync: false,
},
} as const;
return await processVaultInitialisation(host, log, async () => {
const settings = host.services.setting.currentSettings();
await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
const vaultStateToAction = mapVaultStateToAction[vault];
const { makeLocalChunkBeforeSync, makeLocalFilesBeforeSync } = vaultStateToAction;
log(
`Fetching everything with settings: makeLocalChunkBeforeSync=${makeLocalChunkBeforeSync}, makeLocalFilesBeforeSync=${makeLocalFilesBeforeSync}`,
LOG_LEVEL_INFO
);
await host.serviceModules.rebuilder.$fetchLocal(makeLocalChunkBeforeSync, !makeLocalFilesBeforeSync);
await cleanupFlag();
log("Fetch everything operation completed. Vault files will be gradually synced.", LOG_LEVEL_NOTICE);
return true;
});
};
return {
priority: 10,
check: () => isFlagActive(),
handle: async () => {
const res = await onScheduled();
if (res) {
return await verifyAndUnlockSuspension(host, log);
}
return false;
},
};
}
/**
* Adjust setting to remote configuration.
* @param config current configuration to retrieve remote preferred config
* @returns updated configuration if applied, otherwise null.
*/
export async function adjustSettingToRemote(
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
log: LogFunction,
config: ObsidianLiveSyncSettings
) {
// Fetch remote configuration unless prevented.
const SKIP_FETCH = "Skip and proceed";
const RETRY_FETCH = "Retry (recommended)";
let canProceed = false;
do {
const remoteTweaks = await host.services.tweakValue.fetchRemotePreferred(config);
if (!remoteTweaks) {
const choice = await host.services.UI.confirm.askSelectStringDialogue(
"Could not fetch configuration from remote. If you are new to the Self-hosted LiveSync, this might be expected. If not, you should check your network or server settings.",
[SKIP_FETCH, RETRY_FETCH] as const,
{
defaultAction: RETRY_FETCH,
timeout: 0,
title: "Fetch Remote Configuration Failed",
}
);
if (choice === SKIP_FETCH) {
canProceed = true;
}
} else {
const necessary = extractObject(TweakValuesShouldMatchedTemplate, remoteTweaks);
// Check if any necessary tweak value is different from current config.
const differentItems = Object.entries(necessary).filter(([key, value]) => {
return (config as any)[key] !== value;
});
if (differentItems.length === 0) {
log("Remote configuration matches local configuration. No changes applied.", LOG_LEVEL_NOTICE);
} else {
await host.services.UI.confirm.askSelectStringDialogue(
"Your settings differed slightly from the server's. The plug-in has supplemented the incompatible parts with the server settings!",
["OK"] as const,
{
defaultAction: "OK",
timeout: 0,
}
);
}
config = {
...config,
...Object.fromEntries(differentItems),
} satisfies ObsidianLiveSyncSettings;
await host.services.setting.applyPartial(config, true);
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
canProceed = true;
const updatedConfig = host.services.setting.currentSettings();
return updatedConfig;
}
} while (!canProceed);
}
/**
* Adjust setting to remote if needed.
* @param extra result of dialogues that may contain preventFetchingConfig flag (e.g, from FetchEverything or RebuildEverything)
* @param config current configuration to retrieve remote preferred config
*/
export async function adjustSettingToRemoteIfNeeded(
host: NecessaryServices<"tweakValue" | "UI" | "setting", any>,
log: LogFunction,
extra: { preventFetchingConfig: boolean },
config: ObsidianLiveSyncSettings
) {
if (extra && extra.preventFetchingConfig) {
return;
}
// Remote configuration fetched and applied.
if (await adjustSettingToRemote(host, log, config)) {
config = host.services.setting.currentSettings();
} else {
log("Remote configuration not applied.", LOG_LEVEL_NOTICE);
}
// log(JSON.stringify(config), LOG_LEVEL_VERBOSE);
}
/**
* Process vault initialisation with suspending file watching and sync.
* @param proc process to be executed during initialisation, should return true if can be continued, false if app is unable to continue the process.
* @param keepSuspending whether to keep suspending file watching after the process.
* @returns result of the process, or false if error occurs.
*/
export async function processVaultInitialisation(
host: NecessaryServices<"setting", any>,
log: LogFunction,
proc: () => Promise<boolean>,
keepSuspending = false
) {
try {
// Disable batch saving and file watching during initialisation.
await host.services.setting.applyPartial({ batchSave: false }, false);
await host.services.setting.suspendAllSync();
await host.services.setting.suspendExtraSync();
await host.services.setting.applyPartial({ suspendFileWatching: true }, true);
try {
const result = await proc();
return result;
} catch (ex) {
log("Error during vault initialisation process.", LOG_LEVEL_NOTICE);
log(ex, LOG_LEVEL_VERBOSE);
return false;
}
} catch (ex) {
log("Error during vault initialisation.", LOG_LEVEL_NOTICE);
log(ex, LOG_LEVEL_VERBOSE);
return false;
} finally {
if (!keepSuspending) {
// Re-enable file watching after initialisation.
await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
}
}
}
export async function verifyAndUnlockSuspension(
host: NecessaryServices<"setting" | "appLifecycle" | "UI", any>,
log: LogFunction
) {
if (!host.services.setting.currentSettings().suspendFileWatching) {
return true;
}
if (
(await host.services.UI.confirm.askYesNoDialog(
"Do you want to resume file and database processing, and restart obsidian now?",
{ defaultOption: "Yes", timeout: 15 }
)) != "yes"
) {
// TODO: Confirm actually proceed to next process.
return true;
}
await host.services.setting.applyPartial({ suspendFileWatching: false }, true);
host.services.appLifecycle.performRestart();
return false;
}
/**
* Factory function to create a rebuild flag handler.
* All logic related to rebuild flag is encapsulated here.
*/
export function createRebuildFlagHandler(
host: NecessaryServices<"setting" | "appLifecycle" | "UI" | "tweakValue", "storageAccess" | "rebuilder">,
log: LogFunction
) {
// Check if rebuild flag is active
const isFlagActive = async () =>
(await isFlagFileExist(host, FlagFilesOriginal.REBUILD_ALL)) ||
(await isFlagFileExist(host, FlagFilesHumanReadable.REBUILD_ALL));
// Cleanup rebuild flag files
const cleanupFlag = async () => {
await deleteFlagFile(host, log, FlagFilesOriginal.REBUILD_ALL);
await deleteFlagFile(host, log, FlagFilesHumanReadable.REBUILD_ALL);
};
// Handle the rebuild everything scheduled operation
const onScheduled = async () => {
const method = await host.services.UI.dialogManager.openWithExplicitCancel(RebuildEverything);
if (method === "cancelled") {
log("Rebuild everything cancelled by user.", LOG_LEVEL_NOTICE);
await cleanupFlag();
host.services.appLifecycle.performRestart();
return false;
}
const { extra } = method;
const settings = host.services.setting.currentSettings();
await adjustSettingToRemoteIfNeeded(host, log, extra, settings);
return await processVaultInitialisation(host, log, async () => {
await host.serviceModules.rebuilder.$rebuildEverything();
await cleanupFlag();
log("Rebuild everything operation completed.", LOG_LEVEL_NOTICE);
return true;
});
};
return {
priority: 20,
check: () => isFlagActive(),
handle: async () => {
const res = await onScheduled();
if (res) {
return await verifyAndUnlockSuspension(host, log);
}
return false;
},
};
}
/**
* Factory function to create a suspend all flag handler.
* All logic related to suspend flag is encapsulated here.
*/
export function createSuspendFlagHandler(
host: NecessaryServices<"setting", "storageAccess">,
log: LogFunction
): FlagFileHandler {
// Check if suspend flag is active
const isFlagActive = async () => await isFlagFileExist(host, FlagFilesOriginal.SUSPEND_ALL);
// Handle the suspend all scheduled operation
const onScheduled = async () => {
log("SCRAM is detected. All operations are suspended.", LOG_LEVEL_NOTICE);
return await processVaultInitialisation(
host,
log,
async () => {
log(
"All operations are suspended as per SCRAM.\nLogs will be written to the file. This might be a performance impact.",
LOG_LEVEL_NOTICE
);
await host.services.setting.applyPartial({ writeLogToTheFile: true }, true);
return Promise.resolve(false);
},
true
);
};
return {
priority: 5,
check: () => isFlagActive(),
handle: () => onScheduled(),
};
}
export function flagHandlerToEventHandler(flagHandler: FlagFileHandler) {
return async () => {
if (await flagHandler.check()) {
return await flagHandler.handle();
}
return true;
};
}
export function useRedFlagFeatures(
host: NecessaryServices<
"API" | "appLifecycle" | "UI" | "setting" | "tweakValue" | "fileProcessing" | "vault",
"storageAccess" | "rebuilder"
>
) {
const log = createInstanceLogFunction("SF:RedFlag", host.services.API);
const handlerFetch = createFetchAllFlagHandler(host, log);
const handlerRebuild = createRebuildFlagHandler(host, log);
const handlerSuspend = createSuspendFlagHandler(host, log);
host.services.appLifecycle.onLayoutReady.addHandler(flagHandlerToEventHandler(handlerFetch), handlerFetch.priority);
host.services.appLifecycle.onLayoutReady.addHandler(
flagHandlerToEventHandler(handlerRebuild),
handlerRebuild.priority
);
host.services.appLifecycle.onLayoutReady.addHandler(
flagHandlerToEventHandler(handlerSuspend),
handlerSuspend.priority
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,8 @@
import { markChangesAreSame } from "@/common/utils";
import type { AnyEntry } from "@lib/common/types";
import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts";
import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase";
// markChangesAreSame uses persistent data implicitly, we should refactor it too.
// For now, to make the refactoring done once, we just use them directly.
// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB).
// TODO: REFACTOR
export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {
markChangesAreSame(old: AnyEntry, newMtime: number, oldMtime: number): void {
markChangesAreSame(old, newMtime, oldMtime);
}
}
// Refactored, now migrating...
export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {}

View File

@@ -1,160 +1,14 @@
import { markChangesAreSame } from "@/common/utils";
import type { FilePath, UXDataWriteOptions, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import { TFolder, type TAbstractFile, TFile, type Stat, type App, type DataWriteOptions, normalizePath } from "@/deps";
import { FileAccessBase, toArrayBuffer, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts";
import { TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
declare module "obsidian" {
interface Vault {
getAbstractFileByPathInsensitive(path: string): TAbstractFile | null;
}
interface DataAdapter {
reconcileInternalFile?(path: string): Promise<void>;
}
}
export class FileAccessObsidian extends FileAccessBase<TAbstractFile, TFile, TFolder, Stat> {
app: App;
override getPath(file: string | TAbstractFile): FilePath {
return (typeof file === "string" ? file : file.path) as FilePath;
}
override isFile(file: TAbstractFile | null): file is TFile {
return file instanceof TFile;
}
override isFolder(file: TAbstractFile | null): file is TFolder {
return file instanceof TFolder;
}
override _statFromNative(file: TFile): Promise<TFile["stat"]> {
return Promise.resolve(file.stat);
}
override nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub {
return TFileToUXFileInfoStub(file);
}
override nativeFolderToUXFolder(folder: TFolder): UXFolderInfo {
if (folder instanceof TFolder) {
return this.nativeFolderToUXFolder(folder);
} else {
throw new Error(`Not a folder: ${(folder as TAbstractFile)?.name}`);
}
}
import { type App } from "@/deps";
import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts";
import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter";
/**
* Obsidian-specific implementation of FileAccessBase
* Uses ObsidianFileSystemAdapter for platform-specific operations
*/
export class FileAccessObsidian extends FileAccessBase<ObsidianFileSystemAdapter> {
constructor(app: App, dependencies: FileAccessBaseDependencies) {
super({
storageAccessManager: dependencies.storageAccessManager,
vaultService: dependencies.vaultService,
settingService: dependencies.settingService,
APIService: dependencies.APIService,
});
this.app = app;
}
protected override _normalisePath(path: string): string {
return normalizePath(path);
}
protected async _adapterMkdir(path: string) {
await this.app.vault.adapter.mkdir(path);
}
protected _getAbstractFileByPath(path: FilePath) {
return this.app.vault.getAbstractFileByPath(path);
}
protected _getAbstractFileByPathInsensitive(path: FilePath) {
return this.app.vault.getAbstractFileByPathInsensitive(path);
}
protected async _tryAdapterStat(path: FilePath) {
if (!(await this.app.vault.adapter.exists(path))) return null;
return await this.app.vault.adapter.stat(path);
}
protected async _adapterStat(path: FilePath) {
return await this.app.vault.adapter.stat(path);
}
protected async _adapterExists(path: FilePath) {
return await this.app.vault.adapter.exists(path);
}
protected async _adapterRemove(path: FilePath) {
await this.app.vault.adapter.remove(path);
}
protected async _adapterRead(path: FilePath) {
return await this.app.vault.adapter.read(path);
}
protected async _adapterReadBinary(path: FilePath) {
return await this.app.vault.adapter.readBinary(path);
}
_adapterWrite(file: string, data: string, options?: UXDataWriteOptions): Promise<void> {
return this.app.vault.adapter.write(file, data, options);
}
_adapterWriteBinary(file: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
return this.app.vault.adapter.writeBinary(file, toArrayBuffer(data), options);
}
protected _adapterList(basePath: string): Promise<{ files: string[]; folders: string[] }> {
return Promise.resolve(this.app.vault.adapter.list(basePath));
}
async _vaultCacheRead(file: TFile) {
return await this.app.vault.cachedRead(file);
}
protected async _vaultRead(file: TFile): Promise<string> {
return await this.app.vault.read(file);
}
protected async _vaultReadBinary(file: TFile): Promise<ArrayBuffer> {
return await this.app.vault.readBinary(file);
}
protected override markChangesAreSame(path: string, mtime: number, newMtime: number) {
return markChangesAreSame(path, mtime, newMtime);
}
protected override async _vaultModify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.modify(file, data, options);
}
protected override async _vaultModifyBinary(
file: TFile,
data: ArrayBuffer,
options?: UXDataWriteOptions
): Promise<void> {
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
}
protected override async _vaultCreate(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
return await this.app.vault.create(path, data, options);
}
protected override async _vaultCreateBinary(
path: string,
data: ArrayBuffer,
options?: UXDataWriteOptions
): Promise<TFile> {
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
}
protected override _trigger(name: string, ...data: any[]) {
return this.app.vault.trigger(name, ...data);
}
protected override async _reconcileInternalFile(path: string) {
return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path));
}
protected override async _adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) {
return await this.app.vault.adapter.append(normalizedPath, data, options);
}
protected override async _delete(file: TFile | TFolder, force = false) {
return await this.app.vault.delete(file, force);
}
protected override async _trash(file: TFile | TFolder, force = false) {
return await this.app.vault.trash(file, force);
}
protected override _getFiles() {
return this.app.vault.getFiles();
const adapter = new ObsidianFileSystemAdapter(app);
super(adapter, dependencies);
}
}

View File

@@ -1,26 +1,7 @@
import {
compareFileFreshness,
markChangesAreSame,
type BASE_IS_NEW,
type EVEN,
type TARGET_IS_NEW,
} from "@/common/utils";
import type { AnyEntry } from "@lib/common/models/db.type";
import type { UXFileInfo, UXFileInfoStub } from "@lib/common/models/fileaccess.type";
import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase";
// markChangesAreSame uses persistent data implicitly, we should refactor it too.
// also, compareFileFreshness depends on marked changes, so we should refactor it as well. For now, to make the refactoring done once, we just use them directly.
// Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB).
// TODO: REFACTOR
export class ServiceFileHandler extends ServiceFileHandlerBase {
override markChangesAreSame(old: UXFileInfo | AnyEntry, newMtime: number, oldMtime: number) {
return markChangesAreSame(old, newMtime, oldMtime);
}
override compareFileFreshness(
baseFile: UXFileInfoStub | AnyEntry | undefined,
checkTarget: UXFileInfo | AnyEntry | undefined
): typeof TARGET_IS_NEW | typeof BASE_IS_NEW | typeof EVEN {
return compareFileFreshness(baseFile, checkTarget);
}
}
// Refactored: markChangesAreSame, unmarkChanges, compareFileFreshness, isMarkedAsSameChanges are now moved to PathService
export class ServiceFileHandler extends ServiceFileHandlerBase {}

View File

@@ -0,0 +1,18 @@
import type { UXFileInfoStub, UXFolderInfo } from "@/lib/src/common/types";
import type { IConversionAdapter } from "@/lib/src/serviceModules/adapters";
import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian";
import type { TFile, TFolder } from "obsidian";
/**
* Conversion adapter implementation for Obsidian
*/
export class ObsidianConversionAdapter implements IConversionAdapter<TFile, TFolder> {
nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub {
return TFileToUXFileInfoStub(file);
}
nativeFolderToUXFolder(folder: TFolder): UXFolderInfo {
return TFolderToUXFileInfoStub(folder);
}
}

View File

@@ -0,0 +1,64 @@
import type { FilePath, UXStat } from "@/lib/src/common/types";
import type {
IFileSystemAdapter,
IPathAdapter,
ITypeGuardAdapter,
IConversionAdapter,
IStorageAdapter,
IVaultAdapter,
} from "@/lib/src/serviceModules/adapters";
import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian";
import { ObsidianConversionAdapter } from "./ObsidianConversionAdapter";
import { ObsidianPathAdapter } from "./ObsidianPathAdapter";
import { ObsidianStorageAdapter } from "./ObsidianStorageAdapter";
import { ObsidianTypeGuardAdapter } from "./ObsidianTypeGuardAdapter";
import { ObsidianVaultAdapter } from "./ObsidianVaultAdapter";
declare module "obsidian" {
interface Vault {
getAbstractFileByPathInsensitive(path: string): TAbstractFile | null;
}
interface DataAdapter {
reconcileInternalFile?(path: string): Promise<void>;
}
}
/**
* Complete file system adapter implementation for Obsidian
*/
export class ObsidianFileSystemAdapter implements IFileSystemAdapter<TAbstractFile, TFile, TFolder, Stat> {
readonly path: IPathAdapter<TAbstractFile>;
readonly typeGuard: ITypeGuardAdapter<TFile, TFolder>;
readonly conversion: IConversionAdapter<TFile, TFolder>;
readonly storage: IStorageAdapter<Stat>;
readonly vault: IVaultAdapter<TFile>;
constructor(private app: App) {
this.path = new ObsidianPathAdapter();
this.typeGuard = new ObsidianTypeGuardAdapter();
this.conversion = new ObsidianConversionAdapter();
this.storage = new ObsidianStorageAdapter(app);
this.vault = new ObsidianVaultAdapter(app);
}
getAbstractFileByPath(path: FilePath | string): Promise<TAbstractFile | null> {
return Promise.resolve(this.app.vault.getAbstractFileByPath(path));
}
getAbstractFileByPathInsensitive(path: FilePath | string): Promise<TAbstractFile | null> {
return Promise.resolve(this.app.vault.getAbstractFileByPathInsensitive(path));
}
getFiles(): Promise<TFile[]> {
return Promise.resolve(this.app.vault.getFiles());
}
statFromNative(file: TFile): Promise<UXStat> {
return Promise.resolve({ ...file.stat, type: "file" });
}
async reconcileInternalFile(path: string): Promise<void> {
return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path));
}
}

View File

@@ -0,0 +1,16 @@
import { type TAbstractFile, normalizePath } from "@/deps";
import type { FilePath } from "@lib/common/types";
import type { IPathAdapter } from "@lib/serviceModules/adapters";
/**
* Path adapter implementation for Obsidian
*/
export class ObsidianPathAdapter implements IPathAdapter<TAbstractFile> {
getPath(file: string | TAbstractFile): FilePath {
return (typeof file === "string" ? file : file.path) as FilePath;
}
normalisePath(path: string): string {
return normalizePath(path);
}
}

View File

@@ -0,0 +1,57 @@
import type { UXDataWriteOptions } from "@/lib/src/common/types";
import type { IStorageAdapter } from "@/lib/src/serviceModules/adapters";
import { toArrayBuffer } from "@/lib/src/serviceModules/FileAccessBase";
import type { Stat, App } from "obsidian";
/**
* Storage adapter implementation for Obsidian
*/
export class ObsidianStorageAdapter implements IStorageAdapter<Stat> {
constructor(private app: App) {}
async exists(path: string): Promise<boolean> {
return await this.app.vault.adapter.exists(path);
}
async trystat(path: string): Promise<Stat | null> {
if (!(await this.app.vault.adapter.exists(path))) return null;
return await this.app.vault.adapter.stat(path);
}
async stat(path: string): Promise<Stat | null> {
return await this.app.vault.adapter.stat(path);
}
async mkdir(path: string): Promise<void> {
await this.app.vault.adapter.mkdir(path);
}
async remove(path: string): Promise<void> {
await this.app.vault.adapter.remove(path);
}
async read(path: string): Promise<string> {
return await this.app.vault.adapter.read(path);
}
async readBinary(path: string): Promise<ArrayBuffer> {
return await this.app.vault.adapter.readBinary(path);
}
async write(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.adapter.write(path, data, options);
}
async writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options);
}
async append(path: string, data: string, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.adapter.append(path, data, options);
}
list(basePath: string): Promise<{ files: string[]; folders: string[] }> {
return Promise.resolve(this.app.vault.adapter.list(basePath));
}
}

View File

@@ -0,0 +1,16 @@
import type { ITypeGuardAdapter } from "@/lib/src/serviceModules/adapters";
import { TFile, TFolder } from "obsidian";
/**
* Type guard adapter implementation for Obsidian
*/
export class ObsidianTypeGuardAdapter implements ITypeGuardAdapter<TFile, TFolder> {
isFile(file: any): file is TFile {
return file instanceof TFile;
}
isFolder(item: any): item is TFolder {
return item instanceof TFolder;
}
}

View File

@@ -0,0 +1,51 @@
import type { UXDataWriteOptions } from "@/lib/src/common/types";
import type { IVaultAdapter } from "@/lib/src/serviceModules/adapters";
import { toArrayBuffer } from "@/lib/src/serviceModules/FileAccessBase";
import type { TFile, App, TFolder } from "obsidian";
/**
* Vault adapter implementation for Obsidian
*/
export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
constructor(private app: App) {}
async read(file: TFile): Promise<string> {
return await this.app.vault.read(file);
}
async cachedRead(file: TFile): Promise<string> {
return await this.app.vault.cachedRead(file);
}
async readBinary(file: TFile): Promise<ArrayBuffer> {
return await this.app.vault.readBinary(file);
}
async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.modify(file, data, options);
}
async modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<void> {
return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options);
}
async create(path: string, data: string, options?: UXDataWriteOptions): Promise<TFile> {
return await this.app.vault.create(path, data, options);
}
async createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise<TFile> {
return await this.app.vault.createBinary(path, toArrayBuffer(data), options);
}
async delete(file: TFile | TFolder, force = false): Promise<void> {
return await this.app.vault.delete(file, force);
}
async trash(file: TFile | TFolder, force = false): Promise<void> {
return await this.app.vault.trash(file, force);
}
trigger(name: string, ...data: any[]): any {
return this.app.vault.trigger(name, ...data);
}
}

View File

@@ -1,6 +1,5 @@
import type { TAbstractFile, TFile, TFolder, Stat } from "@/deps";
import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase";
import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter";
// For typechecking purpose
export class ServiceFileAccessObsidian extends ServiceFileAccessBase<TAbstractFile, TFile, TFolder, Stat> {}
// For now, this is just a re-export of ServiceFileAccess with the Obsidian-specific adapter type.
export class ServiceFileAccessObsidian extends ServiceFileAccessBase<ObsidianFileSystemAdapter> {}

View File

@@ -1,13 +1,13 @@
.added {
.ls-dialog .added {
color: var(--text-on-accent);
background-color: var(--text-accent);
}
.normal {
.ls-dialog .normal {
color: var(--text-normal);
}
.deleted {
.ls-dialog .deleted {
color: var(--text-on-accent);
background-color: var(--text-muted);
}

View File

@@ -109,7 +109,7 @@ export async function generateHarness(
}
export async function waitForReady(harness: LiveSyncHarness): Promise<void> {
for (let i = 0; i < 10; i++) {
if (harness.plugin.services.appLifecycle.isReady()) {
if (harness.plugin.core.services.appLifecycle.isReady()) {
console.log("App Lifecycle is ready");
return;
}
@@ -122,11 +122,11 @@ export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
for (let i = 0; i < 20; i++) {
await delay(25);
const processing =
harness.plugin.services.replication.databaseQueueCount.value +
harness.plugin.services.fileProcessing.totalQueued.value +
harness.plugin.services.fileProcessing.batched.value +
harness.plugin.services.fileProcessing.processing.value +
harness.plugin.services.replication.storageApplyingCount.value;
harness.plugin.core.services.replication.databaseQueueCount.value +
harness.plugin.core.services.fileProcessing.totalQueued.value +
harness.plugin.core.services.fileProcessing.batched.value +
harness.plugin.core.services.fileProcessing.processing.value +
harness.plugin.core.services.replication.storageApplyingCount.value;
if (processing === 0) {
if (i > 0) {
@@ -139,7 +139,7 @@ export async function waitForIdle(harness: LiveSyncHarness): Promise<void> {
export async function waitForClosed(harness: LiveSyncHarness): Promise<void> {
await delay(100);
for (let i = 0; i < 10; i++) {
if (harness.plugin.services.control.hasUnloaded()) {
if (harness.plugin.core.services.control.hasUnloaded()) {
console.log("App has unloaded");
return;
}

View File

@@ -40,12 +40,12 @@ export async function storeFile(
expect(readContent).toBe(content);
}
}
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
await waitForIdle(harness);
return file;
}
export async function readFromLocalDB(harness: LiveSyncHarness, path: string) {
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
expect(entry).not.toBe(false);
return entry;
}
@@ -95,11 +95,11 @@ export async function testFileWrite(
) {
const file = await storeFile(harness, path, content, false, fileOptions);
expect(file).toBeInstanceOf(TFile);
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
await waitForIdle(harness);
const vaultFile = await readFromVault(harness, path, content instanceof Blob, fileOptions);
expect(await isDocContentSame(vaultFile, content)).toBe(true);
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
await waitForIdle(harness);
if (skipCheckToBeWritten) {
return Promise.resolve();

View File

@@ -28,12 +28,12 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
});
it("should have services initialized", async () => {
expect(harness.plugin.services).toBeDefined();
expect(harness.plugin.core.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);
expect(harness.plugin.core.localDatabase).toBeDefined();
expect(harness.plugin.core.localDatabase.isReady).toBe(true);
return await Promise.resolve();
});
@@ -54,11 +54,11 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
const readContent = await harness.app.vault.read(file);
expect(readContent).toBe(content);
}
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
await waitForIdle(harness);
// await delay(100); // Wait a bit for the local database to process
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
expect(entry).not.toBe(false);
if (entry) {
expect(readContent(entry)).toBe(content);
@@ -80,10 +80,10 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
const readContent = await harness.app.vault.read(file);
expect(readContent).toBe(content);
}
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
await waitForIdle(harness);
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
expect(entry).not.toBe(false);
if (entry) {
expect(readContent(entry)).toBe(content);
@@ -108,9 +108,9 @@ describe.skip("Plugin Integration Test (Local Database)", async () => {
expect(await isDocContentSame(readContent, content)).toBe(true);
}
await harness.plugin.services.fileProcessing.commitPendingFileEvents();
await harness.plugin.core.services.fileProcessing.commitPendingFileEvents();
await waitForIdle(harness);
const entry = await harness.plugin.localDatabase.getDBEntry(path as FilePath);
const entry = await harness.plugin.core.localDatabase.getDBEntry(path as FilePath);
expect(entry).not.toBe(false);
if (entry) {
const entryContent = await readContent(entry);

View File

@@ -68,7 +68,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
await waitForIdle(harnessInit);
});
afterAll(async () => {
await harnessInit.plugin.services.replicator.getActiveReplicator()?.closeReplication();
await harnessInit.plugin.core.services.replicator.getActiveReplicator()?.closeReplication();
await harnessInit.dispose();
await delay(1000);
});
@@ -81,7 +81,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
it("should be prepared for replication", async () => {
await waitForReady(harnessInit);
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
const status = await harnessInit.plugin.services.replicator
const status = await harnessInit.plugin.core.services.replicator
.getActiveReplicator()
?.getRemoteStatus(sync_test_setting_init);
console.log("Connected devices after reset:", status);
@@ -120,12 +120,12 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
});
it("should have services initialized", () => {
expect(harnessUpload.plugin.services).toBeDefined();
expect(harnessUpload.plugin.core.services).toBeDefined();
});
it("should have local database initialized", () => {
expect(harnessUpload.plugin.localDatabase).toBeDefined();
expect(harnessUpload.plugin.localDatabase.isReady).toBe(true);
expect(harnessUpload.plugin.core.localDatabase).toBeDefined();
expect(harnessUpload.plugin.core.localDatabase.isReady).toBe(true);
});
it("should prepare remote database", async () => {
@@ -138,7 +138,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
const path = nameFile("store", "md", 0);
await testFileWrite(harnessUpload, path, content, false, fileOptions);
// Perform replication
// await harness.plugin.services.replication.replicate(true);
// await harness.plugin.core.services.replication.replicate(true);
});
it("should different content of several files have been created correctly", async () => {
await testFileWrite(harnessUpload, nameFile("test-diff-1", "md", 0), "Content A", false, fileOptions);
@@ -149,7 +149,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
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);
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
if (isTooLarge) {
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
expect(true).toBe(true);
@@ -162,7 +162,7 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
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);
const isTooLarge = harnessUpload.plugin.core.services.vault.isFileSizeTooLarge(size);
if (isTooLarge) {
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
expect(true).toBe(true);
@@ -210,12 +210,12 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
});
it("should have services initialized", () => {
expect(harnessDownload.plugin.services).toBeDefined();
expect(harnessDownload.plugin.core.services).toBeDefined();
});
it("should have local database initialized", () => {
expect(harnessDownload.plugin.localDatabase).toBeDefined();
expect(harnessDownload.plugin.localDatabase.isReady).toBe(true);
expect(harnessDownload.plugin.core.localDatabase).toBeDefined();
expect(harnessDownload.plugin.core.localDatabase.isReady).toBe(true);
});
it("should a file has been synchronised", async () => {
@@ -232,9 +232,9 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
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);
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
if (isTooLarge) {
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
expect(entry).toBe(false);
} else {
@@ -245,9 +245,9 @@ export function syncBasicCase(label: string, { setting, fileOptions }: TestOptio
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);
const isTooLarge = harnessDownload.plugin.core.services.vault.isFileSizeTooLarge(size);
if (isTooLarge) {
const entry = await harnessDownload.plugin.localDatabase.getDBEntry(path as FilePath);
const entry = await harnessDownload.plugin.core.localDatabase.getDBEntry(path as FilePath);
console.log(`Skipping file of size ${size} bytes as it is too large to sync.`);
expect(entry).toBe(false);
} else {

View File

@@ -7,11 +7,11 @@ 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) {
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
// Wait for peers to connect
const maxRetries = 20;
let retries = maxRetries;
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
}
@@ -38,8 +38,8 @@ async function waitForP2PPeers(harness: LiveSyncHarness) {
}
}
export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
if (!(replicator instanceof LiveSyncTrysteroReplicator)) {
throw new Error("Replicator is not an instance of LiveSyncTrysteroReplicator");
}
@@ -58,9 +58,9 @@ export async function closeP2PReplicatorConnections(harness: LiveSyncHarness) {
export async function performReplication(harness: LiveSyncHarness) {
await waitForP2PPeers(harness);
await delay(500);
const p = harness.plugin.services.replication.replicate(true);
const p = harness.plugin.core.services.replication.replicate(true);
const task =
harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P
harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P
? waitTaskWithFollowups(
p,
() => {
@@ -74,17 +74,17 @@ export async function performReplication(harness: LiveSyncHarness) {
: p;
const result = await task;
// await waitForIdle(harness);
// if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
// if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
// await closeP2PReplicatorConnections(harness);
// }
return result;
}
export async function closeReplication(harness: LiveSyncHarness) {
if (harness.plugin.settings.remoteType === RemoteTypes.REMOTE_P2P) {
if (harness.plugin.core.settings.remoteType === RemoteTypes.REMOTE_P2P) {
return await closeP2PReplicatorConnections(harness);
}
const replicator = await harness.plugin.services.replicator.getActiveReplicator();
const replicator = await harness.plugin.core.services.replicator.getActiveReplicator();
if (!replicator) {
console.log("No active replicator to close");
return;
@@ -98,19 +98,21 @@ export async function prepareRemote(harness: LiveSyncHarness, setting: ObsidianL
if (setting.remoteType !== RemoteTypes.REMOTE_P2P) {
if (shouldReset) {
await delay(1000);
await harness.plugin.services.replicator
await harness.plugin.core.services.replicator
.getActiveReplicator()
?.tryResetRemoteDatabase(harness.plugin.settings);
?.tryResetRemoteDatabase(harness.plugin.core.settings);
} else {
await harness.plugin.services.replicator
await harness.plugin.core.services.replicator
.getActiveReplicator()
?.tryCreateRemoteDatabase(harness.plugin.settings);
?.tryCreateRemoteDatabase(harness.plugin.core.settings);
}
await harness.plugin.services.replicator.getActiveReplicator()?.markRemoteResolved(harness.plugin.settings);
// No exceptions should be thrown
const status = await harness.plugin.services.replicator
await harness.plugin.core.services.replicator
.getActiveReplicator()
?.getRemoteStatus(harness.plugin.settings);
?.markRemoteResolved(harness.plugin.core.settings);
// No exceptions should be thrown
const status = await harness.plugin.core.services.replicator
.getActiveReplicator()
?.getRemoteStatus(harness.plugin.core.settings);
console.log("Remote status:", status);
expect(status).not.toBeFalsy();
}

View File

@@ -69,7 +69,7 @@ describe("Dialog Tests", async () => {
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 result = harness.plugin.core.services.UI.promptCopyToClipboard(title, testString);
const isDialogShown = await waitForDialogShown(title, 500);
expect(isDialogShown).toBe(true);
const copyButton = page.getByText("📋");

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