import { Logger, LOG_LEVEL_NOTICE } from "octagonal-wheels/common/logger"; import { extractObject } from "octagonal-wheels/object"; import { TweakValuesShouldMatchedTemplate, IncompatibleChanges, confName, type TweakValues, type RemoteDBSettings, IncompatibleChangesInSpecificPattern, CompatibleButLossyChanges, } from "../../lib/src/common/types.ts"; import { escapeMarkdownValue } from "../../lib/src/common/utils.ts"; import { AbstractModule } from "../AbstractModule.ts"; import { $msg } from "../../lib/src/common/i18n.ts"; import type { InjectableServiceHub } from "../../lib/src/services/InjectableServices.ts"; import type { LiveSyncCore } from "../../main.ts"; export class ModuleResolvingMismatchedTweaks extends AbstractModule { async _anyAfterConnectCheckFailed(): Promise { 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: Partial ): Promise<[TweakValues | boolean, boolean]> { const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings); 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: valueMine, remote: 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 preferred = extractObject(TweakValuesShouldMatchedTemplate, tweaks); const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(preferred); if (!conf) return "IGNORE"; if (conf === true) { await this.core.replicator.setPreferredRemoteTweakSettings(this.settings); if (rebuildRequired) { await this.core.rebuilder.$rebuildRemote(); } Logger( `Tweak values on the remote server have been updated. Your other device will see this message.`, 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(`Configuration has been updated as configured by the other device.`, LOG_LEVEL_NOTICE); return "CHECKAGAIN"; } return "IGNORE"; } async _fetchRemotePreferredTweakValues(trialSetting: RemoteDBSettings): Promise { 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 }> { 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 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(preferred[key]); const currentValueForDisplay = `${escapeMarkdownValue((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.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)); } }