Stop Logger being a singleton

This commit is contained in:
Andras Schmelczer 2025-02-22 15:11:59 +00:00
commit 3471a9c498
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
12 changed files with 147 additions and 138 deletions

View file

@ -1,47 +1,45 @@
import type { Syncer } from "sync-client"; import type { SyncClient, Syncer } from "sync-client";
import { Logger } from "sync-client"; import { Logger } from "sync-client";
import type { TAbstractFile } from "obsidian"; import type { TAbstractFile } from "obsidian";
import { TFile } from "obsidian"; import { TFile } from "obsidian";
export class ObsidianFileEventHandler { export class ObsidianFileEventHandler {
public constructor(private readonly syncer: Syncer) {} public constructor(private readonly client: SyncClient) {}
public async onCreate(file: TAbstractFile): Promise<void> { public async onCreate(file: TAbstractFile): Promise<void> {
if (file instanceof TFile) { if (file instanceof TFile) {
Logger.getInstance().info(`File created: ${file.path}`); this.client.logger.info(`File created: ${file.path}`);
await this.syncer.syncLocallyCreatedFile( await this.client.syncer.syncLocallyCreatedFile(
file.path, file.path,
new Date(file.stat.ctime) new Date(file.stat.ctime)
); );
} else { } else {
Logger.getInstance().debug(`Folder created: ${file.path}, ignored`); this.client.logger.debug(`Folder created: ${file.path}, ignored`);
} }
} }
public async onDelete(file: TAbstractFile): Promise<void> { public async onDelete(file: TAbstractFile): Promise<void> {
if (file instanceof TFile) { if (file instanceof TFile) {
Logger.getInstance().info(`File deleted: ${file.path}`); this.client.logger.info(`File deleted: ${file.path}`);
await this.syncer.syncLocallyDeletedFile(file.path); await this.client.syncer.syncLocallyDeletedFile(file.path);
} else { } else {
Logger.getInstance().debug(`Folder deleted: ${file.path}, ignored`); this.client.logger.debug(`Folder deleted: ${file.path}, ignored`);
} }
} }
public async onRename(file: TAbstractFile, oldPath: string): Promise<void> { public async onRename(file: TAbstractFile, oldPath: string): Promise<void> {
if (file instanceof TFile) { if (file instanceof TFile) {
Logger.getInstance().info( this.client.logger.info(`File renamed: ${oldPath} -> ${file.path}`);
`File renamed: ${oldPath} -> ${file.path}`
);
await this.syncer.syncLocallyUpdatedFile({ await this.client.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( this.client.logger.debug(
`Folder renamed: ${oldPath} -> ${file.path}, ignored` `Folder renamed: ${oldPath} -> ${file.path}, ignored`
); );
} }
@ -53,16 +51,14 @@ export class ObsidianFileEventHandler {
return; return;
} }
Logger.getInstance().info(`File modified: ${file.path}`); this.client.logger.info(`File modified: ${file.path}`);
await this.syncer.syncLocallyUpdatedFile({ await this.client.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( this.client.logger.debug(`Folder modified: ${file.path}, ignored`);
`Folder modified: ${file.path}, ignored`
);
} }
} }
} }

View file

@ -10,7 +10,7 @@ import { StatusBar } from "./views/status-bar";
import { LogsView } from "./views/logs-view"; import { LogsView } from "./views/logs-view";
import { StatusDescription } from "./views/status-description"; import { StatusDescription } from "./views/status-description";
import { Logger, SyncClient } from "sync-client"; import { SyncClient } from "sync-client";
import { ObsidianFileSystemOperations } from "./obsidian-file-system"; import { ObsidianFileSystemOperations } from "./obsidian-file-system";
export default class VaultLinkPlugin extends Plugin { export default class VaultLinkPlugin extends Plugin {
@ -18,8 +18,6 @@ export default class VaultLinkPlugin extends Plugin {
private client!: SyncClient; private client!: SyncClient;
public async onload(): Promise<void> { public async onload(): Promise<void> {
Logger.getInstance().info("Starting plugin");
this.client = await SyncClient.create( this.client = await SyncClient.create(
new ObsidianFileSystemOperations(this.app.vault), new ObsidianFileSystemOperations(this.app.vault),
{ {
@ -28,6 +26,8 @@ export default class VaultLinkPlugin extends Plugin {
} }
); );
this.client.logger.info("Starting plugin");
const statusDescription = new StatusDescription(this.client); const statusDescription = new StatusDescription(this.client);
this.settingsTab = new SyncSettingsTab({ this.settingsTab = new SyncSettingsTab({
@ -42,12 +42,11 @@ export default class VaultLinkPlugin extends Plugin {
this.registerView( this.registerView(
HistoryView.TYPE, HistoryView.TYPE,
(leaf) => (leaf) => new HistoryView(leaf, this.client)
new HistoryView(leaf, this.client.settings, this.client.history)
); );
this.registerView( this.registerView(
LogsView.TYPE, LogsView.TYPE,
(leaf) => new LogsView(this, this.client.settings, leaf) (leaf) => new LogsView(this, this.client, leaf)
); );
this.addRibbonIcon( this.addRibbonIcon(
@ -61,11 +60,10 @@ export default class VaultLinkPlugin extends Plugin {
async (_: MouseEvent) => this.activateView(LogsView.TYPE) async (_: MouseEvent) => this.activateView(LogsView.TYPE)
); );
const eventHandler = new ObsidianFileEventHandler(this.client.syncer); const eventHandler = new ObsidianFileEventHandler(this.client);
this.app.workspace.onLayoutReady(async () => { this.app.workspace.onLayoutReady(async () => {
Logger.getInstance().info("Initialising sync handlers"); this.client.logger.info("Initialising sync handlers");
[ [
this.app.vault.on( this.app.vault.on(
"create", "create",
@ -87,7 +85,7 @@ export default class VaultLinkPlugin extends Plugin {
this.registerEvent(event); this.registerEvent(event);
}); });
Logger.getInstance().info("Sync handlers initialised"); this.client.logger.info("Sync handlers initialised");
void this.client.syncer.scheduleSyncForOfflineChanges(); void this.client.syncer.scheduleSyncForOfflineChanges();
}); });

View file

@ -2,7 +2,7 @@ import type { IconName, WorkspaceLeaf } from "obsidian";
import { ItemView, setIcon } from "obsidian"; import { ItemView, setIcon } from "obsidian";
import { intlFormatDistance } from "date-fns"; import { intlFormatDistance } from "date-fns";
import type { SyncHistory, HistoryEntry, Settings } from "sync-client"; import type { HistoryEntry, SyncClient } from "sync-client";
import { SyncType, SyncSource, SyncStatus, Logger } from "sync-client"; import { SyncType, SyncSource, SyncStatus, Logger } from "sync-client";
export class HistoryView extends ItemView { export class HistoryView extends ItemView {
@ -12,15 +12,14 @@ export class HistoryView extends ItemView {
public constructor( public constructor(
leaf: WorkspaceLeaf, leaf: WorkspaceLeaf,
private readonly settings: Settings, private readonly client: SyncClient
private readonly history: SyncHistory
) { ) {
super(leaf); super(leaf);
this.icon = HistoryView.ICON; this.icon = HistoryView.ICON;
history.addSyncHistoryUpdateListener(() => { this.client.history.addSyncHistoryUpdateListener(() => {
this.updateView().catch((_error: unknown) => { this.updateView().catch((_error: unknown) => {
Logger.getInstance().error("Failed to update history view"); this.client.logger.error("Failed to update history view");
}); });
}); });
} }
@ -94,13 +93,13 @@ export class HistoryView extends ItemView {
container.empty(); container.empty();
container.createEl("h4", { text: "VaultLink History" }); container.createEl("h4", { text: "VaultLink History" });
const entries = this.history const entries = this.client.history
.getEntries() .getEntries()
.reverse() .reverse()
.filter( .filter(
(entry) => (entry) =>
entry.status !== SyncStatus.NO_OP || entry.status !== SyncStatus.NO_OP ||
this.settings.getSettings().displayNoopSyncEvents this.client.settings.getSettings().displayNoopSyncEvents
); );
entries.forEach((entry) => { entries.forEach((entry) => {

View file

@ -1,8 +1,7 @@
import type { WorkspaceLeaf } from "obsidian"; import type { WorkspaceLeaf } from "obsidian";
import { ItemView } from "obsidian"; import { ItemView } from "obsidian";
import type VaultLinkPlugin from "src/vault-link-plugin"; import type VaultLinkPlugin from "src/vault-link-plugin";
import type { Settings } from "sync-client"; import type { SyncClient } from "sync-client";
import { Logger } from "sync-client";
export class LogsView extends ItemView { export class LogsView extends ItemView {
public static readonly TYPE = "logs-view"; public static readonly TYPE = "logs-view";
@ -10,20 +9,24 @@ export class LogsView extends ItemView {
public constructor( public constructor(
private readonly plugin: VaultLinkPlugin, private readonly plugin: VaultLinkPlugin,
private readonly settings: Settings, private readonly client: SyncClient,
leaf: WorkspaceLeaf leaf: WorkspaceLeaf
) { ) {
super(leaf); super(leaf);
this.icon = LogsView.ICON; this.icon = LogsView.ICON;
Logger.getInstance().addOnMessageListener(() => { this.client.logger.addOnMessageListener(() => {
this.updateView(); this.updateView();
}); });
settings.addOnSettingsChangeHandlers((newSettings, oldSettings) => { this.client.settings.addOnSettingsChangeHandlers(
if (newSettings.minimumLogLevel !== oldSettings.minimumLogLevel) { (newSettings, oldSettings) => {
this.updateView(); if (
newSettings.minimumLogLevel !== oldSettings.minimumLogLevel
) {
this.updateView();
}
} }
}); );
} }
private static formatTimestamp(timestamp: Date): string { private static formatTimestamp(timestamp: Date): string {
@ -78,8 +81,8 @@ export class LogsView extends ItemView {
} }
); );
const logs = Logger.getInstance().getMessages( const logs = this.client.logger.getMessages(
this.settings.getSettings().minimumLogLevel this.client.settings.getSettings().minimumLogLevel
); );
if (logs.length === 0) { if (logs.length === 0) {

View file

@ -4,16 +4,19 @@ import { RelativePath } from "src/persistence/database";
import { isBinary, isFileTypeMergable, mergeText } from "sync_lib"; import { isBinary, isFileTypeMergable, mergeText } from "sync_lib";
export class FileOperations { export class FileOperations {
public constructor(private readonly fs: FileSystemOperations) {} public constructor(
private readonly logger: Logger,
private readonly fs: FileSystemOperations
) {}
public async listAllFiles(): Promise<RelativePath[]> { public async listAllFiles(): Promise<RelativePath[]> {
const files = await this.fs.listAllFiles(); const files = await this.fs.listAllFiles();
Logger.getInstance().debug(`Listing all files, found ${files.length}`); this.logger.debug(`Listing all files, found ${files.length}`);
return files; return files;
} }
public async read(path: RelativePath): Promise<Uint8Array> { public async read(path: RelativePath): Promise<Uint8Array> {
Logger.getInstance().debug(`Reading file: ${path}`); this.logger.debug(`Reading file: ${path}`);
const content = await this.fs.read(path); const content = await this.fs.read(path);
if (isBinary(content)) { if (isBinary(content)) {
@ -30,17 +33,17 @@ export class FileOperations {
} }
public async getFileSize(path: RelativePath): Promise<number> { public async getFileSize(path: RelativePath): Promise<number> {
Logger.getInstance().debug(`Getting file size: ${path}`); this.logger.debug(`Getting file size: ${path}`);
return this.fs.getFileSize(path); return this.fs.getFileSize(path);
} }
public async getModificationTime(path: RelativePath): Promise<Date> { public async getModificationTime(path: RelativePath): Promise<Date> {
Logger.getInstance().debug(`Getting modification time: ${path}`); this.logger.debug(`Getting modification time: ${path}`);
return this.fs.getModificationTime(path); return this.fs.getModificationTime(path);
} }
public async exists(path: RelativePath): Promise<boolean> { public async exists(path: RelativePath): Promise<boolean> {
Logger.getInstance().debug(`Checking existance of ${path}`); this.logger.debug(`Checking existance of ${path}`);
return this.fs.exists(path); return this.fs.exists(path);
} }
@ -50,9 +53,9 @@ export class FileOperations {
path: RelativePath, path: RelativePath,
newContent: Uint8Array newContent: Uint8Array
): Promise<void> { ): Promise<void> {
Logger.getInstance().debug(`Creating file: ${path}`); this.logger.debug(`Creating file: ${path}`);
if (await this.fs.exists(path)) { if (await this.fs.exists(path)) {
Logger.getInstance().debug( this.logger.debug(
`Didn't expect ${path} to exist, when trying to create it, merging instead` `Didn't expect ${path} to exist, when trying to create it, merging instead`
); );
await this.write(path, new Uint8Array(0), newContent); await this.write(path, new Uint8Array(0), newContent);
@ -71,9 +74,9 @@ export class FileOperations {
expectedContent: Uint8Array, expectedContent: Uint8Array,
newContent: Uint8Array newContent: Uint8Array
): Promise<Uint8Array> { ): Promise<Uint8Array> {
Logger.getInstance().debug(`Writing file: ${path}`); this.logger.debug(`Writing file: ${path}`);
if (!(await this.fs.exists(path))) { if (!(await this.fs.exists(path))) {
Logger.getInstance().debug( this.logger.debug(
`The caller assumed ${path} exists, but it no longer, so we wont recreate it` `The caller assumed ${path} exists, but it no longer, so we wont recreate it`
); );
return new Uint8Array(0); return new Uint8Array(0);
@ -84,7 +87,7 @@ export class FileOperations {
isBinary(expectedContent) || isBinary(expectedContent) ||
isBinary(newContent) isBinary(newContent)
) { ) {
Logger.getInstance().debug( this.logger.debug(
`The expected content is not mergable, so we won't perform a 3-way merge, just overwrite it` `The expected content is not mergable, so we won't perform a 3-way merge, just overwrite it`
); );
await this.fs.write(path, newContent); await this.fs.write(path, newContent);
@ -99,14 +102,14 @@ export class FileOperations {
(currentText) => { (currentText) => {
currentText = currentText.replace(/\r\n/g, "\n"); currentText = currentText.replace(/\r\n/g, "\n");
if (currentText !== expectedText) { if (currentText !== expectedText) {
Logger.getInstance().debug( this.logger.debug(
`Performing a 3-way merge for ${path} with the expected content` `Performing a 3-way merge for ${path} with the expected content`
); );
return mergeText(expectedText, currentText, newText); return mergeText(expectedText, currentText, newText);
} }
Logger.getInstance().debug( this.logger.debug(
`The current content of ${path} is the same as the expected content, so we will just write the new content` `The current content of ${path} is the same as the expected content, so we will just write the new content`
); );
@ -117,7 +120,7 @@ export class FileOperations {
} }
public async remove(path: RelativePath): Promise<void> { public async remove(path: RelativePath): Promise<void> {
Logger.getInstance().debug(`Removing file: ${path}`); this.logger.debug(`Removing file: ${path}`);
return this.fs.delete(path); return this.fs.delete(path);
} }
@ -125,7 +128,7 @@ export class FileOperations {
oldPath: RelativePath, oldPath: RelativePath,
newPath: RelativePath newPath: RelativePath
): Promise<void> { ): Promise<void> {
Logger.getInstance().debug(`Moving file: ${oldPath} -> ${newPath}`); this.logger.debug(`Moving file: ${oldPath} -> ${newPath}`);
if (oldPath === newPath) { if (oldPath === newPath) {
return; return;

View file

@ -20,6 +20,7 @@ export class Database {
private lastSeenUpdateId: VaultUpdateId | undefined; private lastSeenUpdateId: VaultUpdateId | undefined;
public constructor( public constructor(
private readonly logger: Logger,
initialState: Partial<StoredDatabase> | undefined, initialState: Partial<StoredDatabase> | undefined,
private readonly saveData: (data: unknown) => Promise<void> private readonly saveData: (data: unknown) => Promise<void>
) { ) {
@ -32,10 +33,10 @@ export class Database {
this.documents.set(relativePath, metadata as DocumentMetadata); this.documents.set(relativePath, metadata as DocumentMetadata);
} }
} }
Logger.getInstance().debug(`Loaded ${this.documents.size} documents`); this.logger.debug(`Loaded ${this.documents.size} documents`);
this.lastSeenUpdateId = initialState.lastSeenUpdateId; this.lastSeenUpdateId = initialState.lastSeenUpdateId;
Logger.getInstance().debug( this.logger.debug(
`Loaded last seen update id: ${this.lastSeenUpdateId}` `Loaded last seen update id: ${this.lastSeenUpdateId}`
); );
} }

View file

@ -33,6 +33,7 @@ export class Settings {
) => void)[] = []; ) => void)[] = [];
public constructor( public constructor(
private readonly logger: Logger,
initialState: Partial<SyncSettings> | undefined, initialState: Partial<SyncSettings> | undefined,
private readonly saveData: (data: unknown) => Promise<void> private readonly saveData: (data: unknown) => Promise<void>
) { ) {
@ -41,7 +42,7 @@ export class Settings {
...(initialState ?? {}) ...(initialState ?? {})
}; };
Logger.getInstance().debug( this.logger.debug(
`Loaded settings: ${JSON.stringify(this.settings, null, 2)}` `Loaded settings: ${JSON.stringify(this.settings, null, 2)}`
); );
} }
@ -70,7 +71,7 @@ export class Settings {
value: SyncSettings[T] value: SyncSettings[T]
): Promise<void> { ): Promise<void> {
const newSettings = { ...this.settings, [key]: value }; const newSettings = { ...this.settings, [key]: value };
Logger.getInstance().debug( this.logger.debug(
`Setting ${key} to ${value}, new settings: ${JSON.stringify( `Setting ${key} to ${value}, new settings: ${JSON.stringify(
newSettings, newSettings,
null, null,

View file

@ -7,7 +7,7 @@ import type {
VaultUpdateId VaultUpdateId
} from "../persistence/database"; } from "../persistence/database";
import { Logger } from "src/tracing/logger"; import { Logger } from "src/tracing/logger";
import { retriedFetch } from "src/utils/retried-fetch"; import { retriedFetchFactory } from "src/utils/retried-fetch";
import type { SyncSettings } from "dist/types"; import type { SyncSettings } from "dist/types";
import type { Settings } from "src/persistence/settings"; import type { Settings } from "src/persistence/settings";
@ -19,7 +19,10 @@ export class SyncService {
private client!: Client<paths>; private client!: Client<paths>;
private clientWithoutRetries!: Client<paths>; private clientWithoutRetries!: Client<paths>;
public constructor(private readonly settings: Settings) { public constructor(
private readonly settings: Settings,
private readonly logger: Logger
) {
this.createClient(settings.getSettings()); this.createClient(settings.getSettings());
settings.addOnSettingsChangeHandlers(this.createClient.bind(this)); settings.addOnSettingsChangeHandlers(this.createClient.bind(this));
@ -73,7 +76,7 @@ export class SyncService {
); );
} }
Logger.getInstance().debug( this.logger.debug(
`Created document ${JSON.stringify(response.data)} with id ${ `Created document ${JSON.stringify(response.data)} with id ${
response.data.documentId response.data.documentId
}` }`
@ -124,7 +127,7 @@ export class SyncService {
); );
} }
Logger.getInstance().debug( this.logger.debug(
`Updated document ${JSON.stringify(response.data)} with id ${ `Updated document ${JSON.stringify(response.data)} with id ${
response.data.documentId response.data.documentId
}` }`
@ -165,7 +168,7 @@ export class SyncService {
throw new Error(`Failed to delete document`); throw new Error(`Failed to delete document`);
} }
Logger.getInstance().debug( this.logger.debug(
`Deleted document ${relativePath} with id ${documentId}` `Deleted document ${relativePath} with id ${documentId}`
); );
@ -198,7 +201,7 @@ export class SyncService {
); );
} }
Logger.getInstance().debug( this.logger.debug(
`Get document ${response.data.relativePath} with id ${response.data.documentId}` `Get document ${response.data.relativePath} with id ${response.data.documentId}`
); );
@ -229,7 +232,7 @@ export class SyncService {
); );
} }
Logger.getInstance().debug( this.logger.debug(
`Got ${response.data.latestDocuments.length} document metadata` `Got ${response.data.latestDocuments.length} document metadata`
); );
@ -267,9 +270,7 @@ export class SyncService {
} }
}); });
Logger.getInstance().debug( this.logger.debug(`Ping response: ${JSON.stringify(response.data)}`);
`Ping response: ${JSON.stringify(response.data)}`
);
if (!response.data) { if (!response.data) {
throw new Error( throw new Error(
@ -283,7 +284,7 @@ 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: retriedFetchFactory(this.logger)
}); });
this.clientWithoutRetries = createClient<paths>({ this.clientWithoutRetries = createClient<paths>({

View file

@ -12,14 +12,15 @@ import { FileSystemOperations } from "./file-operations/filesystem-operations";
import { FileOperations } from "./file-operations/file-operations"; import { FileOperations } from "./file-operations/file-operations";
export class SyncClient { export class SyncClient {
private remoteListenerIntervalId: number | null = null; private remoteListenerIntervalId: NodeJS.Timeout | null = null;
private constructor( private constructor(
private readonly _history: SyncHistory, private readonly _history: SyncHistory,
private readonly _settings: Settings, private readonly _settings: Settings,
private readonly _database: Database, private readonly _database: Database,
private readonly _syncer: Syncer, private readonly _syncer: Syncer,
private readonly _syncService: SyncService private readonly _syncService: SyncService,
private readonly _logger: Logger
) {} ) {}
public get history(): SyncHistory { public get history(): SyncHistory {
@ -34,12 +35,17 @@ export class SyncClient {
return this._syncer; return this._syncer;
} }
public get logger(): Logger {
return this._logger;
}
public static async create( public static async create(
fs: FileSystemOperations, fs: FileSystemOperations,
persistence: PersistenceProvider persistence: PersistenceProvider
): Promise<SyncClient> { ): Promise<SyncClient> {
const history = new SyncHistory(); const logger = new Logger();
Logger.getInstance().info("Starting SyncClient"); const history = new SyncHistory(logger);
logger.info("Starting SyncClient");
await init( await init(
// eslint-disable-next-line // eslint-disable-next-line
@ -54,6 +60,7 @@ export class SyncClient {
database: undefined database: undefined
}; };
const database = new Database( const database = new Database(
logger,
state.database, state.database,
async (data: unknown): Promise<void> => { async (data: unknown): Promise<void> => {
state = { ...state, database: data }; state = { ...state, database: data };
@ -62,6 +69,7 @@ export class SyncClient {
); );
const settings = new Settings( const settings = new Settings(
logger,
state.settings, state.settings,
async (data: unknown): Promise<void> => { async (data: unknown): Promise<void> => {
state = { ...state, settings: data }; state = { ...state, settings: data };
@ -69,13 +77,14 @@ export class SyncClient {
} }
); );
const syncService = new SyncService(settings); const syncService = new SyncService(settings, logger);
const syncer = new Syncer( const syncer = new Syncer(
logger,
database, database,
settings, settings,
syncService, syncService,
new FileOperations(fs), new FileOperations(logger, fs),
history history
); );
@ -84,7 +93,8 @@ export class SyncClient {
settings, settings,
database, database,
syncer, syncer,
syncService syncService,
logger
); );
void syncer.scheduleSyncForOfflineChanges(); void syncer.scheduleSyncForOfflineChanges();
@ -102,14 +112,14 @@ export class SyncClient {
syncer syncer
.scheduleSyncForOfflineChanges() .scheduleSyncForOfflineChanges()
.catch((_error: unknown) => { .catch((_error: unknown) => {
Logger.getInstance().error( logger.error(
"Failed to schedule sync for offline changes" "Failed to schedule sync for offline changes"
); );
}); });
} }
}); });
Logger.getInstance().info("SyncClient loaded"); logger.info("SyncClient loaded");
return client; return client;
} }
@ -125,21 +135,21 @@ export class SyncClient {
public async reset(): Promise<void> { public async reset(): Promise<void> {
await this._syncer.reset(); await this._syncer.reset();
this._history.reset(); this._history.reset();
Logger.getInstance().reset(); this.logger.reset();
} }
public onunload(): void { public onunload(): void {
if (this.remoteListenerIntervalId !== null) { if (this.remoteListenerIntervalId !== null) {
window.clearInterval(this.remoteListenerIntervalId); clearInterval(this.remoteListenerIntervalId);
} }
} }
private registerRemoteEventListener(intervalMs: number): void { private registerRemoteEventListener(intervalMs: number): void {
if (this.remoteListenerIntervalId !== null) { if (this.remoteListenerIntervalId !== null) {
window.clearInterval(this.remoteListenerIntervalId); clearInterval(this.remoteListenerIntervalId);
} }
this.remoteListenerIntervalId = window.setInterval( this.remoteListenerIntervalId = setInterval(
() => void this._syncer.applyRemoteChangesLocally(), () => void this._syncer.applyRemoteChangesLocally(),
intervalMs intervalMs
); );

View file

@ -29,6 +29,7 @@ export class Syncer {
undefined; undefined;
public constructor( public constructor(
private readonly logger: Logger,
private readonly database: Database, private readonly database: Database,
private readonly settings: Settings, private readonly settings: Settings,
private readonly syncService: SyncService, private readonly syncService: SyncService,
@ -91,16 +92,14 @@ export class Syncer {
public async scheduleSyncForOfflineChanges(): Promise<void> { public async scheduleSyncForOfflineChanges(): Promise<void> {
if (!this.settings.getSettings().isSyncEnabled) { if (!this.settings.getSettings().isSyncEnabled) {
Logger.getInstance().debug( this.logger.debug(
`Syncing is disabled, not uploading local changes` `Syncing is disabled, not uploading local changes`
); );
return; return;
} }
if (this.runningScheduleSyncForOfflineChanges != null) { if (this.runningScheduleSyncForOfflineChanges != null) {
Logger.getInstance().debug( this.logger.debug("Uploading local changes is already in progress");
"Uploading local changes is already in progress"
);
return this.runningScheduleSyncForOfflineChanges; return this.runningScheduleSyncForOfflineChanges;
} }
@ -108,11 +107,9 @@ export class Syncer {
this.runningScheduleSyncForOfflineChanges = this.runningScheduleSyncForOfflineChanges =
this.internalScheduleSyncForOfflineChanges(); this.internalScheduleSyncForOfflineChanges();
await this.runningScheduleSyncForOfflineChanges; await this.runningScheduleSyncForOfflineChanges;
Logger.getInstance().info( this.logger.info(`All local changes have been applied remotely`);
`All local changes have been applied remotely`
);
} catch (e) { } catch (e) {
Logger.getInstance().error( this.logger.error(
`Not all local changes have been applied remotely: ${e}` `Not all local changes have been applied remotely: ${e}`
); );
throw e; throw e;
@ -150,7 +147,7 @@ export class Syncer {
(item) => item != originalFile (item) => item != originalFile
); );
Logger.getInstance().debug( this.logger.debug(
`Document ${relativePath} was not found under its current path in the database but was found under a different path ${originalFile[0]}, scheduling sync to move it` `Document ${relativePath} was not found under its current path in the database but was found under a different path ${originalFile[0]}, scheduling sync to move it`
); );
return this.internalSyncLocallyUpdatedFile({ return this.internalSyncLocallyUpdatedFile({
@ -167,7 +164,7 @@ export class Syncer {
}); });
} }
Logger.getInstance().debug( this.logger.debug(
`Document ${relativePath} not found in database, scheduling sync to create it` `Document ${relativePath} not found in database, scheduling sync to create it`
); );
return this.internalSyncLocallyCreatedFile( return this.internalSyncLocallyCreatedFile(
@ -178,7 +175,7 @@ export class Syncer {
); );
} }
Logger.getInstance().debug( this.logger.debug(
`Document ${relativePath} has been updated locally, scheduling sync to update it` `Document ${relativePath} has been updated locally, scheduling sync to update it`
); );
return this.internalSyncLocallyUpdatedFile({ return this.internalSyncLocallyUpdatedFile({
@ -194,12 +191,12 @@ export class Syncer {
await Promise.all( await Promise.all(
locallyDeletedFiles.map(async ([relativePath, _]) => { locallyDeletedFiles.map(async ([relativePath, _]) => {
Logger.getInstance().debug( this.logger.debug(
`Document ${relativePath} has been deleted locally, scheduling sync to delete it` `Document ${relativePath} has been deleted locally, scheduling sync to delete it`
); );
if (await this.operations.exists(relativePath)) { if (await this.operations.exists(relativePath)) {
Logger.getInstance().debug( this.logger.debug(
`Document ${relativePath} actually exists locally, skipping` `Document ${relativePath} actually exists locally, skipping`
); );
return Promise.resolve(); return Promise.resolve();
@ -212,14 +209,14 @@ export class Syncer {
public async applyRemoteChangesLocally(): Promise<void> { public async applyRemoteChangesLocally(): Promise<void> {
if (!this.settings.getSettings().isSyncEnabled) { if (!this.settings.getSettings().isSyncEnabled) {
Logger.getInstance().debug( this.logger.debug(
`Syncing is disabled, not fetching remote changes` `Syncing is disabled, not fetching remote changes`
); );
return; return;
} }
if (this.runningApplyRemoteChangesLocally != null) { if (this.runningApplyRemoteChangesLocally != null) {
Logger.getInstance().debug( this.logger.debug(
"Applying remote changes locally is already in progress" "Applying remote changes locally is already in progress"
); );
return this.runningApplyRemoteChangesLocally; return this.runningApplyRemoteChangesLocally;
@ -229,13 +226,9 @@ export class Syncer {
this.runningApplyRemoteChangesLocally = this.runningApplyRemoteChangesLocally =
this.internalApplyRemoteChangesLocally(); this.internalApplyRemoteChangesLocally();
await this.runningApplyRemoteChangesLocally; await this.runningApplyRemoteChangesLocally;
Logger.getInstance().info( this.logger.info("All remote changes have been applied locally");
"All remote changes have been applied locally"
);
} catch (e) { } catch (e) {
Logger.getInstance().error( this.logger.error(`Failed to apply remote changes locally: ${e}`);
`Failed to apply remote changes locally: ${e}`
);
throw e; throw e;
} finally { } finally {
this.runningApplyRemoteChangesLocally = undefined; this.runningApplyRemoteChangesLocally = undefined;
@ -248,11 +241,11 @@ export class Syncer {
); );
if (remote.latestDocuments.length === 0) { if (remote.latestDocuments.length === 0) {
Logger.getInstance().debug("No remote changes to apply"); this.logger.debug("No remote changes to apply");
return; return;
} }
Logger.getInstance().info("Applying remote changes locally"); this.logger.info("Applying remote changes locally");
await Promise.all( await Promise.all(
remote.latestDocuments.map(async (remoteDocument) => remote.latestDocuments.map(async (remoteDocument) =>
@ -317,7 +310,7 @@ export class Syncer {
const localMetadata = this.database.getDocument(relativePath); const localMetadata = this.database.getDocument(relativePath);
if (localMetadata) { if (localMetadata) {
Logger.getInstance().debug( this.logger.debug(
`Document metadata already exists for ${relativePath}, it must have been downloaded from the server` `Document metadata already exists for ${relativePath}, it must have been downloaded from the server`
); );
@ -631,7 +624,7 @@ export class Syncer {
const [relativePath, metadata] = localMetadata; const [relativePath, metadata] = localMetadata;
if (metadata.parentVersionId === remoteVersion.vaultUpdateId) { if (metadata.parentVersionId === remoteVersion.vaultUpdateId) {
Logger.getInstance().debug( this.logger.debug(
`Document ${relativePath} is already up to date` `Document ${relativePath} is already up to date`
); );
return; return;
@ -658,7 +651,7 @@ export class Syncer {
const currentHash = hash(currentContent); const currentHash = hash(currentContent);
if (currentHash !== metadata.hash) { if (currentHash !== metadata.hash) {
Logger.getInstance().info( this.logger.info(
`Document ${relativePath} has been updated both remotely and locally, letting the local file update event handle it` `Document ${relativePath} has been updated both remotely and locally, letting the local file update event handle it`
); );
return; return;
@ -716,18 +709,18 @@ export class Syncer {
fn: () => Promise<void> fn: () => Promise<void>
): Promise<void> { ): Promise<void> {
if (!this.settings.getSettings().isSyncEnabled) { if (!this.settings.getSettings().isSyncEnabled) {
Logger.getInstance().info( this.logger.info(
`Syncing is disabled, not syncing ${relativePath}` `Syncing is disabled, not syncing ${relativePath}`
); );
return; return;
} }
if (!this.operations.isFileEligibleForSync(relativePath)) { if (!this.operations.isFileEligibleForSync(relativePath)) {
Logger.getInstance().info( this.logger.info(
`File ${relativePath} is not eligible for syncing` `File ${relativePath} is not eligible for syncing`
); );
return; return;
} }
Logger.getInstance().debug(`Syncing ${relativePath}`); this.logger.debug(`Syncing ${relativePath}`);
await waitForDocumentLock(relativePath); await waitForDocumentLock(relativePath);
try { try {

View file

@ -47,6 +47,8 @@ export class SyncHistory {
error: 0 error: 0
}; };
public constructor(private logger: Logger) {}
public getEntries(): HistoryEntry[] { public getEntries(): HistoryEntry[] {
return [...this.entries]; return [...this.entries];
} }
@ -78,16 +80,16 @@ export class SyncHistory {
if (entry.status === SyncStatus.SUCCESS) { if (entry.status === SyncStatus.SUCCESS) {
this.status.success++; this.status.success++;
Logger.getInstance().info( this.logger.info(
`History entry: ${entry.relativePath} - ${entry.message}` `History entry: ${entry.relativePath} - ${entry.message}`
); );
} else if (entry.status === SyncStatus.ERROR) { } else if (entry.status === SyncStatus.ERROR) {
this.status.error++; this.status.error++;
Logger.getInstance().error( this.logger.error(
`Error syncing file: ${entry.relativePath} - ${entry.message}` `Error syncing file: ${entry.relativePath} - ${entry.message}`
); );
} else { } else {
Logger.getInstance().debug( this.logger.debug(
`No-op syncing file: ${entry.relativePath} - ${entry.message}` `No-op syncing file: ${entry.relativePath} - ${entry.message}`
); );
} }

View file

@ -14,23 +14,25 @@ function getUrlFromInput(input: RequestInfo | URL): string {
return input.url; return input.url;
} }
export async function retriedFetch( export function retriedFetchFactory(logger: Logger) {
input: RequestInfo | URL, return (
init: RequestInitRetryParams<typeof fetch> = {} input: RequestInfo | URL,
): Promise<Response> { init: RequestInitRetryParams<typeof fetch> = {}
return fetchWithRetry(input, { ): Promise<Response> => {
retryOn: function (attempt, error, response) { return fetchWithRetry(input, {
if (error !== null || !response || response.status >= 500) { retryOn: function (attempt, error, response) {
Logger.getInstance().warn( if (error !== null || !response || response.status >= 500) {
`Retrying fetch for ${getUrlFromInput(input)}, attempt ${attempt}` logger.warn(
); `Retrying fetch for ${getUrlFromInput(input)}, attempt ${attempt}`
);
return true; return true;
} }
return false; return false;
}, },
retries: 6, retries: 6,
retryDelay: (attempt) => Math.pow(1.5, attempt) * 500, retryDelay: (attempt) => Math.pow(1.5, attempt) * 500,
...init ...init
}); });
};
} }