Refactored: changed the implementation from using overrides to injecting an adapter.

This commit is contained in:
vorotamoroz
2026-03-02 09:06:23 +00:00
parent 28e06a21e4
commit f3e83d4045
19 changed files with 430 additions and 385 deletions

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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));
}

Submodule src/lib updated: 2ed1925ca7...d2d739a3ab

View File

@@ -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,

View File

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

View File

@@ -1,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<ObsidianStorageEventManagerAdapter> {
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;
}
}

View File

@@ -1,7 +1,7 @@
import { unique } from "octagonal-wheels/collection";
import { throttle } from "octagonal-wheels/function";
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "../../common/events.ts";
import { BASE_IS_NEW, compareFileFreshness, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
import { BASE_IS_NEW, EVEN, isValidPath, TARGET_IS_NEW } from "../../common/utils.ts";
import {
type FilePathWithPrefixLC,
type FilePathWithPrefix,
@@ -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)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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