Use websocket on the FE
This commit is contained in:
parent
48ca6e7f7e
commit
0320308f1a
2 changed files with 81 additions and 66 deletions
|
|
@ -152,7 +152,12 @@ export class SyncClient {
|
||||||
await this.syncer.scheduleSyncForOfflineChanges();
|
await this.syncer.scheduleSyncForOfflineChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
this.syncer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
public async waitAndStop(): Promise<void> {
|
public async waitAndStop(): Promise<void> {
|
||||||
|
this.stop();
|
||||||
await this.syncer.waitUntilFinished();
|
await this.syncer.waitUntilFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,6 +165,7 @@ export class SyncClient {
|
||||||
/// and the local database but retain the settings.
|
/// and the local database but retain the settings.
|
||||||
/// The SyncClient can be used again after calling this method.
|
/// The SyncClient can be used again after calling this method.
|
||||||
public async reset(): Promise<void> {
|
public async reset(): Promise<void> {
|
||||||
|
this.stop();
|
||||||
this.connectionStatus.startReset();
|
this.connectionStatus.startReset();
|
||||||
await this.syncer.reset();
|
await this.syncer.reset();
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import PQueue from "p-queue";
|
||||||
import { hash } from "../utils/hash";
|
import { hash } from "../utils/hash";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import type { components } from "../services/types";
|
import type { components } from "../services/types";
|
||||||
import type { Settings } from "../persistence/settings";
|
import type { Settings, SyncSettings } from "../persistence/settings";
|
||||||
import type { FileOperations } from "../file-operations/file-operations";
|
import type { FileOperations } from "../file-operations/file-operations";
|
||||||
import { findMatchingFile } from "../utils/find-matching-file";
|
import { findMatchingFile } from "../utils/find-matching-file";
|
||||||
import type { UnrestrictedSyncer } from "./unrestricted-syncer";
|
import type { UnrestrictedSyncer } from "./unrestricted-syncer";
|
||||||
|
|
@ -19,12 +19,15 @@ export class Syncer {
|
||||||
private readonly syncQueue: PQueue;
|
private readonly syncQueue: PQueue;
|
||||||
|
|
||||||
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
|
private runningScheduleSyncForOfflineChanges: Promise<void> | undefined;
|
||||||
private runningApplyRemoteChangesLocally: Promise<void> | undefined;
|
private refreshApplyRemoteChangesWebSocketInterval:
|
||||||
|
| NodeJS.Timeout
|
||||||
|
| undefined;
|
||||||
|
private applyRemoteChangesWebSocket: WebSocket | undefined;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly database: Database,
|
private readonly database: Database,
|
||||||
settings: Settings,
|
private readonly settings: Settings,
|
||||||
private readonly syncService: SyncService,
|
private readonly syncService: SyncService,
|
||||||
private readonly operations: FileOperations,
|
private readonly operations: FileOperations,
|
||||||
private readonly internalSyncer: UnrestrictedSyncer
|
private readonly internalSyncer: UnrestrictedSyncer
|
||||||
|
|
@ -33,11 +36,21 @@ export class Syncer {
|
||||||
concurrency: settings.getSettings().syncConcurrency
|
concurrency: settings.getSettings().syncConcurrency
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.updateWebSocket(settings.getSettings());
|
||||||
|
|
||||||
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
settings.addOnSettingsChangeListener((newSettings, oldSettings) => {
|
||||||
if (newSettings.syncConcurrency === oldSettings.syncConcurrency) {
|
if (
|
||||||
return;
|
newSettings.remoteUri !== oldSettings.remoteUri ||
|
||||||
|
newSettings.vaultName !== oldSettings.vaultName ||
|
||||||
|
newSettings.token !== oldSettings.token ||
|
||||||
|
newSettings.isSyncEnabled !== oldSettings.isSyncEnabled
|
||||||
|
) {
|
||||||
|
this.updateWebSocket(newSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newSettings.syncConcurrency !== oldSettings.syncConcurrency) {
|
||||||
this.syncQueue.concurrency = newSettings.syncConcurrency;
|
this.syncQueue.concurrency = newSettings.syncConcurrency;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.syncQueue.on("active", () => {
|
this.syncQueue.on("active", () => {
|
||||||
|
|
@ -45,6 +58,20 @@ export class Syncer {
|
||||||
listener(this.syncQueue.size);
|
listener(this.syncQueue.size);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.setWebSocketRefreshInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reset(): Promise<void> {
|
||||||
|
await this.waitUntilFinished();
|
||||||
|
this.internalSyncer.reset();
|
||||||
|
this.setWebSocketRefreshInterval();
|
||||||
|
this.updateWebSocket(this.settings.getSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
clearInterval(this.refreshApplyRemoteChangesWebSocketInterval);
|
||||||
|
this.applyRemoteChangesWebSocket?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRemainingOperationsListener(
|
public addRemainingOperationsListener(
|
||||||
|
|
@ -206,78 +233,60 @@ export class Syncer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async applyRemoteChangesLocally(): Promise<void> {
|
|
||||||
if (this.runningApplyRemoteChangesLocally !== undefined) {
|
|
||||||
this.logger.debug(
|
|
||||||
"Applying remote changes locally is already in progress"
|
|
||||||
);
|
|
||||||
return this.runningApplyRemoteChangesLocally;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.runningApplyRemoteChangesLocally =
|
|
||||||
this.internalApplyRemoteChangesLocally();
|
|
||||||
await this.runningApplyRemoteChangesLocally;
|
|
||||||
this.logger.info("All remote changes have been applied locally");
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof SyncResetError) {
|
|
||||||
this.logger.info(
|
|
||||||
"Failed to apply remote changes locally due to a reset"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logger.error(`Failed to apply remote changes locally: ${e}`);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
this.runningApplyRemoteChangesLocally = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async reset(): Promise<void> {
|
|
||||||
await this.waitUntilFinished();
|
|
||||||
this.internalSyncer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async waitUntilFinished(): Promise<void> {
|
public async waitUntilFinished(): Promise<void> {
|
||||||
await Promise.allSettled([
|
await this.runningScheduleSyncForOfflineChanges;
|
||||||
this.runningScheduleSyncForOfflineChanges,
|
|
||||||
this.runningApplyRemoteChangesLocally
|
|
||||||
]);
|
|
||||||
return this.syncQueue.onEmpty();
|
return this.syncQueue.onEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalApplyRemoteChangesLocally(): Promise<void> {
|
private updateWebSocket(settings: SyncSettings): void {
|
||||||
const remote = await this.syncQueue.add(async () =>
|
this.applyRemoteChangesWebSocket?.close();
|
||||||
this.syncService.getAll(this.database.getLastSeenUpdateId())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!remote) {
|
if (!settings.isSyncEnabled) {
|
||||||
throw new Error("Failed to fetch remote changes");
|
this.applyRemoteChangesWebSocket = undefined;
|
||||||
}
|
|
||||||
|
|
||||||
if (remote.latestDocuments.length === 0) {
|
|
||||||
this.logger.debug("No remote changes to apply");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info("Applying remote changes locally");
|
const wsUri = new URL(settings.remoteUri);
|
||||||
|
wsUri.protocol = wsUri.protocol === "https" ? "wss" : "ws";
|
||||||
|
wsUri.pathname = `/vaults/${settings.vaultName}/ws`;
|
||||||
|
this.applyRemoteChangesWebSocket = new WebSocket(wsUri);
|
||||||
|
|
||||||
await Promise.all(
|
this.applyRemoteChangesWebSocket.onmessage = (event): void =>
|
||||||
remote.latestDocuments.map(this.syncRemotelyUpdatedFile.bind(this))
|
void this.syncRemotelyUpdatedFile(event.data).catch(
|
||||||
|
(e: unknown) => {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to sync remotely updated file: ${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastSeenUpdateId = this.database.getLastSeenUpdateId();
|
this.applyRemoteChangesWebSocket.onerror = (event): void => {
|
||||||
if (
|
console.error(event);
|
||||||
lastSeenUpdateId === undefined ||
|
this.logger.error(`WebSocket error`);
|
||||||
lastSeenUpdateId < remote.lastUpdateId
|
};
|
||||||
) {
|
|
||||||
this.database.setLastSeenUpdateId(remote.lastUpdateId);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncRemotelyUpdatedFile(
|
private setWebSocketRefreshInterval(): void {
|
||||||
remoteVersion: components["schemas"]["DocumentVersionWithoutContent"]
|
this.refreshApplyRemoteChangesWebSocketInterval = setInterval(() => {
|
||||||
): Promise<void> {
|
if (
|
||||||
|
this.applyRemoteChangesWebSocket?.readyState === WebSocket.OPEN
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateWebSocket(this.settings.getSettings());
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncRemotelyUpdatedFile(message: string): Promise<void> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
|
const remoteVersion = JSON.parse(
|
||||||
|
message
|
||||||
|
) as components["schemas"]["DocumentVersionWithoutContent"];
|
||||||
|
|
||||||
let document = this.database.getDocumentByDocumentId(
|
let document = this.database.getDocumentByDocumentId(
|
||||||
remoteVersion.documentId
|
remoteVersion.documentId
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue