Show WebSocket status on UI

This commit is contained in:
Andras Schmelczer 2025-03-29 09:42:24 +00:00
parent a249919061
commit 1a05e184a7
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
6 changed files with 71 additions and 28 deletions

View file

@ -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();
})

View file

@ -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<void> {
@ -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;

View file

@ -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";

View file

@ -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 {

View file

@ -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<CheckConnectionResult> {
return this.syncService.checkConnection();
public async checkConnection(): Promise<NetworkConnectionStatus> {
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<void> {

View file

@ -22,6 +22,7 @@ export class Syncer {
private readonly remainingOperationsListeners: ((
remainingOperations: number
) => void)[] = [];
private readonly webSocketStatusChangeListeners: (() => void)[] = [];
private readonly syncQueue: PQueue;
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
@ -70,15 +71,8 @@ export class Syncer {
this.setWebSocketRefreshInterval();
}
public async reset(): Promise<void> {
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<void> {
@ -245,6 +243,17 @@ export class Syncer {
return this.syncQueue.onEmpty();
}
public async reset(): Promise<void> {
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 {