From f3e83d404527f1390e442cc1a3462172baaf8e72 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 2 Mar 2026 09:06:23 +0000 Subject: [PATCH] Refactored: changed the implementation from using overrides to injecting an adapter. --- src/common/utils.ts | 16 +- src/features/ConfigSync/CmdConfigSync.ts | 6 +- .../HiddenFileSync/CmdHiddenFileSync.ts | 8 +- src/lib | 2 +- src/main.ts | 1 + .../ObsidianStorageEventManagerAdapter.ts | 137 ++++++++++++++ src/managers/StorageEventManagerObsidian.ts | 179 +----------------- .../essential/ModuleInitializerFile.ts | 4 +- src/modules/services/ObsidianPathService.ts | 33 ++++ src/serviceModules/DatabaseFileAccess.ts | 11 +- src/serviceModules/FileAccessObsidian.ts | 166 +--------------- src/serviceModules/FileHandler.ts | 23 +-- .../ObsidianConversionAdapter.ts | 18 ++ .../ObsidianFileSystemAdapter.ts | 64 +++++++ .../FileSystemAdapters/ObsidianPathAdapter.ts | 16 ++ .../ObsidianStorageAdapter.ts | 57 ++++++ .../ObsidianTypeGuardAdapter.ts | 16 ++ .../ObsidianVaultAdapter.ts | 51 +++++ src/serviceModules/ServiceFileAccessImpl.ts | 7 +- 19 files changed, 430 insertions(+), 385 deletions(-) create mode 100644 src/managers/ObsidianStorageEventManagerAdapter.ts create mode 100644 src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts create mode 100644 src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts create mode 100644 src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts create mode 100644 src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts create mode 100644 src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts create mode 100644 src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts diff --git a/src/common/utils.ts b/src/common/utils.ts index ef1eba1..a64c26e 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -257,20 +257,8 @@ export function requestToCouchDBWithCredentials( import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts"; export { BASE_IS_NEW, EVEN, TARGET_IS_NEW }; // Why 2000? : ZIP FILE Does not have enough resolution. -const resolution = 2000; -export function compareMTime( - baseMTime: number, - targetMTime: number -): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { - const truncatedBaseMTime = ~~(baseMTime / resolution) * resolution; - const truncatedTargetMTime = ~~(targetMTime / resolution) * resolution; - // Logger(`Resolution MTime ${truncatedBaseMTime} and ${truncatedTargetMTime} `, LOG_LEVEL_VERBOSE); - if (truncatedBaseMTime == truncatedTargetMTime) return EVEN; - if (truncatedBaseMTime > truncatedTargetMTime) return BASE_IS_NEW; - if (truncatedBaseMTime < truncatedTargetMTime) return TARGET_IS_NEW; - throw new Error("Unexpected error"); -} - +import { compareMTime } from "@lib/common/utils.ts"; +export { compareMTime }; function getKey(file: AnyEntry | string | UXFileInfoStub) { const key = typeof file == "string" ? file : stripAllPrefixes(file.path); return key; diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index e51af06..0bc732b 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -53,9 +53,7 @@ import { PeriodicProcessor, disposeMemoObject, isCustomisationSyncMetadata, - isMarkedAsSameChanges, isPluginMetadata, - markChangesAreSame, memoIfNotExist, memoObject, retrieveMemoObject, @@ -1308,7 +1306,7 @@ export class ConfigSync extends LiveSyncCommands { eden: {}, }; } else { - if (isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) { + if (this.services.path.isMarkedAsSameChanges(prefixedFileName, [old.mtime, mtime + 1]) == EVEN) { this._log( `STORAGE --> DB:${prefixedFileName}: (config) Skipped (Already checked the same)`, LOG_LEVEL_DEBUG @@ -1328,7 +1326,7 @@ export class ConfigSync extends LiveSyncCommands { `STORAGE --> DB:${prefixedFileName}: (config) Skipped (the same content)`, LOG_LEVEL_VERBOSE ); - markChangesAreSame(prefixedFileName, old.mtime, mtime + 1); + this.services.path.markChangesAreSame(prefixedFileName, old.mtime, mtime + 1); return true; } saveData = { diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 92baa9c..2ada77e 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -29,9 +29,7 @@ import { } from "../../lib/src/common/utils.ts"; import { compareMTime, - unmarkChanges, isInternalMetadata, - markChangesAreSame, PeriodicProcessor, TARGET_IS_NEW, scheduleTask, @@ -362,13 +360,13 @@ export class HiddenFileSync extends LiveSyncCommands { const dbMTime = getComparingMTime(db); const storageMTime = getComparingMTime(stat); if (dbMTime == 0 || storageMTime == 0) { - unmarkChanges(path); + this.services.path.unmarkChanges(path); } else { - markChangesAreSame(path, getComparingMTime(db), getComparingMTime(stat)); + this.services.path.markChangesAreSame(path, getComparingMTime(db), getComparingMTime(stat)); } } updateLastProcessedDeletion(path: FilePath, db: MetaEntry | LoadedEntry | false) { - unmarkChanges(path); + this.services.path.unmarkChanges(path); if (db) this.updateLastProcessedDatabase(path, db); this.updateLastProcessedFile(path, this.statToKey(null)); } diff --git a/src/lib b/src/lib index 2ed1925..d2d739a 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 2ed1925ca7e3d7d9d0b497b74a108bbb881b92b7 +Subproject commit d2d739a3abd9d08a3052da953df0dd4e0796acbe diff --git a/src/main.ts b/src/main.ts index ba52092..3470c7a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -336,6 +336,7 @@ export default class ObsidianLiveSyncPlugin vaultService: this.services.vault, settingService: this.services.setting, APIService: this.services.API, + pathService: this.services.path, }); const storageEventManager = new StorageEventManagerObsidian(this, this, { fileProcessing: this.services.fileProcessing, diff --git a/src/managers/ObsidianStorageEventManagerAdapter.ts b/src/managers/ObsidianStorageEventManagerAdapter.ts new file mode 100644 index 0000000..5645ac6 --- /dev/null +++ b/src/managers/ObsidianStorageEventManagerAdapter.ts @@ -0,0 +1,137 @@ +import { TFile, TFolder } from "@/deps"; +import type { FilePath, UXFileInfoStub, UXInternalFileInfoStub } from "@lib/common/types"; +import type { FileEventItem } from "@lib/common/types"; +import type { IStorageEventManagerAdapter } from "@lib/managers/adapters"; +import type { + IStorageEventTypeGuardAdapter, + IStorageEventPersistenceAdapter, + IStorageEventWatchAdapter, + IStorageEventStatusAdapter, + IStorageEventConverterAdapter, + IStorageEventWatchHandlers, +} from "@lib/managers/adapters"; +import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager"; +import type ObsidianLiveSyncPlugin from "@/main"; +import type { LiveSyncCore } from "@/main"; +import type { FileProcessingService } from "@lib/services/base/FileProcessingService"; +import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian"; + +/** + * Obsidian-specific type guard adapter + */ +class ObsidianTypeGuardAdapter implements IStorageEventTypeGuardAdapter { + isFile(file: any): file is TFile { + if (file instanceof TFile) { + return true; + } + if (file && typeof file === "object" && "isFolder" in file) { + return !file.isFolder; + } + return false; + } + + isFolder(item: any): item is TFolder { + if (item instanceof TFolder) { + return true; + } + if (item && typeof item === "object" && "isFolder" in item) { + return !!item.isFolder; + } + return false; + } +} + +/** + * Obsidian-specific persistence adapter + */ +class ObsidianPersistenceAdapter implements IStorageEventPersistenceAdapter { + constructor(private core: LiveSyncCore) {} + + async saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]): Promise { + await this.core.kvDB.set("storage-event-manager-snapshot", snapshot); + } + + async loadSnapshot(): Promise<(FileEventItem | FileEventItemSentinel)[] | null> { + const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>( + "storage-event-manager-snapshot" + ); + return snapShot; + } +} + +/** + * Obsidian-specific status adapter + */ +class ObsidianStatusAdapter implements IStorageEventStatusAdapter { + constructor(private fileProcessing: FileProcessingService) {} + + updateStatus(status: { batched: number; processing: number; totalQueued: number }): void { + this.fileProcessing.batched.value = status.batched; + this.fileProcessing.processing.value = status.processing; + this.fileProcessing.totalQueued.value = status.totalQueued; + } +} + +/** + * Obsidian-specific converter adapter + */ +class ObsidianConverterAdapter implements IStorageEventConverterAdapter { + toFileInfo(file: TFile, deleted?: boolean): UXFileInfoStub { + return TFileToUXFileInfoStub(file, deleted); + } + + toInternalFileInfo(path: FilePath): UXInternalFileInfoStub { + return InternalFileToUXFileInfoStub(path); + } +} + +/** + * Obsidian-specific watch adapter + */ +class ObsidianWatchAdapter implements IStorageEventWatchAdapter { + constructor(private plugin: ObsidianLiveSyncPlugin) {} + + beginWatch(handlers: IStorageEventWatchHandlers): Promise { + const plugin = this.plugin; + + const boundHandlers = { + onCreate: handlers.onCreate.bind(handlers), + onChange: handlers.onChange.bind(handlers), + onDelete: handlers.onDelete.bind(handlers), + onRename: handlers.onRename.bind(handlers), + onRaw: handlers.onRaw.bind(handlers), + onEditorChange: handlers.onEditorChange?.bind(handlers), + }; + + plugin.registerEvent(plugin.app.vault.on("create", boundHandlers.onCreate)); + plugin.registerEvent(plugin.app.vault.on("modify", boundHandlers.onChange)); + plugin.registerEvent(plugin.app.vault.on("delete", boundHandlers.onDelete)); + plugin.registerEvent(plugin.app.vault.on("rename", boundHandlers.onRename)); + //@ts-ignore : Internal API + plugin.registerEvent(plugin.app.vault.on("raw", boundHandlers.onRaw)); + if (boundHandlers.onEditorChange) { + plugin.registerEvent(plugin.app.workspace.on("editor-change", boundHandlers.onEditorChange)); + } + + return Promise.resolve(); + } +} + +/** + * Composite adapter for Obsidian StorageEventManager + */ +export class ObsidianStorageEventManagerAdapter implements IStorageEventManagerAdapter { + readonly typeGuard: ObsidianTypeGuardAdapter; + readonly persistence: ObsidianPersistenceAdapter; + readonly watch: ObsidianWatchAdapter; + readonly status: ObsidianStatusAdapter; + readonly converter: ObsidianConverterAdapter; + + constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, fileProcessing: FileProcessingService) { + this.typeGuard = new ObsidianTypeGuardAdapter(); + this.persistence = new ObsidianPersistenceAdapter(core); + this.watch = new ObsidianWatchAdapter(plugin); + this.status = new ObsidianStatusAdapter(fileProcessing); + this.converter = new ObsidianConverterAdapter(); + } +} diff --git a/src/managers/StorageEventManagerObsidian.ts b/src/managers/StorageEventManagerObsidian.ts index b48081b..3f5fdf7 100644 --- a/src/managers/StorageEventManagerObsidian.ts +++ b/src/managers/StorageEventManagerObsidian.ts @@ -1,168 +1,32 @@ -import type { FileEventItem } from "@/common/types"; import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync"; -import type { FilePath, UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub } from "@lib/common/types"; -import type { FileEvent } from "@lib/interfaces/StorageEventManager"; -import { TFile, type TAbstractFile, TFolder } from "@/deps"; -import { LOG_LEVEL_DEBUG } from "octagonal-wheels/common/logger"; +import type { FilePath } from "@lib/common/types"; import type ObsidianLiveSyncPlugin from "@/main"; import type { LiveSyncCore } from "@/main"; import { StorageEventManagerBase, - type FileEventItemSentinel, type StorageEventManagerBaseDependencies, } from "@lib/managers/StorageEventManager"; -import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian"; +import { ObsidianStorageEventManagerAdapter } from "./ObsidianStorageEventManagerAdapter"; -export class StorageEventManagerObsidian extends StorageEventManagerBase { +export class StorageEventManagerObsidian extends StorageEventManagerBase { plugin: ObsidianLiveSyncPlugin; core: LiveSyncCore; // Necessary evil. cmdHiddenFileSync: HiddenFileSync; - override isFile(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFile): boolean { - if (file instanceof TFile) { - return true; - } - if (super.isFile(file)) { - return true; - } - return !file.isFolder; - } - override isFolder(file: UXFileInfoStub | UXInternalFileInfoStub | UXFolderInfo | TFolder): boolean { - if (file instanceof TFolder) { - return true; - } - if (super.isFolder(file)) { - return true; - } - return !!file.isFolder; - } - constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, dependencies: StorageEventManagerBaseDependencies) { - super(dependencies); + 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; } - async beginWatch() { - await this.snapShotRestored; - const plugin = this.plugin; - this.watchVaultChange = this.watchVaultChange.bind(this); - this.watchVaultCreate = this.watchVaultCreate.bind(this); - this.watchVaultDelete = this.watchVaultDelete.bind(this); - this.watchVaultRename = this.watchVaultRename.bind(this); - this.watchVaultRawEvents = this.watchVaultRawEvents.bind(this); - this.watchEditorChange = this.watchEditorChange.bind(this); - plugin.registerEvent(plugin.app.vault.on("modify", this.watchVaultChange)); - plugin.registerEvent(plugin.app.vault.on("delete", this.watchVaultDelete)); - plugin.registerEvent(plugin.app.vault.on("rename", this.watchVaultRename)); - plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate)); - //@ts-ignore : Internal API - plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents)); - plugin.registerEvent(plugin.app.workspace.on("editor-change", this.watchEditorChange)); - } - watchEditorChange(editor: any, info: any) { - if (!("path" in info)) { - return; - } - if (!this.shouldBatchSave) { - return; - } - const file = info?.file as TFile; - if (!file) return; - if (this.storageAccess.isFileProcessing(file.path as FilePath)) { - // this._log(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE); - return; - } - if (!this.isWaiting(file.path as FilePath)) { - return; - } - const data = info?.data as string; - const fi: FileEvent = { - type: "CHANGED", - file: TFileToUXFileInfoStub(file), - cachedData: data, - }; - void this.appendQueue([fi]); - } - - watchVaultCreate(file: TAbstractFile, ctx?: any) { - if (file instanceof TFolder) return; - if (this.storageAccess.isFileProcessing(file.path as FilePath)) { - // this._log(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE); - return; - } - const fileInfo = TFileToUXFileInfoStub(file); - void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx); - } - - watchVaultChange(file: TAbstractFile, ctx?: any) { - if (file instanceof TFolder) return; - if (this.storageAccess.isFileProcessing(file.path as FilePath)) { - // this._log(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE); - return; - } - const fileInfo = TFileToUXFileInfoStub(file); - void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx); - } - - watchVaultDelete(file: TAbstractFile, ctx?: any) { - if (file instanceof TFolder) return; - if (this.storageAccess.isFileProcessing(file.path as FilePath)) { - // this._log(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE); - return; - } - const fileInfo = TFileToUXFileInfoStub(file, true); - void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx); - } - watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) { - // vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename'). - if (file instanceof TFile) { - const fileInfo = TFileToUXFileInfoStub(file); - void this.appendQueue( - [ - { - type: "DELETE", - file: { - path: oldFile as FilePath, - name: file.name, - stat: { - mtime: file.stat.mtime, - ctime: file.stat.ctime, - size: file.stat.size, - type: "file", - }, - deleted: true, - }, - skipBatchWait: true, - }, - { type: "CREATE", file: fileInfo, skipBatchWait: true }, - ], - ctx - ); - } - } - // Watch raw events (Internal API) - watchVaultRawEvents(path: FilePath) { - if (this.storageAccess.isFileProcessing(path)) { - // this._log(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE); - return; - } - // Only for internal files. - if (!this.settings) return; - // if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) { - if (this.settings.useIgnoreFiles) { - // If it is one of ignore files, refresh the cached one. - // (Calling$$isTargetFile will refresh the cache) - void this.vaultService.isTargetFile(path).then(() => this._watchVaultRawEvents(path)); - } else { - void this._watchVaultRawEvents(path); - } - } - - async _watchVaultRawEvents(path: FilePath) { + /** + * Override _watchVaultRawEvents to add Obsidian-specific logic + */ + protected override async _watchVaultRawEvents(path: FilePath) { if (!this.settings.syncInternalFiles && !this.settings.usePluginSync) return; if (!this.settings.watchInternalFileChanges) return; if (!path.startsWith(this.plugin.app.vault.configDir)) return; @@ -177,34 +41,11 @@ export class StorageEventManagerObsidian extends StorageEventManagerBase { [ { type: "INTERNAL", - file: InternalFileToUXFileInfoStub(path), + file: this.adapter.converter.toInternalFileInfo(path), skipBatchWait: true, // Internal files should be processed immediately. }, ], null ); } - - async _saveSnapshot(snapshot: (FileEventItem | FileEventItemSentinel)[]) { - await this.core.kvDB.set("storage-event-manager-snapshot", snapshot); - this._log(`Storage operation snapshot saved: ${snapshot.length} items`, LOG_LEVEL_DEBUG); - } - - async _loadSnapshot() { - const snapShot = await this.core.kvDB.get<(FileEventItem | FileEventItemSentinel)[]>( - "storage-event-manager-snapshot" - ); - return snapShot; - } - - updateStatus() { - const allFileEventItems = this.bufferedQueuedItems.filter((e): e is FileEventItem => "args" in e); - const allItems = allFileEventItems.filter((e) => !e.cancelled); - const totalItems = allItems.length + this.concurrentProcessing.waiting; - const processing = this.processingCount; - const batchedCount = this._waitingMap.size; - this.fileProcessing.batched.value = batchedCount; - this.fileProcessing.processing.value = processing; - this.fileProcessing.totalQueued.value = totalItems + batchedCount + processing; - } } diff --git a/src/modules/essential/ModuleInitializerFile.ts b/src/modules/essential/ModuleInitializerFile.ts index 8112bfe..c4e047b 100644 --- a/src/modules/essential/ModuleInitializerFile.ts +++ b/src/modules/essential/ModuleInitializerFile.ts @@ -1,7 +1,7 @@ import { unique } from "octagonal-wheels/collection"; import { throttle } from "octagonal-wheels/function"; import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts"; -import { BASE_IS_NEW, compareFileFreshness, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts"; +import { BASE_IS_NEW, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts"; import { type FilePathWithPrefixLC, type FilePathWithPrefix, @@ -308,7 +308,7 @@ export class ModuleInitializerFile extends AbstractModule { } } - const compareResult = compareFileFreshness(file, doc); + const compareResult = this.services.path.compareFileFreshness(file, doc); switch (compareResult) { case BASE_IS_NEW: if (!this.services.vault.isFileSizeTooLarge(file.stat.size)) { diff --git a/src/modules/services/ObsidianPathService.ts b/src/modules/services/ObsidianPathService.ts index 72e5532..82b5c85 100644 --- a/src/modules/services/ObsidianPathService.ts +++ b/src/modules/services/ObsidianPathService.ts @@ -1,7 +1,40 @@ import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext"; import { normalizePath } from "@/deps"; import { PathService } from "@/lib/src/services/base/PathService"; + +import { + type BASE_IS_NEW, + type TARGET_IS_NEW, + type EVEN, + markChangesAreSame, + unmarkChanges, + compareFileFreshness, + isMarkedAsSameChanges, +} from "@/common/utils"; +import type { UXFileInfo, AnyEntry, UXFileInfoStub, FilePathWithPrefix } from "@/lib/src/common/types"; export class ObsidianPathService extends PathService { + override markChangesAreSame( + old: UXFileInfo | AnyEntry | FilePathWithPrefix, + newMtime: number, + oldMtime: number + ): boolean | undefined { + return markChangesAreSame(old, newMtime, oldMtime); + } + override unmarkChanges(file: AnyEntry | FilePathWithPrefix | UXFileInfoStub): void { + return unmarkChanges(file); + } + override compareFileFreshness( + baseFile: UXFileInfoStub | AnyEntry | undefined, + checkTarget: UXFileInfo | AnyEntry | undefined + ): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN { + return compareFileFreshness(baseFile, checkTarget); + } + override isMarkedAsSameChanges( + file: UXFileInfoStub | AnyEntry | FilePathWithPrefix, + mtimes: number[] + ): undefined | typeof EVEN { + return isMarkedAsSameChanges(file, mtimes); + } protected normalizePath(path: string): string { return normalizePath(path); } diff --git a/src/serviceModules/DatabaseFileAccess.ts b/src/serviceModules/DatabaseFileAccess.ts index 51ffb87..645888e 100644 --- a/src/serviceModules/DatabaseFileAccess.ts +++ b/src/serviceModules/DatabaseFileAccess.ts @@ -1,15 +1,8 @@ -import { markChangesAreSame } from "@/common/utils"; -import type { AnyEntry } from "@lib/common/types"; - import type { DatabaseFileAccess } from "@lib/interfaces/DatabaseFileAccess.ts"; import { ServiceDatabaseFileAccessBase } from "@lib/serviceModules/ServiceDatabaseFileAccessBase"; // markChangesAreSame uses persistent data implicitly, we should refactor it too. // For now, to make the refactoring done once, we just use them directly. // Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB). -// TODO: REFACTOR -export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess { - markChangesAreSame(old: AnyEntry, newMtime: number, oldMtime: number): void { - markChangesAreSame(old, newMtime, oldMtime); - } -} +// Refactored, now migrating... +export class ServiceDatabaseFileAccess extends ServiceDatabaseFileAccessBase implements DatabaseFileAccess {} diff --git a/src/serviceModules/FileAccessObsidian.ts b/src/serviceModules/FileAccessObsidian.ts index 823fd16..5b318cc 100644 --- a/src/serviceModules/FileAccessObsidian.ts +++ b/src/serviceModules/FileAccessObsidian.ts @@ -1,160 +1,14 @@ -import { markChangesAreSame } from "@/common/utils"; -import type { FilePath, UXDataWriteOptions, UXFileInfoStub, UXFolderInfo } from "@lib/common/types"; - -import { TFolder, type TAbstractFile, TFile, type Stat, type App, type DataWriteOptions, normalizePath } from "@/deps"; -import { FileAccessBase, toArrayBuffer, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts"; -import { TFileToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian"; - -declare module "obsidian" { - interface Vault { - getAbstractFileByPathInsensitive(path: string): TAbstractFile | null; - } - interface DataAdapter { - reconcileInternalFile?(path: string): Promise; - } -} - -export class FileAccessObsidian extends FileAccessBase { - app: App; - - override getPath(file: string | TAbstractFile): FilePath { - return (typeof file === "string" ? file : file.path) as FilePath; - } - - override isFile(file: TAbstractFile | null): file is TFile { - return file instanceof TFile; - } - override isFolder(file: TAbstractFile | null): file is TFolder { - return file instanceof TFolder; - } - override _statFromNative(file: TFile): Promise { - return Promise.resolve(file.stat); - } - - override nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub { - return TFileToUXFileInfoStub(file); - } - override nativeFolderToUXFolder(folder: TFolder): UXFolderInfo { - if (folder instanceof TFolder) { - return this.nativeFolderToUXFolder(folder); - } else { - throw new Error(`Not a folder: ${(folder as TAbstractFile)?.name}`); - } - } +import { type App } from "@/deps"; +import { FileAccessBase, type FileAccessBaseDependencies } from "@lib/serviceModules/FileAccessBase.ts"; +import { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; +/** + * Obsidian-specific implementation of FileAccessBase + * Uses ObsidianFileSystemAdapter for platform-specific operations + */ +export class FileAccessObsidian extends FileAccessBase { constructor(app: App, dependencies: FileAccessBaseDependencies) { - super({ - storageAccessManager: dependencies.storageAccessManager, - vaultService: dependencies.vaultService, - settingService: dependencies.settingService, - APIService: dependencies.APIService, - }); - this.app = app; - } - - protected override _normalisePath(path: string): string { - return normalizePath(path); - } - - protected async _adapterMkdir(path: string) { - await this.app.vault.adapter.mkdir(path); - } - protected _getAbstractFileByPath(path: FilePath) { - return this.app.vault.getAbstractFileByPath(path); - } - protected _getAbstractFileByPathInsensitive(path: FilePath) { - return this.app.vault.getAbstractFileByPathInsensitive(path); - } - - protected async _tryAdapterStat(path: FilePath) { - if (!(await this.app.vault.adapter.exists(path))) return null; - return await this.app.vault.adapter.stat(path); - } - - protected async _adapterStat(path: FilePath) { - return await this.app.vault.adapter.stat(path); - } - - protected async _adapterExists(path: FilePath) { - return await this.app.vault.adapter.exists(path); - } - protected async _adapterRemove(path: FilePath) { - await this.app.vault.adapter.remove(path); - } - - protected async _adapterRead(path: FilePath) { - return await this.app.vault.adapter.read(path); - } - - protected async _adapterReadBinary(path: FilePath) { - return await this.app.vault.adapter.readBinary(path); - } - - _adapterWrite(file: string, data: string, options?: UXDataWriteOptions): Promise { - return this.app.vault.adapter.write(file, data, options); - } - _adapterWriteBinary(file: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise { - return this.app.vault.adapter.writeBinary(file, toArrayBuffer(data), options); - } - - protected _adapterList(basePath: string): Promise<{ files: string[]; folders: string[] }> { - return Promise.resolve(this.app.vault.adapter.list(basePath)); - } - - async _vaultCacheRead(file: TFile) { - return await this.app.vault.cachedRead(file); - } - - protected async _vaultRead(file: TFile): Promise { - return await this.app.vault.read(file); - } - - protected async _vaultReadBinary(file: TFile): Promise { - return await this.app.vault.readBinary(file); - } - - protected override markChangesAreSame(path: string, mtime: number, newMtime: number) { - return markChangesAreSame(path, mtime, newMtime); - } - - protected override async _vaultModify(file: TFile, data: string, options?: UXDataWriteOptions): Promise { - return await this.app.vault.modify(file, data, options); - } - protected override async _vaultModifyBinary( - file: TFile, - data: ArrayBuffer, - options?: UXDataWriteOptions - ): Promise { - return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options); - } - protected override async _vaultCreate(path: string, data: string, options?: UXDataWriteOptions): Promise { - return await this.app.vault.create(path, data, options); - } - protected override async _vaultCreateBinary( - path: string, - data: ArrayBuffer, - options?: UXDataWriteOptions - ): Promise { - return await this.app.vault.createBinary(path, toArrayBuffer(data), options); - } - - protected override _trigger(name: string, ...data: any[]) { - return this.app.vault.trigger(name, ...data); - } - protected override async _reconcileInternalFile(path: string) { - return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path)); - } - protected override async _adapterAppend(normalizedPath: string, data: string, options?: DataWriteOptions) { - return await this.app.vault.adapter.append(normalizedPath, data, options); - } - protected override async _delete(file: TFile | TFolder, force = false) { - return await this.app.vault.delete(file, force); - } - protected override async _trash(file: TFile | TFolder, force = false) { - return await this.app.vault.trash(file, force); - } - - protected override _getFiles() { - return this.app.vault.getFiles(); + const adapter = new ObsidianFileSystemAdapter(app); + super(adapter, dependencies); } } diff --git a/src/serviceModules/FileHandler.ts b/src/serviceModules/FileHandler.ts index d5cbff6..c47cc33 100644 --- a/src/serviceModules/FileHandler.ts +++ b/src/serviceModules/FileHandler.ts @@ -1,26 +1,7 @@ -import { - compareFileFreshness, - markChangesAreSame, - type BASE_IS_NEW, - type EVEN, - type TARGET_IS_NEW, -} from "@/common/utils"; -import type { AnyEntry } from "@lib/common/models/db.type"; -import type { UXFileInfo, UXFileInfoStub } from "@lib/common/models/fileaccess.type"; import { ServiceFileHandlerBase } from "@lib/serviceModules/ServiceFileHandlerBase"; // markChangesAreSame uses persistent data implicitly, we should refactor it too. // also, compareFileFreshness depends on marked changes, so we should refactor it as well. For now, to make the refactoring done once, we just use them directly. // Hence it is not on /src/lib/src/serviceModules. (markChangesAreSame is using indexedDB). -// TODO: REFACTOR -export class ServiceFileHandler extends ServiceFileHandlerBase { - override markChangesAreSame(old: UXFileInfo | AnyEntry, newMtime: number, oldMtime: number) { - return markChangesAreSame(old, newMtime, oldMtime); - } - override compareFileFreshness( - baseFile: UXFileInfoStub | AnyEntry | undefined, - checkTarget: UXFileInfo | AnyEntry | undefined - ): typeof TARGET_IS_NEW | typeof BASE_IS_NEW | typeof EVEN { - return compareFileFreshness(baseFile, checkTarget); - } -} +// Refactored: markChangesAreSame, unmarkChanges, compareFileFreshness, isMarkedAsSameChanges are now moved to PathService +export class ServiceFileHandler extends ServiceFileHandlerBase {} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts new file mode 100644 index 0000000..82e6f65 --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/ObsidianConversionAdapter.ts @@ -0,0 +1,18 @@ +import type { UXFileInfoStub, UXFolderInfo } from "@/lib/src/common/types"; +import type { IConversionAdapter } from "@/lib/src/serviceModules/adapters"; +import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "@/modules/coreObsidian/storageLib/utilObsidian"; +import type { TFile, TFolder } from "obsidian"; + +/** + * Conversion adapter implementation for Obsidian + */ + +export class ObsidianConversionAdapter implements IConversionAdapter { + nativeFileToUXFileInfoStub(file: TFile): UXFileInfoStub { + return TFileToUXFileInfoStub(file); + } + + nativeFolderToUXFolder(folder: TFolder): UXFolderInfo { + return TFolderToUXFileInfoStub(folder); + } +} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts new file mode 100644 index 0000000..d0c261e --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/ObsidianFileSystemAdapter.ts @@ -0,0 +1,64 @@ +import type { FilePath, UXStat } from "@/lib/src/common/types"; +import type { + IFileSystemAdapter, + IPathAdapter, + ITypeGuardAdapter, + IConversionAdapter, + IStorageAdapter, + IVaultAdapter, +} from "@/lib/src/serviceModules/adapters"; +import type { TAbstractFile, TFile, TFolder, Stat, App } from "obsidian"; +import { ObsidianConversionAdapter } from "./ObsidianConversionAdapter"; +import { ObsidianPathAdapter } from "./ObsidianPathAdapter"; +import { ObsidianStorageAdapter } from "./ObsidianStorageAdapter"; +import { ObsidianTypeGuardAdapter } from "./ObsidianTypeGuardAdapter"; +import { ObsidianVaultAdapter } from "./ObsidianVaultAdapter"; + +declare module "obsidian" { + interface Vault { + getAbstractFileByPathInsensitive(path: string): TAbstractFile | null; + } + interface DataAdapter { + reconcileInternalFile?(path: string): Promise; + } +} + +/** + * Complete file system adapter implementation for Obsidian + */ + +export class ObsidianFileSystemAdapter implements IFileSystemAdapter { + readonly path: IPathAdapter; + readonly typeGuard: ITypeGuardAdapter; + readonly conversion: IConversionAdapter; + readonly storage: IStorageAdapter; + readonly vault: IVaultAdapter; + + constructor(private app: App) { + this.path = new ObsidianPathAdapter(); + this.typeGuard = new ObsidianTypeGuardAdapter(); + this.conversion = new ObsidianConversionAdapter(); + this.storage = new ObsidianStorageAdapter(app); + this.vault = new ObsidianVaultAdapter(app); + } + + getAbstractFileByPath(path: FilePath | string): TAbstractFile | null { + return this.app.vault.getAbstractFileByPath(path); + } + + getAbstractFileByPathInsensitive(path: FilePath | string): TAbstractFile | null { + return this.app.vault.getAbstractFileByPathInsensitive(path); + } + + getFiles(): TFile[] { + return this.app.vault.getFiles(); + } + + statFromNative(file: TFile): Promise { + return Promise.resolve({ ...file.stat, type: "file" }); + } + + async reconcileInternalFile(path: string): Promise { + return await Promise.resolve(this.app.vault.adapter.reconcileInternalFile?.(path)); + } +} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts new file mode 100644 index 0000000..a21ced1 --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts @@ -0,0 +1,16 @@ +import { type TAbstractFile, normalizePath } from "@/deps"; +import type { FilePath } from "@lib/common/types"; +import type { IPathAdapter } from "@lib/serviceModules/adapters"; + +/** + * Path adapter implementation for Obsidian + */ +export class ObsidianPathAdapter implements IPathAdapter { + getPath(file: string | TAbstractFile): FilePath { + return (typeof file === "string" ? file : file.path) as FilePath; + } + + normalisePath(path: string): string { + return normalizePath(path); + } +} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts new file mode 100644 index 0000000..fe4818e --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/ObsidianStorageAdapter.ts @@ -0,0 +1,57 @@ +import type { UXDataWriteOptions } from "@/lib/src/common/types"; +import type { IStorageAdapter } from "@/lib/src/serviceModules/adapters"; +import { toArrayBuffer } from "@/lib/src/serviceModules/FileAccessBase"; +import type { Stat, App } from "obsidian"; + +/** + * Storage adapter implementation for Obsidian + */ + +export class ObsidianStorageAdapter implements IStorageAdapter { + constructor(private app: App) {} + + async exists(path: string): Promise { + return await this.app.vault.adapter.exists(path); + } + + async trystat(path: string): Promise { + if (!(await this.app.vault.adapter.exists(path))) return null; + return await this.app.vault.adapter.stat(path); + } + + async stat(path: string): Promise { + return await this.app.vault.adapter.stat(path); + } + + async mkdir(path: string): Promise { + await this.app.vault.adapter.mkdir(path); + } + + async remove(path: string): Promise { + await this.app.vault.adapter.remove(path); + } + + async read(path: string): Promise { + return await this.app.vault.adapter.read(path); + } + + async readBinary(path: string): Promise { + return await this.app.vault.adapter.readBinary(path); + } + + async write(path: string, data: string, options?: UXDataWriteOptions): Promise { + return await this.app.vault.adapter.write(path, data, options); + } + + async writeBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise { + return await this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options); + } + + async append(path: string, data: string, options?: UXDataWriteOptions): Promise { + return await this.app.vault.adapter.append(path, data, options); + } + + list(basePath: string): Promise<{ files: string[]; folders: string[] }> { + return Promise.resolve(this.app.vault.adapter.list(basePath)); + } +} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts new file mode 100644 index 0000000..455affb --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/ObsidianTypeGuardAdapter.ts @@ -0,0 +1,16 @@ +import type { ITypeGuardAdapter } from "@/lib/src/serviceModules/adapters"; +import { TFile, TFolder } from "obsidian"; + +/** + * Type guard adapter implementation for Obsidian + */ + +export class ObsidianTypeGuardAdapter implements ITypeGuardAdapter { + isFile(file: any): file is TFile { + return file instanceof TFile; + } + + isFolder(item: any): item is TFolder { + return item instanceof TFolder; + } +} diff --git a/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts new file mode 100644 index 0000000..a7d996c --- /dev/null +++ b/src/serviceModules/FileSystemAdapters/ObsidianVaultAdapter.ts @@ -0,0 +1,51 @@ +import type { UXDataWriteOptions } from "@/lib/src/common/types"; +import type { IVaultAdapter } from "@/lib/src/serviceModules/adapters"; +import { toArrayBuffer } from "@/lib/src/serviceModules/FileAccessBase"; +import type { TFile, App, TFolder } from "obsidian"; + +/** + * Vault adapter implementation for Obsidian + */ +export class ObsidianVaultAdapter implements IVaultAdapter { + constructor(private app: App) {} + + async read(file: TFile): Promise { + return await this.app.vault.read(file); + } + + async cachedRead(file: TFile): Promise { + return await this.app.vault.cachedRead(file); + } + + async readBinary(file: TFile): Promise { + return await this.app.vault.readBinary(file); + } + + async modify(file: TFile, data: string, options?: UXDataWriteOptions): Promise { + return await this.app.vault.modify(file, data, options); + } + + async modifyBinary(file: TFile, data: ArrayBuffer, options?: UXDataWriteOptions): Promise { + return await this.app.vault.modifyBinary(file, toArrayBuffer(data), options); + } + + async create(path: string, data: string, options?: UXDataWriteOptions): Promise { + return await this.app.vault.create(path, data, options); + } + + async createBinary(path: string, data: ArrayBuffer, options?: UXDataWriteOptions): Promise { + return await this.app.vault.createBinary(path, toArrayBuffer(data), options); + } + + async delete(file: TFile | TFolder, force = false): Promise { + return await this.app.vault.delete(file, force); + } + + async trash(file: TFile | TFolder, force = false): Promise { + return await this.app.vault.trash(file, force); + } + + trigger(name: string, ...data: any[]): any { + return this.app.vault.trigger(name, ...data); + } +} diff --git a/src/serviceModules/ServiceFileAccessImpl.ts b/src/serviceModules/ServiceFileAccessImpl.ts index 686dd0b..204855e 100644 --- a/src/serviceModules/ServiceFileAccessImpl.ts +++ b/src/serviceModules/ServiceFileAccessImpl.ts @@ -1,6 +1,5 @@ -import type { TAbstractFile, TFile, TFolder, Stat } from "@/deps"; - import { ServiceFileAccessBase } from "@lib/serviceModules/ServiceFileAccessBase"; +import type { ObsidianFileSystemAdapter } from "./FileSystemAdapters/ObsidianFileSystemAdapter"; -// For typechecking purpose -export class ServiceFileAccessObsidian extends ServiceFileAccessBase {} +// For now, this is just a re-export of ServiceFileAccess with the Obsidian-specific adapter type. +export class ServiceFileAccessObsidian extends ServiceFileAccessBase {}