From 1a05e184a72b8f692bd522352a05c186e5c92fea Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 29 Mar 2025 09:42:24 +0000 Subject: [PATCH] Show WebSocket status on UI --- .../src/views/settings/settings-tab.ts | 2 +- .../status-description/status-description.ts | 24 +++++++--- frontend/sync-client/src/index.ts | 2 +- .../sync-client/src/services/sync-service.ts | 4 +- frontend/sync-client/src/sync-client.ts | 20 ++++++-- .../sync-client/src/sync-operations/syncer.ts | 47 +++++++++++++------ 6 files changed, 71 insertions(+), 28 deletions(-) diff --git a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts index 6f27e3f0..6c21e7af 100644 --- a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts +++ b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts @@ -222,7 +222,7 @@ export class SyncSettingsTab extends PluginSettingTab { .addButton((button) => button.setButtonText("Test connection").onClick(async () => { new Notice( - (await this.syncClient.checkConnection()).message + (await this.syncClient.checkConnection()).serverMessage ); await this.statusDescription.updateConnectionState(); }) diff --git a/frontend/obsidian-plugin/src/views/status-description/status-description.ts b/frontend/obsidian-plugin/src/views/status-description/status-description.ts index 87c0f6db..6d5ac693 100644 --- a/frontend/obsidian-plugin/src/views/status-description/status-description.ts +++ b/frontend/obsidian-plugin/src/views/status-description/status-description.ts @@ -2,14 +2,14 @@ import "./status-description.scss"; import type { HistoryStats, - CheckConnectionResult, + NetworkConnectionStatus, SyncClient } from "sync-client"; export class StatusDescription { private lastHistoryStats: HistoryStats | undefined; private lastRemaining: number | undefined; - private lastConnectionState: CheckConnectionResult | undefined; + private lastConnectionState: NetworkConnectionStatus | undefined; private statusChangeListeners: (() => void)[] = []; @@ -28,9 +28,13 @@ export class StatusDescription { } ); - this.syncClient.addOnSettingsChangeListener(() => { - void this.updateConnectionState(); - }); + this.syncClient.addWebSocketStatusChangeListener( + () => void this.updateConnectionState() + ); + + this.syncClient.addOnSettingsChangeListener( + () => void this.updateConnectionState() + ); } public async updateConnectionState(): Promise { @@ -61,7 +65,15 @@ export class StatusDescription { if (!this.lastConnectionState.isSuccessful) { container.createSpan({ - text: `VaultLink failed to connect to the remote server with the error "${this.lastConnectionState.message}"`, + text: `VaultLink failed to connect to the remote server with error '${this.lastConnectionState.serverMessage}'`, + cls: "error" + }); + return; + } + + if (!this.lastConnectionState.isWebSocketConnected) { + container.createSpan({ + text: `${this.lastConnectionState.serverMessage} but the WebSocket connection could not be established.`, cls: "error" }); return; diff --git a/frontend/sync-client/src/index.ts b/frontend/sync-client/src/index.ts index d40b5aab..6c36965f 100644 --- a/frontend/sync-client/src/index.ts +++ b/frontend/sync-client/src/index.ts @@ -5,10 +5,10 @@ export { type HistoryEntry } from "./tracing/sync-history"; export { Logger, LogLevel, LogLine } from "./tracing/logger"; -export type { CheckConnectionResult } from "./services/sync-service"; export { type SyncSettings } from "./persistence/settings"; export type { RelativePath, StoredDatabase } from "./persistence/database"; export type { FileSystemOperations } from "./file-operations/filesystem-operations"; export type { PersistenceProvider } from "./persistence/persistence"; +export type { NetworkConnectionStatus } from "./sync-client"; export { SyncClient } from "./sync-client"; diff --git a/frontend/sync-client/src/services/sync-service.ts b/frontend/sync-client/src/services/sync-service.ts index 7997c157..3d84947c 100644 --- a/frontend/sync-client/src/services/sync-service.ts +++ b/frontend/sync-client/src/services/sync-service.ts @@ -307,13 +307,13 @@ export class SyncService { if (result.isAuthenticated) { return { isSuccessful: true, - message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated.` + message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated` }; } return { isSuccessful: false, - message: `Successfully connected to server (version: ${result.serverVersion}) but failed to authenticate.` + message: `Successfully connected to server (version: ${result.serverVersion}) but failed to authenticate` }; } catch (e) { return { diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index f771ae06..27aa2172 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -8,7 +8,6 @@ import type { RelativePath, StoredDatabase } from "./persistence/database"; import { Database } from "./persistence/database"; import type { SyncSettings } from "./persistence/settings"; import { Settings } from "./persistence/settings"; -import type { CheckConnectionResult } from "./services/sync-service"; import { SyncService } from "./services/sync-service"; import { Syncer } from "./sync-operations/syncer"; import type { FileSystemOperations } from "./file-operations/filesystem-operations"; @@ -16,6 +15,12 @@ import { FileOperations } from "./file-operations/file-operations"; import { ConnectionStatus } from "./services/connection-status"; import { UnrestrictedSyncer } from "./sync-operations/unrestricted-syncer"; +export interface NetworkConnectionStatus { + isSuccessful: boolean; + serverMessage: string; + isWebSocketConnected: boolean; +} + export class SyncClient { // eslint-disable-next-line @typescript-eslint/max-params private constructor( @@ -134,8 +139,13 @@ export class SyncClient { return client; } - public async checkConnection(): Promise { - return this.syncService.checkConnection(); + public async checkConnection(): Promise { + const server = await this.syncService.checkConnection(); + return { + isSuccessful: server.isSuccessful, + serverMessage: server.message, + isWebSocketConnected: this.syncer.isWebSocketConnected + }; } public getHistoryEntries(): readonly HistoryEntry[] { @@ -202,6 +212,10 @@ export class SyncClient { this.syncer.addRemainingOperationsListener(listener); } + public addWebSocketStatusChangeListener(listener: () => void): void { + this.syncer.addWebSocketStatusChangeListener(listener); + } + public async syncLocallyCreatedFile( relativePath: RelativePath ): Promise { diff --git a/frontend/sync-client/src/sync-operations/syncer.ts b/frontend/sync-client/src/sync-operations/syncer.ts index fa4f5afe..3a956874 100644 --- a/frontend/sync-client/src/sync-operations/syncer.ts +++ b/frontend/sync-client/src/sync-operations/syncer.ts @@ -22,6 +22,7 @@ export class Syncer { private readonly remainingOperationsListeners: (( remainingOperations: number ) => void)[] = []; + private readonly webSocketStatusChangeListeners: (() => void)[] = []; private readonly syncQueue: PQueue; private runningScheduleSyncForOfflineChanges: Promise | undefined; @@ -70,15 +71,8 @@ export class Syncer { this.setWebSocketRefreshInterval(); } - public async reset(): Promise { - await this.waitUntilFinished(); - this.setWebSocketRefreshInterval(); - this.updateWebSocket(this.settings.getSettings()); - } - - public stop(): void { - clearInterval(this.refreshApplyRemoteChangesWebSocketInterval); - this.applyRemoteChangesWebSocket?.close(); + public get isWebSocketConnected(): boolean { + return this.applyRemoteChangesWebSocket?.readyState === WebSocket.OPEN; } public addRemainingOperationsListener( @@ -87,6 +81,10 @@ export class Syncer { this.remainingOperationsListeners.push(listener); } + public addWebSocketStatusChangeListener(listener: () => void): void { + this.webSocketStatusChangeListeners.push(listener); + } + public async syncLocallyCreatedFile( relativePath: RelativePath ): Promise { @@ -245,6 +243,17 @@ export class Syncer { return this.syncQueue.onEmpty(); } + public async reset(): Promise { + await this.waitUntilFinished(); + this.setWebSocketRefreshInterval(); + this.updateWebSocket(this.settings.getSettings()); + } + + public stop(): void { + clearInterval(this.refreshApplyRemoteChangesWebSocketInterval); + this.applyRemoteChangesWebSocket?.close(); + } + private updateWebSocket(settings: SyncSettings): void { this.applyRemoteChangesWebSocket?.close(); @@ -277,14 +286,22 @@ export class Syncer { } ); - this.applyRemoteChangesWebSocket.onerror = (event): void => { - console.error(event); - this.logger.error(`WebSocket error`); + // The JS WebSocket API doesn't support setting headers, so we have to send the token as a message + this.applyRemoteChangesWebSocket.onopen = (): void => { + this.applyRemoteChangesWebSocket?.send(settings.token); + this.webSocketStatusChangeListeners.forEach((listener) => { + listener(); + }); }; - // The JS WebSocket API doesn't support setting headers, so we have to send the token as a message - this.applyRemoteChangesWebSocket.onopen = (): void => - this.applyRemoteChangesWebSocket?.send(settings.token); + this.applyRemoteChangesWebSocket.onclose = (event): void => { + this.logger.error( + `WebSocket closed with code ${event.code}: ${event.reason}` + ); + this.webSocketStatusChangeListeners.forEach((listener) => { + listener(); + }); + }; } private setWebSocketRefreshInterval(): void {