Compare commits

...

1 Commits

Author SHA1 Message Date
vorotamoroz
f954448ef8 Challenge: Make all modules testable, mockable and spied on! 2026-06-26 09:30:34 +01:00
207 changed files with 17765 additions and 3403 deletions

View File

@@ -37,6 +37,7 @@ export declare class LiveSyncBaseCore<T extends ServiceContext = ServiceContext,
*/
_services: InjectableServiceHub<T> | undefined;
get services(): InjectableServiceHub<T>;
get context(): T;
/**
* Service Modules
*/

View File

@@ -1,6 +1,6 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type PluginManifest, TFile } from "@/deps.ts";
import type { PluginManifest, TFile } from "@/deps.ts";
import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts";
export type { CacheData, FileEventItem } from "@lib/common/types.ts";
export interface PluginDataEntry extends DatabaseEntry {

View File

@@ -1,147 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type PluginManifest } from "@/deps.ts";
import type { EntryDoc, LoadedEntry, FilePathWithPrefix, FilePath, AnyEntry } from "@lib/common/types.ts";
import { LiveSyncCommands } from "@/features/LiveSyncCommands.ts";
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import type ObsidianLiveSyncPlugin from "@/main.ts";
import { PluginDialogModal } from "./PluginDialogModal.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
declare global {
interface OPTIONAL_SYNC_FEATURES {
DISABLE: "DISABLE";
CUSTOMIZE: "CUSTOMIZE";
DISABLE_CUSTOM: "DISABLE_CUSTOM";
}
}
export declare const pluginList: import("svelte/store").Writable<PluginDataExDisplay[]>;
export declare const pluginIsEnumerating: import("svelte/store").Writable<boolean>;
export declare const pluginV2Progress: import("svelte/store").Writable<number>;
export type PluginDataExFile = {
filename: string;
data: string[];
mtime: number;
size: number;
version?: string;
hash?: string;
displayName?: string;
};
export interface IPluginDataExDisplay {
documentPath: FilePathWithPrefix;
category: string;
name: string;
term: string;
displayName?: string;
files: (LoadedEntryPluginDataExFile | PluginDataExFile)[];
version?: string;
mtime: number;
}
export type PluginDataExDisplay = {
documentPath: FilePathWithPrefix;
category: string;
name: string;
term: string;
displayName?: string;
files: PluginDataExFile[];
version?: string;
mtime: number;
};
type LoadedEntryPluginDataExFile = LoadedEntry & PluginDataExFile;
export declare const pluginManifests: Map<string, PluginManifest>;
export declare const pluginManifestStore: import("svelte/store").Writable<Map<string, PluginManifest>>;
export declare class PluginDataExDisplayV2 {
documentPath: FilePathWithPrefix;
category: string;
term: string;
files: LoadedEntryPluginDataExFile[];
name: string;
confKey: string;
constructor(data: IPluginDataExDisplay);
setFile(file: LoadedEntryPluginDataExFile): Promise<void>;
deleteFile(filename: string): void;
_displayName: string | undefined;
_version: string | undefined;
applyLoadedManifest(): void;
get displayName(): string;
get version(): string | undefined;
get mtime(): number;
}
export type PluginDataEx = {
documentPath?: FilePathWithPrefix;
category: string;
name: string;
displayName?: string;
term: string;
files: PluginDataExFile[];
version?: string;
mtime: number;
};
export declare class ConfigSync extends LiveSyncCommands {
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore);
get configDir(): string;
get kvDB(): import("../../lib/src/interfaces/KeyValueDatabase.ts").KeyValueDatabase;
get useV2(): boolean;
get useSyncPluginEtc(): boolean;
isThisModuleEnabled(): boolean;
pluginDialog?: PluginDialogModal;
periodicPluginSweepProcessor: PeriodicProcessor;
pluginList: IPluginDataExDisplay[];
showPluginSyncModal(): void;
hidePluginSyncModal(): void;
onunload(): void;
addRibbonIcon: (icon: string, title: string, callback: (evt: MouseEvent) => unknown) => HTMLElement;
onload(): void;
getFileCategory(filePath: string): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "";
isTargetPath(filePath: string): boolean;
private _everyOnDatabaseInitialized;
_everyBeforeReplicate(showNotice: boolean): Promise<boolean>;
_everyOnResumeProcess(): Promise<boolean>;
_everyAfterResumeProcess(): Promise<boolean>;
reloadPluginList(showMessage: boolean): Promise<void>;
loadPluginData(path: FilePathWithPrefix): Promise<PluginDataExDisplay | false>;
pluginScanProcessor: QueueProcessor<AnyEntry, never>;
pluginScanProcessorV2: QueueProcessor<AnyEntry, never>;
filenameToUnifiedKey(path: string, termOverRide?: string): FilePathWithPrefix;
filenameWithUnifiedKey(path: string, termOverRide?: string): FilePathWithPrefix;
unifiedKeyPrefixOfTerminal(termOverRide?: string): FilePathWithPrefix;
parseUnifiedPath(unifiedPath: FilePathWithPrefix): {
category: string;
device: string;
key: string;
filename: string;
pathV1: FilePathWithPrefix;
};
loadedManifest_mTime: Map<string, number>;
createPluginDataExFileV2(unifiedPathV2: FilePathWithPrefix, loaded?: LoadedEntry): Promise<false | LoadedEntryPluginDataExFile>;
createPluginDataFromV2(unifiedPathV2: FilePathWithPrefix): PluginDataExDisplayV2 | undefined;
updatingV2Count: number;
updatePluginListV2(showMessage: boolean, unifiedFilenameWithKey: FilePathWithPrefix): Promise<void>;
migrateV1ToV2(showMessage: boolean, entry: AnyEntry): Promise<void>;
updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void>;
compareUsingDisplayData(dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean): Promise<boolean>;
applyDataV2(data: PluginDataExDisplayV2, content?: string): Promise<boolean>;
applyData(data: IPluginDataExDisplay, content?: string): Promise<boolean>;
deleteData(data: PluginDataEx): Promise<boolean>;
_anyModuleParsedReplicationResultItem(docs: PouchDB.Core.ExistingDocument<EntryDoc>): Promise<boolean>;
_everyRealizeSettingSyncMode(): Promise<boolean>;
recentProcessedInternalFiles: string[];
makeEntryFromFile(path: FilePath): Promise<false | PluginDataExFile>;
storeCustomisationFileV2(path: FilePath, term: string, force?: boolean): Promise<boolean | PouchDB.Core.Response | undefined>;
storeCustomizationFiles(path: FilePath, termOverRide?: string): Promise<boolean | PouchDB.Core.Response | undefined>;
_anyProcessOptionalFileEvent(path: FilePath): Promise<boolean>;
watchVaultRawEventsAsync(path: FilePath): Promise<boolean>;
scanAllConfigFiles(showMessage: boolean): Promise<void>;
deleteConfigOnDatabase(prefixedFileName: FilePathWithPrefix, forceWrite?: boolean): Promise<boolean>;
scanInternalFiles(): Promise<FilePath[]>;
private _allAskUsingOptionalSyncFeature;
private __askHiddenFileConfiguration;
_anyGetOptionalConflictCheckMethod(path: FilePathWithPrefix): Promise<boolean | "newer">;
private _allSuspendExtraSync;
private _allConfigureOptionalSyncFeature;
configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES): Promise<void>;
getFiles(path: string, lastDepth: number): Promise<string[]>;
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void;
}
export {};

View File

@@ -1,59 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type DocumentID, type EntryDoc, type EntryLeaf } from "@lib/common/types";
import { LiveSyncCommands } from "@/features/LiveSyncCommands";
type ChunkID = DocumentID;
type NoteDocumentID = DocumentID;
type Rev = string;
type ChunkUsageMap = Map<NoteDocumentID, Map<Rev, Set<ChunkID>>>;
export declare class LocalDatabaseMaintenance extends LiveSyncCommands {
onunload(): void;
onload(): void | Promise<void>;
allChunks(includeDeleted?: boolean): Promise<{
used: Set<string>;
existing: Map<string, EntryLeaf>;
}>;
get database(): PouchDB.Database<EntryDoc>;
clearHash(): void;
confirm(title: string, message: string, affirmative?: string, negative?: string): Promise<boolean>;
isAvailable(): boolean;
/**
* Resurrect deleted chunks that are still used in the database.
*/
resurrectChunks(): Promise<void>;
/**
* Commit deletion of files that are marked as deleted.
* This method makes the deletion permanent, and the files will not be recovered.
* After this, chunks that are used in the deleted files become ready for compaction.
*/
commitFileDeletion(): Promise<void>;
/**
* Commit deletion of chunks that are not used in the database.
* This method makes the deletion permanent, and the chunks will not be recovered if the database run compaction.
* After this, the database can shrink the database size by compaction.
* It is recommended to compact the database after this operation (History should be kept once before compaction).
*/
commitChunkDeletion(): Promise<void>;
/**
* Compact the database.
* This method removes all deleted chunks that are not used in the database.
* Make sure all devices are synchronized before running this method.
*/
markUnusedChunks(): Promise<void>;
removeUnusedChunks(): Promise<void>;
scanUnusedChunks(): Promise<{
chunkSet: Set<DocumentID>;
chunkUsageMap: ChunkUsageMap;
unusedSet: Set<DocumentID>;
}>;
/**
* Track changes in the database and update the chunk usage map for garbage collection.
* Note that this only able to perform without Fetch chunks on demand.
*/
trackChanges(fromStart?: boolean, showNotice?: boolean): Promise<void>;
performGC(showingNotice?: boolean): Promise<void>;
analyseDatabase(): Promise<void>;
compactDatabase(): Promise<void>;
gcv3(): Promise<void>;
}
export {};

View File

@@ -1,10 +1,9 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { Plugin, type App, type PluginManifest } from "./deps";
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts";
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
import type { LiveSyncCore } from "./types.ts";
export type { LiveSyncCore, NecessaryObsidianFeature, ObsidianServiceFeatureFunction } from "./types.ts";
export { createObsidianServiceFeature } from "./types.ts";
export default class ObsidianLiveSyncPlugin extends Plugin {
core: LiveSyncCore;
/**

View File

@@ -1,15 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { AbstractModule } from "@/modules/AbstractModule.ts";
import type { LiveSyncCore } from "@/main.ts";
export declare class ModuleMigration extends AbstractModule {
migrateUsingDoctor(skipRebuild?: boolean, activateReason?: string, forceRescan?: boolean): Promise<boolean>;
migrateDisableBulkSend(): Promise<void>;
initialMessage(): Promise<boolean>;
askAgainForSetupURI(): Promise<boolean>;
hasIncompleteDocs(force?: boolean): Promise<boolean>;
hasCompromisedChunks(): Promise<boolean>;
_everyOnFirstInitialize(): Promise<boolean>;
_everyOnLayoutReady(): Promise<boolean>;
onBindFunction(core: LiveSyncCore, services: typeof core.services): void;
}

View File

@@ -1,28 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import type { TFile } from "@/deps.ts";
import { type ReactiveSource } from "octagonal-wheels/dataobject/reactive";
import type { LiveSyncCore } from "@/main.ts";
export declare class ModuleObsidianEvents extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean>;
__performAppReload(): void;
initialCallback: (() => void) | undefined;
swapSaveCommand(): void;
registerWatchEvents(): void;
hasFocus: boolean;
isLastHidden: boolean;
setHasFocus(hasFocus: boolean): void;
watchWindowVisibility(): void;
watchOnline(): void;
watchOnlineAsync(): Promise<void>;
watchWindowVisibilityAsync(): Promise<void>;
watchWorkspaceOpen(file: TFile | null): void;
watchWorkspaceOpenAsync(file: TFile): Promise<void>;
_everyOnLayoutReady(): Promise<boolean>;
private _askReload;
_totalProcessingCount?: ReactiveSource<number>;
private _scheduleAppReload;
_isReloadingScheduled(): boolean;
onBindFunction(core: LiveSyncCore, services: typeof core.services): void;
}

View File

@@ -1,8 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LiveSyncCore } from "@/main.ts";
import { AbstractModule } from "@/modules/AbstractModule.ts";
export declare class ModuleObsidianMenu extends AbstractModule {
_everyOnloadStart(): Promise<boolean>;
onBindFunction(core: LiveSyncCore, services: typeof core.services): void;
}

View File

@@ -0,0 +1,3 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
export type { ModuleDev } from "../../serviceFeatures/devFeature/types.ts";

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { ItemView, WorkspaceLeaf } from "@/deps.ts";
import TestPaneComponent from "./TestPane.svelte";
import type ObsidianLiveSyncPlugin from "@/main.ts";
import type { ModuleDev } from "@/modules/extras/ModuleDev.ts";
export declare const VIEW_TYPE_TEST = "ols-pane-test";
declare global {
interface LSEvents {
"debug-sync-status": string[];
}
}
export declare class TestPaneView extends ItemView {
component?: TestPaneComponent;
plugin: ObsidianLiveSyncPlugin;
moduleDev: ModuleDev;
icon: string;
title: string;
navigation: boolean;
getIcon(): string;
constructor(leaf: WorkspaceLeaf, plugin: ObsidianLiveSyncPlugin, moduleDev: ModuleDev);
getViewType(): string;
getDisplayText(): string;
onOpen(): Promise<void>;
onClose(): Promise<void>;
}

View File

@@ -1,8 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
export declare class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean>;
showGlobalHistory(): void;
onBindFunction(core: typeof this.core, services: typeof core.services): void;
}

View File

@@ -1,13 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type FilePathWithPrefix, type diff_result } from "@lib/common/types.ts";
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import type { LiveSyncCore } from "@/main.ts";
export declare class ModuleInteractiveConflictResolver extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean>;
_anyResolveConflictByUI(filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean>;
allConflictCheck(): Promise<void>;
pickFileForResolve(): Promise<boolean>;
_allScanStat(): Promise<boolean>;
onBindFunction(core: LiveSyncCore, services: typeof core.services): void;
}

View File

@@ -1,11 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type TFile } from "@/deps.ts";
import type { FilePathWithPrefix, DocumentID } from "@lib/common/types.ts";
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
export declare class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean>;
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID): void;
fileHistory(): Promise<void>;
onBindFunction(core: typeof this.core, services: typeof core.services): void;
}

View File

@@ -1,24 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type ObsidianLiveSyncSettings } from "@lib/common/types";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import type { ServiceContext } from "@lib/services/base/ServiceBase.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
export declare class ModuleObsidianSettingsAsMarkdown extends AbstractModule {
_everyOnloadStart(): Promise<boolean>;
extractSettingFromWholeText(data: string): {
preamble: string;
body: string;
postscript: string;
};
parseSettingFromMarkdown(filename: string, data?: string): Promise<{
preamble: string;
body: string;
postscript: string;
}>;
checkAndApplySettingFromMarkdown(filename: string, automated?: boolean): Promise<void>;
generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial<ObsidianLiveSyncSettings>;
saveSettingToMarkdown(filename: string): Promise<void>;
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub<ServiceContext>): void;
}

View File

@@ -1,12 +0,0 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts";
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import type { LiveSyncCore } from "@/main.ts";
export declare class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
settingTab: ObsidianLiveSyncSettingTab;
_everyOnloadStart(): Promise<boolean>;
openSetting(): void;
get appId(): string;
onBindFunction(core: LiveSyncCore, services: typeof core.services): void;
}

View File

@@ -1,121 +1,3 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts";
import { AbstractModule } from "@/modules/AbstractModule.ts";
/**
* User modes for onboarding and setup
*/
export declare const enum UserMode {
/**
* New User Mode - for users who are new to the plugin
*/
NewUser = "new-user",
/**
* Existing User Mode - for users who have used the plugin before, or just configuring again
*/
ExistingUser = "existing-user",
/**
* Unknown User Mode - for cases where the user mode is not determined
*/
Unknown = "unknown",
/**
* Update User Mode - for users who are updating configuration. May be `existing-user` as well, but possibly they want to treat it differently.
*/
Update = "unknown" // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value
}
/**
* Setup Manager to handle onboarding and configuration setup
*/
export declare class SetupManager extends AbstractModule {
get dialogManager(): import("../../lib/src/UI/svelteDialog.ts").SvelteDialogManagerBase<import("../../lib/src/services/base/ServiceBase.ts").ServiceContext>;
/**
* Starts the onboarding process
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
*/
startOnBoarding(): Promise<boolean>;
/**
* Handles the onboarding process based on user mode
* @param userMode
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
*/
onOnboard(userMode: UserMode): Promise<boolean>;
/**
* Handles setup using a setup URI
* @param userMode
* @param setupURI
* @returns Promise that resolves to true if onboarding completed successfully, false otherwise
*/
onUseSetupURI(userMode: UserMode, setupURI?: string): Promise<boolean>;
/**
* Handles manual setup for CouchDB
* @param userMode
* @param currentSetting
* @param activate Whether to activate the CouchDB as remote type
* @returns Promise that resolves to true if setup completed successfully, false otherwise
*/
onCouchDBManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise<boolean>;
/**
* Handles manual setup for S3-compatible bucket
* @param userMode
* @param currentSetting
* @param activate Whether to activate the Bucket as remote type
* @returns Promise that resolves to true if setup completed successfully, false otherwise
*/
onBucketManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise<boolean>;
/**
* Handles manual setup for P2P
* @param userMode
* @param currentSetting
* @param activate Whether to activate the P2P as remote type (as P2P Only setup)
* @returns Promise that resolves to true if setup completed successfully, false otherwise
*/
onP2PManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise<boolean>;
/**
* Handles only E2EE configuration
* @param userMode
* @param currentSetting
* @returns
*/
onlyE2EEConfiguration(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings): Promise<boolean>;
/**
* Handles manual configuration flow (E2EE + select server)
* @param originalSetting
* @param userMode
* @returns
*/
onConfigureManually(originalSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean>;
/**
* Handles server selection during manual configuration
* @param currentSetting
* @param userMode
* @returns
*/
onSelectServer(currentSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean>;
/**
* Confirms and applies settings obtained from the wizard
* @param newConf
* @param _userMode
* @param activate Whether to activate the remote type in the new settings
* @param extra Extra function to run before applying settings
* @returns Promise that resolves to true if settings applied successfully, false otherwise
*/
onConfirmApplySettingsFromWizard(newConf: ObsidianLiveSyncSettings, _userMode: UserMode, activate?: boolean, extra?: () => void): Promise<boolean>;
/**
* Prompts the user with QR code scanning instructions
* @returns Promise that resolves to false as QR code instruction dialog does not yield settings directly
*/
onPromptQRCodeInstruction(): Promise<boolean>;
/**
* Decodes settings from a QR code string and applies them
* @param qr QR code string containing encoded settings
* @returns Promise that resolves to true if settings applied successfully, false otherwise
*/
decodeQR(qr: string): Promise<boolean>;
/**
* Applies the new settings to the core settings and saves them
* @param newConf
* @param userMode
* @returns Promise that resolves to true if settings applied successfully, false otherwise
*/
applySetting(newConf: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean>;
}
export { UserMode, getSetupManager, type SetupManagerAPI as SetupManager, } from "@/serviceFeatures/setupManager/index.ts";

View File

@@ -0,0 +1,12 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { ConfigSyncHost } from "./types.ts";
/**
* Registers commands, ribbon icons, and custom SVG icons for configuration synchronisation.
*
* @param host - The service feature host.
* @param handlers - Action triggers.
*/
export declare function registerConfigSyncCommands(host: ConfigSyncHost, handlers: {
showPluginSyncModal: () => void;
}): void;

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { FilePath } from "@lib/common/types.ts";
import type { ConfigSyncHost } from "./types.ts";
import type { ConfigSyncState } from "./state.ts";
/**
* Binds all required events for configuration synchronisation onto the application lifecycle and replicator.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param handlers - Event response triggers.
*/
export declare function bindConfigSyncEvents(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, handlers: {
showPluginSyncModal: () => void;
watchVaultRawEventsAsync: (path: FilePath) => Promise<boolean>;
}): void;
/**
* Configures the customisation synchronisation status.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param mode - The sync activation mode option.
*/
export declare function configureHiddenFileSync(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, mode: "DISABLE" | "CUSTOMIZE" | "DISABLE_CUSTOM"): Promise<void>;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { ConfigSyncServices, ConfigSyncModules } from "./types.ts";
/**
* A service feature hook that initialises and manages the configuration synchronisation module.
* This sets up the scanning processors, watches for local/remote config changes, and binds UI dialogues.
*/
export declare const useConfigSync: import("@/types.ts").ObsidianServiceFeatureFunction<ConfigSyncServices, ConfigSyncModules, "plugin" | "app" | "liveSyncPlugin", void>;

View File

@@ -0,0 +1,128 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { FilePath, FilePathWithPrefix, LoadedEntry, AnyEntry } from "@lib/common/types.ts";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import type { ConfigSyncHost, IPluginDataExDisplay, PluginDataExDisplay, LoadedEntryPluginDataExFile, PluginDataExFile } from "./types.ts";
import type { ConfigSyncState } from "./state.ts";
/**
* Class representing plugin configuration metadata and display structures for V2 synchronisation.
*/
export declare class PluginDataExDisplayV2 {
documentPath: FilePathWithPrefix;
category: string;
term: string;
files: LoadedEntryPluginDataExFile[];
name: string;
confKey: string;
constructor(data: IPluginDataExDisplay);
setFile(file: LoadedEntryPluginDataExFile): Promise<void>;
deleteFile(filename: string): void;
_displayName: string | undefined;
_version: string | undefined;
applyLoadedManifest(): void;
get displayName(): string;
get version(): string | undefined;
get mtime(): number;
}
/**
* Reloads the plugin list by clearing the cache and executing updates.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param showMessage - Whether to display progress messages.
*/
export declare function reloadPluginList(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, showMessage: boolean): Promise<void>;
/**
* Loads plugin configuration data from the database.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param path - The database document path.
* @returns Deserialised plugin display details, or false if not found.
*/
export declare function loadPluginData(host: ConfigSyncHost, log: LogFunction, path: FilePathWithPrefix): Promise<PluginDataExDisplay | false>;
/**
* Creates a V2 plugin metadata descriptor from the unified path.
*
* @param host - The service feature host.
* @param unifiedPathV2 - V2 unified path database key.
* @returns Initialised plugin display descriptor.
*/
export declare function createPluginDataFromV2(host: ConfigSyncHost, unifiedPathV2: FilePathWithPrefix): PluginDataExDisplayV2 | undefined;
/**
* Creates a file entry structure from a V2 unified database document.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param unifiedPathV2 - V2 unified path database key.
* @param loaded - Pre-fetched database document, if available.
* @returns The V2 file descriptor.
*/
export declare function createPluginDataExFileV2(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, unifiedPathV2: FilePathWithPrefix, loaded?: LoadedEntry): Promise<false | LoadedEntryPluginDataExFile>;
/**
* Updates the plugin display list for a V2 unified document path.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param showMessage - Whether to show notifications.
* @param unifiedFilenameWithKey - Unified database document path.
*/
export declare function updatePluginListV2(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, showMessage: boolean, unifiedFilenameWithKey: FilePathWithPrefix): Promise<void>;
/**
* Scans the database and updates the active configuration items list.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param showMessage - Whether to show progress messages.
* @param updatedDocumentPath - Optional target document path to narrow update.
*/
export declare function updatePluginList(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void>;
/**
* Migrates configuration sync structure V1 (single monolithic metadata doc) to V2 (split documents).
*
* @param host - The service feature host.
* @param log - The logging function.
* @param showMessage - Whether to show progress logs in UI.
* @param entry - The database entry to migrate.
*/
export declare function migrateV1ToV2(host: ConfigSyncHost, log: LogFunction, showMessage: boolean, entry: AnyEntry): Promise<void>;
/**
* Helper to recursively list files in Obsidian storage up to a given depth.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param path - The folder path.
* @param lastDepth - Remaining depth levels to traverse.
* @returns Array of file paths found.
*/
export declare function getFiles(host: ConfigSyncHost, log: LogFunction, path: string, lastDepth: number): Promise<string[]>;
/**
* Scans internal configuration files in Obsidian storage config folder.
*
* @param host - The service feature host.
* @param log - The logging function.
* @returns Array of configuration file paths.
*/
export declare function scanInternalFiles(host: ConfigSyncHost, log: LogFunction): Promise<FilePath[]>;
/**
* Creates a file details entry from a local storage file.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param path - Local file path.
* @returns File descriptor details, or false if stat fails.
*/
export declare function makeEntryFromFile(host: ConfigSyncHost, log: LogFunction, path: FilePath): Promise<false | PluginDataExFile>;
/**
* Creates a QueueProcessor for scanning V1 plugins.
*/
export declare function createPluginScanProcessor(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState): QueueProcessor<AnyEntry, never>;
/**
* Creates a QueueProcessor for scanning V2 plugins.
*/
export declare function createPluginScanProcessorV2(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState): QueueProcessor<AnyEntry, never>;

View File

@@ -0,0 +1,30 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import type { PluginDialogModal } from "@/features/ConfigSync/PluginDialogModal.ts";
import type { IPluginDataExDisplay } from "./types.ts";
/**
* Represents the runtime state of the configuration synchronisation feature.
* This state is scoped to the feature lifecycle, containing active processors,
* cached metadata, and UI dialogues.
*/
export interface ConfigSyncState {
pluginList: IPluginDataExDisplay[];
pluginDialog: PluginDialogModal | undefined;
periodicPluginSweepProcessor: PeriodicProcessor | undefined;
conflictResolutionProcessor: QueueProcessor<any, any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
loadedManifest_mTime: Map<string, number>;
updatingV2Count: number;
updatePluginListV2Task: (() => void) | undefined;
pluginScanProcessor: QueueProcessor<any, any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
pluginScanProcessorV2: QueueProcessor<any, any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
recentProcessedInternalFiles: string[];
}
/**
* Creates and initialises a new configuration synchronisation state object
* with default values.
*
* @returns A freshly initialised {@link ConfigSyncState} object.
*/
export declare function createConfigSyncState(): ConfigSyncState;

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { PluginManifest } from "@/deps.ts";
import type { PluginDataExDisplay } from "./types.ts";
/**
* A Svelte store holding the list of plug-ins and their synchronisation details for UI display.
*/
export declare const pluginList: import("svelte/store").Writable<PluginDataExDisplay[]>;
/**
* A Svelte store indicating whether the plug-in enumeration process is currently running.
*/
export declare const pluginIsEnumerating: import("svelte/store").Writable<boolean>;
/**
* A Svelte store representing the progress of version 2 plug-in synchronisation (from 0 to 1).
*/
export declare const pluginV2Progress: import("svelte/store").Writable<number>;
/**
* A local map caching plug-in manifests by their identifier keys.
*/
export declare const pluginManifests: Map<string, PluginManifest>;
/**
* A Svelte store wrapper around {@link pluginManifests} to notify subscribers of updates.
*/
export declare const pluginManifestStore: import("svelte/store").Writable<Map<string, PluginManifest>>;
/**
* Updates a plug-in's manifest inside {@link pluginManifests} and notifies the store subscribers
* if the manifest has changed.
*
* @param key - The plug-in identifier key.
* @param manifest - The new plug-in manifest data.
*/
export declare function setManifest(key: string, manifest: PluginManifest): void;

View File

@@ -0,0 +1,111 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { FilePath, FilePathWithPrefix } from "@lib/common/types.ts";
import type { ConfigSyncHost, IPluginDataExDisplay, PluginDataEx } from "./types.ts";
import type { ConfigSyncState } from "./state.ts";
import { PluginDataExDisplayV2 } from "./pluginScanner.ts";
/**
* Checks whether the configuration synchronisation module is enabled in settings.
*
* @param host - The service feature host.
* @returns True if enabled, false otherwise.
*/
export declare function isThisModuleEnabled(host: ConfigSyncHost): boolean;
/**
* Compares two plugin data sets by displaying a resolve modal dialog.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param dataA - Left hand configuration item.
* @param dataB - Right hand configuration item.
* @param compareEach - Whether to compare file by file.
* @returns Promise resolving to true if applied successfully, false otherwise.
*/
export declare function compareUsingDisplayData(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, dataA: IPluginDataExDisplay, dataB: IPluginDataExDisplay, compareEach?: boolean): Promise<boolean>;
/**
* Applies customization data for V2 split files.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param data - The plugin V2 display model.
* @param content - Optional specific file content override.
* @returns True if applied successfully, false otherwise.
*/
export declare function applyDataV2(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, data: PluginDataExDisplayV2, content?: string): Promise<boolean>;
/**
* Applies configuration data to local storage and updates active systems.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param data - The configuration display description.
* @param content - Optional merged file content.
* @returns True if successful, false otherwise.
*/
export declare function applyData(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, data: IPluginDataExDisplay, content?: string): Promise<boolean>;
/**
* Deletes configuration documents from the database and runs status updates.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param data - The target plugin configurations to clean up.
* @returns True if successful, false otherwise.
*/
export declare function deleteData(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, data: PluginDataEx): Promise<boolean>;
/**
* Stores a customization file in V2 database split format.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param path - Local file path.
* @param term - Local terminal name.
* @param force - True to bypass change verification checks.
* @returns Database operation response structure.
*/
export declare function storeCustomisationFileV2(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, path: FilePath, term: string, force?: boolean): Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
/**
* Stores local customization files to database records.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param path - Local file path.
* @param termOverRide - Device identifier override.
* @returns DB operation response.
*/
export declare function storeCustomizationFiles(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, path: FilePath, termOverRide?: string): Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
/**
* Marks config file deleted in the database.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param prefixedFileName - Unified db file path.
* @param forceWrite - Force deletion write operation.
* @returns True if successfully marked deleted, false otherwise.
*/
export declare function deleteConfigOnDatabase(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, prefixedFileName: FilePathWithPrefix, forceWrite?: boolean): Promise<boolean>;
/**
* Scans all customization config files, comparing local and DB databases.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param showMessage - True to print progress messages.
*/
export declare function scanAllConfigFiles(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, showMessage: boolean): Promise<void>;
/**
* Monitors and processes Obsidian storage raw file events for synchronisation.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The configuration sync state.
* @param path - The modified file path.
* @returns True if processed, false otherwise.
*/
export declare function watchVaultRawEventsAsync(host: ConfigSyncHost, log: LogFunction, state: ConfigSyncState, path: FilePath): Promise<boolean>;

View File

@@ -0,0 +1,71 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { NecessaryObsidianServices } from "@/types.ts";
import type { FilePathWithPrefix, LoadedEntry } from "@lib/common/types.ts";
/**
* A union of service keys required by the configuration synchronisation feature.
*/
export type ConfigSyncServices = "API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "keyValueDB" | "replication" | "conflict" | "control";
/**
* A union of service module keys required by the configuration synchronisation feature.
*/
export type ConfigSyncModules = "storageAccess" | "fileHandler";
/**
* The host type representing the injected service container with configuration synchronisation capabilities.
*/
export type ConfigSyncHost = NecessaryObsidianServices<ConfigSyncServices, ConfigSyncModules, "app" | "plugin">;
/**
* Represents metadata and content structure of an individual file within a plug-in.
*/
export type PluginDataExFile = {
filename: string;
data: string[];
mtime: number;
size: number;
version?: string;
hash?: string;
displayName?: string;
};
/**
* Defines the display properties and structure for a plug-in sync entry used in UI dialogues.
*/
export interface IPluginDataExDisplay {
documentPath: FilePathWithPrefix;
category: string;
name: string;
term: string;
displayName?: string;
files: (LoadedEntryPluginDataExFile | PluginDataExFile)[];
version?: string;
mtime: number;
}
/**
* Represents the display model of a plug-in, including its category, file list, and modification time.
*/
export type PluginDataExDisplay = {
documentPath: FilePathWithPrefix;
category: string;
name: string;
term: string;
displayName?: string;
files: PluginDataExFile[];
version?: string;
mtime: number;
};
/**
* Combines a database loaded entry with plug-in specific file metadata.
*/
export type LoadedEntryPluginDataExFile = LoadedEntry & PluginDataExFile;
/**
* Represents a plug-in's synchronisation schema payload stored in the database.
*/
export type PluginDataEx = {
documentPath?: FilePathWithPrefix;
category: string;
name: string;
displayName?: string;
term: string;
files: PluginDataExFile[];
version?: string;
mtime: number;
};

View File

@@ -0,0 +1,130 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { FilePathWithPrefix } from "@lib/common/types.ts";
import type { PluginDataEx } from "./types.ts";
/**
* A zero-width space character used as a field delimiter in the custom serialisation format.
*/
export declare const d = "\u200B";
/**
* A newline character used as a record delimiter in the custom serialisation format.
*/
export declare const d2 = "\n";
/**
* Serialises a plugin data structure into a custom compact string format.
*
* @param data - The plugin data to serialise.
* @returns The serialised compact string.
*/
export declare function serialize(data: PluginDataEx): string;
/**
* A placeholder header string used to represent the start of the serialised configuration chunk stream.
*/
export declare const DUMMY_HEAD: string;
/**
* A placeholder footer string used to represent the end of the serialised configuration chunk stream.
*/
export declare const DUMMY_END: string;
/**
* Splits source strings by compact format delimiters.
*
* @param sources - The source strings to split.
* @returns Split string array.
*/
export declare function splitWithDelimiters(sources: string[]): string[];
/**
* Creates a tokenizer helper for deserialisation parsing.
*
* @param source - Split string token sources.
* @returns Tokenizer helper object.
*/
export declare function getTokenizer(source: string[]): {
next(): string;
nextLine(): void;
};
/**
* Deserialises tokenised array lines into a plugin data structure.
*
* @param str - The array lines to deserialise.
* @returns Deserialised plugin data.
*/
export declare function deserialize2(str: string[]): PluginDataEx;
/**
* Deserialises file content string arrays into a target object representation.
* Supports compact prefix format, JSON parsing, and YAML fallback.
*
* @param str - Content string lines.
* @param def - Fallback default value.
* @returns Deserialised object structure.
*/
export declare function deserialize<T>(str: string[], def: T): any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
/**
* Maps a configuration category and base path to a vault relative subdirectory.
*
* @param category - Configuration category.
* @param configDir - The main system configuration directory path.
* @returns Vault folder suffix path.
*/
export declare function categoryToFolder(category: string, configDir?: string): string;
/**
* Resolves local file category based on the system configuration directory.
*
* @param filePath - Local file path.
* @param configDir - Vault system config folder name.
* @param useV2 - Whether V2 plugin structure is active.
* @param useSyncPluginEtc - Whether custom subfolders under plugins are synchronised.
* @returns Category identifier.
*/
export declare function getFileCategory(filePath: string, configDir: string, useV2: boolean, useSyncPluginEtc: boolean): "CONFIG" | "THEME" | "SNIPPET" | "PLUGIN_MAIN" | "PLUGIN_ETC" | "PLUGIN_DATA" | "";
/**
* Checks if the file path is a valid customization sync path candidate.
*
* @param filePath - Target file path.
* @param configDir - Vault configuration folder path.
* @param useV2 - Whether V2 sync is enabled.
* @param useSyncPluginEtc - Whether config files sync is enabled.
* @returns True if path is a sync target.
*/
export declare function isTargetPath(filePath: string, configDir: string, useV2: boolean, useSyncPluginEtc: boolean): boolean;
/**
* Converts local path into unified database document path.
*
* @param path - Local file path.
* @param term - Active device name.
* @param configDir - Vault config directory name.
* @param useV2 - Whether V2 is active.
* @param useSyncPluginEtc - Whether sync plugin etc is active.
* @returns The database path identifier.
*/
export declare function filenameToUnifiedKey(path: string, term: string, configDir: string, useV2: boolean, useSyncPluginEtc: boolean): FilePathWithPrefix;
/**
* Converts local path into V2 unified database document path.
*
* @param path - Local file path.
* @param term - Active device name.
* @param configDir - Vault config directory name.
* @param useV2 - Whether V2 is active.
* @param useSyncPluginEtc - Whether sync plugin etc is active.
* @returns The database path identifier.
*/
export declare function filenameWithUnifiedKey(path: string, term: string, configDir: string, useV2: boolean, useSyncPluginEtc: boolean): FilePathWithPrefix;
/**
* Returns database prefix path filter for a terminal configuration.
*
* @param term - Active device name.
* @returns Database path prefix string.
*/
export declare function unifiedKeyPrefixOfTerminal(term: string): FilePathWithPrefix;
/**
* Parses a V2 unified database path into its constituent components.
*
* @param unifiedPath - Unified path metadata document identifier.
* @returns Parsed components.
*/
export declare function parseUnifiedPath(unifiedPath: FilePathWithPrefix): {
category: string;
device: string;
key: string;
filename: string;
pathV1: FilePathWithPrefix;
};

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { DatabaseMaintenanceHost } from "./types.ts";
/**
* Registers commands and event listeners for database maintenance capabilities.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function registerDatabaseMaintenanceCommands(host: DatabaseMaintenanceHost, log: LogFunction): void;

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { DatabaseMaintenanceHost } from "./types.ts";
/**
* Commands the remote CouchDB database to perform compaction and monitors its progress.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function compactDatabase(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { DatabaseMaintenanceHost } from "./types.ts";
/**
* Analyses the database and details chunk utilisation, copying a TSV summary to the clipboard.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function analyseDatabase(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;

View File

@@ -0,0 +1,80 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type DocumentID } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { DatabaseMaintenanceHost } from "./types.ts";
type ChunkID = DocumentID;
type NoteDocumentID = DocumentID;
type Rev = string;
type ChunkUsageMap = Map<NoteDocumentID, Map<Rev, Set<ChunkID>>>;
/**
* Resurrects deleted chunks that are still referenced and used by files in the database.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function resurrectChunks(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;
/**
* Commits the deletion of files marked as deleted, removing them permanently from the database.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function commitFileDeletion(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;
/**
* Permanently deletes chunks already marked as deleted.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function commitChunkDeletion(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;
/**
* Marks chunks that are not referenced by any files in the database as deleted.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function markUnusedChunks(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;
/**
* Directly removes unused chunks from the local database.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function removeUnusedChunks(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;
/**
* Scans key-value store logs to calculate unused chunks.
*
* @param host - The service container host.
* @returns Scan summary.
*/
export declare function scanUnusedChunks(host: DatabaseMaintenanceHost): Promise<{
chunkSet: Set<DocumentID>;
chunkUsageMap: ChunkUsageMap;
unusedSet: Set<DocumentID>;
}>;
/**
* Tracks database changes to maintain the chunk usage map cache.
*
* @param host - The service container host.
* @param log - The logger function.
* @param fromStart - Whether to force scan from the beginning of sequence.
* @param showNotice - Whether to show log notices to user.
*/
export declare function trackChanges(host: DatabaseMaintenanceHost, log: LogFunction, fromStart?: boolean, showNotice?: boolean): Promise<void>;
/**
* Perfroms the legacy Garbage Collection process, scanning and removing unreferenced chunks.
*
* @param host - The service container host.
* @param log - The logger function.
* @param showingNotice - Whether to show log notices to user.
*/
export declare function performGC(host: DatabaseMaintenanceHost, log: LogFunction, showingNotice?: boolean): Promise<void>;
/**
* Runs Garbage Collection V3, which validates synchronization progress across connected nodes before deleting.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function gcv3(host: DatabaseMaintenanceHost, log: LogFunction): Promise<void>;
export {};

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { DatabaseMaintenanceServices } from "./types.ts";
/**
* A service feature hook that initialises and manages the database maintenance module.
* This registers maintenance commands and provides database compaction, diagnostic, and garbage collection utilities.
*/
export declare const useDatabaseMaintenance: import("@/types.ts").ObsidianServiceFeatureFunction<DatabaseMaintenanceServices, "storageAccess", "plugin", {
gcv3: () => Promise<void>;
analyseDatabase: () => Promise<void>;
compactDatabase: () => Promise<void>;
performGC: (showingNotice?: boolean) => Promise<void>;
resurrectChunks: () => Promise<void>;
commitFileDeletion: () => Promise<void>;
commitChunkDeletion: () => Promise<void>;
markUnusedChunks: () => Promise<void>;
removeUnusedChunks: () => Promise<void>;
}>;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { NecessaryObsidianServices } from "@/types.ts";
/**
* A union of service keys required by the database maintenance feature.
*/
export type DatabaseMaintenanceServices = "API" | "setting" | "UI" | "database" | "keyValueDB" | "replication" | "replicator";
/**
* A union of service module keys required by the database maintenance feature.
*/
export type DatabaseMaintenanceModules = "storageAccess";
/**
* The host type representing the injected service container with database maintenance capabilities.
*/
export type DatabaseMaintenanceHost = NecessaryObsidianServices<DatabaseMaintenanceServices, DatabaseMaintenanceModules, "plugin">;

View File

@@ -0,0 +1,49 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type LOG_LEVEL } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { DatabaseMaintenanceHost } from "./types.ts";
/**
* Checks if garbage collection can be performed based on plug-in settings.
*
* @param host - The service container host.
* @param log - The logger function.
* @returns True if garbage collection is available, false otherwise.
*/
export declare function isGCAvailable(host: DatabaseMaintenanceHost, log: LogFunction): boolean;
/**
* Shows a confirmation dialogue to the user with customiseable options.
*
* @param host - The service container host.
* @param title - The title of the dialogue.
* @param message - The body message of the dialogue.
* @param affirmative - The positive confirmation label.
* @param negative - The negative cancellation label.
* @returns A promise resolving to true if approved, false otherwise.
*/
export declare function confirmDialogue(host: DatabaseMaintenanceHost, title: string, message: string, affirmative?: string, negative?: string): Promise<boolean>;
/**
* Retrieves all chunk information from the local database.
*
* @param host - The service container host.
* @param log - The logger function.
* @param includeDeleted - Whether to include deleted chunks in the scan.
* @returns A promise resolving to the retrieved chunk collections.
*/
export declare function retrieveAllChunks(host: DatabaseMaintenanceHost, log: LogFunction, includeDeleted?: boolean): Promise<{
used: Set<string>;
existing: Map<string, import("@lib/common/types.ts").EntryLeaf>;
}>;
/**
* Creates a progress bar tracker that logs lifecycle states.
*
* @param log - The logger function.
* @param prefix - A text prefix to prepend to all progress messages.
* @param level - The log level for progress updates.
* @returns An object to log, perform once-off updates, or finish the progress.
*/
export declare function createProgressBar(log: LogFunction, prefix?: string, level?: LOG_LEVEL): {
log: (msg: string) => void;
once: (msg: string) => void;
done: (msg?: string) => void;
};

View File

@@ -0,0 +1,30 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { DevFeatureHost } from "./types.ts";
import type { DevFeatureState } from "./state.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
/**
* Commits a log entry for missing translation keys inside local settings directory.
*
* @param host - The service feature host context.
* @param log - The logger function.
* @param key - The missing translation key.
*/
export declare function onMissingTranslation(host: DevFeatureHost, log: LogFunction, key: string): Promise<void>;
/**
* Automatically creates a conflicted revision for testing conflict resolution.
*
* @param host - The service feature host context.
*/
export declare function createConflict(host: DevFeatureHost): Promise<void>;
/**
* Appends a test result to the Svelte writable store.
*
* @param state - The active feature state.
* @param name - The test name or category.
* @param key - The unique test identifier.
* @param result - True if passed, false if failed.
* @param summary - Optional summary message.
* @param message - Optional detailed stacktrace or assertion info.
*/
export declare function addTestResult(state: DevFeatureState, name: string, key: string, result: boolean, summary?: string, message?: string): void;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { DevFeatureServices, DevFeatureModules } from "./types.ts";
/**
* A service feature hook that initialises dev/testing utilities.
* Handles missing translation captures, test panels, and debugging commands.
*/
export declare const useDevFeature: import("@/types.ts").ObsidianServiceFeatureFunction<DevFeatureServices, DevFeatureModules, "app" | "liveSyncPlugin", void>;

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type Writable } from "svelte/store";
/**
* Interface representing the state of the dev feature, including test results.
*/
export interface DevFeatureState {
testResults: Writable<[boolean, string, string][]>;
}
/**
* Creates the initial state object.
*/
export declare function createInitialState(): DevFeatureState;

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { NecessaryObsidianServices } from "@/types.ts";
import { type Writable } from "svelte/store";
/**
* Service keys required by the development utility feature.
*/
export type DevFeatureServices = "API" | "setting" | "appLifecycle" | "test" | "path" | "vault" | "keyValueDB" | "database" | "UI";
/**
* Service modules required by the development utility feature.
*/
export type DevFeatureModules = "storageAccess" | "databaseFileAccess";
/**
* The host type representing the injected service container with dev capabilities.
*/
export type DevFeatureHost = NecessaryObsidianServices<DevFeatureServices, DevFeatureModules, "app" | "liveSyncPlugin">;
/**
* Interface for the dev feature matching the shape expected by Svelte test panes.
*/
export interface ModuleDev {
testResults: Writable<[boolean, string, string][]>;
}

View File

@@ -0,0 +1,9 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { GlobalHistoryHost } from "./types.ts";
/**
* Shows the global vault history window.
*
* @param host - The service feature host context.
*/
export declare function showGlobalHistory(host: GlobalHistoryHost): void;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { GlobalHistoryServices } from "./types.ts";
/**
* A service feature hook that initialises and manages the Global History view.
* Registers the global history view and ribbon command.
*/
export declare const useGlobalHistory: import("@/types.ts").ObsidianServiceFeatureFunction<GlobalHistoryServices, never, "liveSyncPlugin", void>;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { NecessaryObsidianServices } from "@/types.ts";
/**
* Service keys required by the global history feature.
*/
export type GlobalHistoryServices = "API" | "appLifecycle";
/**
* Service modules required by the global history feature.
*/
export type GlobalHistoryModules = never;
/**
* The host type representing the injected service container with global history capabilities.
*/
export type GlobalHistoryHost = NecessaryObsidianServices<GlobalHistoryServices, never, "liveSyncPlugin">;

View File

@@ -0,0 +1,10 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { HiddenFileSyncHost } from "./types.ts";
export declare function registerHiddenFileSyncCommands(host: HiddenFileSyncHost, handlers: {
isReady: () => boolean;
initialiseInternalFileSync: (mode: "safe", showNotice: boolean) => Promise<void>;
scanAllStorageChanges: (showNotice: boolean) => Promise<boolean>;
scanAllDatabaseChanges: (showNotice: boolean) => Promise<boolean>;
applyOfflineChanges: (showNotice: boolean) => Promise<void>;
}): void;

View File

@@ -0,0 +1,71 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import type { FilePathWithPrefix, LoadedEntry, MetaEntry, DocumentID } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
/**
* Enqueues a file path for a conflict check if it is not already pending.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The prefix-marked document path.
*/
export declare function queueConflictCheck(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: FilePathWithPrefix): void;
/**
* Marks a conflict check as finished by removing the path from the pending conflicts set.
*
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The prefix-marked document path.
*/
export declare function finishConflictCheck(state: HiddenFileSyncState, path: FilePathWithPrefix): void;
/**
* Re-enqueues a file path for conflict check processing, clearing the previous state first.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The prefix-marked document path.
*/
export declare function requeueConflictCheck(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: FilePathWithPrefix): void;
/**
* Scans the database for any conflicted hidden file entries and enqueues them for resolution.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
*/
export declare function resolveConflictOnInternalFiles(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState): Promise<void>;
/**
* Resolves a conflict automatically by keeping the revision with the newer modification timestamp and removing the older one.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param id - The Document ID in the database.
* @param path - The prefix-marked file path.
* @param currentDoc - The current metadata document version.
* @param currentRev - The revision of the current document.
* @param conflictedRev - The conflicted revision to compare.
*/
export declare function resolveByNewerEntry(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, id: DocumentID, path: FilePathWithPrefix, currentDoc: MetaEntry, currentRev: string, conflictedRev: string): Promise<void>;
/**
* Opens a JSON interactive merge dialogue to let the user resolve conflict revisions manually.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param docA - Loaded entry revision A.
* @param docB - Loaded entry revision B.
* @returns A promise resolving to true if the merge dialogue was successfully completed; otherwise, false.
*/
export declare function showJSONMergeDialogAndMerge(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, docA: LoadedEntry, docB: LoadedEntry): Promise<boolean>;
/**
* Creates a QueueProcessor configuration to handle hidden file conflict resolution sequentially.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @returns A QueueProcessor managing file paths with conflicts.
*/
export declare function createConflictResolutionProcessor(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState): QueueProcessor<FilePathWithPrefix, any>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration

View File

@@ -0,0 +1,131 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { UXFileInfo, UXStat, FilePath, UXDataWriteOptions, MetaEntry, LoadedEntry } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { InternalFileInfo } from "@/common/types.ts";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
/**
* Ensures that the directory structure for a given path exists in the storage.
* If the directory does not exist, it will be created recursively.
*
* @param host - The service feature host providing access to services.
* @param path - The file path for which the parent directories should be ensured.
*/
export declare function ensureDir(host: HiddenFileSyncHost, path: FilePath): Promise<void>;
/**
* Writes data directly to a hidden storage file and returns the updated file metadata.
*
* @param host - The service feature host providing access to services.
* @param path - The destination file path.
* @param data - The text or binary data to be written.
* @param opt - Optional metadata settings such as modification time and creation time.
* @returns The metadata of the written file, or null if the write operation failed.
*/
export declare function writeFile(host: HiddenFileSyncHost, path: FilePath, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<UXStat | null>;
/**
* Internal helper to remove a file from the hidden storage.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param path - The target file path to be removed.
* @returns 'OK' if the file was successfully removed, 'ALREADY' if it did not exist, or false on failure.
*/
export declare function __removeFile(host: HiddenFileSyncHost, log: LogFunction, path: FilePath): Promise<"OK" | "ALREADY" | false>;
/**
* Triggers a storage synchronisation event to notify other modules of a file modification.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param path - The modified file path.
*/
export declare function triggerEvent(host: HiddenFileSyncHost, log: LogFunction, path: FilePath): Promise<void>;
/**
* Internal helper to delete a hidden file and trigger its respective event notifications.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param storageFilePath - The path of the file to be deleted.
* @returns 'OK' if deleted, 'ALREADY' if not found, or false if the operation failed.
*/
export declare function __deleteFile(host: HiddenFileSyncHost, log: LogFunction, storageFilePath: FilePath): Promise<false | "OK" | "ALREADY">;
/**
* Internal helper to check whether a storage file needs to be written by comparing its contents with target data.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param storageFilePath - The path of the storage file.
* @param content - The target content to compare against.
* @returns True if the contents differ or an error occurs; false if they are identical.
*/
export declare function __checkIsNeedToWriteFile(host: HiddenFileSyncHost, log: LogFunction, storageFilePath: FilePath, content: string | ArrayBuffer): Promise<boolean>;
/**
* Internal helper to write a database entry back to a local storage file.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param storageFilePath - The path of the target file in the storage.
* @param fileOnDB - The loaded database entry.
* @param force - If true, writes the file regardless of content equivalence.
* @returns The file metadata on success, or false on failure.
*/
export declare function __writeFile(host: HiddenFileSyncHost, log: LogFunction, storageFilePath: FilePath, fileOnDB: LoadedEntry, force: boolean): Promise<false | UXStat>;
/**
* Loads a hidden file from local storage, wrapping it in a `UXFileInfo` structure.
*
* @param host - The service feature host providing access to services.
* @param path - The local file path.
* @returns A structure containing the file name, path, metadata, and body content.
*/
export declare function loadFileWithInfo(host: HiddenFileSyncHost, path: FilePath): Promise<UXFileInfo>;
/**
* Internal helper to load the base database document entry for a given file.
* Returns a template for a new entry if the file does not exist in the database.
*
* @param host - The service feature host providing access to services.
* @param file - The target file path.
* @param includeContent - Whether to load the content of the document.
* @returns The loaded database entry.
*/
export declare function __loadBaseSaveData(host: HiddenFileSyncHost, file: FilePath, includeContent?: boolean): Promise<LoadedEntry | false>;
/**
* Saves a local hidden file's content and metadata into the database.
* Confirms that the file content has changed before submitting updates to save database storage.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The runtime file description containing metadata and body.
* @param forceWrite - If true, saves the file to the database even if the content is identical.
* @returns True if the update succeeded, undefined if skipped, or false on failure.
*/
export declare function storeInternalFileToDatabase(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, file: InternalFileInfo | UXFileInfo, forceWrite?: boolean): Promise<boolean | undefined>;
/**
* Marks a hidden file as deleted in the database.
* It also cleans up any conflicting revisions associated with the file.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param filenameSrc - The name of the file being deleted.
* @param forceWrite - Unused parameter retained for interface compatibility.
* @returns True if deletion succeeds, undefined if ignored, or false on error.
*/
export declare function deleteInternalFileOnDatabase(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, filenameSrc: FilePath, forceWrite?: boolean): Promise<boolean | undefined>;
/**
* Extracts a hidden file's metadata and content from the database and writes it to local storage.
* Evaluates whether writing is required based on timestamp differences, deletion markings, and conflict states.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param storageFilePath - The local file destination path.
* @param force - If true, ignores cache check optimizations and forces the file to be written.
* @param metaEntry - The pre-fetched metadata of the database document, if available.
* @param preventDoubleProcess - If true, skips processing if this database key revision matches the cache.
* @param onlyNew - If true, writes the file only when the database version has a newer modification time.
* @param includeDeletion - Whether to apply deletion when checking newer times.
* @param queueNotification - Optional callback to queue notification for reload events.
* @returns True if processed successfully, undefined if skipped, or false on failure.
*/
export declare function extractInternalFileFromDatabase(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, storageFilePath: FilePath, force?: boolean, metaEntry?: MetaEntry | LoadedEntry, preventDoubleProcess?: boolean, onlyNew?: boolean, includeDeletion?: boolean, queueNotification?: (key: FilePath) => void): Promise<boolean | undefined>;

View File

@@ -0,0 +1,24 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { FilePath, FilePathWithPrefix, LoadedEntry } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
export declare function bindHiddenFileSyncEvents(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, handlers: {
updateSettingCache: () => void;
isThisModuleEnabled: () => boolean;
isDatabaseReady: () => boolean;
isReady: () => boolean;
scanAllStorageChanges: (showNotice: boolean) => Promise<boolean>;
performStartupScan: (showNotice: boolean) => Promise<void>;
trackStorageFileModification: (path: FilePath) => Promise<boolean>;
queueConflictCheck: (path: FilePathWithPrefix) => void;
processOptionalSyncFiles: (doc: LoadedEntry) => Promise<boolean>;
suspendExtraSync: () => Promise<boolean>;
askUsingOptionalSyncFeature: (opt: {
enableFetch?: boolean;
enableOverwrite?: boolean;
}) => Promise<boolean>;
configureOptionalSyncFeature: (feature: keyof OPTIONAL_SYNC_FEATURES) => Promise<boolean>;
isTargetFile: (path: FilePath) => Promise<boolean>;
}): void;

View File

@@ -0,0 +1,4 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { HiddenFileSyncModules, HiddenFileSyncServices } from "./types.ts";
export declare const useHiddenFileSync: import("@/types.ts").ObsidianServiceFeatureFunction<HiddenFileSyncServices, HiddenFileSyncModules, "app", void>;

View File

@@ -0,0 +1,68 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { FilePath, MetaEntry } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
/**
* Adopts the current local storage files as already processed, updating their cache keys to match their actual current file states.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param targetFiles - A list of target files, or false to adopt all local storage files.
*/
export declare function adoptCurrentStorageFilesAsProcessed(host: HiddenFileSyncHost, state: HiddenFileSyncState, targetFiles: FilePath[] | false): Promise<void>;
/**
* Adopts the current database files as already processed, updating their cache keys to match their actual current database states.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param targetFiles - A list of target files, or false to adopt all database files.
*/
export declare function adoptCurrentDatabaseFilesAsProcessed(host: HiddenFileSyncHost, state: HiddenFileSyncState, targetFiles: FilePath[] | false): Promise<void>;
/**
* Compares and merges files between the storage and local database based on their modification timestamps.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param showNotice - Whether to show progress notifications.
* @param targetFiles - A list of target files to merge, or false to merge all.
* @returns A list of all file names processed during the merge.
*/
export declare function rebuildMerging(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice: boolean, targetFiles?: FilePath[] | false): Promise<FilePath[]>;
/**
* Rebuilds database entries from the local storage files.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param showNotice - Whether to show progress notifications.
* @param targetFiles - A list of target files, or false to process all files.
* @param onlyNew - If true, only updates database records if they are newer than the storage version.
* @returns A list of file paths processed.
*/
export declare function rebuildFromStorage(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice: boolean, targetFiles?: FilePath[] | false, onlyNew?: boolean): Promise<FilePath[]>;
/**
* Rebuilds local storage files from the database entries.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param showNotice - Whether to show progress notifications.
* @param targetFiles - A list of target files, or false to process all files.
* @param onlyNew - If true, only overwrites local files if the database version is newer.
* @returns A list of metadata entries processed.
*/
export declare function rebuildFromDatabase(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice: boolean, targetFiles?: FilePath[] | false, onlyNew?: boolean): Promise<MetaEntry[]>;
/**
* Initialises or synchronises the hidden files synchronisation state based on a specified direction.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param direction - The direction of synchronisation ('pull', 'push', 'safe', 'pullForce', or 'pushForce').
* @param showMessage - Whether to display progress status alerts in the UI.
* @param targetFilesSrc - Specific source file paths to synchronise, or false for all.
*/
export declare function initialiseInternalFileSync(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, direction?: "pull" | "push" | "safe" | "pullForce" | "pushForce", showMessage?: boolean, targetFilesSrc?: string[] | false): Promise<void>;

View File

@@ -0,0 +1,46 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
/**
* Checks whether the hidden file synchronisation module is enabled in the current settings.
*
* @param host - The service feature host providing access to services.
* @returns True if the synchronisation of internal/hidden files is enabled; otherwise, false.
*/
export declare function isThisModuleEnabled(host: HiddenFileSyncHost): boolean;
/**
* Checks whether the local database is ready and available for operations.
*
* @param host - The service feature host providing access to services.
* @returns True if the database is ready; otherwise, false.
*/
export declare function isDatabaseReady(host: HiddenFileSyncHost): boolean;
/**
* Determines if the hidden file synchronisation module is ready to execute.
* It checks if the application lifecycle is ready, is not suspended, and the module is enabled.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @returns True if the module is ready; otherwise, false.
*/
export declare function isReady(host: HiddenFileSyncHost, state: HiddenFileSyncState): boolean;
/**
* Clears the cached configuration and regular expressions when settings are updated.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
*/
export declare function updateSettingCache(host: HiddenFileSyncHost, state: HiddenFileSyncState): void;
/**
* Performs the initial synchronisation scan during startup.
* It invokes the offline changes application handler to process pending local and database modifications.
*
* @param host - The service feature host providing access to services.
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param showNotice - Whether to show system notices for the progress of the operations.
* @param applyOfflineChanges - The callback function to apply offline modifications.
*/
export declare function performStartupScan(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice: boolean, applyOfflineChanges: (showNotice: boolean) => Promise<void>): Promise<void>;

View File

@@ -0,0 +1,48 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { PeriodicProcessor } from "@/common/PeriodicProcessor.ts";
import type { FilePathWithPrefix } from "@lib/common/types.ts";
import type { CustomRegExp } from "@lib/common/utils.ts";
/**
* Represents the mutable runtime state for the hidden file synchronisation module.
*/
export interface HiddenFileSyncState {
/** Processor for executing periodic internal/hidden file scanning. */
periodicInternalFileScanProcessor: PeriodicProcessor | undefined;
/** Map tracking the last processed file key for each local file path. */
_fileInfoLastProcessed: Map<string, string>;
/** Map tracking the last known modification timestamp for each local file path. */
_fileInfoLastKnown: Map<string, number>;
/** Map tracking the last processed database document key for each path. */
_databaseInfoLastProcessed: Map<string, string>;
/** Map tracking the last known database document timestamp for each path. */
_databaseInfoLastKnown: Map<string, number>;
/** Unused map for tracking deleted files. */
_databaseInfoLastDeleted: Map<string, string>;
/** Unused map for tracking deleted file timestamps. */
_databaseInfoLastKnownDeleted: Map<string, number>;
/** Semaphore to serialize operations on individual files and prevent race conditions. */
semaphore: ReturnType<typeof Semaphore>;
/** Set containing the prefix-marked document paths currently pending conflict checks. */
pendingConflictChecks: Set<FilePathWithPrefix>;
/** Processor executing the conflict resolution queue sequentially. */
conflictResolutionProcessor: QueueProcessor<FilePathWithPrefix, void> | undefined;
/** Cached regular expressions for file matching settings. */
cacheFileRegExps: Map<string, CustomRegExp[][]>;
/** Cached ignore file paths dictated by customisation sync. */
cacheCustomisationSyncIgnoredFiles: Map<string, string[]>;
/** Queued folder paths that have changed and require reload notification. */
queuedNotificationFiles: Set<string>;
/** Whether the synchronisation operations are temporarily suspended. */
suspended: boolean;
/** Notice count index for progress keys. */
noticeIndex: number;
}
/**
* Creates and initialises a new runtime state object for the hidden file synchronisation feature.
*
* @returns An initialised HiddenFileSyncState object.
*/
export declare function createHiddenFileSyncState(): HiddenFileSyncState;

View File

@@ -0,0 +1,131 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type MetaEntry, type LoadedEntry, type UXFileInfo, type UXStat, type FilePath } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
/**
* Extracts the modification timestamp (mtime) from various entry types for comparison.
* If the entry represents a deleted file, it returns 0 unless `includeDeleted` is true.
*
* @param doc - The document entry or file info stat.
* @param includeDeleted - Whether to return mtime for deleted entries.
* @returns The modification timestamp, or 0 if empty or deleted.
*/
export declare function getComparingMTime(doc: (MetaEntry | LoadedEntry | false) | UXFileInfo | UXStat | null | undefined, includeDeleted?: boolean): number;
/**
* Converts a storage file stat object into a unique cache key representation.
*
* @param stat - The storage file metadata.
* @returns A string key in the format: "mtime-size".
*/
export declare function statToKey(stat: UXStat | null): string;
/**
* Converts a database document entry into a unique cache key representation.
*
* @param doc - The database document metadata or loaded entry.
* @returns A string key representing mtime, size, revision, and deletion status.
*/
export declare function docToKey(doc: LoadedEntry | MetaEntry): string;
/**
* Calculates the storage metadata key for a given file path.
*
* @param host - The service feature host providing access to services.
* @param file - The target file path.
* @param stat - Pre-fetched metadata stat, if available.
* @returns The calculated key string.
*/
export declare function fileToStatKey(host: HiddenFileSyncHost, file: FilePath, stat?: UXStat | null): Promise<string>;
/**
* Updates the cached state for the last processed storage file metadata.
*
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @param keySrc - The metadata stat or key string representation to cache.
*/
export declare function updateLastProcessedFile(state: HiddenFileSyncState, file: FilePath, keySrc: string | UXStat): void;
/**
* Fetches file stats from the storage and updates the cached state for the last processed file.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @param stat - Pre-fetched metadata stat, if available.
*/
export declare function updateLastProcessedAsActualFile(host: HiddenFileSyncHost, state: HiddenFileSyncState, file: FilePath, stat?: UXStat | null): Promise<void>;
/**
* Clears the last processed storage cache marks for target files or all files.
*
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param targetFiles - A list of target files, or false to clear all cached marks.
*/
export declare function resetLastProcessedFile(log: LogFunction, state: HiddenFileSyncState, targetFiles: FilePath[] | false): void;
/**
* Retrieves the modification timestamp of the last processed storage file.
*
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @returns The cached modification timestamp.
*/
export declare function getLastProcessedFileMTime(state: HiddenFileSyncState, file: FilePath): number;
/**
* Retrieves the cache key for the last processed storage file.
*
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @returns The cached key string.
*/
export declare function getLastProcessedFileKey(state: HiddenFileSyncState, file: FilePath): string | undefined;
/**
* Retrieves the cache key for the last processed database document.
*
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @returns The cached key string.
*/
export declare function getLastProcessedDatabaseKey(state: HiddenFileSyncState, file: FilePath): string | undefined;
/**
* Updates the cached state for the last processed database document key.
*
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @param keySrc - The database document metadata or key representation to cache.
*/
export declare function updateLastProcessedDatabase(state: HiddenFileSyncState, file: FilePath, keySrc: string | MetaEntry | LoadedEntry): void;
/**
* Updates both storage file and database cache records for a path, registering changes in the path manager.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The target file path.
* @param db - The loaded database document entry.
* @param stat - The storage metadata status.
*/
export declare function updateLastProcessed(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: FilePath, db: MetaEntry | LoadedEntry, stat: UXStat): void;
/**
* Updates both storage file and database cache records for a path to represent deletion, clearing path manager records.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The target file path.
* @param db - The database entry representing deletion, or false if not stored.
*/
export declare function updateLastProcessedDeletion(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: FilePath, db: MetaEntry | LoadedEntry | false): void;
/**
* Fetches database document metadata and updates the database cache key for the path.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param file - The target file path.
* @param doc - Optional pre-fetched metadata of the database document.
*/
export declare function updateLastProcessedAsActualDatabase(host: HiddenFileSyncHost, state: HiddenFileSyncState, file: FilePath, doc?: MetaEntry | LoadedEntry | null | false): Promise<void>;
/**
* Clears the last processed database cache marks for target files or all files.
*
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param targetFiles - A list of target files, or false to clear all cached marks.
*/
export declare function resetLastProcessedDatabase(log: LogFunction, state: HiddenFileSyncState, targetFiles: FilePath[] | false): void;

View File

@@ -0,0 +1,289 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { FilePath, LoadedEntry, MetaEntry, DocumentID } from "@lib/common/types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
import { type CustomRegExp } from "@lib/common/utils.ts";
import type { HiddenFileSyncHost } from "./types.ts";
import type { HiddenFileSyncState } from "./state.ts";
/**
* Generates a progress logger that tracks long-running synchronisation operations.
*
* @param log - The logging function.
* @param state - The runtime state of the hidden file synchronisation module.
* @param prefix - The message prefix to prepend to log statements.
* @param level - The log level to use.
* @returns An object containing `log`, `once`, and `done` progress log methods.
*/
export declare function getProgress(log: LogFunction, state: HiddenFileSyncState, prefix?: string, level?: any): { // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
log: (msg: string) => void;
once: (msg: string) => void;
done: (msg?: string) => void;
};
/**
* Parses ignore and target custom regular expression filters from settings, caching the compiled filters.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @returns Compiled regular expressions for target and ignored files.
*/
export declare function parseRegExpSettings(host: HiddenFileSyncHost, state: HiddenFileSyncState): {
ignoreFilter: CustomRegExp[];
targetFilter: CustomRegExp[];
};
/**
* Checks if a given file path is matched by target patterns and not ignored by ignore patterns.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The file path to check.
* @returns True if the path is a synchronisation target based on pattern settings; otherwise, false.
*/
export declare function isTargetFileInPatterns(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: string): boolean;
/**
* Determines which files are synchronised by the customisation sync feature and should be ignored by this module.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @returns A list of ignored file path strings.
*/
export declare function getCustomisationSynchronizationIgnoredFiles(host: HiddenFileSyncHost, state: HiddenFileSyncState): string[];
/**
* Checks whether a path is not ignored due to customisation synchronisation settings.
*
* @param host - The service feature host providing access to services.
* @param state - The runtime state of the hidden file synchronisation module.
* @param path - The file path to check.
* @returns True if not ignored by customisation synchronisation; otherwise, false.
*/
export declare function isNotIgnoredByCustomisationSync(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: string): boolean;
/**
* Verifies if the path represents a hidden configuration file.
* Configuration files start with '.' and are not within the '.trash' folder.
*
* @param path - The file path to verify.
* @returns True if the path represents a hidden file; otherwise, false.
*/
export declare function isHiddenFileSyncHandlingPath(path: FilePath): boolean;
/**
* Validates if the path is a synchronisation target, checking pattern filters, customisation sync rules, hidden file rules, and ignore file rules.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param path - The target file path.
* @returns True if the file should be synchronised; otherwise, false.
*/
export declare function isTargetFile(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, path: FilePath): Promise<boolean>;
/**
* Executes a function sequentially for an event using locks and semaphores to prevent race conditions during file processing.
*
* @param host - The service feature host.
* @param state - The runtime state.
* @param file - The file path.
* @param fn - The function to run.
*/
export declare function serializedForEvent<T>(host: HiddenFileSyncHost, state: HiddenFileSyncState, file: FilePath, fn: () => Promise<T>): Promise<T>;
/**
* Recursively lists files inside the specified directory path that pass the verification check function.
*
* @param host - The service feature host.
* @param state - The runtime state.
* @param path - The directory path to list.
* @param checkFunction - The verification callback.
* @returns A list of file paths.
*/
export declare function getFiles(host: HiddenFileSyncHost, state: HiddenFileSyncState, path: string, checkFunction: (path: FilePath) => Promise<boolean> | boolean): Promise<string[]>;
/**
* Scans the local workspace vault for hidden configuration files that are target synchronisation candidates.
*
* @param host - The service feature host.
* @param state - The runtime state.
* @returns A list of hidden file paths.
*/
export declare function scanInternalFileNames(host: HiddenFileSyncHost, state: HiddenFileSyncState): Promise<FilePath[]>;
/**
* Queries the local database for all hidden configuration file metadata documents.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @returns A list of database metadata entries.
*/
export declare function getAllDatabaseFiles(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState): Promise<MetaEntry[]>;
/**
* Tracks scanned storage changes, synchronising them to the database in bulk.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param processFiles - The list of local files to process.
* @param showNotice - Whether to show system notices.
* @param onlyNew - If true, only updates database files if they are newer.
* @param forceWriteAll - If true, forces database updates.
* @param includeDeleted - Whether to process deleted files.
*/
export declare function trackScannedStorageChanges(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, processFiles: FilePath[], showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeleted?: boolean): Promise<void>;
/**
* Scans all local storage files and compares them with the cache to track any new changes to be saved to the database.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param showNotice - Whether to show progress notices.
* @param onlyNew - If true, only synchronises newer files.
* @param forceWriteAll - If true, forces file updates.
* @param includeDeleted - Whether to process deleted files.
* @returns True if scanning and updates succeeded; otherwise, false.
*/
export declare function scanAllStorageChanges(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeleted?: boolean): Promise<boolean>;
/**
* Tracks a single storage file modification, saving updates or deleting database records accordingly.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param path - The local storage path.
* @param onlyNew - If true, only updates the database if the storage file is newer.
* @param forceWrite - If true, forces database updates.
* @param includeDeleted - Whether to track deletions.
* @returns True if modification tracking succeeded, or false if skipped/failed.
*/
export declare function trackStorageFileModification(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, path: FilePath, onlyNew?: boolean, forceWrite?: boolean, includeDeleted?: boolean): Promise<boolean | undefined>;
/**
* Applies offline database and storage modifications by comparing differences on untracked files.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param showNotice - Whether to show notifications.
*/
export declare function applyOfflineChanges(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice: boolean): Promise<void>;
/**
* Tracks scanned database changes, writing updates to the local storage in bulk.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param processFiles - Database entries to track.
* @param showNotice - Whether to show notices.
* @param onlyNew - If true, only overwrites local files if the database entry is newer.
* @param forceWriteAll - If true, forces local file updates.
* @param includeDeletion - Whether to apply database deletions.
*/
export declare function trackScannedDatabaseChange(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, processFiles: MetaEntry[], showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeletion?: boolean): Promise<void>;
/**
* Scans the database for changed metadata documents to update the local storage.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param showNotice - Whether to show notices.
* @param onlyNew - If true, only updates the local storage if database changes are newer.
* @param forceWriteAll - If true, forces storage updates.
* @param includeDeletion - Whether to apply deletions.
* @returns True if database scan and application succeeded; otherwise, false.
*/
export declare function scanAllDatabaseChanges(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, showNotice?: boolean, onlyNew?: boolean, forceWriteAll?: boolean, includeDeletion?: boolean): Promise<boolean>;
/**
* Processes a single database file modification, resolving conflicts or updating the local storage.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param storageFilePath - The local file path.
* @param reason - The log context string.
* @param preventDoubleProcess - If true, skips processing if this database key revision matches the cache.
* @param onlyNew - If true, only overwrites if database entries are newer.
* @param metaEntry - Pre-fetched database metadata, if available.
* @param includeDeletion - Whether to apply database deletions.
* @returns True if database tracking succeeded.
*/
export declare function trackDatabaseFileModification(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, storageFilePath: FilePath, reason: string, preventDoubleProcess: boolean, onlyNew: boolean, metaEntry?: MetaEntry | LoadedEntry, includeDeletion?: boolean): Promise<boolean | undefined>;
/**
* Event handler triggered when synchronised files change in the database.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param doc - The loaded database document entry.
* @returns True if database change processing was handled; otherwise, false.
*/
export declare function processOptionalSyncFiles(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, doc: LoadedEntry): Promise<boolean>;
/**
* Extracts and formats key metadata properties from a database document.
*
* @param host - The service feature host.
* @param doc - The database document metadata or loaded entry.
* @returns Formatted metadata property strings.
*/
export declare function getDocProps(host: HiddenFileSyncHost, doc: MetaEntry | LoadedEntry): {
id: DocumentID;
rev: string;
revDisplay: string;
prefixedPath: DocumentID;
path: FilePath;
isDeleted: boolean;
shortenedId: string;
shortenedPath: string;
};
/**
* Extracts the numerical revision sequence prefix from a PouchDB revision string.
*
* @param rev - The PouchDB revision string.
* @returns The numerical prefix string of the revision.
*/
export declare function displayRev(rev: string): string;
/**
* Returns a callback wrapper that invokes the inner function only once every N invocations.
*
* @param n - The step frequency threshold.
* @param func - The inner function callback.
* @returns The step count logging wrapper function.
*/
export declare function onlyInNTimes(n: number, func: (progress: number) => void): () => void;
/**
* Queues folder change notifications to warn the user about plugin or configuration updates.
*
* @param host - The service feature host.
* @param state - The runtime state.
* @param key - The file path that was updated.
*/
export declare function queueNotification(host: HiddenFileSyncHost, state: HiddenFileSyncState, key: FilePath): void;
/**
* Triggers user notifications and prompt dialogues for reloading plug-ins or reloading the Obsidian application.
*
* @param host - The service feature host.
* @param state - The runtime state.
*/
export declare function notifyConfigChange(host: HiddenFileSyncHost, state: HiddenFileSyncState): void;
/**
* Temporarily suspends hidden file synchronisation settings during initial replications.
*
* @param host - The service feature host.
* @param state - The runtime state.
* @returns True if setting change was applied.
*/
export declare function suspendExtraSync(host: HiddenFileSyncHost, state: HiddenFileSyncState): Promise<boolean>;
/**
* Prompts the user with dialogue choices to configure hidden file synchronisation modes.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param opt - Configuration options specifying available modes.
* @returns True if configuration completed.
*/
export declare function askUsingOptionalSyncFeature(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, opt: {
enableFetch?: boolean;
enableOverwrite?: boolean;
}): Promise<boolean>;
/**
* Applies settings and initialises synchronisation based on the selected mode.
*
* @param host - The service feature host.
* @param log - The logging function.
* @param state - The runtime state.
* @param feature - The selected configuration feature mode ('FETCH', 'OVERWRITE', 'MERGE', 'DISABLE', or 'DISABLE_HIDDEN').
* @returns True if setting change was applied; otherwise, false.
*/
export declare function configureOptionalSyncFeature(host: HiddenFileSyncHost, log: LogFunction, state: HiddenFileSyncState, feature: keyof any): Promise<boolean>; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration

View File

@@ -0,0 +1,6 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { NecessaryObsidianServices } from "@/types.ts";
export type HiddenFileSyncServices = "API" | "appLifecycle" | "setting" | "vault" | "path" | "database" | "databaseEvents" | "fileProcessing" | "keyValueDB" | "replication" | "conflict" | "control";
export type HiddenFileSyncModules = "storageAccess" | "fileHandler";
export type HiddenFileSyncHost = NecessaryObsidianServices<HiddenFileSyncServices, HiddenFileSyncModules, "app">;

View File

@@ -0,0 +1,38 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type FilePathWithPrefix, type diff_result } from "@lib/common/types.ts";
import type { ConflictResolverHost } from "./types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
/**
* Resolves a conflict using the user interface modal, one-by-one.
*
* @param host - The service feature host context.
* @param log - The logger function.
* @param filename - The path of the conflicted file.
* @param conflictCheckResult - The result of conflict detection / diff.
* @returns A promise resolving to true if successfully resolved, otherwise false.
*/
export declare function resolveConflictByUI(host: ConflictResolverHost, log: LogFunction, filename: FilePathWithPrefix, conflictCheckResult: diff_result): Promise<boolean>;
/**
* Iteratively prompts the user to resolve all conflicted files.
*
* @param host - The service feature host context.
* @param log - The logger function.
*/
export declare function allConflictCheck(host: ConflictResolverHost, log: LogFunction): Promise<void>;
/**
* Prompts the user to pick a file from the list of conflicted files.
*
* @param host - The service feature host context.
* @param log - The logger function.
* @returns A promise resolving to true if a file was selected and queued for checking, otherwise false.
*/
export declare function pickFileForResolve(host: ConflictResolverHost, log: LogFunction): Promise<boolean>;
/**
* Scans the database for conflicted files and displays a safety popup if any are found.
*
* @param host - The service feature host context.
* @param log - The logger function.
* @returns A promise resolving to true if execution completes successfully, otherwise false.
*/
export declare function allScanStat(host: ConflictResolverHost, log: LogFunction): Promise<boolean>;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { ConflictResolverServices } from "./types.ts";
/**
* A service feature hook that initialises and manages the Interactive Conflict Resolver.
* Registers conflict resolution commands and handles user-interactive resolution flows.
*/
export declare const useInteractiveConflictResolver: import("@lib/interfaces/ServiceModule.ts").ServiceFeatureFunction<ConflictResolverServices, "databaseFileAccess", void>;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type NecessaryServices } from "@lib/interfaces/ServiceModule";
/**
* A union of service keys required by the interactive conflict resolver feature.
*/
export type ConflictResolverServices = "API" | "setting" | "UI" | "database" | "conflict" | "appLifecycle" | "replication" | "path";
/**
* A union of service module keys required by the interactive conflict resolver feature.
*/
export type ConflictResolverModules = "databaseFileAccess";
/**
* The host type representing the injected service container with conflict resolution capabilities.
*/
export type ConflictResolverHost = NecessaryServices<ConflictResolverServices, ConflictResolverModules>;

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFeatureServices } from "./types.ts";
/**
* A service feature hook that initialises and manages logging, status display, and debug report generation.
*/
export declare const useLogFeature: import("@lib/interfaces/ServiceModule.ts").ServiceFeatureFunction<LogFeatureServices, "storageAccess", void>;

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type LOG_LEVEL } from "@lib/common/types.ts";
import type { LogFeatureHost } from "./types.ts";
import type { LogFeatureState } from "./state.ts";
export declare const MARK_DONE = "\u2009\u2009";
export declare function addLog(state: LogFeatureState, log: string): void;
export declare function addDisplayLog(state: LogFeatureState, log: string): void;
export declare function redactLog(log: string): string;
export declare function writeLogToTheFile(host: LogFeatureHost, now: Date, vaultName: string, newMessage: string): void;
export declare function processAddLog(host: LogFeatureHost, state: LogFeatureState, message: unknown, level?: LOG_LEVEL, key?: string): void;
export declare function adjustStatusDivPosition(host: LogFeatureHost, state: LogFeatureState): void;
export declare function getActiveFileStatus(host: LogFeatureHost): Promise<string>;
export declare function setFileStatus(host: LogFeatureHost, state: LogFeatureState): Promise<void>;
export declare function updateMessageArea(host: LogFeatureHost, state: LogFeatureState): Promise<void>;
export declare function onActiveLeafChange(host: LogFeatureHost, state: LogFeatureState): void;
export declare function applyStatusBarText(host: LogFeatureHost, state: LogFeatureState): void;
export declare function observeForLogs(host: LogFeatureHost, state: LogFeatureState): void;

View File

@@ -0,0 +1,42 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { reactiveSource, type ReactiveValue } from "octagonal-wheels/dataobject/reactive";
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector.ts";
import { Notice } from "@/deps.ts";
import type { LogEntry } from "@lib/mock_and_interop/stores.ts";
/**
* Interface representing the internal state of the logging and status display feature.
*/
export interface LogFeatureState {
statusBar?: HTMLElement;
statusDiv?: HTMLElement;
statusLine?: HTMLDivElement;
logMessage?: HTMLDivElement;
logHistory?: HTMLDivElement;
messageArea?: HTMLDivElement;
statusBarLabels?: ReactiveValue<{
message: string;
status: string;
}>;
statusLog: ReturnType<typeof reactiveSource<string>>;
activeFileStatus: ReturnType<typeof reactiveSource<string>>;
notifies: {
[key: string]: {
notice: Notice;
count: number;
};
};
p2pLogCollector: P2PLogCollector;
nextFrameQueue?: number;
logLines: {
ttl: number;
message: string;
}[];
recentLogEntries: ReturnType<typeof reactiveSource<LogEntry[]>>;
logForDump: string[];
logForDisplay: string[];
}
/**
* Creates the initial state object.
*/
export declare function createInitialState(): LogFeatureState;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type NecessaryServices } from "@lib/interfaces/ServiceModule";
/**
* Service keys required by the logging and status bar feature.
*/
export type LogFeatureServices = "API" | "setting" | "replication" | "conflict" | "fileProcessing" | "appLifecycle" | "vault" | "replicator" | "UI";
/**
* Service modules required by the logging and status bar feature.
*/
export type LogFeatureModules = "storageAccess";
/**
* The host type representing the injected service container with logging capabilities.
*/
export type LogFeatureHost = NecessaryServices<LogFeatureServices, LogFeatureModules>;

View File

@@ -0,0 +1,3 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
export declare const useMigrationFeature: import("@/types.ts").ObsidianServiceFeatureFunction<"path" | "setting" | "UI" | "appLifecycle" | "API" | "database" | "replicator" | "vault" | "keyValueDB", "storageAccess" | "fileHandler" | "rebuilder", never, void>;

View File

@@ -0,0 +1,21 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type TFile } from "@/deps.ts";
import type { FilePathWithPrefix, DocumentID } from "@lib/common/types.ts";
import type { DocumentHistoryHost } from "./types.ts";
import type { LogFunction } from "@lib/services/lib/logUtils";
/**
* Opens the document history modal dialogue for a given file.
*
* @param host - The service feature host context.
* @param file - The file path or TFile reference to query history.
* @param id - Optional CouchDB document identifier.
*/
export declare function showHistory(host: DocumentHistoryHost, file: TFile | FilePathWithPrefix, id?: DocumentID): void;
/**
* Displays a list of all local documents, prompting the user to select one to view its history.
*
* @param host - The service feature host context.
* @param log - The logger function.
*/
export declare function fileHistory(host: DocumentHistoryHost, log: LogFunction): Promise<void>;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { DocumentHistoryServices } from "./types.ts";
/**
* A service feature hook that initialises and manages Obsidian Document History commands.
* Registers ribbon commands and listens to history request events.
*/
export declare const useObsidianDocumentHistory: import("@lib/interfaces/ServiceModule.ts").ServiceFeatureFunction<DocumentHistoryServices, never, void>;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type NecessaryServices } from "@lib/interfaces/ServiceModule";
/**
* Service keys required by the Obsidian document history feature.
*/
export type DocumentHistoryServices = "API" | "vault" | "database" | "UI" | "path" | "appLifecycle";
/**
* Service modules required by the Obsidian document history feature.
*/
export type DocumentHistoryModules = never;
/**
* The host type representing the injected service container with document history capabilities.
*/
export type DocumentHistoryHost = NecessaryServices<DocumentHistoryServices, DocumentHistoryModules>;

View File

@@ -0,0 +1,34 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { ObsidianEventsHost } from "./types.ts";
import type { ObsidianEventsState } from "./state.ts";
/**
* Executes a restart and reload of the Obsidian application.
*
* @param host - The service container host.
*/
export declare function performAppReload(host: ObsidianEventsHost): void;
/**
* Asks the user if they want to restart and reload Obsidian now, scheduling or executing it.
*
* @param host - The service container host.
* @param log - The logger function.
* @param message - An optional custom message to display in the dialogue.
*/
export declare function askReload(host: ObsidianEventsHost, log: LogFunction, message?: string): void;
/**
* Schedules an application reload, waiting for all background tasks to stabilise to 0.
*
* @param host - The service container host.
* @param log - The logger function.
* @param state - The runtime state of the Obsidian events module.
*/
export declare function scheduleAppReload(host: ObsidianEventsHost, log: LogFunction, state: ObsidianEventsState): void;
/**
* Checks if an application reload has already been scheduled.
*
* @param state - The runtime state of the Obsidian events module.
* @returns True if scheduled, false otherwise.
*/
export declare function isReloadingScheduled(state: ObsidianEventsState): boolean;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { ObsidianEventsServices } from "./types.ts";
/**
* A service feature hook that initialises and manages Obsidian application event bindings.
* This hooks into vault file changes, window focus, visibility states, and schedules restarts.
*/
export declare const useObsidianEvents: import("@lib/interfaces/ServiceModule").ServiceFeatureFunction<ObsidianEventsServices, never, void>;

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { ObsidianEventsHost } from "./types.ts";
import type { ObsidianEventsState } from "./state.ts";
/**
* Swaps the default Obsidian save command callback to trigger a synchronisation sweep.
*
* @param host - The service container host.
* @param log - The logger function.
* @param state - The runtime state of the Obsidian events module.
*/
export declare function swapSaveCommand(host: ObsidianEventsHost, log: LogFunction, state: ObsidianEventsState): void;

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type ReactiveSource } from "octagonal-wheels/dataobject/reactive";
/**
* Represents the runtime state of the Obsidian events module.
*/
export interface ObsidianEventsState {
initialCallback: (() => void) | undefined;
hasFocus: boolean;
isLastHidden: boolean;
totalProcessingCount: ReactiveSource<number> | undefined;
}
/**
* Creates and initialises a new Obsidian events state object.
*
* @returns A freshly initialised {@link ObsidianEventsState} object.
*/
export declare function createObsidianEventsState(): ObsidianEventsState;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type NecessaryServices } from "@lib/interfaces/ServiceModule";
/**
* A union of service keys required by the Obsidian events management feature.
*/
export type ObsidianEventsServices = "API" | "setting" | "appLifecycle" | "control" | "replication" | "vault" | "fileProcessing" | "conflict" | "database" | "UI";
/**
* A union of service module keys required by the Obsidian events management feature.
*/
export type ObsidianEventsModules = never;
/**
* The host type representing the injected service container with Obsidian events capabilities.
*/
export type ObsidianEventsHost = NecessaryServices<ObsidianEventsServices, ObsidianEventsModules>;

View File

@@ -0,0 +1,60 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { TFile } from "@/deps.ts";
import type { LogFunction } from "@lib/services/lib/logUtils.ts";
import type { ObsidianEventsHost } from "./types.ts";
import type { ObsidianEventsState } from "./state.ts";
/**
* Sets the focus status and triggers visibility check scheduling.
*
* @param host - The service container host.
* @param log - The logger function.
* @param state - The runtime state of the Obsidian events module.
* @param hasFocus - The new focus status.
*/
export declare function setHasFocus(host: ObsidianEventsHost, log: LogFunction, state: ObsidianEventsState, hasFocus: boolean): void;
/**
* Schedules a task to check and apply window visibility transitions.
*
* @param host - The service container host.
* @param log - The logger function.
* @param state - The runtime state of the Obsidian events module.
*/
export declare function watchWindowVisibility(host: ObsidianEventsHost, log: LogFunction, state: ObsidianEventsState): void;
/**
* Asynchronously processes window visibility transitions, suspending or resuming replication channels.
*
* @param host - The service container host.
* @param log - The logger function.
* @param state - The runtime state of the Obsidian events module.
*/
export declare function watchWindowVisibilityAsync(host: ObsidianEventsHost, log: LogFunction, state: ObsidianEventsState): Promise<void>;
/**
* Schedules a task to check online recovery and vault rescanning.
*
* @param host - The service container host.
* @param log - The logger function.
*/
export declare function watchOnline(host: ObsidianEventsHost, log: LogFunction): void;
/**
* Asynchronously checks if online recovery is required, performing a vault scan if the network recovers.
*
* @param host - The service container host.
*/
export declare function watchOnlineAsync(host: ObsidianEventsHost): Promise<void>;
/**
* Schedules a task to process files opened in the workspace.
*
* @param host - The service container host.
* @param log - The logger function.
* @param file - The file that was opened.
*/
export declare function watchWorkspaceOpen(host: ObsidianEventsHost, log: LogFunction, file: TFile | null): void;
/**
* Asynchronously handles workspace file open events, running replication and checking for conflicts.
*
* @param host - The service container host.
* @param log - The logger function.
* @param file - The file that was opened.
*/
export declare function watchWorkspaceOpenAsync(host: ObsidianEventsHost, log: LogFunction, file: TFile): Promise<void>;

View File

@@ -0,0 +1,8 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
/**
* Obsidian Menu Feature
*
* Provides Obsidian-specific UI elements like ribbon icons and commands.
*/
export declare const useObsidianMenuFeature: import("@/types.ts").ObsidianServiceFeatureFunction<"replication" | "appLifecycle" | "conflict", never, "plugin", void>;

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts";
export declare const SETTING_HEADER = "````yaml:livesync-setting\n";
export declare const SETTING_FOOTER = "\n````";
/**
* Extracts the YAML settings block from the full text of a markdown file.
*
* Returns the preamble (text before the block), the body (YAML content), and
* the postscript (text after the block). If no block is found, the entire
* `data` string is returned as the preamble with empty body and postscript.
*/
export declare const extractSettingFromWholeText: (data: string) => {
preamble: string;
body: string;
postscript: string;
};
/**
* Strips sensitive / internal-only fields from a settings snapshot so that it
* is safe to serialise into a markdown file.
*
* If `keepCredential` is true (or `writeCredentialsForSettingSync` is set on
* the settings object) the credential fields are retained; otherwise they are
* removed.
*/
export declare const generateSettingForMarkdownPure: (settings: ObsidianLiveSyncSettings, keepCredential?: boolean) => Partial<ObsidianLiveSyncSettings>;
/**
* Obsidian Settings as Markdown Feature
*
* Allows saving and loading settings to/from a markdown file.
*/
export declare const useObsidianSettingAsMarkdownFeature: import("@/types.ts").ObsidianServiceFeatureFunction<"setting" | "UI" | "appLifecycle" | "API", "storageAccess" | "rebuilder", "plugin", void>;

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { SettingDialogueServices } from "./types.ts";
/**
* A service feature hook that registers the plug-in setting tab and listens to settings dialogue triggers.
*/
export declare const useObsidianSettingDialogue: import("@lib/interfaces/ServiceModule.ts").ServiceFeatureFunction<SettingDialogueServices, never, void>;

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { SettingDialogueHost } from "./types.ts";
import type { SettingDialogueState } from "./state.ts";
/**
* Opens the Obsidian settings panel and navigates to the Self-hosted LiveSync tab.
*
* @param host - The service feature host context.
*/
export declare function openSetting(host: SettingDialogueHost): void;
/**
* Opens settings and automatically launches the minimal setup configuration wizard.
*
* @param host - The service feature host context.
* @param state - The state object holding the settings tab reference.
*/
export declare function openSettingWizard(host: SettingDialogueHost, state: SettingDialogueState): Promise<void>;

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import type { ObsidianLiveSyncSettingTab } from "@/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts";
/**
* Interface representing the internal state of the setting dialogue feature.
*/
export interface SettingDialogueState {
settingTab?: ObsidianLiveSyncSettingTab;
}
/**
* Creates the initial state object.
*/
export declare function createInitialState(): SettingDialogueState;

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type NecessaryServices } from "@lib/interfaces/ServiceModule";
/**
* Service keys required by the Obsidian setting tab dialogue feature.
*/
export type SettingDialogueServices = "API" | "appLifecycle";
/**
* Service modules required by the Obsidian setting tab dialogue feature.
*/
export type SettingDialogueModules = never;
/**
* The host type representing the injected service container with setting tab capabilities.
*/
export type SettingDialogueHost = NecessaryServices<SettingDialogueServices, SettingDialogueModules>;

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
// REPO: https://github.com/vrtmrz/livesync-commonlib Commit hash: 0563f26
import { type ObsidianLiveSyncSettings } from "@lib/common/types.ts";
export declare const enum UserMode {
NewUser = "new-user",
ExistingUser = "existing-user",
Unknown = "unknown",
Update = "unknown" // eslint-disable-line @typescript-eslint/no-duplicate-enum-values -- Duplicate enum value
}
export interface SetupManagerAPI {
startOnBoarding(): Promise<boolean>;
onOnboard(userMode: UserMode): Promise<boolean>;
onUseSetupURI(userMode: UserMode, setupURI?: string): Promise<boolean>;
onCouchDBManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise<boolean>;
onBucketManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise<boolean>;
onP2PManualSetup(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings, activate?: boolean): Promise<boolean>;
onlyE2EEConfiguration(userMode: UserMode, currentSetting: ObsidianLiveSyncSettings): Promise<boolean>;
onConfigureManually(originalSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean>;
onSelectServer(currentSetting: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean>;
onConfirmApplySettingsFromWizard(newConf: ObsidianLiveSyncSettings, _userMode: UserMode, activate?: boolean, extra?: () => void): Promise<boolean>;
onPromptQRCodeInstruction(): Promise<boolean>;
decodeQR(qr: string): Promise<boolean>;
applySetting(newConf: ObsidianLiveSyncSettings, userMode: UserMode): Promise<boolean>;
dialogManager: any; // eslint-disable-line @typescript-eslint/no-explicit-any -- Only type declaration
}
export declare const getSetupManager: () => SetupManagerAPI;
export declare const useSetupManagerFeature: import("@/types.ts").ObsidianServiceFeatureFunction<"setting" | "UI" | "appLifecycle" | "API" | "replicator", "rebuilder", never, SetupManagerAPI>;

26
_types/src/types.d.ts vendored
View File

@@ -5,6 +5,10 @@ import type { Rebuilder } from "@lib/interfaces/DatabaseRebuilder";
import type { IFileHandler } from "@lib/interfaces/FileHandler";
import type { StorageAccess } from "@lib/interfaces/StorageAccess";
import type { IServiceHub } from "@lib/services/base/IService";
import type { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
import type { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import type { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
export interface ServiceModules {
storageAccess: StorageAccess;
/**
@@ -24,3 +28,25 @@ export interface LiveSyncHost {
services: IServiceHub;
serviceModules: ServiceModules;
}
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
/**
* Extends the standard `{ services, serviceModules }` host shape with a typed
* `context` slice from `ObsidianServiceContext`.
*
* Use this as the host type for features built with `createServiceFeature` that
* also need type-safe access to Obsidian-specific context properties such as
* `app` or `plugin`.
*
* @typeParam T - Service keys (same constraint as `NecessaryObsidianFeature`).
* @typeParam U - Service module keys from `ServiceModules`.
* @typeParam C - Keys of `ObsidianServiceContext` to expose (e.g. `"app" | "plugin"`).
*/
export type NecessaryObsidianFeature<T extends keyof ObsidianServiceHub, U extends keyof ServiceModules = never, C extends keyof ObsidianServiceContext = never> = {
services: Pick<ObsidianServiceHub, T>;
serviceModules: Pick<ServiceModules, U>;
context: Pick<ObsidianServiceContext, C>;
};
/** Alias to keep backward compatibility with defined feature hosts */
export type NecessaryObsidianServices<T extends keyof ObsidianServiceHub, U extends keyof ServiceModules = never, C extends keyof ObsidianServiceContext = never> = NecessaryObsidianFeature<T, U, C>;
export type ObsidianServiceFeatureFunction<T extends keyof ObsidianServiceHub, U extends keyof ServiceModules, C extends keyof ObsidianServiceContext, TR> = (host: NecessaryObsidianFeature<T, U, C>) => TR;
export declare function createObsidianServiceFeature<T extends keyof ObsidianServiceHub, U extends keyof ServiceModules = never, C extends keyof ObsidianServiceContext = never, TR = void>(featureFunction: ObsidianServiceFeatureFunction<T, U, C, TR>): ObsidianServiceFeatureFunction<T, U, C, TR>;

57
devs.md
View File

@@ -116,6 +116,7 @@ The plugin uses a dynamic module system to reduce coupling and improve maintaina
- **Services**: Core services (e.g., `database`, `replicator`, `storageAccess`) are registered in `ServiceHub` and accessed by modules. They provide an extension point for add new behaviour without modifying existing code.
- For example, checks before the replication can be added to the `replication.onBeforeReplicate` handler, and the handlers can be return `false` to prevent replication-starting. `vault.isTargetFile` also can be used to prevent processing specific files.
- **ServiceModule**: A new type of module that directly depends on services.
- **serviceFeature**: A decoupled functional feature (defined via `createServiceFeature` or `createObsidianServiceFeature`) that encapsulates state and behaviour within its function closure. Unlike legacy modules, it does not register itself onto the `ServiceHub` registry, preventing global namespace pollution, and enabling simple unit testing.
#### Note on Module vs Service
@@ -176,18 +177,56 @@ Hence, the new feature should be implemented as follows:
## Common Patterns
### Module Implementation (Now not recommended for new features, use services instead)
### Service Feature Implementation (Highly Recommended for New Features and UI/Event Registrars)
Instead of subclassing 'AbstractModule' or 'AbstractObsidianModule', features should be implemented as functional closures.
#### Standard Service Feature
Use `createServiceFeature` for features that do not depend on the Obsidian application context:
```typescript
export class ModuleExample extends AbstractObsidianModule {
async _everyOnloadStart(): Promise<boolean> {
/* ... */
}
import { createServiceFeature } from "@lib/interfaces/ServiceModule.ts";
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.handleOnInitialise(this._everyOnloadStart.bind(this));
}
}
export const useMyFeature = createServiceFeature(({ services, serviceModules }) => {
// Encapsulated state in the function closure
let localCache = "";
const onInitialise = (): Promise<boolean> => {
services.setting.saveSettingData();
return Promise.resolve(true);
};
services.appLifecycle.onInitialise.addHandler(onInitialise);
});
```
#### Obsidian-Specific Service Feature
Use `createObsidianServiceFeature` for features requiring Obsidian context (`app`, `plugin`, or `liveSyncPlugin`):
```typescript
import { createObsidianServiceFeature } from "@/types.ts";
export const useMyObsidianFeature = createObsidianServiceFeature<
MyFeatureServices,
MyFeatureModules,
"app" | "liveSyncPlugin"
>((host) => {
const plugin = host.context.liveSyncPlugin;
const onLayoutReady = (): Promise<boolean> => {
host.services.API.addCommand({
id: "my-command",
name: "My plug-in command",
callback: () => {
// Access typed context safely
console.log(host.context.app.vault.getName());
}
});
return Promise.resolve(true);
};
host.services.appLifecycle.onLayoutReady.addHandler(onLayoutReady);
});
```
### Settings Management

View File

@@ -0,0 +1,60 @@
# Architectural Decision Record: Modularity Refactoring via serviceFeature
## Status
Decided / Work in Progress
## Release
Not yet (at 26th June 2026) / Not yet tested
## Context
Previously, many modules in the codebase relied on monolithic base classes, such as 'LiveSyncCommands', 'AbstractObsidianModule', and the foundational 'AbstractModule'. These base classes implicitly granted access to a large global context, which created tight coupling, made unit testing difficult, and hampered maintenance.
While we initially considered migrating these to 'ServiceModule's, doing so would have bloated the 'ServiceModules' registry in 'ServiceHub' with features, dialogue managers, and user interface (UI) bindings that do not need to be globally accessible.
## Decision
We have decided to refactor these modules into **'serviceFeature'**s and **'ObsidianServiceFeature'**s:
1. **'serviceFeature'**: A feature (defined via `createServiceFeature`) that receives injected dependencies (such as `services` and `serviceModules`) but does not register itself onto the `ServiceHub`. State and logic are encapsulated within the function closure, providing excellent testability and loose coupling without polluting the global registry.
2. **'createObsidianServiceFeature'**: To support Obsidian-specific plug-in features that require direct access to the Obsidian application context (`app`, `plugin`, or `liveSyncPlugin`), we introduced the `createObsidianServiceFeature` helper and the `NecessaryObsidianFeature` utility type. This enables type-safe injection of the Obsidian context without casting to `any`.
3. **Core Types Relocation**: All service feature utility types (`LiveSyncCore`, `NecessaryObsidianFeature`, `ObsidianServiceFeatureFunction`, and `createObsidianServiceFeature`) were moved to [src/types.ts](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/types.ts) to prevent circular dependencies.
## Implementation Details
### Phase 1: Core Commands ('LiveSyncCommands' Inheritors)
These contain significant state and business logic. They have been refactored into pure functional modules under `src/serviceFeatures/`:
- **[hiddenFileSync/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/hiddenFileSync/)**: Split monolithic file tracking and state variables into focused functional files.
- **[configSync/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/configSync/)**: Decoupled periodic synchronisation, customisation scanning, and commands.
- **[databaseMaintenance/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/databaseMaintenance/)**: Refactored garbage collection, compaction, and diagnostics into pure modules.
### Phase 2: Obsidian UI & Events ('AbstractObsidianModule' Inheritors)
These modules handle Obsidian-specific event bindings, UI registrations (views, dialogue modals, and ribbon commands), and user preferences. They have been refactored into 'ObsidianServiceFeature' functions:
- **[obsidianEvents/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/obsidianEvents/)**: Decoupled reload scheduling, save command overrides, and window visibility handlers.
- **Stateless UI/Command Registrars**:
- `ModuleInteractiveConflictResolver` -> [interactiveConflictResolver/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/interactiveConflictResolver/)
- `ModuleObsidianDocumentHistory` -> [obsidianDocumentHistory/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/obsidianDocumentHistory/)
- `ModuleGlobalHistory` -> [globalHistory/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/globalHistory/)
- `ModuleLog` -> [logFeature/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/logFeature/)
- `ModuleObsidianSettingTab` -> [obsidianSettingDialogue/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/obsidianSettingDialogue/)
- `ModuleDev` -> [devFeature/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/devFeature/)
- **Obsidian-Specific Tools**:
- `ModuleObsidianMenu` -> [obsidianMenu/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/obsidianMenu/)
- `ModuleObsidianSettingsAsMarkdown` -> [obsidianSettingAsMarkdown/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/obsidianSettingAsMarkdown/)
- `SetupManager` -> [setupManager/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/setupManager/)
- `ModuleMigration` -> [migration/](file:///p:/plant25/obsidian/projects/obsidian-livesync/src/serviceFeatures/migration/)
### Phase 3: Core Modules Evaluation
Foundational modules (replicators and conflict resolver engines) will be evaluated in subsequent stages to decide if they should be true services on 'ServiceHub' or standalone features.
## Consequences
- **Encapsulated State**: Key state variables now live safely in feature closures rather than as global class properties.
- **Improved Testability**: We introduced robust unit test suites (`*.unit.spec.ts`) for all newly refactored features. Features can be easily tested by injecting mocked services and modules.
- **Eliminated Global Pollution**: The 'ServiceHub' remains lightweight, only carrying services that must be globally shared.
- **Type Safety**: Obsidian-specific contexts (`app`, `plugin`, and `liveSyncPlugin`) are strictly typed through the `NecessaryObsidianFeature` shape, minimising unsafe type assertions.

View File

@@ -23,6 +23,7 @@
"lint": "eslint --cache --concurrency auto src",
"svelte-check": "svelte-check --tsconfig ./tsconfig.json",
"tsc-check": "tsc --noEmit",
"check:onlymain": "npm run tsc-check && npm run lint && npm run svelte-check && npm run check:compatibility",
"pretty:importpath": "cd utilsdeno && deno run -A ./normalise-imports.ts",
"pretty:json": "prettier --config ./.prettierrc.mjs \"**/*.json\" --write --log-level error",
"pretty": "npm run prettyNoWrite -- --write --log-level error",

View File

@@ -18,16 +18,9 @@ import { useRemoteConfigurationMigration } from "@lib/serviceFeatures/remoteConf
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import type { InjectableServiceHub } from "@lib/services/InjectableServices";
import { AbstractModule } from "./modules/AbstractModule";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess";
import { ModuleReplicator } from "./modules/core/ModuleReplicator";
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB";
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker";
import { ModuleConflictResolver } from "./modules/coreFeatures/ModuleConflictResolver";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain";
import type { ServiceModules } from "@lib/interfaces/ServiceModule";
import { ModuleBasicMenu } from "./modules/essential/ModuleBasicMenu";
import { usePrepareDatabaseForUse } from "@lib/serviceFeatures/prepareDatabaseForUse";
import type { Constructor } from "@lib/common/utils.type";
@@ -99,6 +92,9 @@ export class LiveSyncBaseCore<
}
return this._services;
}
get context(): T {
return (this.services as any).context;
}
/**
* Service Modules
*/
@@ -138,14 +134,6 @@ export class LiveSyncBaseCore<
public registerModules(extraModules: AbstractModule[] = []) {
this._registerModule(new ModuleLiveSyncMain(this));
this._registerModule(new ModuleConflictChecker(this));
this._registerModule(new ModuleReplicatorMinIO(this));
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleConflictResolver(this));
this._registerModule(new ModulePeriodicProcess(this));
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
this._registerModule(new ModuleBasicMenu(this));
for (const module of extraModules) {
this._registerModule(module);

View File

@@ -1,4 +1,4 @@
import { type PluginManifest, TFile } from "@/deps.ts";
import type { PluginManifest, TFile } from "@/deps.ts";
import { type DatabaseEntry, type EntryBody, type FilePath } from "@lib/common/types.ts";
export type { CacheData, FileEventItem } from "@lib/common/types.ts";

View File

@@ -18,7 +18,7 @@
import type { P2PSyncSetting, RemoteConfiguration } from "@lib/common/models/setting.type";
import { activateP2PRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
import { extractP2PRoomSuffix } from "@lib/common/utils";
import { SetupManager } from "@/modules/features/SetupManager";
import { getSetupManager } from "@/modules/features/SetupManager";
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
interface Props {
@@ -217,7 +217,7 @@
}
async function createAndSelectP2PRemote() {
const setupManager = core.getModule(SetupManager);
const setupManager = getSetupManager();
const dialogManager = setupManager.dialogManager;
const currentSettings = core.services.setting.currentSettings();
const p2pConf = await dialogManager.openWithExplicitCancel(SetupRemoteP2P, currentSettings);

View File

@@ -2,17 +2,25 @@ import { getLanguage, Notice, Plugin, type App, type PluginManifest } from "./de
import { setGetLanguage } from "@lib/common/coreEnvFunctions.ts";
setGetLanguage(getLanguage);
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
// import { ModuleDev } from "./modules/extras/ModuleDev.ts";
import { ModuleInteractiveConflictResolver } from "./modules/features/ModuleInteractiveConflictResolver.ts";
import { ModuleLog } from "./modules/features/ModuleLog.ts";
import { ModuleObsidianEvents } from "./modules/essentialObsidian/ModuleObsidianEvents.ts";
import { ModuleObsidianSettingDialogue } from "./modules/features/ModuleObsidianSettingTab.ts";
import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidianDocumentHistory.ts";
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
// Migrated features
import { useInteractiveConflictResolver } from "./serviceFeatures/interactiveConflictResolver/index.ts";
import { useLogFeature } from "./serviceFeatures/logFeature/index.ts";
import { useObsidianEvents } from "./serviceFeatures/obsidianEvents/index.ts";
import { useObsidianSettingDialogue } from "./serviceFeatures/obsidianSettingDialogue/index.ts";
import { useObsidianDocumentHistory } from "./serviceFeatures/obsidianDocumentHistory/index.ts";
import { useGlobalHistory } from "./serviceFeatures/globalHistory/index.ts";
import { useDevFeature } from "./serviceFeatures/devFeature/index.ts";
import { useConfigSync } from "./serviceFeatures/configSync/index.ts";
import { useHiddenFileSync } from "./serviceFeatures/hiddenFileSync/index.ts";
import { useDatabaseMaintenance } from "./serviceFeatures/databaseMaintenance/index.ts";
import { usePeriodicReplication } from "./serviceFeatures/periodicReplication/index.ts";
import { useConflictChecker, useConflictResolver } from "./serviceFeatures/conflictResolution/index.ts";
import { useMismatchedTweaksResolver } from "./serviceFeatures/tweakMismatch/index.ts";
import {
useReplicator,
useCouchDBReplicatorFactory,
useMinIOReplicatorFactory,
} from "./serviceFeatures/replicator/index.ts";
import type { InjectableServiceHub } from "@lib/services/implements/injectable/InjectableServiceHub.ts";
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
import { ServiceRebuilder } from "@lib/serviceModules/Rebuilder.ts";
@@ -22,14 +30,14 @@ import { StorageAccessManager } from "@lib/managers/StorageProcessingManager.ts"
import { ServiceFileHandler } from "./serviceModules/FileHandler.ts";
import { FileAccessObsidian } from "./serviceModules/FileAccessObsidian.ts";
import { StorageEventManagerObsidian } from "./managers/StorageEventManagerObsidian.ts";
import type { ServiceModules } from "./types.ts";
import type { ServiceModules, LiveSyncCore } from "./types.ts";
import { setNoticeClass } from "@lib/mock_and_interop/wrapper.ts";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
import { LiveSyncBaseCore } from "./LiveSyncBaseCore.ts";
import { ModuleObsidianMenu } from "./modules/essentialObsidian/ModuleObsidianMenu.ts";
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
import { SetupManager } from "./modules/features/SetupManager.ts";
import { ModuleMigration } from "./modules/essential/ModuleMigration.ts";
import { useObsidianMenuFeature } from "./serviceFeatures/obsidianMenu/index.ts";
import { useObsidianSettingAsMarkdownFeature } from "./serviceFeatures/obsidianSettingAsMarkdown/index.ts";
import { useSetupManagerFeature } from "./serviceFeatures/setupManager/index.ts";
import { useMigrationFeature } from "./serviceFeatures/migration/index.ts";
import { enableI18nFeature } from "./serviceFeatures/onLayoutReady/enablei18n.ts";
import { useOfflineScanner } from "@lib/serviceFeatures/offlineScanner.ts";
import { useRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig.ts";
@@ -43,7 +51,10 @@ import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplica
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands.ts";
import { useP2PReplicatorUI } from "./serviceFeatures/useP2PReplicatorUI.ts";
import { createOpenReplicationUI, createOpenRebuildUI } from "./features/P2PSync/P2PReplicator/P2PReplicationUI.ts";
export type LiveSyncCore = LiveSyncBaseCore<ObsidianServiceContext, LiveSyncCommands>;
export type { LiveSyncCore, NecessaryObsidianFeature, ObsidianServiceFeatureFunction } from "./types.ts";
export { createObsidianServiceFeature } from "./types.ts";
export default class ObsidianLiveSyncPlugin extends Plugin {
core: LiveSyncCore;
@@ -144,27 +155,11 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
return this.initialiseServiceModules(core, serviceHub);
},
(core) => {
const extraModules = [
new ModuleObsidianEvents(this, core),
new ModuleObsidianSettingDialogue(this, core),
new ModuleObsidianMenu(core),
new ModuleObsidianSettingsAsMarkdown(core),
new ModuleLog(this, core),
new ModuleObsidianDocumentHistory(this, core),
new ModuleInteractiveConflictResolver(this, core),
new ModuleObsidianGlobalHistory(this, core),
// new ModuleDev(this, core),
new SetupManager(core), // this should be moved to core?
new ModuleMigration(core),
];
const extraModules = [] as any[];
return extraModules;
},
(core) => {
const addOns = [
new ConfigSync(this, core),
new HiddenFileSync(this, core),
new LocalDatabaseMaintenance(this, core),
];
() => {
const addOns = [] as any[];
return addOns;
},
(core) => {
@@ -172,7 +167,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const featuresInitialiser = enableI18nFeature;
const curriedFeature = () => featuresInitialiser(core);
core.services.appLifecycle.onLayoutReady.addHandler(curriedFeature);
const setupManager = core.getModule(SetupManager);
const setupManager = useSetupManagerFeature(core);
useMigrationFeature(core);
const replicator = useP2PReplicatorFeature(
core,
createOpenReplicationUI(this.app),
@@ -186,13 +182,35 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
useSetupQRCodeFeature(core);
useSetupURIFeature(core);
useSetupManagerHandlersFeature(core, setupManager);
useOfflineScanner(core);
useRedFlagFeatures(core);
useObsidianMenuFeature(core);
useObsidianSettingAsMarkdownFeature(core);
useCheckRemoteSize(core);
// p2pReplicatorResult = useP2PReplicator(core, [
// VIEW_TYPE_P2P,
// (leaf: any) => new P2PReplicatorPaneView(leaf, core, p2pReplicatorResult!),
// ]);
useOfflineScanner(core);
useRedFlagFeatures(core);
// Initialise newly migrated features
useObsidianEvents(core);
useObsidianSettingDialogue(core);
useLogFeature(core);
useObsidianDocumentHistory(core);
useInteractiveConflictResolver(core);
useGlobalHistory(core);
useDevFeature(core);
useConfigSync(core);
useHiddenFileSync(core);
useDatabaseMaintenance(core);
usePeriodicReplication(core);
useConflictChecker(core);
useConflictResolver(core);
useMismatchedTweaksResolver(core);
useReplicator(core);
useCouchDBReplicatorFactory(core);
useMinIOReplicatorFactory(core);
}
);
}

View File

@@ -1,41 +0,0 @@
import { PeriodicProcessor } from "@/common/PeriodicProcessor";
import type { LiveSyncCore } from "@/main";
import { AbstractModule } from "@/modules/AbstractModule";
export class ModulePeriodicProcess extends AbstractModule {
periodicSyncProcessor = new PeriodicProcessor(this.core, async () => await this.services.replication.replicate());
disablePeriodic() {
this.periodicSyncProcessor?.disable();
return Promise.resolve(true);
}
resumePeriodic() {
this.periodicSyncProcessor.enable(
this.settings.periodicReplication ? this.settings.periodicReplicationInterval * 1000 : 0
);
return Promise.resolve(true);
}
private _allOnUnload() {
return this.disablePeriodic();
}
private _everyBeforeRealizeSetting(): Promise<boolean> {
return this.disablePeriodic();
}
private _everyBeforeSuspendProcess(): Promise<boolean> {
return this.disablePeriodic();
}
private _everyAfterResumeProcess(): Promise<boolean> {
return this.resumePeriodic();
}
private _everyAfterRealizeSetting(): Promise<boolean> {
return this.resumePeriodic();
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onUnload.addHandler(this._allOnUnload.bind(this));
services.setting.onBeforeRealiseSetting.addHandler(this._everyBeforeRealizeSetting.bind(this));
services.setting.onSettingRealised.addHandler(this._everyAfterRealizeSetting.bind(this));
services.appLifecycle.onSuspending.addHandler(this._everyBeforeSuspendProcess.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
}

View File

@@ -1,291 +0,0 @@
import type PouchDB from "pouchdb-core";
import { fireAndForget } from "octagonal-wheels/promises";
import { AbstractModule } from "@/modules/AbstractModule";
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
import { skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { balanceChunkPurgedDBs } from "@lib/pouchdb/chunks";
import { purgeUnreferencedChunks } from "@lib/pouchdb/chunks";
import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import { type EntryDoc, type RemoteType } from "@lib/common/types";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "@/common/events";
import { $msg } from "@lib/common/i18n";
import type { LiveSyncCore } from "@/main";
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
import { clearHandlers } from "@lib/replication/SyncParamsHandler";
import type { NecessaryServices } from "@lib/interfaces/ServiceModule";
import { MARK_LOG_NETWORK_ERROR } from "@lib/services/lib/logUtils";
function isOnlineAndCanReplicate(
errorManager: UnresolvedErrorManager,
host: NecessaryServices<"API", never>,
showMessage: boolean
): Promise<boolean> {
const errorMessage = "Network is offline";
if (!host.services.API.isOnline) {
errorManager.showError(errorMessage, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return Promise.resolve(false);
}
errorManager.clearError(errorMessage);
return Promise.resolve(true);
}
async function canReplicateWithPBKDF2(
errorManager: UnresolvedErrorManager,
host: NecessaryServices<"replicator" | "setting", never>,
showMessage: boolean
): Promise<boolean> {
const currentSettings = host.services.setting.currentSettings();
// TODO: check using PBKDF2 salt?
const errorMessage = $msg("Replicator.Message.InitialiseFatalError");
const replicator = host.services.replicator.getActiveReplicator();
if (!replicator) {
errorManager.showError(errorMessage, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return false;
}
errorManager.clearError(errorMessage);
// Showing message is false: that because be shown here. (And it is a fatal error, no way to hide it).
// tagged as network error at beginning for error filtering with NetworkWarningStyles
const ensureMessage = `${MARK_LOG_NETWORK_ERROR}Failed to initialise the encryption key, preventing replication.`;
const ensureResult = await replicator.ensurePBKDF2Salt(currentSettings, showMessage, true);
if (!ensureResult) {
errorManager.showError(ensureMessage, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
return false;
}
errorManager.clearError(ensureMessage);
return ensureResult; // is true.
}
export class ModuleReplicator extends AbstractModule {
_replicatorType?: RemoteType;
processor: ReplicateResultProcessor = new ReplicateResultProcessor(this);
private _unresolvedErrorManager: UnresolvedErrorManager = new UnresolvedErrorManager(
this.core.services.appLifecycle
);
clearErrors() {
this._unresolvedErrorManager.clearErrors();
}
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
eventHub.onEvent(EVENT_FILE_SAVED, () => {
if (this.settings.syncOnSave && !this.core.services.appLifecycle.isSuspended()) {
scheduleTask("perform-replicate-after-save", 250, () => this.services.replication.replicateByEvent());
}
});
eventHub.onEvent(EVENT_SETTING_SAVED, (setting) => {
if (this.core.settings.suspendParseReplicationResult) {
this.processor.suspend();
} else {
this.processor.resume();
}
});
return Promise.resolve(true);
}
_onReplicatorInitialised(): Promise<boolean> {
// For now, we only need to clear the error related to replicator initialisation, but in the future, if there are more things to do when the replicator is initialised, we can add them here.
clearHandlers();
return Promise.resolve(true);
}
_everyOnDatabaseInitialized(showNotice: boolean): Promise<boolean> {
fireAndForget(() => this.processor.restoreFromSnapshotOnce());
return Promise.resolve(true);
}
async _everyBeforeReplicate(showMessage: boolean): Promise<boolean> {
await this.processor.restoreFromSnapshotOnce();
this.clearErrors();
return true;
}
/**
* obsolete method. No longer maintained and will be removed in the future.
* @deprecated v0.24.17
* @param showMessage If true, show message to the user.
*/
async cleaned(showMessage: boolean) {
Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
await skipIfDuplicated("cleanup", async () => {
const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
const message = `The remote database has been cleaned up.
To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
However, If there are many chunks to be deleted, maybe fetching again is faster.
We will lose the history of this device if we fetch the remote database again.
Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`;
const CHOICE_FETCH = "Fetch again";
const CHOICE_CLEAN = "Cleanup";
const CHOICE_DISMISS = "Dismiss";
const ret = await this.core.confirm.confirmWithMessage(
"Cleaned",
message,
[CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS],
CHOICE_DISMISS,
30
);
if (ret == CHOICE_FETCH) {
await this.core.rebuilder.$performRebuildDB("localOnly");
}
if (ret == CHOICE_CLEAN) {
const replicator = this.services.replicator.getActiveReplicator();
if (!(replicator instanceof LiveSyncCouchDBReplicator)) return;
const remoteDB = await replicator.connectRemoteCouchDBWithSetting(
this.settings,
this.services.API.isMobile(),
true
);
if (typeof remoteDB == "string") {
Logger(remoteDB, LOG_LEVEL_NOTICE);
return false;
}
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.clearCaches();
// Perform the synchronisation once.
if (await this.core.replicator.openReplication(this.settings, false, showMessage, true)) {
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
this.localDatabase.clearCaches();
await this.services.replicator.getActiveReplicator()?.markRemoteResolved(this.settings);
Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO);
} else {
Logger(
"Replication has been cancelled. Please try it again.",
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
);
}
}
});
}
private async onReplicationFailed(showMessage: boolean = false): Promise<boolean> {
const activeReplicator = this.services.replicator.getActiveReplicator();
if (!activeReplicator) {
Logger(`No active replicator found`, LOG_LEVEL_INFO);
return false;
}
if (activeReplicator.tweakSettingsMismatched && activeReplicator.preferredTweakValue) {
await this.services.tweakValue.askResolvingMismatched(activeReplicator.preferredTweakValue);
} else {
if (activeReplicator.remoteLockedAndDeviceNotAccepted) {
if (activeReplicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
await this.cleaned(showMessage);
} else {
const message = $msg("Replicator.Dialogue.Locked.Message");
const CHOICE_FETCH = $msg("Replicator.Dialogue.Locked.Action.Fetch");
const CHOICE_DISMISS = $msg("Replicator.Dialogue.Locked.Action.Dismiss");
const CHOICE_UNLOCK = $msg("Replicator.Dialogue.Locked.Action.Unlock");
const ret = await this.core.confirm.askSelectStringDialogue(
message,
[CHOICE_FETCH, CHOICE_UNLOCK, CHOICE_DISMISS],
{
title: $msg("Replicator.Dialogue.Locked.Title"),
defaultAction: CHOICE_DISMISS,
timeout: 60,
}
);
if (ret == CHOICE_FETCH) {
this._log($msg("Replicator.Dialogue.Locked.Message.Fetch"), LOG_LEVEL_NOTICE);
await this.core.rebuilder.scheduleFetch();
this.services.appLifecycle.scheduleRestart();
return false;
} else if (ret == CHOICE_UNLOCK) {
await activeReplicator.markRemoteResolved(this.settings);
this._log($msg("Replicator.Dialogue.Locked.Message.Unlocked"), LOG_LEVEL_NOTICE);
return false;
}
}
}
}
// TODO: Check again and true/false return. This will be the result for performReplication.
return false;
}
// private async _replicateByEvent(): Promise<boolean | void> {
// const least = this.settings.syncMinimumInterval;
// if (least > 0) {
// return rateLimitedSharedExecution(KEY_REPLICATION_ON_EVENT, least, async () => {
// return await this.services.replication.replicate();
// });
// }
// return await shareRunningResult(`replication`, () => this.services.replication.replicate());
// }
_parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>): Promise<boolean> {
this.processor.enqueueAll(docs);
return Promise.resolve(true);
}
// _everyBeforeSuspendProcess(): Promise<boolean> {
// this.core.replicator?.closeReplication();
// return Promise.resolve(true);
// }
// private async _replicateAllToServer(
// showingNotice: boolean = false,
// sendChunksInBulkDisabled: boolean = false
// ): Promise<boolean> {
// if (!this.services.appLifecycle.isReady()) return false;
// if (!(await this.services.replication.onBeforeReplicate(showingNotice))) {
// Logger($msg("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
// return false;
// }
// if (!sendChunksInBulkDisabled) {
// if (this.core.replicator instanceof LiveSyncCouchDBReplicator) {
// if (
// (await this.core.confirm.askYesNoDialog("Do you want to send all chunks before replication?", {
// defaultOption: "No",
// timeout: 20,
// })) == "yes"
// ) {
// await this.core.replicator.sendChunks(this.core.settings, undefined, true, 0);
// }
// }
// }
// const ret = await this.core.replicator.replicateAllToServer(this.settings, showingNotice);
// if (ret) return true;
// const checkResult = await this.services.replication.checkConnectionFailure();
// if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllToRemote(showingNotice);
// return !checkResult;
// }
// async _replicateAllFromServer(showingNotice: boolean = false): Promise<boolean> {
// if (!this.services.appLifecycle.isReady()) return false;
// const ret = await this.core.replicator.replicateAllFromServer(this.settings, showingNotice);
// if (ret) return true;
// const checkResult = await this.services.replication.checkConnectionFailure();
// if (checkResult == "CHECKAGAIN") return await this.services.remote.replicateAllFromRemote(showingNotice);
// return !checkResult;
// }
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.onReplicatorInitialised.addHandler(this._onReplicatorInitialised.bind(this));
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.parseSynchroniseResult.addHandler(this._parseReplicationResult.bind(this));
// --> These handlers can be separated.
const isOnlineAndCanReplicateWithHost = isOnlineAndCanReplicate.bind(null, this._unresolvedErrorManager, {
services: {
API: services.API,
},
serviceModules: {},
});
const canReplicateWithPBKDF2WithHost = canReplicateWithPBKDF2.bind(null, this._unresolvedErrorManager, {
services: {
replicator: services.replicator,
setting: services.setting,
},
serviceModules: {},
});
services.replication.onBeforeReplicate.addHandler(isOnlineAndCanReplicateWithHost, 10);
services.replication.onBeforeReplicate.addHandler(canReplicateWithPBKDF2WithHost, 20);
// <-- End of handlers that can be separated.
services.replication.onBeforeReplicate.addHandler(this._everyBeforeReplicate.bind(this), 100);
services.replication.onReplicationFailed.addHandler(this.onReplicationFailed.bind(this));
}
}

View File

@@ -1,42 +0,0 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "@lib/common/types";
import { LiveSyncCouchDBReplicator } from "@lib/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "@/modules/AbstractModule";
import type { LiveSyncCore } from "@/main";
export class ModuleReplicatorCouchDB extends AbstractModule {
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
const settings = { ...this.settings, ...settingOverride };
// If new remote types were added, add them here. Do not use `REMOTE_COUCHDB` directly for the safety valve.
if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
return Promise.resolve(false);
}
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
}
_everyAfterResumeProcess(): Promise<boolean> {
if (this.services.appLifecycle.isSuspended()) return Promise.resolve(true);
if (!this.services.appLifecycle.isReady()) return Promise.resolve(true);
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
const LiveSyncEnabled = this.settings.liveSync;
const continuous = LiveSyncEnabled;
const eventualOnStart = !LiveSyncEnabled && this.settings.syncOnStart;
// If enabled LiveSync or on start, open replication
if (LiveSyncEnabled || eventualOnStart) {
// And note that we do not open the conflict detection dialogue directly during this process.
// This should be raised explicitly if needed.
fireAndForget(async () => {
const canReplicate = await this.services.replication.isReplicationReady(false);
if (!canReplicate) return;
void this.core.replicator.openReplication(this.settings, continuous, false, false);
});
}
}
return Promise.resolve(true);
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
services.appLifecycle.onResumed.addHandler(this._everyAfterResumeProcess.bind(this));
}
}

View File

@@ -1,18 +0,0 @@
import { REMOTE_MINIO, type RemoteDBSettings } from "@lib/common/types";
import { LiveSyncJournalReplicator } from "@lib/replication/journal/LiveSyncJournalReplicator";
import type { LiveSyncAbstractReplicator } from "@lib/replication/LiveSyncAbstractReplicator";
import type { LiveSyncCore } from "@/main";
import { AbstractModule } from "@/modules/AbstractModule";
export class ModuleReplicatorMinIO extends AbstractModule {
_anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator | false> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_MINIO) {
return Promise.resolve(new LiveSyncJournalReplicator(this.core));
}
return Promise.resolve(false);
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.getNewReplicator.addHandler(this._anyNewReplicator.bind(this));
}
}

View File

@@ -1,82 +0,0 @@
import { AbstractModule } from "@/modules/AbstractModule.ts";
import { LOG_LEVEL_NOTICE, type FilePathWithPrefix } from "@lib/common/types";
import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { sendValue } from "octagonal-wheels/messagepassing/signal";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
export class ModuleConflictChecker extends AbstractModule {
async _queueConflictCheckIfOpen(file: FilePathWithPrefix): Promise<void> {
const path = file;
if (this.settings.checkConflictOnlyOnOpen) {
const af = this.services.vault.getActiveFilePath();
if (af && af != path) {
this._log(`${file} is conflicted, merging process has been postponed.`, LOG_LEVEL_NOTICE);
return;
}
}
await this.services.conflict.queueCheckFor(path);
}
async _queueConflictCheck(file: FilePathWithPrefix): Promise<void> {
const optionalConflictResult = await this.services.conflict.getOptionalConflictCheckMethod(file);
if (optionalConflictResult == true) {
// The conflict has been resolved by another process.
return;
} else if (optionalConflictResult === "newer") {
// The conflict should be resolved by the newer entry.
await this.services.conflict.resolveByNewest(file);
} else {
this.conflictCheckQueue.enqueue(file);
}
}
_waitForAllConflictProcessed(): Promise<boolean> {
return this.conflictResolveQueue.waitForAllProcessed();
}
// TODO-> Move to ModuleConflictResolver?
conflictResolveQueue = new QueueProcessor(
async (filenames: FilePathWithPrefix[]) => {
const filename = filenames[0];
return await this.services.conflict.resolve(filename);
},
{
suspended: false,
batchSize: 1,
// No need to limit concurrency to `1` here, subsequent process will handle it,
// And, some cases, we do not need to synchronised. (e.g., auto-merge available).
// Therefore, limiting global concurrency is performed on resolver with the UI.
concurrentLimit: 10,
delay: 0,
keepResultUntilDownstreamConnected: false,
}
).replaceEnqueueProcessor((queue, newEntity) => {
const filename = newEntity;
sendValue("cancel-resolve-conflict:" + filename, true);
const newQueue = [...queue].filter((e) => e != newEntity);
return [...newQueue, newEntity];
});
conflictCheckQueue = // First process - Check is the file actually need resolve -
new QueueProcessor(
(files: FilePathWithPrefix[]) => {
const filename = files[0];
return Promise.resolve([filename]);
},
{
suspended: false,
batchSize: 1,
concurrentLimit: 10,
delay: 0,
keepResultUntilDownstreamConnected: true,
pipeTo: this.conflictResolveQueue,
totalRemainingReactiveSource: this.services.conflict.conflictProcessQueueCount,
}
);
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.conflict.queueCheckForIfOpen.setHandler(this._queueConflictCheckIfOpen.bind(this));
services.conflict.queueCheckFor.setHandler(this._queueConflictCheck.bind(this));
services.conflict.ensureAllProcessed.setHandler(this._waitForAllConflictProcessed.bind(this));
}
}

View File

@@ -1,236 +0,0 @@
import { serialized } from "octagonal-wheels/concurrency/lock";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import {
AUTO_MERGED,
CANCELLED,
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
LOG_LEVEL_VERBOSE,
MISSING_OR_ERROR,
NOT_CONFLICTED,
type diff_check_result,
type FilePathWithPrefix,
} from "@lib/common/types";
import { isCustomisationSyncMetadata, isPluginMetadata } from "@lib/common/typeUtils.ts";
import { TARGET_IS_NEW } from "@lib/common/models/shared.const.symbols.ts";
import { compareMTime, displayRev } from "@lib/common/utils.ts";
import diff_match_patch from "diff-match-patch";
import { stripAllPrefixes, isPlainText } from "@lib/string_and_binary/path";
import { eventHub } from "@/common/events.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
declare global {
interface LSEvents {
"conflict-cancelled": FilePathWithPrefix;
}
}
export class ModuleConflictResolver extends AbstractModule {
private async _resolveConflictByDeletingRev(
path: FilePathWithPrefix,
deleteRevision: string,
subTitle = ""
): Promise<typeof MISSING_OR_ERROR | typeof AUTO_MERGED> {
const title = `Resolving ${subTitle ? `[${subTitle}]` : ""}:`;
if (!(await this.core.fileHandler.deleteRevisionFromDB(path, deleteRevision))) {
this._log(
`${title} Could not delete conflicted revision ${displayRev(deleteRevision)} of ${path}`,
LOG_LEVEL_NOTICE
);
return MISSING_OR_ERROR;
}
eventHub.emitEvent("conflict-cancelled", path);
this._log(
`${title} Conflicted revision has been deleted ${displayRev(deleteRevision)} ${path}`,
LOG_LEVEL_INFO
);
if ((await this.core.databaseFileAccess.getConflictedRevs(path)).length != 0) {
this._log(`${title} some conflicts are left in ${path}`, LOG_LEVEL_INFO);
return AUTO_MERGED;
}
if (isPluginMetadata(path) || isCustomisationSyncMetadata(path)) {
this._log(`${title} ${path} is a plugin metadata file, no need to write to storage`, LOG_LEVEL_INFO);
return AUTO_MERGED;
}
// If no conflicts were found, write the resolved content to the storage.
if (!(await this.core.fileHandler.dbToStorage(path, stripAllPrefixes(path), true))) {
this._log(`Could not write the resolved content to the storage: ${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
const level = subTitle.indexOf("same") !== -1 ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE;
this._log(`${path} has been merged automatically`, level);
return AUTO_MERGED;
}
async checkConflictAndPerformAutoMerge(path: FilePathWithPrefix): Promise<diff_check_result> {
//
const ret = await this.localDatabase.tryAutoMerge(path, !this.settings.disableMarkdownAutoMerge);
if ("ok" in ret) {
return ret.ok;
}
if ("result" in ret) {
const p = ret.result;
// Merged content is coming.
// 1. Store the merged content to the storage
if (!(await this.core.databaseFileAccess.storeContent(path, p))) {
this._log(`Merged content cannot be stored:${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
// 2. As usual, delete the conflicted revision and if there are no conflicts, write the resolved content to the storage.
return await this.services.conflict.resolveByDeletingRevision(path, ret.conflictedRev, "Sensible");
}
const { rightRev, leftLeaf, rightLeaf } = ret;
// should be one or more conflicts;
if (leftLeaf == false) {
// what's going on..
this._log(`could not get current revisions:${path}`, LOG_LEVEL_NOTICE);
return MISSING_OR_ERROR;
}
if (rightLeaf == false) {
// Conflicted item could not load, delete this.
return await this.services.conflict.resolveByDeletingRevision(path, rightRev, "MISSING OLD REV");
}
const isSame = leftLeaf.data == rightLeaf.data && leftLeaf.deleted == rightLeaf.deleted;
const isBinary = !isPlainText(path);
const alwaysNewer = this.settings.resolveConflictsByNewerFile;
if (isSame || isBinary || alwaysNewer) {
const result = compareMTime(leftLeaf.mtime, rightLeaf.mtime);
let loser = leftLeaf;
// if (lMtime > rMtime) {
if (result != TARGET_IS_NEW) {
loser = rightLeaf;
}
const subTitle = [
`${isSame ? "same" : ""}`,
`${isBinary ? "binary" : ""}`,
`${alwaysNewer ? "alwaysNewer" : ""}`,
]
.filter((e) => e.trim())
.join(",");
return await this.services.conflict.resolveByDeletingRevision(path, loser.rev, subTitle);
}
// make diff.
const dmp = new diff_match_patch();
const diff = dmp.diff_main(leftLeaf.data, rightLeaf.data);
dmp.diff_cleanupSemantic(diff);
this._log(`conflict(s) found:${path}`);
return {
left: leftLeaf,
right: rightLeaf,
diff: diff,
};
}
private async _resolveConflict(filename: FilePathWithPrefix): Promise<void> {
// const filename = filenames[0];
return await serialized(`conflict-resolve:${filename}`, async () => {
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
if (
conflictCheckResult === MISSING_OR_ERROR ||
conflictCheckResult === NOT_CONFLICTED ||
conflictCheckResult === CANCELLED
) {
// nothing to do.
this._log(`[conflict] Not conflicted or cancelled: ${filename}`, LOG_LEVEL_VERBOSE);
return;
}
if (conflictCheckResult === AUTO_MERGED) {
//auto resolved, but need check again;
if (this.settings.syncAfterMerge && !this.services.appLifecycle.isSuspended()) {
//Wait for the running replication, if not running replication, run it once.
await this.services.replication.replicateByEvent();
}
this._log("[conflict] Automatically merged, but we have to check it again");
await this.services.conflict.queueCheckFor(filename);
return;
}
if (this.settings.showMergeDialogOnlyOnActive) {
const af = this.services.vault.getActiveFilePath();
if (af && af != filename) {
this._log(
`[conflict] ${filename} is conflicted. Merging process has been postponed to the file have got opened.`,
LOG_LEVEL_NOTICE
);
return;
}
}
this._log("[conflict] Manual merge required!");
eventHub.emitEvent("conflict-cancelled", filename);
await this.services.conflict.resolveByUserInteraction(filename, conflictCheckResult);
});
}
private async _anyResolveConflictByNewest(filename: FilePathWithPrefix): Promise<boolean> {
const currentRev = await this.core.databaseFileAccess.fetchEntryMeta(filename, undefined, true);
if (currentRev == false) {
this._log(`Could not get current revision of ${filename}`);
return Promise.resolve(false);
}
const revs = await this.core.databaseFileAccess.getConflictedRevs(filename);
if (revs.length == 0) {
return Promise.resolve(true);
}
const mTimeAndRev = (
[
[currentRev.mtime, currentRev._rev],
...(await Promise.all(
revs.map(async (rev) => {
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
if (leaf == false) {
return [0, rev];
}
return [leaf.mtime, rev];
})
)),
] as [number, string][]
).sort((a, b) => {
const diff = b[0] - a[0];
if (diff == 0) {
return a[1].localeCompare(b[1], "en", { numeric: true });
}
return diff;
});
// console.warn(mTimeAndRev);
this._log(
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
);
for (let i = 1; i < mTimeAndRev.length; i++) {
this._log(
`conflict: Deleting the older revision ${mTimeAndRev[i][1]} (${new Date(mTimeAndRev[i][0]).toLocaleString()}) of ${filename}`
);
await this.services.conflict.resolveByDeletingRevision(filename, mTimeAndRev[i][1], "NEWEST");
}
return true;
}
private async _resolveAllConflictedFilesByNewerOnes() {
this._log(`Resolving conflicts by newer ones`, LOG_LEVEL_NOTICE);
const files = await this.core.storageAccess.getFileNames();
let i = 0;
for (const file of files) {
if (i++ % 10)
this._log(
`Check and Processing ${i} / ${files.length}`,
LOG_LEVEL_NOTICE,
"resolveAllConflictedFilesByNewerOnes"
);
await this.services.conflict.resolveByNewest(file);
}
this._log(`Done!`, LOG_LEVEL_NOTICE, "resolveAllConflictedFilesByNewerOnes");
}
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.conflict.resolveByDeletingRevision.setHandler(this._resolveConflictByDeletingRev.bind(this));
services.conflict.resolve.setHandler(this._resolveConflict.bind(this));
services.conflict.resolveByNewest.setHandler(this._anyResolveConflictByNewest.bind(this));
services.conflict.resolveAllConflictedFilesByNewerOnes.setHandler(
this._resolveAllConflictedFilesByNewerOnes.bind(this)
);
}
}

View File

@@ -1,415 +0,0 @@
import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import { extractObject } from "octagonal-wheels/object";
import {
TweakValuesShouldMatchedTemplate,
TweakValuesTemplate,
IncompatibleChanges,
confName,
type TweakValues,
type ObsidianLiveSyncSettings,
type RemoteDBSettings,
IncompatibleChangesInSpecificPattern,
CompatibleButLossyChanges,
} from "@lib/common/types.ts";
import { escapeMarkdownValue } from "@lib/common/utils.ts";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import { $msg } from "@lib/common/i18n.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
import { REMOTE_P2P } from "@lib/common/models/setting.const.ts";
function valueToString(value: string | number | boolean | object | undefined): string {
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "object") {
return JSON.stringify(value);
}
return `${value}`;
}
export class ModuleResolvingMismatchedTweaks extends AbstractModule {
private _hasNotifiedAutoAcceptCompatibleUndefined = false;
private _collectMismatchedTweakKeys(current: TweakValues, preferred: Partial<TweakValues>) {
const items = Object.keys(
TweakValuesShouldMatchedTemplate
) as (keyof typeof TweakValuesShouldMatchedTemplate)[];
return items.filter((key) => current[key] !== preferred[key]);
}
private _selectNewerTweakSide(current: TweakValues, preferred: Partial<TweakValues>): "REMOTE" | "CURRENT" {
Logger(`Modified: ${current.tweakModified} (current) vs ${preferred.tweakModified} (preferred)`);
const currentModified = current.tweakModified;
const preferredModified = preferred.tweakModified;
// debugger;
const hasCurrentModified = typeof currentModified === "number" && currentModified > 0;
const hasPreferredModified = typeof preferredModified === "number" && preferredModified > 0;
if (!hasCurrentModified && !hasPreferredModified) return "REMOTE";
if (!hasCurrentModified) return "REMOTE";
if (!hasPreferredModified) return "CURRENT";
if (preferredModified >= currentModified) return "REMOTE";
return "CURRENT";
}
private async _shouldAutoAcceptCompatibleLossy(
current: TweakValues,
preferred: Partial<TweakValues>,
mismatchedKeys: (keyof typeof TweakValuesShouldMatchedTemplate)[]
): Promise<"REMOTE" | "CURRENT" | undefined> {
if (mismatchedKeys.length === 0) return undefined;
const hasOnlyCompatibleLossyMismatches = mismatchedKeys.every(
(key) => CompatibleButLossyChanges.indexOf(key) !== -1
);
if (!hasOnlyCompatibleLossyMismatches) return undefined;
if (this.settings.autoAcceptCompatibleTweak === undefined) {
if (this._hasNotifiedAutoAcceptCompatibleUndefined) {
return undefined;
}
this._hasNotifiedAutoAcceptCompatibleUndefined = true;
const CHOICE_ENABLE = $msg("TweakMismatchResolve.Action.EnableAutoAcceptCompatible");
const CHOICE_DISABLE = $msg("TweakMismatchResolve.Action.DisableAutoAcceptCompatible");
const CHOICES = [CHOICE_ENABLE, CHOICE_DISABLE] as const;
const message = $msg("TweakMismatchResolve.Message.AutoAcceptCompatibleUndefined");
const ret = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
title: $msg("TweakMismatchResolve.Title.AutoAcceptCompatible"),
timeout: 0,
defaultAction: CHOICE_ENABLE,
});
if (ret !== CHOICE_ENABLE) {
return undefined;
}
await this.services.setting.applyPartial(
{
autoAcceptCompatibleTweak: true,
},
true
);
Logger("Auto-accept for compatible tweak mismatch has been enabled.");
}
if (this.settings.autoAcceptCompatibleTweak !== true) return undefined;
return this._selectNewerTweakSide(current, preferred);
}
/**
* Hook before saving settings, to check if there are changes in tweak values, and if so,
* update the tweakModified timestamp to current time.
* This allows other devices to know that the tweak values have been changed and decide whether to accept the new values based on the modification time.
* @param next
* @param previous
* @returns
*/
async _onBeforeSaveSettingData(next: ObsidianLiveSyncSettings, previous: ObsidianLiveSyncSettings) {
const tweakKeys = Object.keys(TweakValuesTemplate) as (keyof TweakValues)[];
const tweakKeysForUpdate = tweakKeys.filter((key) => key !== "tweakModified");
const hasChangedTweak = tweakKeysForUpdate.some((key) => next[key] !== previous[key]);
if (!hasChangedTweak) return;
Logger(
`Some tweak values have been changed. ${tweakKeysForUpdate.filter((key) => next[key] !== previous[key]).join(", ")}`
);
const modified = Date.now();
Logger(`Modified: ${modified}`);
return await Promise.resolve({
tweakModified: modified,
});
}
async _anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
const preferred = this.core.replicator.preferredTweakValue;
if (!preferred) return false;
const ret = await this.services.tweakValue.askResolvingMismatched(preferred);
if (ret == "OK") return false;
if (ret == "CHECKAGAIN") return "CHECKAGAIN";
if (ret == "IGNORE") return true;
}
async _checkAndAskResolvingMismatchedTweaks(preferred: TweakValues): Promise<[TweakValues | boolean, boolean]> {
const mine = extractObject(TweakValuesTemplate, this.settings) as TweakValues;
const mismatchedKeys = this._collectMismatchedTweakKeys(mine, preferred);
const autoAcceptSide = await this._shouldAutoAcceptCompatibleLossy(mine, preferred, mismatchedKeys);
if (autoAcceptSide === "REMOTE") {
return [{ ...mine, ...preferred }, false];
}
if (autoAcceptSide === "CURRENT") {
return [true, false];
}
const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false;
let rebuildRecommended = false;
// Making tables:
// let table = `| Value name | This device | Configured | \n` + `|: --- |: --- :|: ---- :| \n`;
const tableRows = [];
// const items = [mine,preferred]
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const valueMine = escapeMarkdownValue(mine[key]);
const valuePreferred = escapeMarkdownValue(preferred[key]);
if (valueMine == valuePreferred) continue;
if (IncompatibleChanges.indexOf(key) !== -1) {
rebuildRequired = true;
}
for (const pattern of IncompatibleChangesInSpecificPattern) {
if (pattern.key !== key) continue;
// if from value supplied, check if current value have been violated : in other words, if the current value is the same as the from value, it should require a rebuild.
const isFromConditionMet = "from" in pattern ? pattern.from === mine[key] : false;
// and, if to value supplied, same as above.
const isToConditionMet = "to" in pattern ? pattern.to === preferred[key] : false;
// if either of them is true, it should require a rebuild, if the pattern is not a recommendation.
if (isFromConditionMet || isToConditionMet) {
if (pattern.isRecommendation) {
rebuildRecommended = true;
} else {
rebuildRequired = true;
}
}
}
if (CompatibleButLossyChanges.indexOf(key) !== -1) {
rebuildRecommended = true;
}
// table += `| ${confName(key)} | ${valueMine} | ${valuePreferred} | \n`;
tableRows.push(
$msg("TweakMismatchResolve.Table.Row", {
name: confName(key),
self: valueToString(valueMine),
remote: valueToString(valuePreferred),
})
);
}
const additionalMessage =
rebuildRequired && this.core.settings.isConfigured
? $msg("TweakMismatchResolve.Message.WarningIncompatibleRebuildRequired")
: "";
const additionalMessage2 =
rebuildRecommended && this.core.settings.isConfigured
? $msg("TweakMismatchResolve.Message.WarningIncompatibleRebuildRecommended")
: "";
const table = $msg("TweakMismatchResolve.Table", { rows: tableRows.join("\n") });
const message = $msg("TweakMismatchResolve.Message.MainTweakResolving", {
table: table,
additionalMessage: [additionalMessage, additionalMessage2].filter((v) => v).join("\n"),
});
const CHOICE_USE_REMOTE = $msg("TweakMismatchResolve.Action.UseRemote");
const CHOICE_USE_REMOTE_WITH_REBUILD = $msg("TweakMismatchResolve.Action.UseRemoteWithRebuild");
const CHOICE_USE_REMOTE_PREVENT_REBUILD = $msg("TweakMismatchResolve.Action.UseRemoteAcceptIncompatible");
const CHOICE_USE_MINE = $msg("TweakMismatchResolve.Action.UseMine");
const CHOICE_USE_MINE_WITH_REBUILD = $msg("TweakMismatchResolve.Action.UseMineWithRebuild");
const CHOICE_USE_MINE_PREVENT_REBUILD = $msg("TweakMismatchResolve.Action.UseMineAcceptIncompatible");
const CHOICE_DISMISS = $msg("TweakMismatchResolve.Action.Dismiss");
const CHOICE_AND_VALUES = [] as [string, [result: TweakValues | boolean, rebuild: boolean]][];
if (rebuildRequired) {
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [preferred, true]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]);
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_PREVENT_REBUILD, [preferred, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE_PREVENT_REBUILD, [true, false]]);
} else if (rebuildRecommended) {
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE_WITH_REBUILD, [true, true]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE_WITH_REBUILD, [true, true]]);
} else {
CHOICE_AND_VALUES.push([CHOICE_USE_REMOTE, [preferred, false]]);
CHOICE_AND_VALUES.push([CHOICE_USE_MINE, [true, false]]);
}
CHOICE_AND_VALUES.push([CHOICE_DISMISS, [false, false]]);
const CHOICES = Object.fromEntries(CHOICE_AND_VALUES) as Record<
string,
[TweakValues | boolean, performRebuild: boolean]
>;
const retKey = await this.core.confirm.askSelectStringDialogue(message, Object.keys(CHOICES), {
title: $msg("TweakMismatchResolve.Title.TweakResolving"),
timeout: 60,
defaultAction: CHOICE_DISMISS,
});
if (!retKey) return [false, false];
return CHOICES[retKey];
}
async _askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
if (!this.core.replicator.tweakSettingsMismatched) {
return "OK";
}
const tweaks = this.core.replicator.preferredTweakValue;
if (!tweaks) {
return "IGNORE";
}
const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(tweaks);
if (!conf) return "IGNORE";
if (conf === true) {
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
if (rebuildRequired) {
await this.core.rebuilder.$rebuildRemote();
}
Logger($msg("TweakMismatchResolve.Message.remoteUpdated"), LOG_LEVEL_NOTICE);
return "CHECKAGAIN";
}
if (conf) {
this.settings = { ...this.settings, ...conf };
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
await this.services.setting.saveSettingData();
if (rebuildRequired) {
await this.core.rebuilder.$fetchLocal();
}
Logger($msg("TweakMismatchResolve.Message.mineUpdated"), LOG_LEVEL_NOTICE);
return "CHECKAGAIN";
}
return "IGNORE";
}
async _fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise<TweakValues | false> {
const replicator = await this.services.replicator.getNewReplicator(trialSetting);
if (!replicator) {
this._log("The remote type is not supported for fetching preferred tweak values.", LOG_LEVEL_NOTICE);
return false;
}
if (await replicator.tryConnectRemote(trialSetting)) {
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
if (preferred) {
return preferred;
}
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
return false;
}
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
return false;
}
async _checkAndAskUseRemoteConfiguration(
trialSetting: RemoteDBSettings
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
if (trialSetting.remoteType === REMOTE_P2P) {
return { result: false, requireFetch: false };
}
const preferred = await this.services.tweakValue.fetchRemotePreferred(trialSetting);
if (preferred) {
return await this.services.tweakValue.askUseRemoteConfiguration(trialSetting, preferred);
}
return { result: false, requireFetch: false };
}
async _askUseRemoteConfiguration(
trialSetting: RemoteDBSettings,
preferred: TweakValues
): Promise<{ result: false | TweakValues; requireFetch: boolean }> {
const localTweaks = extractObject(TweakValuesTemplate, this.settings) as TweakValues;
const mismatchedKeys = this._collectMismatchedTweakKeys(localTweaks, preferred);
const autoAcceptSide = await this._shouldAutoAcceptCompatibleLossy(localTweaks, preferred, mismatchedKeys);
if (autoAcceptSide === "REMOTE") {
return { result: { ...trialSetting, ...preferred }, requireFetch: false };
}
if (autoAcceptSide === "CURRENT") {
return { result: false, requireFetch: false };
}
const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false;
let rebuildRecommended = false;
// Making tables:
// let table = `| Value name | This device | On Remote | \n` + `|: --- |: ---- :|: ---- :| \n`;
let differenceCount = 0;
const tableRows = [] as string[];
// const items = [mine,preferred]
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const remoteValueForDisplay = escapeMarkdownValue(valueToString(preferred[key]));
const currentValueForDisplay = escapeMarkdownValue(valueToString((trialSetting as TweakValues)?.[key]));
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
if (IncompatibleChanges.indexOf(key) !== -1) {
rebuildRequired = true;
}
for (const pattern of IncompatibleChangesInSpecificPattern) {
if (pattern.key !== key) continue;
// if from value supplied, check if current value have been violated : in other words, if the current value is the same as the from value, it should require a rebuild.
const isFromConditionMet =
"from" in pattern ? pattern.from === (trialSetting as TweakValues)?.[key] : false;
// and, if to value supplied, same as above.
const isToConditionMet = "to" in pattern ? pattern.to === preferred[key] : false;
// if either of them is true, it should require a rebuild, if the pattern is not a recommendation.
if (isFromConditionMet || isToConditionMet) {
if (pattern.isRecommendation) {
rebuildRecommended = true;
} else {
rebuildRequired = true;
}
}
}
if (CompatibleButLossyChanges.indexOf(key) !== -1) {
rebuildRecommended = true;
}
} else {
continue;
}
tableRows.push(
$msg("TweakMismatchResolve.Table.Row", {
name: confName(key),
self: currentValueForDisplay,
remote: remoteValueForDisplay,
})
);
differenceCount++;
}
if (differenceCount === 0) {
this._log("The settings in the remote database are the same as the local database.", LOG_LEVEL_NOTICE);
return { result: false, requireFetch: false };
}
const additionalMessage =
rebuildRequired && this.core.settings.isConfigured
? $msg("TweakMismatchResolve.Message.UseRemote.WarningRebuildRequired")
: "";
const additionalMessage2 =
rebuildRecommended && this.core.settings.isConfigured
? $msg("TweakMismatchResolve.Message.UseRemote.WarningRebuildRecommended")
: "";
const table = $msg("TweakMismatchResolve.Table", { rows: tableRows.join("\n") });
const message = $msg("TweakMismatchResolve.Message.Main", {
table: table,
additionalMessage: [additionalMessage, additionalMessage2].filter((v) => v).join("\n"),
});
const CHOICE_USE_REMOTE = $msg("TweakMismatchResolve.Action.UseConfigured");
const CHOICE_DISMISS = $msg("TweakMismatchResolve.Action.Dismiss");
// const CHOICE_AND_VALUES = [
// [CHOICE_USE_REMOTE, preferred],
// [CHOICE_DISMISS, false]]
const CHOICES = [CHOICE_USE_REMOTE, CHOICE_DISMISS];
const retKey = await this.core.confirm.askSelectStringDialogue(message, CHOICES, {
title: $msg("TweakMismatchResolve.Title.UseRemoteConfig"),
timeout: 0,
defaultAction: CHOICE_DISMISS,
});
if (!retKey) return { result: false, requireFetch: false };
if (retKey === CHOICE_DISMISS) return { result: false, requireFetch: false };
if (retKey === CHOICE_USE_REMOTE) {
return { result: { ...trialSetting, ...preferred }, requireFetch: rebuildRequired };
}
return { result: false, requireFetch: false };
}
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.setting.onBeforeSaveSettingData.addHandler(this._onBeforeSaveSettingData.bind(this));
services.tweakValue.fetchRemotePreferred.setHandler(this._fetchRemotePreferredTweakValues.bind(this));
services.tweakValue.checkAndAskResolvingMismatched.setHandler(
this._checkAndAskResolvingMismatchedTweaks.bind(this)
);
services.tweakValue.askResolvingMismatched.setHandler(this._askResolvingMismatchedTweaks.bind(this));
services.tweakValue.checkAndAskUseRemoteConfiguration.setHandler(
this._checkAndAskUseRemoteConfiguration.bind(this)
);
services.tweakValue.askUseRemoteConfiguration.setHandler(this._askUseRemoteConfiguration.bind(this));
services.replication.checkConnectionFailure.addHandler(this._anyAfterConnectCheckFailed.bind(this));
}
}

View File

@@ -1,108 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, type RemoteDBSettings, type TweakValues } from "@lib/common/types";
import { ModuleResolvingMismatchedTweaks } from "./ModuleResolveMismatchedTweaks";
function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
const askSelectStringDialogue = vi.fn(async () => undefined);
const core = {
_services: {
API: {
addLog: vi.fn(),
addCommand: vi.fn(),
registerWindow: vi.fn(),
addRibbonIcon: vi.fn(),
registerProtocolHandler: vi.fn(),
},
setting: {
saveSettingData: vi.fn(async () => undefined),
},
},
settings: {
...DEFAULT_SETTINGS,
remoteType: REMOTE_COUCHDB,
...settingsOverride,
},
confirm: {
askSelectStringDialogue,
},
} as any;
Object.defineProperty(core, "services", {
get() {
return core._services;
},
});
const module = new ModuleResolvingMismatchedTweaks(core);
return { module, core, askSelectStringDialogue };
}
describe("ModuleResolvingMismatchedTweaks", () => {
it("should auto-accept compatible mismatches on connect check using newer remote tweakModified", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
hashAlg: "xxhash64",
tweakModified: 100,
});
const preferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
hashAlg: "xxhash32",
tweakModified: 200,
} as Partial<TweakValues>;
const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred);
expect(conf).toEqual(preferred);
expect(rebuild).toBe(false);
expect(askSelectStringDialogue).not.toHaveBeenCalled();
});
it("should fallback to manual confirmation when mismatches are mixed on connect check", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
hashAlg: "xxhash64",
encrypt: false,
tweakModified: 100,
});
const preferred = {
...(DEFAULT_SETTINGS as unknown as TweakValues),
hashAlg: "xxhash32",
encrypt: true,
tweakModified: 200,
} as Partial<TweakValues>;
const [conf, rebuild] = await module._checkAndAskResolvingMismatchedTweaks(preferred);
expect(conf).toBe(false);
expect(rebuild).toBe(false);
expect(askSelectStringDialogue).toHaveBeenCalledTimes(1);
});
it("should auto-accept compatible mismatches on remote-config check using newer local tweakModified", async () => {
const { module, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: true,
hashAlg: "xxhash64",
tweakModified: 300,
});
const trialSetting = {
...DEFAULT_SETTINGS,
remoteType: REMOTE_COUCHDB,
hashAlg: "xxhash64",
tweakModified: 300,
} as RemoteDBSettings;
const preferred = {
...(trialSetting as unknown as TweakValues),
hashAlg: "xxhash32",
tweakModified: 200,
} as TweakValues;
const result = await module._askUseRemoteConfiguration(trialSetting, preferred);
expect(result).toEqual({ result: false, requireFetch: false });
expect(askSelectStringDialogue).not.toHaveBeenCalled();
});
});

View File

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

View File

@@ -1,293 +0,0 @@
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "@/common/events.js";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { scheduleTask } from "octagonal-wheels/concurrency/task";
import type { TFile } from "@/deps.ts";
import { fireAndForget } from "octagonal-wheels/promises";
import { type FilePathWithPrefix } from "@lib/common/types.ts";
import { reactive, reactiveSource, type ReactiveSource } from "octagonal-wheels/dataobject/reactive";
import {
collectingChunks,
pluginScanningCount,
hiddenFilesEventCount,
hiddenFilesProcessingCount,
} from "@lib/mock_and_interop/stores.ts";
import type { LiveSyncCore } from "@/main.ts";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
export class ModuleObsidianEvents extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean> {
// this.registerEvent(this.app.workspace.on("editor-change", ));
this.plugin.registerEvent(
this.app.vault.on("rename", (file, oldPath) => {
eventHub.emitEvent(EVENT_FILE_RENAMED, {
newPath: file.path as FilePathWithPrefix,
old: oldPath as FilePathWithPrefix,
});
})
);
this.plugin.registerEvent(
this.app.workspace.on("active-leaf-change", () => eventHub.emitEvent(EVENT_LEAF_ACTIVE_CHANGED))
);
return Promise.resolve(true);
}
__performAppReload() {
this.services.appLifecycle.performRestart();
}
initialCallback: (() => void) | undefined = undefined;
swapSaveCommand() {
this._log("Modifying callback of the save command", LOG_LEVEL_VERBOSE);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Editor Tweaking
const saveCommandDefinition = (this.app as any).commands?.commands?.["editor:save-file"];
const save = saveCommandDefinition?.callback;
if (typeof save === "function") {
this.initialCallback = save;
saveCommandDefinition.callback = () => {
scheduleTask("syncOnEditorSave", 250, () => {
if (this.services.control.hasUnloaded()) {
this._log("Unload and remove the handler.", LOG_LEVEL_VERBOSE);
saveCommandDefinition.callback = this.initialCallback;
this.initialCallback = undefined;
} else {
if (this.settings.syncOnEditorSave) {
this._log("Sync on Editor Save.", LOG_LEVEL_VERBOSE);
fireAndForget(() => this.services.replication.replicateByEvent());
}
}
});
save();
};
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
//@ts-ignore
if (!compatGlobal.CodeMirrorAdapter) {
this._log("CodeMirrorAdapter is not available");
return;
}
//@ts-ignore
compatGlobal.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
void _this.app.commands.executeCommandById("editor:save-file");
// _this.app.performCommand('editor:save-file');
};
}
registerWatchEvents() {
this.setHasFocus = this.setHasFocus.bind(this);
this.watchWindowVisibility = this.watchWindowVisibility.bind(this);
this.watchWorkspaceOpen = this.watchWorkspaceOpen.bind(this);
this.watchOnline = this.watchOnline.bind(this);
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerDomEvent(activeDocument, "visibilitychange", this.watchWindowVisibility);
this.plugin.registerDomEvent(compatGlobal, "focus", () => this.setHasFocus(true));
this.plugin.registerDomEvent(compatGlobal, "blur", () => this.setHasFocus(false));
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerDomEvent(compatGlobal, "online", this.watchOnline);
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerDomEvent(compatGlobal, "offline", this.watchOnline);
}
hasFocus = true;
isLastHidden = false;
setHasFocus(hasFocus: boolean) {
this.hasFocus = hasFocus;
this.watchWindowVisibility();
}
watchWindowVisibility() {
scheduleTask("watch-window-visibility", 100, () => fireAndForget(() => this.watchWindowVisibilityAsync()));
}
watchOnline() {
scheduleTask("watch-online", 500, () => fireAndForget(() => this.watchOnlineAsync()));
}
async watchOnlineAsync() {
// If some files were failed to retrieve, scan files again.
// TODO:FIXME AT V0.17.31, this logic has been disabled.
if (compatGlobal.navigator.onLine && this.localDatabase.needScanning) {
this.localDatabase.needScanning = false;
await this.services.vault.scanVault();
}
}
async watchWindowVisibilityAsync() {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.services.appLifecycle.isReady()) return;
if (this.isLastHidden && !this.hasFocus) {
// NO OP while non-focused after made hidden;
return;
}
const isHidden = activeWindow.document.hidden;
if (this.isLastHidden === isHidden) {
return;
}
this.isLastHidden = isHidden;
await this.services.fileProcessing.commitPendingFileEvents();
// Desktop opt-in (LiveSync/Periodic only): keep the background channel running while the
// window is hidden, instead of suspending on hide. On hide we skip the suspend for both
// modes (LiveSync's continuous replication and Periodic's timer both stall otherwise);
// becoming visible reopens normally, and for LiveSync additionally forces a teardown first
// (see the resume branch) so a stalled continuous channel is always replaced.
const keepActiveInBackground =
this.settings.keepReplicationActiveInBackground &&
(this.settings.liveSync || this.settings.periodicReplication) &&
!this.services.API.isMobile();
if (isHidden) {
if (!keepActiveInBackground) await this.services.appLifecycle.onSuspending();
} else {
// suspend all temporary.
if (this.services.appLifecycle.isSuspended()) return;
// Only the continuous (LiveSync) channel can go stalled-but-not-terminated: PouchDB
// emits paused/retry while the replicator keeps its AbortController set, so the reopen
// below would no-op on exactly the channel that needs replacing. Force a teardown first
// so becoming visible always re-establishes a fresh channel (restoring the default's
// reset-on-visibility). Periodic mode has no such channel — its timer just resumes via
// the normal path below — so this teardown is gated on liveSync to avoid needlessly
// bouncing it. The teardown's closeReplication() aborts synchronously while the reopen is
// deferred (fireAndForget + awaited isReplicationReady/initializeDatabaseForReplication),
// so the aborted continuousReplication run (and its shareRunningResult lock) unwinds in
// microtasks before the reopen runs: it neither double-opens nor gets swallowed by the
// still-registered shared run.
if (keepActiveInBackground && this.settings.liveSync) {
await this.services.appLifecycle.onSuspending();
}
// Resume is not gated on focus in this branch, but note the top-of-handler check
// (isLastHidden && !hasFocus) still defers the whole handler when the window becomes
// visible again while unfocused; in that case recovery happens on the next focus.
await this.services.appLifecycle.onResuming();
await this.services.appLifecycle.onResumed();
}
}
watchWorkspaceOpen(file: TFile | null) {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.services.appLifecycle.isReady()) return;
if (!file) return;
scheduleTask("watch-workspace-open", 500, () => fireAndForget(() => this.watchWorkspaceOpenAsync(file)));
}
async watchWorkspaceOpenAsync(file: TFile) {
if (this.settings.suspendFileWatching) return;
if (!this.settings.isConfigured) return;
if (!this.services.appLifecycle.isReady()) return;
await this.services.fileProcessing.commitPendingFileEvents();
if (file == null) {
return;
}
if (this.settings.syncOnFileOpen && !this.services.appLifecycle.isSuspended()) {
await this.services.replication.replicateByEvent();
}
await this.services.conflict.queueCheckForIfOpen(file.path as FilePathWithPrefix);
}
_everyOnLayoutReady(): Promise<boolean> {
this.swapSaveCommand();
this.registerWatchEvents();
return Promise.resolve(true);
}
private _askReload(message?: string) {
if (this.services.appLifecycle.isReloadingScheduled()) {
this._log(`Reloading is already scheduled`, LOG_LEVEL_VERBOSE);
return;
}
scheduleTask("configReload", 250, async () => {
const RESTART_NOW = "Yes, restart immediately";
const RESTART_AFTER_STABLE = "Yes, schedule a restart after stabilisation";
const RETRY_LATER = "No, Leave it to me";
const ret = await this.core.confirm.askSelectStringDialogue(
message || "Do you want to restart and reload Obsidian now?",
[RESTART_AFTER_STABLE, RESTART_NOW, RETRY_LATER],
{ defaultAction: RETRY_LATER }
);
if (ret == RESTART_NOW) {
this.__performAppReload();
} else if (ret == RESTART_AFTER_STABLE) {
this.services.appLifecycle.scheduleRestart();
}
});
}
// Process counting for app reload scheduling
_totalProcessingCount?: ReactiveSource<number> = undefined;
private _scheduleAppReload() {
if (!this._totalProcessingCount) {
const __tick = reactiveSource(0);
this._totalProcessingCount = reactive(() => {
const dbCount = this.services.replication.databaseQueueCount.value;
const replicationCount = this.services.replication.replicationResultCount.value;
const storageApplyingCount = this.services.replication.storageApplyingCount.value;
const chunkCount = collectingChunks.value;
const pluginScanCount = pluginScanningCount.value;
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
const conflictProcessCount = this.services.conflict.conflictProcessQueueCount.value;
// Now no longer `pendingFileEventCount` and `processingFileEventCount` is used
// const e = this.core.pendingFileEventCount.value;
// const proc = this.core.processingFileEventCount.value;
const e = 0;
const proc = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const __ = __tick.value;
return (
dbCount +
replicationCount +
storageApplyingCount +
chunkCount +
pluginScanCount +
hiddenFilesCount +
conflictProcessCount +
e +
proc
);
});
this.plugin.registerInterval(
compatGlobal.setInterval(() => {
__tick.value++;
}, 1000)
);
let stableCheck = 3;
this._totalProcessingCount.onChanged((e) => {
if (e.value == 0) {
if (stableCheck-- <= 0) {
this.__performAppReload();
}
this._log(
`Obsidian will be restarted soon! (Within ${stableCheck} seconds)`,
LOG_LEVEL_NOTICE,
"restart-notice"
);
} else {
stableCheck = 3;
}
});
}
}
_isReloadingScheduled(): boolean {
return this._totalProcessingCount !== undefined;
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.askRestart.setHandler(this._askReload.bind(this));
services.appLifecycle.scheduleRestart.setHandler(this._scheduleAppReload.bind(this));
services.appLifecycle.isReloadingScheduled.setHandler(this._isReloadingScheduled.bind(this));
}
}

View File

@@ -1,166 +0,0 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { ModuleObsidianEvents } from "./ModuleObsidianEvents";
import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@lib/common/types";
type SetupOptions = {
settings?: Partial<typeof DEFAULT_SETTINGS>;
hidden: boolean;
isLastHidden?: boolean;
hasFocus?: boolean;
isSuspended?: boolean;
// Platform is read via services.API.isMobile(); default desktop (false) so the feature applies.
isMobile?: boolean;
};
function setup(opts: SetupOptions) {
const appLifecycle = {
isReady: vi.fn(() => true),
isSuspended: vi.fn(() => opts.isSuspended ?? false),
onSuspending: vi.fn(async () => true),
onResuming: vi.fn(async () => true),
onResumed: vi.fn(async () => true),
};
const fileProcessing = { commitPendingFileEvents: vi.fn(async () => true) };
const core = {
_services: {
API: {
addLog: vi.fn(),
addCommand: vi.fn(),
registerWindow: vi.fn(),
addRibbonIcon: vi.fn(),
registerProtocolHandler: vi.fn(),
isMobile: vi.fn(() => opts.isMobile ?? false),
},
setting: { saveSettingData: vi.fn(async () => undefined) },
appLifecycle,
fileProcessing,
},
settings: {
...DEFAULT_SETTINGS,
remoteType: REMOTE_COUCHDB,
isConfigured: true,
...opts.settings,
},
} as any;
Object.defineProperty(core, "services", { get: () => core._services });
const module = new ModuleObsidianEvents({} as any, core);
module.isLastHidden = opts.isLastHidden ?? false;
module.hasFocus = opts.hasFocus ?? true;
// The handler reads `activeWindow.document.hidden`.
(globalThis as any).activeWindow = { document: { hidden: opts.hidden } };
return { module, appLifecycle, fileProcessing };
}
describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", () => {
afterEach(() => {
// The handler reads a global `activeWindow`; clear it so it doesn't leak into sibling spec
// files running in the same worker.
delete (globalThis as any).activeWindow;
});
it("does NOT suspend on hide when enabled in LiveSync mode on the desktop app", async () => {
const { module, appLifecycle } = setup({
settings: { keepReplicationActiveInBackground: true, liveSync: true },
hidden: true,
});
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).not.toHaveBeenCalled();
});
it("suspends on hide by default (setting off)", async () => {
const { module, appLifecycle } = setup({
settings: { keepReplicationActiveInBackground: false, liveSync: true },
hidden: true,
});
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1);
});
it("forces onSuspending before the resume on becoming visible when enabled (LiveSync teardown)", async () => {
const { module, appLifecycle } = setup({
settings: { keepReplicationActiveInBackground: true, liveSync: true },
hidden: false,
isLastHidden: true, // hidden -> visible transition
});
await module.watchWindowVisibilityAsync();
// Decision-logic only: on visible + enabled + LiveSync the handler calls onSuspending (the
// forced teardown) before onResuming. The actual stalled-channel replacement is exercised by
// the manual integration test, not here.
expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1);
expect(appLifecycle.onResuming).toHaveBeenCalledTimes(1);
expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1);
expect(appLifecycle.onSuspending.mock.invocationCallOrder[0]).toBeLessThan(
appLifecycle.onResuming.mock.invocationCallOrder[0]
);
});
it("does not force a teardown on becoming visible by default (setting off)", async () => {
const { module, appLifecycle } = setup({
settings: { keepReplicationActiveInBackground: false, liveSync: true },
hidden: false,
isLastHidden: true,
});
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).not.toHaveBeenCalled();
expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1);
});
it("does not apply in On-Events mode even if the flag is set (no scope leak)", async () => {
const { module, appLifecycle } = setup({
settings: {
keepReplicationActiveInBackground: true,
liveSync: false,
periodicReplication: false,
},
hidden: true,
});
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1);
});
it("does NOT suspend on hide when enabled in Periodic mode (the periodic timer also stalls otherwise)", async () => {
const { module, appLifecycle } = setup({
settings: {
keepReplicationActiveInBackground: true,
liveSync: false,
periodicReplication: true,
},
hidden: true,
});
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).not.toHaveBeenCalled();
});
it("does NOT force a teardown on becoming visible in Periodic mode (only the continuous channel can stall)", async () => {
const { module, appLifecycle } = setup({
settings: {
keepReplicationActiveInBackground: true,
liveSync: false,
periodicReplication: true,
},
hidden: false,
isLastHidden: true,
});
await module.watchWindowVisibilityAsync();
// The teardown is gated on liveSync: a periodic timer doesn't go half-open, so bouncing it
// on every restore would be needless churn. Resume still runs normally.
expect(appLifecycle.onSuspending).not.toHaveBeenCalled();
expect(appLifecycle.onResuming).toHaveBeenCalledTimes(1);
expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1);
});
it("does not apply on mobile even if the flag is set", async () => {
const { module, appLifecycle } = setup({
settings: { keepReplicationActiveInBackground: true, liveSync: true },
hidden: true,
isMobile: true,
});
await module.watchWindowVisibilityAsync();
expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,41 +0,0 @@
import { type Editor, type MarkdownFileInfo, type MarkdownView } from "@/deps.ts";
import { addIcon } from "@/deps.ts";
import { type FilePathWithPrefix } from "@lib/common/types.ts";
import { $msg } from "@lib/common/i18n.ts";
import type { LiveSyncCore } from "@/main.ts";
import { AbstractModule } from "@/modules/AbstractModule.ts";
// Obsidian specific menu commands.
export class ModuleObsidianMenu extends AbstractModule {
_everyOnloadStart(): Promise<boolean> {
// UI
addIcon(
"replicate",
`<g transform="matrix(1.15 0 0 1.15 -8.31 -9.52)" fill="currentColor" fill-rule="evenodd">
<path d="m85 22.2c-0.799-4.74-4.99-8.37-9.88-8.37-0.499 0-1.1 0.101-1.6 0.101-2.4-3.03-6.09-4.94-10.3-4.94-6.09 0-11.2 4.14-12.8 9.79-5.59 1.11-9.78 6.05-9.78 12 0 6.76 5.39 12.2 12 12.2h29.9c5.79 0 10.1-4.74 10.1-10.6 0-4.84-3.29-8.88-7.68-10.2zm-2.99 14.7h-29.5c-2.3-0.202-4.29-1.51-5.29-3.53-0.899-2.12-0.699-4.54 0.698-6.46 1.2-1.61 2.99-2.52 4.89-2.52 0.299 0 0.698 0 0.998 0.101l1.8 0.303v-2.02c0-3.63 2.4-6.76 5.89-7.57 0.599-0.101 1.2-0.202 1.8-0.202 2.89 0 5.49 1.62 6.79 4.24l0.598 1.21 1.3-0.504c0.599-0.202 1.3-0.303 2-0.303 1.3 0 2.5 0.404 3.59 1.11 1.6 1.21 2.6 3.13 2.6 5.15v1.61h2c2.6 0 4.69 2.12 4.69 4.74-0.099 2.52-2.2 4.64-4.79 4.64z"/>
<path d="m53.2 49.2h-41.6c-1.8 0-3.2 1.4-3.2 3.2v28.6c0 1.8 1.4 3.2 3.2 3.2h15.8v4h-7v6h24v-6h-7v-4h15.8c1.8 0 3.2-1.4 3.2-3.2v-28.6c0-1.8-1.4-3.2-3.2-3.2zm-2.8 29h-36v-23h36z"/>
<path d="m73 49.2c1.02 1.29 1.53 2.97 1.53 4.56 0 2.97-1.74 5.65-4.39 7.04v-4.06l-7.46 7.33 7.46 7.14v-4.06c7.66-1.98 12.2-9.61 10-17-0.102-0.297-0.205-0.595-0.307-0.892z"/>
<path d="m24.1 43c-0.817-0.991-1.53-2.97-1.53-4.56 0-2.97 1.74-5.65 4.39-7.04v4.06l7.46-7.33-7.46-7.14v4.06c-7.66 1.98-12.2 9.61-10 17 0.102 0.297 0.205 0.595 0.307 0.892z"/>
</g>`
);
this.addRibbonIcon("replicate", $msg("moduleObsidianMenu.replicate"), async () => {
await this.services.replication.replicate(true);
}).addClass("livesync-ribbon-replicate");
this.addCommand({
id: "livesync-checkdoc-conflicted",
name: "Resolve if conflicted.",
editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
const file = view.file;
if (!file) return;
void this.services.conflict.queueCheckForIfOpen(file.path as FilePathWithPrefix);
},
});
return Promise.resolve(true);
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -1,116 +1 @@
import { delay } from "octagonal-wheels/promises";
import { __onMissingTranslation } from "@lib/common/i18n";
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
// import { enableTestFunction } from "./devUtil/testUtils.ts";
import { TestPaneView, VIEW_TYPE_TEST } from "./devUtil/TestPaneView.ts";
import { writable } from "svelte/store";
import type { FilePathWithPrefix } from "@lib/common/types.ts";
import type { LiveSyncCore } from "@/main.ts";
import type { WorkspaceLeaf } from "@/deps.ts";
export class ModuleDev extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean> {
__onMissingTranslation(() => {});
return Promise.resolve(true);
}
async onMissingTranslation(key: string): Promise<void> {
const now = new Date();
const filename = `missing-translation-`;
const time = now.toISOString().split("T")[0];
const outFile = `${filename}${time}.jsonl`;
const piece = JSON.stringify({
[key]: {},
});
const writePiece = piece.substring(1, piece.length - 1) + ",";
try {
await this.core.storageAccess.ensureDir(this.app.vault.configDir + "/ls-debug/");
await this.core.storageAccess.appendHiddenFile(
this.app.vault.configDir + "/ls-debug/" + outFile,
writePiece + "\n"
);
} catch (ex) {
this._log(`Could not write ${outFile}`, LOG_LEVEL_VERBOSE);
this._log(`Missing translation: ${writePiece}`, LOG_LEVEL_VERBOSE);
this._log(ex, LOG_LEVEL_VERBOSE);
}
}
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
this.registerView(VIEW_TYPE_TEST, (leaf: WorkspaceLeaf) => new TestPaneView(leaf, this.plugin, this));
this.addCommand({
id: "view-test",
name: "Open Test dialogue",
callback: () => {
void this.services.API.showWindow(VIEW_TYPE_TEST);
},
});
return Promise.resolve(true);
}
async _everyOnLayoutReady(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
// if (await this.core.storageAccess.isExistsIncludeHidden("_SHOWDIALOGAUTO.md")) {
// void this.core.$$showView(VIEW_TYPE_TEST);
// }
this.addCommand({
id: "test-create-conflict",
name: "Create conflict",
callback: async () => {
const filename = "test-create-conflict.md";
const content = `# Test create conflict\n\n`;
const w = await this.core.databaseFileAccess.store({
name: filename,
path: filename as FilePathWithPrefix,
body: new Blob([content], { type: "text/markdown" }),
stat: {
ctime: new Date().getTime(),
mtime: new Date().getTime(),
size: content.length,
type: "file",
},
});
if (w) {
const id = await this.services.path.path2id(filename as FilePathWithPrefix);
const f = await this.core.localDatabase.getRaw(id);
console.log(f);
console.log(f._rev);
const revConflict = f._rev.split("-")[0] + "-" + (parseInt(f._rev.split("-")[1]) + 1).toString();
console.log(await this.core.localDatabase.bulkDocsRaw([f], { new_edits: false }));
console.log(
await this.core.localDatabase.bulkDocsRaw([{ ...f, _rev: revConflict }], { new_edits: false })
);
}
},
});
await delay(1);
return true;
}
testResults = writable<[boolean, string, string][]>([]);
// testResults: string[] = [];
private _addTestResult(name: string, key: string, result: boolean, summary?: string, message?: string): void {
const logLine = `${name}: ${key} ${summary ?? ""}`;
this.testResults.update((results) => {
results.push([result, logLine, message ?? ""]);
return results;
});
}
private _everyModuleTest(): Promise<boolean> {
if (!this.settings.enableDebugTools) return Promise.resolve(true);
// this.core.$$addTestResult("DevModule", "Test", true);
// return Promise.resolve(true);
// this.addTestResult("Test of test1", true, "Just OK", "This is a test of test");
// this.addTestResult("Test of test2", true, "Just OK?");
// this.addTestResult("Test of test3", true);
return this.testDone();
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLayoutReady.addHandler(this._everyOnLayoutReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.test.test.addHandler(this._everyModuleTest.bind(this));
services.test.addTestResult.setHandler(this._addTestResult.bind(this));
}
}
export type { ModuleDev } from "../../serviceFeatures/devFeature/types.ts";

View File

@@ -1,26 +0,0 @@
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import { VIEW_TYPE_GLOBAL_HISTORY, GlobalHistoryView } from "./GlobalHistory/GlobalHistoryView.ts";
import type { WorkspaceLeaf } from "@/deps.ts";
export class ModuleObsidianGlobalHistory extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-global-history",
name: "Show vault history",
callback: () => {
this.showGlobalHistory();
},
});
this.registerView(VIEW_TYPE_GLOBAL_HISTORY, (leaf: WorkspaceLeaf) => new GlobalHistoryView(leaf, this.plugin));
return Promise.resolve(true);
}
showGlobalHistory() {
void this.services.API.showWindow(VIEW_TYPE_GLOBAL_HISTORY);
}
override onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -1,56 +0,0 @@
import { type TFile } from "@/deps.ts";
import { eventHub } from "@/common/events.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts";
import type { FilePathWithPrefix, LoadedEntry, DocumentID } from "@lib/common/types.ts";
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
import { DocumentHistoryModal } from "./DocumentHistory/DocumentHistoryModal.ts";
import { fireAndForget } from "octagonal-wheels/promises";
export class ModuleObsidianDocumentHistory extends AbstractObsidianModule {
_everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-history",
name: "Show history",
callback: () => {
const file = this.services.vault.getActiveFilePath();
if (file) this.showHistory(file, undefined);
},
});
this.addCommand({
id: "livesync-filehistory",
name: "Pick a file to show history",
callback: () => {
fireAndForget(async () => await this.fileHistory());
},
});
eventHub.onEvent(
EVENT_REQUEST_SHOW_HISTORY,
({ file, fileOnDB }: { file: TFile | FilePathWithPrefix; fileOnDB: LoadedEntry }) => {
this.showHistory(file, fileOnDB._id);
}
);
return Promise.resolve(true);
}
showHistory(file: TFile | FilePathWithPrefix, id?: DocumentID) {
new DocumentHistoryModal(this.app, this.core, this.plugin, file, id).open();
}
async fileHistory() {
const notes: { id: DocumentID; path: FilePathWithPrefix; dispPath: string; mtime: number }[] = [];
for await (const doc of this.localDatabase.findAllDocs()) {
notes.push({ id: doc._id, path: this.getPath(doc), dispPath: this.getPath(doc), mtime: doc.mtime });
}
notes.sort((a, b) => b.mtime - a.mtime);
const notesList = notes.map((e) => e.dispPath);
const target = await this.core.confirm.askSelectString("File to view History", notesList);
if (target) {
const targetId = notes.find((e) => e.dispPath == target)!;
this.showHistory(targetId.path, targetId.id);
}
}
override onBindFunction(core: typeof this.core, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -1,252 +0,0 @@
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import { isObjectDifferent } from "octagonal-wheels/object";
import { EVENT_SETTING_SAVED, eventHub } from "@/common/events";
import { fireAndForget } from "octagonal-wheels/promises";
import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSettings } from "@lib/common/types";
import { parseYaml, stringifyYaml } from "@/deps";
import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { AbstractModule } from "@/modules/AbstractModule.ts";
import type { ServiceContext } from "@lib/services/base/ServiceBase.ts";
import type { InjectableServiceHub } from "@lib/services/InjectableServices.ts";
import type { LiveSyncCore } from "@/main.ts";
const SETTING_HEADER = "````yaml:livesync-setting\n";
const SETTING_FOOTER = "\n````";
export class ModuleObsidianSettingsAsMarkdown extends AbstractModule {
_everyOnloadStart(): Promise<boolean> {
this.addCommand({
id: "livesync-export-config",
name: "Write setting markdown manually",
checkCallback: (checking) => {
if (checking) {
return this.settings.settingSyncFile != "";
}
fireAndForget(async () => {
await this.services.setting.saveSettingData();
});
},
});
this.addCommand({
id: "livesync-import-config",
name: "Parse setting file",
editorCheckCallback: (checking, editor, ctx) => {
if (checking) {
const doc = editor.getValue();
const ret = this.extractSettingFromWholeText(doc);
return ret.body != "";
}
if (ctx.file) {
const file = ctx.file;
fireAndForget(async () => await this.checkAndApplySettingFromMarkdown(file.path, false));
}
},
});
eventHub.onEvent("event-file-changed", (info: { file: FilePathWithPrefix; automated: boolean }) => {
fireAndForget(() => this.checkAndApplySettingFromMarkdown(info.file, info.automated));
});
eventHub.onEvent(EVENT_SETTING_SAVED, (settings: ObsidianLiveSyncSettings) => {
if (settings.settingSyncFile != "") {
fireAndForget(() => this.saveSettingToMarkdown(settings.settingSyncFile));
}
});
return Promise.resolve(true);
}
extractSettingFromWholeText(data: string): {
preamble: string;
body: string;
postscript: string;
} {
if (data.indexOf(SETTING_HEADER) === -1) {
return {
preamble: data,
body: "",
postscript: "",
};
}
const startMarkerPos = data.indexOf(SETTING_HEADER);
const dataStartPos = startMarkerPos == -1 ? data.length : startMarkerPos;
const endMarkerPos = startMarkerPos == -1 ? data.length : data.indexOf(SETTING_FOOTER, dataStartPos);
const dataEndPos = endMarkerPos == -1 ? data.length : endMarkerPos;
const body = data.substring(dataStartPos + SETTING_HEADER.length, dataEndPos);
const ret = {
preamble: data.substring(0, dataStartPos),
body,
postscript: data.substring(dataEndPos + SETTING_FOOTER.length + 1),
};
return ret;
}
async parseSettingFromMarkdown(filename: string, data?: string) {
const file = await this.core.storageAccess.isExists(filename);
if (!file)
return {
preamble: "",
body: "",
postscript: "",
};
if (data) {
return this.extractSettingFromWholeText(data);
}
const parseData = data ?? (await this.core.storageAccess.readFileText(filename));
return this.extractSettingFromWholeText(parseData);
}
async checkAndApplySettingFromMarkdown(filename: string, automated?: boolean) {
if (automated && !this.settings.notifyAllSettingSyncFile) {
if (!this.settings.settingSyncFile || this.settings.settingSyncFile != filename) {
this._log(
`Setting file (${filename}) does not match the current configuration. skipped.`,
LOG_LEVEL_DEBUG
);
return;
}
}
const { body } = await this.parseSettingFromMarkdown(filename);
let newSetting = {} as Partial<ObsidianLiveSyncSettings>;
try {
newSetting = parseYaml(body);
} catch (ex) {
this._log("Could not parse YAML", LOG_LEVEL_NOTICE);
this._log(ex, LOG_LEVEL_VERBOSE);
return;
}
if ("settingSyncFile" in newSetting && newSetting.settingSyncFile != filename) {
this._log(
"This setting file seems to backed up one. Please fix the filename or settingSyncFile value.",
automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE
);
return;
}
let settingToApply = { ...DEFAULT_SETTINGS } as ObsidianLiveSyncSettings;
settingToApply = { ...settingToApply, ...newSetting };
if (!settingToApply?.writeCredentialsForSettingSync) {
//New setting does not contains credentials.
settingToApply.couchDB_USER = this.settings.couchDB_USER;
settingToApply.couchDB_PASSWORD = this.settings.couchDB_PASSWORD;
settingToApply.passphrase = this.settings.passphrase;
}
const oldSetting = this.generateSettingForMarkdown(
this.settings,
settingToApply.writeCredentialsForSettingSync
);
if (!isObjectDifferent(oldSetting, this.generateSettingForMarkdown(settingToApply))) {
this._log(
"Setting markdown has been detected, but not changed.",
automated ? LOG_LEVEL_INFO : LOG_LEVEL_NOTICE
);
return;
}
const addMsg = this.settings.settingSyncFile != filename ? " (This is not-active file)" : "";
this.core.confirm.askInPopup(
"apply-setting-from-md",
`Setting markdown ${filename}${addMsg} has been detected. Apply this from {HERE}.`,
(anchor) => {
anchor.text = "HERE";
anchor.addEventListener("click", () => {
fireAndForget(async () => {
const APPLY_ONLY = "Apply settings";
const APPLY_AND_RESTART = "Apply settings and restart obsidian";
const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md";
const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md";
const CANCEL = "Cancel";
const result = await this.core.confirm.askSelectStringDialogue(
"Ready for apply the setting.",
[APPLY_AND_RESTART, APPLY_ONLY, APPLY_AND_FETCH, APPLY_AND_REBUILD, CANCEL],
{ defaultAction: APPLY_AND_RESTART }
);
if (
result == APPLY_ONLY ||
result == APPLY_AND_RESTART ||
result == APPLY_AND_REBUILD ||
result == APPLY_AND_FETCH
) {
await this.services.setting.applyExternalSettings(settingToApply, true);
this.services.setting.clearUsedPassphrase();
if (result == APPLY_ONLY) {
this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
return;
}
if (result == APPLY_AND_REBUILD) {
await this.core.rebuilder.scheduleRebuild();
}
if (result == APPLY_AND_FETCH) {
await this.core.rebuilder.scheduleFetch();
}
this.services.appLifecycle.performRestart();
}
});
});
}
);
}
generateSettingForMarkdown(
settings?: ObsidianLiveSyncSettings,
keepCredential?: boolean
): Partial<ObsidianLiveSyncSettings> {
const saveData = { ...(settings ? settings : this.settings) } as Partial<ObsidianLiveSyncSettings>;
delete saveData.encryptedCouchDBConnection;
delete saveData.encryptedPassphrase;
delete saveData.additionalSuffixOfDatabaseName;
if (!saveData.writeCredentialsForSettingSync && !keepCredential) {
delete saveData.couchDB_USER;
delete saveData.couchDB_PASSWORD;
delete saveData.passphrase;
delete saveData.jwtKey;
delete saveData.jwtKid;
delete saveData.jwtSub;
delete saveData.couchDB_CustomHeaders;
delete saveData.bucketCustomHeaders;
}
return saveData;
}
async saveSettingToMarkdown(filename: string) {
const saveData = this.generateSettingForMarkdown();
const file = await this.core.storageAccess.isExists(filename);
if (!file) {
await this.core.storageAccess.ensureDir(filename);
const initialContent = `This file contains Self-hosted LiveSync settings as YAML.
Except for the \`livesync-setting\` code block, we can add a note for free.
If the name of this file matches the value of the "settingSyncFile" setting inside the \`livesync-setting\` block, LiveSync will tell us whenever the settings change. We can decide to accept or decline the remote setting. (In other words, we can back up this file by renaming it to another name).
We can perform a command in this file.
- \`Parse setting file\` : load the setting from the file.
**Note** Please handle it with all of your care if you have configured to write credentials in.
`;
await this.core.storageAccess.writeFileAuto(
filename,
initialContent + SETTING_HEADER + "\n" + SETTING_FOOTER
);
}
// if (!(file instanceof TFile)) {
// this._log(`Markdown Setting: ${filename} already exists as a folder`, LOG_LEVEL_NOTICE);
// return;
// }
const data = await this.core.storageAccess.readFileText(filename);
const { preamble, body, postscript } = this.extractSettingFromWholeText(data);
const newBody = stringifyYaml(saveData);
if (newBody == body) {
this._log("Markdown setting: Nothing had been changed", LOG_LEVEL_VERBOSE);
} else {
await this.core.storageAccess.writeFileAuto(
filename,
preamble + SETTING_HEADER + newBody + SETTING_FOOTER + postscript
);
this._log(`Markdown setting: ${filename} has been updated!`, LOG_LEVEL_VERBOSE);
}
}
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub<ServiceContext>): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -1,36 +0,0 @@
import { ObsidianLiveSyncSettingTab } from "./SettingDialogue/ObsidianLiveSyncSettingTab.ts";
import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts";
// import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "@/common/events.ts";
import type { LiveSyncCore } from "@/main.ts";
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
settingTab!: ObsidianLiveSyncSettingTab;
_everyOnloadStart(): Promise<boolean> {
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
this.plugin.addSettingTab(this.settingTab);
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD, () => {
this.openSetting();
void this.settingTab.enableMinimalSetup();
});
return Promise.resolve(true);
}
openSetting() {
// Undocumented API
//@ts-ignore
this.app.setting.open();
//@ts-ignore
this.app.setting.openTabById("obsidian-livesync");
}
get appId() {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
}
}

View File

@@ -23,7 +23,7 @@ import {
getE2EEConfigSummary,
} from "./settingUtils.ts";
import { SETTING_KEY_P2P_DEVICE_NAME } from "@lib/common/types.ts";
import { SetupManager, UserMode } from "@/modules/features/SetupManager.ts";
import { getSetupManager, UserMode } from "@/modules/features/SetupManager.ts";
import { OnDialogSettingsDefault, type AllSettings } from "./settingConstants.ts";
import { activateRemoteConfiguration } from "@lib/serviceFeatures/remoteConfig.ts";
import { ConnectionStringParser } from "@lib/common/ConnectionString.ts";
@@ -118,7 +118,7 @@ export function paneRemoteConfig(
.addButton((button) =>
button
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const setupManager = getSetupManager();
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onlyE2EEConfiguration(UserMode.Update, originalSettings);
updateE2EESummary();
@@ -129,7 +129,7 @@ export function paneRemoteConfig(
.addButton((button) =>
button
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const setupManager = getSetupManager();
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onConfigureManually(originalSettings, UserMode.Update);
updateE2EESummary();
@@ -200,7 +200,7 @@ export function paneRemoteConfig(
baseSettings: ObsidianLiveSyncSettings,
remoteType?: typeof REMOTE_COUCHDB | typeof REMOTE_MINIO | typeof REMOTE_P2P
): Promise<ObsidianLiveSyncSettings | false> => {
const setupManager = this.core.getModule(SetupManager);
const setupManager = getSetupManager();
const dialogManager = setupManager.dialogManager;
let targetRemoteType = remoteType;
@@ -537,7 +537,7 @@ export function paneRemoteConfig(
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const setupManager = getSetupManager();
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onCouchDBManualSetup(
UserMode.Update,
@@ -573,7 +573,7 @@ export function paneRemoteConfig(
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const setupManager = getSetupManager();
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onBucketManualSetup(
UserMode.Update,
@@ -614,7 +614,7 @@ export function paneRemoteConfig(
.setButtonText("Configure")
.setCta()
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const setupManager = getSetupManager();
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onP2PManualSetup(
UserMode.Update,

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