Format files

This commit is contained in:
Andras Schmelczer 2025-01-05 15:35:51 +00:00
parent 02486d671e
commit 438caa96a6
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
19 changed files with 557 additions and 585 deletions

View file

@ -16,8 +16,8 @@
"license": "MIT", "license": "MIT",
"prettier": { "prettier": {
"trailingComma": "none", "trailingComma": "none",
"tabWidth": 2, "tabWidth": 4,
"useTabs": false, "useTabs": true,
"endOfLine": "lf" "endOfLine": "lf"
}, },
"devDependencies": { "devDependencies": {

View file

@ -4,7 +4,7 @@ import type {
DocumentId, DocumentId,
DocumentMetadata, DocumentMetadata,
RelativePath, RelativePath,
VaultUpdateId, VaultUpdateId
} from "./document-metadata"; } from "./document-metadata";
import { Logger } from "src/tracing/logger"; import { Logger } from "src/tracing/logger";
@ -47,7 +47,7 @@ export class Database {
this._settings = { this._settings = {
...DEFAULT_SETTINGS, ...DEFAULT_SETTINGS,
...(initialState.settings ?? {}), ...(initialState.settings ?? {})
}; };
Logger.getInstance().debug( Logger.getInstance().debug(
@ -128,7 +128,7 @@ export class Database {
documentId, documentId,
relativePath, relativePath,
parentVersionId, parentVersionId,
hash, hash
}: { }: {
documentId: DocumentId; documentId: DocumentId;
relativePath: RelativePath; relativePath: RelativePath;
@ -138,7 +138,7 @@ export class Database {
this._documents.set(relativePath, { this._documents.set(relativePath, {
documentId, documentId,
parentVersionId, parentVersionId,
hash, hash
}); });
await this.save(); await this.save();
} }
@ -148,7 +148,7 @@ export class Database {
oldRelativePath, oldRelativePath,
relativePath, relativePath,
parentVersionId, parentVersionId,
hash, hash
}: { }: {
documentId: DocumentId; documentId: DocumentId;
oldRelativePath: RelativePath; oldRelativePath: RelativePath;
@ -160,7 +160,7 @@ export class Database {
this._documents.set(relativePath, { this._documents.set(relativePath, {
documentId, documentId,
parentVersionId, parentVersionId,
hash, hash
}); });
await this.save(); await this.save();
} }
@ -180,7 +180,7 @@ export class Database {
await this.saveData({ await this.saveData({
documents: Object.fromEntries(this._documents.entries()), documents: Object.fromEntries(this._documents.entries()),
settings: this._settings, settings: this._settings,
lastSeenUpdateId: this._lastSeenUpdateId, lastSeenUpdateId: this._lastSeenUpdateId
}); });
} }
} }

View file

@ -19,5 +19,5 @@ export const DEFAULT_SETTINGS: SyncSettings = {
syncConcurrency: 1, syncConcurrency: 1,
isSyncEnabled: false, isSyncEnabled: false,
displayNoopSyncEvents: false, displayNoopSyncEvents: false,
minimumLogLevel: LogLevel.INFO, minimumLogLevel: LogLevel.INFO
}; };

View file

@ -39,7 +39,7 @@ export class ObsidianFileEventHandler implements FileEventHandler {
await this.syncer.syncLocallyUpdatedFile({ await this.syncer.syncLocallyUpdatedFile({
oldPath, oldPath,
relativePath: file.path, relativePath: file.path,
updateTime: new Date(file.stat.ctime), updateTime: new Date(file.stat.ctime)
}); });
} else { } else {
Logger.getInstance().debug( Logger.getInstance().debug(
@ -54,7 +54,7 @@ export class ObsidianFileEventHandler implements FileEventHandler {
await this.syncer.syncLocallyUpdatedFile({ await this.syncer.syncLocallyUpdatedFile({
relativePath: file.path, relativePath: file.path,
updateTime: new Date(file.stat.ctime), updateTime: new Date(file.stat.ctime)
}); });
} else { } else {
Logger.getInstance().debug( Logger.getInstance().debug(

View file

@ -1,8 +1,8 @@
import type { Vault } from "obsidian"; import type { Vault } from "obsidian";
import { normalizePath } from "obsidian"; import { normalizePath } from "obsidian";
import type { FileOperations } from "./file-operations"; import type { FileOperations } from "./file-operations";
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import type { RelativePath } from "src/database/document-metadata"; import type { RelativePath } from "src/database/document-metadata";
import { isBinary, mergeText } from "sync_lib";
export class ObsidianFileOperations implements FileOperations { export class ObsidianFileOperations implements FileOperations {
public constructor(private readonly vault: Vault) {} public constructor(private readonly vault: Vault) {}
@ -49,7 +49,7 @@ export class ObsidianFileOperations implements FileOperations {
return new Uint8Array(0); return new Uint8Array(0);
} }
if (lib.isBinary(expectedContent) || !path.endsWith(".md")) { if (isBinary(expectedContent) || !path.endsWith(".md")) {
await this.vault.adapter.writeBinary( await this.vault.adapter.writeBinary(
normalizePath(path), normalizePath(path),
newContent newContent
@ -64,7 +64,7 @@ export class ObsidianFileOperations implements FileOperations {
normalizePath(path), normalizePath(path),
(currentText) => { (currentText) => {
if (currentText !== expetedText) { if (currentText !== expetedText) {
return lib.mergeText(expetedText, currentText, newText); return mergeText(expetedText, currentText, newText);
} }
return newText; return newText;

View file

@ -1,17 +1,16 @@
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import type { Client } from "openapi-fetch"; import type { Client } from "openapi-fetch";
import createClient from "openapi-fetch"; import createClient from "openapi-fetch";
import type { components, paths } from "./types.js"; // Generated by openapi-typescript import type { components, paths } from "./types"; // Generated by openapi-typescript
import type { Database } from "src/database/database"; import type { Database } from "src/database/database";
import type { SyncSettings } from "src/database/sync-settings"; import type { SyncSettings } from "src/database/sync-settings";
import type { import type {
DocumentId, DocumentId,
RelativePath, RelativePath,
VaultUpdateId, VaultUpdateId
} from "src/database/document-metadata"; } from "src/database/document-metadata";
import { Logger } from "src/tracing/logger.js"; import { Logger } from "src/tracing/logger";
import { retriedFetch } from "src/utils/retried-fetch.js"; import { retriedFetch } from "src/utils/retried-fetch";
import { bytesToBase64 } from "sync_lib";
export interface CheckConnectionResult { export interface CheckConnectionResult {
isSuccessful: boolean; isSuccessful: boolean;
@ -45,11 +44,9 @@ export class SyncService {
const response = await this.clientWithoutRetries.GET("/ping", { const response = await this.clientWithoutRetries.GET("/ping", {
params: { params: {
header: { header: {
authorization: `Bearer ${ authorization: `Bearer ${this.database.getSettings().token}`
this.database.getSettings().token }
}`, }
},
},
}); });
Logger.getInstance().debug( Logger.getInstance().debug(
@ -58,9 +55,7 @@ export class SyncService {
if (!response.data) { if (!response.data) {
throw new Error( throw new Error(
`Failed to ping server: ${SyncService.formatError( `Failed to ping server: ${SyncService.formatError(response.error)}`
response.error
)}`
); );
} }
@ -70,7 +65,7 @@ export class SyncService {
public async create({ public async create({
relativePath, relativePath,
contentBytes, contentBytes,
createdDate, createdDate
}: { }: {
relativePath: RelativePath; relativePath: RelativePath;
contentBytes: Uint8Array; contentBytes: Uint8Array;
@ -81,27 +76,23 @@ export class SyncService {
{ {
params: { params: {
path: { path: {
vault_id: this.database.getSettings().vaultName, vault_id: this.database.getSettings().vaultName
}, },
header: { header: {
authorization: `Bearer ${ authorization: `Bearer ${this.database.getSettings().token}`
this.database.getSettings().token }
}`,
},
}, },
body: { body: {
contentBase64: lib.bytesToBase64(contentBytes), contentBase64: bytesToBase64(contentBytes),
createdDate: createdDate.toISOString(), createdDate: createdDate.toISOString(),
relativePath, relativePath
}, }
} }
); );
if (!response.data) { if (!response.data) {
throw new Error( throw new Error(
`Failed to create document: ${SyncService.formatError( `Failed to create document: ${SyncService.formatError(response.error)}`
response.error
)}`
); );
} }
@ -119,7 +110,7 @@ export class SyncService {
documentId, documentId,
relativePath, relativePath,
contentBytes, contentBytes,
createdDate, createdDate
}: { }: {
parentVersionId: VaultUpdateId; parentVersionId: VaultUpdateId;
documentId: DocumentId; documentId: DocumentId;
@ -133,28 +124,24 @@ export class SyncService {
params: { params: {
path: { path: {
vault_id: this.database.getSettings().vaultName, vault_id: this.database.getSettings().vaultName,
document_id: documentId, document_id: documentId
}, },
header: { header: {
authorization: `Bearer ${ authorization: `Bearer ${this.database.getSettings().token}`
this.database.getSettings().token }
}`,
},
}, },
body: { body: {
parentVersionId, parentVersionId,
contentBase64: lib.bytesToBase64(contentBytes), contentBase64: bytesToBase64(contentBytes),
createdDate: createdDate.toISOString(), createdDate: createdDate.toISOString(),
relativePath, relativePath
}, }
} }
); );
if (!response.data) { if (!response.data) {
throw new Error( throw new Error(
`Failed to update document: ${SyncService.formatError( `Failed to update document: ${SyncService.formatError(response.error)}`
response.error
)}`
); );
} }
@ -168,7 +155,7 @@ export class SyncService {
public async delete({ public async delete({
documentId, documentId,
relativePath, relativePath,
createdDate, createdDate
}: { }: {
documentId: DocumentId; documentId: DocumentId;
relativePath: RelativePath; relativePath: RelativePath;
@ -180,18 +167,16 @@ export class SyncService {
params: { params: {
path: { path: {
vault_id: this.database.getSettings().vaultName, vault_id: this.database.getSettings().vaultName,
document_id: documentId, document_id: documentId
}, },
header: { header: {
authorization: `Bearer ${ authorization: `Bearer ${this.database.getSettings().token}`
this.database.getSettings().token }
}`,
},
}, },
body: { body: {
createdDate: createdDate.toISOString(), createdDate: createdDate.toISOString(),
relativePath, relativePath
}, }
} }
); );
@ -207,7 +192,7 @@ export class SyncService {
} }
public async get({ public async get({
documentId, documentId
}: { }: {
documentId: DocumentId; documentId: DocumentId;
}): Promise<components["schemas"]["DocumentVersion"]> { }): Promise<components["schemas"]["DocumentVersion"]> {
@ -217,22 +202,18 @@ export class SyncService {
params: { params: {
path: { path: {
vault_id: this.database.getSettings().vaultName, vault_id: this.database.getSettings().vaultName,
document_id: documentId, document_id: documentId
}, },
header: { header: {
authorization: `Bearer ${ authorization: `Bearer ${this.database.getSettings().token}`
this.database.getSettings().token }
}`, }
},
},
} }
); );
if (!response.data) { if (!response.data) {
throw new Error( throw new Error(
`Failed to get document: ${SyncService.formatError( `Failed to get document: ${SyncService.formatError(response.error)}`
response.error
)}`
); );
} }
@ -249,25 +230,21 @@ export class SyncService {
const response = await this.client.GET("/vaults/{vault_id}/documents", { const response = await this.client.GET("/vaults/{vault_id}/documents", {
params: { params: {
path: { path: {
vault_id: this.database.getSettings().vaultName, vault_id: this.database.getSettings().vaultName
}, },
header: { header: {
authorization: `Bearer ${ authorization: `Bearer ${this.database.getSettings().token}`
this.database.getSettings().token
}`,
}, },
query: { query: {
since_update_id: since, since_update_id: since
}, }
}, }
}); });
const { error } = response; const { error } = response;
if (error) { if (error) {
throw new Error( throw new Error(
`Failed to get documents: ${SyncService.formatError( `Failed to get documents: ${SyncService.formatError(response.error)}`
response.error
)}`
); );
} }
@ -284,18 +261,18 @@ export class SyncService {
if (result.isAuthenticated) { if (result.isAuthenticated) {
return { return {
isSuccessful: true, isSuccessful: true,
message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated.`, message: `Successfully connected to server (version: ${result.serverVersion}) and authenticated.`
}; };
} }
return { return {
isSuccessful: false, 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) { } catch (e) {
return { return {
isSuccessful: false, isSuccessful: false,
message: `Failed to connect to server: ${e}`, message: `Failed to connect to server: ${e}`
}; };
} }
} }
@ -303,11 +280,11 @@ export class SyncService {
private createClient(settings: SyncSettings): void { private createClient(settings: SyncSettings): void {
this.client = createClient<paths>({ this.client = createClient<paths>({
baseUrl: settings.remoteUri, baseUrl: settings.remoteUri,
fetch: retriedFetch, fetch: retriedFetch
}); });
this.clientWithoutRetries = createClient<paths>({ this.clientWithoutRetries = createClient<paths>({
baseUrl: settings.remoteUri, baseUrl: settings.remoteUri
}); });
} }
} }

View file

@ -4,380 +4,382 @@
*/ */
export interface paths { export interface paths {
"/ping": { "/ping": {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
get: { get: {
parameters: { parameters: {
query?: never; query?: never;
header?: { header?: {
authorization?: string; authorization?: string;
}; };
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
requestBody?: never; requestBody?: never;
responses: { responses: {
200: { 200: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["PingResponse"]; "application/json": components["schemas"]["PingResponse"];
}; };
}; };
default: { default: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SerializedError"]; "application/json": components["schemas"]["SerializedError"];
}; };
}; };
}; };
}; };
put?: never; put?: never;
post?: never; post?: never;
delete?: never; delete?: never;
options?: never; options?: never;
head?: never; head?: never;
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/vaults/{vault_id}/documents": { "/vaults/{vault_id}/documents": {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
get: { get: {
parameters: { parameters: {
query?: { query?: {
since_update_id?: number | null; since_update_id?: number | null;
}; };
header: { header: {
authorization: string; authorization: string;
}; };
path: { path: {
vault_id: string; vault_id: string;
}; };
cookie?: never; cookie?: never;
}; };
requestBody?: never; requestBody?: never;
responses: { responses: {
200: { 200: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["FetchLatestDocumentsResponse"]; "application/json": components["schemas"]["FetchLatestDocumentsResponse"];
}; };
}; };
default: { default: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SerializedError"]; "application/json": components["schemas"]["SerializedError"];
}; };
}; };
}; };
}; };
put?: never; put?: never;
post: { post: {
parameters: { parameters: {
query?: never; query?: never;
header: { header: {
authorization: string; authorization: string;
}; };
path: { path: {
vault_id: string; vault_id: string;
}; };
cookie?: never; cookie?: never;
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["CreateDocumentVersion"]; "application/json": components["schemas"]["CreateDocumentVersion"];
}; };
}; };
responses: { responses: {
200: { 200: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["DocumentUpdateResponse"]; "application/json": components["schemas"]["DocumentUpdateResponse"];
}; };
}; };
default: { default: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SerializedError"]; "application/json": components["schemas"]["SerializedError"];
}; };
}; };
}; };
}; };
delete?: never; delete?: never;
options?: never; options?: never;
head?: never; head?: never;
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/vaults/{vault_id}/documents/{document_id}": { "/vaults/{vault_id}/documents/{document_id}": {
parameters: { parameters: {
query?: never; query?: never;
header?: never; header?: never;
path?: never; path?: never;
cookie?: never; cookie?: never;
}; };
get: { get: {
parameters: { parameters: {
query?: never; query?: never;
header: { header: {
authorization: string; authorization: string;
}; };
path: { path: {
document_id: string; document_id: string;
vault_id: string; vault_id: string;
}; };
cookie?: never; cookie?: never;
}; };
requestBody?: never; requestBody?: never;
responses: { responses: {
200: { 200: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["DocumentVersion"]; "application/json": components["schemas"]["DocumentVersion"];
}; };
}; };
default: { default: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SerializedError"]; "application/json": components["schemas"]["SerializedError"];
}; };
}; };
}; };
}; };
put: { put: {
parameters: { parameters: {
query?: never; query?: never;
header: { header: {
authorization: string; authorization: string;
}; };
path: { path: {
document_id: string; document_id: string;
vault_id: string; vault_id: string;
}; };
cookie?: never; cookie?: never;
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["UpdateDocumentVersion"]; "application/json": components["schemas"]["UpdateDocumentVersion"];
}; };
}; };
responses: { responses: {
200: { 200: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["DocumentUpdateResponse"]; "application/json": components["schemas"]["DocumentUpdateResponse"];
}; };
}; };
default: { default: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SerializedError"]; "application/json": components["schemas"]["SerializedError"];
}; };
}; };
}; };
}; };
post?: never; post?: never;
delete: { delete: {
parameters: { parameters: {
query?: never; query?: never;
header: { header: {
authorization: string; authorization: string;
}; };
path: { path: {
document_id: string; document_id: string;
vault_id: string; vault_id: string;
}; };
cookie?: never; cookie?: never;
}; };
requestBody: { requestBody: {
content: { content: {
"application/json": components["schemas"]["DeleteDocumentVersion"]; "application/json": components["schemas"]["DeleteDocumentVersion"];
}; };
}; };
responses: { responses: {
/** @description no content */ /** @description no content */
200: { 200: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content?: never; content?: never;
}; };
default: { default: {
headers: { headers: {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["SerializedError"]; "application/json": components["schemas"]["SerializedError"];
}; };
}; };
}; };
}; };
options?: never; options?: never;
head?: never; head?: never;
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
} }
export type webhooks = Record<string, never>; export type webhooks = Record<string, never>;
export interface components { export interface components {
schemas: { schemas: {
CreateDocumentVersion: { CreateDocumentVersion: {
contentBase64: string; contentBase64: string;
/** Format: date-time */ /** Format: date-time */
createdDate: string; createdDate: string;
relativePath: string; relativePath: string;
}; };
DeleteDocumentVersion: { DeleteDocumentVersion: {
/** Format: date-time */ /** Format: date-time */
createdDate: string; createdDate: string;
relativePath: string; relativePath: string;
}; };
/** @description Response to a create/update document request. */ /** @description Response to a create/update document request. */
DocumentUpdateResponse: { DocumentUpdateResponse:
/** Format: date-time */ | {
createdDate: string; /** Format: date-time */
/** Format: uuid */ createdDate: string;
documentId: string; /** Format: uuid */
isDeleted: boolean; documentId: string;
relativePath: string; isDeleted: boolean;
/** @enum {string} */ relativePath: string;
type: "FastForwardUpdate"; /** @enum {string} */
/** Format: date-time */ type: "FastForwardUpdate";
updatedDate: string; /** Format: date-time */
vaultId: string; updatedDate: string;
/** Format: int64 */ vaultId: string;
vaultUpdateId: number; /** Format: int64 */
} | { vaultUpdateId: number;
contentBase64: string; }
/** Format: date-time */ | {
createdDate: string; contentBase64: string;
/** Format: uuid */ /** Format: date-time */
documentId: string; createdDate: string;
isDeleted: boolean; /** Format: uuid */
relativePath: string; documentId: string;
/** @enum {string} */ isDeleted: boolean;
type: "MergingUpdate"; relativePath: string;
/** Format: date-time */ /** @enum {string} */
updatedDate: string; type: "MergingUpdate";
vaultId: string; /** Format: date-time */
/** Format: int64 */ updatedDate: string;
vaultUpdateId: number; vaultId: string;
}; /** Format: int64 */
DocumentVersion: { vaultUpdateId: number;
contentBase64: string; };
/** Format: date-time */ DocumentVersion: {
createdDate: string; contentBase64: string;
/** Format: uuid */ /** Format: date-time */
documentId: string; createdDate: string;
isDeleted: boolean; /** Format: uuid */
relativePath: string; documentId: string;
/** Format: date-time */ isDeleted: boolean;
updatedDate: string; relativePath: string;
vaultId: string; /** Format: date-time */
/** Format: int64 */ updatedDate: string;
vaultUpdateId: number; vaultId: string;
}; /** Format: int64 */
DocumentVersionWithoutContent: { vaultUpdateId: number;
/** Format: date-time */ };
createdDate: string; DocumentVersionWithoutContent: {
/** Format: uuid */ /** Format: date-time */
documentId: string; createdDate: string;
isDeleted: boolean; /** Format: uuid */
relativePath: string; documentId: string;
/** Format: date-time */ isDeleted: boolean;
updatedDate: string; relativePath: string;
vaultId: string; /** Format: date-time */
/** Format: int64 */ updatedDate: string;
vaultUpdateId: number; vaultId: string;
}; /** Format: int64 */
/** @description Response to a fetch latest documents request. */ vaultUpdateId: number;
FetchLatestDocumentsResponse: { };
/** /** @description Response to a fetch latest documents request. */
* Format: int64 FetchLatestDocumentsResponse: {
* @description The update ID of the latest document in the response. /**
*/ * Format: int64
lastUpdateId: number; * @description The update ID of the latest document in the response.
latestDocuments: components["schemas"]["DocumentVersionWithoutContent"][]; */
}; lastUpdateId: number;
PathParams: { latestDocuments: components["schemas"]["DocumentVersionWithoutContent"][];
vault_id: string; };
}; PathParams: {
PathParams2: { vault_id: string;
vault_id: string; };
}; PathParams2: {
PathParams3: { vault_id: string;
/** Format: uuid */ };
document_id: string; PathParams3: {
vault_id: string; /** Format: uuid */
}; document_id: string;
PathParams4: { vault_id: string;
/** Format: uuid */ };
document_id: string; PathParams4: {
vault_id: string; /** Format: uuid */
}; document_id: string;
PathParams5: { vault_id: string;
/** Format: uuid */ };
document_id: string; PathParams5: {
vault_id: string; /** Format: uuid */
}; document_id: string;
/** @description Response to a ping request. */ vault_id: string;
PingResponse: { };
/** @description Whether the client is authenticated based on the sent Authorization header. */ /** @description Response to a ping request. */
isAuthenticated: boolean; PingResponse: {
/** @description Semantic version of the server. */ /** @description Whether the client is authenticated based on the sent Authorization header. */
serverVersion: string; isAuthenticated: boolean;
}; /** @description Semantic version of the server. */
QueryParams: { serverVersion: string;
/** Format: int64 */ };
since_update_id?: number | null; QueryParams: {
}; /** Format: int64 */
SerializedError: { since_update_id?: number | null;
causes: string[]; };
message: string; SerializedError: {
}; causes: string[];
UpdateDocumentVersion: { message: string;
contentBase64: string; };
/** Format: date-time */ UpdateDocumentVersion: {
createdDate: string; contentBase64: string;
/** Format: int64 */ /** Format: date-time */
parentVersionId: number; createdDate: string;
relativePath: string; /** Format: int64 */
}; parentVersionId: number;
}; relativePath: string;
responses: never; };
parameters: never; };
requestBodies: never; responses: never;
headers: never; parameters: never;
pathItems: never; requestBodies: never;
headers: never;
pathItems: never;
} }
export type $defs = Record<string, never>; export type $defs = Record<string, never>;
export type operations = Record<string, never>; export type operations = Record<string, never>;

View file

@ -8,7 +8,7 @@ let isRunning = false;
export async function applyRemoteChangesLocally({ export async function applyRemoteChangesLocally({
database, database,
syncService, syncService,
syncer, syncer
}: { }: {
database: Database; database: Database;
syncService: SyncService; syncService: SyncService;

View file

@ -1,7 +1,7 @@
import { import {
tryLockDocument, tryLockDocument,
waitForDocumentLock, waitForDocumentLock,
unlockDocument, unlockDocument
} from "./document-lock"; } from "./document-lock";
import type { RelativePath } from "src/database/document-metadata"; import type { RelativePath } from "src/database/document-metadata";
@ -34,9 +34,9 @@ describe("Document Lock Operations", () => {
}); });
test("should throw an error when unlocking a document that is not locked", () => { test("should throw an error when unlocking a document that is not locked", () => {
expect(() => { unlockDocument(testPath); }).toThrow( expect(() => {
`Document ${testPath} is not locked, cannot unlock` unlockDocument(testPath);
); }).toThrow(`Document ${testPath} is not locked, cannot unlock`);
}); });
test("should wait for a document lock and resolve when unlocked", async () => { test("should wait for a document lock and resolve when unlocked", async () => {

View file

@ -1,10 +1,9 @@
import type { Database } from "src/database/database"; import type { Database } from "src/database/database";
import type { import type {
DocumentMetadata, DocumentMetadata,
RelativePath, RelativePath
} from "src/database/document-metadata"; } from "src/database/document-metadata";
import type { FileOperations } from "src/file-operations/file-operations"; import type { FileOperations } from "src/file-operations/file-operations";
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import type { SyncService } from "src/services/sync-service"; import type { SyncService } from "src/services/sync-service";
import { Logger } from "src/tracing/logger"; import { Logger } from "src/tracing/logger";
import type { SyncHistory } from "src/tracing/sync-history"; import type { SyncHistory } from "src/tracing/sync-history";
@ -12,7 +11,8 @@ import { SyncSource, SyncStatus, SyncType } from "src/tracing/sync-history";
import { unlockDocument, waitForDocumentLock } from "./document-lock"; import { unlockDocument, waitForDocumentLock } from "./document-lock";
import PQueue from "p-queue"; import PQueue from "p-queue";
import { EMPTY_HASH, hash } from "src/utils/hash"; import { EMPTY_HASH, hash } from "src/utils/hash";
import type { components } from "src/services/types.js"; import type { components } from "src/services/types";
import { base64ToBytes } from "sync_lib";
export class Syncer { export class Syncer {
private readonly remainingOperationsListeners: (( private readonly remainingOperationsListeners: ((
@ -30,7 +30,7 @@ export class Syncer {
private readonly history: SyncHistory private readonly history: SyncHistory
) { ) {
this.syncQueue = new PQueue({ this.syncQueue = new PQueue({
concurrency: database.getSettings().syncConcurrency, concurrency: database.getSettings().syncConcurrency
}); });
database.addOnSettingsChangeHandlers((settings) => { database.addOnSettingsChangeHandlers((settings) => {
@ -78,9 +78,8 @@ export class Syncer {
public async syncRemotelyUpdatedFile( public async syncRemotelyUpdatedFile(
remoteVersion: components["schemas"]["DocumentVersionWithoutContent"] remoteVersion: components["schemas"]["DocumentVersionWithoutContent"]
): Promise<void> { ): Promise<void> {
await this.syncQueue.add( await this.syncQueue.add(async () =>
async () => this.internalSyncRemotelyUpdatedFile(remoteVersion)
this.internalSyncRemotelyUpdatedFile(remoteVersion)
); );
} }
@ -104,7 +103,7 @@ export class Syncer {
try { try {
const allLocalFiles = await this.operations.listAllFiles(); const allLocalFiles = await this.operations.listAllFiles();
const locallyDeletedFiles = [ const locallyDeletedFiles = [
...this.database.getDocuments().entries(), ...this.database.getDocuments().entries()
].filter(([path, _]) => !allLocalFiles.includes(path)); ].filter(([path, _]) => !allLocalFiles.includes(path));
await Promise.all( await Promise.all(
@ -134,7 +133,7 @@ export class Syncer {
updateTime: updateTime:
await this.operations.getModificationTime( await this.operations.getModificationTime(
relativePath relativePath
), )
}); });
} }
@ -157,7 +156,7 @@ export class Syncer {
updateTime: updateTime:
await this.operations.getModificationTime( await this.operations.getModificationTime(
relativePath relativePath
), )
}); });
}) })
) )
@ -218,7 +217,7 @@ export class Syncer {
status: SyncStatus.NO_OP, status: SyncStatus.NO_OP,
relativePath, relativePath,
message: `File hash matches with last synced version, no need to sync`, message: `File hash matches with last synced version, no need to sync`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
return; return;
} }
@ -227,7 +226,7 @@ export class Syncer {
const response = await this.syncService.create({ const response = await this.syncService.create({
relativePath, relativePath,
contentBytes, contentBytes,
createdDate: updateTime, createdDate: updateTime
}); });
this.history.addHistoryEntry({ this.history.addHistoryEntry({
@ -235,13 +234,11 @@ export class Syncer {
source: SyncSource.PUSH, source: SyncSource.PUSH,
relativePath, relativePath,
message: `Successfully uploaded locally created file`, message: `Successfully uploaded locally created file`,
type: SyncType.CREATE, type: SyncType.CREATE
}); });
if (response.type === "MergingUpdate") { if (response.type === "MergingUpdate") {
const responseBytes = lib.base64ToBytes( const responseBytes = base64ToBytes(response.contentBase64);
response.contentBase64
);
contentHash = hash(responseBytes); contentHash = hash(responseBytes);
await this.operations.write( await this.operations.write(
@ -254,7 +251,7 @@ export class Syncer {
source: SyncSource.PULL, source: SyncSource.PULL,
relativePath, relativePath,
message: `The file we created locally has already existed remotely, so we have merged them`, message: `The file we created locally has already existed remotely, so we have merged them`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
} }
@ -262,7 +259,7 @@ export class Syncer {
documentId: response.documentId, documentId: response.documentId,
relativePath: response.relativePath, relativePath: response.relativePath,
parentVersionId: response.vaultUpdateId, parentVersionId: response.vaultUpdateId,
hash: contentHash, hash: contentHash
}); });
await this.tryIncrementVaultUpdateId(response.vaultUpdateId); await this.tryIncrementVaultUpdateId(response.vaultUpdateId);
@ -273,7 +270,7 @@ export class Syncer {
private async internalSyncLocallyUpdatedFile({ private async internalSyncLocallyUpdatedFile({
oldPath, oldPath,
relativePath, relativePath,
updateTime, updateTime
}: { }: {
oldPath?: RelativePath; oldPath?: RelativePath;
relativePath: RelativePath; relativePath: RelativePath;
@ -293,7 +290,7 @@ export class Syncer {
status: SyncStatus.NO_OP, status: SyncStatus.NO_OP,
relativePath, relativePath,
message: `The renaming doesn't require a sync because it must have been pulled from remote`, message: `The renaming doesn't require a sync because it must have been pulled from remote`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
return; return;
} }
@ -314,7 +311,7 @@ export class Syncer {
status: SyncStatus.NO_OP, status: SyncStatus.NO_OP,
relativePath, relativePath,
message: `File hash matches with last synced version, no need to sync`, message: `File hash matches with last synced version, no need to sync`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
return; return;
} }
@ -324,7 +321,7 @@ export class Syncer {
parentVersionId: localMetadata.parentVersionId, parentVersionId: localMetadata.parentVersionId,
relativePath, relativePath,
contentBytes, contentBytes,
createdDate: updateTime, createdDate: updateTime
}); });
this.history.addHistoryEntry({ this.history.addHistoryEntry({
@ -332,7 +329,7 @@ export class Syncer {
source: SyncSource.PUSH, source: SyncSource.PUSH,
relativePath, relativePath,
message: `Successfully uploaded locally updated file to the remote server`, message: `Successfully uploaded locally updated file to the remote server`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
if (response.isDeleted) { if (response.isDeleted) {
@ -348,7 +345,7 @@ export class Syncer {
relativePath, relativePath,
message: message:
"The file we tried to update had been deleted remotely, therefore, we have deleted it locally", "The file we tried to update had been deleted remotely, therefore, we have deleted it locally",
type: SyncType.DELETE, type: SyncType.DELETE
}); });
return; return;
@ -367,7 +364,7 @@ export class Syncer {
} }
if (response.type === "MergingUpdate") { if (response.type === "MergingUpdate") {
const responseBytes = lib.base64ToBytes( const responseBytes = base64ToBytes(
response.contentBase64 response.contentBase64
); );
contentHash = hash(responseBytes); contentHash = hash(responseBytes);
@ -383,7 +380,7 @@ export class Syncer {
source: SyncSource.PULL, source: SyncSource.PULL,
relativePath, relativePath,
message: `The file we updated had been updated remotely, so we downloaded the merged version`, message: `The file we updated had been updated remotely, so we downloaded the merged version`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
await this.database.moveDocument({ await this.database.moveDocument({
@ -391,7 +388,7 @@ export class Syncer {
oldRelativePath: oldPath ?? relativePath, oldRelativePath: oldPath ?? relativePath,
relativePath: response.relativePath, relativePath: response.relativePath,
parentVersionId: response.vaultUpdateId, parentVersionId: response.vaultUpdateId,
hash: contentHash, hash: contentHash
}); });
await this.tryIncrementVaultUpdateId( await this.tryIncrementVaultUpdateId(
@ -420,7 +417,7 @@ export class Syncer {
status: SyncStatus.NO_OP, status: SyncStatus.NO_OP,
relativePath, relativePath,
message: `Locally deleted file hasn't been uploaded yet, so there's no need to delete it on the remote server`, message: `Locally deleted file hasn't been uploaded yet, so there's no need to delete it on the remote server`,
type: SyncType.DELETE, type: SyncType.DELETE
}); });
return; return;
} }
@ -428,7 +425,7 @@ export class Syncer {
await this.syncService.delete({ await this.syncService.delete({
documentId: localMetadata.documentId, documentId: localMetadata.documentId,
relativePath, relativePath,
createdDate: new Date(), // We got the event now, so it must have been deleted just now createdDate: new Date() // We got the event now, so it must have been deleted just now
}); });
this.history.addHistoryEntry({ this.history.addHistoryEntry({
@ -436,7 +433,7 @@ export class Syncer {
source: SyncSource.PUSH, source: SyncSource.PUSH,
relativePath, relativePath,
message: `Successfully deleted locally deleted file on the remote server`, message: `Successfully deleted locally deleted file on the remote server`,
type: SyncType.DELETE, type: SyncType.DELETE
}); });
await this.database.removeDocument(relativePath); await this.database.removeDocument(relativePath);
@ -463,17 +460,17 @@ export class Syncer {
source: SyncSource.PULL, source: SyncSource.PULL,
relativePath: remoteVersion.relativePath, relativePath: remoteVersion.relativePath,
message: `Remotely deleted file hasn't been synced yet, so there's no need to delete it locally`, message: `Remotely deleted file hasn't been synced yet, so there's no need to delete it locally`,
type: SyncType.DELETE, type: SyncType.DELETE
}); });
return; return;
} }
const content = ( const content = (
await this.syncService.get({ await this.syncService.get({
documentId: remoteVersion.documentId, documentId: remoteVersion.documentId
}) })
).contentBase64; ).contentBase64;
const contentBytes = lib.base64ToBytes(content); const contentBytes = base64ToBytes(content);
await this.operations.create( await this.operations.create(
remoteVersion.relativePath, remoteVersion.relativePath,
@ -483,14 +480,14 @@ export class Syncer {
documentId: remoteVersion.documentId, documentId: remoteVersion.documentId,
relativePath: remoteVersion.relativePath, relativePath: remoteVersion.relativePath,
parentVersionId: remoteVersion.vaultUpdateId, parentVersionId: remoteVersion.vaultUpdateId,
hash: hash(contentBytes), hash: hash(contentBytes)
}); });
this.history.addHistoryEntry({ this.history.addHistoryEntry({
status: SyncStatus.SUCCESS, status: SyncStatus.SUCCESS,
source: SyncSource.PULL, source: SyncSource.PULL,
relativePath: remoteVersion.relativePath, relativePath: remoteVersion.relativePath,
message: `Successfully downloaded remote file which hasn't existed locally`, message: `Successfully downloaded remote file which hasn't existed locally`,
type: SyncType.CREATE, type: SyncType.CREATE
}); });
return; return;
} }
@ -516,12 +513,11 @@ export class Syncer {
source: SyncSource.PULL, source: SyncSource.PULL,
relativePath: remoteVersion.relativePath, relativePath: remoteVersion.relativePath,
message: `Successfully deleted remotely deleted file locally`, message: `Successfully deleted remotely deleted file locally`,
type: SyncType.DELETE, type: SyncType.DELETE
}); });
} else { } else {
const currentContent = await this.operations.read( const currentContent =
relativePath await this.operations.read(relativePath);
);
const currentHash = hash(currentContent); const currentHash = hash(currentContent);
if (currentHash !== metadata.hash) { if (currentHash !== metadata.hash) {
@ -533,10 +529,10 @@ export class Syncer {
const content = ( const content = (
await this.syncService.get({ await this.syncService.get({
documentId: remoteVersion.documentId, documentId: remoteVersion.documentId
}) })
).contentBase64; ).contentBase64;
const contentBytes = lib.base64ToBytes(content); const contentBytes = base64ToBytes(content);
const contentHash = hash(contentBytes); const contentHash = hash(contentBytes);
if (relativePath !== remoteVersion.relativePath) { if (relativePath !== remoteVersion.relativePath) {
@ -556,7 +552,7 @@ export class Syncer {
oldRelativePath: relativePath, oldRelativePath: relativePath,
relativePath: remoteVersion.relativePath, relativePath: remoteVersion.relativePath,
parentVersionId: remoteVersion.vaultUpdateId, parentVersionId: remoteVersion.vaultUpdateId,
hash: contentHash, hash: contentHash
}); });
this.history.addHistoryEntry({ this.history.addHistoryEntry({
@ -564,7 +560,7 @@ export class Syncer {
source: SyncSource.PULL, source: SyncSource.PULL,
relativePath: remoteVersion.relativePath, relativePath: remoteVersion.relativePath,
message: `Successfully updated remotely updated file locally`, message: `Successfully updated remotely updated file locally`,
type: SyncType.UPDATE, type: SyncType.UPDATE
}); });
} }
} finally { } finally {
@ -599,7 +595,7 @@ export class Syncer {
relativePath, relativePath,
message: `Failed to ${syncSource.toLocaleLowerCase()} file ${e} when trying to ${syncType.toLocaleLowerCase()} it`, message: `Failed to ${syncSource.toLocaleLowerCase()} file ${e} when trying to ${syncType.toLocaleLowerCase()} it`,
type: syncType, type: syncType,
source: syncSource, source: syncSource
}); });
throw e; throw e;
} finally { } finally {

View file

@ -4,19 +4,22 @@ export enum LogLevel {
DEBUG = "DEBUG", DEBUG = "DEBUG",
INFO = "INFO", INFO = "INFO",
WARNING = "WARNING", WARNING = "WARNING",
ERROR = "ERROR", ERROR = "ERROR"
} }
const LOG_LEVEL_ORDER = { const LOG_LEVEL_ORDER = {
[LogLevel.DEBUG]: 0, [LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1, [LogLevel.INFO]: 1,
[LogLevel.WARNING]: 2, [LogLevel.WARNING]: 2,
[LogLevel.ERROR]: 3, [LogLevel.ERROR]: 3
}; };
class LogLine { class LogLine {
public timestamp = new Date(); public timestamp = new Date();
public constructor(public level: LogLevel, public message: string) {} public constructor(
public level: LogLevel,
public message: string
) {}
} }
export class Logger { export class Logger {

View file

@ -12,18 +12,18 @@ export interface CommonHistoryEntry {
export enum SyncType { export enum SyncType {
CREATE = "CREATE", CREATE = "CREATE",
UPDATE = "UPDATE", UPDATE = "UPDATE",
DELETE = "DELETE", DELETE = "DELETE"
} }
export enum SyncSource { export enum SyncSource {
PUSH = "PUSH", PUSH = "PUSH",
PULL = "PULL", PULL = "PULL"
} }
export enum SyncStatus { export enum SyncStatus {
NO_OP = "NO_OP", NO_OP = "NO_OP",
SUCCESS = "SUCCESS", SUCCESS = "SUCCESS",
ERROR = "ERROR", ERROR = "ERROR"
} }
export type HistoryEntry = CommonHistoryEntry & { timestamp: Date }; export type HistoryEntry = CommonHistoryEntry & { timestamp: Date };
@ -44,7 +44,7 @@ export class SyncHistory {
private status: HistoryStats = { private status: HistoryStats = {
success: 0, success: 0,
error: 0, error: 0
}; };
public getEntries(): HistoryEntry[] { public getEntries(): HistoryEntry[] {
@ -55,7 +55,7 @@ export class SyncHistory {
this.entries.length = 0; this.entries.length = 0;
this.status = { this.status = {
success: 0, success: 0,
error: 0, error: 0
}; };
this.syncHistoryUpdateListeners.forEach((listener) => { this.syncHistoryUpdateListeners.forEach((listener) => {
listener(this.status); listener(this.status);
@ -72,7 +72,7 @@ export class SyncHistory {
public addHistoryEntry(entry: CommonHistoryEntry): void { public addHistoryEntry(entry: CommonHistoryEntry): void {
const historyEntry = { const historyEntry = {
...entry, ...entry,
timestamp: new Date(), timestamp: new Date()
}; };
this.entries.push(historyEntry); this.entries.push(historyEntry);

View file

@ -22,9 +22,7 @@ export async function retriedFetch(
retryOn: function (attempt, error, response) { retryOn: function (attempt, error, response) {
if (error !== null || !response || response.status >= 500) { if (error !== null || !response || response.status >= 500) {
Logger.getInstance().warn( Logger.getInstance().warn(
`Retrying fetch for ${getUrlFromInput( `Retrying fetch for ${getUrlFromInput(input)}, attempt ${attempt}`
input
)}, attempt ${attempt}`
); );
return true; return true;
@ -33,6 +31,6 @@ export async function retriedFetch(
}, },
retries: 6, retries: 6,
retryDelay: (attempt) => Math.pow(1.5, attempt) * 500, retryDelay: (attempt) => Math.pow(1.5, attempt) * 500,
...init, ...init
}); });
} }

View file

@ -1,22 +1,22 @@
import type { WorkspaceLeaf } from "obsidian"; import type { WorkspaceLeaf } from "obsidian";
import { Plugin } from "obsidian"; import { Plugin } from "obsidian";
import "./styles.scss";
import * as lib from "../../backend/sync_lib/pkg/sync_lib.js"; import "../manifest.json";
import * as wasmBin from "../../backend/sync_lib/pkg/sync_lib_bg.wasm"; import init, { setPanicHook } from "sync_lib";
import { SyncSettingsTab } from "./views/settings-tab.js"; import wasmBin from "sync_lib/sync_lib_bg.wasm";
import { HistoryView } from "./views/history-view.js"; import { SyncSettingsTab } from "./views/settings-tab";
import { HistoryView } from "./views/history-view";
import { ObsidianFileEventHandler } from "./events/obisidan-event-handler.js"; import { ObsidianFileEventHandler } from "./events/obisidan-event-handler";
import { SyncService } from "./services/sync-service.js"; import { SyncService } from "./services/sync-service";
import { Database } from "./database/database.js"; import { Database } from "./database/database";
import { applyRemoteChangesLocally } from "./sync-operations/apply-remote-changes-locally.js"; import { applyRemoteChangesLocally } from "./sync-operations/apply-remote-changes-locally";
import { ObsidianFileOperations } from "./file-operations/obsidian-file-operations.js"; import { ObsidianFileOperations } from "./file-operations/obsidian-file-operations";
import { StatusBar } from "./views/status-bar.js"; import { StatusBar } from "./views/status-bar";
import { Logger } from "./tracing/logger.js"; import { Logger } from "./tracing/logger";
import { SyncHistory } from "./tracing/sync-history.js"; import { SyncHistory } from "./tracing/sync-history";
import { LogsView } from "./views/logs-view.js"; import { LogsView } from "./views/logs-view";
import { Syncer } from "./sync-operations/syncer.js"; import { Syncer } from "./sync-operations/syncer";
import { StatusDescription } from "./views/status-description.js"; import { StatusDescription } from "./views/status-description";
export default class VaultLinkPlugin extends Plugin { export default class VaultLinkPlugin extends Plugin {
private readonly operations = new ObsidianFileOperations(this.app.vault); private readonly operations = new ObsidianFileOperations(this.app.vault);
@ -27,14 +27,10 @@ export default class VaultLinkPlugin extends Plugin {
public async onload(): Promise<void> { public async onload(): Promise<void> {
Logger.getInstance().info("Starting plugin"); Logger.getInstance().info("Starting plugin");
await lib.default( // eslint-disable-next-line
Promise.resolve( await init((wasmBin as any).default);
// eslint-disable-next-line
(wasmBin as any).default
)
);
lib.setPanicHook(); setPanicHook();
const database = new Database( const database = new Database(
await this.loadData(), await this.loadData(),
@ -63,7 +59,7 @@ export default class VaultLinkPlugin extends Plugin {
database, database,
syncService, syncService,
statusDescription, statusDescription,
syncer, syncer
}); });
this.addSettingTab(this.settingsTab); this.addSettingTab(this.settingsTab);
@ -90,7 +86,7 @@ export default class VaultLinkPlugin extends Plugin {
this.app.vault.on( this.app.vault.on(
"rename", "rename",
eventHandler.onRename.bind(eventHandler) eventHandler.onRename.bind(eventHandler)
), )
].forEach((event) => { ].forEach((event) => {
this.registerEvent(event); this.registerEvent(event);
}); });
@ -196,7 +192,7 @@ export default class VaultLinkPlugin extends Plugin {
applyRemoteChangesLocally({ applyRemoteChangesLocally({
database, database,
syncService, syncService,
syncer, syncer
}), }),
intervalMs intervalMs
); );

View file

@ -61,7 +61,7 @@ export class HistoryView extends ItemView {
} }
element.createEl("span", { element.createEl("span", {
text: entry.relativePath, text: entry.relativePath
}); });
const syncSourceIcon = HistoryView.getSyncSourceIcon(entry.source); const syncSourceIcon = HistoryView.getSyncSourceIcon(entry.source);
@ -107,7 +107,7 @@ export class HistoryView extends ItemView {
entries.forEach((entry) => { entries.forEach((entry) => {
container.createDiv( container.createDiv(
{ {
cls: ["history-card", entry.status.toLocaleLowerCase()], cls: ["history-card", entry.status.toLocaleLowerCase()]
}, },
(card) => { (card) => {
if ( if (
@ -127,13 +127,13 @@ export class HistoryView extends ItemView {
card.createDiv( card.createDiv(
{ {
cls: "history-card-header", cls: "history-card-header"
}, },
(header) => { (header) => {
header.createEl( header.createEl(
"h5", "h5",
{ {
cls: "history-card-title", cls: "history-card-title"
}, },
(title) => { (title) => {
HistoryView.renderSyncItemTitle( HistoryView.renderSyncItemTitle(
@ -148,14 +148,14 @@ export class HistoryView extends ItemView {
entry.timestamp, entry.timestamp,
new Date() new Date()
), ),
cls: "history-card-timestamp", cls: "history-card-timestamp"
}); });
} }
); );
card.createEl("p", { card.createEl("p", {
text: `${entry.message}.`, text: `${entry.message}.`,
cls: "history-card-message", cls: "history-card-message"
}); });
} }
); );

View file

@ -61,13 +61,13 @@ export class LogsView extends ItemView {
container.createEl( container.createEl(
"p", "p",
{ {
text: "This view displays logs generated by VaultLink. You can set the log level in the ", text: "This view displays logs generated by VaultLink. You can set the log level in the "
}, },
(p) => { (p) => {
p.createEl( p.createEl(
"a", "a",
{ {
text: "settings", text: "settings"
}, },
(button) => { (button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
@ -95,17 +95,17 @@ export class LogsView extends ItemView {
logs.forEach((message) => logs.forEach((message) =>
element.createDiv( element.createDiv(
{ {
cls: ["log-message", message.level], cls: ["log-message", message.level]
}, },
(messageContainer) => { (messageContainer) => {
messageContainer.createEl("span", { messageContainer.createEl("span", {
text: LogsView.formatTimestamp( text: LogsView.formatTimestamp(
message.timestamp message.timestamp
), ),
cls: "timestamp", cls: "timestamp"
}); });
messageContainer.createEl("span", { messageContainer.createEl("span", {
text: message.message, text: message.message
}); });
} }
) )

View file

@ -4,11 +4,11 @@ import { Notice, PluginSettingTab, Setting } from "obsidian";
import type VaultLinkPlugin from "src/vault-link-plugin"; import type VaultLinkPlugin from "src/vault-link-plugin";
import type { Database } from "src/database/database"; import type { Database } from "src/database/database";
import type { SyncService } from "src/services/sync-service"; import type { SyncService } from "src/services/sync-service";
import { Logger, LogLevel } from "src/tracing/logger";
import type { Syncer } from "src/sync-operations/syncer"; import type { Syncer } from "src/sync-operations/syncer";
import type { StatusDescription } from "./status-description"; import type { StatusDescription } from "./status-description";
import { LogsView } from "./logs-view"; import { LogsView } from "./logs-view";
import { HistoryView } from "./history-view"; import { HistoryView } from "./history-view";
import { Logger, LogLevel } from "src/tracing/logger";
export class SyncSettingsTab extends PluginSettingTab { export class SyncSettingsTab extends PluginSettingTab {
private editedVaultName: string; private editedVaultName: string;
@ -26,7 +26,7 @@ export class SyncSettingsTab extends PluginSettingTab {
database, database,
syncService, syncService,
statusDescription, statusDescription,
syncer, syncer
}: { }: {
app: App; app: App;
plugin: VaultLinkPlugin; plugin: VaultLinkPlugin;
@ -72,12 +72,12 @@ export class SyncSettingsTab extends PluginSettingTab {
private renderSettingsHeader(containerEl: HTMLElement): void { private renderSettingsHeader(containerEl: HTMLElement): void {
containerEl.createEl("h2", { text: "VaultLink" }).createSpan({ containerEl.createEl("h2", { text: "VaultLink" }).createSpan({
text: this.plugin.manifest.version, text: this.plugin.manifest.version,
cls: "version", cls: "version"
}); });
containerEl.createDiv( containerEl.createDiv(
{ {
cls: "description", cls: "description"
}, },
(descriptionContainer) => { (descriptionContainer) => {
this.setStatusDescriptionSubscription((): void => { this.setStatusDescriptionSubscription((): void => {
@ -90,13 +90,13 @@ export class SyncSettingsTab extends PluginSettingTab {
containerEl.createDiv( containerEl.createDiv(
{ {
cls: "button-container", cls: "button-container"
}, },
(buttonContainer) => { (buttonContainer) => {
buttonContainer.createEl( buttonContainer.createEl(
"button", "button",
{ {
text: "Show history", text: "Show history"
}, },
(button) => (button) =>
(button.onclick = async (): Promise<void> => { (button.onclick = async (): Promise<void> => {
@ -108,7 +108,7 @@ export class SyncSettingsTab extends PluginSettingTab {
buttonContainer.createEl( buttonContainer.createEl(
"button", "button",
{ {
text: "Show logs", text: "Show logs"
}, },
(button) => (button) =>
(button.onclick = async (): Promise<void> => { (button.onclick = async (): Promise<void> => {
@ -296,7 +296,7 @@ export class SyncSettingsTab extends PluginSettingTab {
[LogLevel.DEBUG]: LogLevel.DEBUG, [LogLevel.DEBUG]: LogLevel.DEBUG,
[LogLevel.INFO]: LogLevel.INFO, [LogLevel.INFO]: LogLevel.INFO,
[LogLevel.WARNING]: LogLevel.WARNING, [LogLevel.WARNING]: LogLevel.WARNING,
[LogLevel.ERROR]: LogLevel.ERROR, [LogLevel.ERROR]: LogLevel.ERROR
}) })
.onChange(async (value) => .onChange(async (value) =>
this.database.setSetting( this.database.setSetting(

View file

@ -34,7 +34,7 @@ export class StatusBar {
private updateStatus(): void { private updateStatus(): void {
this.statusBarItem.empty(); this.statusBarItem.empty();
const container = this.statusBarItem.createDiv({ const container = this.statusBarItem.createDiv({
cls: ["sync-status"], cls: ["sync-status"]
}); });
let hasShownMessage = false; let hasShownMessage = false;
@ -47,14 +47,14 @@ export class StatusBar {
if ((this.lastHistoryStats?.success ?? 0) > 0) { if ((this.lastHistoryStats?.success ?? 0) > 0) {
hasShownMessage = true; hasShownMessage = true;
container.createSpan({ container.createSpan({
text: `${this.lastHistoryStats?.success ?? 0}`, text: `${this.lastHistoryStats?.success ?? 0}`
}); });
} }
if ((this.lastHistoryStats?.error ?? 0) > 0) { if ((this.lastHistoryStats?.error ?? 0) > 0) {
hasShownMessage = true; hasShownMessage = true;
container.createSpan({ container.createSpan({
text: `${this.lastHistoryStats?.error ?? 0}`, text: `${this.lastHistoryStats?.error ?? 0}`
}); });
} }
@ -64,7 +64,7 @@ export class StatusBar {
} else { } else {
const button = container.createEl("button", { const button = container.createEl("button", {
text: "VaultLink is disabled, click to configure", text: "VaultLink is disabled, click to configure",
cls: "initialize-button", cls: "initialize-button"
}); });
button.onclick = (): void => { button.onclick = (): void => {
this.plugin.openSettings(); this.plugin.openSettings();

View file

@ -1,7 +1,7 @@
import type { Database } from "src/database/database"; import type { Database } from "src/database/database";
import type { import type {
CheckConnectionResult, CheckConnectionResult,
SyncService, SyncService
} from "src/services/sync-service"; } from "src/services/sync-service";
import type { Syncer } from "src/sync-operations/syncer"; import type { Syncer } from "src/sync-operations/syncer";
import type { HistoryStats, SyncHistory } from "src/tracing/sync-history"; import type { HistoryStats, SyncHistory } from "src/tracing/sync-history";
@ -57,7 +57,7 @@ export class StatusDescription {
if (this.lastConnectionState == undefined) { if (this.lastConnectionState == undefined) {
container.createSpan({ container.createSpan({
text: "VaultLink is starting up…", text: "VaultLink is starting up…",
cls: "warning", cls: "warning"
}); });
return; return;
} }
@ -65,7 +65,7 @@ export class StatusDescription {
if (!this.lastConnectionState.isSuccessful) { if (!this.lastConnectionState.isSuccessful) {
container.createSpan({ 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 the error "${this.lastConnectionState.message}"`,
cls: "error", cls: "error"
}); });
return; return;
} }
@ -73,18 +73,18 @@ export class StatusDescription {
container.createSpan({ text: "VaultLink is connected to the server " }); container.createSpan({ text: "VaultLink is connected to the server " });
container.createEl("a", { container.createEl("a", {
text: this.database.getSettings().remoteUri, text: this.database.getSettings().remoteUri,
href: this.database.getSettings().remoteUri, href: this.database.getSettings().remoteUri
}); });
container.createSpan({ container.createSpan({
text: ` and has indexed approximately `, text: ` and has indexed approximately `
}); });
container.createSpan({ container.createSpan({
text: `${this.database.getDocuments().size}`, text: `${this.database.getDocuments().size}`,
cls: "number", cls: "number"
}); });
container.createSpan({ container.createSpan({
text: ` documents. `, text: ` documents. `
}); });
if ( if (
@ -94,40 +94,40 @@ export class StatusDescription {
) { ) {
if (this.database.getSettings().isSyncEnabled) { if (this.database.getSettings().isSyncEnabled) {
container.createSpan({ container.createSpan({
text: "Syncing is enabled but VaultLink hasn't found anything to sync yet.", text: "Syncing is enabled but VaultLink hasn't found anything to sync yet."
}); });
} else { } else {
container.createSpan({ container.createSpan({
text: "However, syncing is disabled right now.", text: "However, syncing is disabled right now.",
cls: "warning", cls: "warning"
}); });
} }
return; return;
} }
container.createSpan({ container.createSpan({
text: "The plugin has ", text: "The plugin has "
}); });
container.createSpan({ container.createSpan({
text: `${this.lastRemaining ?? 0}`, text: `${this.lastRemaining ?? 0}`,
cls: "number", cls: "number"
}); });
container.createSpan({ container.createSpan({
text: " outstanding operations while having succeeded ", text: " outstanding operations while having succeeded "
}); });
container.createSpan({ container.createSpan({
text: `${this.lastHistoryStats?.success ?? 0}`, text: `${this.lastHistoryStats?.success ?? 0}`,
cls: ["number", "good"], cls: ["number", "good"]
}); });
container.createSpan({ container.createSpan({
text: " times and failed ", text: " times and failed "
}); });
container.createSpan({ container.createSpan({
text: `${this.lastHistoryStats?.error ?? 0}`, text: `${this.lastHistoryStats?.error ?? 0}`,
cls: ["number", "bad"], cls: ["number", "bad"]
}); });
container.createSpan({ container.createSpan({
text: " times.", text: " times."
}); });
} }