Compare commits

..

1 Commits

Author SHA1 Message Date
vorotamoroz
ad18140a37 Improved an error verbosity on concurrent processing on start-up process. 2026-05-18 12:04:09 +01:00
5 changed files with 20 additions and 242 deletions

Submodule src/lib updated: 37c3d78c6c...36b99354f6

View File

@@ -2,11 +2,9 @@ import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger";
import { extractObject } from "octagonal-wheels/object"; import { extractObject } from "octagonal-wheels/object";
import { import {
TweakValuesShouldMatchedTemplate, TweakValuesShouldMatchedTemplate,
TweakValuesTemplate,
IncompatibleChanges, IncompatibleChanges,
confName, confName,
type TweakValues, type TweakValues,
type ObsidianLiveSyncSettings,
type RemoteDBSettings, type RemoteDBSettings,
IncompatibleChangesInSpecificPattern, IncompatibleChangesInSpecificPattern,
CompatibleButLossyChanges, CompatibleButLossyChanges,
@@ -18,105 +16,7 @@ import type { InjectableServiceHub } from "../../lib/src/services/InjectableServ
import type { LiveSyncCore } from "../../main.ts"; import type { LiveSyncCore } from "../../main.ts";
import { REMOTE_P2P } from "@lib/common/models/setting.const.ts"; import { REMOTE_P2P } from "@lib/common/models/setting.const.ts";
function valueToString(value: any) {
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "object") {
return JSON.stringify(value);
}
return `${value}`;
}
export class ModuleResolvingMismatchedTweaks extends AbstractModule { 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> { async _anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false; if (!this.core.replicator.tweakSettingsMismatched && !this.core.replicator.preferredTweakValue) return false;
const preferred = this.core.replicator.preferredTweakValue; const preferred = this.core.replicator.preferredTweakValue;
@@ -127,16 +27,10 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
if (ret == "IGNORE") return true; if (ret == "IGNORE") return true;
} }
async _checkAndAskResolvingMismatchedTweaks(preferred: TweakValues): Promise<[TweakValues | boolean, boolean]> { async _checkAndAskResolvingMismatchedTweaks(
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings) as TweakValues; preferred: Partial<TweakValues>
const mismatchedKeys = this._collectMismatchedTweakKeys(mine, preferred); ): Promise<[TweakValues | boolean, boolean]> {
const autoAcceptSide = await this._shouldAutoAcceptCompatibleLossy(mine, preferred, mismatchedKeys); const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
if (autoAcceptSide === "REMOTE") {
return [{ ...mine, ...preferred }, false];
}
if (autoAcceptSide === "CURRENT") {
return [true, false];
}
const items = Object.entries(TweakValuesShouldMatchedTemplate); const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false; let rebuildRequired = false;
let rebuildRecommended = false; let rebuildRecommended = false;
@@ -175,8 +69,8 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
tableRows.push( tableRows.push(
$msg("TweakMismatchResolve.Table.Row", { $msg("TweakMismatchResolve.Table.Row", {
name: confName(key), name: confName(key),
self: valueToString(valueMine), self: valueMine,
remote: valueToString(valuePreferred), remote: valuePreferred,
}) })
); );
} }
@@ -243,7 +137,9 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
if (!tweaks) { if (!tweaks) {
return "IGNORE"; return "IGNORE";
} }
const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(tweaks); const preferred = extractObject(TweakValuesShouldMatchedTemplate, tweaks);
const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(preferred);
if (!conf) return "IGNORE"; if (!conf) return "IGNORE";
if (conf === true) { if (conf === true) {
@@ -251,7 +147,10 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
if (rebuildRequired) { if (rebuildRequired) {
await this.core.rebuilder.$rebuildRemote(); await this.core.rebuilder.$rebuildRemote();
} }
Logger($msg("TweakMismatchResolve.Message.remoteUpdated"), LOG_LEVEL_NOTICE); Logger(
`Tweak values on the remote server have been updated. Your other device will see this message.`,
LOG_LEVEL_NOTICE
);
return "CHECKAGAIN"; return "CHECKAGAIN";
} }
if (conf) { if (conf) {
@@ -261,7 +160,7 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
if (rebuildRequired) { if (rebuildRequired) {
await this.core.rebuilder.$fetchLocal(); await this.core.rebuilder.$fetchLocal();
} }
Logger($msg("TweakMismatchResolve.Message.mineUpdated"), LOG_LEVEL_NOTICE); Logger(`Configuration has been updated as configured by the other device.`, LOG_LEVEL_NOTICE);
return "CHECKAGAIN"; return "CHECKAGAIN";
} }
return "IGNORE"; return "IGNORE";
@@ -302,16 +201,6 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
trialSetting: RemoteDBSettings, trialSetting: RemoteDBSettings,
preferred: TweakValues preferred: TweakValues
): Promise<{ result: false | TweakValues; requireFetch: boolean }> { ): 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); const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false; let rebuildRequired = false;
let rebuildRecommended = false; let rebuildRecommended = false;
@@ -322,8 +211,8 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
// const items = [mine,preferred] // const items = [mine,preferred]
for (const v of items) { for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate; const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const remoteValueForDisplay = escapeMarkdownValue(valueToString(preferred[key])); const remoteValueForDisplay = escapeMarkdownValue(preferred[key]);
const currentValueForDisplay = escapeMarkdownValue(valueToString((trialSetting as TweakValues)?.[key])); const currentValueForDisplay = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])}`;
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) { if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
if (IncompatibleChanges.indexOf(key) !== -1) { if (IncompatibleChanges.indexOf(key) !== -1) {
rebuildRequired = true; rebuildRequired = true;
@@ -400,7 +289,6 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
} }
override onBindFunction(core: LiveSyncCore, services: InjectableServiceHub): void { 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.fetchRemotePreferred.setHandler(this._fetchRemotePreferredTweakValues.bind(this));
services.tweakValue.checkAndAskResolvingMismatched.setHandler( services.tweakValue.checkAndAskResolvingMismatched.setHandler(
this._checkAndAskResolvingMismatchedTweaks.bind(this) this._checkAndAskResolvingMismatchedTweaks.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

@@ -35,7 +35,6 @@ export function paneAdvanced(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
clampMin: 10, clampMin: 10,
onUpdate: this.onlyOnCouchDB, onUpdate: this.onlyOnCouchDB,
}); });
new Setting(paneEl).setClass("wizardHidden").autoWireToggle("autoAcceptCompatibleTweak");
// new Setting(paneEl) // new Setting(paneEl)
// .setClass("wizardHidden") // .setClass("wizardHidden")
// .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB }) // .autoWireToggle("sendChunksBulk", { onUpdate: onlyOnCouchDB })

View File

@@ -3,13 +3,12 @@ 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. 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.
## Unreleased ## Unreleased (0.25.64-patch1)
### New features 18th May, 2026
- Implement auto-accept compatible tweak setting and enhance mismatch resolution logic.
### Improved ### Improved
- Many messages related to tweak mismatch resolution have been updated for clarity. - Improved an error verbosity on concurrent processing on start-up process.
## 0.25.64 ## 0.25.64