mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-04 00:48:47 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cbb833e9d | ||
|
|
7419d0d2a1 | ||
|
|
f3e83d4045 | ||
|
|
28e06a21e4 | ||
|
|
e08fbbd223 | ||
|
|
a1e331d452 | ||
|
|
646f8af680 | ||
|
|
392f76fd36 |
4
.github/workflows/unit-ci.yml
vendored
4
.github/workflows/unit-ci.yml
vendored
@@ -3,6 +3,10 @@ name: unit-ci
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
8
devs.md
8
devs.md
@@ -52,6 +52,7 @@ Hence, the new feature should be implemented as follows:
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
npm run test:unit # Run unit tests with vitest (or `npm run test:unit:coverage` for coverage)
|
||||
npm run check # TypeScript and svelte type checking
|
||||
npm run dev # Development build with auto-rebuild (uses .env for test vault paths)
|
||||
npm run build # Production build
|
||||
@@ -67,8 +68,11 @@ npm test # Run vitest tests (requires Docker services)
|
||||
|
||||
### Testing Infrastructure
|
||||
|
||||
- **Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)
|
||||
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright
|
||||
- ~~**Deno Tests**: Unit tests for platform-independent code (e.g., `HashManager.test.ts`)~~
|
||||
- This is now obsolete, migrated to vitest.
|
||||
- **Vitest** (`vitest.config.ts`): E2E test by Browser-based-harness using Playwright, unit tests.
|
||||
- Unit tests should be `*.unit.spec.ts` and placed alongside the implementation file (e.g., `ChunkFetcher.unit.spec.ts`).
|
||||
|
||||
- **Docker Services**: Tests require CouchDB, MinIO (S3), and P2P services:
|
||||
```bash
|
||||
npm run test:docker-all:start # Start all test services
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.45",
|
||||
"version": "0.25.48",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
2639
package-lock.json
generated
2639
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.45",
|
||||
"version": "0.25.48",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -24,7 +24,7 @@ export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
|
||||
|
||||
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
|
||||
export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3";
|
||||
export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
|
||||
// export const EVENT_REQUEST_CHECK_REMOTE_SIZE = "request-check-remote-size";
|
||||
// export const EVENT_FILE_CHANGED = "file-changed";
|
||||
|
||||
declare global {
|
||||
@@ -44,7 +44,6 @@ declare global {
|
||||
[EVENT_REQUEST_RUN_DOCTOR]: string;
|
||||
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
|
||||
[EVENT_ANALYSE_DB_USAGE]: undefined;
|
||||
[EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined;
|
||||
[EVENT_REQUEST_PERFORM_GC_V3]: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -29,9 +29,7 @@ import {
|
||||
} from "../../lib/src/common/utils.ts";
|
||||
import {
|
||||
compareMTime,
|
||||
unmarkChanges,
|
||||
isInternalMetadata,
|
||||
markChangesAreSame,
|
||||
PeriodicProcessor,
|
||||
TARGET_IS_NEW,
|
||||
scheduleTask,
|
||||
@@ -92,7 +90,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
return this.plugin.kvDB;
|
||||
}
|
||||
getConflictedDoc(path: FilePathWithPrefix, rev: string) {
|
||||
return this.plugin.managers.conflictManager.getConflictedDoc(path, rev);
|
||||
return this.localDatabase.managers.conflictManager.getConflictedDoc(path, rev);
|
||||
}
|
||||
onunload() {
|
||||
this.periodicInternalFileScanProcessor?.disable();
|
||||
@@ -244,13 +242,23 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
if (this.isThisModuleEnabled()) {
|
||||
//system file
|
||||
const filename = this.getPath(doc);
|
||||
if (await this.services.vault.isTargetFile(filename)) {
|
||||
// this.procInternalFile(filename);
|
||||
await this.processReplicationResult(doc);
|
||||
const unprefixedPath = stripAllPrefixes(filename);
|
||||
// No need to check via vaultService
|
||||
// if (!await this.services.vault.isTargetFile(unprefixedPath)) {
|
||||
// this._log(`Skipped processing sync file:${unprefixedPath} (Not target)`, LOG_LEVEL_VERBOSE);
|
||||
// return true;
|
||||
// }
|
||||
if (!(await this.isTargetFile(stripAllPrefixes(unprefixedPath)))) {
|
||||
this._log(
|
||||
`Skipped processing sync file:${unprefixedPath} (Not Hidden File Sync target)`,
|
||||
LOG_LEVEL_VERBOSE
|
||||
);
|
||||
// We should return true, we made sure that document is a internalMetadata.
|
||||
return true;
|
||||
} else {
|
||||
this._log(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE);
|
||||
return false;
|
||||
}
|
||||
if (!(await this.processReplicationResult(doc))) {
|
||||
this._log(`Failed to process sync file:${unprefixedPath}`, LOG_LEVEL_NOTICE);
|
||||
// Do not yield false, this file had been processed.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -352,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));
|
||||
}
|
||||
@@ -700,7 +708,7 @@ Offline Changed files: ${processFiles.length}`;
|
||||
revFrom._revs_info
|
||||
?.filter((e) => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo)
|
||||
.first()?.rev ?? "";
|
||||
const result = await this.plugin.managers.conflictManager.mergeObject(
|
||||
const result = await this.localDatabase.managers.conflictManager.mergeObject(
|
||||
doc.path,
|
||||
commonBase,
|
||||
doc._rev,
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 4af350bb67...d2d739a3ab
14
src/main.ts
14
src/main.ts
@@ -18,7 +18,7 @@ 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 { 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";
|
||||
@@ -64,6 +64,7 @@ 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";
|
||||
|
||||
export default class ObsidianLiveSyncPlugin
|
||||
extends Plugin
|
||||
@@ -177,7 +178,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
this._registerModule(new ModuleRedFlag(this));
|
||||
this._registerModule(new ModuleInteractiveConflictResolver(this, this));
|
||||
this._registerModule(new ModuleObsidianGlobalHistory(this, this));
|
||||
this._registerModule(new ModuleCheckRemoteSize(this));
|
||||
// this._registerModule(new ModuleCheckRemoteSize(this));
|
||||
// Test and Dev Modules
|
||||
this._registerModule(new ModuleDev(this, this));
|
||||
this._registerModule(new ModuleReplicateTest(this, this));
|
||||
@@ -238,13 +239,6 @@ export default class ObsidianLiveSyncPlugin
|
||||
return this.services.database.localDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.managers instead. The database managers, including entry manager, revision manager, etc.
|
||||
*/
|
||||
get managers() {
|
||||
return this.services.database.managers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @obsolete Use services.database.localDatabase instead. Get the PouchDB database instance. Note that this is not the same as the local database instance, which is a wrapper around the PouchDB database.
|
||||
* @returns The PouchDB database instance.
|
||||
@@ -342,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,
|
||||
@@ -421,6 +416,7 @@ export default class ObsidianLiveSyncPlugin
|
||||
}
|
||||
// enable target filter feature.
|
||||
useTargetFilters(this);
|
||||
useCheckRemoteSize(this);
|
||||
}
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
|
||||
137
src/managers/ObsidianStorageEventManagerAdapter.ts
Normal file
137
src/managers/ObsidianStorageEventManagerAdapter.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE } from "../../lib/src/common/types.ts";
|
||||
import { type ObsidianLiveSyncSettings, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "../../lib/src/common/types.ts";
|
||||
import { configURIBase } from "../../common/types.ts";
|
||||
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser.js";
|
||||
import { fireAndForget } from "../../lib/src/common/utils.ts";
|
||||
@@ -25,16 +25,24 @@ export class ModuleSetupObsidian extends AbstractModule {
|
||||
private _setupManager!: SetupManager;
|
||||
private _everyOnload(): Promise<boolean> {
|
||||
this._setupManager = this.core.getModule(SetupManager);
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||
if (conf.settings) {
|
||||
await this._setupManager.onUseSetupURI(
|
||||
UserMode.Unknown,
|
||||
`${configURIBase}${encodeURIComponent(conf.settings)}`
|
||||
);
|
||||
} else if (conf.settingsQR) {
|
||||
await this._setupManager.decodeQR(conf.settingsQR);
|
||||
}
|
||||
});
|
||||
try {
|
||||
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
|
||||
if (conf.settings) {
|
||||
await this._setupManager.onUseSetupURI(
|
||||
UserMode.Unknown,
|
||||
`${configURIBase}${encodeURIComponent(conf.settings)}`
|
||||
);
|
||||
} else if (conf.settingsQR) {
|
||||
await this._setupManager.decodeQR(conf.settingsQR);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this._log(
|
||||
"Failed to register protocol handler. This feature may not work in some environments.",
|
||||
LOG_LEVEL_NOTICE
|
||||
);
|
||||
this._log(e, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
this.addCommand({
|
||||
id: "livesync-setting-qr",
|
||||
name: "Show settings as a QR code",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
|
||||
path: path,
|
||||
vault: vault,
|
||||
setting: setting,
|
||||
API: API,
|
||||
});
|
||||
const keyValueDB = new ObsidianKeyValueDBService(context, {
|
||||
appLifecycle: appLifecycle,
|
||||
@@ -77,7 +78,6 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
|
||||
const replication = new ObsidianReplicationService(context, {
|
||||
APIService: API,
|
||||
appLifecycleService: appLifecycle,
|
||||
databaseEventService: databaseEvents,
|
||||
replicatorService: replicator,
|
||||
settingService: setting,
|
||||
fileProcessingService: fileProcessing,
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
16
src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts
Normal file
16
src/serviceModules/FileSystemAdapters/ObsidianPathAdapter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> {}
|
||||
|
||||
129
updates.md
129
updates.md
@@ -3,6 +3,59 @@ 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.
|
||||
|
||||
## 0.25.48
|
||||
|
||||
2nd March, 2026
|
||||
|
||||
No behavioural changes except unidentified faults. Please report if you find any unexpected behaviour after this update.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Many storage-related functions have been refactored for better maintainability and testability.
|
||||
- Now all platform-specific logics are supplied as adapters, and the core logic has become platform-agnostic.
|
||||
- Quite a number of tests have been added for the core logic, and the platform-specific logics are also tested with mocked adapters.
|
||||
|
||||
## 0.25.47
|
||||
|
||||
27th February, 2026
|
||||
|
||||
Phew, the financial year is still not over yet, but I have got some time to work on the plug-in again!
|
||||
|
||||
### Fixed and refactored
|
||||
|
||||
- Fixed the inexplicable behaviour when retrieving chunks from the network.
|
||||
- The chunk manager has been layered to be responsible for its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`, and `ArrivalWaitLayer`.
|
||||
- All layers have been tested now!
|
||||
- `LayeredChunkManager` has been implemented to manage these layers. Also tested.
|
||||
- `EntryManager` has been mostly rewritten and also tested.
|
||||
|
||||
- Now we can configure `Never warn` for remote storage size notification again.
|
||||
|
||||
### Tests
|
||||
|
||||
- The following test has been added:
|
||||
- `ConflictManager`.
|
||||
|
||||
## 0.25.46
|
||||
|
||||
26th February, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unexpected errors no longer occurred when the plug-in was unloaded.
|
||||
- Hidden File Sync now respects selectors.
|
||||
- Registering protocol-handlers now works safely without causing unexpected errors.
|
||||
|
||||
### Refactored
|
||||
|
||||
- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and tests have also been added.
|
||||
- Some unnecessary things have been removed.
|
||||
- LiveSyncManagers has now explicit dependencies.
|
||||
- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies.
|
||||
- This is to avoid circular dependencies and clarify the ownership of the managers.
|
||||
- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly.
|
||||
- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage.
|
||||
|
||||
## 0.25.45
|
||||
|
||||
25th February, 2026
|
||||
@@ -12,11 +65,10 @@ As a result of recent refactoring, we are able to write tests more easily now!
|
||||
### Refactored
|
||||
|
||||
- `ModuleTargetFilter`, which was responsible for checking if a file is a target file, has been ported to a serviceFeature.
|
||||
- And also tests have been added. The middleware-style-power.
|
||||
- And also tests have been added. The middleware-style-power.
|
||||
- `ModuleObsidianAPI` has been removed and implemented in `APIService` and `RemoteService`.
|
||||
- Now `APIService` is responsible for the network-online-status, not `databaseService.managers.networkManager`.
|
||||
|
||||
|
||||
## 0.25.44
|
||||
|
||||
24th February, 2026
|
||||
@@ -34,7 +86,7 @@ However, as this update is very substantial, please do feel free to let me know
|
||||
### Improved
|
||||
|
||||
- Now we can configure network-error banners as icons, or hide them completely with the new `Network Warning Style` setting in the `General` pane of the settings dialogue. (#770, PR #804)
|
||||
- Thanks so much to @A-wry!
|
||||
- Thanks so much to @A-wry!
|
||||
|
||||
### Refactored
|
||||
|
||||
@@ -71,8 +123,8 @@ However, as this update is very substantial, please do feel free to let me know
|
||||
#### Dependencies:
|
||||
|
||||
- Bumped dependencies simply to a point where they can be considered problem-free (by human-powered-artefacts-diff).
|
||||
- Svelte, terser, and more something will be bumped later. They have a significant impact on the diff and paint it totally.
|
||||
- You may be surprised, but when I bump the library, I am actually checking for any unintended code.
|
||||
- Svelte, terser, and more something will be bumped later. They have a significant impact on the diff and paint it totally.
|
||||
- You may be surprised, but when I bump the library, I am actually checking for any unintended code.
|
||||
|
||||
## 0.25.43
|
||||
|
||||
@@ -165,72 +217,5 @@ However, this is not a minor refactoring, so please be careful. Let me know if y
|
||||
|
||||
- Fixed an issue where indexedDB would not close correctly on some environments, causing unexpected errors during database operations.
|
||||
|
||||
## 0.25.37
|
||||
|
||||
15th January, 2026
|
||||
|
||||
Thank you for your patience until my return!
|
||||
|
||||
This release contains minor changes discovered and fixed during test implementation.
|
||||
There are no changes affecting usage.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Logging system has been slightly refactored to improve maintainability.
|
||||
- Some import statements have been unified.
|
||||
|
||||
## 0.25.36
|
||||
|
||||
25th December, 2025
|
||||
|
||||
### Improved
|
||||
|
||||
- Now the garbage collector (V3) has been implemented. (Beta)
|
||||
- This garbage collector ensures that all devices are synchronised to the latest progress to prevent inconsistencies.
|
||||
- In other words, it makes sure that no new conflicts would have arisen.
|
||||
- This feature requires additional information (via node information), but it should be more reliable.
|
||||
- This feature requires all devices have v0.25.36 or later.
|
||||
- After the garbage collector runs, the database size may be reduced (Compaction will be run automatically after GC).
|
||||
- We should have an administrative privilege on the remote database to run this garbage collector.
|
||||
- Now the plug-in and device information is stored in the remote database.
|
||||
- This information is used for the garbage collector (V3).
|
||||
- Some additional features may be added in the future using this information.
|
||||
|
||||
## 0.25.35
|
||||
|
||||
24th December, 2025
|
||||
|
||||
Sorry for a small release! I would like to keep things moving along like this if possible. After all, the holidays seem to be starting soon. I will be doubled by my business until the 27th though, indeed.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the conflict resolution dialogue shows correctly which device only has older APIs (#764).
|
||||
|
||||
## 0.25.34
|
||||
|
||||
10th December, 2025
|
||||
|
||||
### Behaviour change
|
||||
|
||||
- The plug-in automatically fetches the missing chunks even if `Fetch chunks on demand` is disabled.
|
||||
- This change is to avoid loss of data when receiving a bulk of revisions.
|
||||
- This can be prevented by enabling `Use Only Local Chunks` in the settings.
|
||||
- Storage application now saved during each event and restored on startup.
|
||||
- Synchronisation result application is also now saved during each event and restored on startup.
|
||||
- These may avoid some unexpected loss of data when the editor crashes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now the plug-in waits for the application of pended batch changes before the synchronisation starts.
|
||||
- This may avoid some unexpected loss or unexpected conflicts.
|
||||
Plug-in sends custom headers correctly when RequestAPI is used.
|
||||
- No longer causing unexpected chunk creation during `Reset synchronisation on This Device` with bucket sync.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Synchronisation result application process has been refactored.
|
||||
- Storage application process has been refactored.
|
||||
- Please report if you find any unexpected behaviour after this update. A bit of large refactoring.
|
||||
|
||||
Full notes are in
|
||||
[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
|
||||
@@ -3,6 +3,59 @@ 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.
|
||||
|
||||
## 0.25.48
|
||||
|
||||
2nd March, 2026
|
||||
|
||||
No behavioural changes except unidentified faults. Please report if you find any unexpected behaviour after this update.
|
||||
|
||||
### Refactored
|
||||
|
||||
- Many storage-related functions have been refactored for better maintainability and testability.
|
||||
- Now all platform-specific logics are supplied as adapters, and the core logic has become platform-agnostic.
|
||||
- Quite a number of tests have been added for the core logic, and the platform-specific logics are also tested with mocked adapters.
|
||||
|
||||
## 0.25.47
|
||||
|
||||
27th February, 2026
|
||||
|
||||
Phew, the financial year is still not over yet, but I have got some time to work on the plug-in again!
|
||||
|
||||
### Fixed and refactored
|
||||
|
||||
- Fixed the inexplicable behaviour when retrieving chunks from the network.
|
||||
- The chunk manager has been layered to be responsible for its own areas and duties. e.g., `DatabaseWriteLayer`, `DatabaseReadLayer`, `NetworkLayer`, `CacheLayer`, and `ArrivalWaitLayer`.
|
||||
- All layers have been tested now!
|
||||
- `LayeredChunkManager` has been implemented to manage these layers. Also tested.
|
||||
- `EntryManager` has been mostly rewritten and also tested.
|
||||
|
||||
- Now we can configure `Never warn` for remote storage size notification again.
|
||||
|
||||
### Tests
|
||||
|
||||
- The following test has been added:
|
||||
- `ConflictManager`.
|
||||
|
||||
## 0.25.46
|
||||
|
||||
26th February, 2026
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unexpected errors no longer occurred when the plug-in was unloaded.
|
||||
- Hidden File Sync now respects selectors.
|
||||
- Registering protocol-handlers now works safely without causing unexpected errors.
|
||||
|
||||
### Refactored
|
||||
|
||||
- `ModuleCheckRemoteSize` has been ported to a serviceFeature, and tests have also been added.
|
||||
- Some unnecessary things have been removed.
|
||||
- LiveSyncManagers has now explicit dependencies.
|
||||
- LiveSyncLocalDB is now responsible for LiveSyncManagers, not accepting the managers as dependencies.
|
||||
- This is to avoid circular dependencies and clarify the ownership of the managers.
|
||||
- ChangeManager has been refactored. This had a potential issue, so something had been fixed, possibly.
|
||||
- Some tests have been ported from Deno's test runner to Vitest to accumulate coverage.
|
||||
|
||||
## 0.25.45
|
||||
|
||||
25th February, 2026
|
||||
|
||||
@@ -89,7 +89,6 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"),
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@lib": path.resolve(__dirname, "./src/lib/src"),
|
||||
src: path.resolve(__dirname, "./src"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineConfig, mergeConfig } from "vitest/config";
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
import viteConfig from "./vitest.config.common";
|
||||
import path from "path";
|
||||
import dotenv from "dotenv";
|
||||
import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands";
|
||||
const defEnv = dotenv.config({ path: ".env" }).parsed;
|
||||
@@ -12,6 +13,11 @@ const headless = !debuggerEnabled && !enableUI;
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
obsidian: path.resolve(__dirname, "./test/harness/obsidian-mock.ts"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
env: env,
|
||||
testTimeout: 40000,
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import { defineConfig, mergeConfig } from "vitest/config";
|
||||
import viteConfig from "./vitest.config.common";
|
||||
|
||||
const importOnlyFiles = ["**/encryption/encryptHKDF.ts"];
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
obsidian: "", // prevent accidental imports of obsidian types in unit tests,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
name: "unit-tests",
|
||||
include: ["**/*unit.test.ts", "**/*.unit.spec.ts"],
|
||||
exclude: ["test/**"],
|
||||
coverage: {
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: ["**/*.test.ts", "src/lib/**/*.test.ts", "**/_*", "src/lib/apps", "src/lib/src/cli"],
|
||||
exclude: [
|
||||
"**/*.test.ts",
|
||||
"src/lib/**/*.test.ts",
|
||||
"**/_*",
|
||||
"src/lib/apps",
|
||||
"src/lib/src/cli",
|
||||
"**/*_obsolete.ts",
|
||||
...importOnlyFiles,
|
||||
],
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user