diff --git a/frontend/sync-client/src/index.ts b/frontend/sync-client/src/index.ts index 631cadea..49272e23 100644 --- a/frontend/sync-client/src/index.ts +++ b/frontend/sync-client/src/index.ts @@ -1,19 +1,6 @@ -export { applyRemoteChangesLocally } from "./sync-operations/apply-remote-changes-locally"; - -export { - Database, - type RelativePath, - type DocumentId, - type VaultUpdateId, - type DocumentMetadata -} from "./persistence/database"; - export { Settings, type SyncSettings } from "./persistence/settings"; -export { - SyncService, - type CheckConnectionResult -} from "./services/sync-service"; +export { type CheckConnectionResult } from "./services/sync-service"; export { Syncer } from "./sync-operations/syncer"; @@ -25,26 +12,17 @@ export { type HistoryStats, type HistoryEntry } from "./tracing/sync-history"; - export { Logger, LogLevel } from "./tracing/logger"; +export { SyncClient } from "./sync-client"; export { type FileOperations } from "./file-operations"; - -import init from "sync_lib"; -import wasmBin from "sync_lib/sync_lib_bg.wasm"; +export { type RelativePath } from "./persistence/database"; +export type { PersistenceProvider } from "./persistence/persistence"; export { isFileTypeMergable, mergeText, bytesToBase64, base64ToBytes, - merge, - isBinary + merge } 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/persistence.ts b/frontend/sync-client/src/persistence/persistence.ts new file mode 100644 index 00000000..43f992b9 --- /dev/null +++ b/frontend/sync-client/src/persistence/persistence.ts @@ -0,0 +1,4 @@ +export interface PersistenceProvider { + load: () => Promise; + save: (data: unknown) => Promise; +} diff --git a/frontend/sync-client/src/services/sync-service.ts b/frontend/sync-client/src/services/sync-service.ts index 478e803e..ec31c0d1 100644 --- a/frontend/sync-client/src/services/sync-service.ts +++ b/frontend/sync-client/src/services/sync-service.ts @@ -1,6 +1,6 @@ import type { Client } from "openapi-fetch"; import createClient from "openapi-fetch"; -import type { components, paths } from "./types"; // Generated by openapi-typescript +import type { components, paths } from "./types"; // generated by openapi-typescript import type { DocumentId, RelativePath, @@ -16,8 +16,8 @@ export interface CheckConnectionResult { message: string; } export class SyncService { - private client: Client; - private clientWithoutRetries: Client; + private client!: Client; + private clientWithoutRetries!: Client; public constructor(private readonly settings: Settings) { this.createClient(settings.getSettings()); @@ -39,28 +39,6 @@ export class SyncService { return result; } - public async ping(): Promise { - const response = await this.clientWithoutRetries.GET("/ping", { - params: { - header: { - authorization: `Bearer ${this.settings.getSettings().token}` - } - } - }); - - Logger.getInstance().debug( - `Ping response: ${JSON.stringify(response.data)}` - ); - - if (!response.data) { - throw new Error( - `Failed to ping server: ${SyncService.formatError(response.error)}` - ); - } - - return response.data; - } - public async create({ relativePath, contentBytes, @@ -282,6 +260,28 @@ export class SyncService { } } + private async ping(): Promise { + const response = await this.clientWithoutRetries.GET("/ping", { + params: { + header: { + authorization: `Bearer ${this.settings.getSettings().token}` + } + } + }); + + Logger.getInstance().debug( + `Ping response: ${JSON.stringify(response.data)}` + ); + + if (!response.data) { + throw new Error( + `Failed to ping server: ${SyncService.formatError(response.error)}` + ); + } + + return response.data; + } + private createClient(settings: SyncSettings): void { this.client = createClient({ baseUrl: settings.remoteUri, diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts new file mode 100644 index 00000000..6e35148c --- /dev/null +++ b/frontend/sync-client/src/sync-client.ts @@ -0,0 +1,168 @@ +import init from "sync_lib"; +import wasmBin from "sync_lib/sync_lib_bg.wasm"; +import type { PersistenceProvider } from "./persistence/persistence"; +import { SyncHistory } from "./tracing/sync-history"; +import type { FileOperations } from "./file-operations"; +import { Logger } from "./tracing/logger"; +import { Database } from "./persistence/database"; +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 { applyRemoteChangesLocally } from "./sync-operations/apply-remote-changes-locally"; + +export class SyncClient { + private remoteListenerIntervalId: number | null = null; + + private constructor( + private readonly _history: SyncHistory, + private readonly _settings: Settings, + private readonly _database: Database, + private readonly _syncer: Syncer, + private readonly _syncService: SyncService + ) {} + + public get history(): SyncHistory { + return this._history; + } + + public get settings(): Settings { + return this._settings; + } + + public get syncer(): Syncer { + return this._syncer; + } + + public static async create( + operations: FileOperations, + persistence: PersistenceProvider + ): Promise { + const history = new SyncHistory(); + Logger.getInstance().info("Starting SyncClient"); + + await init( + // eslint-disable-next-line + (wasmBin as any).default // it is loaded as a base64 string by webpack + ); + + let state: Partial<{ + settings: any; + database: any; + }> = (await persistence.load()) ?? { + settings: undefined, + database: undefined + }; + const database = new Database( + state.database, + async (data: unknown): Promise => { + state = { ...state, database: data }; + return persistence.save(state); + } + ); + + const settings = new Settings( + state.settings, + async (data: unknown): Promise => { + state = { ...state, settings: data }; + return persistence.save(state); + } + ); + + const syncService = new SyncService(settings); + + const syncer = new Syncer( + database, + settings, + syncService, + operations, + history + ); + + const client = new SyncClient( + history, + settings, + database, + syncer, + syncService + ); + + void syncer.scheduleSyncForOfflineChanges(); + + client.registerRemoteEventListener( + settings, + database, + syncService, + syncer, + settings.getSettings().fetchChangesUpdateIntervalMs + ); + + settings.addOnSettingsChangeHandlers((newSettings, oldSettings) => { + client.registerRemoteEventListener( + settings, + database, + syncService, + syncer, + newSettings.fetchChangesUpdateIntervalMs + ); + + if (!oldSettings.isSyncEnabled && newSettings.isSyncEnabled) { + syncer + .scheduleSyncForOfflineChanges() + .catch((_error: unknown) => { + Logger.getInstance().error( + "Failed to schedule sync for offline changes" + ); + }); + } + }); + + Logger.getInstance().info("SyncClient loaded"); + + return client; + } + + public get documentCount(): number { + return this._database.getDocuments().size; + } + + public async checkConnection(): Promise { + return this._syncService.checkConnection(); + } + + public async reset(): Promise { + await this._syncer.reset(); + this._history.reset(); + Logger.getInstance().reset(); + } + + public onunload(): void { + if (this.remoteListenerIntervalId !== null) { + window.clearInterval(this.remoteListenerIntervalId); + } + } + + private registerRemoteEventListener( + settings: Settings, + database: Database, + syncService: SyncService, + syncer: Syncer, + intervalMs: number + ): void { + if (this.remoteListenerIntervalId !== null) { + window.clearInterval(this.remoteListenerIntervalId); + } + + this.remoteListenerIntervalId = window.setInterval( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async () => + applyRemoteChangesLocally({ + settings, + database, + syncService, + syncer + }), + intervalMs + ); + } +} diff --git a/frontend/sync-client/src/tracing/sync-history.ts b/frontend/sync-client/src/tracing/sync-history.ts index 1ada37c6..b64cdd50 100644 --- a/frontend/sync-client/src/tracing/sync-history.ts +++ b/frontend/sync-client/src/tracing/sync-history.ts @@ -1,4 +1,4 @@ -import { RelativePath } from "src/persistence/database"; +import type { RelativePath } from "src/persistence/database"; import { Logger } from "./logger"; export interface CommonHistoryEntry {