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",
"prettier": {
"trailingComma": "none",
"tabWidth": 2,
"useTabs": false,
"tabWidth": 4,
"useTabs": true,
"endOfLine": "lf"
},
"devDependencies": {

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import type { Vault } from "obsidian";
import { normalizePath } from "obsidian";
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 { isBinary, mergeText } from "sync_lib";
export class ObsidianFileOperations implements FileOperations {
public constructor(private readonly vault: Vault) {}
@ -49,7 +49,7 @@ export class ObsidianFileOperations implements FileOperations {
return new Uint8Array(0);
}
if (lib.isBinary(expectedContent) || !path.endsWith(".md")) {
if (isBinary(expectedContent) || !path.endsWith(".md")) {
await this.vault.adapter.writeBinary(
normalizePath(path),
newContent
@ -64,7 +64,7 @@ export class ObsidianFileOperations implements FileOperations {
normalizePath(path),
(currentText) => {
if (currentText !== expetedText) {
return lib.mergeText(expetedText, currentText, newText);
return mergeText(expetedText, currentText, 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 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 { SyncSettings } from "src/database/sync-settings";
import type {
DocumentId,
RelativePath,
VaultUpdateId,
VaultUpdateId
} from "src/database/document-metadata";
import { Logger } from "src/tracing/logger.js";
import { retriedFetch } from "src/utils/retried-fetch.js";
import { Logger } from "src/tracing/logger";
import { retriedFetch } from "src/utils/retried-fetch";
import { bytesToBase64 } from "sync_lib";
export interface CheckConnectionResult {
isSuccessful: boolean;
@ -45,11 +44,9 @@ export class SyncService {
const response = await this.clientWithoutRetries.GET("/ping", {
params: {
header: {
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
authorization: `Bearer ${this.database.getSettings().token}`
}
}
});
Logger.getInstance().debug(
@ -58,9 +55,7 @@ export class SyncService {
if (!response.data) {
throw new Error(
`Failed to ping server: ${SyncService.formatError(
response.error
)}`
`Failed to ping server: ${SyncService.formatError(response.error)}`
);
}
@ -70,7 +65,7 @@ export class SyncService {
public async create({
relativePath,
contentBytes,
createdDate,
createdDate
}: {
relativePath: RelativePath;
contentBytes: Uint8Array;
@ -81,27 +76,23 @@ export class SyncService {
{
params: {
path: {
vault_id: this.database.getSettings().vaultName,
vault_id: this.database.getSettings().vaultName
},
header: {
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
authorization: `Bearer ${this.database.getSettings().token}`
}
},
body: {
contentBase64: lib.bytesToBase64(contentBytes),
contentBase64: bytesToBase64(contentBytes),
createdDate: createdDate.toISOString(),
relativePath,
},
relativePath
}
}
);
if (!response.data) {
throw new Error(
`Failed to create document: ${SyncService.formatError(
response.error
)}`
`Failed to create document: ${SyncService.formatError(response.error)}`
);
}
@ -119,7 +110,7 @@ export class SyncService {
documentId,
relativePath,
contentBytes,
createdDate,
createdDate
}: {
parentVersionId: VaultUpdateId;
documentId: DocumentId;
@ -133,28 +124,24 @@ export class SyncService {
params: {
path: {
vault_id: this.database.getSettings().vaultName,
document_id: documentId,
document_id: documentId
},
header: {
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
authorization: `Bearer ${this.database.getSettings().token}`
}
},
body: {
parentVersionId,
contentBase64: lib.bytesToBase64(contentBytes),
contentBase64: bytesToBase64(contentBytes),
createdDate: createdDate.toISOString(),
relativePath,
},
relativePath
}
}
);
if (!response.data) {
throw new Error(
`Failed to update document: ${SyncService.formatError(
response.error
)}`
`Failed to update document: ${SyncService.formatError(response.error)}`
);
}
@ -168,7 +155,7 @@ export class SyncService {
public async delete({
documentId,
relativePath,
createdDate,
createdDate
}: {
documentId: DocumentId;
relativePath: RelativePath;
@ -180,18 +167,16 @@ export class SyncService {
params: {
path: {
vault_id: this.database.getSettings().vaultName,
document_id: documentId,
document_id: documentId
},
header: {
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
authorization: `Bearer ${this.database.getSettings().token}`
}
},
body: {
createdDate: createdDate.toISOString(),
relativePath,
},
relativePath
}
}
);
@ -207,7 +192,7 @@ export class SyncService {
}
public async get({
documentId,
documentId
}: {
documentId: DocumentId;
}): Promise<components["schemas"]["DocumentVersion"]> {
@ -217,22 +202,18 @@ export class SyncService {
params: {
path: {
vault_id: this.database.getSettings().vaultName,
document_id: documentId,
document_id: documentId
},
header: {
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
authorization: `Bearer ${this.database.getSettings().token}`
}
}
}
);
if (!response.data) {
throw new Error(
`Failed to get document: ${SyncService.formatError(
response.error
)}`
`Failed to get document: ${SyncService.formatError(response.error)}`
);
}
@ -249,25 +230,21 @@ export class SyncService {
const response = await this.client.GET("/vaults/{vault_id}/documents", {
params: {
path: {
vault_id: this.database.getSettings().vaultName,
vault_id: this.database.getSettings().vaultName
},
header: {
authorization: `Bearer ${
this.database.getSettings().token
}`,
authorization: `Bearer ${this.database.getSettings().token}`
},
query: {
since_update_id: since,
},
},
since_update_id: since
}
}
});
const { error } = response;
if (error) {
throw new Error(
`Failed to get documents: ${SyncService.formatError(
response.error
)}`
`Failed to get documents: ${SyncService.formatError(response.error)}`
);
}
@ -284,18 +261,18 @@ 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 {
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 {
this.client = createClient<paths>({
baseUrl: settings.remoteUri,
fetch: retriedFetch,
fetch: retriedFetch
});
this.clientWithoutRetries = createClient<paths>({
baseUrl: settings.remoteUri,
baseUrl: settings.remoteUri
});
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,13 +61,13 @@ export class LogsView extends ItemView {
container.createEl(
"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.createEl(
"a",
{
text: "settings",
text: "settings"
},
(button) => {
button.addEventListener("click", () => {
@ -95,17 +95,17 @@ export class LogsView extends ItemView {
logs.forEach((message) =>
element.createDiv(
{
cls: ["log-message", message.level],
cls: ["log-message", message.level]
},
(messageContainer) => {
messageContainer.createEl("span", {
text: LogsView.formatTimestamp(
message.timestamp
),
cls: "timestamp",
cls: "timestamp"
});
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 { Database } from "src/database/database";
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 { StatusDescription } from "./status-description";
import { LogsView } from "./logs-view";
import { HistoryView } from "./history-view";
import { Logger, LogLevel } from "src/tracing/logger";
export class SyncSettingsTab extends PluginSettingTab {
private editedVaultName: string;
@ -26,7 +26,7 @@ export class SyncSettingsTab extends PluginSettingTab {
database,
syncService,
statusDescription,
syncer,
syncer
}: {
app: App;
plugin: VaultLinkPlugin;
@ -72,12 +72,12 @@ export class SyncSettingsTab extends PluginSettingTab {
private renderSettingsHeader(containerEl: HTMLElement): void {
containerEl.createEl("h2", { text: "VaultLink" }).createSpan({
text: this.plugin.manifest.version,
cls: "version",
cls: "version"
});
containerEl.createDiv(
{
cls: "description",
cls: "description"
},
(descriptionContainer) => {
this.setStatusDescriptionSubscription((): void => {
@ -90,13 +90,13 @@ export class SyncSettingsTab extends PluginSettingTab {
containerEl.createDiv(
{
cls: "button-container",
cls: "button-container"
},
(buttonContainer) => {
buttonContainer.createEl(
"button",
{
text: "Show history",
text: "Show history"
},
(button) =>
(button.onclick = async (): Promise<void> => {
@ -108,7 +108,7 @@ export class SyncSettingsTab extends PluginSettingTab {
buttonContainer.createEl(
"button",
{
text: "Show logs",
text: "Show logs"
},
(button) =>
(button.onclick = async (): Promise<void> => {
@ -296,7 +296,7 @@ export class SyncSettingsTab extends PluginSettingTab {
[LogLevel.DEBUG]: LogLevel.DEBUG,
[LogLevel.INFO]: LogLevel.INFO,
[LogLevel.WARNING]: LogLevel.WARNING,
[LogLevel.ERROR]: LogLevel.ERROR,
[LogLevel.ERROR]: LogLevel.ERROR
})
.onChange(async (value) =>
this.database.setSetting(

View file

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

View file

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