Add event handler class

This commit is contained in:
Andras Schmelczer 2025-12-07 13:30:45 +00:00
commit ad3191957a
14 changed files with 2583 additions and 2464 deletions

View file

@ -1,87 +1,72 @@
import { MAX_LOG_MESSAGE_COUNT } from "../consts";
import { removeFromArray } from "../utils/remove-from-array";
import { EventListeners } from "../utils/data-structures/event-listeners";
export enum LogLevel {
DEBUG = "DEBUG",
INFO = "INFO",
WARNING = "WARNING",
ERROR = "ERROR"
DEBUG = "DEBUG",
INFO = "INFO",
WARNING = "WARNING",
ERROR = "ERROR"
}
const LOG_LEVEL_ORDER = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
[LogLevel.WARNING]: 2,
[LogLevel.ERROR]: 3
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
[LogLevel.WARNING]: 2,
[LogLevel.ERROR]: 3
};
export class LogLine {
public timestamp = new Date();
public constructor(
public level: LogLevel,
public message: string
) {}
public timestamp = new Date();
public constructor(
public level: LogLevel,
public message: string
) { }
}
export class Logger {
private readonly messages: LogLine[] = [];
private readonly onMessageListeners: ((message: LogLine) => unknown)[] = [];
private readonly messages: LogLine[] = [];
public readonly onLogEmitted = new EventListeners<
(message: LogLine) => unknown
>();
public constructor(
...onMessageListeners: ((message: LogLine) => unknown)[]
) {
this.onMessageListeners = onMessageListeners;
}
public debug(message: string): void {
this.pushMessage(message, LogLevel.DEBUG);
}
public debug(message: string): void {
this.pushMessage(message, LogLevel.DEBUG);
}
public info(message: string): void {
this.pushMessage(message, LogLevel.INFO);
}
public info(message: string): void {
this.pushMessage(message, LogLevel.INFO);
}
public warn(message: string): void {
this.pushMessage(message, LogLevel.WARNING);
}
public warn(message: string): void {
this.pushMessage(message, LogLevel.WARNING);
}
public error(message: string): void {
this.pushMessage(message, LogLevel.ERROR);
}
public error(message: string): void {
this.pushMessage(message, LogLevel.ERROR);
}
public getMessages(mininumSeverity: LogLevel): LogLine[] {
return this.messages.filter(
(message) =>
LOG_LEVEL_ORDER[message.level] >=
LOG_LEVEL_ORDER[mininumSeverity]
);
}
public getMessages(mininumSeverity: LogLevel): LogLine[] {
return this.messages.filter(
(message) =>
LOG_LEVEL_ORDER[message.level] >=
LOG_LEVEL_ORDER[mininumSeverity]
);
}
public addOnMessageListener(listener: (message: LogLine) => unknown): void {
this.onMessageListeners.push(listener);
}
public reset(): void {
this.messages.length = 0;
this.debug("Logger has been reset");
}
public removeOnMessageListener(
listener: (message: LogLine) => unknown
): void {
removeFromArray(this.onMessageListeners, listener);
}
private pushMessage(message: string, level: LogLevel): void {
const logLine = new LogLine(level, message);
this.messages.push(logLine);
public reset(): void {
this.messages.length = 0;
this.debug("Logger has been reset");
}
while (this.messages.length > MAX_LOG_MESSAGE_COUNT) {
this.messages.shift();
}
private pushMessage(message: string, level: LogLevel): void {
const logLine = new LogLine(level, message);
this.messages.push(logLine);
while (this.messages.length > MAX_LOG_MESSAGE_COUNT) {
this.messages.shift();
}
this.onMessageListeners.forEach((listener) => {
listener(logLine);
});
}
this.onLogEmitted.trigger(logLine);
}
}

View file

@ -1,183 +1,169 @@
import {
MAX_HISTORY_ENTRY_COUNT,
TIMEOUT_FOR_MERGING_HISTORY_ENTRIES_IN_SECONDS
MAX_HISTORY_ENTRY_COUNT,
TIMEOUT_FOR_MERGING_HISTORY_ENTRIES_IN_SECONDS
} from "../consts";
import type { RelativePath } from "../persistence/database";
import type { Logger } from "./logger";
import { removeFromArray } from "../utils/remove-from-array";
import { EventListeners } from "../utils/data-structures/event-listeners";
export interface SyncCreateDetails {
type: SyncType.CREATE;
relativePath: RelativePath;
type: SyncType.CREATE;
relativePath: RelativePath;
}
export interface SyncUpdateDetails {
type: SyncType.UPDATE;
relativePath: RelativePath;
type: SyncType.UPDATE;
relativePath: RelativePath;
}
export interface SyncMovedDetails {
type: SyncType.MOVE;
relativePath: RelativePath;
movedFrom: RelativePath;
type: SyncType.MOVE;
relativePath: RelativePath;
movedFrom: RelativePath;
}
export interface SyncDeleteDetails {
type: SyncType.DELETE;
relativePath: RelativePath;
type: SyncType.DELETE;
relativePath: RelativePath;
}
export interface SyncSkippedDetails {
type: SyncType.SKIPPED;
relativePath: RelativePath;
type: SyncType.SKIPPED;
relativePath: RelativePath;
}
export type SyncDetails =
| SyncCreateDetails
| SyncUpdateDetails
| SyncDeleteDetails
| SyncMovedDetails
| SyncSkippedDetails;
| SyncCreateDetails
| SyncUpdateDetails
| SyncDeleteDetails
| SyncMovedDetails
| SyncSkippedDetails;
export interface CommonHistoryEntry {
status: SyncStatus;
message: string;
details: SyncDetails;
author?: string;
timestamp?: Date;
status: SyncStatus;
message: string;
details: SyncDetails;
author?: string;
timestamp?: Date;
}
export enum SyncType {
CREATE = "CREATE",
UPDATE = "UPDATE",
DELETE = "DELETE",
MOVE = "MOVE",
SKIPPED = "SKIPPED"
CREATE = "CREATE",
UPDATE = "UPDATE",
DELETE = "DELETE",
MOVE = "MOVE",
SKIPPED = "SKIPPED"
}
export enum SyncStatus {
SUCCESS = "SUCCESS",
ERROR = "ERROR",
SKIPPED = "SKIPPED"
SUCCESS = "SUCCESS",
ERROR = "ERROR",
SKIPPED = "SKIPPED"
}
export type HistoryEntry = CommonHistoryEntry & { timestamp: Date };
export interface HistoryStats {
success: number;
error: number;
success: number;
error: number;
}
export class SyncHistory {
private readonly _entries: HistoryEntry[] = [];
private readonly _entries: HistoryEntry[] = [];
private readonly syncHistoryUpdateListeners: ((
status: HistoryStats
) => unknown)[] = [];
public readonly onHistoryUpdated = new EventListeners<
(status: HistoryStats) => unknown
>();
private status: HistoryStats = {
success: 0,
error: 0
};
private status: HistoryStats = {
success: 0,
error: 0
};
public constructor(private readonly logger: Logger) {}
public constructor(private readonly logger: Logger) { }
public get entries(): readonly HistoryEntry[] {
return this._entries;
}
public get entries(): readonly HistoryEntry[] {
return this._entries;
}
/**
* Insert the entry at the beginning of the history list. If the entry
* already in the list, it will get moved to the beginning and updated.
*
* If the entry list is too long, the oldest entry will be removed.
*/
public addHistoryEntry(entry: CommonHistoryEntry): void {
const historyEntry = {
...entry,
timestamp: entry.timestamp ?? new Date()
};
/**
* Insert the entry at the beginning of the history list. If the entry
* already in the list, it will get moved to the beginning and updated.
*
* If the entry list is too long, the oldest entry will be removed.
*/
public addHistoryEntry(entry: CommonHistoryEntry): void {
const historyEntry = {
...entry,
timestamp: entry.timestamp ?? new Date()
};
const candidate = this.findSimilarRecentUpdateEntry(historyEntry);
if (candidate !== undefined) {
removeFromArray(this._entries, candidate);
}
const candidate = this.findSimilarRecentUpdateEntry(historyEntry);
if (candidate !== undefined) {
removeFromArray(this._entries, candidate);
}
// Insert the entry at the beginning
this._entries.unshift(historyEntry);
// Insert the entry at the beginning
this._entries.unshift(historyEntry);
if (this._entries.length > MAX_HISTORY_ENTRY_COUNT) {
this._entries.pop();
}
if (this._entries.length > MAX_HISTORY_ENTRY_COUNT) {
this._entries.pop();
}
this.updateSuccessCount(historyEntry);
}
this.updateSuccessCount(historyEntry);
}
public addSyncHistoryUpdateListener(
listener: (stats: HistoryStats) => unknown
): void {
this.syncHistoryUpdateListeners.push(listener);
listener({ ...this.status });
}
public removeSyncHistoryUpdateListener(
listener: (stats: HistoryStats) => unknown
): void {
removeFromArray(this.syncHistoryUpdateListeners, listener);
}
public reset(): void {
this._entries.length = 0;
this.status = {
success: 0,
error: 0
};
this.syncHistoryUpdateListeners.forEach((listener) => {
listener(this.status);
});
}
public reset(): void {
this._entries.length = 0;
this.status = {
success: 0,
error: 0
};
this.onHistoryUpdated.trigger(this.status);
}
private findSimilarRecentUpdateEntry(
entry: HistoryEntry
): HistoryEntry | undefined {
if (entry.details.type !== SyncType.UPDATE) {
return;
}
private findSimilarRecentUpdateEntry(
entry: HistoryEntry
): HistoryEntry | undefined {
if (entry.details.type !== SyncType.UPDATE) {
return;
}
const candidate = this._entries.find(
(e) =>
e.details.type === SyncType.UPDATE &&
e.details.relativePath === entry.details.relativePath
);
if (
candidate !== undefined &&
(this._entries[0] === candidate ||
candidate.timestamp.getTime() +
TIMEOUT_FOR_MERGING_HISTORY_ENTRIES_IN_SECONDS * 1000 >
entry.timestamp.getTime())
) {
return candidate;
}
}
const candidate = this._entries.find(
(e) =>
e.details.type === SyncType.UPDATE &&
e.details.relativePath === entry.details.relativePath
);
if (
candidate !== undefined &&
(this._entries[0] === candidate ||
candidate.timestamp.getTime() +
TIMEOUT_FOR_MERGING_HISTORY_ENTRIES_IN_SECONDS * 1000 >
entry.timestamp.getTime())
) {
return candidate;
}
}
private updateSuccessCount(entry: HistoryEntry): void {
const message = `${entry.details.relativePath} - ${entry.message} (${entry.details.type.toLocaleLowerCase()})`;
switch (entry.status) {
case SyncStatus.SUCCESS:
this.status.success++;
this.logger.info(`History entry: ${message}`);
break;
case SyncStatus.ERROR:
this.status.error++;
this.logger.error(`Cannot sync file: ${message}`);
break;
case SyncStatus.SKIPPED:
this.logger.warn(`Skipping file: ${message}`);
break;
}
private updateSuccessCount(entry: HistoryEntry): void {
const message = `${entry.details.relativePath} - ${entry.message} (${entry.details.type.toLocaleLowerCase()})`;
switch (entry.status) {
case SyncStatus.SUCCESS:
this.status.success++;
this.logger.info(`History entry: ${message}`);
break;
case SyncStatus.ERROR:
this.status.error++;
this.logger.error(`Cannot sync file: ${message}`);
break;
case SyncStatus.SKIPPED:
this.logger.warn(`Skipping file: ${message}`);
break;
}
this.syncHistoryUpdateListeners.forEach((listener) => {
listener(this.status);
});
}
this.onHistoryUpdated.trigger(this.status);
}
}