Improved

- New Translation: `es` (Spanish) by @zeedif (Thank you so much)!
- Now all of messages can be selectable and copyable, also on the iPhone, iPad, and Android devices. Now we can copy or share the messages easily.

New Feature

- Peer-to-Peer Synchronisation has been implemented!

Fixed

- No longer memory or resource leaks when the plug-in is disabled.
- Now deleted chunks are correctly detected on conflict resolution, and we are guided to resurrect them.
- Hanging issue during the initial synchronisation has been fixed.
- Some unnecessary logs have been removed.
- Now all modal dialogues are correctly closed when the plug-in is disabled.

Refactor

- Several interfaces have been moved to the separated library.
- Translations have been moved to each language file, and during the build, they are merged into one file.
- Non-mobile friendly code has been removed and replaced with the safer code.
- Started writing Platform impedance-matching-layer.
- Svelte has been updated to v5.
- Some function have got more robust type definitions.
- Terser optimisation has slightly improved.
- During the build, analysis meta-file of the bundled codes will be generated.
This commit is contained in:
vorotamoroz
2025-02-13 12:48:00 +00:00
parent 45ceca8bb6
commit 1cd1465f2c
39 changed files with 9209 additions and 632 deletions

View File

@@ -12,6 +12,7 @@ import type { Rebuilder } from "../interfaces/DatabaseRebuilder.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
import type { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator.ts";
import { fetchAllUsedChunks } from "../../lib/src/pouchdb/utils_couchdb.ts";
import { EVENT_DATABASE_REBUILT, eventHub } from "src/common/events.ts";
export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebuilder {
$everyOnload(): Promise<boolean> {
@@ -214,6 +215,7 @@ export class ModuleRebuilder extends AbstractModule implements ICoreModule, Rebu
const suffix = (await this.core.$anyGetAppId()) || "";
this.core.settings.additionalSuffixOfDatabaseName = suffix;
await this.core.$$resetLocalDatabase();
eventHub.emitEvent(EVENT_DATABASE_REBUILT);
}
async fetchRemoteChunks() {
if (

View File

@@ -84,8 +84,8 @@ export class ModuleReplicator extends AbstractModule implements ICoreModule {
//<-- Here could be an module.
const ret = await this.core.replicator.openReplication(this.settings, false, showMessage, false);
if (!ret) {
if (this.core.replicator.tweakSettingsMismatched) {
await this.core.$$askResolvingMismatchedTweaks();
if (this.core.replicator.tweakSettingsMismatched && this.core.replicator.preferredTweakValue) {
await this.core.$$askResolvingMismatchedTweaks(this.core.replicator.preferredTweakValue);
} else {
if (this.core.replicator?.remoteLockedAndDeviceNotAccepted) {
if (this.core.replicator.remoteCleaned && this.settings.useIndexedDBAdapter) {
@@ -220,6 +220,11 @@ Or if you are sure know what had been happened, we can unlock the database from
const ids = [...new Set((await this.core.kvDB.get<string[]>(kvDBKey)) ?? [])];
const batchSize = 100;
const chunkedIds = arrayToChunkedArray(ids, batchSize);
// suspendParseReplicationResult is true, so we have to resume it if it is suspended.
if (this.replicationResultProcessor.isSuspended) {
this.replicationResultProcessor.resume();
}
for await (const idsBatch of chunkedIds) {
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({
keys: idsBatch,
@@ -233,8 +238,11 @@ Or if you are sure know what had been happened, we can unlock the database from
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
}
this.replicationResultProcessor.enqueueAll(docs);
await this.replicationResultProcessor.waitForAllProcessed();
}
if (this.replicationResultProcessor.isSuspended) {
this.replicationResultProcessor.resume();
}
await this.replicationResultProcessor.waitForAllProcessed();
}
replicationResultProcessor = new QueueProcessor(

View File

@@ -1,5 +1,5 @@
import { fireAndForget } from "octagonal-wheels/promises";
import { REMOTE_MINIO, type RemoteDBSettings } from "../../lib/src/common/types";
import { REMOTE_MINIO, REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
import { LiveSyncCouchDBReplicator } from "../../lib/src/replication/couchdb/LiveSyncReplicator";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "../AbstractModule";
@@ -9,13 +9,13 @@ export class ModuleReplicatorCouchDB extends AbstractModule implements ICoreModu
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
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) {
if (settings.remoteType == REMOTE_MINIO || settings.remoteType == REMOTE_P2P) {
return undefined!;
}
return Promise.resolve(new LiveSyncCouchDBReplicator(this.core));
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.remoteType != REMOTE_MINIO) {
if (this.settings.remoteType != REMOTE_MINIO && this.settings.remoteType != REMOTE_P2P) {
// If LiveSync enabled, open replication
if (this.settings.liveSync) {
fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));

View File

@@ -0,0 +1,30 @@
import { REMOTE_P2P, type RemoteDBSettings } from "../../lib/src/common/types";
import type { LiveSyncAbstractReplicator } from "../../lib/src/replication/LiveSyncAbstractReplicator";
import { AbstractModule } from "../AbstractModule";
import type { ICoreModule } from "../ModuleTypes";
import { LiveSyncTrysteroReplicator } from "../../lib/src/replication/trystero/LiveSyncTrysteroReplicator";
export class ModuleReplicatorP2P extends AbstractModule implements ICoreModule {
$anyNewReplicator(settingOverride: Partial<RemoteDBSettings> = {}): Promise<LiveSyncAbstractReplicator> {
const settings = { ...this.settings, ...settingOverride };
if (settings.remoteType == REMOTE_P2P) {
return Promise.resolve(new LiveSyncTrysteroReplicator(this.core));
}
return undefined!;
}
$everyAfterResumeProcess(): Promise<boolean> {
if (this.settings.remoteType == REMOTE_P2P) {
// // If LiveSync enabled, open replication
// if (this.settings.liveSync) {
// fireAndForget(() => this.core.replicator.openReplication(this.settings, true, false, false));
// }
// // If sync on start enabled, open replication
// if (!this.settings.liveSync && this.settings.syncOnStart) {
// // Possibly ok as if only share the result
// fireAndForget(() => this.core.replicator.openReplication(this.settings, false, false, false));
// }
}
return Promise.resolve(true);
}
}

View File

@@ -192,7 +192,7 @@ export class ModuleConflictResolver extends AbstractModule implements ICoreModul
}
return diff;
});
console.warn(mTimeAndRev);
// console.warn(mTimeAndRev);
this._log(
`Resolving conflict by newest: ${filename} (Newest: ${new Date(mTimeAndRev[0][0]).toLocaleString()}) (${mTimeAndRev.length} revisions exists)`
);

View File

@@ -13,18 +13,18 @@ import type { ICoreModule } from "../ModuleTypes.ts";
export class ModuleResolvingMismatchedTweaks extends AbstractModule implements ICoreModule {
async $anyAfterConnectCheckFailed(): Promise<boolean | "CHECKAGAIN" | undefined> {
if (!this.core.replicator.tweakSettingsMismatched) return false;
const ret = await this.core.$$askResolvingMismatchedTweaks();
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.core.$$askResolvingMismatchedTweaks(preferred);
if (ret == "OK") return false;
if (ret == "CHECKAGAIN") return "CHECKAGAIN";
if (ret == "IGNORE") return true;
}
async $$askResolvingMismatchedTweaks(): Promise<"OK" | "CHECKAGAIN" | "IGNORE"> {
if (!this.core.replicator.tweakSettingsMismatched) {
return "OK";
}
const preferred = extractObject(TweakValuesShouldMatchedTemplate, this.core.replicator.preferredTweakValue!);
async $$checkAndAskResolvingMismatchedTweaks(
preferred: Partial<TweakValues>
): Promise<[TweakValues | boolean, boolean]> {
const mine = extractObject(TweakValuesShouldMatchedTemplate, this.settings);
const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false;
@@ -85,8 +85,22 @@ Please select which one you want to use.
CHOICE_DISMISS,
60
);
if (!retKey) return "IGNORE";
const conf = CHOICES[retKey];
if (!retKey) return [false, false];
return [CHOICES[retKey], rebuildRequired];
}
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.core.$$checkAndAskResolvingMismatchedTweaks(preferred);
if (!conf) return "IGNORE";
if (conf === true) {
await this.core.replicator.setPreferredRemoteTweakSettings(this.settings);
@@ -119,44 +133,55 @@ Please select which one you want to use.
if (await replicator.tryConnectRemote(trialSetting)) {
const preferred = await replicator.getRemotePreferredTweakValues(trialSetting);
if (preferred) {
const items = Object.entries(TweakValuesShouldMatchedTemplate);
let rebuildRequired = false;
// Making tables:
let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`;
let differenceCount = 0;
// const items = [mine,preferred]
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const valuePreferred = escapeMarkdownValue(preferred[key]);
const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`;
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) {
rebuildRequired = true;
}
} else {
continue;
}
table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`;
differenceCount++;
return await this.$$askUseRemoteConfiguration(trialSetting, preferred);
} else {
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
}
return { result: false, requireFetch: false };
} else {
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
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;
// Making tables:
let table = `| Value name | This device | Stored | \n` + `|: --- |: ---- :|: ---- :| \n`;
let differenceCount = 0;
// const items = [mine,preferred]
for (const v of items) {
const key = v[0] as keyof typeof TweakValuesShouldMatchedTemplate;
const valuePreferred = escapeMarkdownValue(preferred[key]);
const currentDisp = `${escapeMarkdownValue((trialSetting as TweakValues)?.[key])} |`;
if ((trialSetting as TweakValues)?.[key] !== preferred[key]) {
if (CompatibilityBreakingTweakValues.indexOf(key) !== -1) {
rebuildRequired = true;
}
} else {
continue;
}
table += `| ${confName(key)} | ${currentDisp} ${valuePreferred} | \n`;
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
? `
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
? `
>[!WARNING]
> Some remote configurations are not compatible with the local database of this device. Rebuilding the local database will be required.
***Please ensure that you have time and are connected to a stable network to apply!***`
: "";
: "";
const message = `
const message = `
The settings in the remote database are as follows.
If you want to use these settings, please select "Use configured".
If you want to keep the settings of this device, please select "Dismiss".
@@ -168,29 +193,22 @@ ${table}
${additionalMessage}`;
const CHOICE_USE_REMOTE = "Use configured";
const CHOICE_DISMISS = "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: "Use Remote Configuration",
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 };
}
} else {
this._log("Failed to get the preferred tweak values from the remote server.", LOG_LEVEL_NOTICE);
}
return { result: false, requireFetch: false };
} else {
this._log("Failed to connect to the remote server.", LOG_LEVEL_NOTICE);
return { result: false, requireFetch: false };
const CHOICE_USE_REMOTE = "Use configured";
const CHOICE_DISMISS = "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: "Use Remote Configuration",
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 };
}
}

View File

@@ -10,7 +10,8 @@ import {
confirmWithMessageWithWideButton,
} from "./UILib/dialogs.ts";
import { Notice } from "../../deps.ts";
import type { Confirm } from "../interfaces/Confirm.ts";
import type { Confirm } from "../../lib/src/interfaces/Confirm.ts";
import { setConfirmInstance } from "../../lib/src/PlatformAPIs/obsidian/Confirm.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
// This module cannot be a common module because it depends on Obsidian's API.
@@ -19,20 +20,20 @@ import { $msg } from "src/lib/src/common/i18n.ts";
export class ModuleInputUIObsidian extends AbstractObsidianModule implements IObsidianModule, Confirm {
$everyOnload(): Promise<boolean> {
this.core.confirm = this;
setConfirmInstance(this);
return Promise.resolve(true);
}
askYesNo(message: string): Promise<"yes" | "no"> {
return askYesNo(this.app, message);
}
askString(title: string, key: string, placeholder: string, isPassword: boolean = false): Promise<string | false> {
return askString(this.app, title, key, placeholder, isPassword);
}
async askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = {}
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number } = { title: "Confirmation" }
): Promise<"yes" | "no"> {
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleConfirmation");
const yesLabel = $msg("moduleInputUIObsidian.optionYes");
@@ -53,11 +54,11 @@ export class ModuleInputUIObsidian extends AbstractObsidianModule implements IOb
return askSelectString(this.app, message, items);
}
askSelectStringDialogue(
askSelectStringDialogue<T extends readonly string[]>(
message: string,
buttons: string[],
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false> {
buttons: T,
opt: { title?: string; defaultAction: T[number]; timeout?: number }
): Promise<T[number] | false> {
const defaultTitle = $msg("moduleInputUIObsidian.defaultTitleSelect");
return confirmWithMessageWithWideButton(
this.plugin,

View File

@@ -1,25 +1,20 @@
import { ButtonComponent } from "obsidian";
import { App, FuzzySuggestModal, MarkdownRenderer, Modal, Plugin, Setting } from "../../../deps.ts";
import { EVENT_PLUGIN_UNLOADED, eventHub } from "../../../common/events.ts";
import { delay } from "octagonal-wheels/promises";
class AutoClosableModal extends Modal {
removeEvent: (() => void) | undefined;
_closeByUnload() {
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
this.close();
}
constructor(app: App) {
super(app);
this.removeEvent = eventHub.onEvent(EVENT_PLUGIN_UNLOADED, async () => {
await delay(100);
if (!this.removeEvent) return;
this.close();
this.removeEvent = undefined;
});
this._closeByUnload = this._closeByUnload.bind(this);
eventHub.once(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
}
onClose() {
if (this.removeEvent) {
this.removeEvent();
this.removeEvent = undefined;
}
eventHub.off(EVENT_PLUGIN_UNLOADED, this._closeByUnload);
}
}
@@ -135,11 +130,11 @@ export class PopoverSelectString extends FuzzySuggestModal<string> {
}
}
export class MessageBox extends AutoClosableModal {
export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
plugin: Plugin;
title: string;
contentMd: string;
buttons: string[];
buttons: T;
result: string | false = false;
isManuallyClosed = false;
defaultAction: string | undefined;
@@ -154,11 +149,11 @@ export class MessageBox extends AutoClosableModal {
plugin: Plugin,
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
buttons: T,
defaultAction: T[number],
timeout: number | undefined,
wideButton: boolean,
onSubmit: (result: (typeof buttons)[number] | false) => void
onSubmit: (result: T[number] | false) => void
) {
super(plugin.app);
this.plugin = plugin;
@@ -194,6 +189,7 @@ export class MessageBox extends AutoClosableModal {
this.titleEl.setText(this.title);
const div = contentEl.createDiv();
div.style.userSelect = "text";
div.style["webkitUserSelect"] = "text";
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
const buttonSetting = new Setting(contentEl);
const labelWrapper = contentEl.createDiv();
@@ -262,14 +258,14 @@ export class MessageBox extends AutoClosableModal {
}
}
export function confirmWithMessage(
export function confirmWithMessage<T extends readonly string[]>(
plugin: Plugin,
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
buttons: T,
defaultAction: T[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
): Promise<T[number] | false> {
return new Promise((res) => {
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, false, (result) =>
res(result)
@@ -277,14 +273,14 @@ export function confirmWithMessage(
dialog.open();
});
}
export function confirmWithMessageWithWideButton(
export function confirmWithMessageWithWideButton<T extends readonly string[]>(
plugin: Plugin,
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
buttons: T,
defaultAction: T[number],
timeout?: number
): Promise<(typeof buttons)[number] | false> {
): Promise<T[number] | false> {
return new Promise((res) => {
const dialog = new MessageBox(plugin, title, contentMd, buttons, defaultAction, timeout, true, (result) =>
res(result)

View File

@@ -1,6 +1,7 @@
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger.js";
import { SETTING_VERSION_SUPPORT_CASE_INSENSITIVE } from "../../lib/src/common/types.js";
import {
EVENT_REQUEST_OPEN_P2P,
EVENT_REQUEST_OPEN_SETTING_WIZARD,
EVENT_REQUEST_OPEN_SETTINGS,
EVENT_REQUEST_OPEN_SETUP_URI,
@@ -194,10 +195,11 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
async askAgainForSetupURI() {
const message = $msg("moduleMigration.msgRecommendSetupUri", { URI_DOC: $msg("moduleMigration.docUri") });
const USE_MINIMAL = $msg("moduleMigration.optionSetupWizard");
const USE_P2P = $msg("moduleMigration.optionSetupViaP2P");
const USE_SETUP = $msg("moduleMigration.optionManualSetup");
const NEXT = $msg("moduleMigration.optionRemindNextLaunch");
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, NEXT], {
const ret = await this.core.confirm.askSelectStringDialogue(message, [USE_MINIMAL, USE_SETUP, USE_P2P, NEXT], {
title: $msg("moduleMigration.titleRecommendSetupUri"),
defaultAction: USE_MINIMAL,
});
@@ -205,6 +207,10 @@ export class ModuleMigration extends AbstractModule implements ICoreModule {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTING_WIZARD);
return false;
}
if (ret === USE_P2P) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P);
return false;
}
if (ret === USE_SETUP) {
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETTINGS);
return false;

View File

@@ -124,7 +124,7 @@ export class ModuleObsidianMenu extends AbstractObsidianModule implements IObsid
});
}
if (leaves.length > 0) {
this.app.workspace.revealLeaf(leaves[0]);
await this.app.workspace.revealLeaf(leaves[0]);
}
}
}

View File

@@ -2,7 +2,7 @@ import { delay } from "octagonal-wheels/promises";
import { AbstractObsidianModule, type IObsidianModule } from "../AbstractObsidianModule.ts";
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger";
import { eventHub } from "../../common/events";
import { webcrypto } from "crypto";
import { getWebCrypto } from "../../lib/src/mods.ts";
import { uint8ArrayToHexString } from "octagonal-wheels/binary/hex";
import { parseYaml, requestUrl, stringifyYaml } from "obsidian";
import type { FilePath } from "../../lib/src/common/types.ts";
@@ -162,6 +162,7 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
async _dumpFileList(outFile?: string) {
const files = this.core.storageAccess.getFiles();
const out = [] as any[];
const webcrypto = await getWebCrypto();
for (const file of files) {
if (!(await this.core.$$isTargetFile(file.path))) {
continue;
@@ -202,7 +203,8 @@ export class ModuleReplicateTest extends AbstractObsidianModule implements IObsi
.map((e) => new RegExp(e, "i"));
const out = [] as any[];
const files = await this.core.storageAccess.getFilesIncludeHidden("", undefined, ignorePatterns);
console.dir(files);
// console.dir(files);
const webcrypto = await getWebCrypto();
for (const file of files) {
// if (!await this.core.$$isTargetFile(file)) {
// continue;

View File

@@ -66,7 +66,10 @@
for (const revInfo of reversedRevs) {
if (revInfo.status == "available") {
const doc = (!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev) ? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true) : await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
const doc =
(!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev)
? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true)
: await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
if (doc === false) continue;
const rev = revInfo.rev;
@@ -94,7 +97,10 @@
[DIFF_EQUAL]: 0,
[DIFF_INSERT]: 0,
} as { [key: number]: number };
const px = diff.reduce((p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }), pxInit);
const px = diff.reduce(
(p, c) => ({ ...p, [c[0]]: (p[c[0]] ?? 0) + c[1].length }),
pxInit
);
diffDetail = `-${px[DIFF_DELETE]}, +${px[DIFF_INSERT]}`;
}
}
@@ -104,9 +110,13 @@
}
if (rev == docA._rev) {
if (checkStorageDiff) {
const isExist = await plugin.storageAccess.isExistsIncludeHidden(stripAllPrefixes(getPath(docA)));
const isExist = await plugin.storageAccess.isExistsIncludeHidden(
stripAllPrefixes(getPath(docA))
);
if (isExist) {
const data = await plugin.storageAccess.readHiddenFileBinary(stripAllPrefixes(getPath(docA)));
const data = await plugin.storageAccess.readHiddenFileBinary(
stripAllPrefixes(getPath(docA))
);
const d = readAsBlob(doc);
const result = await isDocContentSame(data, d);
if (result) {
@@ -187,19 +197,28 @@
<div class="globalhistory">
<h1>Vault history</h1>
<div class="control">
<div class="row"><label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} /></div>
<div class="row">
<label for="">From:</label><input type="date" bind:value={dispDateFrom} disabled={loading} />
</div>
<div class="row"><label for="">To:</label><input type="date" bind:value={dispDateTo} disabled={loading} /></div>
<div class="row">
<label for="">Info:</label>
<label><input type="checkbox" bind:checked={showDiffInfo} disabled={loading} /><span>Diff</span></label>
<label><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span></label>
<label><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span></label>
<label
><input type="checkbox" bind:checked={showChunkCorrected} disabled={loading} /><span>Chunks</span
></label
>
<label
><input type="checkbox" bind:checked={checkStorageDiff} disabled={loading} /><span>File integrity</span
></label
>
</div>
</div>
{#if loading}
<div class="">Gathering information...</div>
{/if}
<table>
<tbody>
<tr>
<th> Date </th>
<th> Path </th>
@@ -212,7 +231,7 @@
<tr>
<td colspan="5" class="more">
{#if loading}
<div class="" />
<div class=""></div>
{:else}
<div><button on:click={() => nextWeek()}>+1 week</button></div>
{/if}
@@ -257,12 +276,13 @@
<tr>
<td colspan="5" class="more">
{#if loading}
<div class="" />
<div class=""></div>
{:else}
<div><button on:click={() => prevWeek()}>+1 week</button></div>
{/if}
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -1,10 +1,20 @@
import { ItemView, WorkspaceLeaf } from "../../../deps.ts";
import { WorkspaceLeaf } from "../../../deps.ts";
import GlobalHistoryComponent from "./GlobalHistory.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
import { mount } from "svelte";
export const VIEW_TYPE_GLOBAL_HISTORY = "global-history";
export class GlobalHistoryView extends ItemView {
component?: GlobalHistoryComponent;
export class GlobalHistoryView extends SvelteItemView {
instantiateComponent(target: HTMLElement) {
return mount(GlobalHistoryComponent, {
target: target,
props: {
plugin: this.plugin,
},
});
}
plugin: ObsidianLiveSyncPlugin;
icon = "clock";
title: string = "";
@@ -26,19 +36,4 @@ export class GlobalHistoryView extends ItemView {
getDisplayText() {
return "Vault history";
}
async onOpen() {
this.component = new GlobalHistoryComponent({
target: this.contentEl,
props: {
plugin: this.plugin,
},
});
await Promise.resolve();
}
async onClose() {
this.component?.$destroy();
await Promise.resolve();
}
}

View File

@@ -6,10 +6,16 @@
import { $msg as msg, currentLang as lang } from "../../../lib/src/common/i18n.ts";
let unsubscribe: () => void;
let messages = [] as string[];
let wrapRight = false;
let autoScroll = true;
let suspended = false;
let messages = $state([] as string[]);
let wrapRight = $state(false);
let autoScroll = $state(true);
let suspended = $state(false);
type Props = {
close: () => void;
};
let { close }: Props = $props();
// export let close: () => void;
function updateLog(logs: ReactiveInstance<string[]>) {
const e = logs.value;
if (!suspended) {
@@ -29,6 +35,9 @@
if (unsubscribe) unsubscribe();
});
let scroll: HTMLDivElement;
function closeDialogue() {
close();
}
</script>
<div class="logpane">
@@ -47,6 +56,8 @@
<input type="checkbox" bind:checked={suspended} />
<span>{msg("logPane.pause", {}, lang)}</span>
</label>
<span class="spacer"></span>
<button onclick={() => closeDialogue()}>Close</button>
</div>
</div>
<div class="log" bind:this={scroll}>
@@ -68,6 +79,7 @@
.log {
overflow-y: scroll;
user-select: text;
-webkit-user-select: text;
padding-bottom: 2em;
}
.log > pre {

View File

@@ -1,15 +1,27 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { WorkspaceLeaf } from "obsidian";
import LogPaneComponent from "./LogPane.svelte";
import type ObsidianLiveSyncPlugin from "../../../main.ts";
import { SvelteItemView } from "../../../common/SvelteItemView.ts";
import { $msg } from "src/lib/src/common/i18n.ts";
import { mount } from "svelte";
export const VIEW_TYPE_LOG = "log-log";
//Log view
export class LogPaneView extends ItemView {
component?: LogPaneComponent;
export class LogPaneView extends SvelteItemView {
instantiateComponent(target: HTMLElement) {
return mount(LogPaneComponent, {
target: target,
props: {
close: () => {
this.leaf.detach();
},
},
});
}
plugin: ObsidianLiveSyncPlugin;
icon = "view-log";
title: string = "";
navigation = true;
navigation = false;
getIcon(): string {
return "view-log";
@@ -28,17 +40,4 @@ export class LogPaneView extends ItemView {
// TODO: This function is not reactive and does not update the title based on the current language
return $msg("logPane.title");
}
async onOpen() {
this.component = new LogPaneComponent({
target: this.contentEl,
props: {},
});
await Promise.resolve();
}
async onClose() {
this.component?.$destroy();
await Promise.resolve();
}
}

View File

@@ -27,6 +27,14 @@ import { QueueProcessor } from "octagonal-wheels/concurrency/processor";
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
import { serialized } from "octagonal-wheels/concurrency/lock";
import { $msg } from "src/lib/src/common/i18n.ts";
import type { P2PReplicationProgress } from "../../lib/src/replication/trystero/TrysteroReplicator.ts";
import {
EVENT_ADVERTISEMENT_RECEIVED,
EVENT_DEVICE_LEAVED,
EVENT_P2P_CONNECTED,
EVENT_P2P_DISCONNECTED,
EVENT_P2P_REPLICATOR_PROGRESS,
} from "src/lib/src/replication/trystero/TrysteroReplicatorP2PServer.ts";
// This module cannot be a core module because it depends on the Obsidian UI.
@@ -168,10 +176,12 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
const queued = queueCountLabel();
const waiting = waitingLabel();
const networkActivity = requestingStatLabel();
const p2p = this.p2pReplicationLine.value;
return {
message: `${networkActivity}Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${queued}`,
message: `${networkActivity}Sync: ${w}${sent}${pushLast}${arrived}${pullLast}${waiting}${queued}${p2p == "" ? "" : "\n" + p2p}`,
};
});
const statusBarLabels = reactive(() => {
const scheduleMessage = this.core.$$isReloadingScheduled()
? `WARNING! RESTARTING OBSIDIAN IS SCHEDULED!\n`
@@ -193,9 +203,90 @@ export class ModuleLog extends AbstractObsidianModule implements IObsidianModule
statusBarLabels.onChanged((label) => applyToDisplay(label.value));
}
p2pReplicationResult = new Map<string, P2PReplicationProgress>();
updateP2PReplicationLine() {
const p2pReplicationResultX = [...this.p2pReplicationResult.values()].sort((a, b) =>
a.peerId.localeCompare(b.peerId)
);
const renderProgress = (current: number, max: number) => {
if (current == max) return `${current}`;
return `${current} (${max})`;
};
const line = p2pReplicationResultX
.map(
(e) =>
`${e.fetching.isActive || e.sending.isActive ? "⚡" : "💤"} ${e.peerName}${renderProgress(e.sending.current, e.sending.max)}${renderProgress(e.fetching.current, e.fetching.max)} `
)
.join("\n");
this.p2pReplicationLine.value = line;
}
// p2pReplicationResultX = reactiveSource([] as P2PReplicationProgress[]);
p2pReplicationLine = reactiveSource("");
$everyOnload(): Promise<boolean> {
eventHub.onEvent(EVENT_LEAF_ACTIVE_CHANGED, () => this.onActiveLeafChange());
eventHub.onceEvent(EVENT_LAYOUT_READY, () => this.onActiveLeafChange());
eventHub.onEvent(EVENT_ADVERTISEMENT_RECEIVED, (data) => {
this.p2pReplicationResult.set(data.peerId, {
peerId: data.peerId,
peerName: data.name,
fetching: {
current: 0,
max: 0,
isActive: false,
},
sending: {
current: 0,
max: 0,
isActive: false,
},
});
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_P2P_CONNECTED, () => {
this.p2pReplicationResult.clear();
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_P2P_DISCONNECTED, () => {
this.p2pReplicationResult.clear();
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_DEVICE_LEAVED, (peerId) => {
this.p2pReplicationResult.delete(peerId);
this.updateP2PReplicationLine();
});
eventHub.onEvent(EVENT_P2P_REPLICATOR_PROGRESS, (data) => {
const prev = this.p2pReplicationResult.get(data.peerId) || {
peerId: data.peerId,
peerName: data.peerName,
fetching: {
current: 0,
max: 0,
isActive: false,
},
sending: {
current: 0,
max: 0,
isActive: false,
},
};
if ("fetching" in data) {
if (data.fetching.isActive) {
prev.fetching = data.fetching;
} else {
prev.fetching.isActive = false;
}
}
if ("sending" in data) {
if (data.sending.isActive) {
prev.sending = data.sending;
} else {
prev.sending.isActive = false;
}
}
this.p2pReplicationResult.set(data.peerId, prev);
this.updateP2PReplicationLine();
});
return Promise.resolve(true);
}
adjustStatusDivPosition() {

View File

@@ -25,6 +25,7 @@ import {
LEVEL_EDGE_CASE,
type MetaEntry,
type FilePath,
REMOTE_P2P,
} from "../../../lib/src/common/types.ts";
import {
createBlob,
@@ -78,6 +79,7 @@ import { ICHeader, ICXHeader, PSCHeader } from "../../../common/types.ts";
import { HiddenFileSync } from "../../../features/HiddenFileSync/CmdHiddenFileSync.ts";
import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
import { LocalDatabaseMaintenance } from "../../../features/LocalDatabaseMainte/CmdLocalDatabaseMainte.ts";
import { mount } from "svelte";
export type OnUpdateResult = {
visibility?: boolean;
@@ -811,6 +813,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
return false;
};
const enableOnlySyncDisabled = enableOnly(() => !isAnySyncEnabled());
const onlyOnP2POrCouchDB = () =>
({
visibility:
this.isConfiguredAs("remoteType", REMOTE_P2P) || this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
}) as OnUpdateResult;
const onlyOnCouchDB = () =>
({
visibility: this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
@@ -819,7 +827,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
({
visibility: this.isConfiguredAs("remoteType", REMOTE_MINIO),
}) as OnUpdateResult;
const onlyOnOnlyP2P = () =>
({
visibility: this.isConfiguredAs("remoteType", REMOTE_P2P),
}) as OnUpdateResult;
const onlyOnCouchDBOrMinIO = () =>
({
visibility:
this.isConfiguredAs("remoteType", REMOTE_COUCHDB) ||
this.isConfiguredAs("remoteType", REMOTE_MINIO),
}) as OnUpdateResult;
// E2EE Function
const checkWorkingPassphrase = async (): Promise<boolean> => {
if (this.editingSettings.remoteType == REMOTE_MINIO) return true;
@@ -1364,10 +1381,35 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
options: {
[REMOTE_COUCHDB]: $msg("obsidianLiveSyncSettingTab.optionCouchDB"),
[REMOTE_MINIO]: $msg("obsidianLiveSyncSettingTab.optionMinioS3R2"),
[REMOTE_P2P]: "Only Peer-to-Peer",
},
onUpdate: enableOnlySyncDisabled,
});
void addPanel(paneEl, "Peer-to-Peer", undefined, onlyOnOnlyP2P).then((paneEl) => {
const syncWarnP2P = this.createEl(paneEl, "div", {
text: "",
});
const p2pMessage = `This feature is a Work In Progress, and configurable on \`P2P Replicator\` Pane.
The pane also can be launched by \`P2P Replicator\` command from the Command Palette.
`;
void MarkdownRenderer.render(this.plugin.app, p2pMessage, syncWarnP2P, "/", this.plugin);
syncWarnP2P.addClass("op-warn-info");
new Setting(paneEl)
.setName("Apply Settings")
.setClass("wizardHidden")
.addApplyButton(["remoteType"]);
// .addOnUpdate(onlyOnMinIO);
// new Setting(paneEl).addButton((button) =>
// button
// .setButtonText("Open P2P Replicator")
// .onClick(() => {
// const addOn = this.plugin.getAddOn<P2PReplicator>(P2PReplicator.name);
// void addOn?.openPane();
// this.closeSetting();
// })
// );
});
void addPanel(
paneEl,
$msg("obsidianLiveSyncSettingTab.titleMinioS3R2"),
@@ -1523,7 +1565,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.addOnUpdate(onlyOnCouchDB);
});
});
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleNotification")).then((paneEl) => {
void addPanel(
paneEl,
$msg("obsidianLiveSyncSettingTab.titleNotification"),
() => {},
onlyOnCouchDB
).then((paneEl) => {
paneEl.addClass("wizardHidden");
new Setting(paneEl)
.autoWireNumeric("notifyThresholdOfRemoteStorageSize", {})
@@ -2001,7 +2048,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
"(RegExp) Empty to sync all files. Set filter as a regular expression to limit synchronising files."
)
.setClass("wizardHidden");
new MultipleRegExpControl({
mount(MultipleRegExpControl, {
target: syncFilesSetting.controlEl,
props: {
patterns: this.editingSettings.syncOnlyRegEx.split("|[]|"),
@@ -2024,7 +2071,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
)
.setClass("wizardHidden");
new MultipleRegExpControl({
mount(MultipleRegExpControl, {
target: nonSyncFilesSetting.controlEl,
props: {
patterns: this.editingSettings.syncIgnoreRegEx.split("|[]|"),
@@ -2057,7 +2104,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
.filter((x) => x != "");
const patSetting = new Setting(paneEl).setName("Ignore patterns").setClass("wizardHidden").setDesc("");
new MultipleRegExpControl({
mount(MultipleRegExpControl, {
target: patSetting.controlEl,
props: {
patterns: pat,
@@ -2233,9 +2280,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
pluginConfig.encryptedCouchDBConnection = REDACTED;
pluginConfig.accessKey = REDACTED;
pluginConfig.secretKey = REDACTED;
pluginConfig.region = `${REDACTED}(${pluginConfig.region.length} letters)`;
pluginConfig.bucket = `${REDACTED}(${pluginConfig.bucket.length} letters)`;
const redact = (source: string) => `${REDACTED}(${source.length} letters)`;
pluginConfig.region = redact(pluginConfig.region);
pluginConfig.bucket = redact(pluginConfig.bucket);
pluginConfig.pluginSyncExtendedSetting = {};
pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID);
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
const endpoint = pluginConfig.endpoint;
if (endpoint == "") {
pluginConfig.endpoint = "Not configured or AWS";
@@ -2918,7 +2970,8 @@ ${stringifyYaml(pluginConfig)}`;
.onClick(async () => {
await this.plugin.$$markRemoteLocked();
})
);
)
.addOnUpdate(onlyOnCouchDBOrMinIO);
new Setting(paneEl)
.setName("Emergency restart")
@@ -2935,7 +2988,7 @@ ${stringifyYaml(pluginConfig)}`;
);
});
void addPanel(paneEl, "Syncing").then((paneEl) => {
void addPanel(paneEl, "Syncing", () => {}, onlyOnCouchDBOrMinIO).then((paneEl) => {
new Setting(paneEl)
.setName("Resend")
.setDesc("Resend all chunks to the remote.")
@@ -2951,6 +3004,7 @@ ${stringifyYaml(pluginConfig)}`;
})
)
.addOnUpdate(onlyOnCouchDB);
new Setting(paneEl)
.setName("Reset journal received history")
.setDesc(
@@ -2994,7 +3048,7 @@ ${stringifyYaml(pluginConfig)}`;
)
.addOnUpdate(onlyOnMinIO);
});
void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, onlyOnCouchDB).then((paneEl) => {
void addPanel(paneEl, "Garbage Collection (Beta)", (e) => e, onlyOnP2POrCouchDB).then((paneEl) => {
new Setting(paneEl)
.setName("Remove all orphaned chunks")
.setDesc("Remove all orphaned chunks from the local database.")
@@ -3080,7 +3134,7 @@ ${stringifyYaml(pluginConfig)}`;
.addOnUpdate(onlyOnCouchDB);
});
void addPanel(paneEl, "Total Overhaul").then((paneEl) => {
void addPanel(paneEl, "Total Overhaul", () => {}, onlyOnCouchDBOrMinIO).then((paneEl) => {
new Setting(paneEl)
.setName("Rebuild everything")
.setDesc("Rebuild local and remote database with local files.")
@@ -3104,94 +3158,98 @@ ${stringifyYaml(pluginConfig)}`;
})
);
});
void addPanel(paneEl, "Rebuilding Operations (Remote Only)").then((paneEl) => {
new Setting(paneEl)
.setName("Perform cleanup")
.setDesc(
"Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."
)
.addButton((button) =>
button
.setButtonText("Perform")
.setDisabled(false)
.onClick(async () => {
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction");
if (await replicator.compactRemote(this.editingSettings)) {
Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction");
} else {
Logger(`Cleanup has been failed!`, LOG_LEVEL_NOTICE, "compaction");
}
})
)
.addOnUpdate(onlyOnCouchDB);
void addPanel(paneEl, "Rebuilding Operations (Remote Only)", () => {}, onlyOnCouchDBOrMinIO).then(
(paneEl) => {
new Setting(paneEl)
.setName("Perform cleanup")
.setDesc(
"Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."
)
.addButton((button) =>
button
.setButtonText("Perform")
.setDisabled(false)
.onClick(async () => {
const replicator = this.plugin.replicator as LiveSyncCouchDBReplicator;
Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction");
if (await replicator.compactRemote(this.editingSettings)) {
Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction");
} else {
Logger(`Cleanup has been failed!`, LOG_LEVEL_NOTICE, "compaction");
}
})
)
.addOnUpdate(onlyOnCouchDB);
new Setting(paneEl)
.setName("Overwrite remote")
.setDesc("Overwrite remote with local DB and passphrase.")
.addButton((button) =>
button
.setButtonText("Send")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("remoteOnly");
})
);
new Setting(paneEl)
.setName("Overwrite remote")
.setDesc("Overwrite remote with local DB and passphrase.")
.addButton((button) =>
button
.setButtonText("Send")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("remoteOnly");
})
);
new Setting(paneEl)
.setName("Reset all journal counter")
.setDesc("Initialise all journal history, On the next sync, every item will be received and sent.")
.addButton((button) =>
button
.setButtonText("Reset all")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().resetCheckpointInfo();
Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(onlyOnMinIO);
new Setting(paneEl)
.setName("Reset all journal counter")
.setDesc(
"Initialise all journal history, On the next sync, every item will be received and sent."
)
.addButton((button) =>
button
.setButtonText("Reset all")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().resetCheckpointInfo();
Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(onlyOnMinIO);
new Setting(paneEl)
.setName("Purge all journal counter")
.setDesc("Purge all download/upload cache.")
.addButton((button) =>
button
.setButtonText("Reset all")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().resetAllCaches();
Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(onlyOnMinIO);
new Setting(paneEl)
.setName("Purge all journal counter")
.setDesc("Purge all download/upload cache.")
.addButton((button) =>
button
.setButtonText("Reset all")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().resetAllCaches();
Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(onlyOnMinIO);
new Setting(paneEl)
.setName("Fresh Start Wipe")
.setDesc("Delete all data on the remote server.")
.addButton((button) =>
button
.setButtonText("Delete")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
...info,
receivedFiles: new Set(),
knownIDs: new Set(),
lastLocalSeq: 0,
sentIDs: new Set(),
sentFiles: new Set(),
}));
await this.resetRemoteBucket();
Logger(`Deleted all data on remote server`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(onlyOnMinIO);
});
new Setting(paneEl)
.setName("Fresh Start Wipe")
.setDesc("Delete all data on the remote server.")
.addButton((button) =>
button
.setButtonText("Delete")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
...info,
receivedFiles: new Set(),
knownIDs: new Set(),
lastLocalSeq: 0,
sentIDs: new Set(),
sentFiles: new Set(),
}));
await this.resetRemoteBucket();
Logger(`Deleted all data on remote server`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(onlyOnMinIO);
}
);
void addPanel(paneEl, "Reset").then((paneEl) => {
new Setting(paneEl)

View File

@@ -1,27 +0,0 @@
export interface Confirm {
askYesNo(message: string): Promise<"yes" | "no">;
askString(title: string, key: string, placeholder: string, isPassword?: boolean): Promise<string | false>;
askYesNoDialog(
message: string,
opt: { title?: string; defaultOption?: "Yes" | "No"; timeout?: number }
): Promise<"yes" | "no">;
askSelectString(message: string, items: string[]): Promise<string>;
askSelectStringDialogue(
message: string,
buttons: string[],
opt: { title?: string; defaultAction: (typeof buttons)[number]; timeout?: number }
): Promise<(typeof buttons)[number] | false>;
askInPopup(key: string, dialogText: string, anchorCallback: (anchor: HTMLAnchorElement) => void): void;
confirmWithMessage(
title: string,
contentMd: string,
buttons: string[],
defaultAction: (typeof buttons)[number],
timeout?: number
): Promise<(typeof buttons)[number] | false>;
}

View File

@@ -14,6 +14,7 @@ import { cancelAllPeriodicTask, cancelAllTasks } from "octagonal-wheels/concurre
import { stopAllRunningProcessors } from "octagonal-wheels/concurrency/processor";
import { AbstractModule } from "../AbstractModule.ts";
import type { ICoreModule } from "../ModuleTypes.ts";
import { EVENT_PLATFORM_UNLOADED } from "../../lib/src/PlatformAPIs/APIBase.ts";
export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
async $$onLiveSyncReady() {
@@ -139,6 +140,8 @@ export class ModuleLiveSyncMain extends AbstractModule implements ICoreModule {
}
await this.localDatabase.close();
}
eventHub.emitEvent(EVENT_PLATFORM_UNLOADED);
eventHub.offAll();
this._log($msg("moduleLiveSyncMain.logUnloadingPlugin"));
}