diff --git a/frontend/obsidian-plugin/src/vault-link-plugin.ts b/frontend/obsidian-plugin/src/vault-link-plugin.ts index db11658f..1d31804c 100644 --- a/frontend/obsidian-plugin/src/vault-link-plugin.ts +++ b/frontend/obsidian-plugin/src/vault-link-plugin.ts @@ -18,7 +18,8 @@ import { Syncer, SyncHistory, SyncService, - initialize + initialize, + Settings } from "sync-client"; export default class VaultLinkPlugin extends Plugin { @@ -32,21 +33,38 @@ export default class VaultLinkPlugin extends Plugin { await initialize(); + let state = (await this.loadData()) ?? { + settings: undefined, + database: undefined + }; const database = new Database( - await this.loadData(), - this.saveData.bind(this) + state.database, + async (data: unknown): Promise => { + state = { ...state, database: data }; + return this.saveData(state); + } + ); + + const settings = new Settings( + state.settings, + async (data: unknown): Promise => { + state = { ...state, settings: data }; + return this.saveData(state); + } ); const syncService = new SyncService(database); const syncer = new Syncer( database, + settings, syncService, this.operations, this.history ); const statusDescription = new StatusDescription( + settings, database, syncService, this.history, @@ -56,22 +74,22 @@ export default class VaultLinkPlugin extends Plugin { this.settingsTab = new SyncSettingsTab({ app: this.app, plugin: this, - database, + settings, syncService, statusDescription, syncer }); this.addSettingTab(this.settingsTab); - new StatusBar(database, this, this.history, syncer); + new StatusBar(settings, this, this.history, syncer); this.registerView( HistoryView.TYPE, - (leaf) => new HistoryView(leaf, database, this.history) + (leaf) => new HistoryView(leaf, settings, this.history) ); this.registerView( LogsView.TYPE, - (leaf) => new LogsView(this, database, leaf) + (leaf) => new LogsView(this, settings, leaf) ); this.addRibbonIcon( @@ -117,21 +135,23 @@ export default class VaultLinkPlugin extends Plugin { }); this.registerRemoteEventListener( + settings, database, syncService, syncer, - database.getSettings().fetchChangesUpdateIntervalMs + settings.getSettings().fetchChangesUpdateIntervalMs ); - database.addOnSettingsChangeHandlers((settings, oldSettings) => { + settings.addOnSettingsChangeHandlers((newSettings, oldSettings) => { this.registerRemoteEventListener( + settings, database, syncService, syncer, - settings.fetchChangesUpdateIntervalMs + newSettings.fetchChangesUpdateIntervalMs ); - if (!oldSettings.isSyncEnabled && settings.isSyncEnabled) { + if (!oldSettings.isSyncEnabled && newSettings.isSyncEnabled) { syncer .scheduleSyncForOfflineChanges() .catch((_error: unknown) => { @@ -182,6 +202,7 @@ export default class VaultLinkPlugin extends Plugin { } private registerRemoteEventListener( + settings: Settings, database: Database, syncService: SyncService, syncer: Syncer, @@ -195,6 +216,7 @@ export default class VaultLinkPlugin extends Plugin { // eslint-disable-next-line @typescript-eslint/no-misused-promises async () => applyRemoteChangesLocally({ + settings, database, syncService, syncer diff --git a/frontend/obsidian-plugin/src/views/history-view.ts b/frontend/obsidian-plugin/src/views/history-view.ts index a54f2d2d..04c8e56d 100644 --- a/frontend/obsidian-plugin/src/views/history-view.ts +++ b/frontend/obsidian-plugin/src/views/history-view.ts @@ -2,7 +2,7 @@ import type { IconName, WorkspaceLeaf } from "obsidian"; import { ItemView, setIcon } from "obsidian"; import { intlFormatDistance } from "date-fns"; -import type { SyncHistory, HistoryEntry, Database } from "sync-client"; +import type { SyncHistory, HistoryEntry, Settings } from "sync-client"; import { SyncType, SyncSource, SyncStatus, Logger } from "sync-client"; export class HistoryView extends ItemView { @@ -12,7 +12,7 @@ export class HistoryView extends ItemView { public constructor( leaf: WorkspaceLeaf, - private readonly database: Database, + private readonly settings: Settings, private readonly history: SyncHistory ) { super(leaf); @@ -101,7 +101,7 @@ export class HistoryView extends ItemView { .filter( (entry) => entry.status !== SyncStatus.NO_OP || - this.database.getSettings().displayNoopSyncEvents + this.settings.getSettings().displayNoopSyncEvents ); entries.forEach((entry) => { diff --git a/frontend/obsidian-plugin/src/views/logs-view.ts b/frontend/obsidian-plugin/src/views/logs-view.ts index 8f7a8643..79dee71f 100644 --- a/frontend/obsidian-plugin/src/views/logs-view.ts +++ b/frontend/obsidian-plugin/src/views/logs-view.ts @@ -1,7 +1,7 @@ import type { WorkspaceLeaf } from "obsidian"; import { ItemView } from "obsidian"; import type VaultLinkPlugin from "src/vault-link-plugin"; -import type { Database } from "sync-client"; +import type { Settings } from "sync-client"; import { Logger } from "sync-client"; export class LogsView extends ItemView { @@ -10,7 +10,7 @@ export class LogsView extends ItemView { public constructor( private readonly plugin: VaultLinkPlugin, - private readonly database: Database, + private readonly settings: Settings, leaf: WorkspaceLeaf ) { super(leaf); @@ -19,7 +19,7 @@ export class LogsView extends ItemView { this.updateView(); }); - database.addOnSettingsChangeHandlers((newSettings, oldSettings) => { + settings.addOnSettingsChangeHandlers((newSettings, oldSettings) => { if (newSettings.minimumLogLevel !== oldSettings.minimumLogLevel) { this.updateView(); } @@ -79,7 +79,7 @@ export class LogsView extends ItemView { ); const logs = Logger.getInstance().getMessages( - this.database.getSettings().minimumLogLevel + this.settings.getSettings().minimumLogLevel ); if (logs.length === 0) { diff --git a/frontend/obsidian-plugin/src/views/settings-tab.ts b/frontend/obsidian-plugin/src/views/settings-tab.ts index b41ff40c..5f911c25 100644 --- a/frontend/obsidian-plugin/src/views/settings-tab.ts +++ b/frontend/obsidian-plugin/src/views/settings-tab.ts @@ -5,14 +5,14 @@ import type VaultLinkPlugin from "src/vault-link-plugin"; import type { StatusDescription } from "./status-description"; import { LogsView } from "./logs-view"; import { HistoryView } from "./history-view"; -import type { SyncService, Syncer, Database } from "sync-client"; +import type { SyncService, Syncer, Settings } from "sync-client"; import { Logger, LogLevel } from "sync-client"; export class SyncSettingsTab extends PluginSettingTab { private editedVaultName: string; private readonly plugin: VaultLinkPlugin; - private readonly database: Database; + private readonly settings: Settings; private readonly syncService: SyncService; private readonly statusDescription: StatusDescription; private readonly syncer: Syncer; @@ -21,27 +21,27 @@ export class SyncSettingsTab extends PluginSettingTab { public constructor({ app, plugin, - database, + settings, syncService, statusDescription, syncer }: { app: App; plugin: VaultLinkPlugin; - database: Database; + settings: Settings; syncService: SyncService; statusDescription: StatusDescription; syncer: Syncer; }) { super(app, plugin); this.plugin = plugin; - this.database = database; + this.settings = settings; this.syncService = syncService; this.statusDescription = statusDescription; this.syncer = syncer; - this.editedVaultName = this.database.getSettings().vaultName; - this.database.addOnSettingsChangeHandlers( + this.editedVaultName = this.settings.getSettings().vaultName; + this.settings.addOnSettingsChangeHandlers( (newSettings, oldSettings) => { if (newSettings.vaultName !== oldSettings.vaultName) { this.editedVaultName = newSettings.vaultName; @@ -130,9 +130,9 @@ export class SyncSettingsTab extends PluginSettingTab { .addText((text) => text .setPlaceholder("https://example.com:3030") - .setValue(this.database.getSettings().remoteUri) + .setValue(this.settings.getSettings().remoteUri) .onChange(async (value) => - this.database.setSetting("remoteUri", value) + this.settings.setSetting("remoteUri", value) ) ) .addButton((button) => @@ -154,9 +154,9 @@ export class SyncSettingsTab extends PluginSettingTab { .addTextArea((text) => text .setPlaceholder("ey...") - .setValue(this.database.getSettings().token) + .setValue(this.settings.getSettings().token) .onChange(async (value) => - this.database.setSetting("token", value) + this.settings.setSetting("token", value) ) ); @@ -169,18 +169,18 @@ export class SyncSettingsTab extends PluginSettingTab { .addText((text) => text .setPlaceholder("My Obsidian Vault") - .setValue(this.database.getSettings().vaultName) + .setValue(this.settings.getSettings().vaultName) .onChange((value) => (this.editedVaultName = value)) ) .addButton((button) => button.setButtonText("Apply").onClick(async () => { if ( this.editedVaultName === - this.database.getSettings().vaultName + this.settings.getSettings().vaultName ) { return; } - await this.database.setSetting( + await this.settings.setSetting( "vaultName", this.editedVaultName ); @@ -223,11 +223,11 @@ export class SyncSettingsTab extends PluginSettingTab { .setDynamicTooltip() .setInstant(false) .setValue( - this.database.getSettings() + this.settings.getSettings() .fetchChangesUpdateIntervalMs / 1000 ) .onChange(async (value) => - this.database.setSetting( + this.settings.setSetting( "fetchChangesUpdateIntervalMs", value * 1000 ) @@ -244,9 +244,9 @@ export class SyncSettingsTab extends PluginSettingTab { .setLimits(1, 16, 1) .setDynamicTooltip() .setInstant(false) - .setValue(this.database.getSettings().syncConcurrency) + .setValue(this.settings.getSettings().syncConcurrency) .onChange(async (value) => - this.database.setSetting("syncConcurrency", value) + this.settings.setSetting("syncConcurrency", value) ) ); @@ -260,9 +260,9 @@ export class SyncSettingsTab extends PluginSettingTab { .setLimits(0, 32, 1) .setDynamicTooltip() .setInstant(false) - .setValue(this.database.getSettings().maxFileSizeMB) + .setValue(this.settings.getSettings().maxFileSizeMB) .onChange(async (value) => - this.database.setSetting("maxFileSizeMB", value) + this.settings.setSetting("maxFileSizeMB", value) ) ); @@ -276,9 +276,9 @@ export class SyncSettingsTab extends PluginSettingTab { ) .addToggle((toggle) => toggle - .setValue(this.database.getSettings().isSyncEnabled) + .setValue(this.settings.getSettings().isSyncEnabled) .onChange(async (value) => - this.database.setSetting("isSyncEnabled", value) + this.settings.setSetting("isSyncEnabled", value) ) ); } @@ -293,9 +293,9 @@ export class SyncSettingsTab extends PluginSettingTab { ) .addToggle((toggle) => toggle - .setValue(this.database.getSettings().displayNoopSyncEvents) + .setValue(this.settings.getSettings().displayNoopSyncEvents) .onChange(async (value) => - this.database.setSetting("displayNoopSyncEvents", value) + this.settings.setSetting("displayNoopSyncEvents", value) ) ); @@ -312,9 +312,9 @@ export class SyncSettingsTab extends PluginSettingTab { [LogLevel.WARNING]: LogLevel.WARNING, [LogLevel.ERROR]: LogLevel.ERROR }) - .setValue(this.database.getSettings().minimumLogLevel) + .setValue(this.settings.getSettings().minimumLogLevel) .onChange(async (value) => - this.database.setSetting( + this.settings.setSetting( "minimumLogLevel", // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion value as LogLevel diff --git a/frontend/obsidian-plugin/src/views/status-bar.ts b/frontend/obsidian-plugin/src/views/status-bar.ts index 0cecaa74..b5babae2 100644 --- a/frontend/obsidian-plugin/src/views/status-bar.ts +++ b/frontend/obsidian-plugin/src/views/status-bar.ts @@ -1,4 +1,4 @@ -import type { Database, HistoryStats, SyncHistory, Syncer } from "sync-client"; +import type { HistoryStats, Settings, SyncHistory, Syncer } from "sync-client"; import type VaultLinkPlugin from "src/vault-link-plugin"; export class StatusBar { @@ -8,7 +8,7 @@ export class StatusBar { private lastRemaining: number | undefined; public constructor( - private readonly database: Database, + private readonly settings: Settings, private readonly plugin: VaultLinkPlugin, history: SyncHistory, syncer: Syncer @@ -24,7 +24,7 @@ export class StatusBar { this.updateStatus(); }); - database.addOnSettingsChangeHandlers(() => { + settings.addOnSettingsChangeHandlers(() => { this.updateStatus(); }); } @@ -57,7 +57,7 @@ export class StatusBar { } if (!hasShownMessage) { - if (this.database.getSettings().isSyncEnabled) { + if (this.settings.getSettings().isSyncEnabled) { container.createSpan({ text: "VaultLink is idle" }); } else { const button = container.createEl("button", { diff --git a/frontend/obsidian-plugin/src/views/status-description.ts b/frontend/obsidian-plugin/src/views/status-description.ts index 40d5c73e..b9c87ad8 100644 --- a/frontend/obsidian-plugin/src/views/status-description.ts +++ b/frontend/obsidian-plugin/src/views/status-description.ts @@ -4,7 +4,8 @@ import type { SyncService, SyncHistory, Syncer, - Database + Database, + Settings } from "sync-client"; export class StatusDescription { @@ -15,6 +16,7 @@ export class StatusDescription { private statusChangeListeners: (() => void)[] = []; public constructor( + private readonly settings: Settings, private readonly database: Database, private readonly syncService: SyncService, history: SyncHistory, @@ -32,7 +34,7 @@ export class StatusDescription { this.updateDescription(); }); - database.addOnSettingsChangeHandlers(() => { + settings.addOnSettingsChangeHandlers(() => { void this.updateConnectionState(); }); } @@ -73,8 +75,8 @@ export class StatusDescription { container.createSpan({ text: "VaultLink is connected to the server " }); container.createEl("a", { - text: this.database.getSettings().remoteUri, - href: this.database.getSettings().remoteUri + text: this.settings.getSettings().remoteUri, + href: this.settings.getSettings().remoteUri }); container.createSpan({ @@ -93,7 +95,7 @@ export class StatusDescription { (this.lastHistoryStats?.success ?? 0) === 0 && (this.lastHistoryStats?.error ?? 0) === 0 ) { - if (this.database.getSettings().isSyncEnabled) { + if (this.settings.getSettings().isSyncEnabled) { container.createSpan({ text: "Syncing is enabled but VaultLink hasn't found anything to sync yet." }); diff --git a/frontend/sync-client/src/database/database.ts b/frontend/sync-client/src/database/database.ts deleted file mode 100644 index 8e56fd1e..00000000 --- a/frontend/sync-client/src/database/database.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { SyncSettings } from "./sync-settings"; -import { DEFAULT_SETTINGS } from "./sync-settings"; -import type { - DocumentId, - DocumentMetadata, - RelativePath, - VaultUpdateId -} from "./document-metadata"; -import { Logger } from "src/tracing/logger"; - -interface StoredDatabase { - documents: Map; - settings: SyncSettings; - lastSeenUpdateId: VaultUpdateId | undefined; -} - -// Todo: split it into settings and documents -export class Database { - private _documents = new Map(); - private _settings: SyncSettings; - private _lastSeenUpdateId: VaultUpdateId | undefined; - - private readonly onSettingsChangeHandlers: (( - newSettings: SyncSettings, - oldSettings: SyncSettings - ) => void)[] = []; - - public constructor( - initialState: Partial | undefined, - private readonly saveData: (data: unknown) => Promise - ) { - initialState ??= {}; - if ( - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - Object.prototype.hasOwnProperty.call(initialState, "documents") && - initialState.documents - ) { - for (const [relativePath, metadata] of Object.entries( - initialState.documents - )) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - this._documents.set(relativePath, metadata as DocumentMetadata); - } - } - - Logger.getInstance().debug(`Loaded ${this._documents.size} documents`); - - this._settings = { - ...DEFAULT_SETTINGS, - ...(initialState.settings ?? {}) - }; - - Logger.getInstance().debug( - `Loaded settings: ${JSON.stringify(this._settings, null, 2)}` - ); - - this._lastSeenUpdateId = initialState.lastSeenUpdateId; - - Logger.getInstance().debug( - `Loaded last seen update id: ${this._lastSeenUpdateId}` - ); - } - - public getDocuments(): Map { - return this._documents; - } - - public getSettings(): SyncSettings { - return this._settings; - } - - public async setSettings(value: SyncSettings): Promise { - const oldSettings = this._settings; - this._settings = value; - this.onSettingsChangeHandlers.forEach((handler) => { - handler(value, oldSettings); - }); - await this.save(); - } - - public addOnSettingsChangeHandlers( - handler: (settings: SyncSettings, oldSettings: SyncSettings) => void - ): void { - this.onSettingsChangeHandlers.push(handler); - } - - public async setSetting( - key: T, - value: SyncSettings[T] - ): Promise { - const newSettings = { ...this._settings, [key]: value }; - Logger.getInstance().debug( - `Setting ${key} to ${value}, new settings: ${JSON.stringify( - newSettings, - null, - 2 - )}` - ); - await this.setSettings(newSettings); - } - - public getLastSeenUpdateId(): VaultUpdateId | undefined { - return this._lastSeenUpdateId; - } - - public async setLastSeenUpdateId( - value: VaultUpdateId | undefined - ): Promise { - this._lastSeenUpdateId = value; - await this.save(); - } - - public async resetSyncState(): Promise { - this._documents = new Map(); - this._lastSeenUpdateId = 0; - await this.save(); - } - - public getDocumentByDocumentId( - documentId: DocumentId - ): [RelativePath, DocumentMetadata] | undefined { - return [...this._documents.entries()].find( - ([_, metadata]) => metadata.documentId === documentId - ); - } - - public async setDocument({ - documentId, - relativePath, - parentVersionId, - hash - }: { - documentId: DocumentId; - relativePath: RelativePath; - parentVersionId: VaultUpdateId; - hash: string; - }): Promise { - this._documents.set(relativePath, { - documentId, - parentVersionId, - hash - }); - await this.save(); - } - - public async moveDocument({ - documentId, - oldRelativePath, - relativePath, - parentVersionId, - hash - }: { - documentId: DocumentId; - oldRelativePath: RelativePath; - relativePath: RelativePath; - parentVersionId: VaultUpdateId; - hash: string; - }): Promise { - this._documents.delete(oldRelativePath); - this._documents.set(relativePath, { - documentId, - parentVersionId, - hash - }); - await this.save(); - } - - public async removeDocument(relativePath: RelativePath): Promise { - this._documents.delete(relativePath); - await this.save(); - } - - public getDocument( - relativePath: RelativePath - ): DocumentMetadata | undefined { - return this._documents.get(relativePath); - } - - private async save(): Promise { - await this.saveData({ - documents: Object.fromEntries(this._documents.entries()), - settings: this._settings, - lastSeenUpdateId: this._lastSeenUpdateId - }); - } -} diff --git a/frontend/sync-client/src/database/document-metadata.ts b/frontend/sync-client/src/database/document-metadata.ts deleted file mode 100644 index 8261e7e2..00000000 --- a/frontend/sync-client/src/database/document-metadata.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type VaultUpdateId = number; -export type DocumentId = string; -export type RelativePath = string; - -export interface DocumentMetadata { - parentVersionId: VaultUpdateId; - documentId: DocumentId; - hash: string; -} diff --git a/frontend/sync-client/src/database/sync-settings.ts b/frontend/sync-client/src/database/sync-settings.ts deleted file mode 100644 index 99e7d81b..00000000 --- a/frontend/sync-client/src/database/sync-settings.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LogLevel } from "src/tracing/logger"; - -export interface SyncSettings { - remoteUri: string; - token: string; - vaultName: string; - fetchChangesUpdateIntervalMs: number; - syncConcurrency: number; - isSyncEnabled: boolean; - displayNoopSyncEvents: boolean; - minimumLogLevel: LogLevel; - maxFileSizeMB: number; -} - -export const DEFAULT_SETTINGS: SyncSettings = { - remoteUri: "", - token: "", - vaultName: "default", - fetchChangesUpdateIntervalMs: 1000, - syncConcurrency: 1, - isSyncEnabled: false, - displayNoopSyncEvents: false, - minimumLogLevel: LogLevel.INFO, - maxFileSizeMB: 10 -}; diff --git a/frontend/sync-client/src/file-operations.ts b/frontend/sync-client/src/file-operations.ts index 2dc182f5..5fce5242 100644 --- a/frontend/sync-client/src/file-operations.ts +++ b/frontend/sync-client/src/file-operations.ts @@ -1,4 +1,4 @@ -import type { RelativePath } from "src/database/document-metadata"; +import type { RelativePath } from "src/persistence/database"; export interface FileOperations { listAllFiles: () => Promise; diff --git a/frontend/sync-client/src/index.ts b/frontend/sync-client/src/index.ts index 219d3c34..631cadea 100644 --- a/frontend/sync-client/src/index.ts +++ b/frontend/sync-client/src/index.ts @@ -1,13 +1,14 @@ export { applyRemoteChangesLocally } from "./sync-operations/apply-remote-changes-locally"; export { + Database, type RelativePath, type DocumentId, type VaultUpdateId, type DocumentMetadata -} from "./database/document-metadata"; +} from "./persistence/database"; -export { Database } from "./database/database"; +export { Settings, type SyncSettings } from "./persistence/settings"; export { SyncService, @@ -32,12 +33,6 @@ export { type FileOperations } from "./file-operations"; import init from "sync_lib"; import wasmBin from "sync_lib/sync_lib_bg.wasm"; -export const initialize = async (): Promise => { - await init( - // eslint-disable-next-line - (wasmBin as any).default // it is loaded as a base64 string by webpack - ); -}; export { isFileTypeMergable, mergeText, @@ -46,3 +41,10 @@ export { merge, isBinary } from "sync_lib"; + +export const initialize = async (): Promise => { + await init( + // eslint-disable-next-line + (wasmBin as any).default // it is loaded as a base64 string by webpack + ); +}; diff --git a/frontend/sync-client/src/persistence/database.ts b/frontend/sync-client/src/persistence/database.ts new file mode 100644 index 00000000..93492b29 --- /dev/null +++ b/frontend/sync-client/src/persistence/database.ts @@ -0,0 +1,130 @@ +export type VaultUpdateId = number; +export type DocumentId = string; +export type RelativePath = string; + +export interface DocumentMetadata { + parentVersionId: VaultUpdateId; + documentId: DocumentId; + hash: string; +} + +import { Logger } from "src/tracing/logger"; + +export interface StoredDatabase { + documents: Map; + lastSeenUpdateId: VaultUpdateId | undefined; +} + +export class Database { + private documents = new Map(); + private lastSeenUpdateId: VaultUpdateId | undefined; + + public constructor( + initialState: Partial | undefined, + private readonly saveData: (data: unknown) => Promise + ) { + initialState ??= {}; + if (initialState.documents) { + for (const [relativePath, metadata] of Object.entries( + initialState.documents + )) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + this.documents.set(relativePath, metadata as DocumentMetadata); + } + } + Logger.getInstance().debug(`Loaded ${this.documents.size} documents`); + + this.lastSeenUpdateId = initialState.lastSeenUpdateId; + Logger.getInstance().debug( + `Loaded last seen update id: ${this.lastSeenUpdateId}` + ); + } + + public getDocuments(): Map { + return this.documents; + } + + public getLastSeenUpdateId(): VaultUpdateId | undefined { + return this.lastSeenUpdateId; + } + + public async setLastSeenUpdateId( + value: VaultUpdateId | undefined + ): Promise { + this.lastSeenUpdateId = value; + await this.save(); + } + + public async resetSyncState(): Promise { + this.documents = new Map(); + this.lastSeenUpdateId = 0; + await this.save(); + } + + public getDocumentByDocumentId( + documentId: DocumentId + ): [RelativePath, DocumentMetadata] | undefined { + return [...this.documents.entries()].find( + ([_, metadata]) => metadata.documentId === documentId + ); + } + + public async setDocument({ + documentId, + relativePath, + parentVersionId, + hash + }: { + documentId: DocumentId; + relativePath: RelativePath; + parentVersionId: VaultUpdateId; + hash: string; + }): Promise { + this.documents.set(relativePath, { + documentId, + parentVersionId, + hash + }); + await this.save(); + } + + public async moveDocument({ + documentId, + oldRelativePath, + relativePath, + parentVersionId, + hash + }: { + documentId: DocumentId; + oldRelativePath: RelativePath; + relativePath: RelativePath; + parentVersionId: VaultUpdateId; + hash: string; + }): Promise { + this.documents.delete(oldRelativePath); + this.documents.set(relativePath, { + documentId, + parentVersionId, + hash + }); + await this.save(); + } + + public async removeDocument(relativePath: RelativePath): Promise { + this.documents.delete(relativePath); + await this.save(); + } + + public getDocument( + relativePath: RelativePath + ): DocumentMetadata | undefined { + return this.documents.get(relativePath); + } + + private async save(): Promise { + await this.saveData({ + documents: Object.fromEntries(this.documents.entries()), + lastSeenUpdateId: this.lastSeenUpdateId + }); + } +} diff --git a/frontend/sync-client/src/persistence/settings.ts b/frontend/sync-client/src/persistence/settings.ts new file mode 100644 index 00000000..57762e4a --- /dev/null +++ b/frontend/sync-client/src/persistence/settings.ts @@ -0,0 +1,86 @@ +import { Logger, LogLevel } from "src/tracing/logger"; + +export interface SyncSettings { + remoteUri: string; + token: string; + vaultName: string; + fetchChangesUpdateIntervalMs: number; + syncConcurrency: number; + isSyncEnabled: boolean; + displayNoopSyncEvents: boolean; + minimumLogLevel: LogLevel; + maxFileSizeMB: number; +} + +const DEFAULT_SETTINGS: SyncSettings = { + remoteUri: "", + token: "", + vaultName: "default", + fetchChangesUpdateIntervalMs: 1000, + syncConcurrency: 1, + isSyncEnabled: false, + displayNoopSyncEvents: false, + minimumLogLevel: LogLevel.INFO, + maxFileSizeMB: 10 +}; + +export class Settings { + private settings: SyncSettings; + + private readonly onSettingsChangeHandlers: (( + newSettings: SyncSettings, + oldSettings: SyncSettings + ) => void)[] = []; + + public constructor( + initialState: Partial | undefined, + private readonly saveData: (data: unknown) => Promise + ) { + this.settings = { + ...DEFAULT_SETTINGS, + ...(initialState ?? {}) + }; + + Logger.getInstance().debug( + `Loaded settings: ${JSON.stringify(this.settings, null, 2)}` + ); + } + + public getSettings(): SyncSettings { + return this.settings; + } + + public async setSettings(value: SyncSettings): Promise { + const oldSettings = this.settings; + this.settings = value; + this.onSettingsChangeHandlers.forEach((handler) => { + handler(value, oldSettings); + }); + await this.save(); + } + + public addOnSettingsChangeHandlers( + handler: (settings: SyncSettings, oldSettings: SyncSettings) => void + ): void { + this.onSettingsChangeHandlers.push(handler); + } + + public async setSetting( + key: T, + value: SyncSettings[T] + ): Promise { + const newSettings = { ...this.settings, [key]: value }; + Logger.getInstance().debug( + `Setting ${key} to ${value}, new settings: ${JSON.stringify( + newSettings, + null, + 2 + )}` + ); + await this.setSettings(newSettings); + } + + private async save(): Promise { + await this.saveData(this.settings); + } +} diff --git a/frontend/sync-client/src/services/sync-service.ts b/frontend/sync-client/src/services/sync-service.ts index f335ed67..478e803e 100644 --- a/frontend/sync-client/src/services/sync-service.ts +++ b/frontend/sync-client/src/services/sync-service.ts @@ -1,15 +1,15 @@ import type { Client } from "openapi-fetch"; import createClient from "openapi-fetch"; import type { components, paths } from "./types"; // Generated by openapi-typescript -import type { Database } from "../database/database"; -import type { SyncSettings } from "../database/sync-settings"; import type { DocumentId, RelativePath, VaultUpdateId -} from "src/database/document-metadata"; +} from "../persistence/database"; import { Logger } from "src/tracing/logger"; import { retriedFetch } from "src/utils/retried-fetch"; +import type { SyncSettings } from "dist/types"; +import type { Settings } from "src/persistence/settings"; export interface CheckConnectionResult { isSuccessful: boolean; @@ -19,10 +19,10 @@ export class SyncService { private client: Client; private clientWithoutRetries: Client; - public constructor(private readonly database: Database) { - this.createClient(database.getSettings()); + public constructor(private readonly settings: Settings) { + this.createClient(settings.getSettings()); - database.addOnSettingsChangeHandlers((s) => { + settings.addOnSettingsChangeHandlers((s) => { this.createClient(s); }); } @@ -43,7 +43,7 @@ export class SyncService { const response = await this.clientWithoutRetries.GET("/ping", { params: { header: { - authorization: `Bearer ${this.database.getSettings().token}` + authorization: `Bearer ${this.settings.getSettings().token}` } } }); @@ -80,10 +80,10 @@ export class SyncService { { params: { path: { - vault_id: this.database.getSettings().vaultName + vault_id: this.settings.getSettings().vaultName }, header: { - authorization: `Bearer ${this.database.getSettings().token}` + authorization: `Bearer ${this.settings.getSettings().token}` } }, // eslint-disable-next-line @@ -130,11 +130,11 @@ export class SyncService { { params: { path: { - vault_id: this.database.getSettings().vaultName, + vault_id: this.settings.getSettings().vaultName, document_id: documentId }, header: { - authorization: `Bearer ${this.database.getSettings().token}` + authorization: `Bearer ${this.settings.getSettings().token}` } }, // eslint-disable-next-line @@ -171,11 +171,11 @@ export class SyncService { { params: { path: { - vault_id: this.database.getSettings().vaultName, + vault_id: this.settings.getSettings().vaultName, document_id: documentId }, header: { - authorization: `Bearer ${this.database.getSettings().token}` + authorization: `Bearer ${this.settings.getSettings().token}` } }, body: { @@ -206,11 +206,11 @@ export class SyncService { { params: { path: { - vault_id: this.database.getSettings().vaultName, + vault_id: this.settings.getSettings().vaultName, document_id: documentId }, header: { - authorization: `Bearer ${this.database.getSettings().token}` + authorization: `Bearer ${this.settings.getSettings().token}` } } } @@ -235,10 +235,10 @@ export class SyncService { const response = await this.client.GET("/vaults/{vault_id}/documents", { params: { path: { - vault_id: this.database.getSettings().vaultName + vault_id: this.settings.getSettings().vaultName }, header: { - authorization: `Bearer ${this.database.getSettings().token}` + authorization: `Bearer ${this.settings.getSettings().token}` }, query: { since_update_id: since diff --git a/frontend/sync-client/src/sync-operations/apply-remote-changes-locally.ts b/frontend/sync-client/src/sync-operations/apply-remote-changes-locally.ts index 706b93c0..5d630fe3 100644 --- a/frontend/sync-client/src/sync-operations/apply-remote-changes-locally.ts +++ b/frontend/sync-client/src/sync-operations/apply-remote-changes-locally.ts @@ -1,20 +1,23 @@ -import type { Database } from "../database/database"; +import type { Database } from "../persistence/database"; import type { SyncService } from "src/services/sync-service"; import { Logger } from "src/tracing/logger"; import type { Syncer } from "./syncer"; +import type { Settings } from "src/persistence/settings"; let isRunning = false; export async function applyRemoteChangesLocally({ + settings, database, syncService, syncer }: { + settings: Settings; database: Database; syncService: SyncService; syncer: Syncer; }): Promise { - if (!database.getSettings().isSyncEnabled) { + if (!settings.getSettings().isSyncEnabled) { Logger.getInstance().debug( `Syncing is disabled, not fetching remote changes` ); diff --git a/frontend/sync-client/src/sync-operations/document-lock.test.ts b/frontend/sync-client/src/sync-operations/document-lock.test.ts index 5b28de18..8def7e6b 100644 --- a/frontend/sync-client/src/sync-operations/document-lock.test.ts +++ b/frontend/sync-client/src/sync-operations/document-lock.test.ts @@ -1,4 +1,4 @@ -import { RelativePath } from "../database/document-metadata"; +import { RelativePath } from "../persistence/database"; import { tryLockDocument, waitForDocumentLock, diff --git a/frontend/sync-client/src/sync-operations/document-lock.ts b/frontend/sync-client/src/sync-operations/document-lock.ts index 55811662..28d97f35 100644 --- a/frontend/sync-client/src/sync-operations/document-lock.ts +++ b/frontend/sync-client/src/sync-operations/document-lock.ts @@ -1,4 +1,4 @@ -import type { RelativePath } from "../database/document-metadata"; +import type { RelativePath } from "../persistence/database"; const locked = new Set(); const waiters = new Map void)[]>(); diff --git a/frontend/sync-client/src/sync-operations/syncer.ts b/frontend/sync-client/src/sync-operations/syncer.ts index deae7d22..9d190860 100644 --- a/frontend/sync-client/src/sync-operations/syncer.ts +++ b/frontend/sync-client/src/sync-operations/syncer.ts @@ -1,8 +1,9 @@ -import type { Database } from "../database/database"; import type { + Database, DocumentMetadata, RelativePath -} from "src/database/document-metadata"; +} from "../persistence/database"; + import type { FileOperations } from "src/file-operations"; import type { SyncService } from "src/services/sync-service"; import { Logger } from "src/tracing/logger"; @@ -13,6 +14,7 @@ import PQueue from "p-queue"; import { EMPTY_HASH, hash } from "src/utils/hash"; import type { components } from "src/services/types"; import { deserialize } from "src/utils/deserialize"; +import type { Settings } from "src/persistence/settings"; export class Syncer { private readonly remainingOperationsListeners: (( @@ -25,16 +27,17 @@ export class Syncer { public constructor( private readonly database: Database, + private readonly settings: Settings, private readonly syncService: SyncService, private readonly operations: FileOperations, private readonly history: SyncHistory ) { this.syncQueue = new PQueue({ - concurrency: database.getSettings().syncConcurrency + concurrency: settings.getSettings().syncConcurrency }); - database.addOnSettingsChangeHandlers((settings) => { - this.syncQueue.concurrency = settings.syncConcurrency; + settings.addOnSettingsChangeHandlers((newSettings) => { + this.syncQueue.concurrency = newSettings.syncConcurrency; }); this.syncQueue.on("active", () => { @@ -91,7 +94,7 @@ export class Syncer { return; } - if (!this.database.getSettings().isSyncEnabled) { + if (!this.settings.getSettings().isSyncEnabled) { Logger.getInstance().debug( `Syncing is disabled, not uploading local changes` ); @@ -229,13 +232,13 @@ export class Syncer { (await this.operations.getFileSize(relativePath)) / 1024 / 1024 > - this.database.getSettings().maxFileSizeMB + this.settings.getSettings().maxFileSizeMB ) { this.history.addHistoryEntry({ status: SyncStatus.ERROR, relativePath, message: `File size exceeds the maximum file size limit of ${ - this.database.getSettings().maxFileSizeMB + this.settings.getSettings().maxFileSizeMB }MB`, type: SyncType.CREATE }); @@ -332,13 +335,13 @@ export class Syncer { (await this.operations.getFileSize(relativePath)) / 1024 / 1024 > - this.database.getSettings().maxFileSizeMB + this.settings.getSettings().maxFileSizeMB ) { this.history.addHistoryEntry({ status: SyncStatus.ERROR, relativePath, message: `File size exceeds the maximum file size limit of ${ - this.database.getSettings().maxFileSizeMB + this.settings.getSettings().maxFileSizeMB }MB`, type: SyncType.CREATE }); @@ -648,7 +651,7 @@ export class Syncer { syncSource: SyncSource, fn: () => Promise ): Promise { - if (!this.database.getSettings().isSyncEnabled) { + if (!this.settings.getSettings().isSyncEnabled) { Logger.getInstance().info( `Syncing is disabled, not syncing ${relativePath}` ); diff --git a/frontend/sync-client/src/tracing/logger.ts b/frontend/sync-client/src/tracing/logger.ts index 3d7e95bd..ea6e39bb 100644 --- a/frontend/sync-client/src/tracing/logger.ts +++ b/frontend/sync-client/src/tracing/logger.ts @@ -12,7 +12,7 @@ const LOG_LEVEL_ORDER = { [LogLevel.ERROR]: 3 }; -class LogLine { +export class LogLine { public timestamp = new Date(); public constructor( public level: LogLevel, @@ -46,19 +46,16 @@ export class Logger { public info(message: string): void { console.info(message); - this.pushMessage(message, LogLevel.INFO); } public warn(message: string): void { console.warn(message); - this.pushMessage(message, LogLevel.WARNING); } public error(message: string): void { console.error(message); - this.pushMessage(message, LogLevel.ERROR); } diff --git a/frontend/sync-client/src/tracing/sync-history.ts b/frontend/sync-client/src/tracing/sync-history.ts index a059a9fc..1ada37c6 100644 --- a/frontend/sync-client/src/tracing/sync-history.ts +++ b/frontend/sync-client/src/tracing/sync-history.ts @@ -1,4 +1,4 @@ -import type { RelativePath } from "src/database/document-metadata"; +import { RelativePath } from "src/persistence/database"; import { Logger } from "./logger"; export interface CommonHistoryEntry {