Compare commits

...

3 Commits
0.0.6 ... 0.0.8

Author SHA1 Message Date
vrtmrz
f3f0639d95 database dedup implemented. 2021-10-15 17:58:42 +09:00
vorotamoroz
531cf0d8a4 add 2021-10-15 12:42:25 +09:00
vrtmrz
e4f62cefb9 first replication error fixed and minor some fixed 2021-10-15 12:30:06 +09:00
8 changed files with 1768 additions and 1275 deletions

13
.gitignore vendored
View File

@@ -1 +1,14 @@
# Intellij
*.iml
.idea
# npm
node_modules node_modules
package-lock.json
# build
# main.js
*.js.map
# obsidian
data.json

View File

@@ -16,6 +16,7 @@ Limitations: File deletion handling is not completed.
- Live sync - Live sync
- Self-Hosted data synchronization with conflict detection and resolving in Obsidian. - Self-Hosted data synchronization with conflict detection and resolving in Obsidian.
- Off line sync is also available.
## How to use the beta build ## How to use the beta build
@@ -106,4 +107,4 @@ example values.
| CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 | | CouchDB Password | (\*4) | c2c11651d75497fa3d3c486e4c8bdf27 |
# License # License
The source code is licensed MIT. The source code is licensed MIT.

2576
main.js

File diff suppressed because one or more lines are too long

418
main.ts
View File

@@ -1,11 +1,13 @@
import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder } from "obsidian"; import { App, debounce, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, addIcon, TFolder } from "obsidian";
import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser"; import { PouchDB } from "./pouchdb-browser-webpack/dist/pouchdb-browser";
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch"; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "diff-match-patch";
import xxhash from "xxhash-wasm";
// docs should be encoded as base64, so 1 char -> 1 bytes // docs should be encoded as base64, so 1 char -> 1 bytes
// and cloudant limitation is 1MB , we use 900kb; // and cloudant limitation is 1MB , we use 900kb;
// const MAX_DOC_SIZE = 921600; // const MAX_DOC_SIZE = 921600;
const MAX_DOC_SIZE = 921600; const MAX_DOC_SIZE = 200; // for .md file
const MAX_DOC_SIZE_BIN = 102400; // 100kb
interface ObsidianLiveSyncSettings { interface ObsidianLiveSyncSettings {
couchDB_URI: string; couchDB_URI: string;
@@ -14,6 +16,9 @@ interface ObsidianLiveSyncSettings {
liveSync: boolean; liveSync: boolean;
syncOnSave: boolean; syncOnSave: boolean;
syncOnStart: boolean; syncOnStart: boolean;
savingDelay: number;
lessInformationInLog: boolean;
gcDelay: number;
} }
const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = { const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
@@ -23,6 +28,9 @@ const DEFAULT_SETTINGS: ObsidianLiveSyncSettings = {
liveSync: false, liveSync: false,
syncOnSave: false, syncOnSave: false,
syncOnStart: false, syncOnStart: false,
savingDelay: 200,
lessInformationInLog: false,
gcDelay: 30,
}; };
interface Entry { interface Entry {
@@ -52,13 +60,11 @@ type LoadedEntry = Entry & {
interface EntryLeaf { interface EntryLeaf {
_id: string; _id: string;
parent: string;
seq: number;
data: string; data: string;
_rev?: string;
_deleted?: boolean; _deleted?: boolean;
type: "leaf"; type: "leaf";
} }
type EntryDoc = Entry | NewEntry | LoadedEntry | EntryLeaf; type EntryDoc = Entry | NewEntry | LoadedEntry | EntryLeaf;
type diff_result_leaf = { type diff_result_leaf = {
rev: string; rev: string;
@@ -160,6 +166,8 @@ class LocalPouchDB {
addLog: (message: any, isNotify?: boolean) => Promise<void>; addLog: (message: any, isNotify?: boolean) => Promise<void>;
localDatabase: PouchDB.Database<EntryDoc>; localDatabase: PouchDB.Database<EntryDoc>;
h32: (input: string, seed?: number) => string;
h64: (input: string, seedHigh?: number, seedLow?: number) => string;
constructor(app: App, plugin: ObsidianLiveSyncPlugin, dbname: string) { constructor(app: App, plugin: ObsidianLiveSyncPlugin, dbname: string) {
this.plugin = plugin; this.plugin = plugin;
this.app = app; this.app = app;
@@ -181,7 +189,7 @@ class LocalPouchDB {
} }
return "disabled"; return "disabled";
} }
initializeDatabase() { async initializeDatabase() {
if (this.localDatabase != null) this.localDatabase.close(); if (this.localDatabase != null) this.localDatabase.close();
this.localDatabase = null; this.localDatabase = null;
this.localDatabase = new PouchDB<EntryDoc>(this.dbname + "-livesync", { this.localDatabase = new PouchDB<EntryDoc>(this.dbname + "-livesync", {
@@ -189,8 +197,14 @@ class LocalPouchDB {
revs_limit: 100, revs_limit: 100,
deterministic_revs: true, deterministic_revs: true,
}); });
await this.prepareHashArg();
}
async prepareHashArg() {
if (this.h32 != null) return;
const { h32, h64 } = await xxhash();
this.h32 = h32;
this.h64 = h64;
} }
async getDatabaseDoc(id: string, opt?: any): Promise<false | LoadedEntry> { async getDatabaseDoc(id: string, opt?: any): Promise<false | LoadedEntry> {
try { try {
let obj: EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = null; let obj: EntryDoc & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta = null;
@@ -225,23 +239,16 @@ class LocalPouchDB {
// search childrens // search childrens
try { try {
let childrens = []; let childrens = [];
// let childPromise = [];
for (var v of obj.children) { for (var v of obj.children) {
// childPromise.push(this.localDatabase.get(v)); // childPromise.push(this.localDatabase.get(v));
let elem = await this.localDatabase.get(v); let elem = await this.localDatabase.get(v);
if (elem.type && elem.type == "leaf") { if (elem.type && elem.type == "leaf") {
childrens.push(elem); childrens.push(elem.data);
} else { } else {
throw new Error("linked document is not leaf"); throw new Error("linked document is not leaf");
} }
} }
// let childrens = await Promise.all(childPromise); let data = childrens.join("");
let data = childrens
// .filter((e) => e.type == "leaf")
// .map((e) => e as NoteLeaf)
.sort((e) => e.seq)
.map((e) => e.data)
.join("");
let doc: LoadedEntry = { let doc: LoadedEntry = {
data: data, data: data,
_id: obj._id, _id: obj._id,
@@ -257,6 +264,7 @@ class LocalPouchDB {
if (ex.status && ex.status == 404) { if (ex.status && ex.status == 404) {
this.addLog(`Missing document content!, could not read ${obj._id} from database.`, true); this.addLog(`Missing document content!, could not read ${obj._id} from database.`, true);
// this.addLog(ex); // this.addLog(ex);
return false;
} }
this.addLog(`Something went wrong on reading ${obj._id} from database.`, true); this.addLog(`Something went wrong on reading ${obj._id} from database.`, true);
this.addLog(ex); this.addLog(ex);
@@ -285,24 +293,12 @@ class LocalPouchDB {
} }
//Check it out and fix docs to regular case //Check it out and fix docs to regular case
if (!obj.type || (obj.type && obj.type == "notes")) { if (!obj.type || (obj.type && obj.type == "notes")) {
// let note = obj as Notes; obj._deleted = true;
// note._deleted=true;
obj._deleted=true;
let r = await this.localDatabase.put(obj); let r = await this.localDatabase.put(obj);
return true; return true;
// simple note // simple note
} }
if (obj.type == "newnote") { if (obj.type == "newnote") {
// search childrens
for (var v of obj.children) {
let d = await this.localDatabase.get(v);
if (d.type != "leaf") {
this.addLog(`structure went wrong:${id}-${v}`);
}
d._deleted = true;
await this.localDatabase.put(d);
this.addLog(`content removed:${(d as EntryLeaf).seq}`);
}
obj._deleted = true; obj._deleted = true;
await this.localDatabase.put(obj); await this.localDatabase.put(obj);
this.addLog(`entry removed:${obj._id}`); this.addLog(`entry removed:${obj._id}`);
@@ -318,25 +314,79 @@ class LocalPouchDB {
async putDBEntry(note: LoadedEntry) { async putDBEntry(note: LoadedEntry) {
let leftData = note.data; let leftData = note.data;
let savenNotes = []; // something occured, kill this . let savenNotes = [];
let seq = 0; let processed = 0;
let now = Date.now(); let made = 0;
let skiped = 0;
let pieceSize = MAX_DOC_SIZE;
if (!note._id.endsWith(".md")) {
pieceSize = MAX_DOC_SIZE_BIN;
}
do { do {
let piece = leftData.substring(0, MAX_DOC_SIZE); // To keep low bandwith and database size,
leftData = leftData.substring(MAX_DOC_SIZE); // Dedup pieces on database.
seq++; let piece = leftData.substring(0, pieceSize);
let leafid = note._id + "-" + now + "-" + seq; leftData = leftData.substring(pieceSize);
let d: EntryLeaf = { processed++;
_id: leafid, // Get has of piece.
parent: note._id, let hashedPiece = this.h32(piece);
data: piece, let leafid = "h:" + hashedPiece;
seq: seq, let hashQ: number = 0; // if hash collided, **IF**, count it up.
type: "leaf", let tryNextHash = false;
}; let needMake = true;
let result = await this.localDatabase.put(d);
do {
let nleafid = leafid;
try {
nleafid = `${leafid}${hashQ}`;
// console.log(nleafid);
let pieceData = await this.localDatabase.get<EntryLeaf>(nleafid);
if (pieceData.type == "leaf" && pieceData.data == piece) {
this.addLog("hashe:data exists.");
leafid = nleafid;
needMake = false;
tryNextHash = false;
} else if (pieceData.type == "leaf") {
this.addLog("hash:collision!!");
hashQ++;
tryNextHash = true;
} else {
this.addLog("hash:no collision, it's not leaf. what's going on..");
leafid = nleafid;
tryNextHash = false;
}
} catch (ex) {
if (ex.status && ex.status == 404) {
//not found, we can use it.
this.addLog(`hash:not found.`);
leafid = nleafid;
needMake = true;
} else {
needMake = false;
throw ex;
}
}
} while (tryNextHash);
if (needMake) {
//have to make
let d: EntryLeaf = {
_id: leafid,
data: piece,
type: "leaf",
};
let result = await this.localDatabase.put(d);
if (result.ok) {
this.addLog(`ok:saven`);
made++;
} else {
this.addLog("save faild");
}
} else {
skiped++;
}
savenNotes.push(leafid); savenNotes.push(leafid);
} while (leftData != ""); } while (leftData != "");
this.addLog(`note content saven, pieces:${seq}`); this.addLog(`note content saven, pieces:${processed} new:${made}, skip:${skiped}`);
let newDoc: NewEntry = { let newDoc: NewEntry = {
NewNote: true, NewNote: true,
children: savenNotes, children: savenNotes,
@@ -351,15 +401,10 @@ class LocalPouchDB {
// Here for upsert logic, // Here for upsert logic,
try { try {
let old = await this.localDatabase.get(newDoc._id); let old = await this.localDatabase.get(newDoc._id);
if (!old.type || old.type == "notes") { if (!old.type || old.type == "notes" || old.type == "newnote") {
// simple use rev for new doc // simple use rev for new doc
newDoc._rev = old._rev; newDoc._rev = old._rev;
} }
if (old.type == "newnote") {
//when save finished, we have to garbage collect.
deldocs = old.children;
newDoc._rev = old._rev;
}
} catch (ex) { } catch (ex) {
if (ex.status && ex.status == 404) { if (ex.status && ex.status == 404) {
// NO OP/ // NO OP/
@@ -369,20 +414,11 @@ class LocalPouchDB {
} }
await this.localDatabase.put(newDoc); await this.localDatabase.put(newDoc);
this.addLog(`note saven:${newDoc._id}`); this.addLog(`note saven:${newDoc._id}`);
let items = 0;
for (var v of deldocs) {
items++;
//TODO: Check for missing link
let d = await this.localDatabase.get(v);
d._deleted = true;
await this.localDatabase.put(d);
}
this.addLog(`old content deleted, pieces:${items}`);
} }
syncHandler: PouchDB.Replication.Sync<{}> = null; syncHandler: PouchDB.Replication.Sync<{}> = null;
async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, callback: (e: PouchDB.Replication.SyncResult<{}>) => Promise<void>) { async openReplication(setting: ObsidianLiveSyncSettings, keepAlive: boolean, showResult: boolean, callback: (e: PouchDB.Core.ExistingDocument<{}>[]) => Promise<void>) {
let uri = setting.couchDB_URI; let uri = setting.couchDB_URI;
let auth: Credential = { let auth: Credential = {
username: setting.couchDB_USER, username: setting.couchDB_USER,
@@ -401,14 +437,26 @@ class LocalPouchDB {
let db = dbret.db; let db = dbret.db;
//replicate once //replicate once
this.localDatabase.replicate let replicate = this.localDatabase.replicate.from(db);
.from(db) replicate
.on("change", async (e) => {
try {
callback(e.docs);
this.addLog(`pulled ${e.docs.length} doc(s)`);
} catch (ex) {
this.addLog("Replication callback error");
this.addLog(ex);
}
})
.on("complete", async (info) => { .on("complete", async (info) => {
replicate.removeAllListeners();
replicate.cancel();
// this.syncHandler = null;
this.syncHandler = this.localDatabase.sync(db, syncOption); this.syncHandler = this.localDatabase.sync(db, syncOption);
this.syncHandler this.syncHandler
.on("change", async (e) => { .on("change", async (e) => {
try { try {
callback(e); callback(e.change.docs);
this.addLog(`replicated ${e.change.docs.length} doc(s)`); this.addLog(`replicated ${e.change.docs.length} doc(s)`);
} catch (ex) { } catch (ex) {
this.addLog("Replication callback error"); this.addLog("Replication callback error");
@@ -419,8 +467,9 @@ class LocalPouchDB {
this.addLog("Replication activated"); this.addLog("Replication activated");
}) })
.on("complete", (e) => { .on("complete", (e) => {
this.addLog("Replication completed", true); this.addLog("Replication completed", showResult);
// this.addLog(e); // this.addLog(e);
console.dir(this.syncHandler);
this.syncHandler = null; this.syncHandler = null;
}) })
.on("denied", (e) => { .on("denied", (e) => {
@@ -433,7 +482,10 @@ class LocalPouchDB {
}) })
.on("paused", (e) => { .on("paused", (e) => {
this.addLog("replication paused"); this.addLog("replication paused");
// console.dir(this.syncHandler);
// this.addLog(e);
}); });
// console.dir();
}) })
.on("error", () => { .on("error", () => {
this.addLog("Pulling Replication error", true); this.addLog("Pulling Replication error", true);
@@ -453,6 +505,7 @@ class LocalPouchDB {
async resetDatabase() { async resetDatabase() {
await this.closeReplication(); await this.closeReplication();
await this.localDatabase.destroy(); await this.localDatabase.destroy();
this.localDatabase = null;
await this.initializeDatabase(); await this.initializeDatabase();
this.addLog("Local Database Reset", true); this.addLog("Local Database Reset", true);
} }
@@ -485,6 +538,53 @@ class LocalPouchDB {
if (con2 === false) return; if (con2 === false) return;
this.addLog("Remote Database Created or Connected", true); this.addLog("Remote Database Created or Connected", true);
} }
async garbageCollect() {
// get all documents of NewEntry2
// we don't use queries , just use allDocs();
let c = 0;
let readCount = 0;
let hashPieces: string[] = [];
let usedPieces: string[] = [];
do {
let result = await this.localDatabase.allDocs({ include_docs: true, skip: c, limit: 100 });
readCount = result.rows.length;
if (readCount > 0) {
//there are some result
for (let v of result.rows) {
let doc = v.doc;
if (doc.type == "newnote") {
// used pieces memo.
usedPieces = Array.from(new Set([...usedPieces, ...doc.children]));
}
if (doc.type == "leaf") {
// all pieces.
hashPieces = Array.from(new Set([...hashPieces, doc._id]));
}
// this.addLog(`GC:processed:${v.doc._id}`);
}
}
c += readCount;
} while (readCount != 0);
// items collected.
const garbages = hashPieces.filter((e) => usedPieces.indexOf(e) == -1);
let deleteCount = 0;
for (let v of garbages) {
try {
let item = await this.localDatabase.get(v);
item._deleted = true;
await this.localDatabase.put(item);
deleteCount++;
} catch (ex) {
if (ex.status && ex.status == 404) {
// NO OP. It should be timing problem.
} else {
throw ex;
}
}
}
this.addLog(`GC:deleted ${deleteCount} items.`);
}
} }
export default class ObsidianLiveSyncPlugin extends Plugin { export default class ObsidianLiveSyncPlugin extends Plugin {
@@ -492,11 +592,12 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
//localDatabase: PouchDB.Database<EntryDoc>; //localDatabase: PouchDB.Database<EntryDoc>;
localDatabase: LocalPouchDB; localDatabase: LocalPouchDB;
logMessage: string[] = []; logMessage: string[] = [];
onLogChanged: () => void; // onLogChanged: () => void;
statusBar: HTMLElement; statusBar: HTMLElement;
statusBar2: HTMLElement; statusBar2: HTMLElement;
async onload() { async onload() {
this.addLog = this.addLog.bind(this);
this.addLog("loading plugin"); this.addLog("loading plugin");
await this.openDatabase(); await this.openDatabase();
await this.loadSettings(); await this.loadSettings();
@@ -517,20 +618,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
</g>` </g>`
); );
this.addRibbonIcon("replicate", "Replicate", async () => { this.addRibbonIcon("replicate", "Replicate", async () => {
await this.replicate(); await this.replicate(true);
}); });
this.addRibbonIcon("view-log", "Show log", () => { let x = this.addRibbonIcon("view-log", "Show log", () => {
new LogDisplayModal(this.app, this).open(); new LogDisplayModal(this.app, this).open();
}); });
this.statusBar = this.addStatusBarItem(); this.statusBar = this.addStatusBarItem();
this.statusBar2 = this.addStatusBarItem(); this.statusBar2 = this.addStatusBarItem();
let delay = this.settings.savingDelay;
this.watchVaultChange = debounce(this.watchVaultChange.bind(this), 200, false); if (delay < 200) delay = 200;
this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), 200, false); if (delay > 5000) delay = 5000;
this.watchVaultRename = debounce(this.watchVaultRename.bind(this), 200, false); this.watchVaultChange = debounce(this.watchVaultChange.bind(this), delay, false);
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), 200, false); this.watchVaultDelete = debounce(this.watchVaultDelete.bind(this), delay, false);
this.watchVaultRename = debounce(this.watchVaultRename.bind(this), delay, false);
this.watchWorkspaceOpen = debounce(this.watchWorkspaceOpen.bind(this), delay, false);
this.registerWatchEvents(); this.registerWatchEvents();
this.parseReplicationResult = this.parseReplicationResult.bind(this); this.parseReplicationResult = this.parseReplicationResult.bind(this);
@@ -540,7 +644,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
await this.initializeDatabase(); await this.initializeDatabase();
this.realizeSettingSyncMode(); this.realizeSettingSyncMode();
if (this.settings.syncOnStart) { if (this.settings.syncOnStart) {
await this.replicate(); await this.replicate(false);
} }
}, 100); }, 100);
@@ -550,16 +654,23 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.settings.liveSync) { if (this.settings.liveSync) {
await this.localDatabase.closeReplication(); await this.localDatabase.closeReplication();
if (this.settings.liveSync) { if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, this.parseReplicationResult); this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
} }
} }
}, 60 * 1000) }, 60 * 1000)
); );
this.watchWindowVisiblity = this.watchWindowVisiblity.bind(this);
window.addEventListener("visibilitychange", this.watchWindowVisiblity);
} }
onunload() { onunload() {
if (this.gcTimerHandler != null) {
clearTimeout(this.gcTimerHandler);
this.gcTimerHandler = null;
}
this.localDatabase.closeReplication(); this.localDatabase.closeReplication();
this.localDatabase.close(); this.localDatabase.close();
window.removeEventListener("visibilitychange", this.watchWindowVisiblity);
this.addLog("unloading plugin"); this.addLog("unloading plugin");
} }
@@ -569,7 +680,10 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
let vaultName = this.app.vault.getName(); let vaultName = this.app.vault.getName();
this.localDatabase = new LocalPouchDB(this.app, this, vaultName); this.localDatabase = new LocalPouchDB(this.app, this, vaultName);
this.localDatabase.initializeDatabase(); await this.localDatabase.initializeDatabase();
}
async garbageCollect() {
await this.localDatabase.garbageCollect();
} }
async loadSettings() { async loadSettings() {
@@ -579,7 +693,19 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
async saveSettings() { async saveSettings() {
await this.saveData(this.settings); await this.saveData(this.settings);
} }
gcTimerHandler: any = null;
gcHook() {
if (this.settings.gcDelay == 0) return;
const GC_DELAY = this.settings.gcDelay * 1000; // if leaving opening window, try GC,
if (this.gcTimerHandler != null) {
clearTimeout(this.gcTimerHandler);
this.gcTimerHandler = null;
}
this.gcTimerHandler = setTimeout(() => {
this.gcTimerHandler = null;
this.garbageCollect();
}, GC_DELAY);
}
registerWatchEvents() { registerWatchEvents() {
this.registerEvent(this.app.vault.on("modify", this.watchVaultChange)); this.registerEvent(this.app.vault.on("modify", this.watchVaultChange));
this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete)); this.registerEvent(this.app.vault.on("delete", this.watchVaultDelete));
@@ -587,12 +713,32 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.registerEvent(this.app.vault.on("create", this.watchVaultChange)); this.registerEvent(this.app.vault.on("create", this.watchVaultChange));
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen)); this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
} }
watchWindowVisiblity() {
this.addLog("visiblity changed");
let isHidden = document.hidden;
// this.addLog(isHidden);
if (isHidden) {
this.localDatabase.closeReplication();
} else {
if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
}
if (this.settings.syncOnStart) {
this.localDatabase.openReplication(this.settings, false, false, this.parseReplicationResult);
}
}
this.gcHook();
}
watchWorkspaceOpen(file: TFile) { watchWorkspaceOpen(file: TFile) {
if (file == null) return; if (file == null) return;
this.showIfConflicted(file); this.showIfConflicted(file);
this.gcHook();
} }
watchVaultChange(file: TFile, ...args: any[]) { watchVaultChange(file: TFile, ...args: any[]) {
this.updateIntoDB(file); this.updateIntoDB(file);
this.gcHook();
} }
watchVaultDelete(file: TFile & TFolder) { watchVaultDelete(file: TFile & TFolder) {
if (file.children) { if (file.children) {
@@ -602,6 +748,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} else { } else {
this.deleteFromDB(file); this.deleteFromDB(file);
} }
this.gcHook();
} }
watchVaultRename(file: TFile & TFolder, oldFile: any) { watchVaultRename(file: TFile & TFolder, oldFile: any) {
if (file.children) { if (file.children) {
@@ -611,10 +758,17 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
this.updateIntoDB(file); this.updateIntoDB(file);
this.deleteFromDBbyPath(oldFile); this.deleteFromDBbyPath(oldFile);
} }
this.gcHook();
} }
//--> Basic document Functions //--> Basic document Functions
async addLog(message: any, isNotify?: boolean) { async addLog(message: any, isNotify?: boolean) {
// debugger;
if (!isNotify && this.settings && this.settings.lessInformationInLog) {
return;
}
// console.log(this.settings);
let timestamp = new Date().toLocaleString(); let timestamp = new Date().toLocaleString();
let messagecontent = typeof message == "string" ? message : JSON.stringify(message, null, 2); let messagecontent = typeof message == "string" ? message : JSON.stringify(message, null, 2);
let newmessage = timestamp + "->" + messagecontent; let newmessage = timestamp + "->" + messagecontent;
@@ -625,9 +779,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (this.statusBar2 != null) { if (this.statusBar2 != null) {
this.statusBar2.setText(newmessage.substring(0, 60)); this.statusBar2.setText(newmessage.substring(0, 60));
} }
if (this.onLogChanged != null) { // if (this.onLogChanged != null) {
this.onLogChanged(); // this.onLogChanged();
} // }
if (isNotify) { if (isNotify) {
new Notice(messagecontent); new Notice(messagecontent);
} }
@@ -725,18 +879,16 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
} }
//---> Sync //---> Sync
async parseReplicationResult(e: PouchDB.Replication.SyncResult<{}>): Promise<void> { async parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<Entry>>): Promise<void> {
let docs = e.change.docs;
for (var change of docs) { for (var change of docs) {
this.addLog("replication change arrived"); this.addLog("replication change arrived");
// this.addLog(change); await this.pouchdbChanged(change);
await this.pouchdbChanged(change as Entry);
} }
} }
async realizeSettingSyncMode() { async realizeSettingSyncMode() {
await this.localDatabase.closeReplication(); await this.localDatabase.closeReplication();
if (this.settings.liveSync) { if (this.settings.liveSync) {
this.localDatabase.openReplication(this.settings, true, this.parseReplicationResult); this.localDatabase.openReplication(this.settings, true, false, this.parseReplicationResult);
this.refreshStatusText(); this.refreshStatusText();
} }
} }
@@ -744,8 +896,8 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let statusStr = this.localDatabase.status(); let statusStr = this.localDatabase.status();
this.statusBar.setText("Sync:" + statusStr); this.statusBar.setText("Sync:" + statusStr);
} }
async replicate() { async replicate(showMessage?: boolean) {
this.localDatabase.openReplication(this.settings, false, this.parseReplicationResult); this.localDatabase.openReplication(this.settings, false, showMessage, this.parseReplicationResult);
} }
//<-- Sync //<-- Sync
@@ -787,13 +939,31 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
if (entry.children) { if (entry.children) {
this.addLog(`->is dir`); this.addLog(`->is dir`);
await this.deleteFolderOnDB(entry); await this.deleteFolderOnDB(entry);
await this.app.vault.delete(entry); try {
await this.app.vault.delete(entry);
} catch (ex) {
if (ex.code && ex.code == "ENOENT") {
//NO OP.
} else {
this.addLog(`error while delete filder:${entry.path}`);
this.addLog(ex);
}
}
} else { } else {
this.addLog(`->is file`); this.addLog(`->is file`);
await this.deleteFromDB(entry); await this.deleteFromDB(entry);
} }
} }
await this.app.vault.delete(folder); try {
await this.app.vault.delete(folder);
} catch (ex) {
if (ex.code && ex.code == "ENOENT") {
//NO OP.
} else {
this.addLog(`error while delete filder:${folder.path}`);
this.addLog(ex);
}
}
} }
async renameFolder(folder: TFolder, oldFile: any) { async renameFolder(folder: TFolder, oldFile: any) {
@@ -956,7 +1126,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin {
let oldData = { data: old.data, deleted: old._deleted }; let oldData = { data: old.data, deleted: old._deleted };
let newData = { data: d.data, deleted: d._deleted }; let newData = { data: d.data, deleted: d._deleted };
if (JSON.stringify(oldData) == JSON.stringify(newData)) { if (JSON.stringify(oldData) == JSON.stringify(newData)) {
this.addLog("no changed" + fullpath + d._deleted); this.addLog("no changed" + fullpath + (d._deleted ? " (deleted)" : ""));
return; return;
} }
// d._rev = old._rev; // d._rev = old._rev;
@@ -1001,9 +1171,8 @@ class LogDisplayModal extends Modal {
this.plugin = plugin; this.plugin = plugin;
} }
updateLog() { updateLog() {
let logs = [...this.plugin.logMessage];
let msg = ""; let msg = "";
for (var v of logs) { for (var v of this.plugin.logMessage) {
msg += escapeStringToHTML(v) + "<br>"; msg += escapeStringToHTML(v) + "<br>";
} }
this.logEl.innerHTML = msg; this.logEl.innerHTML = msg;
@@ -1017,13 +1186,14 @@ class LogDisplayModal extends Modal {
div.addClass("op-scrollable"); div.addClass("op-scrollable");
div.addClass("op-pre"); div.addClass("op-pre");
this.logEl = div; this.logEl = div;
this.plugin.onLogChanged = this.updateLog; this.updateLog = this.updateLog.bind(this);
// this.plugin.onLogChanged = this.updateLog;
this.updateLog(); this.updateLog();
} }
onClose() { onClose() {
let { contentEl } = this; let { contentEl } = this;
contentEl.empty(); contentEl.empty();
this.plugin.onLogChanged = null; // this.plugin.onLogChanged = null;
} }
} }
class ConflictResolveModal extends Modal { class ConflictResolveModal extends Modal {
@@ -1158,6 +1328,49 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}) })
); );
new Setting(containerEl)
.setName("File to Database saving delay")
.setDesc("ms, between 200 and 5000, restart required.")
.addText((text) => {
text.setPlaceholder("")
.setValue(this.plugin.settings.savingDelay + "")
.onChange(async (value) => {
let v = Number(value);
if (isNaN(v) || v < 200 || v > 5000) {
return 200;
//text.inputEl.va;
}
this.plugin.settings.savingDelay = v;
await this.plugin.saveSettings();
});
text.inputEl.setAttribute("type", "number");
});
new Setting(containerEl)
.setName("Auto GC delay")
.setDesc("(seconds), if you set zero, you have to run manually.")
.addText((text) => {
text.setPlaceholder("")
.setValue(this.plugin.settings.gcDelay + "")
.onChange(async (value) => {
let v = Number(value);
if (isNaN(v) || v < 200 || v > 5000) {
return 30;
//text.inputEl.va;
}
this.plugin.settings.gcDelay = v;
await this.plugin.saveSettings();
});
text.inputEl.setAttribute("type", "number");
});
new Setting(containerEl)
.setName("Log")
.setDesc("Reduce log infomations")
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.lessInformationInLog).onChange(async (value) => {
this.plugin.settings.lessInformationInLog = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl) new Setting(containerEl)
.setName("LiveSync") .setName("LiveSync")
.setDesc("Sync realtime") .setDesc("Sync realtime")
@@ -1205,6 +1418,25 @@ class ObsidianLiveSyncSettingTab extends PluginSettingTab {
//await this.test(); //await this.test();
}) })
); );
new Setting(containerEl)
.setName("Garbage Collect")
.addButton((button) =>
button
.setButtonText("Garbage Collection")
.setDisabled(false)
.onClick(async () => {
await this.plugin.garbageCollect();
//await this.test();
})
)
.addButton((button) =>
button
.setButtonText("Reset local files")
.setDisabled(false)
.onClick(async () => {
//await this.test();
})
);
new Setting(containerEl).setName("Remote Database Operations").addButton((button) => new Setting(containerEl).setName("Remote Database Operations").addButton((button) =>
button button
.setButtonText("Reset remote database") .setButtonText("Reset remote database")

View File

@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Obsidian Live sync", "name": "Obsidian Live sync",
"version": "0.0.6", "version": "0.0.8",
"minAppVersion": "0.9.12", "minAppVersion": "0.9.12",
"description": "obsidian Live synchronization plugin.", "description": "obsidian Live synchronization plugin.",
"author": "vorotamoroz", "author": "vorotamoroz",

21
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{ {
"name": "obsidian-pouch", "name": "obsidian-livesync",
"version": "0.12.0", "version": "0.0.8",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-pouch", "name": "obsidian-livesync",
"version": "0.12.0", "version": "0.0.8",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5" "diff-match-patch": "^1.0.5",
"xxhash-wasm": "^0.4.2"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^18.0.0", "@rollup/plugin-commonjs": "^18.0.0",
@@ -539,6 +540,11 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
},
"node_modules/xxhash-wasm": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz",
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
} }
}, },
"dependencies": { "dependencies": {
@@ -975,6 +981,11 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true "dev": true
},
"xxhash-wasm": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz",
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
} }
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.0.6", "version": "0.0.8",
"description": "obsidian Live synchronization plugin.", "description": "obsidian Live synchronization plugin.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@@ -22,6 +22,7 @@
"typescript": "^4.2.4" "typescript": "^4.2.4"
}, },
"dependencies": { "dependencies": {
"diff-match-patch": "^1.0.5" "diff-match-patch": "^1.0.5",
"xxhash-wasm": "^0.4.2"
} }
} }

View File

@@ -20,7 +20,6 @@ export default {
exports: "default", exports: "default",
banner, banner,
}, },
// treeshake: "safest",
external: ["obsidian"], external: ["obsidian"],
plugins: [ plugins: [
typescript({ exclude: ["pouchdb-browser.js", "pouchdb-browser-webpack"] }), typescript({ exclude: ["pouchdb-browser.js", "pouchdb-browser-webpack"] }),
@@ -28,9 +27,5 @@ export default {
browser: true, browser: true,
}), }),
commonjs(), commonjs(),
// nodePolyfills(
// // // {crypto:true}
// { include: "pouchdb-browser" }
// ),
], ],
}; };