diff --git a/src/CmdConfigSync.ts b/src/CmdConfigSync.ts
index 9733e55..898aa2f 100644
--- a/src/CmdConfigSync.ts
+++ b/src/CmdConfigSync.ts
@@ -4,7 +4,7 @@ import { Notice, type PluginManifest, parseYaml } from "./deps";
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry } from "./lib/src/types";
import { LOG_LEVEL } from "./lib/src/types";
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
-import { Parallels, delay, getDocData } from "./lib/src/utils";
+import { delay, getDocData } from "./lib/src/utils";
import { Logger } from "./lib/src/logger";
import { WrappedNotice } from "./lib/src/wrapper";
import { base64ToArrayBuffer, arrayBufferToBase64, readString, crc32CKHash } from "./lib/src/strbin";
diff --git a/src/CmdSetupLiveSync.ts b/src/CmdSetupLiveSync.ts
index 8565a3b..610b364 100644
--- a/src/CmdSetupLiveSync.ts
+++ b/src/CmdSetupLiveSync.ts
@@ -8,6 +8,7 @@ import { LiveSyncCommands } from "./LiveSyncCommands";
import { delay } from "./lib/src/utils";
import { confirmWithMessage } from "./dialogs";
import { Platform } from "./deps";
+import { fetchAllUsedChunks } from "./lib/src/utils_couchdb";
export class SetupLiveSync extends LiveSyncCommands {
onunload() { }
@@ -284,6 +285,26 @@ Of course, we are able to disable these features.`
this.plugin.settings.syncAfterMerge = false;
//this.suspendExtraSync();
}
+ async suspendReflectingDatabase() {
+ if (this.plugin.settings.doNotSuspendOnFetching) return;
+ Logger(`Suspending reflection: Database and storage changes will not be reflected in each other until completely finished the fetching.`, LOG_LEVEL.NOTICE);
+ this.plugin.settings.suspendParseReplicationResult = true;
+ this.plugin.settings.suspendFileWatching = true;
+ await this.plugin.saveSettings();
+ }
+ async resumeReflectingDatabase() {
+ if (this.plugin.settings.doNotSuspendOnFetching) return;
+ Logger(`Database and storage reflection has been resumed!`, LOG_LEVEL.NOTICE);
+ this.plugin.settings.suspendParseReplicationResult = false;
+ this.plugin.settings.suspendFileWatching = false;
+ await this.plugin.saveSettings();
+ if (this.plugin.settings.readChunksOnline) {
+ await this.plugin.syncAllFiles(true);
+ await this.plugin.loadQueuedFiles();
+ // Start processing
+ this.plugin.procQueuedFiles();
+ }
+ }
async askUseNewAdapter() {
if (!this.plugin.settings.useIndexedDBAdapter) {
const message = `Now this plugin has been configured to use the old database adapter for keeping compatibility. Do you want to deactivate it?`;
@@ -297,9 +318,22 @@ Of course, we are able to disable these features.`
}
}
}
+ async fetchRemoteChunks() {
+ if (!this.plugin.settings.doNotSuspendOnFetching && this.plugin.settings.readChunksOnline) {
+ Logger(`Fetching chunks`, LOG_LEVEL.NOTICE);
+ const remoteDB = await this.plugin.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.plugin.getIsMobile(), true);
+ if (typeof remoteDB == "string") {
+ Logger(remoteDB, LOG_LEVEL.NOTICE);
+ } else {
+ await fetchAllUsedChunks(this.localDatabase.localDatabase, remoteDB.db);
+ }
+ Logger(`Fetching chunks done`, LOG_LEVEL.NOTICE);
+ }
+ }
async fetchLocal() {
this.suspendExtraSync();
this.askUseNewAdapter();
+ await this.suspendReflectingDatabase();
await this.plugin.realizeSettingSyncMode();
await this.plugin.resetLocalDatabase();
await delay(1000);
@@ -310,6 +344,8 @@ Of course, we are able to disable these features.`
await this.plugin.replicateAllFromServer(true);
await delay(1000);
await this.plugin.replicateAllFromServer(true);
+ await this.fetchRemoteChunks();
+ await this.resumeReflectingDatabase();
await this.askHiddenFileConfiguration({ enableFetch: true });
}
async rebuildRemote() {
diff --git a/src/ConflictResolveModal.ts b/src/ConflictResolveModal.ts
index ca56eb3..c7ba9be 100644
--- a/src/ConflictResolveModal.ts
+++ b/src/ConflictResolveModal.ts
@@ -30,11 +30,11 @@ export class ConflictResolveModal extends Modal {
const x1 = v[0];
const x2 = v[1];
if (x1 == DIFF_DELETE) {
- diff += "" + escapeStringToHTML(x2) + "";
+ diff += "" + escapeStringToHTML(x2).replace(/\n/g, "\n") + "";
} else if (x1 == DIFF_EQUAL) {
- diff += "" + escapeStringToHTML(x2) + "";
+ diff += "" + escapeStringToHTML(x2).replace(/\n/g, "\n") + "";
} else if (x1 == DIFF_INSERT) {
- diff += "" + escapeStringToHTML(x2) + "";
+ diff += "" + escapeStringToHTML(x2).replace(/\n/g, "\n") + "";
}
}
@@ -48,23 +48,26 @@ export class ConflictResolveModal extends Modal {
`;
contentEl.createEl("button", { text: "Keep A" }, (e) => {
e.addEventListener("click", async () => {
- await this.callback(this.result.right.rev);
+ const callback = this.callback;
this.callback = null;
this.close();
+ await callback(this.result.right.rev);
});
});
contentEl.createEl("button", { text: "Keep B" }, (e) => {
e.addEventListener("click", async () => {
- await this.callback(this.result.left.rev);
+ const callback = this.callback;
this.callback = null;
this.close();
+ await callback(this.result.left.rev);
});
});
contentEl.createEl("button", { text: "Concat both" }, (e) => {
e.addEventListener("click", async () => {
- await this.callback("");
+ const callback = this.callback;
this.callback = null;
this.close();
+ await callback("");
});
});
contentEl.createEl("button", { text: "Not now" }, (e) => {
diff --git a/src/ObsidianLiveSyncSettingTab.ts b/src/ObsidianLiveSyncSettingTab.ts
index 72c5e01..8f67c5e 100644
--- a/src/ObsidianLiveSyncSettingTab.ts
+++ b/src/ObsidianLiveSyncSettingTab.ts
@@ -7,7 +7,7 @@ import { Logger } from "./lib/src/logger";
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
import { testCrypt } from "./lib/src/e2ee_v2";
import ObsidianLiveSyncPlugin from "./main";
-import { performRebuildDB, requestToCouchDB } from "./utils";
+import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
@@ -1610,6 +1610,27 @@ ${stringifyYaml(pluginConfig)}`;
toggle.setValue(this.plugin.settings.suspendFileWatching).onChange(async (value) => {
this.plugin.settings.suspendFileWatching = value;
await this.plugin.saveSettings();
+ scheduleTask("configReload", 250, async () => {
+ if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
+ // @ts-ignore
+ this.app.commands.executeCommandById("app:reload")
+ }
+ })
+ })
+ );
+ new Setting(containerHatchEl)
+ .setName("Suspend database reflecting")
+ .setDesc("Stop reflecting database changes to storage files.")
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.suspendParseReplicationResult).onChange(async (value) => {
+ this.plugin.settings.suspendParseReplicationResult = value;
+ await this.plugin.saveSettings();
+ scheduleTask("configReload", 250, async () => {
+ if (await askYesNo(this.app, "Do you want to restart and reload Obsidian now?") == "yes") {
+ // @ts-ignore
+ this.app.commands.executeCommandById("app:reload")
+ }
+ })
})
);
new Setting(containerHatchEl)
@@ -1731,6 +1752,16 @@ ${stringifyYaml(pluginConfig)}`;
)
.setClass("wizardHidden");
+
+ new Setting(containerHatchEl)
+ .setName("Fetch database with previous behaviour")
+ .setDesc("")
+ .addToggle((toggle) =>
+ toggle.setValue(this.plugin.settings.doNotSuspendOnFetching).onChange(async (value) => {
+ this.plugin.settings.doNotSuspendOnFetching = value;
+ await this.plugin.saveSettings();
+ })
+ );
addScreenElement("50", containerHatchEl);
diff --git a/src/dialogs.ts b/src/dialogs.ts
index 3a893dd..76a2877 100644
--- a/src/dialogs.ts
+++ b/src/dialogs.ts
@@ -183,7 +183,7 @@ export class MessageBox extends Modal {
})
contentEl.createEl("h1", { text: this.title });
const div = contentEl.createDiv();
- MarkdownRenderer.renderMarkdown(this.contentMd, div, "/", null);
+ MarkdownRenderer.renderMarkdown(this.contentMd, div, "/", this.plugin);
const buttonSetting = new Setting(contentEl);
for (const button of this.buttons) {
buttonSetting.addButton((btn) => {
diff --git a/src/lib b/src/lib
index 642efef..ca61c5a 160000
--- a/src/lib
+++ b/src/lib
@@ -1 +1 @@
-Subproject commit 642efefaf15759cae382fa284a79101b1b5c90a4
+Subproject commit ca61c5a64b5cec3955062cc667722eb901385ea6
diff --git a/src/main.ts b/src/main.ts
index 1d9d6bb..0d905e9 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -18,7 +18,7 @@ import { lockStore, logMessageStore, logStore, type LogEntry } from "./lib/src/s
import { setNoticeClass } from "./lib/src/wrapper";
import { base64ToString, versionNumberString2Number, base64ToArrayBuffer, arrayBufferToBase64 } from "./lib/src/strbin";
import { addPrefix, isPlainText, shouldBeIgnored, stripAllPrefixes } from "./lib/src/path";
-import { runWithLock } from "./lib/src/lock";
+import { isLockAcquired, runWithLock } from "./lib/src/lock";
import { Semaphore } from "./lib/src/semaphore";
import { StorageEventManager, StorageEventManagerObsidian } from "./StorageEventManager";
import { LiveSyncLocalDB, type LiveSyncLocalDBEnv } from "./lib/src/LiveSyncLocalDB";
@@ -240,6 +240,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
createPouchDBInstance(name?: string, options?: PouchDB.Configuration.DatabaseConfiguration): PouchDB.Database {
if (this.settings.useIndexedDBAdapter) {
options.adapter = "indexeddb";
+ //@ts-ignore :missing def
options.purged_infos_limit = 1;
return new PouchDB(name + "-indexeddb", options);
}
@@ -421,11 +422,13 @@ export default class ObsidianLiveSyncPlugin extends Plugin
Logger(`${FLAGMD_REDFLAG3} has been detected! Self-hosted LiveSync will discard the local database and fetch everything from the remote once again.`, LOG_LEVEL.NOTICE);
await this.addOnSetup.fetchLocal();
await this.deleteRedFlag3();
- if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
- this.settings.suspendFileWatching = false;
- await this.saveSettings();
- // @ts-ignore
- this.app.commands.executeCommandById("app:reload")
+ if (this.settings.suspendFileWatching) {
+ if (await askYesNo(this.app, "Do you want to disable Suspend file watching and restart obsidian now?") == "yes") {
+ this.settings.suspendFileWatching = false;
+ await this.saveSettings();
+ // @ts-ignore
+ this.app.commands.executeCommandById("app:reload")
+ }
}
} else {
this.settings.writeLogToTheFile = true;
@@ -438,6 +441,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (this.settings.suspendFileWatching) {
Logger("'Suspend file watching' turned on. Are you sure this is what you intended? Every modification on the vault will be ignored.", LOG_LEVEL.NOTICE);
}
+ if (this.settings.suspendParseReplicationResult) {
+ Logger("'Suspend database reflecting' turned on. Are you sure this is what you intended? Every replicated change will be postponed until disabling this option.", LOG_LEVEL.NOTICE);
+ }
const isInitialized = await this.initializeDatabase(false, false);
if (!isInitialized) {
//TODO:stop all sync.
@@ -1317,12 +1323,14 @@ export default class ObsidianLiveSyncPlugin extends Plugin
localStorage.setItem(lsKey, saveData);
}
async loadQueuedFiles() {
- const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
- const ids = JSON.parse(localStorage.getItem(lsKey) || "[]") as string[];
- const ret = await this.localDatabase.allDocsRaw({ keys: ids, include_docs: true });
- for (const doc of ret.rows) {
- if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
- await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument);
+ if (!this.settings.suspendParseReplicationResult) {
+ const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
+ const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
+ const ret = await this.localDatabase.allDocsRaw({ keys: ids, include_docs: true });
+ for (const doc of ret.rows) {
+ if (doc.doc && !this.queuedFiles.some((e) => e.entry._id == doc.doc._id)) {
+ await this.parseIncomingDoc(doc.doc as PouchDB.Core.ExistingDocument);
+ }
}
}
}
@@ -1384,6 +1392,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
// It is better for your own safety, not to handle the following files
const ignoreFiles = [
"_design/replicate",
+ "_design/chunks",
FLAGMD_REDFLAG,
FLAGMD_REDFLAG2,
FLAGMD_REDFLAG3
@@ -1432,21 +1441,41 @@ export default class ObsidianLiveSyncPlugin extends Plugin
L1:
for (const change of docsSorted) {
if (isChunk(change._id)) {
- await this.parseIncomingChunk(change);
+ if (!this.settings.suspendParseReplicationResult) {
+ await this.parseIncomingChunk(change);
+ }
continue;
}
- for (const proc of this.addOns) {
- if (await proc.parseReplicationResultItem(change)) {
- continue L1;
+ if (!this.settings.suspendParseReplicationResult) {
+ for (const proc of this.addOns) {
+ if (await proc.parseReplicationResultItem(change)) {
+ continue L1;
+ }
}
}
if (change._id == SYNCINFO_ID) {
continue;
}
- if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
- await this.parseIncomingDoc(change);
+ if (change._id.startsWith("_design")) {
continue;
}
+
+ if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
+ if (this.settings.suspendParseReplicationResult) {
+ const newQueue = {
+ entry: change,
+ missingChildren: [] as string[],
+ timeout: 0,
+ };
+ Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL.INFO);
+ this.queuedFiles.push(newQueue);
+ this.saveQueuedFiles();
+ continue;
+ } else {
+ await this.parseIncomingDoc(change);
+ continue;
+ }
+ }
if (change.type == "versioninfo") {
if (change.version > VER) {
this.replicator.closeReplication();
@@ -1589,8 +1618,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin
async replicate(showMessage?: boolean) {
if (!this.isReady) return;
+ if (isLockAcquired("cleanup")) {
+ Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL.NOTICE);
+ return;
+ }
if (this.settings.versionUpFlash != "") {
- Logger("Open settings and check message, please.", LOG_LEVEL.NOTICE);
+ Logger("Open settings and check message, please. replication has been cancelled.", LOG_LEVEL.NOTICE);
return;
}
await this.applyBatchChange();
@@ -1600,19 +1633,41 @@ export default class ObsidianLiveSyncPlugin extends Plugin
if (!ret) {
if (this.replicator.remoteLockedAndDeviceNotAccepted) {
if (this.replicator.remoteCleaned) {
- Logger(`The remote database has been cleaned up. The local database of this device also should be done.`, showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
- const remoteDB = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
- if (typeof remoteDB == "string") {
- Logger(remoteDB, LOG_LEVEL.NOTICE);
- return false;
- }
- // TODO Check actually sent.
+ Logger(`The remote database has been cleaned.`, showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO);
await runWithLock("cleanup", true, async () => {
- await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
- await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
- await this.getReplicator().markRemoteResolved(this.settings);
+ const count = await purgeUnreferencedChunks(this.localDatabase.localDatabase, true);
+ const message = `The remote database has been cleaned up.
+To synchronize, this device must be also cleaned up. ${count} chunk(s) will be erased from this device.
+However, If there are many chunks to be deleted, maybe fetching again is faster.
+We will lose the history of this device if we fetch the remote database again.
+Even if you choose to clean up, you will see this option again if you exit Obsidian and then synchronise again.`
+ const CHOICE_FETCH = "Fetch again";
+ const CHOICE_CLEAN = "Cleanup";
+ const CHOICE_DISMISS = "Dismiss";
+ const ret = await confirmWithMessage(this, "Cleaned", message, [CHOICE_FETCH, CHOICE_CLEAN, CHOICE_DISMISS], CHOICE_DISMISS, 30);
+ if (ret == CHOICE_FETCH) {
+ await performRebuildDB(this, "localOnly");
+ }
+ if (ret == CHOICE_CLEAN) {
+ const remoteDB = await this.getReplicator().connectRemoteCouchDBWithSetting(this.settings, this.getIsMobile(), true);
+ if (typeof remoteDB == "string") {
+ Logger(remoteDB, LOG_LEVEL.NOTICE);
+ return false;
+ }
+
+ await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
+ // Perform the synchronisation once.
+ if (await this.replicator.openReplication(this.settings, false, showMessage, true)) {
+ await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
+ await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
+ await this.getReplicator().markRemoteResolved(this.settings);
+ Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO)
+ } else {
+ Logger("Replication has been cancelled. Please try it again.", showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO)
+ }
+
+ }
});
- Logger("The local database has been cleaned up.", showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO)
} else {
const message = `
The remote database has been rebuilt.
@@ -2164,7 +2219,7 @@ Or if you are sure know what had been happened, we can unlock the database from
setTimeout(() => {
//resolved, check again.
this.showIfConflicted(filename);
- }, 500);
+ }, 50);
} else if (toDelete == null) {
Logger("Leave it still conflicted");
} else {
@@ -2177,7 +2232,7 @@ Or if you are sure know what had been happened, we can unlock the database from
setTimeout(() => {
//resolved, check again.
this.showIfConflicted(filename);
- }, 500);
+ }, 50);
}
return res(true);
@@ -2221,7 +2276,7 @@ Or if you are sure know what had been happened, we can unlock the database from
Logger("conflict:Automatically merged, but we have to check it again");
setTimeout(() => {
this.showIfConflicted(filename);
- }, 500);
+ }, 50);
return;
}
//there conflicts, and have to resolve ;
diff --git a/src/utils.ts b/src/utils.ts
index 8e102cb..c10bf69 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -430,11 +430,12 @@ export class PeriodicProcessor {
enable(interval: number) {
this.disable();
if (interval == 0) return;
- this._timer = window.setInterval(() => this._process().then(() => { }), interval);
+ this._timer = window.setInterval(() => this.process().then(() => { }), interval);
this._plugin.registerInterval(this._timer);
}
disable() {
- if (this._timer) clearInterval(this._timer);
+ if (this._timer !== undefined) window.clearInterval(this._timer);
+ this._timer = undefined;
}
}
diff --git a/styles.css b/styles.css
index 094f02f..96fe0b9 100644
--- a/styles.css
+++ b/styles.css
@@ -260,3 +260,14 @@ div.sls-setting-menu-btn {
.password-input > .setting-item-control >input {
-webkit-text-security: disc;
}
+
+span.ls-mark-cr::after {
+ user-select: none;
+ content: "↲";
+ color: var(--text-muted);
+ font-size: 0.8em;
+}
+
+.deleted span.ls-mark-cr::after {
+ color: var(--text-on-accent);
+}
\ No newline at end of file