Lint files

This commit is contained in:
Andras Schmelczer 2024-12-20 16:14:46 +00:00
parent 2f7cad602a
commit ff5af8aea5
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
11 changed files with 184 additions and 276 deletions

View file

@ -1,11 +1,12 @@
import { Logger } from "src/logger";
import { DEFAULT_SETTINGS, SyncSettings } from "./sync-settings";
import {
RelativePath,
DocumentMetadata,
VaultUpdateId,
import type { SyncSettings } from "./sync-settings";
import { DEFAULT_SETTINGS } from "./sync-settings";
import type {
DocumentId,
DocumentMetadata,
RelativePath,
VaultUpdateId,
} from "./document-metadata";
import { Logger } from "src/tracing/logger";
interface StoredDatabase {
documents: Map<RelativePath, DocumentMetadata>;
@ -13,27 +14,31 @@ interface StoredDatabase {
lastSeenUpdateId: VaultUpdateId | undefined;
}
// Todo: split it into settings and documents
export class Database {
private _documents: Map<RelativePath, DocumentMetadata> = new Map();
private _documents = new Map<RelativePath, DocumentMetadata>();
private _settings: SyncSettings;
private _lastSeenUpdateId: VaultUpdateId | undefined;
private onSettingsChangeHandlers: Array<
(newSettings: SyncSettings, oldSettings: SyncSettings) => void
> = [];
private readonly onSettingsChangeHandlers: ((
newSettings: SyncSettings,
oldSettings: SyncSettings
) => void)[] = [];
public constructor(
initialState: Partial<StoredDatabase> | undefined,
private saveData: (data: unknown) => Promise<void>
private readonly saveData: (data: unknown) => Promise<void>
) {
initialState = initialState || {};
initialState ??= {};
if (
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
Object.prototype.hasOwnProperty.call(initialState, "documents") &&
initialState.documents
) {
for (const [relativePath, metadata] of Object.entries(
initialState.documents
)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this._documents.set(relativePath, metadata as DocumentMetadata);
}
}
@ -46,11 +51,10 @@ export class Database {
)}`
);
this._settings = Object.assign(
{},
DEFAULT_SETTINGS,
initialState.settings || {}
);
this._settings = {
...DEFAULT_SETTINGS,
...(initialState.settings ?? {}),
};
Logger.getInstance().debug(
`Loaded settings: ${JSON.stringify(this._settings, null, 2)}`
@ -74,15 +78,15 @@ export class Database {
public async setSettings(value: SyncSettings): Promise<void> {
const oldSettings = this._settings;
this._settings = value;
this.onSettingsChangeHandlers.forEach((handler) =>
handler(value, oldSettings)
);
this.onSettingsChangeHandlers.forEach((handler) => {
handler(value, oldSettings);
});
await this.save();
}
public addOnSettingsChangeHandlers(
handler: (settings: SyncSettings, oldSettings: SyncSettings) => void
) {
): void {
this.onSettingsChangeHandlers.push(handler);
}

View file

@ -1,4 +1,4 @@
import { TAbstractFile } from "obsidian";
import type { TAbstractFile } from "obsidian";
export interface FileEventHandler {
onCreate: (path: TAbstractFile) => Promise<void>;

View file

@ -1,57 +1,48 @@
import { TAbstractFile, TFile } from "obsidian";
import { FileEventHandler } from "./file-event-handler";
import { Logger } from "src/logger";
import { SyncService } from "src/services/sync-service";
import { Database } from "src/database/database";
import type { TAbstractFile } from "obsidian";
import { TFile } from "obsidian";
import type { FileEventHandler } from "./file-event-handler";
import type { SyncService } from "src/services/sync-service";
import type { Database } from "src/database/database";
import { syncLocallyDeletedFile } from "src/sync-operations/sync-locally-deleted-file";
import { syncLocallyUpdatedFile } from "src/sync-operations/sync-locally-updated-file";
import { FileOperations } from "src/file-operations/file-operations";
import type { FileOperations } from "src/file-operations/file-operations";
import { syncLocallyCreatedFile } from "src/sync-operations/sync-locally-created-file";
import { Logger } from "src/tracing/logger";
import type { SyncHistory } from "src/tracing/sync-history";
export class SyncEventHandler implements FileEventHandler {
export class ObsidianFileEventHandler implements FileEventHandler {
public constructor(
private database: Database,
private syncServer: SyncService,
private operations: FileOperations
private readonly database: Database,
private readonly syncServer: SyncService,
private readonly operations: FileOperations,
private readonly history: SyncHistory
) {}
async onCreate(file: TAbstractFile): Promise<void> {
public async onCreate(file: TAbstractFile): Promise<void> {
if (file instanceof TFile) {
Logger.getInstance().info(`File created: ${file.path}`);
if (!this.database.getSettings().isSyncEnabled) {
Logger.getInstance().info(
`Sync is disabled, not syncing ${file.path}`
);
return;
}
await syncLocallyCreatedFile({
database: this.database,
syncServer: this.syncServer,
operations: this.operations,
updateTime: new Date(file.stat.ctime),
filePath: file.path,
relativePath: file.path,
history: this.history,
});
} else {
Logger.getInstance().info(`Folder created: ${file.path}, ignored`);
}
}
async onDelete(file: TAbstractFile): Promise<void> {
public async onDelete(file: TAbstractFile): Promise<void> {
if (file instanceof TFile) {
Logger.getInstance().info(`File deleted: ${file.path}`);
if (!this.database.getSettings().isSyncEnabled) {
Logger.getInstance().info(
`Sync is disabled, not syncing ${file.path}`
);
return;
}
await syncLocallyDeletedFile({
database: this.database,
syncServer: this.syncServer,
history: this.history,
relativePath: file.path,
});
} else {
@ -59,25 +50,19 @@ export class SyncEventHandler implements FileEventHandler {
}
}
async onRename(file: TAbstractFile, oldPath: string): Promise<void> {
public async onRename(file: TAbstractFile, oldPath: string): Promise<void> {
if (file instanceof TFile) {
Logger.getInstance().info(
`File renamed: ${oldPath} -> ${file.path}`
);
if (!this.database.getSettings().isSyncEnabled) {
Logger.getInstance().info(
`Sync is disabled, not syncing ${file.path}`
);
return;
}
await syncLocallyUpdatedFile({
database: this.database,
syncServer: this.syncServer,
operations: this.operations,
history: this.history,
updateTime: new Date(file.stat.ctime),
filePath: file.path,
relativePath: file.path,
oldPath,
});
} else {
@ -87,23 +72,17 @@ export class SyncEventHandler implements FileEventHandler {
}
}
async onModify(file: TAbstractFile): Promise<void> {
public async onModify(file: TAbstractFile): Promise<void> {
if (file instanceof TFile) {
Logger.getInstance().info(`File modified: ${file.path}`);
if (!this.database.getSettings().isSyncEnabled) {
Logger.getInstance().info(
`Sync is disabled, not syncing ${file.path}`
);
return;
}
await syncLocallyUpdatedFile({
database: this.database,
syncServer: this.syncServer,
operations: this.operations,
history: this.history,
updateTime: new Date(file.stat.ctime),
filePath: file.path,
relativePath: file.path,
});
} else {
Logger.getInstance().info(`Folder modified: ${file.path}, ignored`);

View file

@ -1,22 +1,22 @@
import { RelativePath } from "src/database/document-metadata";
import type { RelativePath } from "src/database/document-metadata";
export interface FileOperations {
listAllFiles(): Promise<RelativePath[]>;
listAllFiles: () => Promise<RelativePath[]>;
read(path: RelativePath): Promise<Uint8Array>;
read: (path: RelativePath) => Promise<Uint8Array>;
getModificationTime(path: RelativePath): Promise<Date>;
getModificationTime: (path: RelativePath) => Promise<Date>;
create(path: RelativePath, newContent: Uint8Array): Promise<void>;
create: (path: RelativePath, newContent: Uint8Array) => Promise<void>;
// Writes new content to the file at the given path. If the file's content has changed since the expectedContent was read, the write will merge the changes.
write(
write: (
path: RelativePath,
expectedContent: Uint8Array,
newContent: Uint8Array
): Promise<Uint8Array>;
) => Promise<Uint8Array>;
remove(path: RelativePath): Promise<void>;
remove: (path: RelativePath) => Promise<void>;
move(oldPath: RelativePath, newPath: RelativePath): Promise<void>;
move: (oldPath: RelativePath, newPath: RelativePath) => Promise<void>;
}

View file

@ -1,30 +1,33 @@
import { normalizePath, Vault } from "obsidian";
import { FileOperations } from "./file-operations";
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 { isEqualBytes } from "src/utils/is-equal-bytes";
import { RelativePath } from "src/database/document-metadata";
import type { RelativePath } from "src/database/document-metadata";
export class ObsidianFileOperations implements FileOperations {
public constructor(private vault: Vault) {}
public constructor(private readonly vault: Vault) {}
async listAllFiles(): Promise<RelativePath[]> {
public async listAllFiles(): Promise<RelativePath[]> {
const files = this.vault.getFiles();
return files.map((file) => file.path);
}
async read(path: RelativePath): Promise<Uint8Array> {
public async read(path: RelativePath): Promise<Uint8Array> {
return new Uint8Array(
await this.vault.adapter.readBinary(normalizePath(path))
);
}
async getModificationTime(path: RelativePath): Promise<Date> {
return new Date(
(await this.vault.adapter.stat(normalizePath(path)))!.mtime
);
public async getModificationTime(path: RelativePath): Promise<Date> {
const file = await this.vault.adapter.stat(normalizePath(path));
if (!file) {
throw new Error(`File not found: ${path}`);
}
return new Date(file.mtime);
}
async write(
public async write(
path: RelativePath,
expectedContent: Uint8Array,
newContent: Uint8Array
@ -44,17 +47,16 @@ export class ObsidianFileOperations implements FileOperations {
await this.vault.adapter.writeBinary(normalizePath(path), result);
return result;
} else {
await this.vault.adapter.writeBinary(
normalizePath(path),
newContent
);
return newContent;
}
await this.vault.adapter.writeBinary(normalizePath(path), newContent);
return newContent;
}
async create(path: RelativePath, newContent: Uint8Array): Promise<void> {
public async create(
path: RelativePath,
newContent: Uint8Array
): Promise<void> {
if (await this.vault.adapter.exists(normalizePath(path))) {
await this.write(path, new Uint8Array(0), newContent);
return;
@ -63,18 +65,21 @@ export class ObsidianFileOperations implements FileOperations {
await this.vault.adapter.writeBinary(normalizePath(path), newContent);
}
async remove(path: RelativePath): Promise<void> {
public async remove(path: RelativePath): Promise<void> {
if (await this.vault.adapter.exists(normalizePath(path))) {
return this.vault.adapter.remove(normalizePath(path));
}
}
async move(oldPath: RelativePath, newPath: RelativePath): Promise<void> {
public async move(
oldPath: RelativePath,
newPath: RelativePath
): Promise<void> {
if (oldPath === newPath) {
return;
}
this.vault.adapter.rename(
await this.vault.adapter.rename(
normalizePath(oldPath),
normalizePath(newPath)
);

View file

@ -1,81 +0,0 @@
import { Notice } from "obsidian";
export enum LogLevel {
DEBUG,
INFO,
WARNING,
ERROR,
}
class LogLine {
public constructor(public level: LogLevel, public message: string) {}
public toString(): string {
return `${this.formatLevel()}: ${this.message}`;
}
private formatLevel(): string {
switch (this.level) {
case LogLevel.DEBUG:
return "DEBUG";
case LogLevel.INFO:
return "INFO";
case LogLevel.WARNING:
return "WARNING";
case LogLevel.ERROR:
return "ERROR";
default:
return "UNKNOWN";
}
}
}
export class Logger {
private static readonly MAX_MESSAGES = 1000;
private static instance: Logger;
private messages: LogLine[] = [];
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public debug(message: string): void {
this.pushMessage(message, LogLevel.DEBUG);
console.debug(message);
}
public info(message: string): void {
this.pushMessage(message, LogLevel.INFO);
console.log(message);
}
public warn(message: string): void {
this.pushMessage(message, LogLevel.WARNING);
console.warn(message);
}
public error(message: string): void {
this.pushMessage(message, LogLevel.ERROR);
console.error(message);
new Notice(message);
}
public getMessages(mininumSeverity: LogLevel): LogLine[] {
return this.messages.filter(
(message) => message.level >= mininumSeverity
);
}
private pushMessage(message: string, level: LogLevel): void {
this.messages.push(new LogLine(level, message));
if (this.messages.length > Logger.MAX_MESSAGES) {
this.messages.shift();
}
}
}

View file

@ -1,16 +1,17 @@
import * as lib from "../../../backend/sync_lib/pkg/sync_lib.js";
import createClient, { Client } from "openapi-fetch";
import type { components, paths } from "./types.js"; // generated by openapi-typescript
import { Logger } from "src/logger";
import { Database } from "src/database/database";
import { SyncSettings } from "src/database/sync-settings";
import {
VaultUpdateId,
RelativePath,
import type { Client } from "openapi-fetch";
import createClient from "openapi-fetch";
import type { components, paths } from "./types.js"; // Generated by openapi-typescript
import type { Database } from "src/database/database";
import type { SyncSettings } from "src/database/sync-settings";
import type {
DocumentId,
RelativePath,
VaultUpdateId,
} from "src/database/document-metadata";
import PQueue from "p-queue";
import { Logger } from "src/tracing/logger.js";
export interface RequestCountStatus {
waiting: number;
@ -21,16 +22,17 @@ export interface RequestCountStatus {
export class SyncService {
private client: Client<paths>;
private promiseQueue: PQueue;
private requestCountListeners: Array<(status: RequestCountStatus) => void> =
[];
private status: RequestCountStatus = {
private readonly promiseQueue: PQueue;
private readonly requestCountListeners: ((
status: RequestCountStatus
) => void)[] = [];
private readonly status: RequestCountStatus = {
waiting: 0,
success: 0,
failure: 0,
};
public constructor(private database: Database) {
public constructor(private readonly database: Database) {
this.createClient(database.getSettings());
this.promiseQueue = new PQueue({
concurrency: database.getSettings().uploadConcurrency,
@ -64,36 +66,21 @@ export class SyncService {
listener({ ...this.status });
}
private emitRequestCountChange(): void {
this.requestCountListeners.forEach((listener) =>
listener({ ...this.status })
);
}
private createClient(settings: SyncSettings) {
this.client = createClient<paths>({
baseUrl: settings.remoteUri,
});
}
private enqueue<T>(fn: () => Promise<T>): Promise<T> {
return this.promiseQueue.add(fn) as Promise<T>;
}
public async ping(): Promise<components["schemas"]["PingResponse"]> {
const response = await this.enqueue(() =>
const response = await this.enqueue(async () =>
this.client.GET("/ping", {
params: {
header: {
authorization:
"Bearer " + this.database.getSettings().token,
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
})
);
Logger.getInstance().debug(
"Ping response: " + JSON.stringify(response.data)
`Ping response: ${JSON.stringify(response.data)}`
);
if (!response.data) {
@ -112,15 +99,16 @@ export class SyncService {
contentBytes: Uint8Array;
createdDate: Date;
}): Promise<components["schemas"]["DocumentVersion"]> {
const response = await this.enqueue(() =>
const response = await this.enqueue(async () =>
this.client.POST("/vaults/{vault_id}/documents", {
params: {
path: {
vault_id: this.database.getSettings().vaultName,
},
header: {
authorization:
"Bearer " + this.database.getSettings().token,
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
body: {
@ -136,7 +124,7 @@ export class SyncService {
}
Logger.getInstance().debug(
"Created document " + JSON.stringify(response.data)
`Created document ${JSON.stringify(response.data)}`
);
return response.data;
@ -155,7 +143,7 @@ export class SyncService {
contentBytes: Uint8Array;
createdDate: Date;
}): Promise<components["schemas"]["DocumentVersion"]> {
const response = await this.enqueue(() =>
const response = await this.enqueue(async () =>
this.client.PUT("/vaults/{vault_id}/documents/{document_id}", {
params: {
path: {
@ -163,8 +151,9 @@ export class SyncService {
document_id: documentId,
},
header: {
authorization:
"Bearer " + this.database.getSettings().token,
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
body: {
@ -181,7 +170,7 @@ export class SyncService {
}
Logger.getInstance().debug(
"Updated document " + JSON.stringify(response.data)
`Updated document ${JSON.stringify(response.data)}`
);
return response.data;
@ -196,7 +185,7 @@ export class SyncService {
relativePath: RelativePath;
createdDate: Date;
}): Promise<void> {
const response = await this.enqueue(() =>
const response = await this.enqueue(async () =>
this.client.DELETE("/vaults/{vault_id}/documents/{document_id}", {
params: {
path: {
@ -204,8 +193,9 @@ export class SyncService {
document_id: documentId,
},
header: {
authorization:
"Bearer " + this.database.getSettings().token,
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
body: {
@ -220,7 +210,7 @@ export class SyncService {
}
Logger.getInstance().debug(
"Updated document " + JSON.stringify(response.data)
`Updated document ${JSON.stringify(response.data)}`
);
return response.data;
@ -231,7 +221,7 @@ export class SyncService {
}: {
documentId: DocumentId;
}): Promise<components["schemas"]["DocumentVersion"]> {
const response = await this.enqueue(() =>
const response = await this.enqueue(async () =>
this.client.GET("/vaults/{vault_id}/documents/{document_id}", {
params: {
path: {
@ -239,8 +229,9 @@ export class SyncService {
document_id: documentId,
},
header: {
authorization:
"Bearer " + this.database.getSettings().token,
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
},
})
@ -251,7 +242,7 @@ export class SyncService {
}
Logger.getInstance().debug(
"Get document " + JSON.stringify(response.data)
`Get document ${JSON.stringify(response.data)}`
);
return response.data;
@ -260,15 +251,16 @@ export class SyncService {
public async getAll(
since?: VaultUpdateId
): Promise<components["schemas"]["FetchLatestDocumentsResponse"]> {
const response = await this.enqueue(() =>
const response = await this.enqueue(async () =>
this.client.GET("/vaults/{vault_id}/documents", {
params: {
path: {
vault_id: this.database.getSettings().vaultName,
},
header: {
authorization:
"Bearer " + this.database.getSettings().token,
authorization: `Bearer ${
this.database.getSettings().token
}`,
},
query: {
since_update_id: since,
@ -277,14 +269,32 @@ export class SyncService {
})
);
if (!response.data) {
throw new Error(`Failed to get documents: ${response.error}`);
const { error } = response;
if (error) {
throw new Error(`Failed to get documents: ${error}`);
}
Logger.getInstance().debug(
"Get document " + JSON.stringify(response.data)
`Get document ${JSON.stringify(response.data)}`
);
return response.data;
}
private emitRequestCountChange(): void {
this.requestCountListeners.forEach((listener) => {
listener({ ...this.status });
});
}
private createClient(settings: SyncSettings): void {
this.client = createClient<paths>({
baseUrl: settings.remoteUri,
});
}
private async enqueue<T>(fn: () => Promise<T>): Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return this.promiseQueue.add(fn) as Promise<T>;
}
}

View file

@ -23,9 +23,7 @@ export interface paths {
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
headers: Record<string, unknown>;
content: {
"application/json": components["schemas"]["PingResponse"];
};
@ -63,9 +61,7 @@ export interface paths {
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
headers: Record<string, unknown>;
content: {
"application/json": components["schemas"]["FetchLatestDocumentsResponse"];
};
@ -91,9 +87,7 @@ export interface paths {
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
headers: Record<string, unknown>;
content: {
"application/json": components["schemas"]["DocumentVersion"];
};
@ -128,9 +122,7 @@ export interface paths {
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
headers: Record<string, unknown>;
content: {
"application/json": components["schemas"]["DocumentVersion"];
};
@ -156,9 +148,7 @@ export interface paths {
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
headers: Record<string, unknown>;
content: {
"application/json": components["schemas"]["DocumentVersion"];
};
@ -186,9 +176,7 @@ export interface paths {
responses: {
/** @description no content */
200: {
headers: {
[name: string]: unknown;
};
headers: Record<string, unknown>;
content?: never;
};
};

View file

@ -1,7 +1,7 @@
import { RelativePath } from "src/database/document-metadata";
import type { RelativePath } from "src/database/document-metadata";
const locked = new Set<RelativePath>();
const waiters = new Map<RelativePath, Array<() => void>>();
const locked = new Set<RelativePath>(),
waiters = new Map<RelativePath, (() => void)[]>();
export function tryLockDocument(relativePath: RelativePath): boolean {
if (locked.has(relativePath)) {
@ -12,17 +12,21 @@ export function tryLockDocument(relativePath: RelativePath): boolean {
return true;
}
export function waitForDocumentLock(relativePath: RelativePath): Promise<void> {
export async function waitForDocumentLock(
relativePath: RelativePath
): Promise<void> {
if (tryLockDocument(relativePath)) {
return Promise.resolve();
}
return new Promise((resolve) => {
if (!waiters.has(relativePath)) {
waiters.set(relativePath, []);
let waiting = waiters.get(relativePath);
if (!waiting) {
waiting = [];
waiters.set(relativePath, waiting);
}
waiters.get(relativePath)!.push(resolve);
waiting.push(resolve);
});
}

View file

@ -1,11 +1,12 @@
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
export function hash(content: Uint8Array): string {
let hash = 0;
let result = 0;
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < content.length; i++) {
hash = (hash << 5) - hash + content[i];
hash |= 0; // convert to 32bit integer
result = (result << 5) - result + content[i];
result |= 0; // Convert to 32bit integer
}
return hash.toString(16);
return Math.abs(result).toString(16);
}
export const EMPTY_HASH = hash(new Uint8Array(0));

View file

@ -1,42 +1,40 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { Logger, LogLevel } from "src/logger";
import type { WorkspaceLeaf } from "obsidian";
import { ItemView } from "obsidian";
import { LogLevel, Logger } from "src/tracing/logger";
export class SyncView extends ItemView {
public static TYPE = "example-view";
public static readonly TYPE = "example-view";
public constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType() {
public getViewType(): string {
return SyncView.TYPE;
}
getDisplayText() {
public getDisplayText(): string {
return "Example view";
}
async onOpen() {
public async onOpen(): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
container.createEl("h4", { text: "Example view" });
setInterval(() => this.updateView(), 1000);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
setInterval(async () => this.updateView(), 1000);
}
async updateView() {
public async updateView(): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
const messages = Logger.getInstance()
.getMessages(LogLevel.INFO)
.getMessages(LogLevel.DEBUG)
.map((message) => message.toString())
.join("\n");
container.createEl("pre", { text: messages });
}
async onClose() {
// Nothing to clean up.
}
}