Compare commits

...

5 Commits

Author SHA1 Message Date
vorotamoroz 3201399bdf bump 2026-02-16 11:51:09 +00:00
vorotamoroz 2ae70e8f07 Refactor: DatabaseService and Replicator 2026-02-16 11:51:03 +00:00
vorotamoroz 2b9bb1ed06 beta bump 2026-02-16 06:51:52 +00:00
vorotamoroz e63e3e6725 ### Refactor
- Module dependency refined. (For details, please refer to updates.md)
2026-02-16 06:50:31 +00:00
vorotamoroz 6e9ac6a9f9 - Application LifeCycle has now started in Main, not ServiceHub. 2026-02-14 15:21:00 +09:00
28 changed files with 380 additions and 506 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.25.43-patched-1",
"version": "0.25.43-patched-4",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.25.43-patched-1",
"version": "0.25.43-patched-4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.25.43-patched-1",
"version": "0.25.43-patched-4",
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.25.43-patched-1",
"version": "0.25.43-patched-4",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",
-2
View File
@@ -21,7 +21,6 @@ export const EVENT_REQUEST_CLOSE_P2P = "request-close-p2p";
export const EVENT_REQUEST_RUN_DOCTOR = "request-run-doctor";
export const EVENT_REQUEST_RUN_FIX_INCOMPLETE = "request-run-fix-incomplete";
export const EVENT_ON_UNRESOLVED_ERROR = "on-unresolved-error";
export const EVENT_ANALYSE_DB_USAGE = "analyse-db-usage";
export const EVENT_REQUEST_PERFORM_GC_V3 = "request-perform-gc-v3";
@@ -44,7 +43,6 @@ declare global {
[EVENT_REQUEST_SHOW_SETUP_QR]: undefined;
[EVENT_REQUEST_RUN_DOCTOR]: string;
[EVENT_REQUEST_RUN_FIX_INCOMPLETE]: undefined;
[EVENT_ON_UNRESOLVED_ERROR]: undefined;
[EVENT_ANALYSE_DB_USAGE]: undefined;
[EVENT_REQUEST_CHECK_REMOTE_SIZE]: undefined;
[EVENT_REQUEST_PERFORM_GC_V3]: undefined;
+4 -8
View File
@@ -1,4 +1,4 @@
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
import { LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import {
LOG_LEVEL_INFO,
LOG_LEVEL_NOTICE,
@@ -12,6 +12,7 @@ import type ObsidianLiveSyncPlugin from "../main.ts";
import { MARK_DONE } from "../modules/features/ModuleLog.ts";
import type { LiveSyncCore } from "../main.ts";
import { __$checkInstanceBinding } from "../lib/src/dev/checks.ts";
import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils.ts";
let noticeIndex = 0;
export abstract class LiveSyncCommands {
@@ -43,6 +44,7 @@ export abstract class LiveSyncCommands {
constructor(plugin: ObsidianLiveSyncPlugin) {
this.plugin = plugin;
this.onBindFunction(plugin, plugin.services);
this._log = createInstanceLogFunction(this.constructor.name, this.services.API);
__$checkInstanceBinding(this);
}
abstract onunload(): void;
@@ -58,13 +60,7 @@ export abstract class LiveSyncCommands {
return this.services.database.isDatabaseReady();
}
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
if (typeof msg === "string" && level !== LOG_LEVEL_NOTICE) {
msg = `[${this.constructor.name}]\u{200A} ${msg}`;
}
// console.log(msg);
Logger(msg, level, key);
};
_log: ReturnType<typeof createInstanceLogFunction>;
_verbose = (msg: any, key?: string) => {
this._log(msg, LOG_LEVEL_VERBOSE, key);
+1 -1
View File
@@ -107,7 +107,7 @@ export class P2PReplicator extends LiveSyncCommands implements P2PReplicatorBase
}
init() {
this._simpleStore = this.services.database.openSimpleStore("p2p-sync");
this._simpleStore = this.services.keyValueDB.openSimpleStore("p2p-sync");
return Promise.resolve(this);
}
+1 -1
Submodule src/lib updated: 532f25f947...75d46c7163
+125 -78
View File
@@ -1,4 +1,4 @@
import { Plugin } from "./deps";
import { Plugin, type App, type PluginManifest } from "./deps";
import {
type EntryDoc,
type ObsidianLiveSyncSettings,
@@ -6,12 +6,8 @@ import {
type HasSettings,
} from "./lib/src/common/types.ts";
import { type SimpleStore } from "./lib/src/common/utils.ts";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
import {
LiveSyncAbstractReplicator,
type LiveSyncReplicatorEnv,
} from "./lib/src/replication/LiveSyncAbstractReplicator.js";
import { type KeyValueDatabase } from "./lib/src/interfaces/KeyValueDatabase.ts";
import { type LiveSyncLocalDBEnv } from "./lib/src/pouchdb/LiveSyncLocalDB.ts";
import { type LiveSyncReplicatorEnv } from "./lib/src/replication/LiveSyncAbstractReplicator.js";
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
@@ -48,29 +44,23 @@ import { ModuleObsidianDocumentHistory } from "./modules/features/ModuleObsidian
import { ModuleObsidianGlobalHistory } from "./modules/features/ModuleGlobalHistory.ts";
import { ModuleObsidianSettingsAsMarkdown } from "./modules/features/ModuleObsidianSettingAsMarkdown.ts";
import { ModuleInitializerFile } from "./modules/essential/ModuleInitializerFile.ts";
import { ModuleKeyValueDB } from "./modules/essential/ModuleKeyValueDB.ts";
import { ModulePouchDB } from "./modules/core/ModulePouchDB.ts";
import { ModuleReplicator } from "./modules/core/ModuleReplicator.ts";
import { ModuleReplicatorCouchDB } from "./modules/core/ModuleReplicatorCouchDB.ts";
import { ModuleReplicatorMinIO } from "./modules/core/ModuleReplicatorMinIO.ts";
import { ModuleTargetFilter } from "./modules/core/ModuleTargetFilter.ts";
import { ModulePeriodicProcess } from "./modules/core/ModulePeriodicProcess.ts";
import { ModuleRemoteGovernor } from "./modules/coreFeatures/ModuleRemoteGovernor.ts";
import { ModuleLocalDatabaseObsidian } from "./modules/core/ModuleLocalDatabaseObsidian.ts";
import { ModuleConflictChecker } from "./modules/coreFeatures/ModuleConflictChecker.ts";
import { ModuleResolvingMismatchedTweaks } from "./modules/coreFeatures/ModuleResolveMismatchedTweaks.ts";
import { ModuleIntegratedTest } from "./modules/extras/ModuleIntegratedTest.ts";
import { ModuleRebuilder } from "./modules/core/ModuleRebuilder.ts";
import { ModuleReplicateTest } from "./modules/extras/ModuleReplicateTest.ts";
import { ModuleLiveSyncMain } from "./modules/main/ModuleLiveSyncMain.ts";
import { ModuleExtraSyncObsidian } from "./modules/extraFeaturesObsidian/ModuleExtraSyncObsidian.ts";
import { LocalDatabaseMaintenance } from "./features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { P2PReplicator } from "./features/P2PSync/CmdP2PReplicator.ts";
import type { LiveSyncManagers } from "./lib/src/managers/LiveSyncManagers.ts";
import type { InjectableServiceHub } from "./lib/src/services/implements/injectable/InjectableServiceHub.ts";
import { ObsidianServiceHub } from "./modules/services/ObsidianServiceHub.ts";
import type { ServiceContext } from "./lib/src/services/base/ServiceBase.ts";
// import type { InjectableServiceHub } from "./lib/src/services/InjectableServices.ts";
export default class ObsidianLiveSyncPlugin
extends Plugin
@@ -84,16 +74,36 @@ export default class ObsidianLiveSyncPlugin
/**
* The service hub for managing all services.
*/
_services: InjectableServiceHub<ServiceContext> = new ObsidianServiceHub(this);
_services: InjectableServiceHub<ServiceContext> | undefined = undefined;
get services() {
if (!this._services) {
throw new Error("Services not initialised yet");
}
return this._services;
}
private initialiseServices() {
this._services = new ObsidianServiceHub(this);
}
// Keep order to display the dialogue in order.
addOns = [] as LiveSyncCommands[];
/**
* Bind functions to the service hub (for migration purpose).
*/
// bindFunctions = (this.serviceHub as ObsidianServiceHub).bindFunctions.bind(this.serviceHub);
// --> Module System
private _registerAddOn(addOn: LiveSyncCommands) {
this.addOns.push(addOn);
}
private registerAddOns() {
this._registerAddOn(new ConfigSync(this));
this._registerAddOn(new HiddenFileSync(this));
this._registerAddOn(new LocalDatabaseMaintenance(this));
this._registerAddOn(new P2PReplicator(this));
}
getAddOn<T extends LiveSyncCommands>(cls: string) {
for (const addon of this.addOns) {
if (addon.constructor.name == cls) return addon as T;
@@ -101,56 +111,8 @@ export default class ObsidianLiveSyncPlugin
return undefined;
}
// Keep order to display the dialogue in order.
addOns = [
new ConfigSync(this),
new HiddenFileSync(this),
new LocalDatabaseMaintenance(this),
new P2PReplicator(this),
] as LiveSyncCommands[];
modules = [
new ModuleLiveSyncMain(this),
new ModuleExtraSyncObsidian(this, this),
// Only on Obsidian
new ModuleDatabaseFileAccess(this),
// Common
new ModulePouchDB(this),
new ModuleConflictChecker(this),
new ModuleLocalDatabaseObsidian(this),
new ModuleReplicatorMinIO(this),
new ModuleReplicatorCouchDB(this),
new ModuleReplicator(this),
new ModuleFileHandler(this),
new ModuleConflictResolver(this),
new ModuleRemoteGovernor(this),
new ModuleTargetFilter(this),
new ModulePeriodicProcess(this),
// Essential Modules
new ModuleKeyValueDB(this),
new ModuleInitializerFile(this),
new ModuleObsidianAPI(this, this),
new ModuleObsidianEvents(this, this),
new ModuleFileAccessObsidian(this, this),
new ModuleObsidianSettings(this),
new ModuleResolvingMismatchedTweaks(this),
new ModuleObsidianSettingsAsMarkdown(this),
new ModuleObsidianSettingDialogue(this, this),
new ModuleLog(this, this),
new ModuleObsidianMenu(this),
new ModuleRebuilder(this),
new ModuleSetupObsidian(this),
new ModuleObsidianDocumentHistory(this, this),
new ModuleMigration(this),
new ModuleRedFlag(this),
new ModuleInteractiveConflictResolver(this, this),
new ModuleObsidianGlobalHistory(this, this),
new ModuleCheckRemoteSize(this),
// Test and Dev Modules
new ModuleDev(this, this),
new ModuleReplicateTest(this, this),
new ModuleIntegratedTest(this, this),
new SetupManager(this),
private modules = [
// Move to registerModules
] as (IObsidianModule | AbstractModule)[];
getModule<T extends IObsidianModule>(constructor: new (...args: any[]) => T): T {
@@ -159,26 +121,99 @@ export default class ObsidianLiveSyncPlugin
}
throw new Error(`Module ${constructor} not found or not loaded.`);
}
getModulesByType<T extends IObsidianModule>(constructor: new (...args: any[]) => T): T[] {
const matchedModules: T[] = [];
for (const module of this.modules) {
if (module instanceof constructor) matchedModules.push(module);
}
return matchedModules;
}
private _registerModule(module: IObsidianModule) {
this.modules.push(module);
}
private registerModules() {
this._registerModule(new ModuleLiveSyncMain(this));
// Only on Obsidian
this._registerModule(new ModuleDatabaseFileAccess(this));
// Common
this._registerModule(new ModuleConflictChecker(this));
this._registerModule(new ModuleReplicatorMinIO(this));
this._registerModule(new ModuleReplicatorCouchDB(this));
this._registerModule(new ModuleReplicator(this));
this._registerModule(new ModuleFileHandler(this));
this._registerModule(new ModuleConflictResolver(this));
this._registerModule(new ModuleRemoteGovernor(this));
this._registerModule(new ModuleTargetFilter(this));
this._registerModule(new ModulePeriodicProcess(this));
// Essential Modules
this._registerModule(new ModuleInitializerFile(this));
this._registerModule(new ModuleObsidianAPI(this, this));
this._registerModule(new ModuleObsidianEvents(this, this));
this._registerModule(new ModuleFileAccessObsidian(this, this));
this._registerModule(new ModuleObsidianSettings(this));
this._registerModule(new ModuleResolvingMismatchedTweaks(this));
this._registerModule(new ModuleObsidianSettingsAsMarkdown(this));
this._registerModule(new ModuleObsidianSettingDialogue(this, this));
this._registerModule(new ModuleLog(this, this));
this._registerModule(new ModuleObsidianMenu(this));
this._registerModule(new ModuleRebuilder(this));
this._registerModule(new ModuleSetupObsidian(this));
this._registerModule(new ModuleObsidianDocumentHistory(this, this));
this._registerModule(new ModuleMigration(this));
this._registerModule(new ModuleRedFlag(this));
this._registerModule(new ModuleInteractiveConflictResolver(this, this));
this._registerModule(new ModuleObsidianGlobalHistory(this, this));
this._registerModule(new ModuleCheckRemoteSize(this));
// Test and Dev Modules
this._registerModule(new ModuleDev(this, this));
this._registerModule(new ModuleReplicateTest(this, this));
this._registerModule(new ModuleIntegratedTest(this, this));
this._registerModule(new SetupManager(this));
}
settings!: ObsidianLiveSyncSettings;
localDatabase!: LiveSyncLocalDB;
managers!: LiveSyncManagers;
simpleStore!: SimpleStore<CheckPointInfo>;
replicator!: LiveSyncAbstractReplicator;
get confirm(): Confirm {
return this.services.UI.confirm;
}
storageAccess!: StorageAccess;
databaseFileAccess!: DatabaseFileAccess;
fileHandler!: ModuleFileHandler;
rebuilder!: Rebuilder;
kvDB!: KeyValueDatabase;
// This property will be changed from outside often, so will be set later.
settings!: ObsidianLiveSyncSettings;
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
}
get localDatabase() {
return this.services.database.localDatabase;
}
get managers() {
return this.services.database.managers;
}
getDatabase(): PouchDB.Database<EntryDoc> {
return this.localDatabase.localDatabase;
}
getSettings(): ObsidianLiveSyncSettings {
return this.settings;
get simpleStore() {
return this.services.keyValueDB.simpleStore as SimpleStore<CheckPointInfo>;
}
get replicator() {
return this.services.replicator.getActiveReplicator()!;
}
// initialised at ModuleFileAccessObsidian
storageAccess!: StorageAccess;
// initialised at ModuleDatabaseFileAccess
databaseFileAccess!: DatabaseFileAccess;
// initialised at ModuleFileHandler
fileHandler!: ModuleFileHandler;
// initialised at ModuleRebuilder
rebuilder!: Rebuilder;
get kvDB() {
return this.services.keyValueDB.kvDB;
}
requestCount = reactiveSource(0);
@@ -205,8 +240,20 @@ export default class ObsidianLiveSyncPlugin
syncStatus: "CLOSED" as DatabaseConnectingStatus,
});
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
this.initialiseServices();
this.registerModules();
this.registerAddOns();
}
private async _startUp() {
await this.services.appLifecycle.onLoad();
const onReady = this.services.appLifecycle.onReady.bind(this.services.appLifecycle);
this.app.workspace.onLayoutReady(onReady);
}
onload() {
void this.services.appLifecycle.onLoad();
void this._startUp();
}
async saveSettings() {
await this.services.setting.saveSettingData();
+9 -12
View File
@@ -1,17 +1,18 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
import type { AnyEntry, FilePathWithPrefix, LOG_LEVEL } from "@lib/common/types";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
import type { AnyEntry, FilePathWithPrefix } from "@lib/common/types";
import type { LiveSyncCore } from "@/main";
import { __$checkInstanceBinding } from "@lib/dev/checks";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import { createInstanceLogFunction } from "@/lib/src/services/lib/logUtils";
export abstract class AbstractModule {
_log = (msg: any, level: LOG_LEVEL = LOG_LEVEL_INFO, key?: string) => {
if (typeof msg === "string" && level !== LOG_LEVEL_NOTICE) {
msg = `[${this.constructor.name}]\u{200A} ${msg}`;
_log = createInstanceLogFunction(this.constructor.name, this.services.API);
get services() {
if (!this.core._services) {
throw new Error("Services are not ready yet.");
}
// console.log(msg);
Logger(msg, level, key);
};
return this.core._services;
}
addCommand = this.services.API.addCommand.bind(this.services.API);
registerView = this.services.API.registerWindow.bind(this.services.API);
@@ -73,10 +74,6 @@ export abstract class AbstractModule {
return this.testDone();
}
get services() {
return this.core._services;
}
isMainReady() {
return this.services.appLifecycle.isReady();
}
@@ -1,46 +0,0 @@
import { $msg } from "../../lib/src/common/i18n";
import { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
import { initializeStores } from "../../common/stores.ts";
import { AbstractModule } from "../AbstractModule.ts";
import { LiveSyncManagers } from "../../lib/src/managers/LiveSyncManagers.ts";
import type { LiveSyncCore } from "../../main.ts";
export class ModuleLocalDatabaseObsidian extends AbstractModule {
_everyOnloadStart(): Promise<boolean> {
return Promise.resolve(true);
}
private async _openDatabase(): Promise<boolean> {
if (this.localDatabase != null) {
await this.localDatabase.close();
}
const vaultName = this.services.vault.getVaultName();
this._log($msg("moduleLocalDatabase.logWaitingForReady"));
const getDB = () => this.core.localDatabase.localDatabase;
const getSettings = () => this.core.settings;
this.core.managers = new LiveSyncManagers({
get database() {
return getDB();
},
getActiveReplicator: () => this.core.replicator,
id2path: this.services.path.id2path.bind(this.services.path),
// path2id: this.core.$$path2id.bind(this.core),
path2id: this.services.path.path2id.bind(this.services.path),
get settings() {
return getSettings();
},
});
this.core.localDatabase = new LiveSyncLocalDB(vaultName, this.core);
initializeStores(vaultName);
return await this.localDatabase.initializeDatabase();
}
_isDatabaseReady(): boolean {
return this.localDatabase != null && this.localDatabase.isReady;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.database.isDatabaseReady.setHandler(this._isDatabaseReady.bind(this));
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.database.openDatabase.setHandler(this._openDatabase.bind(this));
}
}
-23
View File
@@ -1,23 +0,0 @@
import { AbstractModule } from "../AbstractModule";
import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser";
import type { LiveSyncCore } from "../../main";
import { ExtraSuffixIndexedDB } from "../../lib/src/common/types";
export class ModulePouchDB extends AbstractModule {
_createPouchDBInstance<T extends object>(
name?: string,
options?: PouchDB.Configuration.DatabaseConfiguration
): PouchDB.Database<T> {
const optionPass = options ?? {};
if (this.settings.useIndexedDBAdapter) {
optionPass.adapter = "indexeddb";
//@ts-ignore :missing def
optionPass.purged_infos_limit = 1;
return new PouchDB(name + ExtraSuffixIndexedDB, optionPass);
}
return new PouchDB(name, optionPass);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.database.createPouchDBInstance.setHandler(this._createPouchDBInstance.bind(this));
}
}
+7 -4
View File
@@ -133,9 +133,9 @@ Please enable them from the settings screen after setup is complete.`,
await this.core.replicator.tryCreateRemoteDatabase(this.settings);
}
private async _resetLocalDatabase(): Promise<boolean> {
private _onResetLocalDatabase(): Promise<boolean> {
this.core.storageAccess.clearTouched();
return await this.localDatabase.resetDatabase();
return Promise.resolve(true);
}
async suspendAllSync() {
@@ -224,7 +224,10 @@ Are you sure you wish to proceed?`;
await this.services.setting.realiseSetting();
await this.resetLocalDatabase();
await delay(1000);
await this.services.database.openDatabase();
await this.services.database.openDatabase({
databaseEvents: this.services.databaseEvents,
replicator: this.services.replicator,
});
// this.core.isReady = true;
this.services.appLifecycle.markIsReady();
if (makeLocalChunkBeforeSync) {
@@ -302,7 +305,7 @@ Are you sure you wish to proceed?`;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
services.database.resetDatabase.setHandler(this._resetLocalDatabase.bind(this));
services.database.onDatabaseReset.addHandler(this._onResetLocalDatabase.bind(this));
services.remote.tryResetDatabase.setHandler(this._tryResetRemoteDatabase.bind(this));
services.remote.tryCreateDatabase.setHandler(this._tryCreateRemoteDatabase.bind(this));
services.setting.suspendAllSync.addHandler(this._allSuspendAllSync.bind(this));
+19 -59
View File
@@ -1,48 +1,36 @@
import { fireAndForget, yieldMicrotask } from "octagonal-wheels/promises";
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB";
import { fireAndForget } from "octagonal-wheels/promises";
import { AbstractModule } from "../AbstractModule";
import {
Logger,
LOG_LEVEL_NOTICE,
LOG_LEVEL_INFO,
LOG_LEVEL_VERBOSE,
LEVEL_NOTICE,
LEVEL_INFO,
type LOG_LEVEL,
} from "octagonal-wheels/common/logger";
import { Logger, LOG_LEVEL_NOTICE, LOG_LEVEL_INFO, LEVEL_NOTICE, type LOG_LEVEL } from "octagonal-wheels/common/logger";
import { isLockAcquired, shareRunningResult, skipIfDuplicated } from "octagonal-wheels/concurrency/lock";
import { balanceChunkPurgedDBs } from "@/lib/src/pouchdb/chunks";
import { purgeUnreferencedChunks } from "@/lib/src/pouchdb/chunks";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import { type EntryDoc, type RemoteType } from "../../lib/src/common/types";
import { rateLimitedSharedExecution, scheduleTask, updatePreviousExecutionTime } from "../../common/utils";
import { EVENT_FILE_SAVED, EVENT_ON_UNRESOLVED_ERROR, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { EVENT_FILE_SAVED, EVENT_SETTING_SAVED, eventHub } from "../../common/events";
import { $msg } from "../../lib/src/common/i18n";
import { clearHandlers } from "../../lib/src/replication/SyncParamsHandler";
import type { LiveSyncCore } from "../../main";
import { ReplicateResultProcessor } from "./ReplicateResultProcessor";
import { UnresolvedErrorManager } from "@/lib/src/services/base/UnresolvedErrorManager";
import { clearHandlers } from "@/lib/src/replication/SyncParamsHandler";
const KEY_REPLICATION_ON_EVENT = "replicationOnEvent";
const REPLICATION_ON_EVENT_FORECASTED_TIME = 5000;
export class ModuleReplicator extends AbstractModule {
_replicatorType?: RemoteType;
_previousErrors = new Set<string>();
processor: ReplicateResultProcessor = new ReplicateResultProcessor(this);
private _unresolvedErrorManager: UnresolvedErrorManager = new UnresolvedErrorManager(
this.core.services.appLifecycle
);
showError(msg: string, max_log_level: LOG_LEVEL = LEVEL_NOTICE) {
const level = this._previousErrors.has(msg) ? LEVEL_INFO : max_log_level;
this._log(msg, level);
if (!this._previousErrors.has(msg)) {
this._previousErrors.add(msg);
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
}
this._unresolvedErrorManager.showError(msg, max_log_level);
}
clearErrors() {
this._previousErrors.clear();
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
this._unresolvedErrorManager.clearErrors();
}
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
@@ -52,9 +40,10 @@ export class ModuleReplicator extends AbstractModule {
}
});
eventHub.onEvent(EVENT_SETTING_SAVED, (setting) => {
if (this._replicatorType !== setting.remoteType) {
void this.setReplicator();
}
// ReplicatorService responds to `settingService.onRealiseSetting`.
// if (this._replicatorType !== setting.remoteType) {
// void this.setReplicator();
// }
if (this.core.settings.suspendParseReplicationResult) {
this.processor.suspend();
} else {
@@ -65,39 +54,17 @@ export class ModuleReplicator extends AbstractModule {
return Promise.resolve(true);
}
async setReplicator() {
const replicator = await this.services.replicator.getNewReplicator();
if (!replicator) {
this.showError($msg("Replicator.Message.InitialiseFatalError"), LOG_LEVEL_NOTICE);
return false;
}
if (this.core.replicator) {
await this.core.replicator.closeReplication();
this._log("Replicator closed for changing", LOG_LEVEL_VERBOSE);
}
this.core.replicator = replicator;
this._replicatorType = this.settings.remoteType;
await yieldMicrotask();
// Clear any existing sync parameter handlers (means clearing key-deriving salt).
_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 true;
return Promise.resolve(true);
}
_getReplicator(): LiveSyncAbstractReplicator {
return this.core.replicator;
}
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.setReplicator();
}
_everyOnDatabaseInitialized(showNotice: boolean): Promise<boolean> {
fireAndForget(() => this.processor.restoreFromSnapshotOnce());
return Promise.resolve(true);
}
_everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.setReplicator();
}
async ensureReplicatorPBKDF2Salt(showMessage: boolean = false): Promise<boolean> {
// Checking salt
const replicator = this.services.replicator.getActiveReplicator();
@@ -324,15 +291,9 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
return !checkResult;
}
private _reportUnresolvedMessages(): Promise<string[]> {
return Promise.resolve([...this._previousErrors]);
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.replicator.getActiveReplicator.setHandler(this._getReplicator.bind(this));
services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
services.replicator.onReplicatorInitialised.addHandler(this._onReplicatorInitialised.bind(this));
services.databaseEvents.onDatabaseInitialised.addHandler(this._everyOnDatabaseInitialized.bind(this));
services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.replication.parseSynchroniseResult.setHandler(this._parseReplicationResult.bind(this));
services.appLifecycle.onSuspending.addHandler(this._everyBeforeSuspendProcess.bind(this));
@@ -342,6 +303,5 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
services.replication.replicateByEvent.setHandler(this._replicateByEvent.bind(this));
services.remote.replicateAllToRemote.setHandler(this._replicateAllToServer.bind(this));
services.remote.replicateAllFromRemote.setHandler(this._replicateAllFromServer.bind(this));
services.appLifecycle.getUnresolvedMessages.addHandler(this._reportUnresolvedMessages.bind(this));
}
}
-5
View File
@@ -146,10 +146,5 @@ export class ModuleTargetFilter extends AbstractModule {
services.vault.isTargetFile.addHandler(this._isTargetFileByLocalDB.bind(this));
services.vault.isTargetFile.addHandler(this._isTargetFileFinal.bind(this));
services.setting.onSettingRealised.addHandler(this.refreshSettings.bind(this));
// services.vault.isTargetFile.use((ctx, next) => {
// const [fileName, keepFileCheckList] = ctx.args;
// const file = getS
// });
}
}
@@ -78,7 +78,7 @@ export class ModuleInitializerFile extends AbstractModule {
const _filesStorage = [] as typeof filesStorageSrc;
for (const f of filesStorageSrc) {
if (await this.services.vault.isTargetFile(f.path, f != filesStorageSrc[0])) {
if (await this.services.vault.isTargetFile(f.path)) {
_filesStorage.push(f);
}
}
@@ -122,7 +122,7 @@ export class ModuleInitializerFile extends AbstractModule {
);
const path = this.getPath(doc);
if (isValidPath(path) && (await this.services.vault.isTargetFile(path, true))) {
if (isValidPath(path) && (await this.services.vault.isTargetFile(path))) {
if (!isMetaEntry(doc)) {
this._log(`Invalid entry: ${path}`, LOG_LEVEL_INFO);
continue;
@@ -393,7 +393,13 @@ export class ModuleInitializerFile extends AbstractModule {
ignoreSuspending: boolean = false
): Promise<boolean> {
this.services.appLifecycle.resetIsReady();
if (!reopenDatabase || (await this.services.database.openDatabase())) {
if (
!reopenDatabase ||
(await this.services.database.openDatabase({
databaseEvents: this.services.databaseEvents,
replicator: this.services.replicator,
}))
) {
if (this.localDatabase.isReady) {
await this.services.vault.scanVault(showingNotice, ignoreSuspending);
}
-114
View File
@@ -1,114 +0,0 @@
import { delay, yieldMicrotask } from "octagonal-wheels/promises";
import { OpenKeyValueDatabase } from "../../common/KeyValueDB.ts";
import type { LiveSyncLocalDB } from "../../lib/src/pouchdb/LiveSyncLocalDB.ts";
import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { AbstractModule } from "../AbstractModule.ts";
import type { LiveSyncCore } from "../../main.ts";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import type { InjectableServiceHub } from "@/lib/src/services/InjectableServices.ts";
import type { ObsidianDatabaseService } from "../services/ObsidianServices.ts";
export class ModuleKeyValueDB extends AbstractModule {
async tryCloseKvDB() {
try {
await this.core.kvDB?.close();
return true;
} catch (e) {
this._log("Failed to close KeyValueDB", LOG_LEVEL_VERBOSE);
this._log(e);
return false;
}
}
async openKeyValueDB(): Promise<boolean> {
await delay(10);
try {
await this.tryCloseKvDB();
await delay(10);
await yieldMicrotask();
this.core.kvDB = await OpenKeyValueDatabase(this.services.vault.getVaultName() + "-livesync-kv");
await yieldMicrotask();
await delay(100);
} catch (e) {
this.core.kvDB = undefined!;
this._log("Failed to open KeyValueDB", LOG_LEVEL_NOTICE);
this._log(e, LOG_LEVEL_VERBOSE);
return false;
}
return true;
}
async _onDBUnload(db: LiveSyncLocalDB) {
if (this.core.kvDB) await this.core.kvDB.close();
return Promise.resolve(true);
}
async _onDBClose(db: LiveSyncLocalDB) {
if (this.core.kvDB) await this.core.kvDB.close();
return Promise.resolve(true);
}
private async _everyOnloadAfterLoadSettings(): Promise<boolean> {
if (!(await this.openKeyValueDB())) {
return false;
}
this.core.simpleStore = this.services.database.openSimpleStore<any>("os");
return Promise.resolve(true);
}
_getSimpleStore<T>(kind: string) {
const getDB = () => this.core.kvDB;
const prefix = `${kind}-`;
return {
get: async (key: string): Promise<T> => {
return await getDB().get(`${prefix}${key}`);
},
set: async (key: string, value: any): Promise<void> => {
await getDB().set(`${prefix}${key}`, value);
},
delete: async (key: string): Promise<void> => {
await getDB().del(`${prefix}${key}`);
},
keys: async (
from: string | undefined,
to: string | undefined,
count?: number | undefined
): Promise<string[]> => {
const ret = await getDB().keys(
IDBKeyRange.bound(`${prefix}${from || ""}`, `${prefix}${to || ""}`),
count
);
return ret
.map((e) => e.toString())
.filter((e) => e.startsWith(prefix))
.map((e) => e.substring(prefix.length));
},
db: Promise.resolve(getDB()),
} satisfies SimpleStore<T>;
}
_everyOnInitializeDatabase(db: LiveSyncLocalDB): Promise<boolean> {
return this.openKeyValueDB();
}
async _everyOnResetDatabase(db: LiveSyncLocalDB): Promise<boolean> {
try {
const kvDBKey = "queued-files";
await this.core.kvDB.del(kvDBKey);
// localStorage.removeItem(lsKey);
await this.core.kvDB.destroy();
await yieldMicrotask();
this.core.kvDB = await OpenKeyValueDatabase(this.services.vault.getVaultName() + "-livesync-kv");
await delay(100);
} catch (e) {
this.core.kvDB = undefined!;
this._log("Failed to reset KeyValueDB", LOG_LEVEL_NOTICE);
this._log(e, LOG_LEVEL_VERBOSE);
return false;
}
return true;
}
onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void {
services.databaseEvents.onUnloadDatabase.addHandler(this._onDBUnload.bind(this));
services.databaseEvents.onCloseDatabase.addHandler(this._onDBClose.bind(this));
services.databaseEvents.onDatabaseInitialisation.addHandler(this._everyOnInitializeDatabase.bind(this));
services.databaseEvents.onResetDatabase.addHandler(this._everyOnResetDatabase.bind(this));
(services.database as ObsidianDatabaseService).openSimpleStore.setHandler(this._getSimpleStore.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
}
}
+1 -1
View File
@@ -133,7 +133,7 @@ export class ModuleMigration extends AbstractModule {
if (!isValidPath(path)) {
continue;
}
if (!(await this.services.vault.isTargetFile(path, true))) {
if (!(await this.services.vault.isTargetFile(path))) {
continue;
}
if (!isMetaEntry(metaDoc)) {
@@ -69,7 +69,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
//@ts-ignore
window.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
_this.app.commands.executeCommandById("editor:save-file");
void _this.app.commands.executeCommandById("editor:save-file");
// _this.app.performCommand('editor:save-file');
};
}
@@ -1,18 +0,0 @@
import type { LiveSyncCore } from "../../main.ts";
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
export class ModuleExtraSyncObsidian extends AbstractObsidianModule {
deviceAndVaultName: string = "";
_getDeviceAndVaultName(): string {
return this.deviceAndVaultName;
}
_setDeviceAndVaultName(name: string): void {
this.deviceAndVaultName = name;
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.setting.getDeviceAndVaultName.setHandler(this._getDeviceAndVaultName.bind(this));
services.setting.setDeviceAndVaultName.setHandler(this._setDeviceAndVaultName.bind(this));
}
}
+8 -4
View File
@@ -39,19 +39,22 @@ import {
isValidFilenameInDarwin,
isValidFilenameInWidows,
} from "@/lib/src/string_and_binary/path.ts";
import { MARK_LOG_SEPARATOR } from "@/lib/src/services/lib/logUtils.ts";
// This module cannot be a core module because it depends on the Obsidian UI.
// DI the log again.
const recentLogEntries = reactiveSource<LogEntry[]>([]);
setGlobalLogFunction((message: any, level?: number, key?: string) => {
const globalLogFunction = (message: any, level?: number, key?: string) => {
const messageX =
message instanceof Error
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
: message;
const entry = { message: messageX, level, key } as LogEntry;
recentLogEntries.value = [...recentLogEntries.value, entry];
});
};
setGlobalLogFunction(globalLogFunction);
let recentLogs = [] as string[];
function addLog(log: string) {
@@ -304,9 +307,9 @@ export class ModuleLog extends AbstractObsidianModule {
// const recent = logMessages.value;
const newMsg = message;
let newLog = this.settings?.showOnlyIconsOnEditor ? "" : status;
const moduleTagEnd = newLog.indexOf(`]\u{200A}`);
const moduleTagEnd = newLog.indexOf(`]${MARK_LOG_SEPARATOR}`);
if (moduleTagEnd != -1) {
newLog = newLog.substring(moduleTagEnd + 2);
newLog = newLog.substring(moduleTagEnd + MARK_LOG_SEPARATOR.length + 1);
}
this.statusBar?.setText(newMsg.split("\n")[0]);
@@ -493,6 +496,7 @@ export class ModuleLog extends AbstractObsidianModule {
}
}
onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
services.API.addLog.setHandler(globalLogFunction);
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
services.appLifecycle.onLoaded.addHandler(this._everyOnload.bind(this));
@@ -424,8 +424,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
//@ts-ignore
manifestVersion: string = MANIFEST_VERSION || "-";
//@ts-ignore
updateInformation: string = UPDATE_INFO || "";
lastVersion = ~~(versionNumberString2Number(this.manifestVersion) / 1000);
+4 -1
View File
@@ -126,7 +126,10 @@ export class ModuleLiveSyncMain extends AbstractModule {
await this.saveSettings();
}
localStorage.setItem(lsKey, `${VER}`);
await this.services.database.openDatabase();
await this.services.database.openDatabase({
databaseEvents: this.services.databaseEvents,
replicator: this.services.replicator,
});
// this.core.$$realizeSettingSyncMode = this.core.$$realizeSettingSyncMode.bind(this);
// this.$$parseReplicationResult = this.$$parseReplicationResult.bind(this);
// this.$$replicate = this.$$replicate.bind(this);
@@ -0,0 +1,92 @@
import { InjectableAPIService } from "@/lib/src/services/implements/injectable/InjectableAPIService";
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import { Platform, type Command, type ViewCreator } from "obsidian";
import { ObsHttpHandler } from "../essentialObsidian/APILib/ObsHttpHandler";
// All Services will be migrated to be based on Plain Services, not Injectable Services.
// This is a migration step.
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
_customHandler: ObsHttpHandler | undefined;
getCustomFetchHandler(): ObsHttpHandler {
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
return this._customHandler;
}
async showWindow(viewType: string): Promise<void> {
const leaves = this.app.workspace.getLeavesOfType(viewType);
if (leaves.length == 0) {
await this.app.workspace.getLeaf(true).setViewState({
type: viewType,
active: true,
});
} else {
await leaves[0].setViewState({
type: viewType,
active: true,
});
}
if (leaves.length > 0) {
await this.app.workspace.revealLeaf(leaves[0]);
}
}
private get app() {
return this.context.app;
}
getPlatform(): string {
if (Platform.isAndroidApp) {
return "android-app";
} else if (Platform.isIosApp) {
return "ios";
} else if (Platform.isMacOS) {
return "macos";
} else if (Platform.isMobileApp) {
return "mobile-app";
} else if (Platform.isMobile) {
return "mobile";
} else if (Platform.isSafari) {
return "safari";
} else if (Platform.isDesktop) {
return "desktop";
} else if (Platform.isDesktopApp) {
return "desktop-app";
} else {
return "unknown-obsidian";
}
}
override isMobile(): boolean {
//@ts-ignore : internal API
return this.app.isMobile;
}
override getAppID(): string {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
override getAppVersion(): string {
const navigatorString = globalThis.navigator?.userAgent ?? "";
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
if (match && match.length >= 2) {
return match[1];
}
return "0.0.0";
}
override getPluginVersion(): string {
return this.context.plugin.manifest.version;
}
addCommand<TCommand extends Command>(command: TCommand): TCommand {
return this.context.plugin.addCommand(command) as TCommand;
}
registerWindow(type: string, factory: ViewCreator): void {
return this.context.plugin.registerView(type, factory);
}
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement {
return this.context.plugin.addRibbonIcon(icon, title, callback);
}
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => any): void {
return this.context.plugin.registerObsidianProtocolHandler(action, handler);
}
}
@@ -9,12 +9,6 @@ declare module "obsidian" {
}
// InjectableAppLifecycleService
export class ObsidianAppLifecycleService<T extends ObsidianServiceContext> extends AppLifecycleServiceBase<T> {
constructor(context: T) {
super(context);
// The main entry point when Obsidian's workspace is ready
const onReady = this.onReady;
this.context.app.workspace.onLayoutReady(onReady);
}
performRestart(): void {
void this.context.plugin.app.commands.executeCommandById("app:reload");
}
@@ -0,0 +1,16 @@
import { initializeStores } from "@/common/stores";
// import { InjectableDatabaseService } from "@/lib/src/services/implements/injectable/InjectableDatabaseService";
import type { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/ObsidianServiceContext";
import { DatabaseService, type DatabaseServiceDependencies } from "@lib/services/base/DatabaseService.ts";
export class ObsidianDatabaseService<T extends ObsidianServiceContext> extends DatabaseService<T> {
private __onOpenDatabase(vaultName: string) {
initializeStores(vaultName);
return Promise.resolve(true);
}
constructor(context: T, dependencies: DatabaseServiceDependencies) {
super(context, dependencies);
this.onOpenDatabase.addHandler(this.__onOpenDatabase.bind(this));
}
}
+20 -4
View File
@@ -3,9 +3,7 @@ import { ObsidianServiceContext } from "@/lib/src/services/implements/obsidian/O
import type { ServiceInstances } from "@/lib/src/services/ServiceHub";
import type ObsidianLiveSyncPlugin from "@/main";
import {
ObsidianAPIService,
ObsidianConflictService,
ObsidianDatabaseService,
ObsidianFileProcessingService,
ObsidianReplicationService,
ObsidianReplicatorService,
@@ -15,7 +13,10 @@ import {
ObsidianTestService,
ObsidianDatabaseEventService,
ObsidianConfigService,
ObsidianKeyValueDBService,
} from "./ObsidianServices";
import { ObsidianDatabaseService } from "./ObsidianDatabaseService";
import { ObsidianAPIService } from "./ObsidianAPIService";
import { ObsidianAppLifecycleService } from "./ObsidianAppLifecycleService";
import { ObsidianPathService } from "./ObsidianPathService";
import { ObsidianVaultService } from "./ObsidianVaultService";
@@ -30,10 +31,9 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
const API = new ObsidianAPIService(context);
const appLifecycle = new ObsidianAppLifecycleService(context);
const conflict = new ObsidianConflictService(context);
const database = new ObsidianDatabaseService(context);
const fileProcessing = new ObsidianFileProcessingService(context);
const replication = new ObsidianReplicationService(context);
const replicator = new ObsidianReplicatorService(context);
const remote = new ObsidianRemoteService(context);
const setting = new ObsidianSettingService(context);
const tweakValue = new ObsidianTweakValueService(context);
@@ -45,7 +45,22 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
const path = new ObsidianPathService(context, {
settingService: setting,
});
const database = new ObsidianDatabaseService(context, {
path: path,
vault: vault,
setting: setting,
});
const keyValueDB = new ObsidianKeyValueDBService(context, {
appLifecycle: appLifecycle,
databaseEvents: databaseEvents,
vault: vault,
});
const config = new ObsidianConfigService(context, vault);
const replicator = new ObsidianReplicatorService(context, {
settingService: setting,
appLifecycleService: appLifecycle,
databaseEventService: databaseEvents,
});
const ui = new ObsidianUIService(context, {
appLifecycle,
config,
@@ -70,6 +85,7 @@ export class ObsidianServiceHub extends InjectableServiceHub<ObsidianServiceCont
path: path,
API: API,
config: config,
keyValueDB: keyValueDB,
} satisfies Required<ServiceInstances<ObsidianServiceContext>>;
super(context, serviceInstancesToInit);
+3 -100
View File
@@ -1,7 +1,5 @@
import { InjectableAPIService } from "@lib/services/implements/injectable/InjectableAPIService";
import { InjectableConflictService } from "@lib/services/implements/injectable/InjectableConflictService";
import { InjectableDatabaseEventService } from "@lib/services/implements/injectable/InjectableDatabaseEventService";
import { InjectableDatabaseService } from "@lib/services/implements/injectable/InjectableDatabaseService";
import { InjectableFileProcessingService } from "@lib/services/implements/injectable/InjectableFileProcessingService";
import { InjectableRemoteService } from "@lib/services/implements/injectable/InjectableRemoteService";
import { InjectableReplicationService } from "@lib/services/implements/injectable/InjectableReplicationService";
@@ -11,105 +9,8 @@ import { InjectableTestService } from "@lib/services/implements/injectable/Injec
import { InjectableTweakValueService } from "@lib/services/implements/injectable/InjectableTweakValueService";
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
import type { ObsidianServiceContext } from "@lib/services/implements/obsidian/ObsidianServiceContext.ts";
import { Platform } from "@/deps";
import type { SimpleStore } from "@/lib/src/common/utils";
import type { IDatabaseService } from "@/lib/src/services/base/IService";
import { handlers } from "@/lib/src/services/lib/HandlerUtils";
import { ObsHttpHandler } from "../essentialObsidian/APILib/ObsHttpHandler";
import type { Command, ViewCreator } from "obsidian";
import { KeyValueDBService } from "@/lib/src/services/base/KeyValueDBService";
// All Services will be migrated to be based on Plain Services, not Injectable Services.
// This is a migration step.
export class ObsidianAPIService extends InjectableAPIService<ObsidianServiceContext> {
_customHandler: ObsHttpHandler | undefined;
getCustomFetchHandler(): ObsHttpHandler {
if (!this._customHandler) this._customHandler = new ObsHttpHandler(undefined, undefined);
return this._customHandler;
}
async showWindow(viewType: string): Promise<void> {
const leaves = this.app.workspace.getLeavesOfType(viewType);
if (leaves.length == 0) {
await this.app.workspace.getLeaf(true).setViewState({
type: viewType,
active: true,
});
} else {
await leaves[0].setViewState({
type: viewType,
active: true,
});
}
if (leaves.length > 0) {
await this.app.workspace.revealLeaf(leaves[0]);
}
}
private get app() {
return this.context.app;
}
getPlatform(): string {
if (Platform.isAndroidApp) {
return "android-app";
} else if (Platform.isIosApp) {
return "ios";
} else if (Platform.isMacOS) {
return "macos";
} else if (Platform.isMobileApp) {
return "mobile-app";
} else if (Platform.isMobile) {
return "mobile";
} else if (Platform.isSafari) {
return "safari";
} else if (Platform.isDesktop) {
return "desktop";
} else if (Platform.isDesktopApp) {
return "desktop-app";
} else {
return "unknown-obsidian";
}
}
override isMobile(): boolean {
//@ts-ignore : internal API
return this.app.isMobile;
}
override getAppID(): string {
return `${"appId" in this.app ? this.app.appId : ""}`;
}
override getAppVersion(): string {
const navigatorString = globalThis.navigator?.userAgent ?? "";
const match = navigatorString.match(/obsidian\/([0-9]+\.[0-9]+\.[0-9]+)/);
if (match && match.length >= 2) {
return match[1];
}
return "0.0.0";
}
override getPluginVersion(): string {
return this.context.plugin.manifest.version;
}
addCommand<TCommand extends Command>(command: TCommand): TCommand {
return this.context.plugin.addCommand(command) as TCommand;
}
registerWindow(type: string, factory: ViewCreator): void {
return this.context.plugin.registerView(type, factory);
}
addRibbonIcon(icon: string, title: string, callback: (evt: MouseEvent) => any): HTMLElement {
return this.context.plugin.addRibbonIcon(icon, title, callback);
}
registerProtocolHandler(action: string, handler: (params: Record<string, string>) => any): void {
return this.context.plugin.registerObsidianProtocolHandler(action, handler);
}
}
export class ObsidianDatabaseService extends InjectableDatabaseService<ObsidianServiceContext> {
openSimpleStore = handlers<IDatabaseService>().binder("openSimpleStore") as (<T>(
kind: string
) => SimpleStore<T>) & { setHandler: (handler: IDatabaseService["openSimpleStore"], override?: boolean) => void };
}
export class ObsidianDatabaseEventService extends InjectableDatabaseEventService<ObsidianServiceContext> {}
// InjectableReplicatorService
@@ -129,3 +30,5 @@ export class ObsidianTweakValueService extends InjectableTweakValueService<Obsid
// InjectableTestService
export class ObsidianTestService extends InjectableTestService<ObsidianServiceContext> {}
export class ObsidianConfigService extends ConfigServiceBrowserCompat<ObsidianServiceContext> {}
export class ObsidianKeyValueDBService extends KeyValueDBService<ObsidianServiceContext> {}
+56 -9
View File
@@ -3,11 +3,58 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## 0.25.43-patched-4
16th February, 2026
I have been working on it little by little in my spare time. Sorry for the delayed response for issues! ! However, thanks for your patience, we seems the `revert to 0.25.43` is not necessary, and I will keep going with this version.
### Refactored
- No longer `DatabaseService` is an injectable service. It is now actually a service which has its own handlers. No dynamic binding for necessary functions.
- Now the following properties of `ObsidianLiveSyncPlugin` belong to each service:
- `replicator` : `services.replicator` (still we can access `ObsidianLiveSyncPlugin.replicator` for the active replicator)
- A Handy class `UnresolvedErrorManager` has been added, which is responsible for managing unresolved errors and their handlers (we will see `unresolved errors` on a red-background-banner in the editor when they occur).
- This manager can be used to handle unresolved errors in a unified way, and it can also be used to display notifications or something when unresolved errors occur.
## 0.25.43-patched-3
16th February, 2026
### Refactored
- Now following properties of `ObsidianLiveSyncPlugin` belong to each service:
- property : service (still we can access these properties from `ObsidianLiveSyncPlugin` for better usability, but probably we should access these from services to clarify the dependencies)
- `localDatabase` : `services.database`
- `managers` : `services.database`
- `simpleStore` : `services.keyValueDB`
- `kvDB`: `services.keyValueDB`
- Initialising modules, addOns, and services are now explicitly separated in the `_startUp` function of the main plug-in class.
- LiveSyncLocalDB now depends more explicitly on specified services, not the whole `ServiceHub`.
- New service `keyValueDB` has been added. This had been separated from the `database` service.
- Non-trivial modules, such as `ModuleExtraSyncObsidian` (which only holds deviceAndVaultName), are simply implemented in the service.
- Add `logUtils` for unifying logging method injection and formatting. This utility is able to accept the API service for log writing.
- `ModuleKeyValueDB` has been removed, and its functionality is now implemented in the `keyValueDB` service.
- `ModulePouchDB` and `ModuleLocalDatabaseObsidian` have been removed, and their functionality is now implemented in the `database` service.
- Please be aware that you have overridden createPouchDBInstance or something by dynamic binding; you should now override the createPouchDBInstance in the database service instead of using the module.
- You can refer to the `DirectFileManipulatorV2` for an example of how to override the createPouchDBInstance function in the database service.
## 0.25.43-patched-2
14th February, 2026
### Fixed
- Application LifeCycle has now started in Main, not ServiceHub.
- Indeed, ServiceHub cannot be known other things in main have got ready, so it is quite natural to start the lifecycle in main.
## 0.25.43-patched-1
13th February, 2026
You know this is a patch version, is the beta-release practically? Do not worry about the following memos, as they are indeed freaking us out. I trust that you have thought this was too large; you're right.
**NOTE: Hidden File Sync and Customisation Sync may not work in this version.**
Just a heads-up: this is a patch version, which is essentially a beta release. Do not worry about the following memos, as they are indeed freaking us out. I trust that you have thought this was too large; you're right.
If this cannot be stable, I will revert to 0.24.43 and try again.
@@ -16,23 +63,23 @@ If this cannot be stable, I will revert to 0.24.43 and try again.
- Now resolving unexpected and inexplicable dependency order issues...
- The function which is able to implement to the service is now moved to each service.
- AppLifecycleService.performRestart
- VaultService.isTargetFile is now uses separated multiple checkers instead of a single function.
- VaultService.isTargetFile is now using multiple checkers instead of a single function.
- This change allows better separation of concerns and easier extension in the future.
- Application LifeCycle has now started in ServiceHub, not ObsidianMenuModule.
- It was on a QUITE unexpected place..., isn't it?
- It was in a QUITE unexpected place..., isn't it?
- Instead of, we should call `await this.services.appLifecycle.onReady()` in other platforms.
- As in browser platform, it will be called at `DOMContentLoaded` event.
- As in the browser platform, it will be called at `DOMContentLoaded` event.
- ModuleTargetFilter, which is responsible for parsing ignore files has been refined.
- This should be separated to a TargetFilter and a IgnoreFileFilter for better maintainability.
- ModuleTargetFilter, which is responsible for parsing ignore files, has been refined.
- This should be separated to a TargetFilter and an IgnoreFileFilter for better maintainability.
- Using `API.addCommand` or some Obsidian API and shimmer APIs, Many modules have been refactored to be derived to AbstractModule from AbstractObsidianModule, to clarify the dependencies. (we should make `app` usage clearer...)
- Fixed initialising `storageAccess` too late in `FileAccessObsidian` module (I am still wonder why it worked before...).
- Fixed initialising `storageAccess` too late in `FileAccessObsidian` module (I am still wondering why it worked before...).
- Remove some redundant overrides in modules.
### Planned
- Some services have ambiguous name, such as `Injectable`. These will be renamed in the future for better clarity.
- Some services have an ambiguous name, such as `Injectable`. These will be renamed in the future for better clarity.
- Following properties of `ObsidianLiveSyncPlugin` should be initialised more explicitly:
- property : where it is initialised currently
- `localDatabase` : `ModuleLocalDatabaseObsidian`
@@ -44,7 +91,7 @@ If this cannot be stable, I will revert to 0.24.43 and try again.
- `fileHandler` : `ModuleFileHandler`
- `rebuilder` : `ModuleRebuilder`
- `kvDB`: `ModuleKeyValueDB`
- And I think that having feature in modules directly is not good for maintainability, these should be separated to some module (loader) and implementation (not only service, but also independent something).
- And I think that having a feature in modules directly is not good for maintainability, these should be separated to some module (loader) and implementation (not only service, but also independent something).
- Plug-in statuses such as requestCount, responseCount... should be moved to a status service or somewhere for better separation of concerns.
## 0.25.43