Skip to content

Commit

Permalink
Implement/metadata-cache-events (#21)
Browse files Browse the repository at this point in the history
* Implement ILogger Interface for Logging System

Refactored the Logging class to implement the newly defined ILogger interface for a more structured and formal logging approach. The getLogger method now returns an instance of ILogger, ensuring consistent logging method signatures and enabling potential future extensions where different loggers conforming to the ILogger interface can be used interchangeably. This enhances maintainability and supports adherence to the dependency inversion principle.

* Add event handling system with registration and firing

Introduced a new events system via the GenericEvents class to facilitate event registration, deregistration, and firing within the application. This system allows defining events with associated types for data and return values, enabling a clear and type-safe handling of app-wide events. Implementations provide registration and deregistration methods, ensuring that events can be dynamically managed at runtime. The fireEvent method executes registered callbacks, and the system incorporates an optional logger for debugging purposes.

* Implement event system for MetadataCache updates

Introduced a new event handling mechanism within the MetadataCache class, allowing for the registration and firing of events when plugin-related file metadata changes. Notably, an event is emitted for the 'prj-task-management-changed-status' scenario, enabling dynamic reaction to task status updates. This feature facilitates the extensibility of the metadata cache and improves its integration capabilities with task-related plugin functionalities. Additionally, the process of metadata change detection has been refined to include event emission when specific conditions are met.

* Add task status file sync on change

Implemented syncing of task files to their respective status directories when status changes are detected. A listener on `prj-task-management-changed-status` now triggers `syncStatusToPath`, moving files to an 'Archiv' directory for completed tasks or to the main status folder for other valid statuses. Ensured that file relocations only occur when the new path differs from the current one, logging each action for debugging purposes. Additionally, refactored `StaticPrjTaskManagementModel` to include missing imports and enhance code maintainability.

* Version bump to V0.0.30
  • Loading branch information
PxaMMaxP authored Jan 16, 2024
1 parent 6143b1e commit c9539ad
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 6 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "prj",
"name": "Prj Plugin",
"version": "0.0.29",
"version": "0.0.30",
"minAppVersion": "0.15.0",
"description": "Prj Plugin - Project, Document, and Task Management",
"author": "M. Passarello",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-sample-plugin",
"version": "0.0.29",
"version": "0.0.30",
"description": "Prj Plugin - Project, Document, and Task Management",
"main": "main.js",
"scripts": {
Expand Down
7 changes: 5 additions & 2 deletions src/classes/Logging.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { ILogger } from "src/interfaces/ILogger";

/**
* Logging class; encapsulates console.log, console.debug, console.warn and console.error
*/
export default class Logging {
export default class Logging implements ILogger {
private static instance: Logging;
private logLevel: LoggingLevel;
private logPrefix: string;
Expand Down Expand Up @@ -39,7 +42,7 @@ export default class Logging {
* Returns an object with logging methods that prepend a specified prefix to messages
* @param prefix The prefix to prepend to all log messages
*/
public static getLogger(prefix: string): { [key in Exclude<LoggingLevel, "none">]: (...args: any[]) => void } {
public static getLogger(prefix: string): ILogger {
const instance = Logging.getInstance();
prefix = `${prefix}: `;
const logMethods: { [key in Exclude<LoggingLevel, "none">]: (...args: any[]) => void } = {
Expand Down
41 changes: 41 additions & 0 deletions src/interfaces/ILogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Interface for the logger.
* @remarks You can attach your own logger or `console` as logger.
*/
export interface ILogger {
/**
* Log a `trace` message.
* @param message The trace message to log.
* @param optionalParams Optional parameters: strings, objects, etc.
*/
trace(message?: unknown, ...optionalParams: unknown[]): void;

/**
* Log an `info` message.
* @param message The info message to log.
* @param optionalParams Optional parameters: strings, objects, etc.
*/
info(message?: unknown, ...optionalParams: unknown[]): void;

/**
* Log a `debug` message.
* @param message The debug message to log.
* @param optionalParams Optional parameters: strings, objects, etc.
*/
debug(message?: unknown, ...optionalParams: unknown[]): void;

/**
* Log a `warn` message.
* @param message The warn message to log.
* @param optionalParams Optional parameters: strings, objects, etc.
*/
warn(message?: unknown, ...optionalParams: unknown[]): void;

/**
* Log an `error` message.
* @param message The error message to log.
* @param optionalParams Optional parameters: strings, objects, etc.
*/
error(message?: unknown, ...optionalParams: unknown[]): void;

}
140 changes: 140 additions & 0 deletions src/libs/Events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ILogger } from "src/interfaces/ILogger";

/**
* Interface for the callback.
*/
export interface ICallback {
events: {
[key: string]: IEvent<unknown, unknown>;
};
}

/**
* Interface for the events.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface IEvent<TData, TReturn = void> {
name: string;
data: TData;
}

/**
* Interface for the events.
*/
type RegisteredEvent<T extends ICallback, K extends keyof T['events']> = {
eventName: K;
callback: (data: T['events'][K]['data']) => T['events'][K] extends IEvent<unknown, infer TReturn> ? TReturn : unknown;
};

/**
* Events class; encapsulates event registration and firing
* @example
* ```typescript
* // Define a concrete interface for your specific events
* interface MyEvents extends ICallback {
* events: {
* 'myEvent1': IEvent<string, number>; // Event with a string as input and a number as output
* 'myEvent2': IEvent<number, void>; // Event with a number as input and no output
* // Add more events here
* };
* }
*
* // Use the Events class with your concrete interface
* const eventsInstance = new GenericEvents<MyEvents>();
*
* // Register event one with a string as input and a number as output
* eventsInstance.registerEvent('myEvent1', (data) => {
* console.log(data);
* return 100;
* });
* // Fire event one with a string as input and a number as output
* eventsInstance.fireEvent('myEvent1', 'Fire event 1', (result) => {
* console.log(result); // Result is a number
* });
* ```
*/
export default class GenericEvents<T extends ICallback> {
private logger: ILogger | undefined;
private _events: Array<RegisteredEvent<T, keyof T['events']>> = [];

/**
* Creates a new Events instance
* @param logger The logger to use. You can use your own logger or `console` as logger.
*/
constructor(logger?: ILogger) {
this.logger = logger;
}

/**
* Registers an event with a callback.
* @param eventName The name of the event to register.
* @param callback The callback to execute when the event is fired.
*/
public registerEvent<K extends keyof T['events']>(
eventName: K,
callback: (data: T['events'][K]['data']) => T['events'][K] extends IEvent<unknown, infer TReturn> ? TReturn : unknown
): void {
// Add the event to the _events array
this._events.push({ eventName, callback });
this.logger?.debug(`Event ${eventName.toString()} registered`);
}

/**
* Deregisters an event with a callback.
* @param eventName The name of the event to deregister.
* @param callback The callback to deregister.
*/
public deregisterEvent<K extends keyof T['events']>(
eventName: K,
callback: (data: T['events'][K]['data']) => T['events'][K] extends IEvent<unknown, infer TReturn> ? TReturn : unknown
): void {
// Delete the event from the _events array
this._events = this._events.filter(event => event.eventName !== eventName || event.callback !== callback);
this.logger?.debug(`Event ${eventName.toString()} deregistered`);
}

/**
* Fires an event with a callback.
* @param eventName The name of the event to fire.
* @param eventData The data to pass to the event handler.
* @param callback The callback to execute when the event is fired.
*/
public fireEvent<K extends keyof T['events']>(
eventName: K,
eventData: T['events'][K]['data'],
callback?: (result: T['events'][K] extends IEvent<unknown, infer TReturn> ? TReturn : unknown) => void
): void {
// Find the event in the _events array and execute the callback
this._events.filter(event => event.eventName === eventName)
.forEach(event => {
this._executeEventHandler(event.callback, eventData, callback);
this.logger?.debug(`Event ${eventName.toString()} fired`);
});
}

/**
* Executes an event handler and calls the callback with the result.
* @param handler The event handler to execute.
* @param eventData The data to pass to the event handler.
* @param callback The callback to execute when the event handler is executed.
*/
private async _executeEventHandler<K extends keyof T['events']>(
handler: (data: T['events'][K]['data']) => T['events'][K] extends IEvent<unknown, infer TReturn> ? TReturn : unknown,
eventData: T['events'][K]['data'],
callback?: (result: T['events'][K] extends IEvent<unknown, infer TReturn> ? TReturn : unknown) => void
): Promise<void> {
try {
// Execute the handler and call the callback with the result
const result = await handler(eventData);
this.logger?.debug(`Event handler for ${handler.toString()} executed`);
if (callback) {
callback(result);
this.logger?.debug(`Callback for ${handler.toString()} executed`);
}
} catch (error) {
this.logger?.error(`Error in event handler for ${handler.toString()}: ${error}`);
}
}

}
66 changes: 66 additions & 0 deletions src/libs/MetadataCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import Logging from "src/classes/Logging";
import Global from "../classes/Global";
import { App, CachedMetadata, TFile } from "obsidian";
import GenericEvents, { ICallback, IEvent } from "./Events";
import PrjTypes from "src/types/PrjTypes";

/**
* FileMetadata interface
Expand All @@ -17,6 +19,7 @@ export class FileMetadata { file: TFile; metadata: CachedMetadata }
* @description This class is used to cache metadata for all files in the vault. It is used to speed up processing of dataview queries.
*/
export default class MetadataCache {
private eventHandler: GenericEvents<MetadataCacheEvents>;
private app: App = Global.getInstance().app;
private logger = Logging.getLogger("MetadataCache");
private metadataCachePromise: Promise<void> | undefined = undefined;
Expand Down Expand Up @@ -69,6 +72,8 @@ export default class MetadataCache {
this.renameEventHandler = this.renameEventHandler.bind(this);
this.deleteEventHandler = this.deleteEventHandler.bind(this);

this.eventHandler = new GenericEvents<MetadataCacheEvents>();

if (!this.metadataCache) {
this.buildMetadataCache().then(() => {
this.logger.debug("Metadata cache built");
Expand Down Expand Up @@ -122,6 +127,57 @@ export default class MetadataCache {
}
}

/**
* Register an event listener for the metadata cache. The event is emitted when the status of a plugin file is changed.
* @param eventName The name of the event: `prj-task-management-changed-status`
* @param listener The listener function. The listener function receives the file object as an argument.
*/
public on(eventName: 'prj-task-management-changed-status', listener: (file: TFile) => void): void;

/**
* Register an event listener for the metadata cache.
* @param eventName The name of the event
* @param listener The listener function. The listener function receives the file object as an argument.
*/
public on<K extends keyof MetadataCacheEvents['events']>(
eventName: K,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (file: MetadataCacheEvents['events'][K]['data']) => MetadataCacheEvents['events'][K] extends IEvent<any, infer TReturn> ? TReturn : void
): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.eventHandler.registerEvent(eventName, listener as any);
}

/**
* Will be called when the metadata of a file is changed. Checks if the file is a plugin file and emits an event if necessary.
* @param newMetadata The changed metadata
* @param oldMetadata The old metadata
* @param file The file object
*/
private async onChangedMetadata(newMetadata: CachedMetadata, oldMetadata: CachedMetadata, file: TFile) {
this.logger.trace(`Metadata changed for file ${file.path} and is processed.`);
// Check if the file is plugin file
if (newMetadata.frontmatter?.type && PrjTypes.isValidFileType(newMetadata.frontmatter.type)) {
switch (newMetadata.frontmatter.type) {
case "Topic":
case "Project":
case "Task":
// Changed status
if (newMetadata.frontmatter?.status !== oldMetadata.frontmatter?.status) {
this.eventHandler.fireEvent('prj-task-management-changed-status', file);
}
break;
case "Metadata":
// Check if the file is a metadata file

break;
default:
this.logger.error(`Invalid file type ${newMetadata.frontmatter?.type} for file ${file.path}`);
break;
}
}
}

/**
* Invalidate the metadata cache
* @remarks Set the metadata cache array to undefined.
Expand Down Expand Up @@ -218,7 +274,9 @@ export default class MetadataCache {
if (this.metadataCache) {
const entry = this.metadataCache.get(file.path);
if (entry && cache) {
const oldMetadata = entry.metadata;
entry.metadata = cache;
this.onChangedMetadata(cache, oldMetadata, file);
this.invalidateMetadataCacheArray();
} else if (!entry) {
this.logger.warn(`No metadata cache entry found for file ${file.path}`);
Expand Down Expand Up @@ -304,4 +362,12 @@ export default class MetadataCache {
}
}

}


interface MetadataCacheEvents extends ICallback {
events: {
'prj-task-management-changed-status': IEvent<TFile, undefined>;
// Add more events here
};
}
6 changes: 6 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ChangeStatusModal from './libs/Modals/ChangeStatusModal';
import CreateNewTaskManagementModal from './libs/Modals/CreateNewTaskManagementModal';
import CreateNewTaskModal from './libs/Modals/CreateNewTaskModal';
import AddAnnotationModal from './libs/Modals/AddAnnotationModal';
import { StaticPrjTaskManagementModel } from './models/StaticHelper/StaticPrjTaskManagementModel';

export default class Prj extends Plugin {
public settings: PrjSettings;
Expand Down Expand Up @@ -55,6 +56,11 @@ export default class Prj extends Plugin {
// Change Status Command
ChangeStatusModal.registerCommand();

//Register event on `Status` change..
Global.getInstance().metadataCache.on('prj-task-management-changed-status', (file) => {
StaticPrjTaskManagementModel.syncStatusToPath(file);
});

}

onunload() {
Expand Down
Loading

0 comments on commit c9539ad

Please sign in to comment.