Compare commits

...

8 Commits

Author SHA1 Message Date
vorotamoroz
dcfb9867f2 Fixed:
- Rewritten lock acquiring logic.
- Fixed plugin dialog's message.
- Fixed some error messages.
- Fixed action on replicating non-note entries.
2022-06-14 19:01:31 +09:00
vorotamoroz
46ff17fdf3 New Feature:
- Skip conflicted check while replication

Fixed:
- Rewrited replication reflection algorithm.
2022-06-13 17:36:26 +09:00
vorotamoroz
728dabce60 Fixed repo issue. 2022-06-10 19:04:11 +09:00
vorotamoroz
3783fc6926 Implemented:
- Exporting settings and setup from uri.

Fixed:
- Change "Leaf" into "Chunk"
- Reduced meaninglessly verbose logging
- Trimmed deadcode.
2022-06-10 18:48:04 +09:00
vorotamoroz
236f2293ce Remove notice. 2022-06-10 01:28:41 +09:00
vorotamoroz
4cb908cf62 Fixed migration problem. 2022-06-10 01:26:55 +09:00
vorotamoroz
fab2327937 fix a typo. 2022-06-09 18:22:38 +09:00
vorotamoroz
0837648aa6 Add a note 2022-06-09 18:20:51 +09:00
10 changed files with 374 additions and 100 deletions

1
lib

Submodule lib deleted from 315ef99845

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "0.10.0",
"version": "0.11.2",
"minAppVersion": "0.9.12",
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"author": "vorotamoroz",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "0.10.0",
"version": "0.11.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "0.10.0",
"version": "0.11.2",
"license": "MIT",
"dependencies": {
"diff-match-patch": "^1.0.5",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "0.10.0",
"version": "0.11.2",
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
"main": "main.js",
"type": "module",

View File

@@ -16,7 +16,6 @@ import {
MAX_DOC_SIZE,
MAX_DOC_SIZE_BIN,
NODEINFO_DOCID,
RECENT_MOFIDIED_DOCS_QTY,
VER,
MILSTONE_DOCID,
DatabaseConnectingStatus,
@@ -142,23 +141,10 @@ export class LocalPouchDB {
}
}
updateRecentModifiedDocs(id: string, rev: string, deleted: boolean) {
const idrev = id + rev;
if (deleted) {
this.recentModifiedDocs = this.recentModifiedDocs.filter((e) => e != idrev);
} else {
this.recentModifiedDocs.push(idrev);
this.recentModifiedDocs = this.recentModifiedDocs.slice(0 - RECENT_MOFIDIED_DOCS_QTY);
}
}
isSelfModified(id: string, rev: string): boolean {
const idrev = id + rev;
return this.recentModifiedDocs.indexOf(idrev) !== -1;
}
async isOldDatabaseExists() {
const db = new PouchDB<EntryDoc>(this.dbname + "-livesync", {
auto_compaction: this.settings.useHistory ? false : true,
revs_limit: 100,
revs_limit: 20,
deterministic_revs: true,
skip_setup: true,
});
@@ -253,9 +239,9 @@ export class LocalPouchDB {
Logger(newDbStatus);
if (this.settings.encrypt) {
enableEncryption(old, this.settings.passphrase);
enableEncryption(old, this.settings.passphrase, true);
}
const rep = old.replicate.to(this.localDatabase);
const rep = old.replicate.to(this.localDatabase, { batch_size: 25, batches_limit: 10 });
rep.on("change", (e) => {
progress.setMessage(`Converting ${e.docs_written} docs...`);
Logger(`Converting ${e.docs_written} docs...`, LOG_LEVEL.VERBOSE);
@@ -267,7 +253,7 @@ export class LocalPouchDB {
Logger("Conversion completed!", LOG_LEVEL.NOTICE);
old.destroy(); // delete the old database.
this.isReady = true;
return nextSeq();
return await nextSeq();
} else {
throw new Error("Conversion failed!");
}
@@ -279,7 +265,7 @@ export class LocalPouchDB {
return false;
}
} else {
return nextSeq();
return await nextSeq();
}
}
@@ -305,7 +291,7 @@ export class LocalPouchDB {
waitForLeafReady(id: string): Promise<boolean> {
return new Promise((res, rej) => {
// Set timeout.
const timer = setTimeout(() => rej(new Error(`Leaf timed out:${id}`)), LEAF_WAIT_TIMEOUT);
const timer = setTimeout(() => rej(new Error(`Chunk reading timed out:${id}`)), LEAF_WAIT_TIMEOUT);
if (typeof this.leafArrivedCallbacks[id] == "undefined") {
this.leafArrivedCallbacks[id] = [];
}
@@ -329,21 +315,21 @@ export class LocalPouchDB {
this.hashCaches.set(id, w.data);
return w.data;
}
throw new Error(`retrive leaf, but it was not leaf.`);
throw new Error(`Corrupted chunk detected: ${id}`);
} catch (ex) {
if (ex.status && ex.status == 404) {
if (waitForReady) {
// just leaf is not ready.
// wait for on
if ((await this.waitForLeafReady(id)) === false) {
throw new Error(`time out (waiting leaf)`);
throw new Error(`time out (waiting chunk)`);
}
return this.getDBLeaf(id, false);
} else {
throw new Error("Leaf was not found");
throw new Error(`Chunk was not found: ${id}`);
}
} else {
Logger(`Something went wrong on retriving leaf`);
Logger(`Something went wrong on retriving chunk`);
throw ex;
}
}
@@ -447,11 +433,11 @@ export class LocalPouchDB {
try {
childrens = await Promise.all(obj.children.map((e) => this.getDBLeaf(e, waitForReady)));
if (dump) {
Logger(`childrens:`);
Logger(`Chunks:`);
Logger(childrens);
}
} catch (ex) {
Logger(`Something went wrong on reading elements of ${obj._id} from database:`, LOG_LEVEL.NOTICE);
Logger(`Something went wrong on reading chunks of ${obj._id} from database, see verbose info for detail.`, LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.VERBOSE);
this.corruptedEntries[obj._id] = obj;
return false;
@@ -515,7 +501,7 @@ export class LocalPouchDB {
if (!obj.type || (obj.type && obj.type == "notes")) {
obj._deleted = true;
const r = await this.localDatabase.put(obj);
this.updateRecentModifiedDocs(r.id, r.rev, true);
Logger(`entry removed:${obj._id}-${r.rev}`);
if (typeof this.corruptedEntries[obj._id] != "undefined") {
delete this.corruptedEntries[obj._id];
}
@@ -526,7 +512,6 @@ export class LocalPouchDB {
obj._deleted = true;
const r = await this.localDatabase.put(obj);
Logger(`entry removed:${obj._id}-${r.rev}`);
this.updateRecentModifiedDocs(r.id, r.rev, true);
if (typeof this.corruptedEntries[obj._id] != "undefined") {
delete this.corruptedEntries[obj._id];
}
@@ -579,7 +564,6 @@ export class LocalPouchDB {
const item = await this.localDatabase.get(v);
item._deleted = true;
await this.localDatabase.put(item);
this.updateRecentModifiedDocs(item._id, item._rev, true);
});
deleteCount++;
@@ -702,21 +686,21 @@ export class LocalPouchDB {
try {
const result = await this.localDatabase.bulkDocs(newLeafs);
for (const item of result) {
if ((item as any).ok) {
this.updateRecentModifiedDocs(item.id, item.rev, false);
Logger(`save ok:id:${item.id} rev:${item.rev}`, LOG_LEVEL.VERBOSE);
} else {
if (!(item as any).ok) {
if ((item as any).status && (item as any).status == 409) {
// conflicted, but it would be ok in childrens.
} else {
Logger(`save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE);
Logger(`Save failed:id:${item.id} rev:${item.rev}`, LOG_LEVEL.NOTICE);
Logger(item);
saved = false;
}
}
}
if (saved) {
Logger(`Chunk saved:${newLeafs.length} chunks`);
}
} catch (ex) {
Logger("ERROR ON SAVING LEAVES:", LOG_LEVEL.NOTICE);
Logger("Chunk save failed:", LOG_LEVEL.NOTICE);
Logger(ex, LOG_LEVEL.NOTICE);
saved = false;
}
@@ -748,7 +732,6 @@ export class LocalPouchDB {
}
}
const r = await this.localDatabase.put(newDoc, { force: true });
this.updateRecentModifiedDocs(r.id, r.rev, newDoc._deleted);
if (typeof this.corruptedEntries[note._id] != "undefined") {
delete this.corruptedEntries[note._id];
}

View File

@@ -710,6 +710,24 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
})
);
new Setting(containerSyncSettingEl)
.setName("Skip old files on sync")
.setDesc("Skip old incoming if incoming changes older than storage.")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.skipOlderFilesOnSync).onChange(async (value) => {
this.plugin.settings.skipOlderFilesOnSync = value;
await this.plugin.saveSettings();
})
);
new Setting(containerSyncSettingEl)
.setName("Check conflict only on opening file.")
.setDesc("Do not check conflict while replication")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
this.plugin.settings.checkConflictOnlyOnOpen = value;
await this.plugin.saveSettings();
})
);
containerSyncSettingEl.createEl("div", {
text: sanitizeHTMLToDom(`Advanced settings<br>
If you reached the payload size limit when using IBM Cloudant, please set batch size and batch limit to a lower value.`),
@@ -1119,8 +1137,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
const containerCorruptedDataEl = containerEl.createDiv();
containerCorruptedDataEl.createEl("h3", { text: "Corrupted data" });
containerCorruptedDataEl.createEl("h3", { text: "Corrupted or missing data" });
containerCorruptedDataEl.createEl("h4", { text: "Corrupted" });
if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
const cx = containerCorruptedDataEl.createEl("div", { text: "If you have copy of these items on any device, simply edit once or twice. Or not, delete this. sorry.." });
for (const k in this.plugin.localDatabase.corruptedEntries) {
@@ -1149,6 +1167,38 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
} else {
containerCorruptedDataEl.createEl("div", { text: "There is no corrupted data." });
}
containerCorruptedDataEl.createEl("h4", { text: "Missing or waiting" });
if (Object.keys(this.plugin.queuedFiles).length > 0) {
const cx = containerCorruptedDataEl.createEl("div", {
text: "These files have missing or waiting chunks. Perhaps almost chunks will be found in a while after replication. But if there're no chunk, you have to restore database entry from existed file by hitting the button below.",
});
const files = [...new Set([...this.plugin.queuedFiles.map((e) => e.entry._id)])];
for (const k of files) {
const xx = cx.createEl("div", { text: `${id2path(k)}` });
const ba = xx.createEl("button", { text: `Delete this` }, (e) => {
e.addEventListener("click", async () => {
await this.plugin.localDatabase.deleteDBEntry(k);
xx.remove();
});
});
ba.addClass("mod-warning");
xx.createEl("button", { text: `Restore from file` }, (e) => {
e.addEventListener("click", async () => {
const f = await this.app.vault.getFiles().filter((e) => path2id(e.path) == k);
if (f.length == 0) {
Logger("Not found in vault", LOG_LEVEL.NOTICE);
return;
}
await this.plugin.updateIntoDB(f[0]);
xx.remove();
});
});
xx.addClass("mod-warning");
}
} else {
containerCorruptedDataEl.createEl("div", { text: "There is no missing or waiting chunk." });
}
applyDisplayEnabled();
addScreenElement("70", containerCorruptedDataEl);
changeDisplay("0");

View File

@@ -16,7 +16,7 @@
export let plugin: ObsidianLiveSyncPlugin;
let plugins: PluginDataEntry[] = [];
let deviceAndPlugins: { [key: string]: PluginDataEntryDisp[] } = {};
let devicePluginList: [string, PluginDataEntryDisp[]][] = [];
let devicePluginList: [string, PluginDataEntryDisp[]][] = null;
let ownPlugins: DevicePluginList = null;
let showOwnPlugins = false;
let targetList: { [key: string]: boolean } = {};
@@ -205,58 +205,63 @@
<th class="sls-plugins-tbl-device-head">Info</th>
<th class="sls-plugins-tbl-device-head">Target</th>
</tr>
{#if devicePluginList.length == 0}
{#if !devicePluginList}
<tr>
<td colspan="3" class="sls-table-tail tcenter"> Retrieving... </td>
</tr>
{/if}
{#each devicePluginList as [deviceName, devicePlugins]}
{:else if devicePluginList.length == 0}
<tr>
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
<th class="sls-plugins-tbl-device-head">
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
</th>
<td colspan="3" class="sls-table-tail tcenter"> No plugins found. </td>
</tr>
{#each devicePlugins as plugin}
{:else}
{#each devicePluginList as [deviceName, devicePlugins]}
<tr>
<td class="sls-table-head">{plugin.manifest.name}</td>
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
/>
</div>
{/if}
</td>
</tr>
<tr>
<td class="sls-table-head">Settings</td>
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
/>
</div>
{/if}
</td>
</tr>
<tr class="divider">
<th colspan="3" />
<th colspan="2" class="sls-plugins-tbl-device-head">{deviceName}</th>
<th class="sls-plugins-tbl-device-head">
<button class="mod-cta" on:click={() => toggleAll(deviceName)}>✔</button>
</th>
</tr>
{#each devicePlugins as plugin}
<tr>
<td class="sls-table-head">{plugin.manifest.name}</td>
<td class="sls-table-tail tcenter">{plugin.versionInfo}{getDispString(plugin.versionFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.versionFlag === "EVEN" || plugin.versionFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---plugin")}
/>
</div>
{/if}
</td>
</tr>
<tr>
<td class="sls-table-head">Settings</td>
<td class="sls-table-tail tcenter">{plugin.mtimeInfo}{getDispString(plugin.mtimeFlag)}</td>
<td class="sls-table-tail tcenter">
{#if plugin.mtimeFlag === "EVEN" || plugin.mtimeFlag === ""}
-
{:else}
<div class="wrapToggle">
<div
class="checkbox-container"
class:is-enabled={targetList[plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting"]}
on:click={() => toggleTarget(plugin.deviceVaultName + "---" + plugin.manifest.id + "---setting")}
/>
</div>
{/if}
</td>
</tr>
<tr class="divider">
<th colspan="3" />
</tr>
{/each}
{/each}
{/each}
{/if}
</table>
</div>
<div class="ols-plugins-div-buttons">

Submodule src/lib updated: 92c1bbc45d...6451afd112

View File

@@ -1,4 +1,4 @@
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App } from "obsidian";
import { debounce, Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, PluginManifest, Modal, App, FuzzySuggestModal } from "obsidian";
import { diff_match_patch } from "diff-match-patch";
import { EntryDoc, LoadedEntry, ObsidianLiveSyncSettings, diff_check_result, diff_result_leaf, EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, diff_result, FLAGMD_REDFLAG, SYNCINFO_ID } from "./lib/src/types";
@@ -28,6 +28,7 @@ import { DocumentHistoryModal } from "./DocumentHistoryModal";
import PluginPane from "./PluginPane.svelte";
import { id2path, path2id } from "./utils";
import { decrypt, encrypt } from "./lib/src/e2ee";
const isDebug = false;
setNoticeClass(Notice);
class PluginDialogModal extends Modal {
@@ -57,6 +58,45 @@ class PluginDialogModal extends Modal {
}
}
}
class PopoverYesNo extends FuzzySuggestModal<string> {
app: App;
callback: (e: string) => void = () => {};
constructor(app: App, note: string, callback: (e: string) => void) {
super(app);
this.app = app;
this.setPlaceholder("y/n) " + note);
this.callback = callback;
}
getItems(): string[] {
return ["yes", "no"];
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
// debugger;
this.callback(item);
this.callback = null;
}
onClose(): void {
setTimeout(() => {
if (this.callback != null) {
this.callback("");
}
}, 100);
}
}
const askYesNo = (app: App, message: string): Promise<"yes" | "no"> => {
return new Promise((res) => {
const popover = new PopoverYesNo(app, message, (result) => res(result as "yes" | "no"));
popover.open();
});
};
export default class ObsidianLiveSyncPlugin extends Plugin {
settings: ObsidianLiveSyncSettings;
@@ -200,6 +240,81 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
Logger(ex, LOG_LEVEL.VERBOSE);
}
});
this.addCommand({
id: "livesync-exportconfig",
name: "Copy setup uri (beta)",
callback: async () => {
const encryptedSetting = encodeURIComponent(await encrypt(JSON.stringify(this.settings), "---"));
const uri = `obsidian://setuplivesync?settings=${encryptedSetting}`;
await navigator.clipboard.writeText(uri);
Logger("Setup uri copied to clipboard", LOG_LEVEL.NOTICE);
},
});
this.registerObsidianProtocolHandler("setuplivesync", async (conf: any) => {
try {
const oldConf = JSON.parse(JSON.stringify(this.settings));
const newconf = await JSON.parse(await decrypt(conf.settings, "---"));
if (newconf) {
const result = await askYesNo(this.app, "Importing LiveSync's conf, OK?");
if (result == "yes") {
const newSettingW = Object.assign({}, this.settings, newconf);
// stopping once.
this.localDatabase.closeReplication();
this.settings.suspendFileWatching = true;
console.dir(newSettingW);
const keepLocalDB = await askYesNo(this.app, "Keep local DB?");
const keepRemoteDB = await askYesNo(this.app, "Keep remote DB?");
if (keepLocalDB == "yes" && keepRemoteDB == "yes") {
// nothing to do. so peaceful.
this.settings = newSettingW;
await this.saveSettings();
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
return;
}
if (keepLocalDB == "no" && keepRemoteDB == "no") {
const reset = await askYesNo(this.app, "Drop everything?");
if (reset != "yes") {
Logger("Cancelled", LOG_LEVEL.NOTICE);
this.settings = oldConf;
return;
}
}
let initDB;
await this.saveSettings();
if (keepLocalDB == "no") {
this.resetLocalOldDatabase();
this.resetLocalDatabase();
this.localDatabase.initializeDatabase();
const rebuild = await askYesNo(this.app, "Rebuild the database?");
if (rebuild == "yes") {
initDB = this.initializeDatabase(true);
} else {
this.markRemoteResolved();
}
}
if (keepRemoteDB == "no") {
await this.tryResetRemoteDatabase();
await this.markRemoteLocked();
}
if (keepLocalDB == "no" || keepRemoteDB == "no") {
const replicate = await askYesNo(this.app, "Replicate once?");
if (replicate == "yes") {
if (initDB != null) {
await initDB;
}
await this.replicate(true);
}
}
}
Logger("Configuration loaded.", LOG_LEVEL.NOTICE);
} else {
Logger("Cancelled.", LOG_LEVEL.NOTICE);
}
} catch (ex) {
Logger("Couldn't parse configuration uri.", LOG_LEVEL.NOTICE);
}
});
this.addCommand({
id: "livesync-replicate",
name: "Replicate now",
@@ -587,8 +702,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//--> Basic document Functions
notifies: { [key: string]: { notice: Notice; timer: NodeJS.Timeout; count: number } } = {};
// eslint-disable-next-line require-await
lastLog = "";
// eslint-disable-next-line require-await
async addLog(message: any, level: LOG_LEVEL = LOG_LEVEL.INFO) {
if (level == LOG_LEVEL.DEBUG && !isDebug) {
return;
@@ -607,9 +722,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.logMessage = [].concat(this.logMessage).concat([newmessage]).slice(-100);
console.log(valutName + ":" + newmessage);
this.setStatusBarText(null, messagecontent.substring(0, 30));
if (message instanceof Error) {
console.trace(message);
}
// if (message instanceof Error) {
// console.trace(message);
// }
if (level >= LOG_LEVEL.NOTICE) {
if (messagecontent in this.notifies) {
@@ -808,12 +923,120 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
const doc = change;
const file = targetFile;
await this.doc2storage_modify(doc, file);
this.queueConflictedCheck(file);
if (!this.settings.checkConflictOnlyOnOpen) {
this.queueConflictedCheck(file);
} else {
const af = app.workspace.getActiveFile();
if (af && af.path == file.path) {
this.queueConflictedCheck(file);
}
}
} else {
Logger(`${id2path(change._id)} is already exist as the folder`);
}
}
queuedFiles: {
entry: EntryBody;
missingChildren: string[];
timeout?: number;
done?: boolean;
warned?: boolean;
}[] = [];
chunkWaitTimeout = 60000;
async saveQueuedFiles() {
const saveData = JSON.stringify(this.queuedFiles.filter((e) => !e.done).map((e) => e.entry._id));
const lsname = "obsidian-livesync-queuefiles-" + this.app.vault.getName();
localStorage.setItem(lsname, saveData);
}
async loadQueuedFiles() {
const lsname = "obsidian-livesync-queuefiles-" + this.app.vault.getName();
const ids = JSON.parse(localStorage.getItem(lsname) || "[]") as string[];
const ret = await this.localDatabase.localDatabase.allDocs({ 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<EntryBody & PouchDB.Core.AllDocsMeta>);
}
}
}
async procQueuedFiles() {
await runWithLock("procQueue", true, async () => {
this.saveQueuedFiles();
for (const queue of this.queuedFiles) {
if (queue.done) continue;
const now = new Date().getTime();
if (queue.missingChildren.length == 0) {
queue.done = true;
if (isValidPath(id2path(queue.entry._id))) {
Logger(`Applying ${queue.entry._id} (${queue.entry._rev}) change...`);
await this.handleDBChanged(queue.entry);
}
}
if (now > queue.timeout) {
if (!queue.warned) Logger(`Timed out: ${queue.entry._id} could not collect ${queue.missingChildren.length} chunks. plugin keeps watching, but you have to check the file after the replication.`, LOG_LEVEL.NOTICE);
queue.warned = true;
continue;
}
}
this.queuedFiles = this.queuedFiles.filter((e) => !e.done);
this.saveQueuedFiles();
});
}
parseIncomingChunk(chunk: PouchDB.Core.ExistingDocument<EntryDoc>) {
const now = new Date().getTime();
let isNewFileCompleted = false;
for (const queue of this.queuedFiles) {
if (queue.done) continue;
if (queue.missingChildren.indexOf(chunk._id) !== -1) {
queue.missingChildren = queue.missingChildren.filter((e) => e != chunk._id);
queue.timeout = now + this.chunkWaitTimeout;
}
if (queue.missingChildren.length == 0) {
for (const e of this.queuedFiles) {
if (e.entry._id == queue.entry._id && e.entry.mtime < queue.entry.mtime) {
e.done = true;
}
}
isNewFileCompleted = true;
}
}
if (isNewFileCompleted) this.procQueuedFiles();
}
async parseIncomingDoc(doc: PouchDB.Core.ExistingDocument<EntryBody>) {
const skipOldFile = this.settings.skipOlderFilesOnSync;
if (skipOldFile) {
const info = this.app.vault.getAbstractFileByPath(id2path(doc._id));
if (info && info instanceof TFile) {
const localMtime = ~~((info as TFile).stat.mtime / 1000);
const docMtime = ~~(doc.mtime / 1000);
if (localMtime >= docMtime) {
Logger(`${doc._id} Skipped, older than storage.`, LOG_LEVEL.VERBOSE);
return;
}
}
}
const now = new Date().getTime();
const newQueue = {
entry: doc,
missingChildren: [] as string[],
timeout: now + this.chunkWaitTimeout,
};
if ("children" in doc) {
const c = await this.localDatabase.localDatabase.allDocs({ keys: doc.children, include_docs: false });
const missing = c.rows.filter((e) => "error" in e).map((e) => e.key);
if (missing.length) Logger(`${doc._id}(${doc._rev}) Queued (waiting ${missing.length} items)`, LOG_LEVEL.VERBOSE);
newQueue.missingChildren = missing;
this.queuedFiles.push(newQueue);
this.saveQueuedFiles();
} else {
this.queuedFiles.push(newQueue);
this.saveQueuedFiles();
this.procQueuedFiles();
}
}
periodicSyncHandler: number = null;
//---> Sync
@@ -827,14 +1050,15 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
continue;
}
if (change._id.startsWith("h:")) {
await this.parseIncomingChunk(change);
continue;
}
if (change._id == SYNCINFO_ID) {
continue;
}
if (change.type != "leaf" && change.type != "versioninfo" && change.type != "milestoneinfo" && change.type != "nodeinfo") {
Logger("replication change arrived", LOG_LEVEL.VERBOSE);
await this.handleDBChanged(change);
await this.parseIncomingDoc(change);
continue;
}
if (change.type == "versioninfo") {
if (change.version > VER) {
@@ -971,9 +1195,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
waiting = " " + this.batchFileChange.map((e) => "🛫").join("");
waiting = waiting.replace(/(🛫){10}/g, "🚀");
}
let queued = "";
const queue = Object.entries(this.queuedFiles).filter((e) => !e[1].warned);
const queuedCount = queue.length;
if (queuedCount) {
const pieces = queue.map((e) => e[1].missingChildren).reduce((prev, cur) => prev + cur.length, 0);
queued = ` 🧩 ${queuedCount} (${pieces})`;
}
const procs = getProcessingCounts();
const procsDisp = procs == 0 ? "" : `${procs}`;
const message = `Sync:${w}${sent}${arrived}${waiting}${procsDisp}`;
const message = `Sync:${w}${sent}${arrived}${waiting}${procsDisp}${queued}`;
const locks = getLocks();
const pendingTask = locks.pending.length ? `\nPending:${locks.pending.join(", ")}` : "";
const runningTask = locks.running.length ? `\nRunning:${locks.running.join(", ")}` : "";
@@ -1014,6 +1246,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.settings.autoSweepPlugins) {
await this.sweepPlugin(false);
}
await this.loadQueuedFiles();
this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
}
@@ -1053,6 +1286,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (showingNotice) {
notice = NewNotice("Initializing", 0);
}
const filesStorage = this.app.vault.getFiles();
const filesStorageName = filesStorage.map((e) => e.path);
const wf = await this.localDatabase.localDatabase.allDocs();
@@ -1440,6 +1674,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
});
if (isNotChanged) return;
await this.localDatabase.putDBEntry(d);
this.queuedFiles = this.queuedFiles.map((e) => ({ ...e, ...(e.entry._id == d._id ? { done: true } : {}) }));
Logger("put database:" + fullpath + "(" + datatype + ") ");
if (this.settings.syncOnSave && !this.suspended) {
@@ -1500,7 +1735,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async sweepPlugin(showMessage = false) {
if (!this.settings.usePluginSync) return;
await runWithLock("sweepplugin", false, async () => {
if (!this.localDatabase.isReady) return;
await runWithLock("sweepplugin", true, async () => {
const logLevel = showMessage ? LOG_LEVEL.NOTICE : LOG_LEVEL.INFO;
if (!this.settings.encrypt) {
Logger("You have to encrypt the database to use plugin setting sync.", LOG_LEVEL.NOTICE);
@@ -1517,7 +1753,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
endkey: `ps:${this.deviceAndVaultName}.`,
include_docs: true,
});
Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
// Logger("OLD DOCS.", LOG_LEVEL.VERBOSE);
// sweep current plugin.
// @ts-ignore
const pl = this.app.plugins;

View File

@@ -134,6 +134,7 @@ const connectRemoteCouchDB = async (uri: string, auth: { username: string; passw
// return await fetch(url, opts);
},
};
const db: PouchDB.Database<EntryDoc> = new PouchDB<EntryDoc>(uri, conf);
if (passphrase && typeof passphrase === "string") {
enableEncryption(db, passphrase);