import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js"; import type { Client } from "openapi-fetch"; import createClient from "openapi-fetch"; import type { components, paths } from "./types.js"; // Generated by openapi-typescript import type { Database } from "src/database/database"; import type { SyncSettings } from "src/database/sync-settings"; import type { DocumentId, RelativePath, VaultUpdateId, } from "src/database/document-metadata"; import { Logger } from "src/tracing/logger.js"; import { retriedFetch } from "src/utils/retried-fetch.js"; export interface CheckConnectionResult { isSuccessful: boolean; message: string; } export class SyncService { private client: Client; private clientWithoutRetries: Client; public constructor(private readonly database: Database) { this.createClient(database.getSettings()); database.addOnSettingsChangeHandlers((s) => { this.createClient(s); }); } private static formatError( error: components["schemas"]["SerializedError"] ): string { let result = error.message; if (error.causes.length > 0) { const causes = error.causes.join(", "); result += ` caused by: ${causes}`; } return result; } public async ping(): Promise { const response = await this.clientWithoutRetries.GET("/ping", { params: { header: { authorization: `Bearer ${ this.database.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, createdDate, }: { relativePath: RelativePath; contentBytes: Uint8Array; createdDate: Date; }): Promise { const response = await this.client.POST( "/vaults/{vault_id}/documents", { params: { path: { vault_id: this.database.getSettings().vaultName, }, header: { authorization: `Bearer ${ this.database.getSettings().token }`, }, }, body: { contentBase64: lib.bytesToBase64(contentBytes), createdDate: createdDate.toISOString(), relativePath, }, } ); if (!response.data) { throw new Error( `Failed to create document: ${SyncService.formatError( response.error )}` ); } Logger.getInstance().debug( `Created document ${JSON.stringify( response.data.relativePath )} with id ${response.data.documentId}` ); return response.data; } public async put({ parentVersionId, documentId, relativePath, contentBytes, createdDate, }: { parentVersionId: VaultUpdateId; documentId: DocumentId; relativePath: RelativePath; contentBytes: Uint8Array; createdDate: Date; }): Promise { const response = await this.client.PUT( "/vaults/{vault_id}/documents/{document_id}", { params: { path: { vault_id: this.database.getSettings().vaultName, document_id: documentId, }, header: { authorization: `Bearer ${ this.database.getSettings().token }`, }, }, body: { parentVersionId, contentBase64: lib.bytesToBase64(contentBytes), createdDate: createdDate.toISOString(), relativePath, }, } ); if (!response.data) { throw new Error( `Failed to update document: ${SyncService.formatError( response.error )}` ); } Logger.getInstance().debug( `Updated document ${response.data.relativePath} with id ${response.data.documentId}` ); return response.data; } public async delete({ documentId, relativePath, createdDate, }: { documentId: DocumentId; relativePath: RelativePath; createdDate: Date; }): Promise { const response = await this.client.DELETE( "/vaults/{vault_id}/documents/{document_id}", { params: { path: { vault_id: this.database.getSettings().vaultName, document_id: documentId, }, header: { authorization: `Bearer ${ this.database.getSettings().token }`, }, }, body: { createdDate: createdDate.toISOString(), relativePath, }, } ); if (response.error) { throw new Error(`Failed to delete document`); } Logger.getInstance().debug( `Deleted document ${relativePath} with id ${documentId}` ); return response.data; } public async get({ documentId, }: { documentId: DocumentId; }): Promise { const response = await this.client.GET( "/vaults/{vault_id}/documents/{document_id}", { params: { path: { vault_id: this.database.getSettings().vaultName, document_id: documentId, }, header: { authorization: `Bearer ${ this.database.getSettings().token }`, }, }, } ); if (!response.data) { throw new Error( `Failed to get document: ${SyncService.formatError( response.error )}` ); } Logger.getInstance().debug( `Get document ${response.data.relativePath} with id ${response.data.documentId}` ); return response.data; } public async getAll( since?: VaultUpdateId ): Promise { const response = await this.client.GET("/vaults/{vault_id}/documents", { params: { path: { vault_id: this.database.getSettings().vaultName, }, header: { authorization: `Bearer ${ this.database.getSettings().token }`, }, query: { since_update_id: since, }, }, }); const { error } = response; if (error) { throw new Error( `Failed to get documents: ${SyncService.formatError( response.error )}` ); } Logger.getInstance().debug( `Got ${response.data.latestDocuments.length} document metadata` ); return response.data; } public async checkConnection(): Promise { try { const result = await this.ping(); if (result.isAuthenticated) { return { isSuccessful: true, 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.`, }; } catch (e) { return { isSuccessful: false, message: `Failed to connect to server: ${e}`, }; } } private createClient(settings: SyncSettings): void { this.client = createClient({ baseUrl: settings.remoteUri, fetch: retriedFetch, }); this.clientWithoutRetries = createClient({ baseUrl: settings.remoteUri, }); } }