From 0dfd42259deccd283def3b44344c1f7c809b6bf1 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Wed, 11 Mar 2026 05:47:00 +0100 Subject: [PATCH] 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. --- .gitignore | 3 +- src/LiveSyncBaseCore.ts | 287 +++++++++++ src/apps/webapp/.gitignore | 4 + .../webapp/adapters/FSAPIConversionAdapter.ts | 34 ++ .../webapp/adapters/FSAPIFileSystemAdapter.ts | 214 ++++++++ src/apps/webapp/adapters/FSAPIPathAdapter.ts | 18 + .../webapp/adapters/FSAPIStorageAdapter.ts | 210 ++++++++ .../webapp/adapters/FSAPITypeGuardAdapter.ts | 17 + src/apps/webapp/adapters/FSAPITypes.ts | 24 + src/apps/webapp/adapters/FSAPIVaultAdapter.ts | 123 +++++ src/apps/webapp/index.html | 209 ++++++++ src/apps/webapp/main.ts | 345 +++++++++++++ .../FSAPIStorageEventManagerAdapter.ts | 281 +++++++++++ .../managers/StorageEventManagerFSAPI.ts | 39 ++ src/apps/webapp/package.json | 21 + .../serviceModules/DatabaseFileAccess.ts | 15 + .../serviceModules/FSAPIServiceModules.ts | 104 ++++ .../webapp/serviceModules/FileAccessFSAPI.ts | 20 + .../serviceModules/ServiceFileAccessImpl.ts | 15 + src/apps/webapp/svelte.config.js | 7 + src/apps/webapp/tsconfig.json | 32 ++ src/apps/webapp/vite.config.ts | 34 ++ src/apps/webpeer/src/P2PReplicatorShim.ts | 76 ++- src/apps/webpeer/src/SyncMain.svelte | 2 +- src/common/PeriodicProcessor.ts | 45 ++ src/common/events.ts | 4 +- src/common/utils.ts | 51 +- src/features/ConfigSync/CmdConfigSync.ts | 109 ++-- src/features/ConfigSync/PluginCombo.svelte | 7 +- src/features/ConfigSync/PluginDialogModal.ts | 2 +- src/features/ConfigSync/PluginPane.svelte | 33 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 108 ++-- src/features/LiveSyncCommands.ts | 17 +- .../CmdLocalDatabaseMainte.ts | 22 +- src/features/P2PSync/CmdP2PReplicator.ts | 29 +- .../P2PReplicator/P2PReplicatorPane.svelte | 56 ++- .../P2PReplicator/P2PReplicatorPaneView.ts | 43 +- src/lib | 2 +- src/main.ts | 467 ++++-------------- src/managers/StorageEventManagerObsidian.ts | 10 +- src/modules/AbstractModule.ts | 16 +- src/modules/core/ModulePeriodicProcess.ts | 2 +- src/modules/core/ModuleReplicator.ts | 3 +- src/modules/core/ReplicateResultProcessor.ts | 8 +- .../coreFeatures/ModuleConflictResolver.ts | 12 +- src/modules/essential/ModuleBasicMenu.ts | 86 ++++ .../essential/ModuleInitializerFile.ts | 4 +- src/modules/essential/ModuleMigration.ts | 6 +- .../ModuleCheckRemoteSize_obsolete.ts | 9 +- .../essentialObsidian/ModuleObsidianMenu.ts | 83 +--- src/modules/extras/ModuleReplicateTest.ts | 7 +- src/modules/extras/devUtil/TestPane.svelte | 11 +- src/modules/extras/devUtil/testUtils.ts | 4 +- src/modules/extras/devUtil/tests.ts | 8 +- .../DocumentHistory/DocumentHistoryModal.ts | 12 +- .../GlobalHistory/GlobalHistory.svelte | 12 +- .../GlobalHistory/GlobalHistoryView.ts | 1 + src/modules/features/ModuleLog.ts | 7 +- .../features/ModuleObsidianDocumentHistory.ts | 2 +- .../features/ModuleObsidianSettingTab.ts | 3 +- .../ObsidianLiveSyncSettingTab.ts | 45 +- .../features/SettingDialogue/PaneHatch.ts | 56 +-- .../SettingDialogue/PaneMaintenance.ts | 16 +- .../features/SettingDialogue/PanePatches.ts | 22 +- .../SettingDialogue/PaneRemoteConfig.ts | 12 +- .../features/SettingDialogue/PaneSetup.ts | 6 +- .../SettingDialogue/PaneSyncSettings.ts | 2 +- src/modules/main/ModuleLiveSyncMain.ts | 2 +- src/modules/services/ObsidianAPIService.ts | 14 + .../ObsidianFileSystemAdapter.ts | 12 +- test/harness/harness.ts | 14 +- test/suite/db_common.ts | 8 +- test/suite/onlylocaldb.test.ts | 18 +- test/suite/sync.senario.basic.ts | 30 +- test/suite/sync_common.ts | 36 +- test/unit/dialog.test.ts | 2 +- updates.md | 28 +- 77 files changed, 2849 insertions(+), 909 deletions(-) create mode 100644 src/LiveSyncBaseCore.ts create mode 100644 src/apps/webapp/.gitignore create mode 100644 src/apps/webapp/adapters/FSAPIConversionAdapter.ts create mode 100644 src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts create mode 100644 src/apps/webapp/adapters/FSAPIPathAdapter.ts create mode 100644 src/apps/webapp/adapters/FSAPIStorageAdapter.ts create mode 100644 src/apps/webapp/adapters/FSAPITypeGuardAdapter.ts create mode 100644 src/apps/webapp/adapters/FSAPITypes.ts create mode 100644 src/apps/webapp/adapters/FSAPIVaultAdapter.ts create mode 100644 src/apps/webapp/index.html create mode 100644 src/apps/webapp/main.ts create mode 100644 src/apps/webapp/managers/FSAPIStorageEventManagerAdapter.ts create mode 100644 src/apps/webapp/managers/StorageEventManagerFSAPI.ts create mode 100644 src/apps/webapp/package.json create mode 100644 src/apps/webapp/serviceModules/DatabaseFileAccess.ts create mode 100644 src/apps/webapp/serviceModules/FSAPIServiceModules.ts create mode 100644 src/apps/webapp/serviceModules/FileAccessFSAPI.ts create mode 100644 src/apps/webapp/serviceModules/ServiceFileAccessImpl.ts create mode 100644 src/apps/webapp/svelte.config.js create mode 100644 src/apps/webapp/tsconfig.json create mode 100644 src/apps/webapp/vite.config.ts create mode 100644 src/common/PeriodicProcessor.ts create mode 100644 src/modules/essential/ModuleBasicMenu.ts diff --git a/.gitignore b/.gitignore index 51ae124..43e267e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ data.json cov_profile/** -coverage \ No newline at end of file +coverage +src/apps/cli/dist/* \ No newline at end of file diff --git a/src/LiveSyncBaseCore.ts b/src/LiveSyncBaseCore.ts new file mode 100644 index 0000000..1f822bd --- /dev/null +++ b/src/LiveSyncBaseCore.ts @@ -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 +{ + 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(cls: string) { + for (const addon of this.addOns) { + if (addon.constructor.name == cls) return addon as T; + } + return undefined; + } + + constructor( + serviceHub: InjectableServiceHub, + serviceModuleInitialiser: ( + core: LiveSyncBaseCore, + serviceHub: InjectableServiceHub + ) => ServiceModules, + extraModuleInitialiser: (core: LiveSyncBaseCore) => AbstractModule[], + addOnsInitialiser: (core: LiveSyncBaseCore) => TCommands[], + featuresInitialiser: (core: LiveSyncBaseCore) => 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 | 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(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 { + 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; + } + + /** + * @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(serviceHub: InjectableServiceHub) { + // 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; + constructor: { name: string }; +} diff --git a/src/apps/webapp/.gitignore b/src/apps/webapp/.gitignore new file mode 100644 index 0000000..7ac8ea1 --- /dev/null +++ b/src/apps/webapp/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.DS_Store +*.log diff --git a/src/apps/webapp/adapters/FSAPIConversionAdapter.ts b/src/apps/webapp/adapters/FSAPIConversionAdapter.ts new file mode 100644 index 0000000..7acd106 --- /dev/null +++ b/src/apps/webapp/adapters/FSAPIConversionAdapter.ts @@ -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 { + 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, + }; + } +} diff --git a/src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts b/src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts new file mode 100644 index 0000000..1c87b46 --- /dev/null +++ b/src/apps/webapp/adapters/FSAPIFileSystemAdapter.ts @@ -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 { + readonly path: FSAPIPathAdapter; + readonly typeGuard: FSAPITypeGuardAdapter; + readonly conversion: FSAPIConversionAdapter; + readonly storage: FSAPIStorageAdapter; + readonly vault: FSAPIVaultAdapter; + + private fileCache = new Map(); + private handleCache = new Map(); + + 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 { + 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 { + 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 { + 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 { + if (this.fileCache.size === 0) { + await this.scanDirectory(); + } + return Array.from(this.fileCache.values()); + } + + async statFromNative(file: FSAPIFile): Promise { + // 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 { + // 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 { + 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 { + 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(); + } +} diff --git a/src/apps/webapp/adapters/FSAPIPathAdapter.ts b/src/apps/webapp/adapters/FSAPIPathAdapter.ts new file mode 100644 index 0000000..640f3bf --- /dev/null +++ b/src/apps/webapp/adapters/FSAPIPathAdapter.ts @@ -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 { + 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, ""); + } +} diff --git a/src/apps/webapp/adapters/FSAPIStorageAdapter.ts b/src/apps/webapp/adapters/FSAPIStorageAdapter.ts new file mode 100644 index 0000000..ec4fb81 --- /dev/null +++ b/src/apps/webapp/adapters/FSAPIStorageAdapter.ts @@ -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 { + 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 { + 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 { + 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 { + const fileHandle = await this.getFileHandle(p); + if (fileHandle) return true; + + const dirHandle = await this.getDirectoryHandle(p); + return dirHandle !== null; + } + + async trystat(p: string): Promise { + // 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 { + return await this.trystat(p); + } + + async mkdir(p: string): Promise { + 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 { + const resolved = await this.resolvePath(p); + if (!resolved) return; + + await resolved.dirHandle.removeEntry(resolved.fileName, { recursive: true }); + } + + async read(p: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 }; + } +} diff --git a/src/apps/webapp/adapters/FSAPITypeGuardAdapter.ts b/src/apps/webapp/adapters/FSAPITypeGuardAdapter.ts new file mode 100644 index 0000000..49e34a8 --- /dev/null +++ b/src/apps/webapp/adapters/FSAPITypeGuardAdapter.ts @@ -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 { + 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; + } +} diff --git a/src/apps/webapp/adapters/FSAPITypes.ts b/src/apps/webapp/adapters/FSAPITypes.ts new file mode 100644 index 0000000..fd696a9 --- /dev/null +++ b/src/apps/webapp/adapters/FSAPITypes.ts @@ -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; diff --git a/src/apps/webapp/adapters/FSAPIVaultAdapter.ts b/src/apps/webapp/adapters/FSAPIVaultAdapter.ts new file mode 100644 index 0000000..c709e47 --- /dev/null +++ b/src/apps/webapp/adapters/FSAPIVaultAdapter.ts @@ -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 { + constructor(private rootHandle: FileSystemDirectoryHandle) {} + + async read(file: FSAPIFile): Promise { + const fileObject = await file.handle.getFile(); + return await fileObject.text(); + } + + async cachedRead(file: FSAPIFile): Promise { + // No caching in webapp version, just read directly + return await this.read(file); + } + + async readBinary(file: FSAPIFile): Promise { + const fileObject = await file.handle.getFile(); + return await fileObject.arrayBuffer(); + } + + async modify(file: FSAPIFile, data: string, options?: UXDataWriteOptions): Promise { + const writable = await file.handle.createWritable(); + await writable.write(data); + await writable.close(); + } + + async modifyBinary(file: FSAPIFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise { + const writable = await file.handle.createWritable(); + await writable.write(data); + await writable.close(); + } + + async create(p: string, data: string, options?: UXDataWriteOptions): Promise { + 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 { + 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 { + 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 { + // 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; + } +} diff --git a/src/apps/webapp/index.html b/src/apps/webapp/index.html new file mode 100644 index 0000000..72acdc3 --- /dev/null +++ b/src/apps/webapp/index.html @@ -0,0 +1,209 @@ + + + + + + Self-hosted LiveSync WebApp + + + +
+

🔄 Self-hosted LiveSync

+

Browser-based Self-hosted LiveSync using FileSystem API

+ +
+ Initialising... +
+ +
+

About This Application

+
    +
  • Runs entirely in your browser
  • +
  • Uses FileSystem API to access your local vault
  • +
  • Syncs with CouchDB server (like Obsidian plugin)
  • +
  • Settings stored in .livesync/settings.json
  • +
  • Real-time file watching with FileSystemObserver (Chrome 124+)
  • +
+
+ +
+

How to Use

+
    +
  • Grant directory access when prompted
  • +
  • Create .livesync/settings.json in your vault folder. (Compatible with Obsidian's Self-hosted LiveSync)
  • +
  • Add your CouchDB connection details
  • +
  • Your files will be synced automatically
  • +
+
+ + + + +
+ + + + diff --git a/src/apps/webapp/main.ts b/src/apps/webapp/main.ts new file mode 100644 index 0000000..b723395 --- /dev/null +++ b/src/apps/webapp/main.ts @@ -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 = { + 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 | null = null; + private serviceHub: BrowserServiceHub | 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(); + + // Setup API service + (this.serviceHub.API as BrowserAPIService).getSystemVaultName.setHandler( + () => this.rootHandle?.name || "livesync-webapp" + ); + + // Setup settings handlers - save to .livesync folder + const settingService = this.serviceHub.setting as InjectableSettingService; + + 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 => { + 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 { + 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 | 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 { + try { + const db = await this.openHandleDB(); + const transaction = db.transaction(["handles"], "readonly"); + const store = transaction.objectStore("handles"); + const handle = await new Promise((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 { + 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; diff --git a/src/apps/webapp/managers/FSAPIStorageEventManagerAdapter.ts b/src/apps/webapp/managers/FSAPIStorageEventManagerAdapter.ts new file mode 100644 index 0000000..5977c58 --- /dev/null +++ b/src/apps/webapp/managers/FSAPIStorageEventManagerAdapter.ts @@ -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 { + 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 { + 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 { + try { + const db = await this.openDB(); + const transaction = db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + + await new Promise((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 { + 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 { + // 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 { + 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 { + 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(); + } +} diff --git a/src/apps/webapp/managers/StorageEventManagerFSAPI.ts b/src/apps/webapp/managers/StorageEventManagerFSAPI.ts new file mode 100644 index 0000000..1bbc483 --- /dev/null +++ b/src/apps/webapp/managers/StorageEventManagerFSAPI.ts @@ -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 { + core: LiveSyncBaseCore; + private fsapiAdapter: FSAPIStorageEventManagerAdapter; + + constructor( + rootHandle: FileSystemDirectoryHandle, + core: LiveSyncBaseCore, + 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?.(); + } + } +} diff --git a/src/apps/webapp/package.json b/src/apps/webapp/package.json new file mode 100644 index 0000000..0e1f5b1 --- /dev/null +++ b/src/apps/webapp/package.json @@ -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" + } +} diff --git a/src/apps/webapp/serviceModules/DatabaseFileAccess.ts b/src/apps/webapp/serviceModules/DatabaseFileAccess.ts new file mode 100644 index 0000000..6365042 --- /dev/null +++ b/src/apps/webapp/serviceModules/DatabaseFileAccess.ts @@ -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); + } +} diff --git a/src/apps/webapp/serviceModules/FSAPIServiceModules.ts b/src/apps/webapp/serviceModules/FSAPIServiceModules.ts new file mode 100644 index 0000000..152422c --- /dev/null +++ b/src/apps/webapp/serviceModules/FSAPIServiceModules.ts @@ -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, + services: InjectableServiceHub +): 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, + }; +} diff --git a/src/apps/webapp/serviceModules/FileAccessFSAPI.ts b/src/apps/webapp/serviceModules/FileAccessFSAPI.ts new file mode 100644 index 0000000..45e5660 --- /dev/null +++ b/src/apps/webapp/serviceModules/FileAccessFSAPI.ts @@ -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 { + 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; + } +} diff --git a/src/apps/webapp/serviceModules/ServiceFileAccessImpl.ts b/src/apps/webapp/serviceModules/ServiceFileAccessImpl.ts new file mode 100644 index 0000000..4b6a474 --- /dev/null +++ b/src/apps/webapp/serviceModules/ServiceFileAccessImpl.ts @@ -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 { + constructor(services: StorageAccessBaseDependencies) { + super(services); + } +} diff --git a/src/apps/webapp/svelte.config.js b/src/apps/webapp/svelte.config.js new file mode 100644 index 0000000..9a3adfb --- /dev/null +++ b/src/apps/webapp/svelte.config.js @@ -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(), +}; diff --git a/src/apps/webapp/tsconfig.json b/src/apps/webapp/tsconfig.json new file mode 100644 index 0000000..f79193a --- /dev/null +++ b/src/apps/webapp/tsconfig.json @@ -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"] +} diff --git a/src/apps/webapp/vite.config.ts b/src/apps/webapp/vite.config.ts new file mode 100644 index 0000000..ea99b14 --- /dev/null +++ b/src/apps/webapp/vite.config.ts @@ -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, + }, +}); diff --git a/src/apps/webpeer/src/P2PReplicatorShim.ts b/src/apps/webpeer/src/P2PReplicatorShim.ts index 73fe39a..735e572 100644 --- a/src/apps/webpeer/src/P2PReplicatorShim.ts +++ b/src/apps/webpeer/src/P2PReplicatorShim.ts @@ -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).getSystemVaultName.setHandler( () => "p2p-livesync-web-peer" ); + this.services.API.addLog.setHandler(Logger); + const repStore = SimpleStoreIDBv2.open("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).saveData.setHandler(async (data) => { + await repStore.set("settings", data); + eventHub.emitEvent(EVENT_SETTING_SAVED, data); + }); + (this.services.setting as InjectableSettingService).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("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); } } diff --git a/src/apps/webpeer/src/SyncMain.svelte b/src/apps/webpeer/src/SyncMain.svelte index 853ce86..538fff3 100644 --- a/src/apps/webpeer/src/SyncMain.svelte +++ b/src/apps/webpeer/src/SyncMain.svelte @@ -27,7 +27,7 @@
{#await synchronised then cmdSync} - + {:catch error}

{error.message}

{/await} diff --git a/src/common/PeriodicProcessor.ts b/src/common/PeriodicProcessor.ts new file mode 100644 index 0000000..04d92ba --- /dev/null +++ b/src/common/PeriodicProcessor.ts @@ -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; + _timer?: number = undefined; + _core: PeriodicProcessorHost; + constructor(core: PeriodicProcessorHost, process: () => Promise) { + // 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; + } + } +} diff --git a/src/common/events.ts b/src/common/events.ts index 0372882..e34346f 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -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"; @@ -29,7 +29,7 @@ export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3"; 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; diff --git a/src/common/utils.ts b/src/common/utils.ts index a64c26e..eacc758 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -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; - _timer?: number = undefined; - _plugin: ObsidianLiveSyncPlugin; - constructor(plugin: ObsidianLiveSyncPlugin, process: () => Promise) { - 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, @@ -373,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; } @@ -446,3 +397,5 @@ export function onlyInNTimes(n: number, proc: (progress: number) => any) { } }; } + +export { displayRev } from "@lib/common/utils.ts"; diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 0bc732b..c98142c 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -50,7 +50,6 @@ import { LiveSyncCommands } from "../LiveSyncCommands.ts"; import { stripAllPrefixes } from "../../lib/src/string_and_binary/path.ts"; import { EVEN, - PeriodicProcessor, disposeMemoObject, isCustomisationSyncMetadata, isPluginMetadata, @@ -59,6 +58,7 @@ import { 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"; @@ -392,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() { @@ -439,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", @@ -447,7 +450,7 @@ export class ConfigSync extends LiveSyncCommands { ` ); - this.plugin.addCommand({ + this.services.API.addCommand({ id: "livesync-plugin-dialog-ex", name: "Show customization sync dialog", callback: () => { @@ -464,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") || @@ -485,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) != ""; } @@ -854,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); @@ -996,16 +998,16 @@ export class ConfigSync extends LiveSyncCommands { } } async applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise { - 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; @@ -1015,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); @@ -1031,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); @@ -1046,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()); @@ -1065,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); @@ -1078,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) { @@ -1172,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) => { @@ -1230,13 +1232,13 @@ export class ConfigSync extends LiveSyncCommands { recentProcessedInternalFiles = [] as string[]; async makeEntryFromFile(path: FilePath): Promise { - 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); @@ -1265,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, @@ -1280,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 { @@ -1504,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) @@ -1674,7 +1676,7 @@ export class ConfigSync extends LiveSyncCommands { } async scanInternalFiles(): Promise { - 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[]; @@ -1705,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", @@ -1728,13 +1730,13 @@ export class ConfigSync extends LiveSyncCommands { } private _allSuspendExtraSync(): Promise { - 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); } @@ -1745,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"; @@ -1777,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); } } diff --git a/src/features/ConfigSync/PluginCombo.svelte b/src/features/ConfigSync/PluginCombo.svelte index ceb9256..e2b7dc4 100644 --- a/src/features/ConfigSync/PluginCombo.svelte +++ b/src/features/ConfigSync/PluginCombo.svelte @@ -30,7 +30,8 @@ export let plugin: ObsidianLiveSyncPlugin; export let isMaintenanceMode: boolean = false; export let isFlagged: boolean = false; - const addOn = plugin.getAddOn(ConfigSync.name)!; + $: core = plugin.core; + const addOn = plugin.core.getAddOn(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)); } diff --git a/src/features/ConfigSync/PluginDialogModal.ts b/src/features/ConfigSync/PluginDialogModal.ts index d75043b..08825ba 100644 --- a/src/features/ConfigSync/PluginDialogModal.ts +++ b/src/features/ConfigSync/PluginDialogModal.ts @@ -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 }, }); } } diff --git a/src/features/ConfigSync/PluginPane.svelte b/src/features/ConfigSync/PluginPane.svelte index a7a2e61..5b0a167 100644 --- a/src/features/ConfigSync/PluginPane.svelte +++ b/src/features/ConfigSync/PluginPane.svelte @@ -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.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.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(); // 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 = {}; 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;
diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 2ada77e..d20ae55 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -30,7 +30,6 @@ import { import { compareMTime, isInternalMetadata, - PeriodicProcessor, TARGET_IS_NEW, scheduleTask, getLogLevel, @@ -41,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"; @@ -78,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.localDatabase.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: () => { @@ -105,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: () => { @@ -114,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: () => { @@ -123,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: () => { @@ -267,7 +267,7 @@ export class HiddenFileSync extends LiveSyncCommands { } async loadFileWithInfo(path: FilePath): Promise { - const stat = await this.plugin.storageAccess.statHidden(path); + const stat = await this.core.storageAccess.statHidden(path); if (!stat) return { name: path.split("/").pop() ?? "", @@ -282,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, @@ -304,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); } @@ -318,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)); } @@ -371,27 +371,27 @@ export class HiddenFileSync extends LiveSyncCommands { 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 { - 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"; } @@ -404,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); @@ -518,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 @@ -560,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; @@ -815,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) { @@ -894,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)) { @@ -902,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]); } @@ -941,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)) { @@ -1058,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}`); @@ -1206,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) => { @@ -1238,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) => { @@ -1258,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); @@ -1296,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); @@ -1616,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; @@ -1670,7 +1670,7 @@ Offline Changed files: ${files.length}`; async __checkIsNeedToWriteFile(storageFilePath: FilePath, content: string | ArrayBuffer): Promise { 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) { @@ -1682,7 +1682,7 @@ Offline Changed files: ${files.length}`; async __writeFile(storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise { 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); @@ -1768,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, @@ -1787,12 +1787,12 @@ ${messageFetch}${messageOverwrite}${messageMerge} } private _allSuspendExtraSync(): Promise { - 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); } @@ -1815,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); @@ -1828,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 @@ -1851,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[] = []; @@ -1956,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)); } } diff --git a/src/features/LiveSyncCommands.ts b/src/features/LiveSyncCommands.ts index 17978ac..2f79fc9 100644 --- a/src/features/LiveSyncCommands.ts +++ b/src/features/LiveSyncCommands.ts @@ -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; _isMainReady() { - return this.plugin.services.appLifecycle.isReady(); + return this.services.appLifecycle.isReady(); } _isMainSuspended() { return this.services.appLifecycle.isSuspended(); diff --git a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts index d8fd482..1f75385 100644 --- a/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts +++ b/src/features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts @@ -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>(DB_KEY_CHUNK_SET)) || new Set(); const chunkUsageMap = (await kvDB.get(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(DB_KEY_SEQ); const chunkSet = (await kvDB.get>(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, }); diff --git a/src/features/P2PSync/CmdP2PReplicator.ts b/src/features/P2PSync/CmdP2PReplicator.ts index e9b2125..fbbd37b 100644 --- a/src/features/P2PSync/CmdP2PReplicator.ts +++ b/src/features/P2PSync/CmdP2PReplicator.ts @@ -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; @@ -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 = {}): Promise { 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 { - 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", diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte b/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte index a0382bb..7ac67eb 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2PReplicatorPane.svelte @@ -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")!; setContext("getReplicator", () => cmdSync); - - const initialSettings = { ...plugin.settings }; + const currentSettings = () => core.services.setting.currentSettings() as P2PSyncSetting; + const initialSettings = { ...currentSettings() } as P2PSyncSetting; let settings = $state(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(undefined); let replicatorInfo = $state(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"; }); diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.ts b/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.ts index 806da90..fcde9a2 100644 --- a/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.ts +++ b/src/features/P2PSync/P2PReplicator/P2PReplicatorPaneView.ts @@ -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.name); + const r = this.core.getAddOn(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.name); + const cmdSync = this.core.getAddOn(P2PReplicator.name); if (!cmdSync) { throw new Error("Replicator not found"); } return mount(ReplicatorPaneComponent, { target: target, props: { - plugin: cmdSync.plugin, cmdSync: cmdSync, + core: this.core, }, }); } diff --git a/src/lib b/src/lib index 27d1d4a..7989f57 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 27d1d4a6e727a42307fe10c35d618864302fbbf7 +Subproject commit 7989f57e06c6858e3a99ebde02ec71d6a7811dbf diff --git a/src/main.ts b/src/main.ts index 3d63841..807ec55 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,396 +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 { useCheckRemoteSize } from "./lib/src/serviceFeatures/checkRemoteSize.ts"; -import { useRedFlagFeatures } from "./serviceFeatures/redFlag.ts"; -import { useOfflineScanner } from "./lib/src/serviceFeatures/offlineScanner.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; +export default class ObsidianLiveSyncPlugin extends Plugin { + core: LiveSyncCore; -export default class ObsidianLiveSyncPlugin - extends Plugin - implements - LiveSyncLocalDBEnv, - LiveSyncReplicatorEnv, - LiveSyncJournalReplicatorEnv, - LiveSyncCouchDBReplicatorEnv, - HasSettings -{ - /** - * The service hub for managing all services. - */ - _services: InjectableServiceHub | 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(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(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.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 { - 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; - } - - /** - * @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; - - // 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, + services: InjectableServiceHub + ): 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, - pathService: this.services.path, + 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, @@ -404,24 +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); - } - useRedFlagFeatures(this); - useOfflineScanner(this); - - // enable target filter feature. - useTargetFilters(this); - useCheckRemoteSize(this); + await this.core.services.setting.saveSettingData(); } constructor(app: App, manifest: PluginManifest) { @@ -429,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; diff --git a/src/managers/StorageEventManagerObsidian.ts b/src/managers/StorageEventManagerObsidian.ts index 979ccdc..d57f204 100644 --- a/src/managers/StorageEventManagerObsidian.ts +++ b/src/managers/StorageEventManagerObsidian.ts @@ -1,4 +1,3 @@ -import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync"; import type { FilePath } from "@lib/common/types"; import type ObsidianLiveSyncPlugin from "@/main"; import type { LiveSyncCore } from "@/main"; @@ -6,18 +5,15 @@ import { StorageEventManagerBase, type StorageEventManagerBaseDependencies } fro import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter"; export class StorageEventManagerObsidian extends StorageEventManagerBase { - plugin: ObsidianLiveSyncPlugin; core: LiveSyncCore; // Necessary evil. - cmdHiddenFileSync: HiddenFileSync; + // cmdHiddenFileSync: HiddenFileSync; constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) { const adapter = new ObsidianStorageEventManagerAdapter(plugin, core, dependencies.fileProcessing); super(adapter, dependencies); - this.plugin = plugin; this.core = core; - this.cmdHiddenFileSync = this.plugin.getAddOn(HiddenFileSync.name) as HiddenFileSync; } /** @@ -26,12 +22,12 @@ export class StorageEventManagerObsidian extends StorageEventManagerBase = 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); diff --git a/src/modules/core/ModulePeriodicProcess.ts b/src/modules/core/ModulePeriodicProcess.ts index a50a95e..2eef12f 100644 --- a/src/modules/core/ModulePeriodicProcess.ts +++ b/src/modules/core/ModulePeriodicProcess.ts @@ -1,4 +1,4 @@ -import { PeriodicProcessor } from "../../common/utils"; +import { PeriodicProcessor } from "@/common/PeriodicProcessor"; import type { LiveSyncCore } from "../../main"; import { AbstractModule } from "../AbstractModule"; diff --git a/src/modules/core/ModuleReplicator.ts b/src/modules/core/ModuleReplicator.ts index 0150fbe..79b7b49 100644 --- a/src/modules/core/ModuleReplicator.ts +++ b/src/modules/core/ModuleReplicator.ts @@ -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"; diff --git a/src/modules/core/ReplicateResultProcessor.ts b/src/modules/core/ReplicateResultProcessor.ts index 1fe9a53..fb35a5b 100644 --- a/src/modules/core/ReplicateResultProcessor.ts +++ b/src/modules/core/ReplicateResultProcessor.ts @@ -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); diff --git a/src/modules/coreFeatures/ModuleConflictResolver.ts b/src/modules/coreFeatures/ModuleConflictResolver.ts index f09271a..ebce9d4 100644 --- a/src/modules/coreFeatures/ModuleConflictResolver.ts +++ b/src/modules/coreFeatures/ModuleConflictResolver.ts @@ -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) { diff --git a/src/modules/essential/ModuleBasicMenu.ts b/src/modules/essential/ModuleBasicMenu.ts new file mode 100644 index 0000000..5728be8 --- /dev/null +++ b/src/modules/essential/ModuleBasicMenu.ts @@ -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 { + 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)); + } +} diff --git a/src/modules/essential/ModuleInitializerFile.ts b/src/modules/essential/ModuleInitializerFile.ts index b122b94..b26ac88 100644 --- a/src/modules/essential/ModuleInitializerFile.ts +++ b/src/modules/essential/ModuleInitializerFile.ts @@ -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 { diff --git a/src/modules/essential/ModuleMigration.ts b/src/modules/essential/ModuleMigration.ts index edd172c..e19dc39 100644 --- a/src/modules/essential/ModuleMigration.ts +++ b/src/modules/essential/ModuleMigration.ts @@ -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; diff --git a/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts b/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts index b27c286..7d1c9a3 100644 --- a/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts +++ b/src/modules/essentialObsidian/ModuleCheckRemoteSize_obsolete.ts @@ -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 } diff --git a/src/modules/essentialObsidian/ModuleObsidianMenu.ts b/src/modules/essentialObsidian/ModuleObsidianMenu.ts index cab9093..40597a0 100644 --- a/src/modules/essentialObsidian/ModuleObsidianMenu.ts +++ b/src/modules/essentialObsidian/ModuleObsidianMenu.ts @@ -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 { // 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); } diff --git a/src/modules/extras/ModuleReplicateTest.ts b/src/modules/extras/ModuleReplicateTest.ts index 999bdce..394042a 100644 --- a/src/modules/extras/ModuleReplicateTest.ts +++ b/src/modules/extras/ModuleReplicateTest.ts @@ -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); diff --git a/src/modules/extras/devUtil/TestPane.svelte b/src/modules/extras/devUtil/TestPane.svelte index f7d6bbb..6ea0eea 100644 --- a/src/modules/extras/devUtil/TestPane.svelte +++ b/src/modules/extras/devUtil/TestPane.svelte @@ -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; } diff --git a/src/modules/extras/devUtil/testUtils.ts b/src/modules/extras/devUtil/testUtils.ts index 6d3e079..6d1e86e 100644 --- a/src/modules/extras/devUtil/testUtils.ts +++ b/src/modules/extras/devUtil/testUtils.ts @@ -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 { diff --git a/src/modules/extras/devUtil/tests.ts b/src/modules/extras/devUtil/tests.ts index ce5b0ba..207bf89 100644 --- a/src/modules/extras/devUtil/tests.ts +++ b/src/modules/extras/devUtil/tests.ts @@ -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 () => { diff --git a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts index 00bcf64..f28f263 100644 --- a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -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(); }); diff --git a/src/modules/features/GlobalHistory/GlobalHistory.svelte b/src/modules/features/GlobalHistory/GlobalHistory.svelte index a30177b..f150e72 100644 --- a/src/modules/features/GlobalHistory/GlobalHistory.svelte +++ b/src/modules/features/GlobalHistory/GlobalHistory.svelte @@ -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 { 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); diff --git a/src/modules/features/GlobalHistory/GlobalHistoryView.ts b/src/modules/features/GlobalHistory/GlobalHistoryView.ts index 0edcfe4..63ef474 100644 --- a/src/modules/features/GlobalHistory/GlobalHistoryView.ts +++ b/src/modules/features/GlobalHistory/GlobalHistoryView.ts @@ -11,6 +11,7 @@ export class GlobalHistoryView extends SvelteItemView { target: target, props: { plugin: this.plugin, + core: this.plugin.core, }, }); } diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index 7d9c3e9..d04f9c3 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -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); diff --git a/src/modules/features/ModuleObsidianDocumentHistory.ts b/src/modules/features/ModuleObsidianDocumentHistory.ts index d1182fc..1e6a8bb 100644 --- a/src/modules/features/ModuleObsidianDocumentHistory.ts +++ b/src/modules/features/ModuleObsidianDocumentHistory.ts @@ -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() { diff --git a/src/modules/features/ModuleObsidianSettingTab.ts b/src/modules/features/ModuleObsidianSettingTab.ts index 52e8ccd..29c4862 100644 --- a/src/modules/features/ModuleObsidianSettingTab.ts +++ b/src/modules/features/ModuleObsidianSettingTab.ts @@ -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)); } } diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 23c65bc..6d5d5e9 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -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(); diff --git a/src/modules/features/SettingDialogue/PaneHatch.ts b/src/modules/features/SettingDialogue/PaneHatch.ts index 2898afa..92b9326 100644 --- a/src/modules/features/SettingDialogue/PaneHatch.ts +++ b/src/modules/features/SettingDialogue/PaneHatch.ts @@ -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.name); + const addOn = this.core.getAddOn(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.name); + const addOn = this.core.getAddOn(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.`, diff --git a/src/modules/features/SettingDialogue/PaneMaintenance.ts b/src/modules/features/SettingDialogue/PaneMaintenance.ts index 4a9838a..71ac0d7 100644 --- a/src/modules/features/SettingDialogue/PaneMaintenance.ts +++ b/src/modules/features/SettingDialogue/PaneMaintenance.ts @@ -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"); diff --git a/src/modules/features/SettingDialogue/PanePatches.ts b/src/modules/features/SettingDialogue/PanePatches.ts index 906127e..0631bbd 100644 --- a/src/modules/features/SettingDialogue/PanePatches.ts +++ b/src/modules/features/SettingDialogue/PanePatches.ts @@ -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, }); 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, { diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index cd3473e..216fa47 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -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, diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts index 0b131a1..de996bc 100644 --- a/src/modules/features/SettingDialogue/PaneSetup.ts +++ b/src/modules/features/SettingDialogue/PaneSetup.ts @@ -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(); diff --git a/src/modules/features/SettingDialogue/PaneSyncSettings.ts b/src/modules/features/SettingDialogue/PaneSyncSettings.ts index aba3296..394c7c1 100644 --- a/src/modules/features/SettingDialogue/PaneSyncSettings.ts +++ b/src/modules/features/SettingDialogue/PaneSyncSettings.ts @@ -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", diff --git a/src/modules/main/ModuleLiveSyncMain.ts b/src/modules/main/ModuleLiveSyncMain.ts index 3618c61..0dec4ff 100644 --- a/src/modules/main/ModuleLiveSyncMain.ts +++ b/src/modules/main/ModuleLiveSyncMain.ts @@ -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); diff --git a/src/modules/services/ObsidianAPIService.ts b/src/modules/services/ObsidianAPIService.ts index 6ec05c0..f543c30 100644 --- a/src/modules/services/ObsidianAPIService.ts +++ b/src/modules/services/ObsidianAPIService.ts @@ -171,4 +171,18 @@ export class ObsidianAPIService extends InjectableAPIService 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; + } } diff --git a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts index d0c261e..944a007 100644 --- a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts +++ b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts @@ -42,16 +42,16 @@ export class ObsidianFileSystemAdapter implements IFileSystemAdapter { + return Promise.resolve(this.app.vault.getAbstractFileByPath(path)); } - getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null { - return this.app.vault.getAbstractFileByPathInsensitive(path); + getAbstractFileByPathInsensitive(path: FilePath | string): Promise { + return Promise.resolve(this.app.vault.getAbstractFileByPathInsensitive(path)); } - getFiles(): TFile[] { - return this.app.vault.getFiles(); + getFiles(): Promise { + return Promise.resolve(this.app.vault.getFiles()); } statFromNative(file: TFile): Promise { diff --git a/test/harness/harness.ts b/test/harness/harness.ts index cbb4e74..ef2d20e 100644 --- a/test/harness/harness.ts +++ b/test/harness/harness.ts @@ -109,7 +109,7 @@ export async function generateHarness( } export async function waitForReady(harness: LiveSyncHarness): Promise { 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 { 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 { export async function waitForClosed(harness: LiveSyncHarness): Promise { 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; } diff --git a/test/suite/db_common.ts b/test/suite/db_common.ts index e9d3831..f81f084 100644 --- a/test/suite/db_common.ts +++ b/test/suite/db_common.ts @@ -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(); diff --git a/test/suite/onlylocaldb.test.ts b/test/suite/onlylocaldb.test.ts index 3734ab8..acfbb65 100644 --- a/test/suite/onlylocaldb.test.ts +++ b/test/suite/onlylocaldb.test.ts @@ -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); diff --git a/test/suite/sync.senario.basic.ts b/test/suite/sync.senario.basic.ts index 84b9935..3eafe3c 100644 --- a/test/suite/sync.senario.basic.ts +++ b/test/suite/sync.senario.basic.ts @@ -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 { diff --git a/test/suite/sync_common.ts b/test/suite/sync_common.ts index ad4d724..f8ee4c2 100644 --- a/test/suite/sync_common.ts +++ b/test/suite/sync_common.ts @@ -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(); } diff --git a/test/unit/dialog.test.ts b/test/unit/dialog.test.ts index 4554263..86c424c 100644 --- a/test/unit/dialog.test.ts +++ b/test/unit/dialog.test.ts @@ -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("📋"); diff --git a/updates.md b/updates.md index 53a4a9f..17fabc4 100644 --- a/updates.md +++ b/updates.md @@ -3,6 +3,29 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025) The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. +## -- Unreleased -- + +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. + ## 0.25.52 9th March, 2026 @@ -14,11 +37,12 @@ I would like to devise a mechanism for running simple test scenarios. Now that w To improve the bus factor, we really need to organise the source code more thoroughly. Your cooperation and contributions would be greatly appreciated. ### Fixed + - No longer unexpected deletion-propagation occurs when the parent directory is not empty (#813). ### Revert reversions -- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature. +- Reverted the reversion of ModuleCheckRemoteSize. Now it is back to the service feature. ## 0.25.51 @@ -27,7 +51,7 @@ To improve the bus factor, we really need to organise the source code more thoro ### Reverted - Reverted to ModuleRedFlag and ModuleInitializerFile to the previous version because of some unexpected issues. (#813) - - I will re-implement them in the future with better design and tests. + - I will re-implement them in the future with better design and tests. ## 0.25.50