From 2a2b39009c0b5f114a2dbbd9265064939aa1e4d9 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Tue, 28 May 2024 12:26:23 +0100 Subject: [PATCH] Fixed: - Now we *surely* can set the device name and enable customised synchronisation. - Unnecessary dialogue update processes have been eliminated. - Customisation sync no longer stores half-collected files. - No longer hangs up when removing or renaming files with the `Sync on Save` toggle enabled. Improved: - Customisation sync now performs data deserialization more smoothly. - New translations have been merged. --- src/features/CmdConfigSync.ts | 79 +++++++++++++++++++++------- src/lib | 2 +- src/main.ts | 27 +++++----- src/ui/ObsidianLiveSyncSettingTab.ts | 9 ++-- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/src/features/CmdConfigSync.ts b/src/features/CmdConfigSync.ts index aa64e0e..4fce65b 100644 --- a/src/features/CmdConfigSync.ts +++ b/src/features/CmdConfigSync.ts @@ -4,7 +4,7 @@ import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry, SavingEntry } from "../lib/src/common/types.ts"; import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "../lib/src/common/types.ts"; import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "../common/types.ts"; -import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocDataAsArray, isDocContentSame, throttle } from "../lib/src/common/utils.ts"; +import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocDataAsArray, isDocContentSame } from "../lib/src/common/utils.ts"; import { Logger } from "../lib/src/common/logger.ts"; import { readString, decodeBinary, arrayBufferToBase64, digestHash } from "../lib/src/string_and_binary/strbin.ts"; import { serialized, shareRunningResult } from "../lib/src/concurrency/lock.ts"; @@ -19,7 +19,6 @@ import type ObsidianLiveSyncPlugin from '../main.ts'; const d = "\u200b"; const d2 = "\n"; -const delimiters = /(?<=[\n|\u200b])/g; function serialize(data: PluginDataEx): string { @@ -42,8 +41,45 @@ function serialize(data: PluginDataEx): string { return ret; } +function splitWithDelimiters(sources: string[]): string[] { + const result: string[] = []; + for (const str of sources) { + let startIndex = 0; + const maxLen = str.length; + let i = -1; + let i1; + let i2; + do { + i1 = str.indexOf(d, startIndex); + i2 = str.indexOf(d2, startIndex); + if (i1 == -1 && i2 == -1) { + break; + } + if (i1 == -1) { + i = i2; + } else if (i2 == -1) { + i = i1; + } else { + i = i1 < i2 ? i1 : i2; + } + result.push(str.slice(startIndex, i + 1)); + startIndex = i + 1; + } while (i < maxLen); + if (startIndex < maxLen) { + result.push(str.slice(startIndex)); + } + } + + // To keep compatibilities + if (sources[sources.length - 1] == "") { + result.push(""); + } + + return result; +} + function getTokenizer(source: string[]) { - const sources = source.flatMap(e => e.split(delimiters)) + const sources = splitWithDelimiters(source); sources[0] = sources[0].substring(1); let pos = 0; let lineRunOut = false; @@ -318,8 +354,7 @@ export class ConfigSync extends LiveSyncCommands { } return false; } - createMissingConfigurationEntry = throttle(() => this._createMissingConfigurationEntry(), 1000); - _createMissingConfigurationEntry() { + async createMissingConfigurationEntry() { let saveRequired = false; for (const v of this.pluginList) { const key = `${v.category}/${v.name}`; @@ -337,7 +372,7 @@ export class ConfigSync extends LiveSyncCommands { } } if (saveRequired) { - this.plugin.saveSettingData(); + await this.plugin.saveSettingData(); } } @@ -365,7 +400,11 @@ export class ConfigSync extends LiveSyncCommands { } return []; }, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline().root.onUpdateProgress(() => { - this.createMissingConfigurationEntry(); + scheduleTask("checkMissingConfigurations", 250, async () => { + if (this.pluginScanProcessor.isIdle()) { + await this.createMissingConfigurationEntry(); + } + }); }); @@ -654,7 +693,7 @@ export class ConfigSync extends LiveSyncCommands { for (const target of fileTargets) { const data = await this.makeEntryFromFile(target); if (data == false) { - // Logger(`Config: skipped: ${target} `, LOG_LEVEL_VERBOSE); + Logger(`Config: skipped (Possibly is not exist): ${target} `, LOG_LEVEL_VERBOSE); continue; } if (data.version) { @@ -703,13 +742,15 @@ export class ConfigSync extends LiveSyncCommands { const oldC = await this.localDatabase.getDBEntryFromMeta(old, {}, false, false); if (oldC) { const d = await deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx; - const diffs = (d.files.map(previous => ({ prev: previous, curr: dt.files.find(e => e.filename == previous.filename) })).map(async e => { - try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch (_) { return false } - })) - const isSame = (await Promise.all(diffs)).every(e => e == true); - if (isSame) { - Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); - return true; + if (d.files.length == dt.files.length) { + const diffs = (d.files.map(previous => ({ prev: previous, curr: dt.files.find(e => e.filename == previous.filename) })).map(async e => { + try { return await isDocContentSame(e.curr?.data ?? [], e.prev.data) } catch (_) { return false } + })) + const isSame = (await Promise.all(diffs)).every(e => e == true); + if (isSame) { + Logger(`STORAGE --> DB:${prefixedFileName}: (config) Skipped (Same content)`, LOG_LEVEL_VERBOSE); + return true; + } } } saveData = @@ -757,9 +798,11 @@ export class ConfigSync extends LiveSyncCommands { return true; } this.recentProcessedInternalFiles = [key, ...this.recentProcessedInternalFiles].slice(0, 100); - - this.storeCustomizationFiles(path).then(() => {/* Fire and forget */ }); - + // To prevent saving half-collected file sets. + const keySchedule = this.filenameToUnifiedKey(path); + scheduleTask(keySchedule, 100, async () => { + await this.storeCustomizationFiles(path); + }) } diff --git a/src/lib b/src/lib index 302a2e7..9825b64 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 302a2e7c0b73d88b9a943a22b80a195e2a7a6051 +Subproject commit 9825b64d179382d504c4071221c9320b4d3818e6 diff --git a/src/main.ts b/src/main.ts index be0e7b2..8b94b8a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -878,6 +878,7 @@ Note: We can always able to read V1 format. It will be progressively converted. async onload() { logStore.pipeTo(new QueueProcessor(logs => logs.forEach(e => this.addLog(e.message, e.level, e.key)), { suspended: false, batchSize: 20, concurrentLimit: 1, delay: 0 })).startPipeline(); Logger("loading plugin"); + __onMissingTranslation(() => { }); // eslint-disable-next-line no-unused-labels DEV: { __onMissingTranslation((key) => { @@ -1137,12 +1138,14 @@ Note: We can always able to read V1 format. It will be progressively converted. this.settingTab.requestReload() } - async saveSettingData() { + saveDeviceAndVaultName() { const lsKey = "obsidian-live-sync-vaultanddevicename-" + this.getVaultName(); - localStorage.setItem(lsKey, this.deviceAndVaultName || ""); - + } + async saveSettingData() { + this.saveDeviceAndVaultName(); const settings = { ...this.settings }; + settings.deviceAndVaultName = ""; if (this.usedPassphrase == "" && !await this.getPassphrase(settings)) { Logger("Could not determine passphrase for saving data.json! Our data.json have insecure items!", LOG_LEVEL_NOTICE); } else { @@ -3060,28 +3063,28 @@ Or if you are sure know what had been happened, we can unlock the database from const ret = await this.localDatabase.putDBEntry(d); if (ret !== false) { Logger(msg + fullPath); - if (this.settings.syncOnSave && !this.suspended) { - scheduleTask("perform-replicate-after-save", 250, () => this.replicate()); - } + this.scheduleReplicateIfSyncOnSave(); } return ret != false; } + scheduleReplicateIfSyncOnSave() { + if (this.settings.syncOnSave && !this.suspended) { + scheduleTask("perform-replicate-after-save", 250, () => this.replicate()); + } + } + async deleteFromDB(file: TFile) { if (!await this.isTargetFile(file)) return; const fullPath = getPathFromTFile(file); Logger(`deleteDB By path:${fullPath}`); await this.deleteFromDBbyPath(fullPath); - if (this.settings.syncOnSave && !this.suspended) { - await this.replicate(); - } + this.scheduleReplicateIfSyncOnSave(); } async deleteFromDBbyPath(fullPath: FilePath) { await this.localDatabase.deleteDBEntry(fullPath); - if (this.settings.syncOnSave && !this.suspended) { - await this.replicate(); - } + this.scheduleReplicateIfSyncOnSave(); } async resetLocalDatabase() { diff --git a/src/ui/ObsidianLiveSyncSettingTab.ts b/src/ui/ObsidianLiveSyncSettingTab.ts index 6dcc123..39feaf8 100644 --- a/src/ui/ObsidianLiveSyncSettingTab.ts +++ b/src/ui/ObsidianLiveSyncSettingTab.ts @@ -439,6 +439,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } if (key == "deviceAndVaultName") { this.plugin.deviceAndVaultName = this.editingSettings?.[key]; + this.plugin.saveDeviceAndVaultName(); return await Promise.resolve(); } } @@ -519,12 +520,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { /** * Reread all settings and request invalidate */ - reloadAllSettings() { + reloadAllSettings(skipUpdate: boolean = false) { const localSetting = this.reloadAllLocalSettings(); this._editingSettings = { ...this.plugin.settings, ...localSetting }; this._editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() }; this.initialSettings = { ...this.editingSettings, }; - this.requestUpdate(); + if (!skipUpdate) this.requestUpdate(); } /** @@ -667,9 +668,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { this.requestUpdate(); } } else { - Logger(`reread: all! hidden`, LOG_LEVEL_VERBOSE) - this.reloadAllSettings(); - this.display(); + this.reloadAllSettings(true); } }